Skip to main content

turso_sql/
value.rs

1//! Bound parameter values, modeled by [`Value`].
2//!
3//! SQLite stores exactly five storage classes, so the value model is just
4//! those five: there is no boolean, date or decimal variant to invent a
5//! representation for. Richer Rust types are flattened at the edge through
6//! `From` implementations — booleans become `0` / `1`, dates and times
7//! become ISO 8601 text, UUIDs hyphenated text, JSON its textual form and
8//! decimals their exact decimal text — so the writer only ever binds what
9//! the engine natively understands. The reverse direction, decoding a column
10//! into a Rust type, lives in `turso-orm-driver` where the requested type is
11//! known.
12//!
13//! Values are always bound as parameters. [`Value::to_literal`] exists for
14//! logs and tests and is never used to execute anything, which keeps SQL
15//! injection out of the picture by construction.
16
17use std::fmt;
18
19/// A bound parameter or literal.
20///
21/// SQLite has exactly these storage classes. Richer Rust types are flattened
22/// through [`From`] implementations: booleans become `0` / `1`, dates and
23/// times become ISO 8601 text, UUIDs become hyphenated text, JSON becomes its
24/// textual form and decimals become their exact decimal text.
25#[derive(Clone, Debug, PartialEq)]
26pub enum Value {
27    /// SQL `NULL`.
28    Null,
29    /// A 64-bit signed integer.
30    Integer(i64),
31    /// A 64-bit float.
32    Real(f64),
33    /// UTF-8 text.
34    Text(String),
35    /// Raw bytes.
36    Blob(Vec<u8>),
37}
38
39impl Value {
40    /// Whether this is `NULL`.
41    pub fn is_null(&self) -> bool {
42        matches!(self, Value::Null)
43    }
44
45    /// Renders the value as a SQL literal.
46    ///
47    /// This is for debugging output only; execution always binds parameters,
48    /// so the escaping here never has to be injection-proof against the
49    /// engine, only readable.
50    pub fn to_literal(&self) -> String {
51        match self {
52            Value::Null => "NULL".to_owned(),
53            Value::Integer(n) => n.to_string(),
54            Value::Real(f) => {
55                // An integral float is printed with one decimal so that it
56                // stays recognisable as a REAL rather than an INTEGER.
57                if f.fract() == 0.0 && f.is_finite() {
58                    format!("{f:.1}")
59                } else {
60                    f.to_string()
61                }
62            }
63            Value::Text(s) => format!("'{}'", s.replace('\'', "''")),
64            Value::Blob(b) => {
65                use std::fmt::Write as _;
66                let mut out = String::with_capacity(b.len() * 2 + 3);
67                out.push_str("X'");
68                for byte in b {
69                    let _ = write!(out, "{byte:02X}");
70                }
71                out.push('\'');
72                out
73            }
74        }
75    }
76}
77
78impl fmt::Display for Value {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        f.write_str(&self.to_literal())
81    }
82}
83
84/// Implements `From<$t> for Value` for integer types that widen losslessly
85/// into `i64`.
86macro_rules! int_from {
87    ($($t:ty),*) => {$(
88        impl From<$t> for Value {
89            fn from(v: $t) -> Self {
90                Value::Integer(i64::from(v))
91            }
92        }
93    )*};
94}
95int_from!(i8, i16, i32, i64, u8, u16, u32);
96
97impl From<bool> for Value {
98    fn from(v: bool) -> Self {
99        Value::Integer(i64::from(v))
100    }
101}
102
103impl From<f32> for Value {
104    fn from(v: f32) -> Self {
105        Value::Real(f64::from(v))
106    }
107}
108
109impl From<f64> for Value {
110    fn from(v: f64) -> Self {
111        Value::Real(v)
112    }
113}
114
115impl From<&str> for Value {
116    fn from(v: &str) -> Self {
117        Value::Text(v.to_owned())
118    }
119}
120
121impl From<String> for Value {
122    fn from(v: String) -> Self {
123        Value::Text(v)
124    }
125}
126
127impl From<&String> for Value {
128    fn from(v: &String) -> Self {
129        Value::Text(v.clone())
130    }
131}
132
133impl From<char> for Value {
134    fn from(v: char) -> Self {
135        Value::Text(v.to_string())
136    }
137}
138
139impl From<Vec<u8>> for Value {
140    fn from(v: Vec<u8>) -> Self {
141        Value::Blob(v)
142    }
143}
144
145impl From<&[u8]> for Value {
146    fn from(v: &[u8]) -> Self {
147        Value::Blob(v.to_vec())
148    }
149}
150
151impl<T: Into<Value>> From<Option<T>> for Value {
152    fn from(v: Option<T>) -> Self {
153        v.map_or(Value::Null, Into::into)
154    }
155}
156
157impl<T: Into<Value> + Clone> From<&T> for Value
158where
159    T: ValueRefInto,
160{
161    fn from(v: &T) -> Self {
162        v.clone().into()
163    }
164}
165
166/// Marker that lets `&T` convert into a [`Value`] for owned-value types.
167///
168/// A blanket `From<&T>` would conflict with the dedicated `&str`, `&[u8]`
169/// and `&String` implementations, so the types that convert by cloning opt in
170/// through this marker instead.
171pub trait ValueRefInto {}
172impl ValueRefInto for bool {}
173impl ValueRefInto for i8 {}
174impl ValueRefInto for i16 {}
175impl ValueRefInto for i32 {}
176impl ValueRefInto for i64 {}
177impl ValueRefInto for u8 {}
178impl ValueRefInto for u16 {}
179impl ValueRefInto for u32 {}
180impl ValueRefInto for f32 {}
181impl ValueRefInto for f64 {}
182impl ValueRefInto for Vec<u8> {}
183impl<T: ValueRefInto> ValueRefInto for Option<T> {}
184
185/// Conversions for the `chrono` date and time types.
186///
187/// Naive values are written in the `YYYY-MM-DD HH:MM:SS.fff` family that
188/// SQLite's date functions understand; zoned values are written as RFC 3339
189/// so the offset survives the round trip.
190#[cfg(feature = "with-chrono")]
191#[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))]
192mod chrono_impls {
193    use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
194
195    use super::Value;
196
197    /// The text format used for naive timestamps.
198    ///
199    /// The fractional seconds are emitted only when non-zero, which keeps
200    /// whole-second timestamps identical to what SQLite's `datetime()`
201    /// produces.
202    pub const NAIVE_DATETIME_FORMAT: &str = "%Y-%m-%d %H:%M:%S%.f";
203
204    impl From<NaiveDate> for Value {
205        fn from(v: NaiveDate) -> Self {
206            Value::Text(v.format("%Y-%m-%d").to_string())
207        }
208    }
209    impl From<NaiveTime> for Value {
210        fn from(v: NaiveTime) -> Self {
211            Value::Text(v.format("%H:%M:%S%.f").to_string())
212        }
213    }
214    impl From<NaiveDateTime> for Value {
215        fn from(v: NaiveDateTime) -> Self {
216            Value::Text(v.format(NAIVE_DATETIME_FORMAT).to_string())
217        }
218    }
219    impl<Tz: TimeZone> From<DateTime<Tz>> for Value {
220        fn from(v: DateTime<Tz>) -> Self {
221            Value::Text(v.to_rfc3339())
222        }
223    }
224    impl super::ValueRefInto for NaiveDate {}
225    impl super::ValueRefInto for NaiveTime {}
226    impl super::ValueRefInto for NaiveDateTime {}
227    impl super::ValueRefInto for DateTime<Utc> {}
228    impl super::ValueRefInto for DateTime<FixedOffset> {}
229}
230#[cfg(feature = "with-chrono")]
231#[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))]
232pub use chrono_impls::NAIVE_DATETIME_FORMAT;
233
234#[cfg(feature = "with-uuid")]
235#[cfg_attr(docsrs, doc(cfg(feature = "with-uuid")))]
236impl From<uuid::Uuid> for Value {
237    fn from(v: uuid::Uuid) -> Self {
238        Value::Text(v.hyphenated().to_string())
239    }
240}
241#[cfg(feature = "with-uuid")]
242impl ValueRefInto for uuid::Uuid {}
243
244#[cfg(feature = "with-json")]
245#[cfg_attr(docsrs, doc(cfg(feature = "with-json")))]
246impl From<serde_json::Value> for Value {
247    fn from(v: serde_json::Value) -> Self {
248        Value::Text(v.to_string())
249    }
250}
251#[cfg(feature = "with-json")]
252impl ValueRefInto for serde_json::Value {}
253
254#[cfg(feature = "with-rust_decimal")]
255#[cfg_attr(docsrs, doc(cfg(feature = "with-rust_decimal")))]
256impl From<rust_decimal::Decimal> for Value {
257    fn from(v: rust_decimal::Decimal) -> Self {
258        Value::Text(v.to_string())
259    }
260}
261#[cfg(feature = "with-rust_decimal")]
262impl ValueRefInto for rust_decimal::Decimal {}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    /// Literal rendering flattens booleans, escapes quotes, hex-encodes
269    /// blobs, maps `None` to `NULL` and keeps integral floats recognisable.
270    #[test]
271    fn literals() {
272        assert_eq!(Value::from(true).to_literal(), "1");
273        assert_eq!(Value::from("it's").to_literal(), "'it''s'");
274        assert_eq!(Value::from(vec![0xAB, 0x01]).to_literal(), "X'AB01'");
275        assert_eq!(Value::from(Option::<i32>::None), Value::Null);
276        assert_eq!(Value::from(2.0f64).to_literal(), "2.0");
277    }
278}