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