Skip to main content

chain_builder/
value.rs

1//! Bound value model for v2.
2//!
3//! [`Value`] is the dialect-agnostic representation of a bound parameter. The
4//! [`IntoBind`] trait converts common Rust types into a [`Value`] so they can be
5//! used ergonomically as query bindings.
6
7/// A dialect-agnostic bound parameter value.
8///
9/// This enum is `#[non_exhaustive]` so new variants can be added without a
10/// breaking change; downstream `match`es must include a wildcard arm.
11#[non_exhaustive]
12#[derive(Debug, Clone, PartialEq)]
13pub enum Value {
14    /// SQL `NULL`.
15    Null,
16    /// Boolean.
17    Bool(bool),
18    /// Signed 64-bit integer (the canonical integer binding type).
19    I64(i64),
20    /// 64-bit floating point.
21    F64(f64),
22    /// UTF-8 text.
23    Text(String),
24    /// Raw bytes / BLOB.
25    Bytes(Vec<u8>),
26    /// JSON value (requires the `json` feature).
27    #[cfg(feature = "json")]
28    Json(serde_json::Value),
29    /// UUID (requires the `uuid` feature).
30    #[cfg(feature = "uuid")]
31    Uuid(uuid::Uuid),
32    /// Timezone-aware timestamp in UTC (requires the `chrono` feature).
33    #[cfg(feature = "chrono")]
34    DateTimeUtc(chrono::DateTime<chrono::Utc>),
35    /// Naive (timezone-less) date and time (requires the `chrono` feature).
36    #[cfg(feature = "chrono")]
37    NaiveDateTime(chrono::NaiveDateTime),
38    /// Naive date (requires the `chrono` feature).
39    #[cfg(feature = "chrono")]
40    NaiveDate(chrono::NaiveDate),
41    /// Naive time of day (requires the `chrono` feature).
42    #[cfg(feature = "chrono")]
43    NaiveTime(chrono::NaiveTime),
44    /// Fixed-point decimal for money/exact-numeric columns (requires the
45    /// `decimal` feature). Bound natively on Postgres/MySQL (`NUMERIC`/`DECIMAL`)
46    /// and as TEXT on SQLite (which has no native decimal type).
47    #[cfg(feature = "decimal")]
48    Decimal(rust_decimal::Decimal),
49}
50
51/// Conversion of a Rust value into a bound [`Value`].
52pub trait IntoBind {
53    /// Convert `self` into a [`Value`].
54    fn into_bind(self) -> Value;
55}
56
57macro_rules! impl_into_bind_i64 {
58    ($($t:ty),* $(,)?) => {
59        $(
60            impl IntoBind for $t {
61                fn into_bind(self) -> Value {
62                    Value::I64(self as i64)
63                }
64            }
65        )*
66    };
67}
68
69// Integers that always fit losslessly in i64.
70impl_into_bind_i64!(i8, i16, i32, i64, u8, u16, u32);
71
72// Wider/unsigned integers: narrowed via `as i64`.
73//
74// Values above `i64::MAX` wrap per Rust's `as` truncation semantics rather
75// than erroring or saturating. For example `u64::MAX` becomes `-1`, and
76// `(i64::MAX as u64) + 1` becomes `i64::MIN`. This is intentional: the
77// canonical integer binding type is `i64`, and these conversions are provided
78// for ergonomics. Callers binding values above `i64::MAX` must account for the
79// wrap (or bind as `Value::Text` / a wider numeric type explicitly).
80//
81// (A `//` comment rather than `///` because rustdoc cannot attach a doc comment
82// to a macro invocation; the wrap is also asserted in the test module below.)
83impl_into_bind_i64!(u64, usize, isize);
84
85impl IntoBind for f32 {
86    fn into_bind(self) -> Value {
87        Value::F64(self as f64)
88    }
89}
90
91impl IntoBind for f64 {
92    fn into_bind(self) -> Value {
93        Value::F64(self)
94    }
95}
96
97impl IntoBind for bool {
98    fn into_bind(self) -> Value {
99        Value::Bool(self)
100    }
101}
102
103impl IntoBind for &str {
104    fn into_bind(self) -> Value {
105        Value::Text(self.to_owned())
106    }
107}
108
109impl IntoBind for String {
110    fn into_bind(self) -> Value {
111        Value::Text(self)
112    }
113}
114
115impl IntoBind for &String {
116    fn into_bind(self) -> Value {
117        Value::Text(self.clone())
118    }
119}
120
121impl IntoBind for Vec<u8> {
122    fn into_bind(self) -> Value {
123        Value::Bytes(self)
124    }
125}
126
127impl IntoBind for &[u8] {
128    fn into_bind(self) -> Value {
129        Value::Bytes(self.to_vec())
130    }
131}
132
133impl IntoBind for Value {
134    fn into_bind(self) -> Value {
135        self
136    }
137}
138
139impl<T: IntoBind> IntoBind for Option<T> {
140    fn into_bind(self) -> Value {
141        match self {
142            None => Value::Null,
143            Some(inner) => inner.into_bind(),
144        }
145    }
146}
147
148#[cfg(feature = "json")]
149impl IntoBind for serde_json::Value {
150    fn into_bind(self) -> Value {
151        Value::Json(self)
152    }
153}
154
155#[cfg(feature = "uuid")]
156impl IntoBind for uuid::Uuid {
157    fn into_bind(self) -> Value {
158        Value::Uuid(self)
159    }
160}
161
162#[cfg(feature = "chrono")]
163impl IntoBind for chrono::DateTime<chrono::Utc> {
164    fn into_bind(self) -> Value {
165        Value::DateTimeUtc(self)
166    }
167}
168
169#[cfg(feature = "chrono")]
170impl IntoBind for chrono::NaiveDateTime {
171    fn into_bind(self) -> Value {
172        Value::NaiveDateTime(self)
173    }
174}
175
176#[cfg(feature = "chrono")]
177impl IntoBind for chrono::NaiveDate {
178    fn into_bind(self) -> Value {
179        Value::NaiveDate(self)
180    }
181}
182
183#[cfg(feature = "chrono")]
184impl IntoBind for chrono::NaiveTime {
185    fn into_bind(self) -> Value {
186        Value::NaiveTime(self)
187    }
188}
189
190#[cfg(feature = "decimal")]
191impl IntoBind for rust_decimal::Decimal {
192    fn into_bind(self) -> Value {
193        Value::Decimal(self)
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn integers_become_i64() {
203        assert_eq!(7i8.into_bind(), Value::I64(7));
204        assert_eq!(7i16.into_bind(), Value::I64(7));
205        assert_eq!(7i32.into_bind(), Value::I64(7));
206        assert_eq!(7i64.into_bind(), Value::I64(7));
207        assert_eq!(7u8.into_bind(), Value::I64(7));
208        assert_eq!(7u16.into_bind(), Value::I64(7));
209        assert_eq!(7u32.into_bind(), Value::I64(7));
210        assert_eq!(7u64.into_bind(), Value::I64(7));
211        assert_eq!(7usize.into_bind(), Value::I64(7));
212        assert_eq!(7isize.into_bind(), Value::I64(7));
213    }
214
215    #[test]
216    fn u64_above_i64_max_wraps_intentionally() {
217        // Values above i64::MAX truncate via `as i64` (documented behaviour).
218        assert_eq!(((i64::MAX as u64) + 1).into_bind(), Value::I64(i64::MIN));
219        assert_eq!(u64::MAX.into_bind(), Value::I64(-1));
220    }
221
222    #[test]
223    fn floats_become_f64() {
224        assert_eq!(1.5f32.into_bind(), Value::F64(1.5));
225        assert_eq!(1.5f64.into_bind(), Value::F64(1.5));
226    }
227
228    #[test]
229    fn bool_becomes_bool() {
230        assert_eq!(true.into_bind(), Value::Bool(true));
231        assert_eq!(false.into_bind(), Value::Bool(false));
232    }
233
234    #[test]
235    fn strings_become_text() {
236        assert_eq!("hi".into_bind(), Value::Text("hi".to_string()));
237        assert_eq!(
238            String::from("hi").into_bind(),
239            Value::Text("hi".to_string())
240        );
241        let owned = String::from("hi");
242        assert_eq!((&owned).into_bind(), Value::Text("hi".to_string()));
243    }
244
245    #[test]
246    fn bytes_become_bytes() {
247        assert_eq!(vec![1u8, 2, 3].into_bind(), Value::Bytes(vec![1, 2, 3]));
248        let slice: &[u8] = &[1, 2, 3];
249        assert_eq!(slice.into_bind(), Value::Bytes(vec![1, 2, 3]));
250    }
251
252    #[test]
253    fn option_none_becomes_null() {
254        assert_eq!(Option::<i64>::None.into_bind(), Value::Null);
255    }
256
257    #[test]
258    fn option_some_becomes_inner() {
259        assert_eq!(Some(5i64).into_bind(), Value::I64(5));
260    }
261
262    #[test]
263    fn value_into_bind_is_identity() {
264        assert_eq!(Value::I64(1).into_bind(), Value::I64(1));
265    }
266}