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/// The value is rescaled to `scale`, which **rounds** when the value has more
350/// fractional digits than `scale` (e.g. `numeric(dec!(12.999), 18, 2)` stores
351/// `13.00`). If the rescaled value has more significant digits than `precision`,
352/// [`ToSql::to_sql`] returns an error rather than silently storing a value
353/// outside the column's domain — the server cannot range-check an encrypted
354/// value, so the client enforces it.
355#[cfg(feature = "decimal")]
356#[must_use]
357pub fn numeric(value: rust_decimal::Decimal, precision: u8, scale: u8) -> Numeric {
358    Numeric {
359        value,
360        precision,
361        scale,
362    }
363}
364
365#[cfg(feature = "decimal")]
366impl ToSql for Numeric {
367    fn to_sql(&self) -> Result<SqlValue, TypeError> {
368        let mut value = self.value;
369        value.rescale(u32::from(self.scale));
370        // The server cannot range-check an encrypted value, so a value that
371        // exceeds the declared precision must be rejected client-side rather
372        // than silently stored out of the column's domain (matches the
373        // Always Encrypted behaviour of Microsoft.Data.SqlClient). After
374        // rescaling, the magnitude bound `|mantissa| < 10^precision` is exactly
375        // the `decimal(precision, scale)` domain.
376        let mantissa = value.mantissa().unsigned_abs();
377        let digits = if mantissa == 0 {
378            0
379        } else {
380            mantissa.ilog10() + 1
381        };
382        if digits > u32::from(self.precision) {
383            return Err(TypeError::InvalidDecimal(format!(
384                "value has {digits} significant digits, which exceeds the declared precision {}",
385                self.precision
386            )));
387        }
388        Ok(SqlValue::Decimal(value))
389    }
390
391    fn sql_type(&self) -> &'static str {
392        "DECIMAL"
393    }
394
395    fn encrypted_param_type(&self) -> Option<EncryptedParamType> {
396        Some(EncryptedParamType::Decimal {
397            precision: self.precision,
398            scale: self.scale,
399        })
400    }
401}
402
403/// Fractional-second scale for `time`/`datetime2`/`datetimeoffset` is 0–7.
404#[cfg(feature = "chrono")]
405fn validate_temporal_scale(scale: u8) -> Result<(), TypeError> {
406    if scale > 7 {
407        return Err(TypeError::InvalidDateTime(format!(
408            "fractional-second scale {scale} is out of range (0–7)"
409        )));
410    }
411    Ok(())
412}
413
414/// A `time(scale)` parameter for an Always Encrypted column (see [`time`]).
415#[cfg(feature = "chrono")]
416#[derive(Debug, Clone, Copy)]
417pub struct Time {
418    value: chrono::NaiveTime,
419    scale: u8,
420}
421
422/// Create a `time(scale)` parameter for an Always Encrypted `time` column.
423///
424/// AE requires the declared `time(scale)` to match the column exactly, and the
425/// scale also determines the encrypted byte length, so the value alone is
426/// insufficient. `scale` is the fractional-second digits (0–7).
427#[cfg(feature = "chrono")]
428#[must_use]
429pub fn time(value: chrono::NaiveTime, scale: u8) -> Time {
430    Time { value, scale }
431}
432
433#[cfg(feature = "chrono")]
434impl ToSql for Time {
435    fn to_sql(&self) -> Result<SqlValue, TypeError> {
436        validate_temporal_scale(self.scale)?;
437        Ok(SqlValue::Time(self.value))
438    }
439
440    fn sql_type(&self) -> &'static str {
441        "TIME"
442    }
443
444    fn encrypted_param_type(&self) -> Option<EncryptedParamType> {
445        Some(EncryptedParamType::Time { scale: self.scale })
446    }
447}
448
449/// A `datetime2(scale)` parameter for an Always Encrypted column (see [`datetime2`]).
450#[cfg(feature = "chrono")]
451#[derive(Debug, Clone, Copy)]
452pub struct DateTime2 {
453    value: chrono::NaiveDateTime,
454    scale: u8,
455}
456
457/// Create a `datetime2(scale)` parameter for an Always Encrypted `datetime2`
458/// column. A plain `NaiveDateTime` defaults to `datetime2(7)`, so an explicit
459/// scale is required to match a column with a different scale (and to encrypt
460/// at the right byte length). `scale` is the fractional-second digits (0–7).
461#[cfg(feature = "chrono")]
462#[must_use]
463pub fn datetime2(value: chrono::NaiveDateTime, scale: u8) -> DateTime2 {
464    DateTime2 { value, scale }
465}
466
467#[cfg(feature = "chrono")]
468impl ToSql for DateTime2 {
469    fn to_sql(&self) -> Result<SqlValue, TypeError> {
470        validate_temporal_scale(self.scale)?;
471        Ok(SqlValue::DateTime(self.value))
472    }
473
474    fn sql_type(&self) -> &'static str {
475        "DATETIME2"
476    }
477
478    fn encrypted_param_type(&self) -> Option<EncryptedParamType> {
479        Some(EncryptedParamType::DateTime2 { scale: self.scale })
480    }
481}
482
483/// A `datetimeoffset(scale)` parameter for an Always Encrypted column (see
484/// [`datetimeoffset`]).
485#[cfg(feature = "chrono")]
486#[derive(Debug, Clone, Copy)]
487pub struct DateTimeOffset {
488    value: chrono::DateTime<chrono::FixedOffset>,
489    scale: u8,
490}
491
492/// Create a `datetimeoffset(scale)` parameter for an Always Encrypted
493/// `datetimeoffset` column. `scale` is the fractional-second digits (0–7).
494#[cfg(feature = "chrono")]
495#[must_use]
496pub fn datetimeoffset(value: chrono::DateTime<chrono::FixedOffset>, scale: u8) -> DateTimeOffset {
497    DateTimeOffset { value, scale }
498}
499
500#[cfg(feature = "chrono")]
501impl ToSql for DateTimeOffset {
502    fn to_sql(&self) -> Result<SqlValue, TypeError> {
503        validate_temporal_scale(self.scale)?;
504        Ok(SqlValue::DateTimeOffset(self.value))
505    }
506
507    fn sql_type(&self) -> &'static str {
508        "DATETIMEOFFSET"
509    }
510
511    fn encrypted_param_type(&self) -> Option<EncryptedParamType> {
512        Some(EncryptedParamType::DateTimeOffset { scale: self.scale })
513    }
514}
515
516/// A legacy `datetime` parameter for an Always Encrypted column (see [`datetime`]).
517#[cfg(feature = "chrono")]
518#[derive(Debug, Clone, Copy)]
519pub struct DateTimeLegacy {
520    value: chrono::NaiveDateTime,
521}
522
523/// Create a legacy `datetime` parameter for an Always Encrypted `datetime`
524/// column. A plain `NaiveDateTime` defaults to `datetime2`, which an encrypted
525/// legacy `datetime` column rejects; this declares `datetime` explicitly.
526#[cfg(feature = "chrono")]
527#[must_use]
528pub fn datetime(value: chrono::NaiveDateTime) -> DateTimeLegacy {
529    DateTimeLegacy { value }
530}
531
532#[cfg(feature = "chrono")]
533impl ToSql for DateTimeLegacy {
534    fn to_sql(&self) -> Result<SqlValue, TypeError> {
535        Ok(SqlValue::DateTime(self.value))
536    }
537
538    fn sql_type(&self) -> &'static str {
539        "DATETIME"
540    }
541
542    fn encrypted_param_type(&self) -> Option<EncryptedParamType> {
543        Some(EncryptedParamType::DateTime)
544    }
545}
546
547/// A fixed-length `char(length)` parameter for an Always Encrypted column (see [`char()`]).
548#[derive(Debug, Clone)]
549pub struct Char {
550    value: String,
551    length: u16,
552}
553
554/// Create a `char(length)` parameter for an Always Encrypted `char` column.
555///
556/// The encrypted column must use a `*_BIN2` collation (SQL Server requires it
557/// for deterministic encryption of character types). The value is encoded in
558/// the column's code page; only Windows-1252 (the default) is supported.
559#[must_use]
560pub fn char(value: impl Into<String>, length: u16) -> Char {
561    Char {
562        value: value.into(),
563        length,
564    }
565}
566
567impl ToSql for Char {
568    fn to_sql(&self) -> Result<SqlValue, TypeError> {
569        Ok(SqlValue::String(self.value.clone()))
570    }
571
572    fn sql_type(&self) -> &'static str {
573        "CHAR"
574    }
575
576    fn encrypted_param_type(&self) -> Option<EncryptedParamType> {
577        Some(EncryptedParamType::Char {
578            length: self.length,
579        })
580    }
581}
582
583/// A fixed-length `nchar(length)` parameter for an Always Encrypted column (see [`nchar`]).
584#[derive(Debug, Clone)]
585pub struct NChar {
586    value: String,
587    length: u16,
588}
589
590/// Create an `nchar(length)` parameter for an Always Encrypted `nchar` column.
591///
592/// The encrypted column must use a `*_BIN2` collation. The value is encoded as
593/// UTF-16, identically to `nvarchar`.
594#[must_use]
595pub fn nchar(value: impl Into<String>, length: u16) -> NChar {
596    NChar {
597        value: value.into(),
598        length,
599    }
600}
601
602impl ToSql for NChar {
603    fn to_sql(&self) -> Result<SqlValue, TypeError> {
604        Ok(SqlValue::String(self.value.clone()))
605    }
606
607    fn sql_type(&self) -> &'static str {
608        "NCHAR"
609    }
610
611    fn encrypted_param_type(&self) -> Option<EncryptedParamType> {
612        Some(EncryptedParamType::NChar {
613            length: self.length,
614        })
615    }
616}
617
618/// A fixed-length `binary(length)` parameter for an Always Encrypted column (see [`binary`]).
619#[derive(Debug, Clone)]
620pub struct Binary {
621    value: bytes::Bytes,
622    length: u16,
623}
624
625/// Create a `binary(length)` parameter for an Always Encrypted `binary` column.
626#[must_use]
627pub fn binary(value: impl Into<bytes::Bytes>, length: u16) -> Binary {
628    Binary {
629        value: value.into(),
630        length,
631    }
632}
633
634impl ToSql for Binary {
635    fn to_sql(&self) -> Result<SqlValue, TypeError> {
636        Ok(SqlValue::Binary(self.value.clone()))
637    }
638
639    fn sql_type(&self) -> &'static str {
640        "BINARY"
641    }
642
643    fn encrypted_param_type(&self) -> Option<EncryptedParamType> {
644        Some(EncryptedParamType::Binary {
645            length: self.length,
646        })
647    }
648}
649
650#[cfg(feature = "decimal")]
651impl ToSql for crate::value::Money {
652    fn to_sql(&self) -> Result<SqlValue, TypeError> {
653        Ok(SqlValue::Money(self.0))
654    }
655
656    fn sql_type(&self) -> &'static str {
657        "MONEY"
658    }
659}
660
661#[cfg(feature = "decimal")]
662impl ToSql for crate::value::SmallMoney {
663    fn to_sql(&self) -> Result<SqlValue, TypeError> {
664        Ok(SqlValue::SmallMoney(self.0))
665    }
666
667    fn sql_type(&self) -> &'static str {
668        "SMALLMONEY"
669    }
670}
671
672#[cfg(feature = "chrono")]
673impl ToSql for chrono::NaiveDate {
674    fn to_sql(&self) -> Result<SqlValue, TypeError> {
675        Ok(SqlValue::Date(*self))
676    }
677
678    fn sql_type(&self) -> &'static str {
679        "DATE"
680    }
681}
682
683#[cfg(feature = "chrono")]
684impl ToSql for chrono::NaiveTime {
685    fn to_sql(&self) -> Result<SqlValue, TypeError> {
686        Ok(SqlValue::Time(*self))
687    }
688
689    fn sql_type(&self) -> &'static str {
690        "TIME"
691    }
692}
693
694#[cfg(feature = "chrono")]
695impl ToSql for chrono::NaiveDateTime {
696    fn to_sql(&self) -> Result<SqlValue, TypeError> {
697        Ok(SqlValue::DateTime(*self))
698    }
699
700    fn sql_type(&self) -> &'static str {
701        "DATETIME2"
702    }
703}
704
705#[cfg(feature = "chrono")]
706impl ToSql for crate::value::SmallDateTime {
707    fn to_sql(&self) -> Result<SqlValue, TypeError> {
708        Ok(SqlValue::SmallDateTime(self.0))
709    }
710
711    fn sql_type(&self) -> &'static str {
712        "SMALLDATETIME"
713    }
714}
715
716#[cfg(feature = "chrono")]
717impl ToSql for chrono::DateTime<chrono::FixedOffset> {
718    fn to_sql(&self) -> Result<SqlValue, TypeError> {
719        Ok(SqlValue::DateTimeOffset(*self))
720    }
721
722    fn sql_type(&self) -> &'static str {
723        "DATETIMEOFFSET"
724    }
725}
726
727#[cfg(feature = "chrono")]
728impl ToSql for chrono::DateTime<chrono::Utc> {
729    fn to_sql(&self) -> Result<SqlValue, TypeError> {
730        // Convert UTC to FixedOffset with +00:00 offset
731        let fixed = self.with_timezone(&chrono::FixedOffset::east_opt(0).expect("valid offset"));
732        Ok(SqlValue::DateTimeOffset(fixed))
733    }
734
735    fn sql_type(&self) -> &'static str {
736        "DATETIMEOFFSET"
737    }
738}
739
740#[cfg(feature = "json")]
741impl ToSql for serde_json::Value {
742    fn to_sql(&self) -> Result<SqlValue, TypeError> {
743        Ok(SqlValue::Json(self.clone()))
744    }
745
746    fn sql_type(&self) -> &'static str {
747        "NVARCHAR(MAX)"
748    }
749}
750
751#[cfg(test)]
752#[allow(clippy::unwrap_used)]
753mod tests {
754    use super::*;
755
756    #[test]
757    fn test_to_sql_i32() {
758        let value: i32 = 42;
759        assert_eq!(value.to_sql().unwrap(), SqlValue::Int(42));
760        assert_eq!(value.sql_type(), "INT");
761    }
762
763    #[test]
764    fn test_typed_null_carries_type() {
765        // A typed NULL is a NULL value that still reports its SQL type, and that
766        // type matches what a value of the same Rust type reports.
767        assert_eq!(null::<i32>().to_sql().unwrap(), SqlValue::Null);
768        assert_eq!(null::<i32>().sql_type(), 42i32.sql_type());
769        assert_eq!(null::<i64>().sql_type(), "BIGINT");
770        assert_eq!(null::<Vec<u8>>().sql_type(), "VARBINARY");
771        assert_eq!(null::<String>().sql_type(), "NVARCHAR");
772    }
773
774    #[test]
775    fn test_to_sql_string() {
776        let value = "hello".to_string();
777        assert_eq!(
778            value.to_sql().unwrap(),
779            SqlValue::String("hello".to_string())
780        );
781        assert_eq!(value.sql_type(), "NVARCHAR");
782    }
783
784    #[test]
785    fn test_to_sql_option() {
786        let some: Option<i32> = Some(42);
787        assert_eq!(some.to_sql().unwrap(), SqlValue::Int(42));
788
789        let none: Option<i32> = None;
790        assert_eq!(none.to_sql().unwrap(), SqlValue::Null);
791    }
792
793    #[cfg(feature = "decimal")]
794    #[test]
795    fn test_numeric_precision_validation() {
796        use rust_decimal::Decimal;
797
798        // Fits: a scale-2 value declared decimal(18,4) rescales without loss.
799        assert!(numeric(Decimal::new(1_234_567, 2), 18, 4).to_sql().is_ok());
800
801        // Exceeds declared precision: 6 significant digits into decimal(4,0).
802        assert!(
803            numeric(Decimal::new(123_456, 0), 4, 0).to_sql().is_err(),
804            "value exceeding the declared precision must error"
805        );
806
807        // Rounds (does not error) when the value scale exceeds the declared scale.
808        let rounded = numeric(Decimal::new(12_999, 3), 18, 2).to_sql().unwrap();
809        assert_eq!(rounded, SqlValue::Decimal(Decimal::new(1_300, 2)));
810
811        // Zero fits any precision.
812        assert!(numeric(Decimal::ZERO, 1, 0).to_sql().is_ok());
813    }
814}