Skip to main content

dbkit/
value.rs

1//! Backend-neutral parameter values.
2//!
3//! [`DbValue`] is the single owned value type used for binding parameters on
4//! both the write side (sqlx) and the read side (analytical engines). It
5//! replaces backend-specific parameter types (such as `tokio_postgres::ToSql`
6//! and the old DuckDB-specific param enum) so the same value can flow to any
7//! supported database without per-backend conversion at the call site.
8
9/// A backend-neutral parameter value.
10///
11/// Construct variants directly, or use the `From` conversions for ergonomics —
12/// including `Option<T>`, which maps `None` to [`DbValue::Null`]:
13///
14/// ```
15/// use dbkit::DbValue;
16///
17/// let a: DbValue = 42i64.into();          // Int(42)
18/// let b: DbValue = "hello".into();        // Text("hello")
19/// let c: DbValue = Some(3.5f64).into();   // Float(3.5)
20/// let d: DbValue = None::<i64>.into();     // Null
21/// assert_eq!(d, DbValue::Null);
22/// ```
23#[derive(Debug, Clone, PartialEq)]
24pub enum DbValue {
25    /// SQL `NULL`.
26    Null,
27    /// Boolean.
28    Bool(bool),
29    /// Signed 64-bit integer. Smaller integer types widen into this.
30    Int(i64),
31    /// 64-bit floating point.
32    Float(f64),
33    /// UTF-8 text.
34    Text(String),
35    /// Raw bytes (`BYTEA` / `BLOB`).
36    Bytes(Vec<u8>),
37
38    // ---- Rich Postgres types (native pool only) ----
39    // These carry types the multi-backend `Any` pool can't represent. They bind
40    // natively through [`PgHandler`](crate::PgHandler) (sqlx Postgres). On the
41    // `Any` write path and the DuckDB read path they fall back to a text/ISO
42    // rendering, which is enough for filters and Postgres' assignment casts.
43    /// SQL `DATE`.
44    #[cfg(feature = "postgres-native")]
45    Date(sqlx::types::chrono::NaiveDate),
46    /// SQL `TIMESTAMP` (no time zone).
47    #[cfg(feature = "postgres-native")]
48    DateTime(sqlx::types::chrono::NaiveDateTime),
49    /// SQL `TIMESTAMPTZ` (UTC).
50    #[cfg(feature = "postgres-native")]
51    TimestampTz(sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>),
52    /// SQL `JSON` / `JSONB`.
53    #[cfg(feature = "postgres-native")]
54    Json(sqlx::types::JsonValue),
55    /// SQL `UUID`.
56    #[cfg(feature = "postgres-native")]
57    Uuid(sqlx::types::Uuid),
58    /// SQL `TIME` (no time zone).
59    #[cfg(feature = "postgres-native")]
60    Time(sqlx::types::chrono::NaiveTime),
61    /// SQL `TEXT[]`.
62    #[cfg(feature = "postgres-native")]
63    TextArray(Vec<String>),
64    /// SQL `FLOAT8[]` (double precision array).
65    #[cfg(feature = "postgres-native")]
66    FloatArray(Vec<f64>),
67    /// SQL `FLOAT8[]` with nullable elements.
68    #[cfg(feature = "postgres-native")]
69    OptFloatArray(Vec<Option<f64>>),
70}
71
72#[cfg(feature = "postgres-native")]
73impl From<sqlx::types::chrono::NaiveDate> for DbValue {
74    fn from(v: sqlx::types::chrono::NaiveDate) -> Self {
75        DbValue::Date(v)
76    }
77}
78
79#[cfg(feature = "postgres-native")]
80impl From<sqlx::types::chrono::NaiveDateTime> for DbValue {
81    fn from(v: sqlx::types::chrono::NaiveDateTime) -> Self {
82        DbValue::DateTime(v)
83    }
84}
85
86#[cfg(feature = "postgres-native")]
87impl From<sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>> for DbValue {
88    fn from(v: sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>) -> Self {
89        DbValue::TimestampTz(v)
90    }
91}
92
93#[cfg(feature = "postgres-native")]
94impl From<sqlx::types::JsonValue> for DbValue {
95    fn from(v: sqlx::types::JsonValue) -> Self {
96        DbValue::Json(v)
97    }
98}
99
100#[cfg(feature = "postgres-native")]
101impl From<sqlx::types::Uuid> for DbValue {
102    fn from(v: sqlx::types::Uuid) -> Self {
103        DbValue::Uuid(v)
104    }
105}
106
107#[cfg(feature = "postgres-native")]
108impl From<sqlx::types::chrono::NaiveTime> for DbValue {
109    fn from(v: sqlx::types::chrono::NaiveTime) -> Self {
110        DbValue::Time(v)
111    }
112}
113
114#[cfg(feature = "postgres-native")]
115impl From<Vec<String>> for DbValue {
116    fn from(v: Vec<String>) -> Self {
117        DbValue::TextArray(v)
118    }
119}
120
121#[cfg(feature = "postgres-native")]
122impl From<Vec<f64>> for DbValue {
123    fn from(v: Vec<f64>) -> Self {
124        DbValue::FloatArray(v)
125    }
126}
127
128#[cfg(feature = "postgres-native")]
129impl From<Vec<Option<f64>>> for DbValue {
130    fn from(v: Vec<Option<f64>>) -> Self {
131        DbValue::OptFloatArray(v)
132    }
133}
134
135impl From<bool> for DbValue {
136    fn from(v: bool) -> Self {
137        DbValue::Bool(v)
138    }
139}
140
141macro_rules! impl_from_int {
142    ($($t:ty),*) => {
143        $(
144            impl From<$t> for DbValue {
145                fn from(v: $t) -> Self {
146                    DbValue::Int(v as i64)
147                }
148            }
149        )*
150    };
151}
152impl_from_int!(i8, i16, i32, i64, u8, u16, u32);
153
154impl From<f32> for DbValue {
155    fn from(v: f32) -> Self {
156        DbValue::Float(v as f64)
157    }
158}
159
160impl From<f64> for DbValue {
161    fn from(v: f64) -> Self {
162        DbValue::Float(v)
163    }
164}
165
166impl From<&str> for DbValue {
167    fn from(v: &str) -> Self {
168        DbValue::Text(v.to_string())
169    }
170}
171
172impl From<String> for DbValue {
173    fn from(v: String) -> Self {
174        DbValue::Text(v)
175    }
176}
177
178impl From<Vec<u8>> for DbValue {
179    fn from(v: Vec<u8>) -> Self {
180        DbValue::Bytes(v)
181    }
182}
183
184impl From<&[u8]> for DbValue {
185    fn from(v: &[u8]) -> Self {
186        DbValue::Bytes(v.to_vec())
187    }
188}
189
190/// `None` becomes [`DbValue::Null`]; `Some(x)` delegates to `x`'s conversion.
191impl<T: Into<DbValue>> From<Option<T>> for DbValue {
192    fn from(v: Option<T>) -> Self {
193        match v {
194            Some(x) => x.into(),
195            None => DbValue::Null,
196        }
197    }
198}
199
200/// Render a text array as a Postgres array literal (`{"a","b"}`), quoting and
201/// escaping each element. Used by the text-fallback binding paths (the `Any`
202/// write pool and the DuckDB read engine, neither of which carries native
203/// arrays); the native [`PgHandler`](crate::PgHandler) binds arrays directly.
204#[cfg(feature = "postgres-native")]
205pub(crate) fn pg_text_array_literal(items: &[String]) -> String {
206    let inner = items
207        .iter()
208        .map(|s| format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")))
209        .collect::<Vec<_>>()
210        .join(",");
211    format!("{{{inner}}}")
212}
213
214/// Render a (possibly nullable) float array as a Postgres array literal
215/// (`{1,2,NULL}`). See [`pg_text_array_literal`] for the rationale.
216#[cfg(feature = "postgres-native")]
217pub(crate) fn pg_float_array_literal<I: IntoIterator<Item = Option<f64>>>(items: I) -> String {
218    let inner = items
219        .into_iter()
220        .map(|o| o.map(|x| x.to_string()).unwrap_or_else(|| "NULL".to_string()))
221        .collect::<Vec<_>>()
222        .join(",");
223    format!("{{{inner}}}")
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn integers_widen_to_int() {
232        assert_eq!(DbValue::from(7i32), DbValue::Int(7));
233        assert_eq!(DbValue::from(7u8), DbValue::Int(7));
234        assert_eq!(DbValue::from(-3i64), DbValue::Int(-3));
235    }
236
237    #[test]
238    fn text_from_str_and_string() {
239        assert_eq!(DbValue::from("hi"), DbValue::Text("hi".into()));
240        assert_eq!(DbValue::from(String::from("hi")), DbValue::Text("hi".into()));
241    }
242
243    #[test]
244    fn option_maps_none_to_null() {
245        let some: DbValue = Some(5i32).into();
246        let none: DbValue = None::<i32>.into();
247        assert_eq!(some, DbValue::Int(5));
248        assert_eq!(none, DbValue::Null);
249    }
250
251    #[test]
252    fn bytes_conversions() {
253        let v: DbValue = vec![1u8, 2, 3].into();
254        assert_eq!(v, DbValue::Bytes(vec![1, 2, 3]));
255        let s: DbValue = [4u8, 5].as_slice().into();
256        assert_eq!(s, DbValue::Bytes(vec![4, 5]));
257    }
258}