1use chrono::{FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
2
3#[derive(Debug, Clone, PartialEq)]
9#[non_exhaustive]
10pub enum DbValue {
11 Null,
13 Integer(i64),
15 Real(f64),
17 Text(String),
19 Blob(Vec<u8>),
21 Bool(bool),
23 DateTime(chrono::DateTime<FixedOffset>),
25 NaiveDateTime(NaiveDateTime),
27 Date(NaiveDate),
29 Time(NaiveTime),
31 Json(serde_json::Value),
33 #[cfg(feature = "uuid")]
35 Uuid(uuid::Uuid),
36 Encrypted(super::encrypted::Unsealed),
39}
40
41impl DbValue {
42 pub(crate) fn to_json(&self) -> serde_json::Value {
44 use serde_json::Value;
45 match self {
46 DbValue::Null => Value::Null,
47 DbValue::Integer(v) => Value::from(*v),
48 DbValue::Real(v) => Value::from(*v),
49 DbValue::Text(v) => Value::from(v.clone()),
50 DbValue::Blob(v) => Value::from(v.clone()),
51 DbValue::Bool(v) => Value::from(*v),
52 DbValue::DateTime(v) => Value::from(v.to_rfc3339()),
53 DbValue::NaiveDateTime(v) => Value::from(v.format("%Y-%m-%dT%H:%M:%S").to_string()),
54 DbValue::Date(v) => Value::from(v.to_string()),
55 DbValue::Time(v) => Value::from(v.to_string()),
56 DbValue::Json(v) => v.clone(),
57 #[cfg(feature = "uuid")]
58 DbValue::Uuid(v) => Value::from(v.to_string()),
59 DbValue::Encrypted(_) => Value::from("[encrypted]"),
60 }
61 }
62
63 pub(crate) fn for_sqlite(self) -> DbValue {
67 match self {
68 DbValue::Bool(v) => DbValue::Integer(i64::from(v)),
69 DbValue::DateTime(v) => DbValue::Text(v.format("%F %T%.f%:z").to_string()),
70 DbValue::NaiveDateTime(v) => DbValue::Text(v.format("%F %T%.f").to_string()),
71 DbValue::Date(v) => DbValue::Text(v.format("%F").to_string()),
72 DbValue::Time(v) => DbValue::Text(v.format("%T%.f").to_string()),
73 DbValue::Json(v) => DbValue::Text(v.to_string()),
74 #[cfg(feature = "uuid")]
75 DbValue::Uuid(v) => DbValue::Blob(v.as_bytes().to_vec()),
76 other => other,
77 }
78 }
79}
80
81pub trait ToDbValue {
85 fn to_db_value(&self) -> DbValue;
87}
88
89macro_rules! integer {
90 ($($t:ty),*) => {$(
91 impl ToDbValue for $t {
92 fn to_db_value(&self) -> DbValue {
93 DbValue::Integer(i64::from(*self))
94 }
95 }
96 )*};
97}
98
99integer!(i8, i16, i32, i64, u8, u16, u32);
100
101impl ToDbValue for bool {
102 fn to_db_value(&self) -> DbValue {
103 DbValue::Bool(*self)
104 }
105}
106
107impl ToDbValue for f32 {
108 fn to_db_value(&self) -> DbValue {
109 DbValue::Real(f64::from(*self))
110 }
111}
112
113impl ToDbValue for f64 {
114 fn to_db_value(&self) -> DbValue {
115 DbValue::Real(*self)
116 }
117}
118
119impl ToDbValue for str {
120 fn to_db_value(&self) -> DbValue {
121 DbValue::Text(self.to_owned())
122 }
123}
124
125impl ToDbValue for String {
126 fn to_db_value(&self) -> DbValue {
127 DbValue::Text(self.clone())
128 }
129}
130
131impl ToDbValue for Vec<u8> {
132 fn to_db_value(&self) -> DbValue {
133 DbValue::Blob(self.clone())
134 }
135}
136
137impl ToDbValue for serde_json::Value {
138 fn to_db_value(&self) -> DbValue {
139 DbValue::Json(self.clone())
140 }
141}
142
143#[cfg(feature = "uuid")]
144impl ToDbValue for uuid::Uuid {
145 fn to_db_value(&self) -> DbValue {
146 DbValue::Uuid(*self)
147 }
148}
149
150impl<Tz: TimeZone> ToDbValue for chrono::DateTime<Tz> {
151 fn to_db_value(&self) -> DbValue {
152 DbValue::DateTime(self.fixed_offset())
153 }
154}
155
156impl ToDbValue for NaiveDateTime {
157 fn to_db_value(&self) -> DbValue {
158 DbValue::NaiveDateTime(*self)
159 }
160}
161
162impl ToDbValue for NaiveDate {
163 fn to_db_value(&self) -> DbValue {
164 DbValue::Date(*self)
165 }
166}
167
168impl ToDbValue for NaiveTime {
169 fn to_db_value(&self) -> DbValue {
170 DbValue::Time(*self)
171 }
172}
173
174impl<T: ToDbValue> ToDbValue for Option<T> {
175 fn to_db_value(&self) -> DbValue {
176 self.as_ref().map_or(DbValue::Null, ToDbValue::to_db_value)
177 }
178}
179
180impl<T: ToDbValue + ?Sized> ToDbValue for &T {
181 fn to_db_value(&self) -> DbValue {
182 (**self).to_db_value()
183 }
184}
185
186impl ToDbValue for DbValue {
187 fn to_db_value(&self) -> DbValue {
188 self.clone()
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use serde_json::json;
196
197 #[test]
200 fn every_value_has_a_json_form() {
201 let at = chrono::DateTime::parse_from_rfc3339("2026-10-01T09:30:00Z").unwrap();
202 let local = at.naive_utc();
203 let cases = [
204 (DbValue::Null, json!(null)),
205 (DbValue::Integer(7), json!(7)),
206 (DbValue::Real(1.5), json!(1.5)),
207 (DbValue::Text("a".into()), json!("a")),
208 (DbValue::Blob(vec![1, 2]), json!([1, 2])),
209 (DbValue::Bool(true), json!(true)),
210 (DbValue::DateTime(at), json!("2026-10-01T09:30:00+00:00")),
211 (DbValue::NaiveDateTime(local), json!("2026-10-01T09:30:00")),
212 (DbValue::Date(local.date()), json!("2026-10-01")),
213 (DbValue::Time(local.time()), json!("09:30:00")),
214 (DbValue::Json(json!({"a": 1})), json!({"a": 1})),
215 (
216 DbValue::Encrypted(super::super::encrypted::Unsealed("secret".into())),
217 json!("[encrypted]"),
218 ),
219 ];
220 for (value, expected) in cases {
221 assert_eq!(value.to_json(), expected, "{value:?}");
222 }
223 }
224
225 #[test]
226 fn floats_times_and_bytes_become_values() {
227 assert!(matches!(1.5f32.to_db_value(), DbValue::Real(v) if v == 1.5));
228 let time = chrono::NaiveTime::from_hms_opt(9, 30, 0).unwrap();
229 assert!(matches!(time.to_db_value(), DbValue::Time(t) if t == time));
230 assert!(matches!(vec![1u8, 2].to_db_value(), DbValue::Blob(b) if b == [1, 2]));
231 }
232}