1#![allow(clippy::expect_used)]
5
6use crate::error::TypeError;
7use crate::value::SqlValue;
8
9pub trait ToSql {
14 fn to_sql(&self) -> Result<SqlValue, TypeError>;
16
17 fn sql_type(&self) -> &'static str;
19
20 fn encrypted_param_type(&self) -> Option<EncryptedParamType> {
32 None
33 }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41#[non_exhaustive]
42pub enum EncryptedParamType {
43 Decimal {
45 precision: u8,
47 scale: u8,
49 },
50 Time {
52 scale: u8,
54 },
55 DateTime2 {
57 scale: u8,
59 },
60 DateTimeOffset {
62 scale: u8,
64 },
65 DateTime,
67 Char {
70 length: u16,
72 },
73 NChar {
76 length: u16,
78 },
79 Binary {
81 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
196pub trait SqlTyped {
202 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#[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#[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#[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#[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 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 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#[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#[cfg(feature = "chrono")]
454#[derive(Debug, Clone, Copy)]
455pub struct Time {
456 value: chrono::NaiveTime,
457 scale: u8,
458}
459
460#[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#[cfg(feature = "chrono")]
489#[derive(Debug, Clone, Copy)]
490pub struct DateTime2 {
491 value: chrono::NaiveDateTime,
492 scale: u8,
493}
494
495#[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#[cfg(feature = "chrono")]
524#[derive(Debug, Clone, Copy)]
525pub struct DateTimeOffset {
526 value: chrono::DateTime<chrono::FixedOffset>,
527 scale: u8,
528}
529
530#[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#[cfg(feature = "chrono")]
556#[derive(Debug, Clone, Copy)]
557pub struct DateTimeLegacy {
558 value: chrono::NaiveDateTime,
559}
560
561#[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#[derive(Debug, Clone)]
587pub struct Char {
588 value: String,
589 length: u16,
590}
591
592#[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#[derive(Debug, Clone)]
623pub struct NChar {
624 value: String,
625 length: u16,
626}
627
628#[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#[derive(Debug, Clone)]
658pub struct Binary {
659 value: bytes::Bytes,
660 length: u16,
661}
662
663#[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 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 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 assert!(numeric(Decimal::new(1_234_567, 2), 18, 4).to_sql().is_ok());
838
839 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 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 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 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 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 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 assert!(
887 numeric(Decimal::new(1, 2), 1, 2).to_sql().is_err(),
888 "scale > precision"
889 );
890 }
891}