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")]
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 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#[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#[cfg(feature = "chrono")]
416#[derive(Debug, Clone, Copy)]
417pub struct Time {
418 value: chrono::NaiveTime,
419 scale: u8,
420}
421
422#[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#[cfg(feature = "chrono")]
451#[derive(Debug, Clone, Copy)]
452pub struct DateTime2 {
453 value: chrono::NaiveDateTime,
454 scale: u8,
455}
456
457#[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#[cfg(feature = "chrono")]
486#[derive(Debug, Clone, Copy)]
487pub struct DateTimeOffset {
488 value: chrono::DateTime<chrono::FixedOffset>,
489 scale: u8,
490}
491
492#[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#[cfg(feature = "chrono")]
518#[derive(Debug, Clone, Copy)]
519pub struct DateTimeLegacy {
520 value: chrono::NaiveDateTime,
521}
522
523#[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#[derive(Debug, Clone)]
549pub struct Char {
550 value: String,
551 length: u16,
552}
553
554#[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#[derive(Debug, Clone)]
585pub struct NChar {
586 value: String,
587 length: u16,
588}
589
590#[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#[derive(Debug, Clone)]
620pub struct Binary {
621 value: bytes::Bytes,
622 length: u16,
623}
624
625#[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 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 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 assert!(numeric(Decimal::new(1_234_567, 2), 18, 4).to_sql().is_ok());
800
801 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 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 assert!(numeric(Decimal::ZERO, 1, 0).to_sql().is_ok());
813 }
814}