Skip to main content

mssql_types/
to_sql.rs

1//! Trait for converting Rust types to SQL values.
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 to SQL values.
10///
11/// This trait is implemented for common Rust types to enable
12/// type-safe parameter binding in queries.
13pub trait ToSql {
14    /// Convert this value to a SQL value.
15    fn to_sql(&self) -> Result<SqlValue, TypeError>;
16
17    /// Get the SQL type name for this value.
18    fn sql_type(&self) -> &'static str;
19
20    /// Explicit decimal precision and scale, when the value alone cannot convey
21    /// it. `None` for every type except [`Numeric`] (created with [`numeric`]),
22    /// which uses it to declare `decimal(precision, scale)` for an Always
23    /// Encrypted column whose declared precision must match exactly.
24    fn decimal_param_info(&self) -> Option<DecimalParamInfo> {
25        None
26    }
27}
28
29/// Explicit precision and scale for a `decimal`/`numeric` parameter (see
30/// [`numeric`]).
31#[derive(Debug, Clone, Copy)]
32pub struct DecimalParamInfo {
33    /// Total number of significant digits (1–38).
34    pub precision: u8,
35    /// Number of digits to the right of the decimal point.
36    pub scale: u8,
37}
38
39impl ToSql for bool {
40    fn to_sql(&self) -> Result<SqlValue, TypeError> {
41        Ok(SqlValue::Bool(*self))
42    }
43
44    fn sql_type(&self) -> &'static str {
45        "BIT"
46    }
47}
48
49impl ToSql for u8 {
50    fn to_sql(&self) -> Result<SqlValue, TypeError> {
51        Ok(SqlValue::TinyInt(*self))
52    }
53
54    fn sql_type(&self) -> &'static str {
55        "TINYINT"
56    }
57}
58
59impl ToSql for i16 {
60    fn to_sql(&self) -> Result<SqlValue, TypeError> {
61        Ok(SqlValue::SmallInt(*self))
62    }
63
64    fn sql_type(&self) -> &'static str {
65        "SMALLINT"
66    }
67}
68
69impl ToSql for i32 {
70    fn to_sql(&self) -> Result<SqlValue, TypeError> {
71        Ok(SqlValue::Int(*self))
72    }
73
74    fn sql_type(&self) -> &'static str {
75        "INT"
76    }
77}
78
79impl ToSql for i64 {
80    fn to_sql(&self) -> Result<SqlValue, TypeError> {
81        Ok(SqlValue::BigInt(*self))
82    }
83
84    fn sql_type(&self) -> &'static str {
85        "BIGINT"
86    }
87}
88
89impl ToSql for f32 {
90    fn to_sql(&self) -> Result<SqlValue, TypeError> {
91        Ok(SqlValue::Float(*self))
92    }
93
94    fn sql_type(&self) -> &'static str {
95        "REAL"
96    }
97}
98
99impl ToSql for f64 {
100    fn to_sql(&self) -> Result<SqlValue, TypeError> {
101        Ok(SqlValue::Double(*self))
102    }
103
104    fn sql_type(&self) -> &'static str {
105        "FLOAT"
106    }
107}
108
109impl ToSql for str {
110    fn to_sql(&self) -> Result<SqlValue, TypeError> {
111        Ok(SqlValue::String(self.to_owned()))
112    }
113
114    fn sql_type(&self) -> &'static str {
115        "NVARCHAR"
116    }
117}
118
119impl ToSql for String {
120    fn to_sql(&self) -> Result<SqlValue, TypeError> {
121        Ok(SqlValue::String(self.clone()))
122    }
123
124    fn sql_type(&self) -> &'static str {
125        "NVARCHAR"
126    }
127}
128
129impl ToSql for [u8] {
130    fn to_sql(&self) -> Result<SqlValue, TypeError> {
131        Ok(SqlValue::Binary(bytes::Bytes::copy_from_slice(self)))
132    }
133
134    fn sql_type(&self) -> &'static str {
135        "VARBINARY"
136    }
137}
138
139impl ToSql for Vec<u8> {
140    fn to_sql(&self) -> Result<SqlValue, TypeError> {
141        Ok(SqlValue::Binary(bytes::Bytes::copy_from_slice(self)))
142    }
143
144    fn sql_type(&self) -> &'static str {
145        "VARBINARY"
146    }
147}
148
149/// Associates a Rust type with its SQL type name so a typed NULL can be
150/// declared without a value (see [`null`]).
151///
152/// `SQL_TYPE` must match what [`ToSql::sql_type`] returns for a value of the
153/// same type.
154pub trait SqlTyped {
155    /// The SQL type name for this Rust type.
156    const SQL_TYPE: &'static str;
157}
158
159impl SqlTyped for bool {
160    const SQL_TYPE: &'static str = "BIT";
161}
162impl SqlTyped for u8 {
163    const SQL_TYPE: &'static str = "TINYINT";
164}
165impl SqlTyped for i16 {
166    const SQL_TYPE: &'static str = "SMALLINT";
167}
168impl SqlTyped for i32 {
169    const SQL_TYPE: &'static str = "INT";
170}
171impl SqlTyped for i64 {
172    const SQL_TYPE: &'static str = "BIGINT";
173}
174impl SqlTyped for f32 {
175    const SQL_TYPE: &'static str = "REAL";
176}
177impl SqlTyped for f64 {
178    const SQL_TYPE: &'static str = "FLOAT";
179}
180impl SqlTyped for String {
181    const SQL_TYPE: &'static str = "NVARCHAR";
182}
183impl SqlTyped for Vec<u8> {
184    const SQL_TYPE: &'static str = "VARBINARY";
185}
186#[cfg(feature = "uuid")]
187impl SqlTyped for uuid::Uuid {
188    const SQL_TYPE: &'static str = "UNIQUEIDENTIFIER";
189}
190#[cfg(feature = "chrono")]
191impl SqlTyped for chrono::NaiveDate {
192    const SQL_TYPE: &'static str = "DATE";
193}
194
195/// A typed NULL parameter, created with [`null`].
196///
197/// Unlike `Option::<T>::None`, which produces an untyped NULL declared as
198/// `nvarchar(1)`, this carries its SQL type. That matters for Always Encrypted
199/// columns, whose strict typing rejects an untyped NULL bound to, for example,
200/// an `int` or `varbinary` column.
201#[derive(Debug, Clone, Copy)]
202pub struct TypedNull {
203    sql_type: &'static str,
204}
205
206impl ToSql for TypedNull {
207    fn to_sql(&self) -> Result<SqlValue, TypeError> {
208        Ok(SqlValue::Null)
209    }
210
211    fn sql_type(&self) -> &'static str {
212        self.sql_type
213    }
214}
215
216/// Create a typed NULL parameter for SQL type `T`, e.g. `null::<i32>()`.
217///
218/// Use this in place of `Option::<T>::None` when binding NULL to a strongly
219/// typed column — required for an Always Encrypted column of a non-string type.
220#[must_use]
221pub fn null<T: SqlTyped>() -> TypedNull {
222    TypedNull {
223        sql_type: T::SQL_TYPE,
224    }
225}
226
227impl<T: ToSql> ToSql for Option<T> {
228    fn to_sql(&self) -> Result<SqlValue, TypeError> {
229        match self {
230            Some(v) => v.to_sql(),
231            None => Ok(SqlValue::Null),
232        }
233    }
234
235    fn sql_type(&self) -> &'static str {
236        match self {
237            Some(v) => v.sql_type(),
238            None => "NULL",
239        }
240    }
241
242    fn decimal_param_info(&self) -> Option<DecimalParamInfo> {
243        self.as_ref().and_then(ToSql::decimal_param_info)
244    }
245}
246
247impl<T: ToSql + ?Sized> ToSql for &T {
248    fn to_sql(&self) -> Result<SqlValue, TypeError> {
249        (*self).to_sql()
250    }
251
252    fn sql_type(&self) -> &'static str {
253        (*self).sql_type()
254    }
255
256    fn decimal_param_info(&self) -> Option<DecimalParamInfo> {
257        (*self).decimal_param_info()
258    }
259}
260
261#[cfg(feature = "uuid")]
262impl ToSql for uuid::Uuid {
263    fn to_sql(&self) -> Result<SqlValue, TypeError> {
264        Ok(SqlValue::Uuid(*self))
265    }
266
267    fn sql_type(&self) -> &'static str {
268        "UNIQUEIDENTIFIER"
269    }
270}
271
272#[cfg(feature = "decimal")]
273impl ToSql for rust_decimal::Decimal {
274    fn to_sql(&self) -> Result<SqlValue, TypeError> {
275        Ok(SqlValue::Decimal(*self))
276    }
277
278    fn sql_type(&self) -> &'static str {
279        "DECIMAL"
280    }
281}
282
283/// A `decimal`/`numeric` parameter with explicit precision and scale.
284///
285/// A plain [`rust_decimal::Decimal`] carries scale but not precision, so it
286/// cannot be matched against an Always Encrypted `decimal` column, whose
287/// declared `decimal(precision, scale)` must match the column exactly.
288/// Construct one with [`numeric`].
289#[cfg(feature = "decimal")]
290#[derive(Debug, Clone, Copy)]
291pub struct Numeric {
292    value: rust_decimal::Decimal,
293    precision: u8,
294    scale: u8,
295}
296
297/// Create a `decimal`/`numeric` parameter with explicit precision and scale.
298///
299/// Required when binding to an Always Encrypted `decimal` column, whose declared
300/// `decimal(precision, scale)` must match the column exactly. The value is
301/// rescaled to `scale`.
302#[cfg(feature = "decimal")]
303#[must_use]
304pub fn numeric(value: rust_decimal::Decimal, precision: u8, scale: u8) -> Numeric {
305    Numeric {
306        value,
307        precision,
308        scale,
309    }
310}
311
312#[cfg(feature = "decimal")]
313impl ToSql for Numeric {
314    fn to_sql(&self) -> Result<SqlValue, TypeError> {
315        let mut value = self.value;
316        value.rescale(u32::from(self.scale));
317        Ok(SqlValue::Decimal(value))
318    }
319
320    fn sql_type(&self) -> &'static str {
321        "DECIMAL"
322    }
323
324    fn decimal_param_info(&self) -> Option<DecimalParamInfo> {
325        Some(DecimalParamInfo {
326            precision: self.precision,
327            scale: self.scale,
328        })
329    }
330}
331
332#[cfg(feature = "decimal")]
333impl ToSql for crate::value::Money {
334    fn to_sql(&self) -> Result<SqlValue, TypeError> {
335        Ok(SqlValue::Money(self.0))
336    }
337
338    fn sql_type(&self) -> &'static str {
339        "MONEY"
340    }
341}
342
343#[cfg(feature = "decimal")]
344impl ToSql for crate::value::SmallMoney {
345    fn to_sql(&self) -> Result<SqlValue, TypeError> {
346        Ok(SqlValue::SmallMoney(self.0))
347    }
348
349    fn sql_type(&self) -> &'static str {
350        "SMALLMONEY"
351    }
352}
353
354#[cfg(feature = "chrono")]
355impl ToSql for chrono::NaiveDate {
356    fn to_sql(&self) -> Result<SqlValue, TypeError> {
357        Ok(SqlValue::Date(*self))
358    }
359
360    fn sql_type(&self) -> &'static str {
361        "DATE"
362    }
363}
364
365#[cfg(feature = "chrono")]
366impl ToSql for chrono::NaiveTime {
367    fn to_sql(&self) -> Result<SqlValue, TypeError> {
368        Ok(SqlValue::Time(*self))
369    }
370
371    fn sql_type(&self) -> &'static str {
372        "TIME"
373    }
374}
375
376#[cfg(feature = "chrono")]
377impl ToSql for chrono::NaiveDateTime {
378    fn to_sql(&self) -> Result<SqlValue, TypeError> {
379        Ok(SqlValue::DateTime(*self))
380    }
381
382    fn sql_type(&self) -> &'static str {
383        "DATETIME2"
384    }
385}
386
387#[cfg(feature = "chrono")]
388impl ToSql for crate::value::SmallDateTime {
389    fn to_sql(&self) -> Result<SqlValue, TypeError> {
390        Ok(SqlValue::SmallDateTime(self.0))
391    }
392
393    fn sql_type(&self) -> &'static str {
394        "SMALLDATETIME"
395    }
396}
397
398#[cfg(feature = "chrono")]
399impl ToSql for chrono::DateTime<chrono::FixedOffset> {
400    fn to_sql(&self) -> Result<SqlValue, TypeError> {
401        Ok(SqlValue::DateTimeOffset(*self))
402    }
403
404    fn sql_type(&self) -> &'static str {
405        "DATETIMEOFFSET"
406    }
407}
408
409#[cfg(feature = "chrono")]
410impl ToSql for chrono::DateTime<chrono::Utc> {
411    fn to_sql(&self) -> Result<SqlValue, TypeError> {
412        // Convert UTC to FixedOffset with +00:00 offset
413        let fixed = self.with_timezone(&chrono::FixedOffset::east_opt(0).expect("valid offset"));
414        Ok(SqlValue::DateTimeOffset(fixed))
415    }
416
417    fn sql_type(&self) -> &'static str {
418        "DATETIMEOFFSET"
419    }
420}
421
422#[cfg(feature = "json")]
423impl ToSql for serde_json::Value {
424    fn to_sql(&self) -> Result<SqlValue, TypeError> {
425        Ok(SqlValue::Json(self.clone()))
426    }
427
428    fn sql_type(&self) -> &'static str {
429        "NVARCHAR(MAX)"
430    }
431}
432
433#[cfg(test)]
434#[allow(clippy::unwrap_used)]
435mod tests {
436    use super::*;
437
438    #[test]
439    fn test_to_sql_i32() {
440        let value: i32 = 42;
441        assert_eq!(value.to_sql().unwrap(), SqlValue::Int(42));
442        assert_eq!(value.sql_type(), "INT");
443    }
444
445    #[test]
446    fn test_typed_null_carries_type() {
447        // A typed NULL is a NULL value that still reports its SQL type, and that
448        // type matches what a value of the same Rust type reports.
449        assert_eq!(null::<i32>().to_sql().unwrap(), SqlValue::Null);
450        assert_eq!(null::<i32>().sql_type(), 42i32.sql_type());
451        assert_eq!(null::<i64>().sql_type(), "BIGINT");
452        assert_eq!(null::<Vec<u8>>().sql_type(), "VARBINARY");
453        assert_eq!(null::<String>().sql_type(), "NVARCHAR");
454    }
455
456    #[test]
457    fn test_to_sql_string() {
458        let value = "hello".to_string();
459        assert_eq!(
460            value.to_sql().unwrap(),
461            SqlValue::String("hello".to_string())
462        );
463        assert_eq!(value.sql_type(), "NVARCHAR");
464    }
465
466    #[test]
467    fn test_to_sql_option() {
468        let some: Option<i32> = Some(42);
469        assert_eq!(some.to_sql().unwrap(), SqlValue::Int(42));
470
471        let none: Option<i32> = None;
472        assert_eq!(none.to_sql().unwrap(), SqlValue::Null);
473    }
474}