Skip to main content

sqlx_odbc/
value.rs

1use std::borrow::Cow;
2
3/// A small owned ODBC value representation used by unit tests and Any-mapping work.
4#[derive(Debug, Clone, PartialEq)]
5pub struct OdbcValue {
6    kind: OdbcValueKind,
7}
8
9impl OdbcValue {
10    /// Creates a new value from a raw kind.
11    pub fn new(kind: OdbcValueKind) -> Self {
12        Self { kind }
13    }
14
15    /// Returns the raw value kind.
16    pub fn kind(&self) -> &OdbcValueKind {
17        &self.kind
18    }
19
20    /// Returns whether this value is NULL.
21    pub fn is_null(&self) -> bool {
22        matches!(self.kind, OdbcValueKind::Null)
23    }
24
25    /// Returns this value as a signed integer where possible.
26    pub fn as_i64(&self) -> Option<i64> {
27        match &self.kind {
28            OdbcValueKind::TinyInt(value) => Some(i64::from(*value)),
29            OdbcValueKind::SmallInt(value) => Some(i64::from(*value)),
30            OdbcValueKind::Integer(value) => Some(i64::from(*value)),
31            OdbcValueKind::BigInt(value) => Some(*value),
32            OdbcValueKind::Text(value) => parse_integer_text(value),
33            _ => None,
34        }
35    }
36
37    /// Returns this value as `f64` where possible.
38    pub fn as_f64(&self) -> Option<f64> {
39        match &self.kind {
40            OdbcValueKind::Real(value) => Some(f64::from(*value)),
41            OdbcValueKind::Double(value) => Some(*value),
42            OdbcValueKind::TinyInt(value) => Some(f64::from(*value)),
43            OdbcValueKind::SmallInt(value) => Some(f64::from(*value)),
44            OdbcValueKind::Integer(value) => Some(f64::from(*value)),
45            OdbcValueKind::BigInt(value) => Some(*value as f64),
46            OdbcValueKind::Text(value) => value.trim().parse().ok(),
47            _ => None,
48        }
49    }
50
51    /// Returns this value as text where possible.
52    pub fn as_str(&self) -> Option<Cow<'_, str>> {
53        match &self.kind {
54            OdbcValueKind::Text(value) => Some(Cow::Borrowed(value)),
55            _ => None,
56        }
57    }
58
59    /// Returns this value as bytes where possible.
60    pub fn as_bytes(&self) -> Option<Cow<'_, [u8]>> {
61        match &self.kind {
62            OdbcValueKind::Binary(value) => Some(Cow::Borrowed(value)),
63            _ => None,
64        }
65    }
66}
67
68impl sqlx_core::value::Value for OdbcValue {
69    type Database = crate::Odbc;
70
71    fn as_ref(&self) -> <Self::Database as sqlx_core::database::Database>::ValueRef<'_> {
72        OdbcValueRef { value: self }
73    }
74
75    fn type_info(&self) -> Cow<'_, crate::OdbcTypeInfo> {
76        Cow::Owned(self.kind.type_info())
77    }
78
79    fn is_null(&self) -> bool {
80        self.is_null()
81    }
82}
83
84/// Borrowed ODBC value reference.
85#[derive(Debug, Clone, Copy)]
86pub struct OdbcValueRef<'r> {
87    value: &'r OdbcValue,
88}
89
90impl<'r> OdbcValueRef<'r> {
91    /// Returns this value as a signed integer where possible.
92    pub fn as_i64(&self) -> Option<i64> {
93        self.value.as_i64()
94    }
95
96    /// Returns this value as `f64` where possible.
97    pub fn as_f64(&self) -> Option<f64> {
98        self.value.as_f64()
99    }
100
101    /// Returns this value as borrowed text where possible.
102    pub fn as_str(&self) -> Option<&'r str> {
103        match &self.value.kind {
104            OdbcValueKind::Text(value) => Some(value),
105            _ => None,
106        }
107    }
108
109    /// Returns this value as borrowed bytes where possible.
110    pub fn as_bytes(&self) -> Option<&'r [u8]> {
111        match &self.value.kind {
112            OdbcValueKind::Binary(value) => Some(value),
113            _ => None,
114        }
115    }
116
117    /// Returns this value as a boolean where possible.
118    pub fn as_bool(&self) -> Option<bool> {
119        match &self.value.kind {
120            OdbcValueKind::Bit(value) => Some(*value),
121            OdbcValueKind::TinyInt(value) => Some(*value != 0),
122            OdbcValueKind::SmallInt(value) => Some(*value != 0),
123            OdbcValueKind::Integer(value) => Some(*value != 0),
124            OdbcValueKind::BigInt(value) => Some(*value != 0),
125            OdbcValueKind::Real(value) => Some(*value != 0.0),
126            OdbcValueKind::Double(value) => Some(*value != 0.0),
127            OdbcValueKind::Text(value) => parse_bool_text(value),
128            _ => None,
129        }
130    }
131}
132
133impl<'r> sqlx_core::value::ValueRef<'r> for OdbcValueRef<'r> {
134    type Database = crate::Odbc;
135
136    fn to_owned(&self) -> OdbcValue {
137        self.value.clone()
138    }
139
140    fn type_info(&self) -> Cow<'_, crate::OdbcTypeInfo> {
141        Cow::Owned(self.value.kind.type_info())
142    }
143
144    fn is_null(&self) -> bool {
145        self.value.is_null()
146    }
147}
148
149macro_rules! impl_decode_integer {
150    ($ty:ty) => {
151        impl<'r> sqlx_core::decode::Decode<'r, crate::Odbc> for $ty {
152            fn decode(value: OdbcValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
153                let Some(integer) = value.as_i64() else {
154                    return Err(decode_error(
155                        value,
156                        stringify!($ty),
157                        "source value is not an integer",
158                    )
159                    .into());
160                };
161
162                Self::try_from(integer).map_err(|_| {
163                    decode_error(
164                        value,
165                        stringify!($ty),
166                        format!("integer value {integer} is outside the target range"),
167                    )
168                    .into()
169                })
170            }
171        }
172    };
173}
174
175impl_decode_integer!(i8);
176impl_decode_integer!(i16);
177impl_decode_integer!(i32);
178impl_decode_integer!(i64);
179impl_decode_integer!(u8);
180impl_decode_integer!(u16);
181impl_decode_integer!(u32);
182impl_decode_integer!(u64);
183
184impl<'r> sqlx_core::decode::Decode<'r, crate::Odbc> for bool {
185    fn decode(value: OdbcValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
186        value.as_bool().ok_or_else(|| {
187            decode_error(value, "bool", "source value is not boolean-compatible").into()
188        })
189    }
190}
191
192impl<'r> sqlx_core::decode::Decode<'r, crate::Odbc> for f32 {
193    fn decode(value: OdbcValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
194        value
195            .as_f64()
196            .map(|value| value as f32)
197            .ok_or_else(|| decode_error(value, "f32", "source value is not numeric").into())
198    }
199}
200
201impl<'r> sqlx_core::decode::Decode<'r, crate::Odbc> for f64 {
202    fn decode(value: OdbcValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
203        value
204            .as_f64()
205            .ok_or_else(|| decode_error(value, "f64", "source value is not numeric").into())
206    }
207}
208
209impl<'r> sqlx_core::decode::Decode<'r, crate::Odbc> for String {
210    fn decode(value: OdbcValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
211        if let Some(text) = value.as_str() {
212            return Ok(text.to_owned());
213        }
214
215        if let Some(bytes) = value.as_bytes() {
216            return Ok(String::from_utf8(bytes.to_vec())?);
217        }
218
219        Err(decode_error(
220            value,
221            "String",
222            "source value is neither text nor UTF-8 bytes",
223        )
224        .into())
225    }
226}
227
228impl<'r> sqlx_core::decode::Decode<'r, crate::Odbc> for &'r str {
229    fn decode(value: OdbcValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
230        if let Some(text) = value.as_str() {
231            return Ok(text);
232        }
233
234        Err(decode_error(value, "&str", "source value is not text").into())
235    }
236}
237
238impl<'r> sqlx_core::decode::Decode<'r, crate::Odbc> for Vec<u8> {
239    fn decode(value: OdbcValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
240        value
241            .as_bytes()
242            .map(<[u8]>::to_vec)
243            .ok_or_else(|| decode_error(value, "Vec<u8>", "source value is not binary").into())
244    }
245}
246
247impl<'r> sqlx_core::decode::Decode<'r, crate::Odbc> for &'r [u8] {
248    fn decode(value: OdbcValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
249        value
250            .as_bytes()
251            .ok_or_else(|| decode_error(value, "&[u8]", "source value is not binary").into())
252    }
253}
254
255impl<'r> sqlx_core::decode::Decode<'r, crate::Odbc> for odbc_api::sys::Date {
256    fn decode(value: OdbcValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
257        match value.value.kind() {
258            OdbcValueKind::Date(value) => Ok(*value),
259            _ => Err(decode_error(value, "Date", "source value is not an ODBC date").into()),
260        }
261    }
262}
263
264impl<'r> sqlx_core::decode::Decode<'r, crate::Odbc> for odbc_api::sys::Time {
265    fn decode(value: OdbcValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
266        match value.value.kind() {
267            OdbcValueKind::Time(value) => Ok(*value),
268            _ => Err(decode_error(value, "Time", "source value is not an ODBC time").into()),
269        }
270    }
271}
272
273impl<'r> sqlx_core::decode::Decode<'r, crate::Odbc> for odbc_api::sys::Timestamp {
274    fn decode(value: OdbcValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
275        match value.value.kind() {
276            OdbcValueKind::Timestamp(value) => Ok(*value),
277            _ => Err(
278                decode_error(value, "Timestamp", "source value is not an ODBC timestamp").into(),
279            ),
280        }
281    }
282}
283
284fn decode_error(value: OdbcValueRef<'_>, target: &str, reason: impl std::fmt::Display) -> String {
285    format!(
286        "ODBC cannot decode value kind {:?} as {target}: {reason}",
287        value.value.kind()
288    )
289}
290
291fn parse_bool_text(value: &str) -> Option<bool> {
292    match value.trim() {
293        "0" | "0.0" | "false" | "FALSE" | "f" | "F" => Some(false),
294        "1" | "1.0" | "true" | "TRUE" | "t" | "T" => Some(true),
295        value => value
296            .parse::<f64>()
297            .map(|value| value != 0.0)
298            .or_else(|_| value.parse::<i64>().map(|value| value != 0))
299            .ok(),
300    }
301}
302
303fn parse_integer_text(value: &str) -> Option<i64> {
304    let value = value.trim();
305
306    if let Ok(value) = value.parse() {
307        return Some(value);
308    }
309
310    let (integer, fraction) = value.split_once('.')?;
311
312    if fraction.chars().all(|ch| ch == '0') {
313        integer.parse().ok()
314    } else {
315        None
316    }
317}
318
319/// Supported owned ODBC value kinds.
320#[derive(Debug, Clone, PartialEq)]
321pub enum OdbcValueKind {
322    /// NULL value.
323    Null,
324    /// 8-bit signed integer.
325    TinyInt(i8),
326    /// 16-bit signed integer.
327    SmallInt(i16),
328    /// 32-bit signed integer.
329    Integer(i32),
330    /// 64-bit signed integer.
331    BigInt(i64),
332    /// 32-bit float.
333    Real(f32),
334    /// 64-bit float.
335    Double(f64),
336    /// Boolean value.
337    Bit(bool),
338    /// Text value.
339    Text(String),
340    /// Binary value.
341    Binary(Vec<u8>),
342    /// Date value.
343    Date(odbc_api::sys::Date),
344    /// Time value.
345    Time(odbc_api::sys::Time),
346    /// Timestamp value.
347    Timestamp(odbc_api::sys::Timestamp),
348}
349
350impl OdbcValueKind {
351    fn type_info(&self) -> crate::OdbcTypeInfo {
352        let data_type = match self {
353            Self::Null => odbc_api::DataType::Unknown,
354            Self::TinyInt(_) => odbc_api::DataType::TinyInt,
355            Self::SmallInt(_) => odbc_api::DataType::SmallInt,
356            Self::Integer(_) => odbc_api::DataType::Integer,
357            Self::BigInt(_) => odbc_api::DataType::BigInt,
358            Self::Real(_) => odbc_api::DataType::Real,
359            Self::Double(_) => odbc_api::DataType::Double,
360            Self::Bit(_) => odbc_api::DataType::Bit,
361            Self::Text(_) => odbc_api::DataType::WVarchar { length: None },
362            Self::Binary(_) => odbc_api::DataType::Varbinary { length: None },
363            Self::Date(_) => odbc_api::DataType::Date,
364            Self::Time(_) => odbc_api::DataType::Time { precision: 0 },
365            Self::Timestamp(_) => odbc_api::DataType::Timestamp { precision: 6 },
366        };
367
368        crate::OdbcTypeInfo::new(data_type)
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    #[test]
377    fn integer_values_convert_to_i64() {
378        assert_eq!(OdbcValue::new(OdbcValueKind::TinyInt(1)).as_i64(), Some(1));
379        assert_eq!(OdbcValue::new(OdbcValueKind::SmallInt(2)).as_i64(), Some(2));
380        assert_eq!(OdbcValue::new(OdbcValueKind::Integer(3)).as_i64(), Some(3));
381        assert_eq!(OdbcValue::new(OdbcValueKind::BigInt(4)).as_i64(), Some(4));
382        assert_eq!(
383            OdbcValue::new(OdbcValueKind::Text("42.000".to_owned())).as_i64(),
384            Some(42)
385        );
386        assert_eq!(
387            OdbcValue::new(OdbcValueKind::Text("42.5".to_owned())).as_i64(),
388            None
389        );
390    }
391
392    #[test]
393    fn text_numeric_values_convert_to_float() {
394        assert_eq!(
395            OdbcValue::new(OdbcValueKind::Text("42.5".to_owned())).as_f64(),
396            Some(42.5)
397        );
398    }
399
400    #[test]
401    fn text_and_bytes_borrow_from_value() {
402        let text = OdbcValue::new(OdbcValueKind::Text("hello".to_owned()));
403        assert_eq!(text.as_str().as_deref(), Some("hello"));
404
405        let bytes = OdbcValue::new(OdbcValueKind::Binary(vec![1, 2, 3]));
406        assert_eq!(bytes.as_bytes().as_deref(), Some(&[1, 2, 3][..]));
407    }
408
409    #[test]
410    fn null_reports_null() {
411        assert!(OdbcValue::new(OdbcValueKind::Null).is_null());
412    }
413
414    #[test]
415    fn borrowed_values_decode_basic_scalars() {
416        use sqlx_core::decode::Decode;
417        use sqlx_core::value::Value;
418
419        let int = OdbcValue::new(OdbcValueKind::BigInt(42));
420        assert_eq!(
421            <i32 as Decode<crate::Odbc>>::decode(int.as_ref()).unwrap(),
422            42
423        );
424
425        let truthy = OdbcValue::new(OdbcValueKind::Text("true".to_owned()));
426        assert!(<bool as Decode<crate::Odbc>>::decode(truthy.as_ref()).unwrap());
427
428        let text = OdbcValue::new(OdbcValueKind::Text("hello".to_owned()));
429        assert_eq!(
430            <String as Decode<crate::Odbc>>::decode(text.as_ref()).unwrap(),
431            "hello"
432        );
433
434        let bytes = OdbcValue::new(OdbcValueKind::Binary(vec![1, 2, 3]));
435        assert_eq!(
436            <Vec<u8> as Decode<crate::Odbc>>::decode(bytes.as_ref()).unwrap(),
437            vec![1, 2, 3]
438        );
439    }
440
441    #[test]
442    fn borrowed_values_decode_temporal_scalars() {
443        use sqlx_core::decode::Decode;
444        use sqlx_core::value::Value;
445
446        let date = odbc_api::sys::Date {
447            year: 2026,
448            month: 5,
449            day: 29,
450        };
451        let date_value = OdbcValue::new(OdbcValueKind::Date(date));
452        assert_eq!(
453            <odbc_api::sys::Date as Decode<crate::Odbc>>::decode(date_value.as_ref()).unwrap(),
454            date
455        );
456
457        let time = odbc_api::sys::Time {
458            hour: 12,
459            minute: 30,
460            second: 45,
461        };
462        let time_value = OdbcValue::new(OdbcValueKind::Time(time));
463        assert_eq!(
464            <odbc_api::sys::Time as Decode<crate::Odbc>>::decode(time_value.as_ref()).unwrap(),
465            time
466        );
467
468        let timestamp = odbc_api::sys::Timestamp {
469            year: 2026,
470            month: 5,
471            day: 29,
472            hour: 12,
473            minute: 30,
474            second: 45,
475            fraction: 123_456_000,
476        };
477        let timestamp_value = OdbcValue::new(OdbcValueKind::Timestamp(timestamp));
478        assert_eq!(
479            <odbc_api::sys::Timestamp as Decode<crate::Odbc>>::decode(timestamp_value.as_ref())
480                .unwrap(),
481            timestamp
482        );
483    }
484}