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    /// The explicit SQL type a parameter must be declared and encrypted as,
21    /// when the value alone cannot convey it.
22    ///
23    /// Returns `None` for every type except the typed-parameter wrappers
24    /// (e.g. [`numeric`], [`datetime2`], [`time`]). An Always Encrypted column
25    /// requires the declared type — including precision, scale, or length — to
26    /// match the column exactly, which a bare value cannot always express (a
27    /// `Decimal` carries no precision; a `NaiveDateTime` is ambiguous between
28    /// `datetime` and `datetime2(n)`). The driver uses this to declare the
29    /// parameter for `sp_describe_parameter_encryption` and to normalize the
30    /// value before encryption.
31    fn encrypted_param_type(&self) -> Option<EncryptedParamType> {
32        None
33    }
34}
35
36/// The explicit SQL type for an Always Encrypted parameter whose value cannot
37/// convey it (see [`numeric`], [`time`], [`datetime2`], [`datetimeoffset`],
38/// [`datetime`]). Carries the precision/scale/length the encrypted column
39/// requires the declared parameter type to match exactly.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41#[non_exhaustive]
42pub enum EncryptedParamType {
43    /// `decimal(precision, scale)`.
44    Decimal {
45        /// Total number of significant digits (1–38).
46        precision: u8,
47        /// Number of digits to the right of the decimal point.
48        scale: u8,
49    },
50    /// `time(scale)`.
51    Time {
52        /// Fractional-second digits (0–7).
53        scale: u8,
54    },
55    /// `datetime2(scale)`.
56    DateTime2 {
57        /// Fractional-second digits (0–7).
58        scale: u8,
59    },
60    /// `datetimeoffset(scale)`.
61    DateTimeOffset {
62        /// Fractional-second digits (0–7).
63        scale: u8,
64    },
65    /// Legacy `datetime` (8 bytes; ~3.33 ms resolution).
66    DateTime,
67    /// Fixed-length `char(length)` (length in bytes; the encrypted column must
68    /// use a `*_BIN2` collation, and the value is encoded in its code page).
69    Char {
70        /// Declared length in bytes.
71        length: u16,
72    },
73    /// Fixed-length `nchar(length)` (length in UTF-16 characters; the encrypted
74    /// column must use a `*_BIN2` collation).
75    NChar {
76        /// Declared length in characters.
77        length: u16,
78    },
79    /// Fixed-length `binary(length)` (length in bytes).
80    Binary {
81        /// Declared length in bytes.
82        length: u16,
83    },
84}
85
86impl ToSql for bool {
87    fn to_sql(&self) -> Result<SqlValue, TypeError> {
88        Ok(SqlValue::Bool(*self))
89    }
90
91    fn sql_type(&self) -> &'static str {
92        "BIT"
93    }
94}
95
96impl ToSql for u8 {
97    fn to_sql(&self) -> Result<SqlValue, TypeError> {
98        Ok(SqlValue::TinyInt(*self))
99    }
100
101    fn sql_type(&self) -> &'static str {
102        "TINYINT"
103    }
104}
105
106impl ToSql for i16 {
107    fn to_sql(&self) -> Result<SqlValue, TypeError> {
108        Ok(SqlValue::SmallInt(*self))
109    }
110
111    fn sql_type(&self) -> &'static str {
112        "SMALLINT"
113    }
114}
115
116impl ToSql for i32 {
117    fn to_sql(&self) -> Result<SqlValue, TypeError> {
118        Ok(SqlValue::Int(*self))
119    }
120
121    fn sql_type(&self) -> &'static str {
122        "INT"
123    }
124}
125
126impl ToSql for i64 {
127    fn to_sql(&self) -> Result<SqlValue, TypeError> {
128        Ok(SqlValue::BigInt(*self))
129    }
130
131    fn sql_type(&self) -> &'static str {
132        "BIGINT"
133    }
134}
135
136impl ToSql for f32 {
137    fn to_sql(&self) -> Result<SqlValue, TypeError> {
138        Ok(SqlValue::Float(*self))
139    }
140
141    fn sql_type(&self) -> &'static str {
142        "REAL"
143    }
144}
145
146impl ToSql for f64 {
147    fn to_sql(&self) -> Result<SqlValue, TypeError> {
148        Ok(SqlValue::Double(*self))
149    }
150
151    fn sql_type(&self) -> &'static str {
152        "FLOAT"
153    }
154}
155
156impl ToSql for str {
157    fn to_sql(&self) -> Result<SqlValue, TypeError> {
158        Ok(SqlValue::String(self.to_owned()))
159    }
160
161    fn sql_type(&self) -> &'static str {
162        "NVARCHAR"
163    }
164}
165
166impl ToSql for String {
167    fn to_sql(&self) -> Result<SqlValue, TypeError> {
168        Ok(SqlValue::String(self.clone()))
169    }
170
171    fn sql_type(&self) -> &'static str {
172        "NVARCHAR"
173    }
174}
175
176impl ToSql for [u8] {
177    fn to_sql(&self) -> Result<SqlValue, TypeError> {
178        Ok(SqlValue::Binary(bytes::Bytes::copy_from_slice(self)))
179    }
180
181    fn sql_type(&self) -> &'static str {
182        "VARBINARY"
183    }
184}
185
186impl ToSql for Vec<u8> {
187    fn to_sql(&self) -> Result<SqlValue, TypeError> {
188        Ok(SqlValue::Binary(bytes::Bytes::copy_from_slice(self)))
189    }
190
191    fn sql_type(&self) -> &'static str {
192        "VARBINARY"
193    }
194}
195
196/// Associates a Rust type with its SQL type name so a typed NULL can be
197/// declared without a value (see [`null`]).
198///
199/// `SQL_TYPE` must match what [`ToSql::sql_type`] returns for a value of the
200/// same type.
201pub trait SqlTyped {
202    /// The SQL type name for this Rust type.
203    const SQL_TYPE: &'static str;
204}
205
206impl SqlTyped for bool {
207    const SQL_TYPE: &'static str = "BIT";
208}
209impl SqlTyped for u8 {
210    const SQL_TYPE: &'static str = "TINYINT";
211}
212impl SqlTyped for i16 {
213    const SQL_TYPE: &'static str = "SMALLINT";
214}
215impl SqlTyped for i32 {
216    const SQL_TYPE: &'static str = "INT";
217}
218impl SqlTyped for i64 {
219    const SQL_TYPE: &'static str = "BIGINT";
220}
221impl SqlTyped for f32 {
222    const SQL_TYPE: &'static str = "REAL";
223}
224impl SqlTyped for f64 {
225    const SQL_TYPE: &'static str = "FLOAT";
226}
227impl SqlTyped for String {
228    const SQL_TYPE: &'static str = "NVARCHAR";
229}
230impl SqlTyped for Vec<u8> {
231    const SQL_TYPE: &'static str = "VARBINARY";
232}
233#[cfg(feature = "uuid")]
234impl SqlTyped for uuid::Uuid {
235    const SQL_TYPE: &'static str = "UNIQUEIDENTIFIER";
236}
237#[cfg(feature = "chrono")]
238impl SqlTyped for chrono::NaiveDate {
239    const SQL_TYPE: &'static str = "DATE";
240}
241
242/// A typed NULL parameter, created with [`null`].
243///
244/// Unlike `Option::<T>::None`, which produces an untyped NULL declared as
245/// `nvarchar(1)`, this carries its SQL type. That matters for Always Encrypted
246/// columns, whose strict typing rejects an untyped NULL bound to, for example,
247/// an `int` or `varbinary` column.
248#[derive(Debug, Clone, Copy)]
249pub struct TypedNull {
250    sql_type: &'static str,
251}
252
253impl ToSql for TypedNull {
254    fn to_sql(&self) -> Result<SqlValue, TypeError> {
255        Ok(SqlValue::Null)
256    }
257
258    fn sql_type(&self) -> &'static str {
259        self.sql_type
260    }
261}
262
263/// Create a typed NULL parameter for SQL type `T`, e.g. `null::<i32>()`.
264///
265/// Use this in place of `Option::<T>::None` when binding NULL to a strongly
266/// typed column — required for an Always Encrypted column of a non-string type.
267#[must_use]
268pub fn null<T: SqlTyped>() -> TypedNull {
269    TypedNull {
270        sql_type: T::SQL_TYPE,
271    }
272}
273
274impl<T: ToSql> ToSql for Option<T> {
275    fn to_sql(&self) -> Result<SqlValue, TypeError> {
276        match self {
277            Some(v) => v.to_sql(),
278            None => Ok(SqlValue::Null),
279        }
280    }
281
282    fn sql_type(&self) -> &'static str {
283        match self {
284            Some(v) => v.sql_type(),
285            None => "NULL",
286        }
287    }
288
289    fn encrypted_param_type(&self) -> Option<EncryptedParamType> {
290        self.as_ref().and_then(ToSql::encrypted_param_type)
291    }
292}
293
294impl<T: ToSql + ?Sized> ToSql for &T {
295    fn to_sql(&self) -> Result<SqlValue, TypeError> {
296        (*self).to_sql()
297    }
298
299    fn sql_type(&self) -> &'static str {
300        (*self).sql_type()
301    }
302
303    fn encrypted_param_type(&self) -> Option<EncryptedParamType> {
304        (*self).encrypted_param_type()
305    }
306}
307
308#[cfg(feature = "uuid")]
309impl ToSql for uuid::Uuid {
310    fn to_sql(&self) -> Result<SqlValue, TypeError> {
311        Ok(SqlValue::Uuid(*self))
312    }
313
314    fn sql_type(&self) -> &'static str {
315        "UNIQUEIDENTIFIER"
316    }
317}
318
319#[cfg(feature = "decimal")]
320impl ToSql for rust_decimal::Decimal {
321    fn to_sql(&self) -> Result<SqlValue, TypeError> {
322        Ok(SqlValue::Decimal(*self))
323    }
324
325    fn sql_type(&self) -> &'static str {
326        "DECIMAL"
327    }
328}
329
330/// A `decimal`/`numeric` parameter with explicit precision and scale.
331///
332/// A plain [`rust_decimal::Decimal`] carries scale but not precision, so it
333/// cannot be matched against an Always Encrypted `decimal` column, whose
334/// declared `decimal(precision, scale)` must match the column exactly.
335/// Construct one with [`numeric`].
336#[cfg(feature = "decimal")]
337#[derive(Debug, Clone, Copy)]
338pub struct Numeric {
339    value: rust_decimal::Decimal,
340    precision: u8,
341    scale: u8,
342}
343
344/// Create a `decimal`/`numeric` parameter with explicit precision and scale.
345///
346/// Required when binding to an Always Encrypted `decimal` column, whose declared
347/// `decimal(precision, scale)` must match the column exactly.
348///
349/// `numeric(value, precision, scale)` **asserts that `value` fits the
350/// `decimal(precision, scale)` domain**, and [`ToSql::to_sql`] enforces that
351/// assertion on every path — encrypted or plaintext (it is the caller's
352/// declared domain, not a property of the connection). It errors when:
353///
354/// - `precision` is outside `1..=38`, or `scale > precision`;
355/// - `scale > 28` — [`rust_decimal::Decimal`] caps its backing scale at 28, so a larger
356///   declared scale cannot be represented and, on the Always Encrypted path,
357///   would emit a magnitude a Microsoft client reads at the wrong scale (e.g.
358///   `0.5` → `0.005`) with no server backstop. The driver's `decimal` Always
359///   Encrypted support is therefore bounded to `scale ≤ 28`;
360/// - the value, after rescaling, has more significant digits than `precision`.
361///
362/// The value is rescaled to `scale`, which **rounds** when the value has more
363/// fractional digits than `scale` (e.g. `numeric(dec!(12.999), 18, 2)` stores
364/// `13.00`).
365#[cfg(feature = "decimal")]
366#[must_use]
367pub fn numeric(value: rust_decimal::Decimal, precision: u8, scale: u8) -> Numeric {
368    Numeric {
369        value,
370        precision,
371        scale,
372    }
373}
374
375#[cfg(feature = "decimal")]
376impl ToSql for Numeric {
377    fn to_sql(&self) -> Result<SqlValue, TypeError> {
378        // Validate the declared `decimal(precision, scale)` domain before
379        // rescaling. `numeric(v, p, s)` asserts that `v` fits `decimal(p, s)`;
380        // this contract holds on both the Always Encrypted path (where the
381        // declaration must match the column and the server cannot range-check
382        // an encrypted value) and the plaintext path (#288). The scale ≤ 28
383        // bound is load-bearing: rust_decimal caps its backing scale at 28, so
384        // a declared scale > 28 would be silently capped by `rescale` and emit
385        // a magnitude a Microsoft client reads at the wrong scale (0.5 → 0.005)
386        // — silent Always Encrypted corruption with no server backstop.
387        if self.precision < 1 || self.precision > 38 {
388            return Err(TypeError::InvalidDecimal(format!(
389                "precision {} is out of range (1–38)",
390                self.precision
391            )));
392        }
393        if self.scale > self.precision {
394            return Err(TypeError::InvalidDecimal(format!(
395                "scale {} exceeds precision {}",
396                self.scale, self.precision
397            )));
398        }
399        if self.scale > 28 {
400            return Err(TypeError::InvalidDecimal(format!(
401                "scale {} is out of range (max 28: rust_decimal cannot represent more fractional digits)",
402                self.scale
403            )));
404        }
405
406        let mut value = self.value;
407        value.rescale(u32::from(self.scale));
408        // The server cannot range-check an encrypted value, so a value that
409        // exceeds the declared precision must be rejected client-side rather
410        // than silently stored out of the column's domain (matches the
411        // Always Encrypted behaviour of Microsoft.Data.SqlClient). After
412        // rescaling, the magnitude bound `|mantissa| < 10^precision` is exactly
413        // the `decimal(precision, scale)` domain.
414        let mantissa = value.mantissa().unsigned_abs();
415        let digits = if mantissa == 0 {
416            0
417        } else {
418            mantissa.ilog10() + 1
419        };
420        if digits > u32::from(self.precision) {
421            return Err(TypeError::InvalidDecimal(format!(
422                "value has {digits} significant digits, which exceeds the declared precision {}",
423                self.precision
424            )));
425        }
426        Ok(SqlValue::Decimal(value))
427    }
428
429    fn sql_type(&self) -> &'static str {
430        "DECIMAL"
431    }
432
433    fn encrypted_param_type(&self) -> Option<EncryptedParamType> {
434        Some(EncryptedParamType::Decimal {
435            precision: self.precision,
436            scale: self.scale,
437        })
438    }
439}
440
441/// Fractional-second scale for `time`/`datetime2`/`datetimeoffset` is 0–7.
442#[cfg(feature = "chrono")]
443fn validate_temporal_scale(scale: u8) -> Result<(), TypeError> {
444    if scale > 7 {
445        return Err(TypeError::InvalidDateTime(format!(
446            "fractional-second scale {scale} is out of range (0–7)"
447        )));
448    }
449    Ok(())
450}
451
452/// A `time(scale)` parameter for an Always Encrypted column (see [`time`]).
453#[cfg(feature = "chrono")]
454#[derive(Debug, Clone, Copy)]
455pub struct Time {
456    value: chrono::NaiveTime,
457    scale: u8,
458}
459
460/// Create a `time(scale)` parameter for an Always Encrypted `time` column.
461///
462/// AE requires the declared `time(scale)` to match the column exactly, and the
463/// scale also determines the encrypted byte length, so the value alone is
464/// insufficient. `scale` is the fractional-second digits (0–7).
465#[cfg(feature = "chrono")]
466#[must_use]
467pub fn time(value: chrono::NaiveTime, scale: u8) -> Time {
468    Time { value, scale }
469}
470
471#[cfg(feature = "chrono")]
472impl ToSql for Time {
473    fn to_sql(&self) -> Result<SqlValue, TypeError> {
474        validate_temporal_scale(self.scale)?;
475        Ok(SqlValue::Time(self.value))
476    }
477
478    fn sql_type(&self) -> &'static str {
479        "TIME"
480    }
481
482    fn encrypted_param_type(&self) -> Option<EncryptedParamType> {
483        Some(EncryptedParamType::Time { scale: self.scale })
484    }
485}
486
487/// A `datetime2(scale)` parameter for an Always Encrypted column (see [`datetime2`]).
488#[cfg(feature = "chrono")]
489#[derive(Debug, Clone, Copy)]
490pub struct DateTime2 {
491    value: chrono::NaiveDateTime,
492    scale: u8,
493}
494
495/// Create a `datetime2(scale)` parameter for an Always Encrypted `datetime2`
496/// column. A plain `NaiveDateTime` defaults to `datetime2(7)`, so an explicit
497/// scale is required to match a column with a different scale (and to encrypt
498/// at the right byte length). `scale` is the fractional-second digits (0–7).
499#[cfg(feature = "chrono")]
500#[must_use]
501pub fn datetime2(value: chrono::NaiveDateTime, scale: u8) -> DateTime2 {
502    DateTime2 { value, scale }
503}
504
505#[cfg(feature = "chrono")]
506impl ToSql for DateTime2 {
507    fn to_sql(&self) -> Result<SqlValue, TypeError> {
508        validate_temporal_scale(self.scale)?;
509        Ok(SqlValue::DateTime(self.value))
510    }
511
512    fn sql_type(&self) -> &'static str {
513        "DATETIME2"
514    }
515
516    fn encrypted_param_type(&self) -> Option<EncryptedParamType> {
517        Some(EncryptedParamType::DateTime2 { scale: self.scale })
518    }
519}
520
521/// A `datetimeoffset(scale)` parameter for an Always Encrypted column (see
522/// [`datetimeoffset`]).
523#[cfg(feature = "chrono")]
524#[derive(Debug, Clone, Copy)]
525pub struct DateTimeOffset {
526    value: chrono::DateTime<chrono::FixedOffset>,
527    scale: u8,
528}
529
530/// Create a `datetimeoffset(scale)` parameter for an Always Encrypted
531/// `datetimeoffset` column. `scale` is the fractional-second digits (0–7).
532#[cfg(feature = "chrono")]
533#[must_use]
534pub fn datetimeoffset(value: chrono::DateTime<chrono::FixedOffset>, scale: u8) -> DateTimeOffset {
535    DateTimeOffset { value, scale }
536}
537
538#[cfg(feature = "chrono")]
539impl ToSql for DateTimeOffset {
540    fn to_sql(&self) -> Result<SqlValue, TypeError> {
541        validate_temporal_scale(self.scale)?;
542        Ok(SqlValue::DateTimeOffset(self.value))
543    }
544
545    fn sql_type(&self) -> &'static str {
546        "DATETIMEOFFSET"
547    }
548
549    fn encrypted_param_type(&self) -> Option<EncryptedParamType> {
550        Some(EncryptedParamType::DateTimeOffset { scale: self.scale })
551    }
552}
553
554/// A legacy `datetime` parameter for an Always Encrypted column (see [`datetime`]).
555#[cfg(feature = "chrono")]
556#[derive(Debug, Clone, Copy)]
557pub struct DateTimeLegacy {
558    value: chrono::NaiveDateTime,
559}
560
561/// Create a legacy `datetime` parameter for an Always Encrypted `datetime`
562/// column. A plain `NaiveDateTime` defaults to `datetime2`, which an encrypted
563/// legacy `datetime` column rejects; this declares `datetime` explicitly.
564#[cfg(feature = "chrono")]
565#[must_use]
566pub fn datetime(value: chrono::NaiveDateTime) -> DateTimeLegacy {
567    DateTimeLegacy { value }
568}
569
570#[cfg(feature = "chrono")]
571impl ToSql for DateTimeLegacy {
572    fn to_sql(&self) -> Result<SqlValue, TypeError> {
573        Ok(SqlValue::DateTime(self.value))
574    }
575
576    fn sql_type(&self) -> &'static str {
577        "DATETIME"
578    }
579
580    fn encrypted_param_type(&self) -> Option<EncryptedParamType> {
581        Some(EncryptedParamType::DateTime)
582    }
583}
584
585/// A fixed-length `char(length)` parameter for an Always Encrypted column (see [`char()`]).
586#[derive(Debug, Clone)]
587pub struct Char {
588    value: String,
589    length: u16,
590}
591
592/// Create a `char(length)` parameter for an Always Encrypted `char` column.
593///
594/// The encrypted column must use a `*_BIN2` collation (SQL Server requires it
595/// for deterministic encryption of character types). The value is encoded in
596/// the column's code page; only Windows-1252 (the default) is supported.
597#[must_use]
598pub fn char(value: impl Into<String>, length: u16) -> Char {
599    Char {
600        value: value.into(),
601        length,
602    }
603}
604
605impl ToSql for Char {
606    fn to_sql(&self) -> Result<SqlValue, TypeError> {
607        Ok(SqlValue::String(self.value.clone()))
608    }
609
610    fn sql_type(&self) -> &'static str {
611        "CHAR"
612    }
613
614    fn encrypted_param_type(&self) -> Option<EncryptedParamType> {
615        Some(EncryptedParamType::Char {
616            length: self.length,
617        })
618    }
619}
620
621/// A fixed-length `nchar(length)` parameter for an Always Encrypted column (see [`nchar`]).
622#[derive(Debug, Clone)]
623pub struct NChar {
624    value: String,
625    length: u16,
626}
627
628/// Create an `nchar(length)` parameter for an Always Encrypted `nchar` column.
629///
630/// The encrypted column must use a `*_BIN2` collation. The value is encoded as
631/// UTF-16, identically to `nvarchar`.
632#[must_use]
633pub fn nchar(value: impl Into<String>, length: u16) -> NChar {
634    NChar {
635        value: value.into(),
636        length,
637    }
638}
639
640impl ToSql for NChar {
641    fn to_sql(&self) -> Result<SqlValue, TypeError> {
642        Ok(SqlValue::String(self.value.clone()))
643    }
644
645    fn sql_type(&self) -> &'static str {
646        "NCHAR"
647    }
648
649    fn encrypted_param_type(&self) -> Option<EncryptedParamType> {
650        Some(EncryptedParamType::NChar {
651            length: self.length,
652        })
653    }
654}
655
656/// A fixed-length `binary(length)` parameter for an Always Encrypted column (see [`binary`]).
657#[derive(Debug, Clone)]
658pub struct Binary {
659    value: bytes::Bytes,
660    length: u16,
661}
662
663/// Create a `binary(length)` parameter for an Always Encrypted `binary` column.
664#[must_use]
665pub fn binary(value: impl Into<bytes::Bytes>, length: u16) -> Binary {
666    Binary {
667        value: value.into(),
668        length,
669    }
670}
671
672impl ToSql for Binary {
673    fn to_sql(&self) -> Result<SqlValue, TypeError> {
674        Ok(SqlValue::Binary(self.value.clone()))
675    }
676
677    fn sql_type(&self) -> &'static str {
678        "BINARY"
679    }
680
681    fn encrypted_param_type(&self) -> Option<EncryptedParamType> {
682        Some(EncryptedParamType::Binary {
683            length: self.length,
684        })
685    }
686}
687
688#[cfg(feature = "decimal")]
689impl ToSql for crate::value::Money {
690    fn to_sql(&self) -> Result<SqlValue, TypeError> {
691        Ok(SqlValue::Money(self.0))
692    }
693
694    fn sql_type(&self) -> &'static str {
695        "MONEY"
696    }
697}
698
699#[cfg(feature = "decimal")]
700impl ToSql for crate::value::SmallMoney {
701    fn to_sql(&self) -> Result<SqlValue, TypeError> {
702        Ok(SqlValue::SmallMoney(self.0))
703    }
704
705    fn sql_type(&self) -> &'static str {
706        "SMALLMONEY"
707    }
708}
709
710#[cfg(feature = "chrono")]
711impl ToSql for chrono::NaiveDate {
712    fn to_sql(&self) -> Result<SqlValue, TypeError> {
713        Ok(SqlValue::Date(*self))
714    }
715
716    fn sql_type(&self) -> &'static str {
717        "DATE"
718    }
719}
720
721#[cfg(feature = "chrono")]
722impl ToSql for chrono::NaiveTime {
723    fn to_sql(&self) -> Result<SqlValue, TypeError> {
724        Ok(SqlValue::Time(*self))
725    }
726
727    fn sql_type(&self) -> &'static str {
728        "TIME"
729    }
730}
731
732#[cfg(feature = "chrono")]
733impl ToSql for chrono::NaiveDateTime {
734    fn to_sql(&self) -> Result<SqlValue, TypeError> {
735        Ok(SqlValue::DateTime(*self))
736    }
737
738    fn sql_type(&self) -> &'static str {
739        "DATETIME2"
740    }
741}
742
743#[cfg(feature = "chrono")]
744impl ToSql for crate::value::SmallDateTime {
745    fn to_sql(&self) -> Result<SqlValue, TypeError> {
746        Ok(SqlValue::SmallDateTime(self.0))
747    }
748
749    fn sql_type(&self) -> &'static str {
750        "SMALLDATETIME"
751    }
752}
753
754#[cfg(feature = "chrono")]
755impl ToSql for chrono::DateTime<chrono::FixedOffset> {
756    fn to_sql(&self) -> Result<SqlValue, TypeError> {
757        Ok(SqlValue::DateTimeOffset(*self))
758    }
759
760    fn sql_type(&self) -> &'static str {
761        "DATETIMEOFFSET"
762    }
763}
764
765#[cfg(feature = "chrono")]
766impl ToSql for chrono::DateTime<chrono::Utc> {
767    fn to_sql(&self) -> Result<SqlValue, TypeError> {
768        // Convert UTC to FixedOffset with +00:00 offset
769        let fixed = self.with_timezone(&chrono::FixedOffset::east_opt(0).expect("valid offset"));
770        Ok(SqlValue::DateTimeOffset(fixed))
771    }
772
773    fn sql_type(&self) -> &'static str {
774        "DATETIMEOFFSET"
775    }
776}
777
778#[cfg(feature = "json")]
779impl ToSql for serde_json::Value {
780    fn to_sql(&self) -> Result<SqlValue, TypeError> {
781        Ok(SqlValue::Json(self.clone()))
782    }
783
784    fn sql_type(&self) -> &'static str {
785        "NVARCHAR(MAX)"
786    }
787}
788
789#[cfg(test)]
790#[allow(clippy::unwrap_used)]
791mod tests {
792    use super::*;
793
794    #[test]
795    fn test_to_sql_i32() {
796        let value: i32 = 42;
797        assert_eq!(value.to_sql().unwrap(), SqlValue::Int(42));
798        assert_eq!(value.sql_type(), "INT");
799    }
800
801    #[test]
802    fn test_typed_null_carries_type() {
803        // A typed NULL is a NULL value that still reports its SQL type, and that
804        // type matches what a value of the same Rust type reports.
805        assert_eq!(null::<i32>().to_sql().unwrap(), SqlValue::Null);
806        assert_eq!(null::<i32>().sql_type(), 42i32.sql_type());
807        assert_eq!(null::<i64>().sql_type(), "BIGINT");
808        assert_eq!(null::<Vec<u8>>().sql_type(), "VARBINARY");
809        assert_eq!(null::<String>().sql_type(), "NVARCHAR");
810    }
811
812    #[test]
813    fn test_to_sql_string() {
814        let value = "hello".to_string();
815        assert_eq!(
816            value.to_sql().unwrap(),
817            SqlValue::String("hello".to_string())
818        );
819        assert_eq!(value.sql_type(), "NVARCHAR");
820    }
821
822    #[test]
823    fn test_to_sql_option() {
824        let some: Option<i32> = Some(42);
825        assert_eq!(some.to_sql().unwrap(), SqlValue::Int(42));
826
827        let none: Option<i32> = None;
828        assert_eq!(none.to_sql().unwrap(), SqlValue::Null);
829    }
830
831    #[cfg(feature = "decimal")]
832    #[test]
833    fn test_numeric_precision_validation() {
834        use rust_decimal::Decimal;
835
836        // Fits: a scale-2 value declared decimal(18,4) rescales without loss.
837        assert!(numeric(Decimal::new(1_234_567, 2), 18, 4).to_sql().is_ok());
838
839        // Exceeds declared precision: 6 significant digits into decimal(4,0).
840        assert!(
841            numeric(Decimal::new(123_456, 0), 4, 0).to_sql().is_err(),
842            "value exceeding the declared precision must error"
843        );
844
845        // Rounds (does not error) when the value scale exceeds the declared scale.
846        let rounded = numeric(Decimal::new(12_999, 3), 18, 2).to_sql().unwrap();
847        assert_eq!(rounded, SqlValue::Decimal(Decimal::new(1_300, 2)));
848
849        // Zero fits any precision.
850        assert!(numeric(Decimal::ZERO, 1, 0).to_sql().is_ok());
851    }
852
853    #[cfg(feature = "decimal")]
854    #[test]
855    fn test_numeric_rejects_scale_above_28() {
856        use rust_decimal::Decimal;
857
858        // rust_decimal caps the backing scale at 28. A column declared
859        // decimal(38,30) would have a Microsoft client read the scale-28
860        // magnitude at scale 30 — silently turning 0.5 into 0.005 — with no
861        // server backstop (the parameter declaration is correct; only the
862        // encrypted bytes are wrong). The wrapper must reject scale > 28
863        // rather than emit unreadable ciphertext.
864        assert!(
865            numeric(Decimal::new(5, 1), 38, 30).to_sql().is_err(),
866            "scale > 28 must be rejected (rust_decimal cannot represent it)"
867        );
868        // The 28 boundary is still accepted (rust_decimal can hold it).
869        assert!(numeric(Decimal::new(5, 1), 38, 28).to_sql().is_ok());
870    }
871
872    #[cfg(feature = "decimal")]
873    #[test]
874    fn test_numeric_rejects_out_of_range_precision_and_scale() {
875        use rust_decimal::Decimal;
876
877        // #288: the precision/scale contract is global — `to_sql()` here runs
878        // with no encryption context (the plaintext path), and it still asserts
879        // the caller-declared decimal(p, s) domain. precision must be 1..=38.
880        assert!(numeric(Decimal::ONE, 0, 0).to_sql().is_err(), "precision 0");
881        assert!(
882            numeric(Decimal::ONE, 39, 0).to_sql().is_err(),
883            "precision > 38"
884        );
885        // scale must not exceed precision.
886        assert!(
887            numeric(Decimal::new(1, 2), 1, 2).to_sql().is_err(),
888            "scale > precision"
889        );
890    }
891}