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