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
21impl ToSql for bool {
22    fn to_sql(&self) -> Result<SqlValue, TypeError> {
23        Ok(SqlValue::Bool(*self))
24    }
25
26    fn sql_type(&self) -> &'static str {
27        "BIT"
28    }
29}
30
31impl ToSql for u8 {
32    fn to_sql(&self) -> Result<SqlValue, TypeError> {
33        Ok(SqlValue::TinyInt(*self))
34    }
35
36    fn sql_type(&self) -> &'static str {
37        "TINYINT"
38    }
39}
40
41impl ToSql for i16 {
42    fn to_sql(&self) -> Result<SqlValue, TypeError> {
43        Ok(SqlValue::SmallInt(*self))
44    }
45
46    fn sql_type(&self) -> &'static str {
47        "SMALLINT"
48    }
49}
50
51impl ToSql for i32 {
52    fn to_sql(&self) -> Result<SqlValue, TypeError> {
53        Ok(SqlValue::Int(*self))
54    }
55
56    fn sql_type(&self) -> &'static str {
57        "INT"
58    }
59}
60
61impl ToSql for i64 {
62    fn to_sql(&self) -> Result<SqlValue, TypeError> {
63        Ok(SqlValue::BigInt(*self))
64    }
65
66    fn sql_type(&self) -> &'static str {
67        "BIGINT"
68    }
69}
70
71impl ToSql for f32 {
72    fn to_sql(&self) -> Result<SqlValue, TypeError> {
73        Ok(SqlValue::Float(*self))
74    }
75
76    fn sql_type(&self) -> &'static str {
77        "REAL"
78    }
79}
80
81impl ToSql for f64 {
82    fn to_sql(&self) -> Result<SqlValue, TypeError> {
83        Ok(SqlValue::Double(*self))
84    }
85
86    fn sql_type(&self) -> &'static str {
87        "FLOAT"
88    }
89}
90
91impl ToSql for str {
92    fn to_sql(&self) -> Result<SqlValue, TypeError> {
93        Ok(SqlValue::String(self.to_owned()))
94    }
95
96    fn sql_type(&self) -> &'static str {
97        "NVARCHAR"
98    }
99}
100
101impl ToSql for String {
102    fn to_sql(&self) -> Result<SqlValue, TypeError> {
103        Ok(SqlValue::String(self.clone()))
104    }
105
106    fn sql_type(&self) -> &'static str {
107        "NVARCHAR"
108    }
109}
110
111impl ToSql for [u8] {
112    fn to_sql(&self) -> Result<SqlValue, TypeError> {
113        Ok(SqlValue::Binary(bytes::Bytes::copy_from_slice(self)))
114    }
115
116    fn sql_type(&self) -> &'static str {
117        "VARBINARY"
118    }
119}
120
121impl ToSql for Vec<u8> {
122    fn to_sql(&self) -> Result<SqlValue, TypeError> {
123        Ok(SqlValue::Binary(bytes::Bytes::copy_from_slice(self)))
124    }
125
126    fn sql_type(&self) -> &'static str {
127        "VARBINARY"
128    }
129}
130
131impl<T: ToSql> ToSql for Option<T> {
132    fn to_sql(&self) -> Result<SqlValue, TypeError> {
133        match self {
134            Some(v) => v.to_sql(),
135            None => Ok(SqlValue::Null),
136        }
137    }
138
139    fn sql_type(&self) -> &'static str {
140        match self {
141            Some(v) => v.sql_type(),
142            None => "NULL",
143        }
144    }
145}
146
147impl<T: ToSql + ?Sized> ToSql for &T {
148    fn to_sql(&self) -> Result<SqlValue, TypeError> {
149        (*self).to_sql()
150    }
151
152    fn sql_type(&self) -> &'static str {
153        (*self).sql_type()
154    }
155}
156
157#[cfg(feature = "uuid")]
158impl ToSql for uuid::Uuid {
159    fn to_sql(&self) -> Result<SqlValue, TypeError> {
160        Ok(SqlValue::Uuid(*self))
161    }
162
163    fn sql_type(&self) -> &'static str {
164        "UNIQUEIDENTIFIER"
165    }
166}
167
168#[cfg(feature = "decimal")]
169impl ToSql for rust_decimal::Decimal {
170    fn to_sql(&self) -> Result<SqlValue, TypeError> {
171        Ok(SqlValue::Decimal(*self))
172    }
173
174    fn sql_type(&self) -> &'static str {
175        "DECIMAL"
176    }
177}
178
179#[cfg(feature = "chrono")]
180impl ToSql for chrono::NaiveDate {
181    fn to_sql(&self) -> Result<SqlValue, TypeError> {
182        Ok(SqlValue::Date(*self))
183    }
184
185    fn sql_type(&self) -> &'static str {
186        "DATE"
187    }
188}
189
190#[cfg(feature = "chrono")]
191impl ToSql for chrono::NaiveTime {
192    fn to_sql(&self) -> Result<SqlValue, TypeError> {
193        Ok(SqlValue::Time(*self))
194    }
195
196    fn sql_type(&self) -> &'static str {
197        "TIME"
198    }
199}
200
201#[cfg(feature = "chrono")]
202impl ToSql for chrono::NaiveDateTime {
203    fn to_sql(&self) -> Result<SqlValue, TypeError> {
204        Ok(SqlValue::DateTime(*self))
205    }
206
207    fn sql_type(&self) -> &'static str {
208        "DATETIME2"
209    }
210}
211
212#[cfg(feature = "chrono")]
213impl ToSql for chrono::DateTime<chrono::FixedOffset> {
214    fn to_sql(&self) -> Result<SqlValue, TypeError> {
215        Ok(SqlValue::DateTimeOffset(*self))
216    }
217
218    fn sql_type(&self) -> &'static str {
219        "DATETIMEOFFSET"
220    }
221}
222
223#[cfg(feature = "chrono")]
224impl ToSql for chrono::DateTime<chrono::Utc> {
225    fn to_sql(&self) -> Result<SqlValue, TypeError> {
226        // Convert UTC to FixedOffset with +00:00 offset
227        let fixed = self.with_timezone(&chrono::FixedOffset::east_opt(0).expect("valid offset"));
228        Ok(SqlValue::DateTimeOffset(fixed))
229    }
230
231    fn sql_type(&self) -> &'static str {
232        "DATETIMEOFFSET"
233    }
234}
235
236#[cfg(feature = "json")]
237impl ToSql for serde_json::Value {
238    fn to_sql(&self) -> Result<SqlValue, TypeError> {
239        Ok(SqlValue::Json(self.clone()))
240    }
241
242    fn sql_type(&self) -> &'static str {
243        "NVARCHAR(MAX)"
244    }
245}
246
247#[cfg(test)]
248#[allow(clippy::unwrap_used)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_to_sql_i32() {
254        let value: i32 = 42;
255        assert_eq!(value.to_sql().unwrap(), SqlValue::Int(42));
256        assert_eq!(value.sql_type(), "INT");
257    }
258
259    #[test]
260    fn test_to_sql_string() {
261        let value = "hello".to_string();
262        assert_eq!(
263            value.to_sql().unwrap(),
264            SqlValue::String("hello".to_string())
265        );
266        assert_eq!(value.sql_type(), "NVARCHAR");
267    }
268
269    #[test]
270    fn test_to_sql_option() {
271        let some: Option<i32> = Some(42);
272        assert_eq!(some.to_sql().unwrap(), SqlValue::Int(42));
273
274        let none: Option<i32> = None;
275        assert_eq!(none.to_sql().unwrap(), SqlValue::Null);
276    }
277}