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) => {
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::Int(v) => Ok(rust_decimal::Decimal::from(*v)),
202            SqlValue::BigInt(v) => Ok(rust_decimal::Decimal::from(*v)),
203            SqlValue::String(s) => s
204                .parse()
205                .map_err(|e| TypeError::InvalidDecimal(format!("{e}"))),
206            SqlValue::Null => Err(TypeError::UnexpectedNull),
207            _ => Err(TypeError::TypeMismatch {
208                expected: "Decimal",
209                actual: value.type_name().to_string(),
210            }),
211        }
212    }
213}
214
215#[cfg(feature = "chrono")]
216impl FromSql for chrono::NaiveDate {
217    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
218        match value {
219            SqlValue::Date(v) => Ok(*v),
220            SqlValue::DateTime(v) => Ok(v.date()),
221            SqlValue::Null => Err(TypeError::UnexpectedNull),
222            _ => Err(TypeError::TypeMismatch {
223                expected: "NaiveDate",
224                actual: value.type_name().to_string(),
225            }),
226        }
227    }
228}
229
230#[cfg(feature = "chrono")]
231impl FromSql for chrono::NaiveTime {
232    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
233        match value {
234            SqlValue::Time(v) => Ok(*v),
235            SqlValue::DateTime(v) => Ok(v.time()),
236            SqlValue::Null => Err(TypeError::UnexpectedNull),
237            _ => Err(TypeError::TypeMismatch {
238                expected: "NaiveTime",
239                actual: value.type_name().to_string(),
240            }),
241        }
242    }
243}
244
245#[cfg(feature = "chrono")]
246impl FromSql for chrono::NaiveDateTime {
247    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
248        match value {
249            SqlValue::DateTime(v) => Ok(*v),
250            SqlValue::DateTimeOffset(v) => Ok(v.naive_utc()),
251            SqlValue::Null => Err(TypeError::UnexpectedNull),
252            _ => Err(TypeError::TypeMismatch {
253                expected: "NaiveDateTime",
254                actual: value.type_name().to_string(),
255            }),
256        }
257    }
258}
259
260#[cfg(feature = "chrono")]
261impl FromSql for chrono::DateTime<chrono::FixedOffset> {
262    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
263        match value {
264            SqlValue::DateTimeOffset(v) => Ok(*v),
265            SqlValue::Null => Err(TypeError::UnexpectedNull),
266            _ => Err(TypeError::TypeMismatch {
267                expected: "DateTime<FixedOffset>",
268                actual: value.type_name().to_string(),
269            }),
270        }
271    }
272}
273
274#[cfg(feature = "chrono")]
275impl FromSql for chrono::DateTime<chrono::Utc> {
276    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
277        match value {
278            SqlValue::DateTimeOffset(v) => Ok(v.to_utc()),
279            SqlValue::DateTime(v) => {
280                Ok(chrono::DateTime::from_naive_utc_and_offset(*v, chrono::Utc))
281            }
282            SqlValue::Null => Err(TypeError::UnexpectedNull),
283            _ => Err(TypeError::TypeMismatch {
284                expected: "DateTime<Utc>",
285                actual: value.type_name().to_string(),
286            }),
287        }
288    }
289}
290
291#[cfg(feature = "json")]
292impl FromSql for serde_json::Value {
293    fn from_sql(value: &SqlValue) -> Result<Self, TypeError> {
294        match value {
295            SqlValue::Json(v) => Ok(v.clone()),
296            SqlValue::String(s) => serde_json::from_str(s).map_err(|e| TypeError::TypeMismatch {
297                expected: "JSON",
298                actual: format!("invalid JSON: {e}"),
299            }),
300            SqlValue::Null => Ok(serde_json::Value::Null),
301            _ => Err(TypeError::TypeMismatch {
302                expected: "JSON",
303                actual: value.type_name().to_string(),
304            }),
305        }
306    }
307}
308
309#[cfg(test)]
310#[allow(clippy::unwrap_used)]
311mod tests {
312    use super::*;
313
314    #[test]
315    fn test_from_sql_i32() {
316        let value = SqlValue::Int(42);
317        assert_eq!(i32::from_sql(&value).unwrap(), 42);
318    }
319
320    #[test]
321    fn test_from_sql_string() {
322        let value = SqlValue::String("hello".to_string());
323        assert_eq!(String::from_sql(&value).unwrap(), "hello");
324    }
325
326    #[test]
327    fn test_from_sql_null() {
328        let value = SqlValue::Null;
329        assert!(i32::from_sql(&value).is_err());
330    }
331
332    #[test]
333    fn test_from_sql_option() {
334        let value = SqlValue::Int(42);
335        assert_eq!(Option::<i32>::from_sql(&value).unwrap(), Some(42));
336
337        let null = SqlValue::Null;
338        assert_eq!(Option::<i32>::from_sql(&null).unwrap(), None);
339    }
340}