1use crate::error::QueryError;
31use crate::value::Value;
32use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, TimeDelta, Utc};
33use rust_decimal::Decimal;
34use rust_decimal::prelude::ToPrimitive;
35use std::net::IpAddr;
36use uuid::Uuid;
37
38pub const DATE_FORMAT: &str = "%Y-%m-%d";
40pub const TIME_FORMAT: &str = "%H:%M:%S%.6f";
42pub const TIMESTAMP_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.6fZ";
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47#[non_exhaustive]
48pub enum SqlType {
49 SmallInt,
51 Integer,
53 BigInt,
55 Real,
57 Double,
59 Decimal,
61 Bool,
63 Text,
65 Binary,
67 Date,
69 Time,
71 Timestamp,
73 Duration,
75 Uuid,
77 Json,
79 IpAddr,
81}
82
83pub trait DbType: Sized + Send + Sync + 'static {
85 const SQL_TYPE: SqlType;
87 const NULLABLE: bool = false;
89
90 fn to_value(&self) -> Value;
92
93 fn from_value(value: Value) -> Result<Self, String>;
99}
100
101pub fn decode<T: DbType>(column: &str, value: Value) -> Result<T, QueryError> {
106 T::from_value(value).map_err(|reason| QueryError::Decode {
107 column: column.to_owned(),
108 reason,
109 })
110}
111
112fn mismatch(expected: &str, got: &Value) -> String {
113 format!("expected {expected}, got {}", got.kind())
114}
115
116macro_rules! int_type {
117 ($($t:ty => $sql:ident),*) => {$(
118 impl DbType for $t {
119 const SQL_TYPE: SqlType = SqlType::$sql;
120 fn to_value(&self) -> Value {
121 Value::Int(i64::from(*self))
122 }
123 fn from_value(value: Value) -> Result<Self, String> {
124 let int = match value {
125 Value::Int(i) => i,
126 Value::Decimal(d) if d.is_integer() => {
128 i64::try_from(d).map_err(|_| format!("{d} is out of range"))?
129 }
130 other => return Err(mismatch("integer", &other)),
131 };
132 <$t>::try_from(int).map_err(|_| format!("{int} is out of range"))
133 }
134 }
135 )*};
136}
137int_type!(i16 => SmallInt, i32 => Integer, i64 => BigInt);
138
139impl DbType for f64 {
140 const SQL_TYPE: SqlType = SqlType::Double;
141 fn to_value(&self) -> Value {
142 Value::Float(*self)
143 }
144 #[allow(clippy::cast_precision_loss)]
145 fn from_value(value: Value) -> Result<Self, String> {
146 match value {
147 Value::Float(f) => Ok(f),
148 Value::Int(i) => Ok(i as f64),
149 Value::Decimal(d) => d
150 .to_f64()
151 .ok_or_else(|| format!("{d} is not a valid float")),
152 other => Err(mismatch("float", &other)),
153 }
154 }
155}
156
157impl DbType for f32 {
158 const SQL_TYPE: SqlType = SqlType::Real;
159 fn to_value(&self) -> Value {
160 Value::Float(f64::from(*self))
161 }
162 #[allow(clippy::cast_possible_truncation)]
163 fn from_value(value: Value) -> Result<Self, String> {
164 f64::from_value(value).map(|f| f as f32)
165 }
166}
167
168impl DbType for bool {
169 const SQL_TYPE: SqlType = SqlType::Bool;
170 fn to_value(&self) -> Value {
171 Value::Bool(*self)
172 }
173 fn from_value(value: Value) -> Result<Self, String> {
174 match value {
175 Value::Bool(b) => Ok(b),
176 Value::Int(0) => Ok(false),
177 Value::Int(1) => Ok(true),
178 other => Err(mismatch("boolean", &other)),
179 }
180 }
181}
182
183impl DbType for String {
184 const SQL_TYPE: SqlType = SqlType::Text;
185 fn to_value(&self) -> Value {
186 Value::Text(self.clone())
187 }
188 fn from_value(value: Value) -> Result<Self, String> {
189 match value {
190 Value::Text(s) => Ok(s),
191 other => Err(mismatch("text", &other)),
192 }
193 }
194}
195
196impl DbType for Vec<u8> {
197 const SQL_TYPE: SqlType = SqlType::Binary;
198 fn to_value(&self) -> Value {
199 Value::Bytes(self.clone())
200 }
201 fn from_value(value: Value) -> Result<Self, String> {
202 match value {
203 Value::Bytes(b) => Ok(b),
204 other => Err(mismatch("bytes", &other)),
205 }
206 }
207}
208
209impl DbType for Decimal {
210 const SQL_TYPE: SqlType = SqlType::Decimal;
211 fn to_value(&self) -> Value {
212 Value::Decimal(*self)
213 }
214 fn from_value(value: Value) -> Result<Self, String> {
215 match value {
216 Value::Decimal(d) => Ok(d),
217 Value::Int(i) => Ok(Decimal::from(i)),
218 Value::Float(f) => Decimal::try_from(f).map_err(|e| e.to_string()),
219 Value::Text(s) => s.parse().map_err(|e| format!("invalid decimal {s:?}: {e}")),
220 other => Err(mismatch("decimal", &other)),
221 }
222 }
223}
224
225impl DbType for Uuid {
226 const SQL_TYPE: SqlType = SqlType::Uuid;
227 fn to_value(&self) -> Value {
228 Value::Uuid(*self)
229 }
230 fn from_value(value: Value) -> Result<Self, String> {
231 match value {
232 Value::Uuid(u) => Ok(u),
233 Value::Text(s) => Uuid::parse_str(&s).map_err(|e| format!("invalid uuid {s:?}: {e}")),
234 Value::Bytes(b) => Uuid::from_slice(&b).map_err(|e| e.to_string()),
235 other => Err(mismatch("uuid", &other)),
236 }
237 }
238}
239
240impl DbType for NaiveDate {
241 const SQL_TYPE: SqlType = SqlType::Date;
242 fn to_value(&self) -> Value {
243 Value::Date(*self)
244 }
245 fn from_value(value: Value) -> Result<Self, String> {
246 match value {
247 Value::Date(d) => Ok(d),
248 Value::Text(s) => NaiveDate::parse_from_str(&s, DATE_FORMAT)
249 .map_err(|e| format!("invalid date {s:?}: {e}")),
250 other => Err(mismatch("date", &other)),
251 }
252 }
253}
254
255impl DbType for NaiveTime {
256 const SQL_TYPE: SqlType = SqlType::Time;
257 fn to_value(&self) -> Value {
258 Value::Time(*self)
259 }
260 fn from_value(value: Value) -> Result<Self, String> {
261 match value {
262 Value::Time(t) => Ok(t),
263 Value::Text(s) => NaiveTime::parse_from_str(&s, TIME_FORMAT)
264 .or_else(|_| NaiveTime::parse_from_str(&s, "%H:%M:%S"))
265 .map_err(|e| format!("invalid time {s:?}: {e}")),
266 other => Err(mismatch("time", &other)),
267 }
268 }
269}
270
271impl DbType for DateTime<Utc> {
272 const SQL_TYPE: SqlType = SqlType::Timestamp;
273 fn to_value(&self) -> Value {
274 Value::Timestamp(*self)
275 }
276 fn from_value(value: Value) -> Result<Self, String> {
277 match value {
278 Value::Timestamp(t) => Ok(t),
279 Value::Text(s) => DateTime::parse_from_rfc3339(&s)
280 .map(|t| t.with_timezone(&Utc))
281 .or_else(|_| {
282 NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S%.f").map(|t| t.and_utc())
283 })
284 .map_err(|e| format!("invalid timestamp {s:?}: {e}")),
285 other => Err(mismatch("timestamp", &other)),
286 }
287 }
288}
289
290impl DbType for TimeDelta {
291 const SQL_TYPE: SqlType = SqlType::Duration;
292 fn to_value(&self) -> Value {
293 Value::Int(self.num_microseconds().unwrap_or(i64::MAX))
295 }
296 fn from_value(value: Value) -> Result<Self, String> {
297 match value {
298 Value::Int(us) => Ok(TimeDelta::microseconds(us)),
299 other => Err(mismatch("duration (microseconds)", &other)),
300 }
301 }
302}
303
304impl DbType for IpAddr {
305 const SQL_TYPE: SqlType = SqlType::IpAddr;
306 fn to_value(&self) -> Value {
307 Value::Text(self.to_string())
308 }
309 fn from_value(value: Value) -> Result<Self, String> {
310 match value {
311 Value::Text(s) => s.parse().map_err(|e| format!("invalid ip {s:?}: {e}")),
312 other => Err(mismatch("ip address", &other)),
313 }
314 }
315}
316
317impl DbType for serde_json::Value {
318 const SQL_TYPE: SqlType = SqlType::Json;
319 fn to_value(&self) -> Value {
320 Value::Json(self.clone())
321 }
322 fn from_value(value: Value) -> Result<Self, String> {
323 match value {
324 Value::Json(j) => Ok(j),
325 Value::Text(s) => serde_json::from_str(&s).map_err(|e| format!("invalid json: {e}")),
326 other => Err(mismatch("json", &other)),
327 }
328 }
329}
330
331impl<T: DbType> DbType for Option<T> {
332 const SQL_TYPE: SqlType = T::SQL_TYPE;
333 const NULLABLE: bool = true;
334 fn to_value(&self) -> Value {
335 self.as_ref().map_or(Value::Null, DbType::to_value)
336 }
337 fn from_value(value: Value) -> Result<Self, String> {
338 match value {
339 Value::Null => Ok(None),
340 other => T::from_value(other).map(Some),
341 }
342 }
343}
344
345pub fn canonical_text(value: &Value) -> Option<String> {
349 Some(match value {
350 Value::Decimal(d) => d.to_string(),
351 Value::Uuid(u) => u.hyphenated().to_string(),
352 Value::Date(d) => d.format(DATE_FORMAT).to_string(),
353 Value::Time(t) => t.format(TIME_FORMAT).to_string(),
354 Value::Timestamp(t) => t.format(TIMESTAMP_FORMAT).to_string(),
355 Value::Json(j) => j.to_string(),
356 _ => return None,
357 })
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 fn round_trip<T: DbType + PartialEq + std::fmt::Debug>(v: T) {
365 let native = v.to_value();
366 let text = canonical_text(&native).map_or(native.clone(), Value::Text);
367 assert_eq!(T::from_value(native).as_ref(), Ok(&v));
368 assert_eq!(T::from_value(text).as_ref(), Ok(&v));
369 }
370
371 #[test]
372 fn native_and_canonical_forms_round_trip() {
373 round_trip(42_i64);
374 round_trip(true);
375 round_trip("x".to_owned());
376 round_trip(Decimal::new(1250, 2));
377 round_trip(Uuid::from_u128(7));
378 round_trip(NaiveDate::from_ymd_opt(2026, 9, 29).unwrap_or_default());
379 round_trip(NaiveTime::from_hms_micro_opt(1, 2, 3, 4).unwrap_or_default());
380 round_trip(DateTime::from_timestamp_micros(1_700_000_000_123_456).unwrap_or_default());
381 round_trip(TimeDelta::milliseconds(1500));
382 round_trip(serde_json::json!({"a": [1]}));
383 round_trip(Some(3_i32));
384 round_trip(None::<String>);
385 }
386
387 #[test]
388 fn sqlite_bool_and_range_errors() {
389 assert_eq!(bool::from_value(Value::Int(1)), Ok(true));
390 assert!(i16::from_value(Value::Int(1 << 20)).is_err());
391 assert!(matches!(
392 decode::<i64>("age", Value::Text("x".into())),
393 Err(QueryError::Decode { column, .. }) if column == "age"
394 ));
395 }
396
397 #[test]
398 fn timestamp_text_orders_like_time() {
399 let a = canonical_text(&Value::Timestamp(
400 DateTime::from_timestamp(9, 0).unwrap_or_default(),
401 ));
402 let b = canonical_text(&Value::Timestamp(
403 DateTime::from_timestamp(10, 5).unwrap_or_default(),
404 ));
405 assert!(a < b);
406 }
407}