Skip to main content

turso_orm_driver/
decode.rs

1//! Decoding of storage values into Rust types, modeled by [`FromValue`].
2//!
3//! SQLite columns have affinity, not a type: an `INTEGER` column may hold
4//! text, a `TEXT` column may hold a number, and a boolean is whatever the
5//! application wrote. Decoding is therefore driven by the Rust type the
6//! caller asks for rather than by the column's declared type, and it is
7//! lenient in the way SQLite users expect — integers decode into booleans,
8//! integral reals into integers, numbers into strings, text into dates,
9//! UUIDs and JSON. The alternative, failing on any storage-class mismatch,
10//! would make rows written by other tools unreadable.
11//!
12//! `Option<T>` is the only type that accepts `NULL`; every other type
13//! reports a decoding error so that a nullable column read as a plain type
14//! fails loudly instead of defaulting. Every error names the column and the
15//! requested type.
16//!
17//! Values arrive as `turso_sql::Value`, the five storage classes, whichever
18//! engine produced them; the executor converts from the engine's own value
19//! type before a row is built.
20//!
21//! This module owns the conversions only. Rows and column lookup live in
22//! `crate::executor`; the inverse direction, Rust values into parameters,
23//! lives in `turso-sql`.
24
25use turso_sql::Value;
26
27use crate::error::{Error, Result};
28
29/// Types that can be read out of a result column.
30///
31/// Decoding is lenient in the way SQLite users expect: integers decode into
32/// booleans (`0` / `1`), reals accept integers, text parses into dates, UUIDs
33/// and JSON, and blobs or text decode into UUIDs. `Option<T>` maps `NULL` to
34/// `None`; any other type rejects `NULL`.
35pub trait FromValue: Sized {
36    /// The name used in error messages.
37    const TYPE_NAME: &'static str;
38
39    /// Decodes a value; `column` is only used for error messages.
40    ///
41    /// # Errors
42    ///
43    /// Returns [`Error::Decode`] when the value's storage class or content
44    /// cannot be converted to `Self`.
45    fn from_value(value: Value, column: &str) -> Result<Self>;
46}
47
48/// Builds the decoding error for a storage class `T` does not accept.
49fn mismatch<T: FromValue>(column: &str, value: &Value) -> Error {
50    Error::decode(column, T::TYPE_NAME, format!("unexpected {value:?}"))
51}
52
53impl<T: FromValue> FromValue for Option<T> {
54    const TYPE_NAME: &'static str = T::TYPE_NAME;
55
56    fn from_value(value: Value, column: &str) -> Result<Self> {
57        match value {
58            Value::Null => Ok(None),
59            other => T::from_value(other, column).map(Some),
60        }
61    }
62}
63
64impl FromValue for Value {
65    const TYPE_NAME: &'static str = "Value";
66
67    fn from_value(value: Value, _column: &str) -> Result<Self> {
68        Ok(value)
69    }
70}
71
72/// Implements [`FromValue`] for integer types: integers are range-checked,
73/// integral reals are accepted, and text is parsed.
74macro_rules! int_from_value {
75    ($($t:ty),*) => {$(
76        impl FromValue for $t {
77            const TYPE_NAME: &'static str = stringify!($t);
78
79            fn from_value(value: Value, column: &str) -> Result<Self> {
80                match value {
81                    Value::Integer(n) => <$t>::try_from(n)
82                        .map_err(|_| Error::decode(column, Self::TYPE_NAME, format!("{n} out of range"))),
83                    Value::Real(f) if f.fract() == 0.0 => {
84                        // The guard makes the cast exact for any real that
85                        // fits; one beyond the i64 range saturates and is
86                        // then rejected by the range check below.
87                        #[allow(
88                            clippy::cast_possible_truncation,
89                            reason = "the fract() guard makes the cast exact within range"
90                        )]
91                        let n = f as i64;
92                        <$t>::try_from(n)
93                            .map_err(|_| Error::decode(column, Self::TYPE_NAME, format!("{f} out of range")))
94                    }
95                    Value::Text(s) => s.trim().parse::<$t>()
96                        .map_err(|e| Error::decode(column, Self::TYPE_NAME, e)),
97                    other => Err(mismatch::<$t>(column, &other)),
98                }
99            }
100        }
101    )*};
102}
103int_from_value!(i8, i16, i32, i64, u8, u16, u32, u64, isize, usize);
104
105impl FromValue for bool {
106    const TYPE_NAME: &'static str = "bool";
107
108    /// Decodes any non-zero number as `true`, and the usual textual
109    /// spellings case-insensitively; empty text is `false`.
110    fn from_value(value: Value, column: &str) -> Result<Self> {
111        match value {
112            Value::Integer(n) => Ok(n != 0),
113            Value::Real(f) => Ok(f != 0.0),
114            Value::Text(s) => match s.to_ascii_lowercase().as_str() {
115                "1" | "true" | "t" | "yes" | "y" => Ok(true),
116                "0" | "false" | "f" | "no" | "n" | "" => Ok(false),
117                _ => Err(Error::decode(
118                    column,
119                    "bool",
120                    format!("unexpected text {s:?}"),
121                )),
122            },
123            other => Err(mismatch::<bool>(column, &other)),
124        }
125    }
126}
127
128impl FromValue for f64 {
129    const TYPE_NAME: &'static str = "f64";
130
131    fn from_value(value: Value, column: &str) -> Result<Self> {
132        match value {
133            Value::Real(f) => Ok(f),
134            #[allow(
135                clippy::cast_precision_loss,
136                reason = "an integer column read as a float is expected to round above 2^53"
137            )]
138            Value::Integer(n) => Ok(n as f64),
139            Value::Text(s) => s
140                .trim()
141                .parse()
142                .map_err(|e| Error::decode(column, "f64", e)),
143            other => Err(mismatch::<f64>(column, &other)),
144        }
145    }
146}
147
148impl FromValue for f32 {
149    const TYPE_NAME: &'static str = "f32";
150
151    fn from_value(value: Value, column: &str) -> Result<Self> {
152        #[allow(
153            clippy::cast_possible_truncation,
154            reason = "narrowing to f32 is what the caller asked for"
155        )]
156        f64::from_value(value, column).map(|f| f as f32)
157    }
158}
159
160impl FromValue for String {
161    const TYPE_NAME: &'static str = "String";
162
163    /// Decodes text as is, numbers through their `Display` form and blobs
164    /// as UTF-8.
165    fn from_value(value: Value, column: &str) -> Result<Self> {
166        match value {
167            Value::Text(s) => Ok(s),
168            Value::Integer(n) => Ok(n.to_string()),
169            Value::Real(f) => Ok(f.to_string()),
170            Value::Blob(b) => String::from_utf8(b).map_err(|e| Error::decode(column, "String", e)),
171            other @ Value::Null => Err(mismatch::<String>(column, &other)),
172        }
173    }
174}
175
176impl FromValue for Vec<u8> {
177    const TYPE_NAME: &'static str = "Vec<u8>";
178
179    fn from_value(value: Value, column: &str) -> Result<Self> {
180        match value {
181            Value::Blob(b) => Ok(b),
182            Value::Text(s) => Ok(s.into_bytes()),
183            other => Err(mismatch::<Vec<u8>>(column, &other)),
184        }
185    }
186}
187
188/// Conversions for the `chrono` date and time types.
189///
190/// Each type accepts the formats `turso-sql` writes plus the common
191/// variants found in databases written by other tools: `T` separators,
192/// missing fractions or seconds, RFC 3339 and, for timestamps, Unix
193/// seconds stored as integers.
194#[cfg(feature = "with-chrono")]
195#[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))]
196mod chrono_impls {
197    use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc};
198
199    use super::{Error, FromValue, Result, Value, mismatch};
200
201    /// Extracts the text of a value, rejecting any other storage class on
202    /// behalf of `T`.
203    ///
204    /// # Errors
205    ///
206    /// Returns [`Error::Decode`] when the value is not text.
207    fn text<T: FromValue>(value: Value, column: &str) -> Result<String> {
208        match value {
209            Value::Text(s) => Ok(s),
210            other => Err(mismatch::<T>(column, &other)),
211        }
212    }
213
214    impl FromValue for NaiveDate {
215        const TYPE_NAME: &'static str = "NaiveDate";
216
217        /// Parses `YYYY-MM-DD`, falling back to the date part of any
218        /// timestamp format [`NaiveDateTime`] accepts.
219        fn from_value(value: Value, column: &str) -> Result<Self> {
220            let s = text::<Self>(value, column)?;
221            NaiveDate::parse_from_str(s.trim(), "%Y-%m-%d")
222                .or_else(|_| {
223                    NaiveDateTime::from_value(Value::Text(s.clone()), column).map(|dt| dt.date())
224                })
225                .map_err(|e| Error::decode(column, Self::TYPE_NAME, e))
226        }
227    }
228
229    impl FromValue for NaiveTime {
230        const TYPE_NAME: &'static str = "NaiveTime";
231
232        fn from_value(value: Value, column: &str) -> Result<Self> {
233            let s = text::<Self>(value, column)?;
234            ["%H:%M:%S%.f", "%H:%M:%S", "%H:%M"]
235                .iter()
236                .find_map(|f| NaiveTime::parse_from_str(s.trim(), f).ok())
237                .ok_or_else(|| Error::decode(column, Self::TYPE_NAME, format!("unparsable {s:?}")))
238        }
239    }
240
241    impl FromValue for NaiveDateTime {
242        const TYPE_NAME: &'static str = "NaiveDateTime";
243
244        /// Accepts Unix seconds as an integer, the space- and `T`-separated
245        /// forms with or without fractions and seconds, RFC 3339 reduced to
246        /// UTC, and a bare date at midnight.
247        fn from_value(value: Value, column: &str) -> Result<Self> {
248            if let Value::Integer(n) = value {
249                return DateTime::from_timestamp(n, 0)
250                    .map(|dt| dt.naive_utc())
251                    .ok_or_else(|| {
252                        Error::decode(column, Self::TYPE_NAME, "timestamp out of range")
253                    });
254            }
255            let s = text::<Self>(value, column)?;
256            let t = s.trim();
257            [
258                "%Y-%m-%d %H:%M:%S%.f",
259                "%Y-%m-%d %H:%M:%S",
260                "%Y-%m-%dT%H:%M:%S%.f",
261                "%Y-%m-%dT%H:%M:%S",
262                "%Y-%m-%d %H:%M",
263            ]
264            .iter()
265            .find_map(|f| NaiveDateTime::parse_from_str(t, f).ok())
266            .or_else(|| {
267                DateTime::parse_from_rfc3339(t)
268                    .ok()
269                    .map(|dt| dt.naive_utc())
270            })
271            .or_else(|| {
272                NaiveDate::parse_from_str(t, "%Y-%m-%d")
273                    .ok()
274                    .and_then(|d| d.and_hms_opt(0, 0, 0))
275            })
276            .ok_or_else(|| Error::decode(column, Self::TYPE_NAME, format!("unparsable {s:?}")))
277        }
278    }
279
280    impl FromValue for DateTime<FixedOffset> {
281        const TYPE_NAME: &'static str = "DateTime<FixedOffset>";
282
283        /// Accepts Unix seconds as an integer, RFC 3339, the space-separated
284        /// forms with an offset, and any naive form, which is taken as UTC.
285        fn from_value(value: Value, column: &str) -> Result<Self> {
286            if let Value::Integer(n) = value {
287                return DateTime::from_timestamp(n, 0)
288                    .map(|dt| dt.fixed_offset())
289                    .ok_or_else(|| {
290                        Error::decode(column, Self::TYPE_NAME, "timestamp out of range")
291                    });
292            }
293            let s = text::<Self>(value, column)?;
294            let t = s.trim();
295            DateTime::parse_from_rfc3339(t)
296                .ok()
297                .or_else(|| {
298                    [
299                        "%Y-%m-%d %H:%M:%S%.f%:z",
300                        "%Y-%m-%d %H:%M:%S%:z",
301                        "%Y-%m-%d %H:%M:%S%.f%#z",
302                    ]
303                    .iter()
304                    .find_map(|f| DateTime::parse_from_str(t, f).ok())
305                })
306                .or_else(|| {
307                    NaiveDateTime::from_value(Value::Text(t.to_owned()), column)
308                        .ok()
309                        .map(|n| n.and_utc().fixed_offset())
310                })
311                .ok_or_else(|| Error::decode(column, Self::TYPE_NAME, format!("unparsable {s:?}")))
312        }
313    }
314
315    impl FromValue for DateTime<Utc> {
316        const TYPE_NAME: &'static str = "DateTime<Utc>";
317
318        fn from_value(value: Value, column: &str) -> Result<Self> {
319            DateTime::<FixedOffset>::from_value(value, column).map(|dt| dt.with_timezone(&Utc))
320        }
321    }
322}
323
324#[cfg(feature = "with-uuid")]
325#[cfg_attr(docsrs, doc(cfg(feature = "with-uuid")))]
326impl FromValue for uuid::Uuid {
327    const TYPE_NAME: &'static str = "Uuid";
328
329    /// Accepts the hyphenated or simple text forms and 16-byte blobs.
330    fn from_value(value: Value, column: &str) -> Result<Self> {
331        match value {
332            Value::Text(s) => {
333                uuid::Uuid::parse_str(s.trim()).map_err(|e| Error::decode(column, "Uuid", e))
334            }
335            Value::Blob(b) => {
336                uuid::Uuid::from_slice(&b).map_err(|e| Error::decode(column, "Uuid", e))
337            }
338            other => Err(mismatch::<uuid::Uuid>(column, &other)),
339        }
340    }
341}
342
343#[cfg(feature = "with-json")]
344#[cfg_attr(docsrs, doc(cfg(feature = "with-json")))]
345impl FromValue for serde_json::Value {
346    const TYPE_NAME: &'static str = "serde_json::Value";
347
348    /// Parses text and blobs as JSON documents and maps scalars, including
349    /// `NULL`, to the corresponding JSON value.
350    fn from_value(value: Value, column: &str) -> Result<Self> {
351        match value {
352            Value::Text(s) => {
353                serde_json::from_str(&s).map_err(|e| Error::decode(column, Self::TYPE_NAME, e))
354            }
355            Value::Blob(b) => {
356                serde_json::from_slice(&b).map_err(|e| Error::decode(column, Self::TYPE_NAME, e))
357            }
358            Value::Integer(n) => Ok(serde_json::Value::from(n)),
359            Value::Real(f) => Ok(serde_json::Value::from(f)),
360            Value::Null => Ok(serde_json::Value::Null),
361        }
362    }
363}
364
365#[cfg(feature = "with-rust_decimal")]
366#[cfg_attr(docsrs, doc(cfg(feature = "with-rust_decimal")))]
367impl FromValue for rust_decimal::Decimal {
368    const TYPE_NAME: &'static str = "Decimal";
369
370    /// Parses text exactly and converts integers and reals, the latter
371    /// with the precision loss inherent to binary floats.
372    fn from_value(value: Value, column: &str) -> Result<Self> {
373        match value {
374            Value::Text(s) => s
375                .trim()
376                .parse()
377                .map_err(|e| Error::decode(column, "Decimal", e)),
378            Value::Integer(n) => Ok(rust_decimal::Decimal::from(n)),
379            Value::Real(f) => {
380                rust_decimal::Decimal::try_from(f).map_err(|e| Error::decode(column, "Decimal", e))
381            }
382            other => Err(mismatch::<rust_decimal::Decimal>(column, &other)),
383        }
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390
391    /// Scalars decode leniently across storage classes, out-of-range
392    /// integers are rejected, and only `Option<T>` accepts `NULL`.
393    #[test]
394    fn lenient_scalars() {
395        assert!(bool::from_value(Value::Integer(1), "c").unwrap());
396        assert_eq!(i32::from_value(Value::Integer(7), "c").unwrap(), 7);
397        assert!(i8::from_value(Value::Integer(300), "c").is_err());
398        assert!((f64::from_value(Value::Integer(2), "c").unwrap() - 2.0).abs() < f64::EPSILON);
399        assert_eq!(Option::<i64>::from_value(Value::Null, "c").unwrap(), None);
400        assert!(i64::from_value(Value::Null, "c").is_err());
401        assert_eq!(String::from_value(Value::Integer(5), "c").unwrap(), "5");
402    }
403
404    /// Timestamps decode from the stored text form, from RFC 3339 with an
405    /// offset normalised to UTC, and from Unix seconds.
406    #[cfg(feature = "with-chrono")]
407    #[test]
408    fn chrono() {
409        use chrono::{DateTime, NaiveDateTime, Utc};
410        let dt =
411            NaiveDateTime::from_value(Value::Text("2024-01-02 03:04:05.123".into()), "c").unwrap();
412        assert_eq!(dt.to_string(), "2024-01-02 03:04:05.123");
413        let utc = DateTime::<Utc>::from_value(Value::Text("2024-01-02T03:04:05+02:00".into()), "c")
414            .unwrap();
415        assert_eq!(utc.to_rfc3339(), "2024-01-02T01:04:05+00:00");
416        let from_int = DateTime::<Utc>::from_value(Value::Integer(0), "c").unwrap();
417        assert_eq!(from_int.timestamp(), 0);
418    }
419}