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
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 = "decimal")]
180impl ToSql for crate::value::Money {
181    fn to_sql(&self) -> Result<SqlValue, TypeError> {
182        Ok(SqlValue::Money(self.0))
183    }
184
185    fn sql_type(&self) -> &'static str {
186        "MONEY"
187    }
188}
189
190#[cfg(feature = "decimal")]
191impl ToSql for crate::value::SmallMoney {
192    fn to_sql(&self) -> Result<SqlValue, TypeError> {
193        Ok(SqlValue::SmallMoney(self.0))
194    }
195
196    fn sql_type(&self) -> &'static str {
197        "SMALLMONEY"
198    }
199}
200
201#[cfg(feature = "chrono")]
202impl ToSql for chrono::NaiveDate {
203    fn to_sql(&self) -> Result<SqlValue, TypeError> {
204        Ok(SqlValue::Date(*self))
205    }
206
207    fn sql_type(&self) -> &'static str {
208        "DATE"
209    }
210}
211
212#[cfg(feature = "chrono")]
213impl ToSql for chrono::NaiveTime {
214    fn to_sql(&self) -> Result<SqlValue, TypeError> {
215        Ok(SqlValue::Time(*self))
216    }
217
218    fn sql_type(&self) -> &'static str {
219        "TIME"
220    }
221}
222
223#[cfg(feature = "chrono")]
224impl ToSql for chrono::NaiveDateTime {
225    fn to_sql(&self) -> Result<SqlValue, TypeError> {
226        Ok(SqlValue::DateTime(*self))
227    }
228
229    fn sql_type(&self) -> &'static str {
230        "DATETIME2"
231    }
232}
233
234#[cfg(feature = "chrono")]
235impl ToSql for crate::value::SmallDateTime {
236    fn to_sql(&self) -> Result<SqlValue, TypeError> {
237        Ok(SqlValue::SmallDateTime(self.0))
238    }
239
240    fn sql_type(&self) -> &'static str {
241        "SMALLDATETIME"
242    }
243}
244
245#[cfg(feature = "chrono")]
246impl ToSql for chrono::DateTime<chrono::FixedOffset> {
247    fn to_sql(&self) -> Result<SqlValue, TypeError> {
248        Ok(SqlValue::DateTimeOffset(*self))
249    }
250
251    fn sql_type(&self) -> &'static str {
252        "DATETIMEOFFSET"
253    }
254}
255
256#[cfg(feature = "chrono")]
257impl ToSql for chrono::DateTime<chrono::Utc> {
258    fn to_sql(&self) -> Result<SqlValue, TypeError> {
259        // Convert UTC to FixedOffset with +00:00 offset
260        let fixed = self.with_timezone(&chrono::FixedOffset::east_opt(0).expect("valid offset"));
261        Ok(SqlValue::DateTimeOffset(fixed))
262    }
263
264    fn sql_type(&self) -> &'static str {
265        "DATETIMEOFFSET"
266    }
267}
268
269#[cfg(feature = "json")]
270impl ToSql for serde_json::Value {
271    fn to_sql(&self) -> Result<SqlValue, TypeError> {
272        Ok(SqlValue::Json(self.clone()))
273    }
274
275    fn sql_type(&self) -> &'static str {
276        "NVARCHAR(MAX)"
277    }
278}
279
280#[cfg(test)]
281#[allow(clippy::unwrap_used)]
282mod tests {
283    use super::*;
284
285    #[test]
286    fn test_to_sql_i32() {
287        let value: i32 = 42;
288        assert_eq!(value.to_sql().unwrap(), SqlValue::Int(42));
289        assert_eq!(value.sql_type(), "INT");
290    }
291
292    #[test]
293    fn test_to_sql_string() {
294        let value = "hello".to_string();
295        assert_eq!(
296            value.to_sql().unwrap(),
297            SqlValue::String("hello".to_string())
298        );
299        assert_eq!(value.sql_type(), "NVARCHAR");
300    }
301
302    #[test]
303    fn test_to_sql_option() {
304        let some: Option<i32> = Some(42);
305        assert_eq!(some.to_sql().unwrap(), SqlValue::Int(42));
306
307        let none: Option<i32> = None;
308        assert_eq!(none.to_sql().unwrap(), SqlValue::Null);
309    }
310}