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    ///
48    /// **SQLite caveat:** because the value is bound as TEXT, comparison and
49    /// `ORDER BY` against a SQLite column are *lexicographic*, not numeric (e.g.
50    /// `"19.99" < "5"`). Use it for exact storage/round-trip; for numeric range
51    /// queries on SQLite, store a scaled integer or compare with `CAST(... AS REAL)`.
52    #[cfg(feature = "decimal")]
53    Decimal(rust_decimal::Decimal),
54}
55
56/// Conversion of a Rust value into a bound [`Value`].
57pub trait IntoBind {
58    /// Convert `self` into a [`Value`].
59    fn into_bind(self) -> Value;
60}
61
62macro_rules! impl_into_bind_i64 {
63    ($($t:ty),* $(,)?) => {
64        $(
65            impl IntoBind for $t {
66                fn into_bind(self) -> Value {
67                    Value::I64(self as i64)
68                }
69            }
70        )*
71    };
72}
73
74// Integers that always fit losslessly in i64.
75impl_into_bind_i64!(i8, i16, i32, i64, u8, u16, u32);
76
77// Wider/unsigned integers: narrowed via `as i64`.
78//
79// Values above `i64::MAX` wrap per Rust's `as` truncation semantics rather
80// than erroring or saturating. For example `u64::MAX` becomes `-1`, and
81// `(i64::MAX as u64) + 1` becomes `i64::MIN`. This is intentional: the
82// canonical integer binding type is `i64`, and these conversions are provided
83// for ergonomics. Callers binding values above `i64::MAX` must account for the
84// wrap (or bind as `Value::Text` / a wider numeric type explicitly).
85//
86// (A `//` comment rather than `///` because rustdoc cannot attach a doc comment
87// to a macro invocation; the wrap is also asserted in the test module below.)
88impl_into_bind_i64!(u64, usize, isize);
89
90impl IntoBind for f32 {
91    fn into_bind(self) -> Value {
92        Value::F64(self as f64)
93    }
94}
95
96impl IntoBind for f64 {
97    fn into_bind(self) -> Value {
98        Value::F64(self)
99    }
100}
101
102impl IntoBind for bool {
103    fn into_bind(self) -> Value {
104        Value::Bool(self)
105    }
106}
107
108impl IntoBind for &str {
109    fn into_bind(self) -> Value {
110        Value::Text(self.to_owned())
111    }
112}
113
114impl IntoBind for String {
115    fn into_bind(self) -> Value {
116        Value::Text(self)
117    }
118}
119
120impl IntoBind for &String {
121    fn into_bind(self) -> Value {
122        Value::Text(self.clone())
123    }
124}
125
126impl IntoBind for Vec<u8> {
127    fn into_bind(self) -> Value {
128        Value::Bytes(self)
129    }
130}
131
132impl IntoBind for &[u8] {
133    fn into_bind(self) -> Value {
134        Value::Bytes(self.to_vec())
135    }
136}
137
138impl IntoBind for Value {
139    fn into_bind(self) -> Value {
140        self
141    }
142}
143
144impl<T: IntoBind> IntoBind for Option<T> {
145    fn into_bind(self) -> Value {
146        match self {
147            None => Value::Null,
148            Some(inner) => inner.into_bind(),
149        }
150    }
151}
152
153#[cfg(feature = "json")]
154impl IntoBind for serde_json::Value {
155    fn into_bind(self) -> Value {
156        Value::Json(self)
157    }
158}
159
160#[cfg(feature = "uuid")]
161impl IntoBind for uuid::Uuid {
162    fn into_bind(self) -> Value {
163        Value::Uuid(self)
164    }
165}
166
167#[cfg(feature = "chrono")]
168impl IntoBind for chrono::DateTime<chrono::Utc> {
169    fn into_bind(self) -> Value {
170        Value::DateTimeUtc(self)
171    }
172}
173
174#[cfg(feature = "chrono")]
175impl IntoBind for chrono::NaiveDateTime {
176    fn into_bind(self) -> Value {
177        Value::NaiveDateTime(self)
178    }
179}
180
181#[cfg(feature = "chrono")]
182impl IntoBind for chrono::NaiveDate {
183    fn into_bind(self) -> Value {
184        Value::NaiveDate(self)
185    }
186}
187
188#[cfg(feature = "chrono")]
189impl IntoBind for chrono::NaiveTime {
190    fn into_bind(self) -> Value {
191        Value::NaiveTime(self)
192    }
193}
194
195#[cfg(feature = "decimal")]
196impl IntoBind for rust_decimal::Decimal {
197    fn into_bind(self) -> Value {
198        Value::Decimal(self)
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn integers_become_i64() {
208        assert_eq!(7i8.into_bind(), Value::I64(7));
209        assert_eq!(7i16.into_bind(), Value::I64(7));
210        assert_eq!(7i32.into_bind(), Value::I64(7));
211        assert_eq!(7i64.into_bind(), Value::I64(7));
212        assert_eq!(7u8.into_bind(), Value::I64(7));
213        assert_eq!(7u16.into_bind(), Value::I64(7));
214        assert_eq!(7u32.into_bind(), Value::I64(7));
215        assert_eq!(7u64.into_bind(), Value::I64(7));
216        assert_eq!(7usize.into_bind(), Value::I64(7));
217        assert_eq!(7isize.into_bind(), Value::I64(7));
218    }
219
220    #[test]
221    fn u64_above_i64_max_wraps_intentionally() {
222        // Values above i64::MAX truncate via `as i64` (documented behaviour).
223        assert_eq!(((i64::MAX as u64) + 1).into_bind(), Value::I64(i64::MIN));
224        assert_eq!(u64::MAX.into_bind(), Value::I64(-1));
225    }
226
227    #[test]
228    fn floats_become_f64() {
229        assert_eq!(1.5f32.into_bind(), Value::F64(1.5));
230        assert_eq!(1.5f64.into_bind(), Value::F64(1.5));
231    }
232
233    #[test]
234    fn bool_becomes_bool() {
235        assert_eq!(true.into_bind(), Value::Bool(true));
236        assert_eq!(false.into_bind(), Value::Bool(false));
237    }
238
239    #[test]
240    fn strings_become_text() {
241        assert_eq!("hi".into_bind(), Value::Text("hi".to_string()));
242        assert_eq!(
243            String::from("hi").into_bind(),
244            Value::Text("hi".to_string())
245        );
246        let owned = String::from("hi");
247        assert_eq!((&owned).into_bind(), Value::Text("hi".to_string()));
248    }
249
250    #[test]
251    fn bytes_become_bytes() {
252        assert_eq!(vec![1u8, 2, 3].into_bind(), Value::Bytes(vec![1, 2, 3]));
253        let slice: &[u8] = &[1, 2, 3];
254        assert_eq!(slice.into_bind(), Value::Bytes(vec![1, 2, 3]));
255    }
256
257    #[test]
258    fn option_none_becomes_null() {
259        assert_eq!(Option::<i64>::None.into_bind(), Value::Null);
260    }
261
262    #[test]
263    fn option_some_becomes_inner() {
264        assert_eq!(Some(5i64).into_bind(), Value::I64(5));
265    }
266
267    #[test]
268    fn value_into_bind_is_identity() {
269        assert_eq!(Value::I64(1).into_bind(), Value::I64(1));
270    }
271}