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