1use turso_sql::Value;
26
27use crate::error::{Error, Result};
28
29pub trait FromValue: Sized {
36 const TYPE_NAME: &'static str;
38
39 fn from_value(value: Value, column: &str) -> Result<Self>;
46}
47
48fn 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
72macro_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 #[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 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 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#[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 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 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 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 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 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 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 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 #[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 #[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}