Skip to main content

rust_ef/provider/
db_value.rs

1//! `DbValue` — typed database parameter value and conversion error type.
2
3use std::fmt;
4
5/// A typed database parameter value for parameterized queries.
6///
7/// Native variants (`DateTime`/`NaiveDateTime`/`NaiveDate`/`Uuid`/`Decimal`)
8/// are enabled by the `chrono`/`uuid`/`decimal` Cargo features. When enabled,
9/// the PostgreSQL provider binds these via `tokio_postgres`'s binary protocol
10/// (`with-chrono-0_4`/`with-uuid-1` features) for type-safe, lossless
11/// parameter transmission. SQLite and MySQL providers collapse native
12/// variants to their canonical string representation (matching v1.0 behavior)
13/// since neither driver requires native type binding.
14#[derive(Debug, Clone, PartialEq)]
15pub enum DbValue {
16    Null,
17    Bool(bool),
18    I16(i16),
19    I32(i32),
20    I64(i64),
21    F32(f32),
22    F64(f64),
23    String(String),
24    Bytes(Vec<u8>),
25    /// UTC timestamp — bound natively as `TIMESTAMPTZ` on PostgreSQL.
26    #[cfg(feature = "chrono")]
27    DateTime(chrono::DateTime<chrono::Utc>),
28    /// Naive (timezone-less) timestamp — bound natively as `TIMESTAMP` on PG.
29    #[cfg(feature = "chrono")]
30    NaiveDateTime(chrono::NaiveDateTime),
31    /// Calendar date — bound natively as `DATE` on PostgreSQL.
32    #[cfg(feature = "chrono")]
33    NaiveDate(chrono::NaiveDate),
34    /// UUID — bound natively as `UUID` on PostgreSQL.
35    #[cfg(feature = "uuid")]
36    Uuid(uuid::Uuid),
37    /// Fixed-precision decimal — bound as `NUMERIC` string on PostgreSQL
38    /// (tokio_postgres lacks a native `rust_decimal` adapter; the string
39    /// form round-trips losslessly through PG's `NUMERIC` type).
40    #[cfg(feature = "decimal")]
41    Decimal(rust_decimal::Decimal),
42}
43
44impl fmt::Display for DbValue {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            DbValue::Null => write!(f, "NULL"),
48            DbValue::Bool(v) => write!(f, "{}", if *v { "TRUE" } else { "FALSE" }),
49            DbValue::I16(v) => write!(f, "{}", v),
50            DbValue::I32(v) => write!(f, "{}", v),
51            DbValue::I64(v) => write!(f, "{}", v),
52            DbValue::F32(v) => write!(f, "{}", v),
53            DbValue::F64(v) => write!(f, "{}", v),
54            DbValue::String(v) => write!(f, "'{}'", v.replace('\'', "''")),
55            DbValue::Bytes(v) => write!(f, "{}", hex::encode(v)),
56            #[cfg(feature = "chrono")]
57            DbValue::DateTime(v) => write!(f, "'{}'", v.to_rfc3339()),
58            #[cfg(feature = "chrono")]
59            DbValue::NaiveDateTime(v) => write!(f, "'{}'", v),
60            #[cfg(feature = "chrono")]
61            DbValue::NaiveDate(v) => write!(f, "'{}'", v),
62            #[cfg(feature = "uuid")]
63            DbValue::Uuid(v) => write!(f, "'{}'", v),
64            #[cfg(feature = "decimal")]
65            DbValue::Decimal(v) => write!(f, "'{}'", v),
66        }
67    }
68}
69
70mod hex {
71    const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
72
73    pub fn encode(bytes: &[u8]) -> String {
74        let mut s = String::with_capacity(bytes.len() * 2);
75        for &b in bytes {
76            s.push(HEX_CHARS[(b >> 4) as usize] as char);
77            s.push(HEX_CHARS[(b & 0x0f) as usize] as char);
78        }
79        s
80    }
81}
82
83#[cfg(test)]
84mod hex_tests {
85    use super::hex;
86
87    #[test]
88    fn empty_bytes() {
89        assert_eq!(hex::encode(&[]), "");
90    }
91
92    #[test]
93    fn single_byte() {
94        assert_eq!(hex::encode(&[0xff]), "ff");
95        assert_eq!(hex::encode(&[0x0a]), "0a");
96    }
97
98    #[test]
99    fn multi_byte() {
100        assert_eq!(hex::encode(&[0x00, 0x7f, 0xab, 0xcd]), "007fabcd");
101    }
102
103    #[test]
104    fn matches_format_based_output() {
105        let bytes: Vec<u8> = vec![0xde, 0xad, 0xbe, 0xef];
106        let expected: String = bytes.iter().map(|b| format!("{:02x}", b)).collect();
107        assert_eq!(hex::encode(&bytes), expected);
108    }
109}
110
111impl From<i32> for DbValue {
112    fn from(v: i32) -> Self {
113        DbValue::I32(v)
114    }
115}
116impl From<&i32> for DbValue {
117    fn from(v: &i32) -> Self {
118        DbValue::I32(*v)
119    }
120}
121impl From<i64> for DbValue {
122    fn from(v: i64) -> Self {
123        DbValue::I64(v)
124    }
125}
126impl From<&i64> for DbValue {
127    fn from(v: &i64) -> Self {
128        DbValue::I64(*v)
129    }
130}
131impl From<String> for DbValue {
132    fn from(v: String) -> Self {
133        DbValue::String(v)
134    }
135}
136impl From<&str> for DbValue {
137    fn from(v: &str) -> Self {
138        DbValue::String(v.to_string())
139    }
140}
141impl From<bool> for DbValue {
142    fn from(v: bool) -> Self {
143        DbValue::Bool(v)
144    }
145}
146impl From<f64> for DbValue {
147    fn from(v: f64) -> Self {
148        DbValue::F64(v)
149    }
150}
151impl From<f32> for DbValue {
152    fn from(v: f32) -> Self {
153        DbValue::F32(v)
154    }
155}
156impl From<i16> for DbValue {
157    fn from(v: i16) -> Self {
158        DbValue::I16(v)
159    }
160}
161impl From<Vec<u8>> for DbValue {
162    fn from(v: Vec<u8>) -> Self {
163        DbValue::Bytes(v)
164    }
165}
166
167// --- Feature-gated From impls for chrono / uuid / decimal ---
168//
169// These construct native `DbValue` variants (not `String`) so that the
170// PostgreSQL provider can bind them via tokio_postgres's binary protocol.
171// SQLite and MySQL providers collapse these variants to their canonical
172// string form in their respective `type_conversion.rs`.
173
174#[cfg(feature = "chrono")]
175impl From<chrono::DateTime<chrono::Utc>> for DbValue {
176    fn from(dt: chrono::DateTime<chrono::Utc>) -> Self {
177        DbValue::DateTime(dt)
178    }
179}
180
181#[cfg(feature = "chrono")]
182impl From<chrono::NaiveDateTime> for DbValue {
183    fn from(ndt: chrono::NaiveDateTime) -> Self {
184        DbValue::NaiveDateTime(ndt)
185    }
186}
187
188#[cfg(feature = "chrono")]
189impl From<chrono::NaiveDate> for DbValue {
190    fn from(nd: chrono::NaiveDate) -> Self {
191        DbValue::NaiveDate(nd)
192    }
193}
194
195#[cfg(feature = "uuid")]
196impl From<uuid::Uuid> for DbValue {
197    fn from(u: uuid::Uuid) -> Self {
198        DbValue::Uuid(u)
199    }
200}
201
202#[cfg(feature = "decimal")]
203impl From<rust_decimal::Decimal> for DbValue {
204    fn from(d: rust_decimal::Decimal) -> Self {
205        DbValue::Decimal(d)
206    }
207}
208impl<T> From<Option<T>> for DbValue
209where
210    T: Into<DbValue>,
211{
212    fn from(v: Option<T>) -> Self {
213        match v {
214            Some(val) => val.into(),
215            None => DbValue::Null,
216        }
217    }
218}
219
220/// Error returned when a [`DbValue`] cannot be converted to the requested type.
221///
222/// Used by `TryFrom<DbValue>` impls for `i32`/`i64`/`f64`/`String`/`bool`/...
223/// and by `QueryBuilder::min_internal` / `max_internal` to surface type
224/// mismatches when reading aggregation results.
225#[derive(Debug, Clone, PartialEq)]
226pub struct DbValueConvertError {
227    /// The [`DbValue`] that could not be converted.
228    pub source: DbValue,
229    /// The target Rust type name (e.g. `"i32"`).
230    pub target_type: &'static str,
231}
232
233impl fmt::Display for DbValueConvertError {
234    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235        write!(
236            f,
237            "cannot convert {:?} to {}",
238            self.source, self.target_type
239        )
240    }
241}
242
243impl std::error::Error for DbValueConvertError {}
244
245impl From<DbValueConvertError> for crate::error::EFError {
246    fn from(e: DbValueConvertError) -> Self {
247        crate::error::EFError::type_conversion(e.to_string())
248    }
249}