Skip to main content

renox_core/db/
value.rs

1use chrono::{FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
2
3/// A value bound to a query parameter.
4///
5/// SQLite stores booleans as `0`/`1` and dates as text; PostgreSQL gets them
6/// with their own types (`BOOLEAN`, `TIMESTAMPTZ`, `TIMESTAMP`, `DATE`,
7/// `TIME`), so the same model works on both.
8#[derive(Debug, Clone, PartialEq)]
9#[non_exhaustive]
10pub enum DbValue {
11    /// SQL `NULL`.
12    Null,
13    /// A 64-bit integer (`INTEGER` on SQLite, `BIGINT` on PostgreSQL).
14    Integer(i64),
15    /// A 64-bit float (`REAL` on SQLite, `DOUBLE PRECISION` on PostgreSQL).
16    Real(f64),
17    /// Text.
18    Text(String),
19    /// Bytes (`BLOB` on SQLite, `BYTEA` on PostgreSQL).
20    Blob(Vec<u8>),
21    /// A boolean (`0`/`1` on SQLite, `BOOLEAN` on PostgreSQL).
22    Bool(bool),
23    /// A point in time with its offset (`TIMESTAMPTZ` on PostgreSQL).
24    DateTime(chrono::DateTime<FixedOffset>),
25    /// A date and time without a zone (`TIMESTAMP` on PostgreSQL).
26    NaiveDateTime(NaiveDateTime),
27    /// A date (`YYYY-MM-DD` text on SQLite, `DATE` on PostgreSQL).
28    Date(NaiveDate),
29    /// A time of day (text on SQLite, `TIME` on PostgreSQL).
30    Time(NaiveTime),
31    /// Text on SQLite; JSON on PostgreSQL (fits `JSONB` and `TEXT` columns).
32    Json(serde_json::Value),
33    /// A 16-byte BLOB on SQLite; `UUID` on PostgreSQL.
34    #[cfg(feature = "uuid")]
35    Uuid(uuid::Uuid),
36    /// An [`Encrypted`](super::Encrypted) field's value, sealed with the
37    /// database's key when the statement runs (text in the column).
38    Encrypted(super::encrypted::Unsealed),
39}
40
41impl DbValue {
42    /// The value as JSON, as templates and APIs would see it.
43    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    /// The value as SQLite stores it: booleans as integers, dates in the text
64    /// formats sqlx uses, so values written through models and through raw
65    /// sqlx queries compare equal.
66    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
81/// Converts a Rust value into a query parameter. Implemented for the usual
82/// scalar types, `Option`, chrono dates and `serde_json::Value`; implement it
83/// for your own types to use them in models and `where_*` filters.
84pub trait ToDbValue {
85    /// The value to bind for `self`.
86    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    // #254: every variant as JSON (User's extra columns and the grid show
198    // them this way), and the less common ToDbValue impls.
199    #[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}