Skip to main content

sqlx_mssql_odbc_core/
value.rs

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