Skip to main content

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            #[cfg(feature = "decimal")]
122            SqlValue::Decimal(v) | SqlValue::Money(v) | SqlValue::SmallMoney(v) => {
123                use rust_decimal::prelude::ToPrimitive;
124                v.to_f64().ok_or_else(|| TypeError::TypeMismatch {
125                    expected: "f64",
126                    actual: "Decimal out of range".to_string(),
127                })
128            }
129            SqlValue::Int(v) => Ok(*v as f64),
130            SqlValue::BigInt(v) => Ok(*v as f64),
131            SqlValue::Null => Err(TypeError::UnexpectedNull),
132            _ => Err(TypeError::TypeMismatch {
133                expected: "f64",
134                actual: value.type_name().to_string(),
135            }),
136        }
137    }
138}
139
140impl FromSql for String {
141    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
142        match value {
143            SqlValue::String(v) => Ok(v.clone()),
144            SqlValue::Xml(v) => Ok(v.clone()),
145            SqlValue::Null => Err(TypeError::UnexpectedNull),
146            _ => Err(TypeError::TypeMismatch {
147                expected: "String",
148                actual: value.type_name().to_string(),
149            }),
150        }
151    }
152}
153
154impl FromSql for Vec<u8> {
155    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
156        match value {
157            SqlValue::Binary(v) => Ok(v.to_vec()),
158            SqlValue::Null => Err(TypeError::UnexpectedNull),
159            _ => Err(TypeError::TypeMismatch {
160                expected: "Vec<u8>",
161                actual: value.type_name().to_string(),
162            }),
163        }
164    }
165}
166
167impl<T: FromSql> FromSql for Option<T> {
168    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
169        T::from_sql_nullable(value)
170    }
171}
172
173#[cfg(feature = "uuid")]
174impl FromSql for uuid::Uuid {
175    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
176        match value {
177            SqlValue::Uuid(v) => Ok(*v),
178            SqlValue::Binary(b) if b.len() == 16 => {
179                let bytes: [u8; 16] = b[..]
180                    .try_into()
181                    .map_err(|_| TypeError::InvalidUuid("invalid UUID length".to_string()))?;
182                Ok(uuid::Uuid::from_bytes(bytes))
183            }
184            SqlValue::String(s) => s
185                .parse()
186                .map_err(|e| TypeError::InvalidUuid(format!("{e}"))),
187            SqlValue::Null => Err(TypeError::UnexpectedNull),
188            _ => Err(TypeError::TypeMismatch {
189                expected: "Uuid",
190                actual: value.type_name().to_string(),
191            }),
192        }
193    }
194}
195
196#[cfg(feature = "decimal")]
197impl FromSql for rust_decimal::Decimal {
198    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
199        match value {
200            SqlValue::Decimal(v) => Ok(*v),
201            SqlValue::Money(v) | SqlValue::SmallMoney(v) => Ok(*v),
202            SqlValue::Int(v) => Ok(rust_decimal::Decimal::from(*v)),
203            SqlValue::BigInt(v) => Ok(rust_decimal::Decimal::from(*v)),
204            SqlValue::String(s) => s
205                .parse()
206                .map_err(|e| TypeError::InvalidDecimal(format!("{e}"))),
207            SqlValue::Null => Err(TypeError::UnexpectedNull),
208            _ => Err(TypeError::TypeMismatch {
209                expected: "Decimal",
210                actual: value.type_name().to_string(),
211            }),
212        }
213    }
214}
215
216#[cfg(feature = "chrono")]
217impl FromSql for chrono::NaiveDate {
218    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
219        match value {
220            SqlValue::Date(v) => Ok(*v),
221            SqlValue::DateTime(v) | SqlValue::SmallDateTime(v) => Ok(v.date()),
222            SqlValue::Null => Err(TypeError::UnexpectedNull),
223            _ => Err(TypeError::TypeMismatch {
224                expected: "NaiveDate",
225                actual: value.type_name().to_string(),
226            }),
227        }
228    }
229}
230
231#[cfg(feature = "chrono")]
232impl FromSql for chrono::NaiveTime {
233    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
234        match value {
235            SqlValue::Time(v) => Ok(*v),
236            SqlValue::DateTime(v) | SqlValue::SmallDateTime(v) => Ok(v.time()),
237            SqlValue::Null => Err(TypeError::UnexpectedNull),
238            _ => Err(TypeError::TypeMismatch {
239                expected: "NaiveTime",
240                actual: value.type_name().to_string(),
241            }),
242        }
243    }
244}
245
246#[cfg(feature = "chrono")]
247impl FromSql for chrono::NaiveDateTime {
248    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
249        match value {
250            SqlValue::DateTime(v) | SqlValue::SmallDateTime(v) => Ok(*v),
251            SqlValue::DateTimeOffset(v) => Ok(v.naive_utc()),
252            SqlValue::Null => Err(TypeError::UnexpectedNull),
253            _ => Err(TypeError::TypeMismatch {
254                expected: "NaiveDateTime",
255                actual: value.type_name().to_string(),
256            }),
257        }
258    }
259}
260
261#[cfg(feature = "chrono")]
262impl FromSql for chrono::DateTime<chrono::FixedOffset> {
263    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
264        match value {
265            SqlValue::DateTimeOffset(v) => Ok(*v),
266            SqlValue::Null => Err(TypeError::UnexpectedNull),
267            _ => Err(TypeError::TypeMismatch {
268                expected: "DateTime<FixedOffset>",
269                actual: value.type_name().to_string(),
270            }),
271        }
272    }
273}
274
275#[cfg(feature = "chrono")]
276impl FromSql for chrono::DateTime<chrono::Utc> {
277    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
278        match value {
279            SqlValue::DateTimeOffset(v) => Ok(v.to_utc()),
280            SqlValue::DateTime(v) | SqlValue::SmallDateTime(v) => {
281                Ok(chrono::DateTime::from_naive_utc_and_offset(*v, chrono::Utc))
282            }
283            SqlValue::Null => Err(TypeError::UnexpectedNull),
284            _ => Err(TypeError::TypeMismatch {
285                expected: "DateTime<Utc>",
286                actual: value.type_name().to_string(),
287            }),
288        }
289    }
290}
291
292#[cfg(feature = "json")]
293impl FromSql for serde_json::Value {
294    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
295        match value {
296            SqlValue::Json(v) => Ok(v.clone()),
297            SqlValue::String(s) => serde_json::from_str(s).map_err(|e| TypeError::TypeMismatch {
298                expected: "JSON",
299                actual: format!("invalid JSON: {e}"),
300            }),
301            SqlValue::Null => Ok(serde_json::Value::Null),
302            _ => Err(TypeError::TypeMismatch {
303                expected: "JSON",
304                actual: value.type_name().to_string(),
305            }),
306        }
307    }
308}
309
310#[cfg(test)]
311#[allow(clippy::unwrap_used)]
312mod tests {
313    use super::*;
314
315    #[test]
316    fn test_from_sql_i32() {
317        let value = SqlValue::Int(42);
318        assert_eq!(i32::from_sql(&value).unwrap(), 42);
319    }
320
321    #[test]
322    fn test_from_sql_string() {
323        let value = SqlValue::String("hello".to_string());
324        assert_eq!(String::from_sql(&value).unwrap(), "hello");
325    }
326
327    #[test]
328    fn test_from_sql_null() {
329        let value = SqlValue::Null;
330        assert!(i32::from_sql(&value).is_err());
331    }
332
333    #[test]
334    fn test_from_sql_option() {
335        let value = SqlValue::Int(42);
336        assert_eq!(Option::<i32>::from_sql(&value).unwrap(), Some(42));
337
338        let null = SqlValue::Null;
339        assert_eq!(Option::<i32>::from_sql(&null).unwrap(), None);
340    }
341}