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 decimal_param_info(&self) -> Option<DecimalParamInfo> {
25 None
26 }
27}
28
29#[derive(Debug, Clone, Copy)]
32pub struct DecimalParamInfo {
33 pub precision: u8,
35 pub scale: u8,
37}
38
39impl ToSql for bool {
40 fn to_sql(&self) -> Result<SqlValue, TypeError> {
41 Ok(SqlValue::Bool(*self))
42 }
43
44 fn sql_type(&self) -> &'static str {
45 "BIT"
46 }
47}
48
49impl ToSql for u8 {
50 fn to_sql(&self) -> Result<SqlValue, TypeError> {
51 Ok(SqlValue::TinyInt(*self))
52 }
53
54 fn sql_type(&self) -> &'static str {
55 "TINYINT"
56 }
57}
58
59impl ToSql for i16 {
60 fn to_sql(&self) -> Result<SqlValue, TypeError> {
61 Ok(SqlValue::SmallInt(*self))
62 }
63
64 fn sql_type(&self) -> &'static str {
65 "SMALLINT"
66 }
67}
68
69impl ToSql for i32 {
70 fn to_sql(&self) -> Result<SqlValue, TypeError> {
71 Ok(SqlValue::Int(*self))
72 }
73
74 fn sql_type(&self) -> &'static str {
75 "INT"
76 }
77}
78
79impl ToSql for i64 {
80 fn to_sql(&self) -> Result<SqlValue, TypeError> {
81 Ok(SqlValue::BigInt(*self))
82 }
83
84 fn sql_type(&self) -> &'static str {
85 "BIGINT"
86 }
87}
88
89impl ToSql for f32 {
90 fn to_sql(&self) -> Result<SqlValue, TypeError> {
91 Ok(SqlValue::Float(*self))
92 }
93
94 fn sql_type(&self) -> &'static str {
95 "REAL"
96 }
97}
98
99impl ToSql for f64 {
100 fn to_sql(&self) -> Result<SqlValue, TypeError> {
101 Ok(SqlValue::Double(*self))
102 }
103
104 fn sql_type(&self) -> &'static str {
105 "FLOAT"
106 }
107}
108
109impl ToSql for str {
110 fn to_sql(&self) -> Result<SqlValue, TypeError> {
111 Ok(SqlValue::String(self.to_owned()))
112 }
113
114 fn sql_type(&self) -> &'static str {
115 "NVARCHAR"
116 }
117}
118
119impl ToSql for String {
120 fn to_sql(&self) -> Result<SqlValue, TypeError> {
121 Ok(SqlValue::String(self.clone()))
122 }
123
124 fn sql_type(&self) -> &'static str {
125 "NVARCHAR"
126 }
127}
128
129impl ToSql for [u8] {
130 fn to_sql(&self) -> Result<SqlValue, TypeError> {
131 Ok(SqlValue::Binary(bytes::Bytes::copy_from_slice(self)))
132 }
133
134 fn sql_type(&self) -> &'static str {
135 "VARBINARY"
136 }
137}
138
139impl ToSql for Vec<u8> {
140 fn to_sql(&self) -> Result<SqlValue, TypeError> {
141 Ok(SqlValue::Binary(bytes::Bytes::copy_from_slice(self)))
142 }
143
144 fn sql_type(&self) -> &'static str {
145 "VARBINARY"
146 }
147}
148
149pub trait SqlTyped {
155 const SQL_TYPE: &'static str;
157}
158
159impl SqlTyped for bool {
160 const SQL_TYPE: &'static str = "BIT";
161}
162impl SqlTyped for u8 {
163 const SQL_TYPE: &'static str = "TINYINT";
164}
165impl SqlTyped for i16 {
166 const SQL_TYPE: &'static str = "SMALLINT";
167}
168impl SqlTyped for i32 {
169 const SQL_TYPE: &'static str = "INT";
170}
171impl SqlTyped for i64 {
172 const SQL_TYPE: &'static str = "BIGINT";
173}
174impl SqlTyped for f32 {
175 const SQL_TYPE: &'static str = "REAL";
176}
177impl SqlTyped for f64 {
178 const SQL_TYPE: &'static str = "FLOAT";
179}
180impl SqlTyped for String {
181 const SQL_TYPE: &'static str = "NVARCHAR";
182}
183impl SqlTyped for Vec<u8> {
184 const SQL_TYPE: &'static str = "VARBINARY";
185}
186#[cfg(feature = "uuid")]
187impl SqlTyped for uuid::Uuid {
188 const SQL_TYPE: &'static str = "UNIQUEIDENTIFIER";
189}
190#[cfg(feature = "chrono")]
191impl SqlTyped for chrono::NaiveDate {
192 const SQL_TYPE: &'static str = "DATE";
193}
194
195#[derive(Debug, Clone, Copy)]
202pub struct TypedNull {
203 sql_type: &'static str,
204}
205
206impl ToSql for TypedNull {
207 fn to_sql(&self) -> Result<SqlValue, TypeError> {
208 Ok(SqlValue::Null)
209 }
210
211 fn sql_type(&self) -> &'static str {
212 self.sql_type
213 }
214}
215
216#[must_use]
221pub fn null<T: SqlTyped>() -> TypedNull {
222 TypedNull {
223 sql_type: T::SQL_TYPE,
224 }
225}
226
227impl<T: ToSql> ToSql for Option<T> {
228 fn to_sql(&self) -> Result<SqlValue, TypeError> {
229 match self {
230 Some(v) => v.to_sql(),
231 None => Ok(SqlValue::Null),
232 }
233 }
234
235 fn sql_type(&self) -> &'static str {
236 match self {
237 Some(v) => v.sql_type(),
238 None => "NULL",
239 }
240 }
241
242 fn decimal_param_info(&self) -> Option<DecimalParamInfo> {
243 self.as_ref().and_then(ToSql::decimal_param_info)
244 }
245}
246
247impl<T: ToSql + ?Sized> ToSql for &T {
248 fn to_sql(&self) -> Result<SqlValue, TypeError> {
249 (*self).to_sql()
250 }
251
252 fn sql_type(&self) -> &'static str {
253 (*self).sql_type()
254 }
255
256 fn decimal_param_info(&self) -> Option<DecimalParamInfo> {
257 (*self).decimal_param_info()
258 }
259}
260
261#[cfg(feature = "uuid")]
262impl ToSql for uuid::Uuid {
263 fn to_sql(&self) -> Result<SqlValue, TypeError> {
264 Ok(SqlValue::Uuid(*self))
265 }
266
267 fn sql_type(&self) -> &'static str {
268 "UNIQUEIDENTIFIER"
269 }
270}
271
272#[cfg(feature = "decimal")]
273impl ToSql for rust_decimal::Decimal {
274 fn to_sql(&self) -> Result<SqlValue, TypeError> {
275 Ok(SqlValue::Decimal(*self))
276 }
277
278 fn sql_type(&self) -> &'static str {
279 "DECIMAL"
280 }
281}
282
283#[cfg(feature = "decimal")]
290#[derive(Debug, Clone, Copy)]
291pub struct Numeric {
292 value: rust_decimal::Decimal,
293 precision: u8,
294 scale: u8,
295}
296
297#[cfg(feature = "decimal")]
303#[must_use]
304pub fn numeric(value: rust_decimal::Decimal, precision: u8, scale: u8) -> Numeric {
305 Numeric {
306 value,
307 precision,
308 scale,
309 }
310}
311
312#[cfg(feature = "decimal")]
313impl ToSql for Numeric {
314 fn to_sql(&self) -> Result<SqlValue, TypeError> {
315 let mut value = self.value;
316 value.rescale(u32::from(self.scale));
317 Ok(SqlValue::Decimal(value))
318 }
319
320 fn sql_type(&self) -> &'static str {
321 "DECIMAL"
322 }
323
324 fn decimal_param_info(&self) -> Option<DecimalParamInfo> {
325 Some(DecimalParamInfo {
326 precision: self.precision,
327 scale: self.scale,
328 })
329 }
330}
331
332#[cfg(feature = "decimal")]
333impl ToSql for crate::value::Money {
334 fn to_sql(&self) -> Result<SqlValue, TypeError> {
335 Ok(SqlValue::Money(self.0))
336 }
337
338 fn sql_type(&self) -> &'static str {
339 "MONEY"
340 }
341}
342
343#[cfg(feature = "decimal")]
344impl ToSql for crate::value::SmallMoney {
345 fn to_sql(&self) -> Result<SqlValue, TypeError> {
346 Ok(SqlValue::SmallMoney(self.0))
347 }
348
349 fn sql_type(&self) -> &'static str {
350 "SMALLMONEY"
351 }
352}
353
354#[cfg(feature = "chrono")]
355impl ToSql for chrono::NaiveDate {
356 fn to_sql(&self) -> Result<SqlValue, TypeError> {
357 Ok(SqlValue::Date(*self))
358 }
359
360 fn sql_type(&self) -> &'static str {
361 "DATE"
362 }
363}
364
365#[cfg(feature = "chrono")]
366impl ToSql for chrono::NaiveTime {
367 fn to_sql(&self) -> Result<SqlValue, TypeError> {
368 Ok(SqlValue::Time(*self))
369 }
370
371 fn sql_type(&self) -> &'static str {
372 "TIME"
373 }
374}
375
376#[cfg(feature = "chrono")]
377impl ToSql for chrono::NaiveDateTime {
378 fn to_sql(&self) -> Result<SqlValue, TypeError> {
379 Ok(SqlValue::DateTime(*self))
380 }
381
382 fn sql_type(&self) -> &'static str {
383 "DATETIME2"
384 }
385}
386
387#[cfg(feature = "chrono")]
388impl ToSql for crate::value::SmallDateTime {
389 fn to_sql(&self) -> Result<SqlValue, TypeError> {
390 Ok(SqlValue::SmallDateTime(self.0))
391 }
392
393 fn sql_type(&self) -> &'static str {
394 "SMALLDATETIME"
395 }
396}
397
398#[cfg(feature = "chrono")]
399impl ToSql for chrono::DateTime<chrono::FixedOffset> {
400 fn to_sql(&self) -> Result<SqlValue, TypeError> {
401 Ok(SqlValue::DateTimeOffset(*self))
402 }
403
404 fn sql_type(&self) -> &'static str {
405 "DATETIMEOFFSET"
406 }
407}
408
409#[cfg(feature = "chrono")]
410impl ToSql for chrono::DateTime<chrono::Utc> {
411 fn to_sql(&self) -> Result<SqlValue, TypeError> {
412 let fixed = self.with_timezone(&chrono::FixedOffset::east_opt(0).expect("valid offset"));
414 Ok(SqlValue::DateTimeOffset(fixed))
415 }
416
417 fn sql_type(&self) -> &'static str {
418 "DATETIMEOFFSET"
419 }
420}
421
422#[cfg(feature = "json")]
423impl ToSql for serde_json::Value {
424 fn to_sql(&self) -> Result<SqlValue, TypeError> {
425 Ok(SqlValue::Json(self.clone()))
426 }
427
428 fn sql_type(&self) -> &'static str {
429 "NVARCHAR(MAX)"
430 }
431}
432
433#[cfg(test)]
434#[allow(clippy::unwrap_used)]
435mod tests {
436 use super::*;
437
438 #[test]
439 fn test_to_sql_i32() {
440 let value: i32 = 42;
441 assert_eq!(value.to_sql().unwrap(), SqlValue::Int(42));
442 assert_eq!(value.sql_type(), "INT");
443 }
444
445 #[test]
446 fn test_typed_null_carries_type() {
447 assert_eq!(null::<i32>().to_sql().unwrap(), SqlValue::Null);
450 assert_eq!(null::<i32>().sql_type(), 42i32.sql_type());
451 assert_eq!(null::<i64>().sql_type(), "BIGINT");
452 assert_eq!(null::<Vec<u8>>().sql_type(), "VARBINARY");
453 assert_eq!(null::<String>().sql_type(), "NVARCHAR");
454 }
455
456 #[test]
457 fn test_to_sql_string() {
458 let value = "hello".to_string();
459 assert_eq!(
460 value.to_sql().unwrap(),
461 SqlValue::String("hello".to_string())
462 );
463 assert_eq!(value.sql_type(), "NVARCHAR");
464 }
465
466 #[test]
467 fn test_to_sql_option() {
468 let some: Option<i32> = Some(42);
469 assert_eq!(some.to_sql().unwrap(), SqlValue::Int(42));
470
471 let none: Option<i32> = None;
472 assert_eq!(none.to_sql().unwrap(), SqlValue::Null);
473 }
474}