1use std::borrow::Cow;
2
3#[derive(Debug, Clone, PartialEq)]
5pub struct MssqlValue {
6 kind: MssqlValueKind,
7}
8
9impl MssqlValue {
10 pub fn new(kind: MssqlValueKind) -> Self {
12 Self { kind }
13 }
14
15 pub fn kind(&self) -> &MssqlValueKind {
17 &self.kind
18 }
19
20 pub fn is_null(&self) -> bool {
22 matches!(self.kind, MssqlValueKind::Null)
23 }
24
25 pub fn as_i64(&self) -> Option<i64> {
27 match &self.kind {
28 MssqlValueKind::TinyInt(value) => Some(i64::from(*value)),
29 MssqlValueKind::SmallInt(value) => Some(i64::from(*value)),
30 MssqlValueKind::Integer(value) => Some(i64::from(*value)),
31 MssqlValueKind::BigInt(value) => Some(*value),
32 MssqlValueKind::Text(value) => parse_integer_text(value),
33 _ => None,
34 }
35 }
36
37 pub fn as_f64(&self) -> Option<f64> {
39 match &self.kind {
40 MssqlValueKind::Real(value) => Some(f64::from(*value)),
41 MssqlValueKind::Double(value) => Some(*value),
42 MssqlValueKind::TinyInt(value) => Some(f64::from(*value)),
43 MssqlValueKind::SmallInt(value) => Some(f64::from(*value)),
44 MssqlValueKind::Integer(value) => Some(f64::from(*value)),
45 MssqlValueKind::BigInt(value) => Some(*value as f64),
46 MssqlValueKind::Text(value) => value.trim().parse().ok(),
47 _ => None,
48 }
49 }
50
51 pub fn as_str(&self) -> Option<Cow<'_, str>> {
53 match &self.kind {
54 MssqlValueKind::Text(value) => Some(Cow::Borrowed(value)),
55 MssqlValueKind::Guid(bytes) => {
56 let guid_str = uuid_guid_to_string(bytes);
57 Some(Cow::Owned(guid_str))
58 }
59 _ => None,
60 }
61 }
62
63 pub fn as_bytes(&self) -> Option<Cow<'_, [u8]>> {
65 match &self.kind {
66 MssqlValueKind::Binary(value) => Some(Cow::Borrowed(value)),
67 MssqlValueKind::Text(value) => Some(Cow::Borrowed(value.as_bytes())),
68 MssqlValueKind::Guid(bytes) => Some(Cow::Borrowed(bytes)),
69 _ => None,
70 }
71 }
72}
73
74impl sqlx_core::value::Value for MssqlValue {
75 type Database = crate::Mssql;
76
77 fn as_ref(&self) -> <Self::Database as sqlx_core::database::Database>::ValueRef<'_> {
78 MssqlValueRef { value: self }
79 }
80
81 fn type_info(&self) -> Cow<'_, crate::MssqlTypeInfo> {
82 Cow::Owned(self.kind.type_info())
83 }
84
85 fn is_null(&self) -> bool {
86 self.is_null()
87 }
88}
89
90#[derive(Debug, Clone, Copy)]
92pub struct MssqlValueRef<'r> {
93 value: &'r MssqlValue,
94}
95
96impl<'r> MssqlValueRef<'r> {
97 pub fn as_i64(&self) -> Option<i64> {
99 self.value.as_i64()
100 }
101
102 pub fn as_f64(&self) -> Option<f64> {
104 self.value.as_f64()
105 }
106
107 pub fn as_str(&self) -> Option<&'r str> {
109 match &self.value.kind {
110 MssqlValueKind::Text(value) => Some(value),
111 MssqlValueKind::Guid(bytes) => {
112 let _ = bytes;
115 None
116 }
117 _ => None,
118 }
119 }
120
121 pub fn as_bytes(&self) -> Option<&'r [u8]> {
123 match &self.value.kind {
124 MssqlValueKind::Binary(value) => Some(value),
125 MssqlValueKind::Text(value) => Some(value.as_bytes()),
126 MssqlValueKind::Guid(bytes) => Some(bytes),
127 _ => None,
128 }
129 }
130
131 pub fn as_bool(&self) -> Option<bool> {
133 match &self.value.kind {
134 MssqlValueKind::Bit(value) => Some(*value),
135 MssqlValueKind::TinyInt(value) => Some(*value != 0),
136 MssqlValueKind::SmallInt(value) => Some(*value != 0),
137 MssqlValueKind::Integer(value) => Some(*value != 0),
138 MssqlValueKind::BigInt(value) => Some(*value != 0),
139 MssqlValueKind::Real(value) => Some(*value != 0.0),
140 MssqlValueKind::Double(value) => Some(*value != 0.0),
141 MssqlValueKind::Text(value) => parse_bool_text(value),
142 _ => None,
143 }
144 }
145}
146
147impl<'r> sqlx_core::value::ValueRef<'r> for MssqlValueRef<'r> {
148 type Database = crate::Mssql;
149
150 fn to_owned(&self) -> MssqlValue {
151 self.value.clone()
152 }
153
154 fn type_info(&self) -> Cow<'_, crate::MssqlTypeInfo> {
155 Cow::Owned(self.value.kind.type_info())
156 }
157
158 fn is_null(&self) -> bool {
159 self.value.is_null()
160 }
161}
162
163macro_rules! impl_decode_integer {
164 ($ty:ty) => {
165 impl<'r> sqlx_core::decode::Decode<'r, crate::Mssql> for $ty {
166 fn decode(value: MssqlValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
167 let Some(integer) = value.as_i64() else {
168 return Err(decode_error(
169 value,
170 stringify!($ty),
171 "source value is not an integer",
172 )
173 .into());
174 };
175
176 Self::try_from(integer).map_err(|_| {
177 decode_error(
178 value,
179 stringify!($ty),
180 format!("integer value {integer} is outside the target range"),
181 )
182 .into()
183 })
184 }
185 }
186 };
187}
188
189impl_decode_integer!(i8);
190impl_decode_integer!(i16);
191impl_decode_integer!(i32);
192impl_decode_integer!(i64);
193impl_decode_integer!(u8);
194impl_decode_integer!(u16);
195impl_decode_integer!(u32);
196impl_decode_integer!(u64);
197
198impl<'r> sqlx_core::decode::Decode<'r, crate::Mssql> for bool {
199 fn decode(value: MssqlValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
200 value.as_bool().ok_or_else(|| {
201 decode_error(value, "bool", "source value is not boolean-compatible").into()
202 })
203 }
204}
205
206impl<'r> sqlx_core::decode::Decode<'r, crate::Mssql> for f32 {
207 fn decode(value: MssqlValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
208 value
209 .as_f64()
210 .map(|value| value as f32)
211 .ok_or_else(|| decode_error(value, "f32", "source value is not numeric").into())
212 }
213}
214
215impl<'r> sqlx_core::decode::Decode<'r, crate::Mssql> for f64 {
216 fn decode(value: MssqlValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
217 value
218 .as_f64()
219 .ok_or_else(|| decode_error(value, "f64", "source value is not numeric").into())
220 }
221}
222
223impl<'r> sqlx_core::decode::Decode<'r, crate::Mssql> for String {
224 fn decode(value: MssqlValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
225 if let Some(text) = value.as_str() {
226 return Ok(text.to_owned());
227 }
228
229 if let Some(bytes) = value.as_bytes() {
230 return Ok(String::from_utf8(bytes.to_vec())?);
231 }
232
233 Err(decode_error(
234 value,
235 "String",
236 "source value is neither text nor UTF-8 bytes",
237 )
238 .into())
239 }
240}
241
242impl<'r> sqlx_core::decode::Decode<'r, crate::Mssql> for &'r str {
243 fn decode(value: MssqlValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
244 if let Some(text) = value.as_str() {
245 return Ok(text);
246 }
247
248 Err(decode_error(value, "&str", "source value is not text").into())
249 }
250}
251
252impl<'r> sqlx_core::decode::Decode<'r, crate::Mssql> for Vec<u8> {
253 fn decode(value: MssqlValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
254 value
255 .as_bytes()
256 .map(<[u8]>::to_vec)
257 .ok_or_else(|| decode_error(value, "Vec<u8>", "source value is not binary").into())
258 }
259}
260
261impl<'r> sqlx_core::decode::Decode<'r, crate::Mssql> for &'r [u8] {
262 fn decode(value: MssqlValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
263 value
264 .as_bytes()
265 .ok_or_else(|| decode_error(value, "&[u8]", "source value is not binary").into())
266 }
267}
268
269impl<'r> sqlx_core::decode::Decode<'r, crate::Mssql> for odbc_api::sys::Date {
270 fn decode(value: MssqlValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
271 match value.value.kind() {
272 MssqlValueKind::Date(value) => Ok(*value),
273 _ => Err(decode_error(value, "Date", "source value is not an ODBC date").into()),
274 }
275 }
276}
277
278impl<'r> sqlx_core::decode::Decode<'r, crate::Mssql> for odbc_api::sys::Time {
279 fn decode(value: MssqlValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
280 match value.value.kind() {
281 MssqlValueKind::Time(value) => Ok(*value),
282 _ => Err(decode_error(value, "Time", "source value is not an ODBC time").into()),
283 }
284 }
285}
286
287impl<'r> sqlx_core::decode::Decode<'r, crate::Mssql> for odbc_api::sys::Timestamp {
288 fn decode(value: MssqlValueRef<'r>) -> Result<Self, sqlx_core::error::BoxDynError> {
289 match value.value.kind() {
290 MssqlValueKind::Timestamp(value) => Ok(*value),
291 _ => Err(
292 decode_error(value, "Timestamp", "source value is not an ODBC timestamp").into(),
293 ),
294 }
295 }
296}
297
298fn decode_error(value: MssqlValueRef<'_>, target: &str, reason: impl std::fmt::Display) -> String {
299 format!(
300 "ODBC cannot decode value kind {:?} as {target}: {reason}",
301 value.value.kind()
302 )
303}
304
305fn parse_bool_text(value: &str) -> Option<bool> {
306 match value.trim() {
307 "0" | "0.0" | "false" | "FALSE" | "f" | "F" => Some(false),
308 "1" | "1.0" | "true" | "TRUE" | "t" | "T" => Some(true),
309 value => value
310 .parse::<f64>()
311 .map(|value| value != 0.0)
312 .or_else(|_| value.parse::<i64>().map(|value| value != 0))
313 .ok(),
314 }
315}
316
317fn parse_integer_text(value: &str) -> Option<i64> {
318 let value = value.trim();
319
320 if let Ok(value) = value.parse() {
321 return Some(value);
322 }
323
324 let (integer, fraction) = value.split_once('.')?;
325
326 if fraction.chars().all(|ch| ch == '0') {
327 integer.parse().ok()
328 } else {
329 None
330 }
331}
332
333fn uuid_guid_to_string(bytes: &[u8; 16]) -> String {
335 format!(
336 "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
337 bytes[3], bytes[2], bytes[1], bytes[0],
338 bytes[5], bytes[4],
339 bytes[7], bytes[6],
340 bytes[8], bytes[9],
341 bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
342 )
343}
344
345#[derive(Debug, Clone, PartialEq)]
347pub enum MssqlValueKind {
348 Null,
350 TinyInt(i16),
352 SmallInt(i16),
354 Integer(i32),
356 BigInt(i64),
358 Real(f32),
360 Double(f64),
362 Bit(bool),
364 Text(String),
366 Binary(Vec<u8>),
368 Guid([u8; 16]),
370 Date(odbc_api::sys::Date),
372 Time(odbc_api::sys::Time),
374 Timestamp(odbc_api::sys::Timestamp),
376}
377
378impl MssqlValueKind {
379 fn type_info(&self) -> crate::MssqlTypeInfo {
380 let data_type = match self {
381 Self::Null => odbc_api::DataType::Unknown,
382 Self::TinyInt(_) => odbc_api::DataType::TinyInt,
383 Self::SmallInt(_) => odbc_api::DataType::SmallInt,
384 Self::Integer(_) => odbc_api::DataType::Integer,
385 Self::BigInt(_) => odbc_api::DataType::BigInt,
386 Self::Real(_) => odbc_api::DataType::Real,
387 Self::Double(_) => odbc_api::DataType::Double,
388 Self::Bit(_) => odbc_api::DataType::Bit,
389 Self::Text(_) => odbc_api::DataType::WVarchar { length: None },
390 Self::Binary(_) => odbc_api::DataType::Varbinary { length: None },
391 Self::Guid(_) => odbc_api::DataType::Other {
392 data_type: odbc_api::sys::SqlDataType(-11),
393 column_size: None,
394 decimal_digits: 0,
395 },
396 Self::Date(_) => odbc_api::DataType::Date,
397 Self::Time(_) => odbc_api::DataType::Time { precision: 0 },
398 Self::Timestamp(_) => odbc_api::DataType::Timestamp { precision: 6 },
399 };
400
401 crate::MssqlTypeInfo::new(data_type)
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn integer_values_convert_to_i64() {
411 assert_eq!(MssqlValue::new(MssqlValueKind::TinyInt(1)).as_i64(), Some(1));
412 assert_eq!(MssqlValue::new(MssqlValueKind::SmallInt(2)).as_i64(), Some(2));
413 assert_eq!(MssqlValue::new(MssqlValueKind::Integer(3)).as_i64(), Some(3));
414 assert_eq!(MssqlValue::new(MssqlValueKind::BigInt(4)).as_i64(), Some(4));
415 assert_eq!(
416 MssqlValue::new(MssqlValueKind::Text("42.000".to_owned())).as_i64(),
417 Some(42)
418 );
419 assert_eq!(
420 MssqlValue::new(MssqlValueKind::Text("42.5".to_owned())).as_i64(),
421 None
422 );
423 }
424
425 #[test]
426 fn text_numeric_values_convert_to_float() {
427 assert_eq!(
428 MssqlValue::new(MssqlValueKind::Text("42.5".to_owned())).as_f64(),
429 Some(42.5)
430 );
431 }
432
433 #[test]
434 fn text_and_bytes_borrow_from_value() {
435 let text = MssqlValue::new(MssqlValueKind::Text("hello".to_owned()));
436 assert_eq!(text.as_str().as_deref(), Some("hello"));
437 assert_eq!(text.as_bytes().as_deref(), Some(b"hello".as_slice()));
438
439 let bytes = MssqlValue::new(MssqlValueKind::Binary(vec![1, 2, 3]));
440 assert_eq!(bytes.as_bytes().as_deref(), Some(&[1, 2, 3][..]));
441 }
442
443 #[test]
444 fn null_reports_null() {
445 assert!(MssqlValue::new(MssqlValueKind::Null).is_null());
446 }
447
448 #[test]
449 fn borrowed_values_decode_basic_scalars() {
450 use sqlx_core::decode::Decode;
451 use sqlx_core::value::Value;
452
453 let int = MssqlValue::new(MssqlValueKind::BigInt(42));
454 assert_eq!(
455 <i32 as Decode<crate::Mssql>>::decode(int.as_ref()).unwrap(),
456 42
457 );
458
459 let truthy = MssqlValue::new(MssqlValueKind::Text("true".to_owned()));
460 assert!(<bool as Decode<crate::Mssql>>::decode(truthy.as_ref()).unwrap());
461
462 let text = MssqlValue::new(MssqlValueKind::Text("hello".to_owned()));
463 assert_eq!(
464 <String as Decode<crate::Mssql>>::decode(text.as_ref()).unwrap(),
465 "hello"
466 );
467
468 let bytes = MssqlValue::new(MssqlValueKind::Binary(vec![1, 2, 3]));
469 assert_eq!(
470 <Vec<u8> as Decode<crate::Mssql>>::decode(bytes.as_ref()).unwrap(),
471 vec![1, 2, 3]
472 );
473
474 let bytes_from_text = MssqlValue::new(MssqlValueKind::Text("abc".to_owned()));
475 assert_eq!(
476 <Vec<u8> as Decode<crate::Mssql>>::decode(bytes_from_text.as_ref()).unwrap(),
477 b"abc".to_vec()
478 );
479 assert_eq!(
480 <&[u8] as Decode<crate::Mssql>>::decode(bytes_from_text.as_ref()).unwrap(),
481 b"abc".as_slice()
482 );
483 }
484
485 #[test]
486 fn borrowed_values_decode_bool_variants() {
487 use sqlx_core::decode::Decode;
488 use sqlx_core::value::Value;
489
490 for value in [
491 MssqlValueKind::Bit(true),
492 MssqlValueKind::TinyInt(1),
493 MssqlValueKind::SmallInt(-1),
494 MssqlValueKind::Integer(42),
495 MssqlValueKind::BigInt(1),
496 MssqlValueKind::Real(1.0),
497 MssqlValueKind::Double(42.5),
498 MssqlValueKind::Text("true".to_owned()),
499 MssqlValueKind::Text("TRUE".to_owned()),
500 MssqlValueKind::Text("t".to_owned()),
501 MssqlValueKind::Text("1".to_owned()),
502 MssqlValueKind::Text("1.0".to_owned()),
503 MssqlValueKind::Text(" 42 ".to_owned()),
504 ] {
505 let value = MssqlValue::new(value);
506 assert!(<bool as Decode<crate::Mssql>>::decode(value.as_ref()).unwrap());
507 }
508
509 for value in [
510 MssqlValueKind::Bit(false),
511 MssqlValueKind::TinyInt(0),
512 MssqlValueKind::SmallInt(0),
513 MssqlValueKind::Integer(0),
514 MssqlValueKind::BigInt(0),
515 MssqlValueKind::Real(0.0),
516 MssqlValueKind::Double(0.0),
517 MssqlValueKind::Text("false".to_owned()),
518 MssqlValueKind::Text("FALSE".to_owned()),
519 MssqlValueKind::Text("f".to_owned()),
520 MssqlValueKind::Text("0".to_owned()),
521 MssqlValueKind::Text("0.0".to_owned()),
522 MssqlValueKind::Text(" 0 ".to_owned()),
523 ] {
524 let value = MssqlValue::new(value);
525 assert!(!<bool as Decode<crate::Mssql>>::decode(value.as_ref()).unwrap());
526 }
527 }
528
529 #[test]
530 fn borrowed_values_reject_invalid_bool_text() {
531 use sqlx_core::decode::Decode;
532 use sqlx_core::value::Value;
533
534 let value = MssqlValue::new(MssqlValueKind::Text("not a bool".to_owned()));
535 let error = <bool as Decode<crate::Mssql>>::decode(value.as_ref()).unwrap_err();
536
537 assert!(error.to_string().contains("bool"));
538 assert!(error.to_string().contains("not boolean-compatible"));
539 }
540
541 #[test]
542 fn borrowed_values_decode_temporal_scalars() {
543 use sqlx_core::decode::Decode;
544 use sqlx_core::value::Value;
545
546 let date = odbc_api::sys::Date {
547 year: 2026,
548 month: 5,
549 day: 29,
550 };
551 let date_value = MssqlValue::new(MssqlValueKind::Date(date));
552 assert_eq!(
553 <odbc_api::sys::Date as Decode<crate::Mssql>>::decode(date_value.as_ref()).unwrap(),
554 date
555 );
556
557 let time = odbc_api::sys::Time {
558 hour: 12,
559 minute: 30,
560 second: 45,
561 };
562 let time_value = MssqlValue::new(MssqlValueKind::Time(time));
563 assert_eq!(
564 <odbc_api::sys::Time as Decode<crate::Mssql>>::decode(time_value.as_ref()).unwrap(),
565 time
566 );
567
568 let timestamp = odbc_api::sys::Timestamp {
569 year: 2026,
570 month: 5,
571 day: 29,
572 hour: 12,
573 minute: 30,
574 second: 45,
575 fraction: 123_456_000,
576 };
577 let timestamp_value = MssqlValue::new(MssqlValueKind::Timestamp(timestamp));
578 assert_eq!(
579 <odbc_api::sys::Timestamp as Decode<crate::Mssql>>::decode(timestamp_value.as_ref())
580 .unwrap(),
581 timestamp
582 );
583 }
584}