mssql_types/
from_sql.rs

1//! Trait for converting from SQL values to Rust types.
2
3// Allow expect() for chrono date construction with known-valid constant dates
4#![allow(clippy::expect_used)]
5
6use crate::error::TypeError;
7use crate::value::SqlValue;
8
9/// Trait for types that can be converted from SQL values.
10///
11/// This trait is implemented for common Rust types to enable
12/// type-safe extraction of values from query results.
13pub trait FromSql: Sized {
14    /// Convert from a SQL value to this type.
15    fn from_sql(value: &SqlValue) -> Result<Self, TypeError>;
16
17    /// Convert from an optional SQL value.
18    ///
19    /// Returns `None` if the value is NULL.
20    fn from_sql_nullable(value: &SqlValue) -> Result<Option<Self>, TypeError> {
21        if value.is_null() {
22            Ok(None)
23        } else {
24            Self::from_sql(value).map(Some)
25        }
26    }
27}
28
29impl FromSql for bool {
30    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
31        match value {
32            SqlValue::Bool(v) => Ok(*v),
33            SqlValue::TinyInt(v) => Ok(*v != 0),
34            SqlValue::SmallInt(v) => Ok(*v != 0),
35            SqlValue::Int(v) => Ok(*v != 0),
36            SqlValue::Null => Err(TypeError::UnexpectedNull),
37            _ => Err(TypeError::TypeMismatch {
38                expected: "bool",
39                actual: value.type_name().to_string(),
40            }),
41        }
42    }
43}
44
45impl FromSql for u8 {
46    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
47        match value {
48            SqlValue::TinyInt(v) => Ok(*v),
49            SqlValue::Null => Err(TypeError::UnexpectedNull),
50            _ => Err(TypeError::TypeMismatch {
51                expected: "u8",
52                actual: value.type_name().to_string(),
53            }),
54        }
55    }
56}
57
58impl FromSql for i16 {
59    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
60        match value {
61            SqlValue::SmallInt(v) => Ok(*v),
62            SqlValue::TinyInt(v) => Ok(*v as i16),
63            SqlValue::Null => Err(TypeError::UnexpectedNull),
64            _ => Err(TypeError::TypeMismatch {
65                expected: "i16",
66                actual: value.type_name().to_string(),
67            }),
68        }
69    }
70}
71
72impl FromSql for i32 {
73    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
74        match value {
75            SqlValue::Int(v) => Ok(*v),
76            SqlValue::SmallInt(v) => Ok(*v as i32),
77            SqlValue::TinyInt(v) => Ok(*v as i32),
78            SqlValue::Null => Err(TypeError::UnexpectedNull),
79            _ => Err(TypeError::TypeMismatch {
80                expected: "i32",
81                actual: value.type_name().to_string(),
82            }),
83        }
84    }
85}
86
87impl FromSql for i64 {
88    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
89        match value {
90            SqlValue::BigInt(v) => Ok(*v),
91            SqlValue::Int(v) => Ok(*v as i64),
92            SqlValue::SmallInt(v) => Ok(*v as i64),
93            SqlValue::TinyInt(v) => Ok(*v as i64),
94            SqlValue::Null => Err(TypeError::UnexpectedNull),
95            _ => Err(TypeError::TypeMismatch {
96                expected: "i64",
97                actual: value.type_name().to_string(),
98            }),
99        }
100    }
101}
102
103impl FromSql for f32 {
104    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
105        match value {
106            SqlValue::Float(v) => Ok(*v),
107            SqlValue::Null => Err(TypeError::UnexpectedNull),
108            _ => Err(TypeError::TypeMismatch {
109                expected: "f32",
110                actual: value.type_name().to_string(),
111            }),
112        }
113    }
114}
115
116impl FromSql for f64 {
117    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
118        match value {
119            SqlValue::Double(v) => Ok(*v),
120            SqlValue::Float(v) => Ok(*v as f64),
121            SqlValue::Null => Err(TypeError::UnexpectedNull),
122            _ => Err(TypeError::TypeMismatch {
123                expected: "f64",
124                actual: value.type_name().to_string(),
125            }),
126        }
127    }
128}
129
130impl FromSql for String {
131    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
132        match value {
133            SqlValue::String(v) => Ok(v.clone()),
134            SqlValue::Xml(v) => Ok(v.clone()),
135            SqlValue::Null => Err(TypeError::UnexpectedNull),
136            _ => Err(TypeError::TypeMismatch {
137                expected: "String",
138                actual: value.type_name().to_string(),
139            }),
140        }
141    }
142}
143
144impl FromSql for Vec<u8> {
145    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
146        match value {
147            SqlValue::Binary(v) => Ok(v.to_vec()),
148            SqlValue::Null => Err(TypeError::UnexpectedNull),
149            _ => Err(TypeError::TypeMismatch {
150                expected: "Vec<u8>",
151                actual: value.type_name().to_string(),
152            }),
153        }
154    }
155}
156
157impl<T: FromSql> FromSql for Option<T> {
158    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
159        T::from_sql_nullable(value)
160    }
161}
162
163#[cfg(feature = "uuid")]
164impl FromSql for uuid::Uuid {
165    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
166        match value {
167            SqlValue::Uuid(v) => Ok(*v),
168            SqlValue::Binary(b) if b.len() == 16 => {
169                let bytes: [u8; 16] = b[..]
170                    .try_into()
171                    .map_err(|_| TypeError::InvalidUuid("invalid UUID length".to_string()))?;
172                Ok(uuid::Uuid::from_bytes(bytes))
173            }
174            SqlValue::String(s) => s
175                .parse()
176                .map_err(|e| TypeError::InvalidUuid(format!("{e}"))),
177            SqlValue::Null => Err(TypeError::UnexpectedNull),
178            _ => Err(TypeError::TypeMismatch {
179                expected: "Uuid",
180                actual: value.type_name().to_string(),
181            }),
182        }
183    }
184}
185
186#[cfg(feature = "decimal")]
187impl FromSql for rust_decimal::Decimal {
188    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
189        match value {
190            SqlValue::Decimal(v) => Ok(*v),
191            SqlValue::Int(v) => Ok(rust_decimal::Decimal::from(*v)),
192            SqlValue::BigInt(v) => Ok(rust_decimal::Decimal::from(*v)),
193            SqlValue::String(s) => s
194                .parse()
195                .map_err(|e| TypeError::InvalidDecimal(format!("{e}"))),
196            SqlValue::Null => Err(TypeError::UnexpectedNull),
197            _ => Err(TypeError::TypeMismatch {
198                expected: "Decimal",
199                actual: value.type_name().to_string(),
200            }),
201        }
202    }
203}
204
205#[cfg(feature = "chrono")]
206impl FromSql for chrono::NaiveDate {
207    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
208        match value {
209            SqlValue::Date(v) => Ok(*v),
210            SqlValue::DateTime(v) => Ok(v.date()),
211            SqlValue::Null => Err(TypeError::UnexpectedNull),
212            _ => Err(TypeError::TypeMismatch {
213                expected: "NaiveDate",
214                actual: value.type_name().to_string(),
215            }),
216        }
217    }
218}
219
220#[cfg(feature = "chrono")]
221impl FromSql for chrono::NaiveTime {
222    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
223        match value {
224            SqlValue::Time(v) => Ok(*v),
225            SqlValue::DateTime(v) => Ok(v.time()),
226            SqlValue::Null => Err(TypeError::UnexpectedNull),
227            _ => Err(TypeError::TypeMismatch {
228                expected: "NaiveTime",
229                actual: value.type_name().to_string(),
230            }),
231        }
232    }
233}
234
235#[cfg(feature = "chrono")]
236impl FromSql for chrono::NaiveDateTime {
237    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
238        match value {
239            SqlValue::DateTime(v) => Ok(*v),
240            SqlValue::DateTimeOffset(v) => Ok(v.naive_utc()),
241            SqlValue::Null => Err(TypeError::UnexpectedNull),
242            _ => Err(TypeError::TypeMismatch {
243                expected: "NaiveDateTime",
244                actual: value.type_name().to_string(),
245            }),
246        }
247    }
248}
249
250#[cfg(feature = "chrono")]
251impl FromSql for chrono::DateTime<chrono::FixedOffset> {
252    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
253        match value {
254            SqlValue::DateTimeOffset(v) => Ok(*v),
255            SqlValue::Null => Err(TypeError::UnexpectedNull),
256            _ => Err(TypeError::TypeMismatch {
257                expected: "DateTime<FixedOffset>",
258                actual: value.type_name().to_string(),
259            }),
260        }
261    }
262}
263
264#[cfg(feature = "chrono")]
265impl FromSql for chrono::DateTime<chrono::Utc> {
266    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
267        match value {
268            SqlValue::DateTimeOffset(v) => Ok(v.to_utc()),
269            SqlValue::DateTime(v) => {
270                Ok(chrono::DateTime::from_naive_utc_and_offset(*v, chrono::Utc))
271            }
272            SqlValue::Null => Err(TypeError::UnexpectedNull),
273            _ => Err(TypeError::TypeMismatch {
274                expected: "DateTime<Utc>",
275                actual: value.type_name().to_string(),
276            }),
277        }
278    }
279}
280
281#[cfg(feature = "json")]
282impl FromSql for serde_json::Value {
283    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
284        match value {
285            SqlValue::Json(v) => Ok(v.clone()),
286            SqlValue::String(s) => serde_json::from_str(s).map_err(|e| TypeError::TypeMismatch {
287                expected: "JSON",
288                actual: format!("invalid JSON: {e}"),
289            }),
290            SqlValue::Null => Ok(serde_json::Value::Null),
291            _ => Err(TypeError::TypeMismatch {
292                expected: "JSON",
293                actual: value.type_name().to_string(),
294            }),
295        }
296    }
297}
298
299#[cfg(test)]
300#[allow(clippy::unwrap_used)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn test_from_sql_i32() {
306        let value = SqlValue::Int(42);
307        assert_eq!(i32::from_sql(&value).unwrap(), 42);
308    }
309
310    #[test]
311    fn test_from_sql_string() {
312        let value = SqlValue::String("hello".to_string());
313        assert_eq!(String::from_sql(&value).unwrap(), "hello");
314    }
315
316    #[test]
317    fn test_from_sql_null() {
318        let value = SqlValue::Null;
319        assert!(i32::from_sql(&value).is_err());
320    }
321
322    #[test]
323    fn test_from_sql_option() {
324        let value = SqlValue::Int(42);
325        assert_eq!(Option::<i32>::from_sql(&value).unwrap(), Some(42));
326
327        let null = SqlValue::Null;
328        assert_eq!(Option::<i32>::from_sql(&null).unwrap(), None);
329    }
330}