1use std::borrow::Cow;
36
37use oracledb_protocol::sql;
38use oracledb_protocol::thin::{BindValue, ColumnMetadata, QueryResult, QueryValue};
39
40#[derive(Clone, Debug, Eq, PartialEq)]
47#[non_exhaustive]
48pub enum ConversionError {
49 UnexpectedNull,
52 TypeMismatch {
55 expected: &'static str,
57 found: &'static str,
59 },
60 OutOfRange {
64 expected: &'static str,
66 detail: String,
68 },
69}
70
71impl std::fmt::Display for ConversionError {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 match self {
74 ConversionError::UnexpectedNull => {
75 write!(f, "value is SQL NULL but the target type is not Option<_>")
76 }
77 ConversionError::TypeMismatch { expected, found } => {
78 write!(f, "cannot convert Oracle {found} into {expected}")
79 }
80 ConversionError::OutOfRange { expected, detail } => {
81 write!(f, "value does not fit {expected}: {detail}")
82 }
83 }
84 }
85}
86
87impl std::error::Error for ConversionError {}
88
89impl From<ConversionError> for crate::Error {
90 fn from(err: ConversionError) -> Self {
91 crate::Error::Conversion(err)
92 }
93}
94
95#[derive(Clone, Debug, Eq, PartialEq)]
98#[non_exhaustive]
99pub enum BindError {
100 Sql(sql::SqlError),
102 PositionalCountMismatch { expected: usize, actual: usize },
104 MissingNamedBind { name: String },
106 ExtraNamedBind { name: String },
108 BatchRowWidthMismatch {
110 row_index: usize,
111 expected: usize,
112 actual: usize,
113 },
114 BatchColumnTypeMismatch {
122 row_index: usize,
124 column_index: usize,
126 expected: &'static str,
128 actual: &'static str,
130 },
131}
132
133impl std::fmt::Display for BindError {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 match self {
136 BindError::Sql(err) => write!(f, "SQL bind scan failed: {err}"),
137 BindError::PositionalCountMismatch { expected, actual } => write!(
138 f,
139 "SQL expects {expected} bind values but {actual} were supplied"
140 ),
141 BindError::MissingNamedBind { name } => {
142 write!(f, "missing value for named bind :{name}")
143 }
144 BindError::ExtraNamedBind { name } => {
145 write!(f, "named bind {name} is not referenced by the SQL")
146 }
147 BindError::BatchRowWidthMismatch {
148 row_index,
149 expected,
150 actual,
151 } => write!(
152 f,
153 "batch row {row_index} has {actual} bind values; expected {expected}"
154 ),
155 BindError::BatchColumnTypeMismatch {
156 row_index,
157 column_index,
158 expected,
159 actual,
160 } => write!(
161 f,
162 "batch row {row_index} binds {actual} at position {column_index}, but an \
163 earlier row established {expected} there; every row must supply the same \
164 type for a given bind"
165 ),
166 }
167 }
168}
169
170impl std::error::Error for BindError {
171 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
172 match self {
173 BindError::Sql(err) => Some(err),
174 _ => None,
175 }
176 }
177}
178
179impl From<sql::SqlError> for BindError {
180 fn from(err: sql::SqlError) -> Self {
181 BindError::Sql(err)
182 }
183}
184
185fn value_kind(value: &QueryValue) -> &'static str {
188 match value {
189 QueryValue::Text(_) => "VARCHAR2/CHAR text",
190 QueryValue::TextRaw { .. } => "undecodable character data",
191 QueryValue::Raw(_) => "RAW",
192 QueryValue::Rowid(_) => "ROWID",
193 QueryValue::BinaryDouble(_) => "BINARY_DOUBLE/BINARY_FLOAT",
194 QueryValue::IntervalDS { .. } => "INTERVAL DAY TO SECOND",
195 QueryValue::IntervalYM { .. } => "INTERVAL YEAR TO MONTH",
196 QueryValue::Number(_) => "NUMBER",
197 QueryValue::Boolean(_) => "BOOLEAN",
198 QueryValue::Cursor(_) => "REF CURSOR",
199 QueryValue::DateTime { .. } => "DATE/TIMESTAMP",
200 QueryValue::Object(_) => "object/ADT",
201 QueryValue::Lob(_) => "LOB locator",
202 QueryValue::Vector(_) => "VECTOR",
203 QueryValue::Json(_) => "JSON",
204 QueryValue::Array(_) => "collection",
205 _ => "Oracle value",
208 }
209}
210
211fn mismatch<T>(expected: &'static str, value: &QueryValue) -> Result<T, ConversionError> {
212 Err(ConversionError::TypeMismatch {
213 expected,
214 found: value_kind(value),
215 })
216}
217
218pub trait FromSql: Sized {
229 fn from_sql(value: &QueryValue) -> Result<Self, ConversionError>;
231}
232
233impl FromSql for i64 {
238 fn from_sql(value: &QueryValue) -> Result<Self, ConversionError> {
239 match value {
240 QueryValue::Number(num) => num.to_i64().ok_or_else(|| ConversionError::OutOfRange {
243 expected: "i64",
244 detail: format!(
245 "NUMBER {:?} is not an integer that fits i64",
246 num.to_canonical_string()
247 ),
248 }),
249 QueryValue::Boolean(b) => Ok(i64::from(*b)),
250 other => mismatch("i64", other),
251 }
252 }
253}
254
255impl FromSql for i128 {
256 fn from_sql(value: &QueryValue) -> Result<Self, ConversionError> {
257 match value {
258 QueryValue::Number(num) => num.to_i128().ok_or_else(|| ConversionError::OutOfRange {
260 expected: "i128",
261 detail: format!(
262 "NUMBER {:?} is not an integer that fits i128",
263 num.to_canonical_string()
264 ),
265 }),
266 QueryValue::Boolean(b) => Ok(i128::from(*b)),
267 other => mismatch("i128", other),
268 }
269 }
270}
271
272impl FromSql for i32 {
273 fn from_sql(value: &QueryValue) -> Result<Self, ConversionError> {
274 let wide = i64::from_sql(value)?;
275 i32::try_from(wide).map_err(|_| ConversionError::OutOfRange {
276 expected: "i32",
277 detail: format!("{wide} is out of range for i32"),
278 })
279 }
280}
281
282impl FromSql for u32 {
283 fn from_sql(value: &QueryValue) -> Result<Self, ConversionError> {
284 let wide = i64::from_sql(value)?;
285 u32::try_from(wide).map_err(|_| ConversionError::OutOfRange {
286 expected: "u32",
287 detail: format!("{wide} is out of range for u32"),
288 })
289 }
290}
291
292impl FromSql for f64 {
293 fn from_sql(value: &QueryValue) -> Result<Self, ConversionError> {
294 match value {
295 QueryValue::Number(num) => {
296 let text = num.to_canonical_string();
297 text.parse::<f64>()
298 .map_err(|_| ConversionError::OutOfRange {
299 expected: "f64",
300 detail: format!("{text:?} is not a finite f64"),
301 })
302 }
303 QueryValue::BinaryDouble(text) => {
304 text.trim()
305 .parse::<f64>()
306 .map_err(|_| ConversionError::OutOfRange {
307 expected: "f64",
308 detail: format!("{text:?} is not a finite f64"),
309 })
310 }
311 other => mismatch("f64", other),
312 }
313 }
314}
315
316impl FromSql for f32 {
317 fn from_sql(value: &QueryValue) -> Result<Self, ConversionError> {
318 let wide = f64::from_sql(value)?;
319 let narrowed = wide as f32;
320 if !narrowed.is_finite() {
321 return Err(ConversionError::OutOfRange {
322 expected: "f32",
323 detail: format!("{wide:?} is out of range for f32"),
324 });
325 }
326 Ok(narrowed)
327 }
328}
329
330impl FromSql for bool {
331 fn from_sql(value: &QueryValue) -> Result<Self, ConversionError> {
332 match value {
333 QueryValue::Boolean(b) => Ok(*b),
334 QueryValue::Number(num) => match num.to_i64() {
336 Some(0) => Ok(false),
337 Some(1) => Ok(true),
338 _ => Err(ConversionError::OutOfRange {
339 expected: "bool",
340 detail: format!("NUMBER {:?} is neither 0 nor 1", num.to_canonical_string()),
341 }),
342 },
343 other => mismatch("bool", other),
344 }
345 }
346}
347
348impl FromSql for String {
349 fn from_sql(value: &QueryValue) -> Result<Self, ConversionError> {
350 match value {
351 QueryValue::Text(s) => Ok(s.clone()),
352 QueryValue::Rowid(s) => Ok(s.clone()),
353 QueryValue::Number(num) => Ok(num.to_canonical_string()),
354 QueryValue::BinaryDouble(text) => Ok(text.clone()),
355 other => mismatch("String", other),
356 }
357 }
358}
359
360impl FromSql for Vec<u8> {
361 fn from_sql(value: &QueryValue) -> Result<Self, ConversionError> {
362 match value {
363 QueryValue::Raw(bytes) => Ok(bytes.clone()),
364 QueryValue::TextRaw { bytes, .. } => Ok(bytes.clone()),
365 other => mismatch("Vec<u8>", other),
366 }
367 }
368}
369
370impl<T: FromSql> FromSql for Option<T> {
371 fn from_sql(value: &QueryValue) -> Result<Self, ConversionError> {
372 T::from_sql(value).map(Some)
373 }
374}
375
376#[cfg(feature = "chrono")]
381mod chrono_impls {
382 use super::{mismatch, ConversionError, FromSql};
383 use chrono::{
384 DateTime, Duration as ChronoDuration, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime,
385 TimeZone, Utc,
386 };
387 use oracledb_protocol::thin::QueryValue;
388
389 fn naive_from_components(
390 year: i32,
391 month: u8,
392 day: u8,
393 hour: u8,
394 minute: u8,
395 second: u8,
396 nanosecond: u32,
397 ) -> Result<NaiveDateTime, ConversionError> {
398 let date =
399 NaiveDate::from_ymd_opt(year, u32::from(month), u32::from(day)).ok_or_else(|| {
400 ConversionError::OutOfRange {
401 expected: "chrono::NaiveDateTime",
402 detail: format!("invalid date {year:04}-{month:02}-{day:02}"),
403 }
404 })?;
405 let time = NaiveTime::from_hms_nano_opt(
406 u32::from(hour),
407 u32::from(minute),
408 u32::from(second),
409 nanosecond,
410 )
411 .ok_or_else(|| ConversionError::OutOfRange {
412 expected: "chrono::NaiveDateTime",
413 detail: format!("invalid time {hour:02}:{minute:02}:{second:02}.{nanosecond:09}"),
414 })?;
415 Ok(NaiveDateTime::new(date, time))
416 }
417
418 impl FromSql for NaiveDateTime {
419 fn from_sql(value: &QueryValue) -> Result<Self, ConversionError> {
420 match value {
421 QueryValue::DateTime {
422 year,
423 month,
424 day,
425 hour,
426 minute,
427 second,
428 nanosecond,
429 } => {
430 naive_from_components(*year, *month, *day, *hour, *minute, *second, *nanosecond)
431 }
432 QueryValue::TimestampTz {
433 year,
434 month,
435 day,
436 hour,
437 minute,
438 second,
439 nanosecond,
440 offset_minutes,
441 } => {
442 let naive = naive_from_components(
443 *year,
444 *month,
445 *day,
446 *hour,
447 *minute,
448 *second,
449 *nanosecond,
450 )?;
451 naive
452 .checked_add_signed(ChronoDuration::minutes(i64::from(*offset_minutes)))
453 .ok_or_else(|| ConversionError::OutOfRange {
454 expected: "chrono::NaiveDateTime",
455 detail: "TIMESTAMP WITH TIME ZONE offset adjustment overflow"
456 .to_string(),
457 })
458 }
459 other => mismatch("chrono::NaiveDateTime", other),
460 }
461 }
462 }
463
464 impl FromSql for NaiveDate {
465 fn from_sql(value: &QueryValue) -> Result<Self, ConversionError> {
466 match value {
467 QueryValue::DateTime {
468 year, month, day, ..
469 } => NaiveDate::from_ymd_opt(*year, u32::from(*month), u32::from(*day)).ok_or_else(
470 || ConversionError::OutOfRange {
471 expected: "chrono::NaiveDate",
472 detail: format!("invalid date {year:04}-{month:02}-{day:02}"),
473 },
474 ),
475 QueryValue::TimestampTz { .. } => {
476 let datetime = NaiveDateTime::from_sql(value)?;
477 Ok(datetime.date())
478 }
479 other => mismatch("chrono::NaiveDate", other),
480 }
481 }
482 }
483
484 fn fixed_offset_from_minutes(offset_minutes: i32) -> Result<FixedOffset, ConversionError> {
485 let seconds =
486 offset_minutes
487 .checked_mul(60)
488 .ok_or_else(|| ConversionError::OutOfRange {
489 expected: "chrono::FixedOffset",
490 detail: format!("offset minutes {offset_minutes} overflow seconds"),
491 })?;
492 FixedOffset::east_opt(seconds).ok_or_else(|| ConversionError::OutOfRange {
493 expected: "chrono::FixedOffset",
494 detail: format!("offset minutes {offset_minutes} out of chrono range"),
495 })
496 }
497
498 impl FromSql for DateTime<FixedOffset> {
499 fn from_sql(value: &QueryValue) -> Result<Self, ConversionError> {
500 match value {
501 QueryValue::TimestampTz {
502 year,
503 month,
504 day,
505 hour,
506 minute,
507 second,
508 nanosecond,
509 offset_minutes,
510 } => {
511 let naive_utc = naive_from_components(
519 *year,
520 *month,
521 *day,
522 *hour,
523 *minute,
524 *second,
525 *nanosecond,
526 )?;
527 Ok(fixed_offset_from_minutes(*offset_minutes)?.from_utc_datetime(&naive_utc))
528 }
529 QueryValue::DateTime {
530 year,
531 month,
532 day,
533 hour,
534 minute,
535 second,
536 nanosecond,
537 } => {
538 let naive = naive_from_components(
539 *year,
540 *month,
541 *day,
542 *hour,
543 *minute,
544 *second,
545 *nanosecond,
546 )?;
547 fixed_offset_from_minutes(0)?
548 .from_local_datetime(&naive)
549 .single()
550 .ok_or_else(|| ConversionError::OutOfRange {
551 expected: "chrono::DateTime<FixedOffset>",
552 detail: "invalid UTC local datetime".to_string(),
553 })
554 }
555 other => mismatch("chrono::DateTime<FixedOffset>", other),
556 }
557 }
558 }
559
560 impl FromSql for DateTime<Utc> {
561 fn from_sql(value: &QueryValue) -> Result<Self, ConversionError> {
562 DateTime::<FixedOffset>::from_sql(value).map(|datetime| datetime.with_timezone(&Utc))
563 }
564 }
565}
566
567#[cfg(feature = "uuid")]
572mod uuid_impls {
573 use super::{mismatch, ConversionError, FromSql};
574 use oracledb_protocol::thin::QueryValue;
575 use uuid::Uuid;
576
577 impl FromSql for Uuid {
578 fn from_sql(value: &QueryValue) -> Result<Self, ConversionError> {
579 match value {
580 QueryValue::Raw(bytes) => {
581 let array: [u8; 16] =
582 bytes
583 .as_slice()
584 .try_into()
585 .map_err(|_| ConversionError::OutOfRange {
586 expected: "uuid::Uuid",
587 detail: format!("RAW length {} is not 16 bytes", bytes.len()),
588 })?;
589 Ok(Uuid::from_bytes(array))
590 }
591 QueryValue::Text(text) => {
592 Uuid::parse_str(text.trim()).map_err(|err| ConversionError::OutOfRange {
593 expected: "uuid::Uuid",
594 detail: format!("text {text:?} is not a UUID: {err}"),
595 })
596 }
597 other => mismatch("uuid::Uuid", other),
598 }
599 }
600 }
601}
602
603#[cfg(feature = "serde_json")]
608mod serde_json_impls {
609 use super::{mismatch, ConversionError, FromSql};
610 use oracledb_protocol::oson::OsonValue;
611 use oracledb_protocol::thin::QueryValue;
612 use serde_json::{Map, Number, Value};
613
614 fn oson_to_json(node: &OsonValue) -> Value {
618 match node {
619 OsonValue::Null => Value::Null,
620 OsonValue::Bool(b) => Value::Bool(*b),
621 OsonValue::Number(text) => number_to_json(text),
622 OsonValue::BinaryFloat(v) => f64_to_json(f64::from(*v)),
623 OsonValue::BinaryDouble(v) => f64_to_json(*v),
624 OsonValue::String(s) => Value::String(s.clone()),
625 OsonValue::Raw(bytes) => Value::String(hex_encode(bytes)),
626 OsonValue::DateTime {
627 year,
628 month,
629 day,
630 hour,
631 minute,
632 second,
633 nanosecond,
634 } => Value::String(format!(
635 "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}.{nanosecond:09}"
636 )),
637 OsonValue::IntervalDS {
638 days,
639 hours,
640 minutes,
641 seconds,
642 fseconds,
643 } => Value::String(format!(
644 "P{days}DT{hours}H{minutes}M{seconds}.{fseconds:09}S"
645 )),
646 OsonValue::Vector(_) => Value::String("<vector>".to_string()),
647 OsonValue::Array(items) => Value::Array(items.iter().map(oson_to_json).collect()),
648 OsonValue::Object(entries) => {
649 let mut map = Map::with_capacity(entries.len());
650 for (key, val) in entries {
651 map.insert(key.clone(), oson_to_json(val));
652 }
653 Value::Object(map)
654 }
655 }
656 }
657
658 fn number_to_json(text: &str) -> Value {
659 let trimmed = text.trim();
660 if let Ok(i) = trimmed.parse::<i64>() {
661 return Value::Number(Number::from(i));
662 }
663 if let Ok(u) = trimmed.parse::<u64>() {
664 return Value::Number(Number::from(u));
665 }
666 if significant_digit_count(trimmed) <= 15 {
667 if let Ok(f) = trimmed.parse::<f64>() {
668 if let Some(n) = Number::from_f64(f) {
669 return Value::Number(n);
670 }
671 }
672 }
673 Value::String(trimmed.to_string())
675 }
676
677 fn significant_digit_count(text: &str) -> usize {
678 let mantissa = text
679 .split_once(['e', 'E'])
680 .map_or(text, |(mantissa, _)| mantissa)
681 .trim_start_matches(['+', '-']);
682 let mut seen_non_zero = false;
683 let mut count = 0usize;
684 for ch in mantissa.chars().filter(|ch| ch.is_ascii_digit()) {
685 if ch != '0' || seen_non_zero {
686 seen_non_zero = true;
687 count += 1;
688 }
689 }
690 count
691 }
692
693 fn f64_to_json(v: f64) -> Value {
694 Number::from_f64(v).map_or_else(|| Value::String(v.to_string()), Value::Number)
695 }
696
697 fn hex_encode(bytes: &[u8]) -> String {
698 let mut out = String::with_capacity(bytes.len() * 2);
699 for byte in bytes {
700 out.push_str(&format!("{byte:02x}"));
701 }
702 out
703 }
704
705 impl FromSql for Value {
706 fn from_sql(value: &QueryValue) -> Result<Self, ConversionError> {
707 match value {
708 QueryValue::Json(oson) => Ok(oson_to_json(oson)),
709 QueryValue::Text(text) => {
712 serde_json::from_str(text).map_err(|err| ConversionError::OutOfRange {
713 expected: "serde_json::Value",
714 detail: format!("text is not valid JSON: {err}"),
715 })
716 }
717 other => mismatch("serde_json::Value", other),
718 }
719 }
720 }
721}
722
723#[cfg(feature = "rust_decimal")]
728mod rust_decimal_impls {
729 use super::{mismatch, ConversionError, FromSql};
730 use oracledb_protocol::thin::QueryValue;
731 use rust_decimal::Decimal;
732 use std::str::FromStr;
733
734 impl FromSql for Decimal {
735 fn from_sql(value: &QueryValue) -> Result<Self, ConversionError> {
736 match value {
737 QueryValue::Number(num) => {
738 if let (Some(coefficient), Some(scale)) = (num.coefficient(), num.scale()) {
743 if (0..=28).contains(&scale) {
744 if let Ok(dec) = Decimal::try_from_i128_with_scale(
745 coefficient,
746 u32::from(scale as u16),
747 ) {
748 return Ok(dec);
749 }
750 }
751 }
752 let text = num.to_canonical_string();
755 Decimal::from_str(&text).or_else(|_| {
756 Decimal::from_scientific(&text).map_err(|err| ConversionError::OutOfRange {
757 expected: "rust_decimal::Decimal",
758 detail: format!("NUMBER {text:?} does not fit Decimal: {err}"),
759 })
760 })
761 }
762 other => mismatch("rust_decimal::Decimal", other),
763 }
764 }
765 }
766}
767
768use oracledb_protocol::vector::{Vector, VectorValues};
773
774impl FromSql for Vec<f32> {
775 fn from_sql(value: &QueryValue) -> Result<Self, ConversionError> {
776 match value {
777 QueryValue::Vector(vector) => vector_to_f32(vector),
778 other => mismatch("Vec<f32>", other),
779 }
780 }
781}
782
783impl FromSql for Vec<f64> {
784 fn from_sql(value: &QueryValue) -> Result<Self, ConversionError> {
785 match value {
786 QueryValue::Vector(vector) => vector_to_f64(vector),
787 other => mismatch("Vec<f64>", other),
788 }
789 }
790}
791
792fn dense_values(vector: &Vector) -> Result<&VectorValues, ConversionError> {
793 match vector {
794 Vector::Dense(values) => Ok(values),
795 Vector::Sparse { .. } => Err(ConversionError::OutOfRange {
796 expected: "Vec<element>",
797 detail: "sparse VECTOR cannot be read as a dense Vec".to_string(),
798 }),
799 }
800}
801
802fn vector_to_f32(vector: &Vector) -> Result<Vec<f32>, ConversionError> {
803 match dense_values(vector)? {
804 VectorValues::Float32(v) => Ok(v.clone()),
805 VectorValues::Float64(v) => Ok(v.iter().map(|x| *x as f32).collect()),
806 VectorValues::Int8(v) => Ok(v.iter().map(|x| f32::from(*x)).collect()),
807 VectorValues::Binary(_) => Err(ConversionError::OutOfRange {
808 expected: "Vec<f32>",
809 detail: "BINARY-format VECTOR has no float elements".to_string(),
810 }),
811 }
812}
813
814fn vector_to_f64(vector: &Vector) -> Result<Vec<f64>, ConversionError> {
815 match dense_values(vector)? {
816 VectorValues::Float64(v) => Ok(v.clone()),
817 VectorValues::Float32(v) => Ok(v.iter().map(|x| f64::from(*x)).collect()),
818 VectorValues::Int8(v) => Ok(v.iter().map(|x| f64::from(*x)).collect()),
819 VectorValues::Binary(_) => Err(ConversionError::OutOfRange {
820 expected: "Vec<f64>",
821 detail: "BINARY-format VECTOR has no float elements".to_string(),
822 }),
823 }
824}
825
826pub trait QueryResultExt {
837 fn get<T: FromSql>(&self, row: usize, col: usize) -> crate::Result<T>;
841
842 fn get_by_name<T: FromSql>(&self, row: usize, name: &str) -> crate::Result<T>;
846
847 fn typed_row(&self, row: usize) -> TypedRow<'_>;
850
851 fn rows_as<T: FromRow>(&self) -> crate::Result<Vec<T>>;
872}
873
874fn convert_cell<T: FromSql>(cell: Option<&Option<QueryValue>>, what: String) -> crate::Result<T> {
875 convert_cell_ce(cell, what).map_err(crate::Error::Conversion)
876}
877
878fn convert_cell_ce<T: FromSql>(
884 cell: Option<&Option<QueryValue>>,
885 what: String,
886) -> Result<T, ConversionError> {
887 match cell {
888 None => Err(ConversionError::OutOfRange {
889 expected: std::any::type_name::<T>(),
890 detail: what,
891 }),
892 Some(None) => Err(ConversionError::UnexpectedNull),
893 Some(Some(value)) => T::from_sql(value),
894 }
895}
896
897fn convert_cell_opt_ce<T: FromSql>(
903 cell: Option<&Option<QueryValue>>,
904 what: String,
905) -> Result<Option<T>, ConversionError> {
906 match cell {
907 None => Err(ConversionError::OutOfRange {
908 expected: std::any::type_name::<Option<T>>(),
909 detail: what,
910 }),
911 Some(None) => Ok(None),
912 Some(Some(value)) => T::from_sql(value).map(Some),
913 }
914}
915
916impl QueryResultExt for QueryResult {
917 fn get<T: FromSql>(&self, row: usize, col: usize) -> crate::Result<T> {
918 let cell = self.rows.get(row).and_then(|r| r.get(col));
919 convert_cell(cell, format!("no cell at (row {row}, col {col})"))
920 }
921
922 fn get_by_name<T: FromSql>(&self, row: usize, name: &str) -> crate::Result<T> {
923 match self.column_index(name) {
924 Some(col) => self.get(row, col),
925 None => Err(crate::Error::Conversion(ConversionError::OutOfRange {
926 expected: std::any::type_name::<T>(),
927 detail: format!("no column named {name:?}"),
928 })),
929 }
930 }
931
932 fn typed_row(&self, row: usize) -> TypedRow<'_> {
933 let cells = self.rows.get(row).map_or(&[][..], Vec::as_slice);
934 TypedRow::new(&self.columns, cells, row)
935 }
936
937 fn rows_as<T: FromRow>(&self) -> crate::Result<Vec<T>> {
938 let mut out = Vec::with_capacity(self.rows.len());
939 for row in 0..self.rows.len() {
940 out.push(T::from_row(&self.typed_row(row))?);
941 }
942 Ok(out)
943 }
944}
945
946#[derive(Clone, Copy)]
949pub struct TypedRow<'a> {
950 columns: &'a [ColumnMetadata],
951 cells: &'a [Option<QueryValue>],
952 row: usize,
953}
954
955impl TypedRow<'_> {
956 pub(crate) fn new<'a>(
957 columns: &'a [ColumnMetadata],
958 cells: &'a [Option<QueryValue>],
959 row: usize,
960 ) -> TypedRow<'a> {
961 TypedRow {
962 columns,
963 cells,
964 row,
965 }
966 }
967
968 pub fn get<T: FromSql>(&self, col: usize) -> crate::Result<T> {
970 convert_cell(
971 self.cell_at(col),
972 format!("no cell at (row {}, col {col})", self.row),
973 )
974 }
975
976 pub fn get_by_name<T: FromSql>(&self, name: &str) -> crate::Result<T> {
979 match column_index(self.columns, name) {
980 Some(col) => self.get(col),
981 None => Err(crate::Error::Conversion(ConversionError::OutOfRange {
982 expected: std::any::type_name::<T>(),
983 detail: format!("no column named {name:?}"),
984 })),
985 }
986 }
987
988 fn cell_at(&self, col: usize) -> Option<&Option<QueryValue>> {
992 self.cells.get(col)
993 }
994
995 pub fn try_get<T: FromSql>(&self, col: usize) -> Result<T, ConversionError> {
1005 convert_cell_ce(
1006 self.cell_at(col),
1007 format!("no cell at (row {}, col {col})", self.row),
1008 )
1009 }
1010
1011 pub fn try_get_opt<T: FromSql>(&self, col: usize) -> Result<Option<T>, ConversionError> {
1015 convert_cell_opt_ce(
1016 self.cell_at(col),
1017 format!("no cell at (row {}, col {col})", self.row),
1018 )
1019 }
1020
1021 pub fn try_get_by_name<T: FromSql>(&self, name: &str) -> Result<T, ConversionError> {
1031 match column_index(self.columns, name) {
1032 Some(col) => self.try_get(col),
1033 None => Err(ConversionError::OutOfRange {
1034 expected: std::any::type_name::<T>(),
1035 detail: format!("no column named {name:?}"),
1036 }),
1037 }
1038 }
1039
1040 pub fn try_get_by_name_opt<T: FromSql>(
1044 &self,
1045 name: &str,
1046 ) -> Result<Option<T>, ConversionError> {
1047 match column_index(self.columns, name) {
1048 Some(col) => self.try_get_opt(col),
1049 None => Err(ConversionError::OutOfRange {
1050 expected: std::any::type_name::<Option<T>>(),
1051 detail: format!("no column named {name:?}"),
1052 }),
1053 }
1054 }
1055}
1056
1057fn column_index(columns: &[ColumnMetadata], name: &str) -> Option<usize> {
1058 columns
1059 .iter()
1060 .position(|col| col.name().eq_ignore_ascii_case(name))
1061}
1062
1063pub trait FromRow: Sized {
1102 fn from_row(row: &TypedRow<'_>) -> Result<Self, ConversionError>;
1105}
1106
1107pub trait ToSql {
1119 fn to_sql(&self) -> BindValue;
1121
1122 fn try_to_sql(&self) -> Result<BindValue, ConversionError> {
1132 Ok(self.to_sql())
1133 }
1134}
1135
1136impl ToSql for i64 {
1137 fn to_sql(&self) -> BindValue {
1138 BindValue::Number(self.to_string())
1139 }
1140}
1141
1142impl ToSql for i32 {
1143 fn to_sql(&self) -> BindValue {
1144 BindValue::Number(self.to_string())
1145 }
1146}
1147
1148impl ToSql for u32 {
1149 fn to_sql(&self) -> BindValue {
1150 BindValue::Number(self.to_string())
1151 }
1152}
1153
1154impl ToSql for f64 {
1155 fn to_sql(&self) -> BindValue {
1156 BindValue::BinaryDouble(*self)
1157 }
1158}
1159
1160impl ToSql for f32 {
1161 fn to_sql(&self) -> BindValue {
1162 BindValue::BinaryFloat(f64::from(*self))
1163 }
1164}
1165
1166impl ToSql for bool {
1167 fn to_sql(&self) -> BindValue {
1168 BindValue::Boolean(*self)
1169 }
1170}
1171
1172impl ToSql for str {
1173 fn to_sql(&self) -> BindValue {
1174 BindValue::Text(self.to_string())
1175 }
1176}
1177
1178impl ToSql for String {
1179 fn to_sql(&self) -> BindValue {
1180 BindValue::Text(self.clone())
1181 }
1182}
1183
1184impl ToSql for [u8] {
1185 fn to_sql(&self) -> BindValue {
1186 BindValue::Raw(self.to_vec())
1187 }
1188}
1189
1190impl ToSql for Vec<u8> {
1191 fn to_sql(&self) -> BindValue {
1192 BindValue::Raw(self.clone())
1193 }
1194}
1195
1196impl<T: ToSql + ?Sized> ToSql for &T {
1197 fn to_sql(&self) -> BindValue {
1198 (**self).to_sql()
1199 }
1200
1201 fn try_to_sql(&self) -> Result<BindValue, ConversionError> {
1202 (**self).try_to_sql()
1203 }
1204}
1205
1206impl<T: ToSql> ToSql for Option<T> {
1207 fn to_sql(&self) -> BindValue {
1208 match self {
1209 Some(value) => value.to_sql(),
1210 None => BindValue::Null,
1211 }
1212 }
1213
1214 fn try_to_sql(&self) -> Result<BindValue, ConversionError> {
1215 match self {
1216 Some(value) => value.try_to_sql(),
1217 None => Ok(BindValue::Null),
1218 }
1219 }
1220}
1221
1222#[cfg(feature = "chrono")]
1223mod chrono_to_sql {
1224 use super::{ConversionError, ToSql};
1225 use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, NaiveDateTime, Timelike, Utc};
1226 use oracledb_protocol::thin::BindValue;
1227
1228 impl ToSql for NaiveDateTime {
1229 fn to_sql(&self) -> BindValue {
1230 BindValue::Timestamp {
1231 ora_type_num: 180,
1233 year: self.year(),
1234 month: self.month() as u8,
1235 day: self.day() as u8,
1236 hour: self.hour() as u8,
1237 minute: self.minute() as u8,
1238 second: self.second() as u8,
1239 nanosecond: self.nanosecond(),
1240 }
1241 }
1242 }
1243
1244 impl ToSql for NaiveDate {
1245 fn to_sql(&self) -> BindValue {
1246 BindValue::DateTime {
1247 year: self.year(),
1248 month: self.month() as u8,
1249 day: self.day() as u8,
1250 hour: 0,
1251 minute: 0,
1252 second: 0,
1253 }
1254 }
1255 }
1256
1257 impl ToSql for DateTime<FixedOffset> {
1258 fn to_sql(&self) -> BindValue {
1259 self.try_to_sql().unwrap_or_else(|error| {
1260 panic!(
1261 "cannot convert chrono::DateTime<FixedOffset> to an Oracle TIMESTAMP WITH TIME ZONE without losing offset precision: {error}; use ToSql::try_to_sql to handle this conversion error"
1262 )
1263 })
1264 }
1265
1266 fn try_to_sql(&self) -> Result<BindValue, ConversionError> {
1267 let utc = self.naive_utc();
1275 let offset_seconds = self.offset().local_minus_utc();
1276 let residual_seconds = offset_seconds.rem_euclid(60);
1277 if residual_seconds != 0 {
1278 return Err(ConversionError::OutOfRange {
1279 expected: "Oracle TIMESTAMP WITH TIME ZONE offset",
1280 detail: format!(
1281 "UTC offset {offset_seconds:+} seconds contains {residual_seconds} sub-minute second(s) that Oracle TIMESTAMP WITH TIME ZONE cannot represent"
1282 ),
1283 });
1284 }
1285
1286 Ok(BindValue::TimestampTz {
1287 year: utc.year(),
1288 month: utc.month() as u8,
1289 day: utc.day() as u8,
1290 hour: utc.hour() as u8,
1291 minute: utc.minute() as u8,
1292 second: utc.second() as u8,
1293 nanosecond: utc.nanosecond(),
1294 offset_minutes: offset_seconds / 60,
1295 })
1296 }
1297 }
1298
1299 impl ToSql for DateTime<Utc> {
1300 fn to_sql(&self) -> BindValue {
1301 BindValue::TimestampTz {
1302 year: self.year(),
1303 month: self.month() as u8,
1304 day: self.day() as u8,
1305 hour: self.hour() as u8,
1306 minute: self.minute() as u8,
1307 second: self.second() as u8,
1308 nanosecond: self.nanosecond(),
1309 offset_minutes: 0,
1310 }
1311 }
1312 }
1313}
1314
1315#[cfg(feature = "uuid")]
1316mod uuid_to_sql {
1317 use super::ToSql;
1318 use oracledb_protocol::thin::BindValue;
1319 use uuid::Uuid;
1320
1321 impl ToSql for Uuid {
1322 fn to_sql(&self) -> BindValue {
1323 BindValue::Raw(self.as_bytes().to_vec())
1324 }
1325 }
1326}
1327
1328#[cfg(feature = "serde_json")]
1329mod serde_json_to_sql {
1330 use super::ToSql;
1331 use oracledb_protocol::thin::BindValue;
1332 use serde_json::Value;
1333
1334 impl ToSql for Value {
1335 fn to_sql(&self) -> BindValue {
1336 BindValue::Text(self.to_string())
1339 }
1340 }
1341}
1342
1343#[cfg(feature = "rust_decimal")]
1344mod rust_decimal_to_sql {
1345 use super::ToSql;
1346 use oracledb_protocol::thin::BindValue;
1347 use rust_decimal::Decimal;
1348
1349 impl ToSql for Decimal {
1350 fn to_sql(&self) -> BindValue {
1351 BindValue::Number(self.to_string())
1355 }
1356 }
1357}
1358
1359impl ToSql for Vec<f32> {
1360 fn to_sql(&self) -> BindValue {
1361 BindValue::Vector(Vector::Dense(VectorValues::Float32(self.clone())))
1362 }
1363}
1364
1365impl ToSql for [f32] {
1366 fn to_sql(&self) -> BindValue {
1367 BindValue::Vector(Vector::Dense(VectorValues::Float32(self.to_vec())))
1368 }
1369}
1370
1371#[derive(Clone, Debug, Default, PartialEq)]
1383pub enum Params<'a> {
1384 #[default]
1386 None,
1387 Positional(Cow<'a, [BindValue]>),
1389 Named(Cow<'a, [(String, BindValue)]>),
1391}
1392
1393impl<'a, T: IntoBinds> From<T> for Params<'a> {
1394 fn from(value: T) -> Self {
1395 Params::Positional(Cow::Owned(value.into_binds()))
1396 }
1397}
1398
1399impl<'a> From<&'a [BindValue]> for Params<'a> {
1400 fn from(value: &'a [BindValue]) -> Self {
1401 Params::Positional(Cow::Borrowed(value))
1402 }
1403}
1404
1405impl<'a> From<&'a Vec<BindValue>> for Params<'a> {
1406 fn from(value: &'a Vec<BindValue>) -> Self {
1407 Params::from(value.as_slice())
1408 }
1409}
1410
1411impl<'a> From<Vec<(String, BindValue)>> for Params<'a> {
1412 fn from(value: Vec<(String, BindValue)>) -> Self {
1413 Params::Named(Cow::Owned(value))
1414 }
1415}
1416
1417impl<'a> From<&'a [(String, BindValue)]> for Params<'a> {
1418 fn from(value: &'a [(String, BindValue)]) -> Self {
1419 Params::Named(Cow::Borrowed(value))
1420 }
1421}
1422
1423impl<'a> From<&'a Vec<(String, BindValue)>> for Params<'a> {
1424 fn from(value: &'a Vec<(String, BindValue)>) -> Self {
1425 Params::from(value.as_slice())
1426 }
1427}
1428
1429pub trait IntoBinds {
1437 fn into_binds(self) -> Vec<BindValue>;
1439}
1440
1441impl IntoBinds for Vec<BindValue> {
1442 fn into_binds(self) -> Vec<BindValue> {
1443 self
1444 }
1445}
1446
1447impl IntoBinds for () {
1448 fn into_binds(self) -> Vec<BindValue> {
1449 Vec::new()
1450 }
1451}
1452
1453impl<T: ToSql> IntoBinds for Vec<T> {
1454 fn into_binds(self) -> Vec<BindValue> {
1455 self.iter().map(ToSql::to_sql).collect()
1456 }
1457}
1458
1459impl<T: ToSql, const N: usize> IntoBinds for [T; N] {
1460 fn into_binds(self) -> Vec<BindValue> {
1461 self.iter().map(ToSql::to_sql).collect()
1462 }
1463}
1464
1465macro_rules! impl_into_binds_tuple {
1466 ($($name:ident),+) => {
1467 impl<$($name: ToSql),+> IntoBinds for ($($name,)+) {
1468 fn into_binds(self) -> Vec<BindValue> {
1469 #[allow(non_snake_case)]
1470 let ($($name,)+) = self;
1471 vec![$($name.to_sql()),+]
1472 }
1473 }
1474 };
1475}
1476
1477impl_into_binds_tuple!(A);
1478impl_into_binds_tuple!(A, B);
1479impl_into_binds_tuple!(A, B, C);
1480impl_into_binds_tuple!(A, B, C, D);
1481impl_into_binds_tuple!(A, B, C, D, E);
1482impl_into_binds_tuple!(A, B, C, D, E, F);
1483impl_into_binds_tuple!(A, B, C, D, E, F, G);
1484impl_into_binds_tuple!(A, B, C, D, E, F, G, H);
1485impl_into_binds_tuple!(A, B, C, D, E, F, G, H, I);
1486impl_into_binds_tuple!(A, B, C, D, E, F, G, H, I, J);
1487impl_into_binds_tuple!(A, B, C, D, E, F, G, H, I, J, K);
1488impl_into_binds_tuple!(A, B, C, D, E, F, G, H, I, J, K, L);
1489
1490#[macro_export]
1506macro_rules! params {
1507 ($($name:expr => $value:expr),+ $(,)?) => {{
1509 let binds: ::std::vec::Vec<(::std::string::String, $crate::protocol::thin::BindValue)> =
1510 ::std::vec![$(
1511 (::std::string::String::from($name), $crate::ToSql::to_sql(&$value))
1512 ),+];
1513 binds
1514 }};
1515 ($($value:expr),+ $(,)?) => {{
1517 let binds: ::std::vec::Vec<$crate::protocol::thin::BindValue> =
1518 ::std::vec![$( $crate::ToSql::to_sql(&$value) ),+];
1519 binds
1520 }};
1521}
1522
1523pub(crate) fn order_named_binds(
1531 sql: &str,
1532 named: Vec<(String, BindValue)>,
1533) -> Result<Vec<BindValue>, BindError> {
1534 if !bind_shape_validation_enabled(sql) {
1535 return Ok(named.into_iter().map(|(_, value)| value).collect());
1536 }
1537
1538 let order = sql::unique_bind_names(sql)?;
1539 let mut remaining = named;
1540 let mut out = Vec::with_capacity(remaining.len());
1541 for placeholder in &order {
1542 if let Some(pos) = remaining
1543 .iter()
1544 .position(|(name, _)| sql::bind_name_matches_key(placeholder, name))
1545 {
1546 let (_, value) = remaining.remove(pos);
1547 out.push(value);
1548 } else {
1549 return Err(BindError::MissingNamedBind {
1550 name: placeholder.clone(),
1551 });
1552 }
1553 }
1554 if let Some((name, _)) = remaining.first() {
1555 return Err(BindError::ExtraNamedBind { name: name.clone() });
1556 }
1557 Ok(out)
1558}
1559
1560pub(crate) fn resolve_params(sql: &str, params: Params<'_>) -> Result<Vec<BindValue>, BindError> {
1563 match params {
1564 Params::None => {
1565 validate_positional_bind_count(sql, 0)?;
1566 Ok(Vec::new())
1567 }
1568 Params::Positional(binds) => {
1569 let binds = binds.into_owned();
1570 validate_positional_bind_count(sql, binds.len())?;
1571 Ok(binds)
1572 }
1573 Params::Named(named) => order_named_binds(sql, named.into_owned()),
1574 }
1575}
1576
1577pub(crate) fn validate_bind_rows_shape(
1578 _sql: &str,
1579 bind_rows: &[Vec<BindValue>],
1580) -> Result<(), BindError> {
1581 let Some(first_row) = bind_rows.first() else {
1593 return Ok(());
1594 };
1595 let expected = first_row.len();
1596 for (row_index, row) in bind_rows.iter().enumerate().skip(1) {
1597 if row.len() != expected {
1598 return Err(BindError::BatchRowWidthMismatch {
1599 row_index,
1600 expected,
1601 actual: row.len(),
1602 });
1603 }
1604 }
1605 Ok(())
1606}
1607
1608pub(crate) fn validate_positional_bind_count(sql: &str, actual: usize) -> Result<(), BindError> {
1609 if !bind_shape_validation_enabled(sql) {
1610 return Ok(());
1611 }
1612 let expected = sql::bind_names_per_occurrence(sql)?.len();
1613 if expected != actual {
1614 return Err(BindError::PositionalCountMismatch { expected, actual });
1615 }
1616 Ok(())
1617}
1618
1619fn bind_shape_validation_enabled(sql: &str) -> bool {
1620 !sql::statement_is_ddl(sql)
1621}
1622
1623pub(crate) fn validate_bind_rows_types(bind_rows: &[Vec<BindValue>]) -> Result<(), BindError> {
1646 use oracledb_protocol::thin::{bind_value_type_info, public_dbtype_name_from_bind};
1647
1648 let Some(first_row) = bind_rows.first() else {
1649 return Ok(());
1650 };
1651 if bind_rows.len() < 2 {
1652 return Ok(());
1653 }
1654 for column_index in 0..first_row.len() {
1655 let mut established: Option<(u8, u8, &'static str)> = None;
1657 for (row_index, row) in bind_rows.iter().enumerate() {
1658 let Some(value) = row.get(column_index) else {
1659 continue;
1661 };
1662 let Some(info) = bind_value_type_info(value) else {
1663 continue; };
1665 let family = crate::bind_type_family(info.ora_type_num);
1666 match established {
1667 None => {
1668 established = Some((family, info.csfrm, public_dbtype_name_from_bind(value)));
1669 }
1670 Some((established_family, established_csfrm, expected)) => {
1671 if family != established_family || info.csfrm != established_csfrm {
1672 return Err(BindError::BatchColumnTypeMismatch {
1673 row_index,
1674 column_index,
1675 expected,
1676 actual: public_dbtype_name_from_bind(value),
1677 });
1678 }
1679 }
1680 }
1681 }
1682 }
1683 Ok(())
1684}
1685
1686pub fn declared_bind_count(statement: &str) -> Result<usize, BindError> {
1698 Ok(sql::bind_names_per_occurrence(statement)?.len())
1699}
1700
1701pub fn check_positional_binds(statement: &str, supplied: usize) -> Result<(), BindError> {
1708 validate_positional_bind_count(statement, supplied)
1709}
1710
1711pub fn check_bind_rows(statement: &str, bind_rows: &[Vec<BindValue>]) -> Result<(), BindError> {
1717 validate_bind_rows_shape(statement, bind_rows)?;
1718 validate_bind_rows_types(bind_rows)
1719}
1720
1721#[cfg(test)]
1722mod tests {
1723 use super::*;
1724 use oracledb_protocol::thin::ColumnMetadata;
1725
1726 fn num(text: &str) -> QueryValue {
1727 QueryValue::number_from_text(text, !text.contains('.'))
1728 }
1729
1730 #[test]
1731 fn core_from_sql_scalars() {
1732 assert_eq!(i64::from_sql(&num("42")).unwrap(), 42);
1733 assert_eq!(i32::from_sql(&num("42")).unwrap(), 42);
1734 assert_eq!(u32::from_sql(&num("42")).unwrap(), 42);
1735 assert_eq!(f64::from_sql(&num("2.5")).unwrap(), 2.5);
1736 assert_eq!(f32::from_sql(&num("2.5")).unwrap(), 2.5_f32);
1737 assert!(bool::from_sql(&QueryValue::Boolean(true)).unwrap());
1738 assert!(bool::from_sql(&num("1")).unwrap());
1739 assert_eq!(
1740 String::from_sql(&QueryValue::Text("hi".into())).unwrap(),
1741 "hi"
1742 );
1743 assert_eq!(
1744 Vec::<u8>::from_sql(&QueryValue::Raw(vec![1, 2, 3])).unwrap(),
1745 vec![1, 2, 3]
1746 );
1747 }
1748
1749 #[test]
1750 fn f32_from_sql_rejects_number_overflow() {
1751 let err = f32::from_sql(&num("1e39")).expect_err("finite f64 outside f32 range must fail");
1752 assert!(matches!(
1753 err,
1754 ConversionError::OutOfRange {
1755 expected: "f32",
1756 ..
1757 }
1758 ));
1759
1760 assert_eq!(
1761 f32::from_sql(&num("1.5")).expect("normal f32 value must convert"),
1762 1.5_f32
1763 );
1764 }
1765
1766 #[test]
1767 fn i128_from_sql_is_exact_beyond_i64() {
1768 let big = "123456789012345678901234567890";
1771 assert_eq!(
1772 i128::from_sql(&num(big)).unwrap(),
1773 123_456_789_012_345_678_901_234_567_890_i128
1774 );
1775 assert!(matches!(
1777 i64::from_sql(&num(big)).unwrap_err(),
1778 ConversionError::OutOfRange { .. }
1779 ));
1780 assert!(matches!(
1782 i128::from_sql(&num("3.14")).unwrap_err(),
1783 ConversionError::OutOfRange { .. }
1784 ));
1785 }
1786
1787 #[test]
1788 fn string_from_sql_is_canonical_byte_exact() {
1789 for text in ["0", "-1", "2.5", "100", "0.001", "12345678901234567890"] {
1791 assert_eq!(String::from_sql(&num(text)).unwrap(), text);
1792 }
1793 }
1794
1795 #[test]
1796 fn from_sql_errors_are_typed() {
1797 let err = i64::from_sql(&QueryValue::Text("x".into())).unwrap_err();
1799 assert!(matches!(err, ConversionError::TypeMismatch { .. }));
1800 let err = i32::from_sql(&num("9999999999")).unwrap_err();
1802 assert!(matches!(err, ConversionError::OutOfRange { .. }));
1803 }
1804
1805 #[test]
1806 fn option_accepts_any() {
1807 let v: Option<i64> = Option::<i64>::from_sql(&num("7")).unwrap();
1808 assert_eq!(v, Some(7));
1809 }
1810
1811 #[test]
1812 fn core_to_sql_scalars() {
1813 assert_eq!(40_i64.to_sql(), BindValue::Number("40".into()));
1814 assert_eq!(40_i32.to_sql(), BindValue::Number("40".into()));
1815 assert_eq!(2.5_f64.to_sql(), BindValue::BinaryDouble(2.5));
1816 assert_eq!(true.to_sql(), BindValue::Boolean(true));
1817 assert_eq!("alice".to_sql(), BindValue::Text("alice".into()));
1818 assert_eq!(vec![1u8, 2, 3].to_sql(), BindValue::Raw(vec![1, 2, 3]));
1819 let none: Option<i64> = None;
1820 assert_eq!(none.to_sql(), BindValue::Null);
1821 }
1822
1823 #[test]
1824 fn into_binds_tuple_and_slice() {
1825 let binds = (40_i64, "alice").into_binds();
1826 assert_eq!(
1827 binds,
1828 vec![
1829 BindValue::Number("40".into()),
1830 BindValue::Text("alice".into())
1831 ]
1832 );
1833 let binds = [1_i64, 2, 3].into_binds();
1834 assert_eq!(binds.len(), 3);
1835 }
1836
1837 #[test]
1838 fn params_macro_positional_and_named() {
1839 let positional = params![40_i64, "alice"];
1840 assert_eq!(
1841 positional,
1842 vec![
1843 BindValue::Number("40".into()),
1844 BindValue::Text("alice".into())
1845 ]
1846 );
1847 let named = params! { ":id" => 40_i64, ":name" => "alice" };
1848 assert_eq!(named.len(), 2);
1849 assert_eq!(named[0].0, ":id");
1850 assert_eq!(named[0].1, BindValue::Number("40".into()));
1851 assert_eq!(named[1].0, ":name");
1852 }
1853
1854 #[test]
1855 fn params_from_positional_sources() {
1856 assert_eq!(Params::default(), Params::None);
1857 assert_eq!(Params::from(()), Params::Positional(Cow::Owned(Vec::new())));
1858
1859 let from_tuple = Params::from((40_i64, "alice"));
1860 assert_eq!(
1861 from_tuple,
1862 Params::Positional(Cow::Owned(vec![
1863 BindValue::Number("40".into()),
1864 BindValue::Text("alice".into())
1865 ]))
1866 );
1867
1868 let raw = vec![BindValue::Number("7".into()), BindValue::Boolean(true)];
1869 assert_eq!(
1870 Params::from(raw.clone()),
1871 Params::Positional(Cow::Owned(raw.clone()))
1872 );
1873 assert_eq!(
1874 Params::from(raw.as_slice()),
1875 Params::Positional(Cow::Borrowed(raw.as_slice()))
1876 );
1877 assert_eq!(
1878 Params::from(&raw),
1879 Params::Positional(Cow::Borrowed(raw.as_slice()))
1880 );
1881
1882 let empty: &[BindValue] = &[];
1883 assert_eq!(
1884 Params::from(empty),
1885 Params::Positional(Cow::Borrowed(empty))
1886 );
1887 }
1888
1889 #[test]
1890 fn params_from_named_sources() {
1891 let named = params! { ":id" => 40_i64, ":name" => "alice" };
1892
1893 assert_eq!(
1894 Params::from(named.clone()),
1895 Params::Named(Cow::Owned(named.clone()))
1896 );
1897 assert_eq!(
1898 Params::from(named.as_slice()),
1899 Params::Named(Cow::Borrowed(named.as_slice()))
1900 );
1901 assert_eq!(
1902 Params::from(&named),
1903 Params::Named(Cow::Borrowed(named.as_slice()))
1904 );
1905
1906 let empty: Vec<(String, BindValue)> = Vec::new();
1907 assert_eq!(
1908 Params::from(empty.clone()),
1909 Params::Named(Cow::Owned(empty.clone()))
1910 );
1911 assert_eq!(
1912 Params::from(empty.as_slice()),
1913 Params::Named(Cow::Borrowed(empty.as_slice()))
1914 );
1915 }
1916
1917 #[test]
1918 fn resolve_params_reuses_named_ordering() {
1919 let named = params! { ":b" => 2_i64, ":a" => 1_i64 };
1920 let ordered = resolve_params(
1921 "select * from t where a = :a and b = :b",
1922 Params::from(named),
1923 )
1924 .expect("named binds should resolve");
1925 assert_eq!(
1926 ordered,
1927 vec![BindValue::Number("1".into()), BindValue::Number("2".into())]
1928 );
1929
1930 assert!(resolve_params("select 1 from dual", Params::None)
1931 .expect("no binds should resolve")
1932 .is_empty());
1933 }
1934
1935 #[test]
1936 fn resolve_params_rejects_missing_and_extra_named_binds() {
1937 let missing = resolve_params(
1938 "select * from t where a = :a and b = :b",
1939 Params::from(params! { ":a" => 1_i64 }),
1940 )
1941 .unwrap_err();
1942 assert_eq!(
1943 missing,
1944 BindError::MissingNamedBind {
1945 name: "b".to_string()
1946 }
1947 );
1948
1949 let extra = resolve_params(
1950 "select * from t where a = :a",
1951 Params::from(params! { ":a" => 1_i64, ":b" => 2_i64 }),
1952 )
1953 .unwrap_err();
1954 assert_eq!(
1955 extra,
1956 BindError::ExtraNamedBind {
1957 name: ":b".to_string()
1958 }
1959 );
1960 }
1961
1962 #[test]
1963 fn resolve_params_uses_protocol_tokenizer_for_named_binds() {
1964 let ordered = resolve_params(
1965 r#"select ':skip', q'[not :this]', :"MiX", :a# from dual -- :comment"#,
1966 Params::from(params! { ":a#" => 7_i64, r#""MiX""# => 6_i64 }),
1967 )
1968 .expect("tokenizer-backed named binds should resolve");
1969
1970 assert_eq!(
1971 ordered,
1972 vec![BindValue::Number("6".into()), BindValue::Number("7".into())]
1973 );
1974 }
1975
1976 #[test]
1977 fn resolve_params_rejects_unambiguous_positional_count_mismatch() {
1978 let err = resolve_params(
1979 "select :1 + :2 from dual",
1980 Params::from(vec![BindValue::Number("1".into())]),
1981 )
1982 .unwrap_err();
1983 assert_eq!(
1984 err,
1985 BindError::PositionalCountMismatch {
1986 expected: 2,
1987 actual: 1
1988 }
1989 );
1990
1991 let repeated = resolve_params(
1992 "select :1 + :1 from dual",
1993 Params::from(vec![
1994 BindValue::Number("1".into()),
1995 BindValue::Number("1".into()),
1996 ]),
1997 )
1998 .expect("plain SQL repeated placeholders consume one positional value per occurrence");
1999 assert_eq!(repeated.len(), 2);
2000 }
2001
2002 #[test]
2003 fn validate_bind_rows_shape_rejects_ragged_raw_rows() {
2004 let rows = vec![
2005 vec![
2006 BindValue::Number("1".to_string()),
2007 BindValue::Text("a".into()),
2008 ],
2009 vec![BindValue::Number("2".to_string())],
2010 ];
2011
2012 let err = validate_bind_rows_shape("insert into t values (:1, :2)", &rows).unwrap_err();
2013 assert_eq!(
2014 err,
2015 BindError::BatchRowWidthMismatch {
2016 row_index: 1,
2017 expected: 2,
2018 actual: 1
2019 }
2020 );
2021 }
2022
2023 #[test]
2024 fn validate_bind_rows_shape_accepts_repeated_named_bind_single_value() {
2025 let rows = vec![vec![BindValue::Number("1".to_string())]];
2031 validate_bind_rows_shape(
2032 "select :v + 1 from dual union all select :v + 2 from dual \
2033 union all select :v + 3 from dual",
2034 &rows,
2035 )
2036 .expect("repeated named bind is satisfied by one supplied value");
2037 }
2038
2039 #[test]
2040 fn resolve_params_dedups_group_by_case_repeated_named_bind() {
2041 let sql = "\
2042 select case when deptno = :dept then 'target' else 'other' end bucket, count(*) \
2043 from emp \
2044 group by case when deptno = :dept then 'target' else 'other' end";
2045 let ordered = resolve_params(sql, Params::from(params! { ":dept" => 10_i64 }))
2046 .expect("repeated named GROUP BY CASE bind should resolve once");
2047 assert_eq!(ordered, vec![BindValue::Number("10".to_string())]);
2048 }
2049
2050 #[test]
2051 fn validate_bind_rows_shape_accepts_empty_rows_for_parse() {
2052 validate_bind_rows_shape("select :v from dual", &[])
2055 .expect("a no-bind execute / parse-only describe is valid");
2056 }
2057
2058 #[test]
2064 fn declared_bind_count_uses_real_tokenizer_ignoring_literals_and_comments() {
2065 let sql = "select :a /* :not_a_bind */, ':also_not', \
2068 q'[:nope]' from dual where x = :b -- :tail";
2069 assert_eq!(declared_bind_count(sql).expect("tokenizes"), 2);
2070
2071 assert_eq!(
2074 declared_bind_count("insert into t values (:1, :1, :2)").expect("tokenizes"),
2075 3
2076 );
2077 assert_eq!(
2079 declared_bind_count("select :v, :v from dual").expect("tokenizes"),
2080 2
2081 );
2082 assert_eq!(
2084 declared_bind_count("begin proc(:x, :x, :y); end;").expect("tokenizes"),
2085 2
2086 );
2087 assert_eq!(
2089 declared_bind_count("select 1 from dual").expect("tokenizes"),
2090 0
2091 );
2092 }
2093
2094 #[test]
2095 fn declared_bind_count_propagates_tokenizer_error() {
2096 let err = declared_bind_count("select ':unterminated from dual").unwrap_err();
2097 assert_eq!(err, BindError::Sql(sql::SqlError::MissingEndingSingleQuote));
2098 }
2099
2100 #[test]
2101 fn check_positional_binds_catches_mismatch_and_passes_correct() {
2102 assert_eq!(
2104 check_positional_binds("select :1 + :2 from dual", 1).unwrap_err(),
2105 BindError::PositionalCountMismatch {
2106 expected: 2,
2107 actual: 1
2108 }
2109 );
2110 assert_eq!(
2112 check_positional_binds("select :1 from dual", 3).unwrap_err(),
2113 BindError::PositionalCountMismatch {
2114 expected: 1,
2115 actual: 3
2116 }
2117 );
2118 check_positional_binds("insert into t values (:1, :2)", 2).expect("matching count passes");
2120 check_positional_binds("select :v, :v from dual", 2)
2121 .expect("repeated plain-SQL placeholder consumes one value per occurrence");
2122 check_positional_binds("create table t (id number)", 5)
2124 .expect("DDL is exempt from bind-count validation");
2125 }
2126
2127 fn text(value: &str) -> BindValue {
2132 BindValue::Text(value.into())
2133 }
2134 fn number(value: &str) -> BindValue {
2135 BindValue::Number(value.into())
2136 }
2137
2138 #[test]
2139 fn type_validation_accepts_homogeneous_columns() {
2140 let rows = vec![
2142 vec![number("1"), text("a")],
2143 vec![number("2"), text("bb")],
2144 vec![number("3"), text("ccc")],
2145 ];
2146 check_bind_rows("insert into t values (:1, :2)", &rows)
2147 .expect("consistent per-column types are accepted");
2148 }
2149
2150 #[test]
2151 fn type_validation_catches_column_type_mismatch() {
2152 let rows = vec![vec![number("1")], vec![text("oops")]];
2155 let err = validate_bind_rows_types(&rows).unwrap_err();
2156 assert_eq!(
2157 err,
2158 BindError::BatchColumnTypeMismatch {
2159 row_index: 1,
2160 column_index: 0,
2161 expected: "DB_TYPE_NUMBER",
2162 actual: "DB_TYPE_VARCHAR",
2163 }
2164 );
2165 }
2166
2167 #[test]
2168 fn type_validation_reports_the_offending_row_and_column() {
2169 let rows = vec![
2171 vec![number("1"), text("a")],
2172 vec![number("2"), text("b")],
2173 vec![number("3"), number("4")],
2174 ];
2175 let err = validate_bind_rows_types(&rows).unwrap_err();
2176 assert_eq!(
2177 err,
2178 BindError::BatchColumnTypeMismatch {
2179 row_index: 2,
2180 column_index: 1,
2181 expected: "DB_TYPE_VARCHAR",
2182 actual: "DB_TYPE_NUMBER",
2183 }
2184 );
2185 }
2186
2187 #[test]
2188 fn type_validation_treats_untyped_null_as_a_wildcard() {
2189 let rows = vec![
2192 vec![BindValue::Null, text("a")],
2193 vec![number("2"), BindValue::Null],
2194 vec![number("3"), text("c")],
2195 ];
2196 check_bind_rows("insert into t values (:1, :2)", &rows)
2197 .expect("untyped NULLs ride any column type");
2198 }
2199
2200 #[test]
2201 fn type_validation_establishes_type_from_first_typed_row() {
2202 let rows = vec![
2205 vec![BindValue::Null],
2206 vec![BindValue::Null],
2207 vec![number("7")],
2208 vec![text("x")],
2209 ];
2210 let err = validate_bind_rows_types(&rows).unwrap_err();
2211 assert_eq!(
2212 err,
2213 BindError::BatchColumnTypeMismatch {
2214 row_index: 3,
2215 column_index: 0,
2216 expected: "DB_TYPE_NUMBER",
2217 actual: "DB_TYPE_VARCHAR",
2218 }
2219 );
2220 }
2221
2222 #[test]
2223 fn type_validation_folds_char_varchar_and_raw_families() {
2224 use oracledb_protocol::thin::{CS_FORM_IMPLICIT, ORA_TYPE_NUM_CHAR, ORA_TYPE_NUM_LONG_RAW};
2225 let char_then_varchar = vec![
2227 vec![BindValue::TypedNull {
2228 ora_type_num: ORA_TYPE_NUM_CHAR,
2229 csfrm: CS_FORM_IMPLICIT,
2230 buffer_size: 10,
2231 }],
2232 vec![text("hello")],
2233 ];
2234 validate_bind_rows_types(&char_then_varchar)
2235 .expect("CHAR and VARCHAR share a wire type family");
2236
2237 let raw_then_long_raw = vec![
2239 vec![BindValue::Raw(vec![1, 2, 3])],
2240 vec![BindValue::TypedNull {
2241 ora_type_num: ORA_TYPE_NUM_LONG_RAW,
2242 csfrm: 0,
2243 buffer_size: 10,
2244 }],
2245 ];
2246 validate_bind_rows_types(&raw_then_long_raw)
2247 .expect("RAW and LONG_RAW share a wire type family");
2248 }
2249
2250 #[test]
2251 fn type_validation_rejects_charset_form_mismatch_within_a_family() {
2252 use oracledb_protocol::thin::{CS_FORM_NCHAR, ORA_TYPE_NUM_VARCHAR};
2253 let rows = vec![
2256 vec![text("implicit")],
2257 vec![BindValue::TypedNull {
2258 ora_type_num: ORA_TYPE_NUM_VARCHAR,
2259 csfrm: CS_FORM_NCHAR,
2260 buffer_size: 10,
2261 }],
2262 ];
2263 let err = validate_bind_rows_types(&rows).unwrap_err();
2264 assert!(
2265 matches!(
2266 err,
2267 BindError::BatchColumnTypeMismatch {
2268 row_index: 1,
2269 column_index: 0,
2270 ..
2271 }
2272 ),
2273 "charset-form mismatch must be rejected, got {err:?}"
2274 );
2275 }
2276
2277 #[test]
2278 fn type_validation_passes_single_and_empty_rows() {
2279 check_bind_rows(
2281 "insert into t values (:1, :2)",
2282 &[vec![number("1"), text("a")]],
2283 )
2284 .expect("a single row is always type-consistent");
2285 check_bind_rows("insert into t values (:1)", &[]).expect("no rows to validate");
2287 }
2288
2289 #[test]
2290 fn check_bind_rows_reports_width_before_type() {
2291 let rows = vec![vec![number("1"), text("a")], vec![number("2")]];
2293 let err = check_bind_rows("insert into t values (:1, :2)", &rows).unwrap_err();
2294 assert_eq!(
2295 err,
2296 BindError::BatchRowWidthMismatch {
2297 row_index: 1,
2298 expected: 2,
2299 actual: 1
2300 }
2301 );
2302 }
2303
2304 #[test]
2305 fn query_result_typed_get() {
2306 let result = QueryResult {
2307 columns: vec![ColumnMetadata::new("ID", 0), ColumnMetadata::new("NAME", 0)],
2308 rows: vec![vec![Some(num("7")), Some(QueryValue::Text("bob".into()))]],
2309 ..Default::default()
2310 };
2311 assert_eq!(result.get::<i64>(0, 0).unwrap(), 7);
2312 assert_eq!(result.get_by_name::<i64>(0, "id").unwrap(), 7);
2313 assert_eq!(result.get_by_name::<String>(0, "name").unwrap(), "bob");
2314 let row = result.typed_row(0);
2315 assert_eq!(row.get::<i64>(0).unwrap(), 7);
2316 assert_eq!(row.get_by_name::<String>("NAME").unwrap(), "bob");
2317 assert!(result.get_by_name::<i64>(0, "nope").is_err());
2319 }
2320
2321 #[cfg(feature = "rust_decimal")]
2322 #[test]
2323 fn decimal_roundtrip_is_lossless_to_full_precision() {
2324 use rust_decimal::Decimal;
2325 use std::str::FromStr;
2326 let text = "7922816251426433759354.395033"; let dec = Decimal::from_str(text).unwrap();
2333 let bind = dec.to_sql();
2335 assert_eq!(bind, BindValue::Number(text.to_string()));
2336 let back = Decimal::from_sql(&num(text)).unwrap();
2338 assert_eq!(back, dec);
2339 assert_eq!(back.to_string(), text);
2340
2341 let as_f64: f64 = text.parse().unwrap();
2343 assert_ne!(
2344 as_f64.to_string(),
2345 text,
2346 "f64 must lose precision here, proving Decimal is the lossless path"
2347 );
2348 }
2349
2350 #[cfg(feature = "chrono")]
2351 #[test]
2352 fn chrono_from_and_to_sql() {
2353 use chrono::{
2354 DateTime, Datelike, FixedOffset, NaiveDate, NaiveDateTime, TimeZone, Timelike, Utc,
2355 };
2356 let dt = QueryValue::DateTime {
2357 year: 2026,
2358 month: 6,
2359 day: 14,
2360 hour: 12,
2361 minute: 30,
2362 second: 45,
2363 nanosecond: 123_456_789,
2364 };
2365 let parsed = NaiveDateTime::from_sql(&dt).unwrap();
2366 assert_eq!(parsed.to_string(), "2026-06-14 12:30:45.123456789");
2367 let date = NaiveDate::from_sql(&dt).unwrap();
2368 assert_eq!(date, NaiveDate::from_ymd_opt(2026, 6, 14).unwrap());
2369 let bce = QueryValue::DateTime {
2370 year: -4712,
2371 month: 1,
2372 day: 1,
2373 hour: 0,
2374 minute: 0,
2375 second: 0,
2376 nanosecond: 0,
2377 };
2378 let bce_date = NaiveDate::from_sql(&bce).unwrap();
2379 assert_eq!(bce_date.year(), -4712);
2380 match parsed.to_sql() {
2382 BindValue::Timestamp {
2383 year, nanosecond, ..
2384 } => {
2385 assert_eq!(year, 2026);
2386 assert_eq!(nanosecond, 123_456_789);
2387 }
2388 other => panic!("expected Timestamp bind, got {other:?}"),
2389 }
2390
2391 let offset_value = QueryValue::TimestampTz {
2396 year: 2026,
2397 month: 6,
2398 day: 29,
2399 hour: 12,
2400 minute: 34,
2401 second: 56,
2402 nanosecond: 987_654_321,
2403 offset_minutes: -330,
2404 };
2405 let fixed = DateTime::<FixedOffset>::from_sql(&offset_value).unwrap();
2406 assert_eq!(fixed.to_rfc3339(), "2026-06-29T07:04:56.987654321-05:30");
2407 let utc = DateTime::<Utc>::from_sql(&offset_value).unwrap();
2408 assert_eq!(utc.to_rfc3339(), "2026-06-29T12:34:56.987654321+00:00");
2409 let legacy_naive = NaiveDateTime::from_sql(&offset_value).unwrap();
2412 assert_eq!(legacy_naive.to_string(), "2026-06-29 07:04:56.987654321");
2413 assert_eq!(
2416 fixed
2417 .try_to_sql()
2418 .expect("whole-minute FixedOffset must encode"),
2419 BindValue::TimestampTz {
2420 year: 2026,
2421 month: 6,
2422 day: 29,
2423 hour: 12,
2424 minute: 34,
2425 second: 56,
2426 nanosecond: 987_654_321,
2427 offset_minutes: -330,
2428 }
2429 );
2430
2431 let outbound = FixedOffset::east_opt(5 * 3600 + 45 * 60)
2434 .unwrap()
2435 .with_ymd_and_hms(2026, 6, 29, 7, 8, 9)
2436 .unwrap()
2437 .with_nanosecond(123_000_000)
2438 .unwrap();
2439 assert_eq!(
2440 outbound.try_to_sql().expect("+05:45 must encode exactly"),
2441 BindValue::TimestampTz {
2442 year: 2026,
2443 month: 6,
2444 day: 29,
2445 hour: 1,
2446 minute: 23,
2447 second: 9,
2448 nanosecond: 123_000_000,
2449 offset_minutes: 345,
2450 }
2451 );
2452 let outbound_round_trip = DateTime::<FixedOffset>::from_sql(&QueryValue::TimestampTz {
2453 year: 2026,
2454 month: 6,
2455 day: 29,
2456 hour: 1,
2457 minute: 23,
2458 second: 9,
2459 nanosecond: 123_000_000,
2460 offset_minutes: 345,
2461 })
2462 .unwrap();
2463 assert_eq!(outbound_round_trip.to_rfc3339(), outbound.to_rfc3339());
2464
2465 let negative_outbound = FixedOffset::west_opt(3 * 3600 + 30 * 60)
2468 .unwrap()
2469 .with_ymd_and_hms(2026, 6, 29, 7, 8, 9)
2470 .unwrap();
2471 assert_eq!(
2472 negative_outbound
2473 .try_to_sql()
2474 .expect("-03:30 must encode exactly"),
2475 BindValue::TimestampTz {
2476 year: 2026,
2477 month: 6,
2478 day: 29,
2479 hour: 10,
2480 minute: 38,
2481 second: 9,
2482 nanosecond: 0,
2483 offset_minutes: -210,
2484 }
2485 );
2486 let negative_round_trip = DateTime::<FixedOffset>::from_sql(&QueryValue::TimestampTz {
2487 year: 2026,
2488 month: 6,
2489 day: 29,
2490 hour: 10,
2491 minute: 38,
2492 second: 9,
2493 nanosecond: 0,
2494 offset_minutes: -210,
2495 })
2496 .unwrap();
2497 assert_eq!(
2498 negative_round_trip.to_rfc3339(),
2499 negative_outbound.to_rfc3339()
2500 );
2501
2502 let utc_outbound = outbound.with_timezone(&Utc);
2503 assert_eq!(
2504 utc_outbound.try_to_sql().expect("UTC must encode exactly"),
2505 BindValue::TimestampTz {
2506 year: 2026,
2507 month: 6,
2508 day: 29,
2509 hour: 1,
2510 minute: 23,
2511 second: 9,
2512 nanosecond: 123_000_000,
2513 offset_minutes: 0,
2514 }
2515 );
2516 assert_eq!(
2517 DateTime::<Utc>::from_sql(&QueryValue::TimestampTz {
2518 year: 2026,
2519 month: 6,
2520 day: 29,
2521 hour: 1,
2522 minute: 23,
2523 second: 9,
2524 nanosecond: 123_000_000,
2525 offset_minutes: 0,
2526 })
2527 .unwrap(),
2528 utc_outbound
2529 );
2530
2531 for offset_seconds in [30, -30] {
2535 let value = FixedOffset::east_opt(offset_seconds)
2536 .unwrap()
2537 .with_ymd_and_hms(2026, 6, 29, 12, 0, 0)
2538 .unwrap();
2539 let error = value
2540 .try_to_sql()
2541 .expect_err("sub-minute FixedOffset must not emit a TimestampTz bind");
2542 match error {
2543 ConversionError::OutOfRange { expected, detail } => {
2544 assert_eq!(expected, "Oracle TIMESTAMP WITH TIME ZONE offset");
2545 assert!(
2546 detail.contains(&format!("{offset_seconds:+} seconds")),
2547 "detail must name the original offset: {detail}"
2548 );
2549 assert!(
2550 detail.contains("30 sub-minute second(s)"),
2551 "detail must name the precision that would be lost: {detail}"
2552 );
2553 }
2554 other => panic!("expected a typed offset precision error, got {other:?}"),
2555 }
2556 }
2557 }
2558
2559 #[cfg(feature = "uuid")]
2560 #[test]
2561 fn uuid_from_raw_and_text() {
2562 use uuid::Uuid;
2563 let id = Uuid::from_u128(0x0102_0304_0506_0708_090a_0b0c_0d0e_0f10);
2564 let raw = QueryValue::Raw(id.as_bytes().to_vec());
2566 assert_eq!(Uuid::from_sql(&raw).unwrap(), id);
2567 let text = QueryValue::Text(id.to_string());
2569 assert_eq!(Uuid::from_sql(&text).unwrap(), id);
2570 assert_eq!(id.to_sql(), BindValue::Raw(id.as_bytes().to_vec()));
2572 }
2573
2574 #[cfg(feature = "serde_json")]
2575 #[test]
2576 fn serde_json_from_oson_tree() {
2577 use oracledb_protocol::oson::OsonValue;
2578 use serde_json::json;
2579 let oson = OsonValue::Object(vec![
2580 ("id".into(), OsonValue::Number("7".into())),
2581 ("name".into(), OsonValue::String("bob".into())),
2582 ("active".into(), OsonValue::Bool(true)),
2583 (
2584 "tags".into(),
2585 OsonValue::Array(vec![
2586 OsonValue::String("a".into()),
2587 OsonValue::String("b".into()),
2588 ]),
2589 ),
2590 ]);
2591 let value = serde_json::Value::from_sql(&QueryValue::Json(Box::new(oson)))
2592 .expect("OSON tree converts to serde_json");
2593 assert_eq!(
2594 value,
2595 json!({"id": 7, "name": "bob", "active": true, "tags": ["a", "b"]})
2596 );
2597 }
2598
2599 #[cfg(feature = "serde_json")]
2600 #[test]
2601 fn serde_json_number_conversion_preserves_high_precision_text() {
2602 use oracledb_protocol::oson::OsonValue;
2603 use serde_json::{json, Value};
2604
2605 let oson = OsonValue::Object(vec![
2606 (
2607 "precise_decimal".into(),
2608 OsonValue::Number("1.234567890123456789".into()),
2609 ),
2610 (
2611 "wide_integer".into(),
2612 OsonValue::Number("99999999999999999999999999999999999999".into()),
2613 ),
2614 ("small_int".into(), OsonValue::Number("42".into())),
2615 ("small_decimal".into(), OsonValue::Number("12.5".into())),
2616 ]);
2617
2618 let value = serde_json::Value::from_sql(&QueryValue::Json(Box::new(oson)))
2619 .expect("OSON tree converts to serde_json");
2620 assert_eq!(
2621 value,
2622 json!({
2623 "precise_decimal": "1.234567890123456789",
2624 "wide_integer": "99999999999999999999999999999999999999",
2625 "small_int": 42,
2626 "small_decimal": 12.5
2627 })
2628 );
2629 assert!(matches!(value["small_int"], Value::Number(_)));
2630 assert!(matches!(value["small_decimal"], Value::Number(_)));
2631 }
2632
2633 #[test]
2634 fn named_binds_reorder_to_first_appearance() {
2635 let named = params! { ":b" => 2_i64, ":a" => 1_i64 };
2638 let ordered = order_named_binds("select * from t where a = :a and b = :b", named)
2639 .expect("named binds should reorder");
2640 assert_eq!(
2641 ordered,
2642 vec![BindValue::Number("1".into()), BindValue::Number("2".into())]
2643 );
2644 }
2645
2646 #[test]
2647 fn named_binds_repeated_placeholder_counts_once() {
2648 let named = params! { ":id" => 5_i64, ":name" => "x" };
2650 let ordered = order_named_binds("select :id from t where id = :id and name = :name", named)
2651 .expect("named binds should coalesce repeated placeholders");
2652 assert_eq!(
2653 ordered,
2654 vec![BindValue::Number("5".into()), BindValue::Text("x".into())]
2655 );
2656 }
2657
2658 #[test]
2659 fn named_binds_ignore_colon_in_string_literal() {
2660 let named = params! { ":real" => 9_i64 };
2663 let ordered = order_named_binds("select 'time is 12:30' as t, :real from dual", named)
2664 .expect("bind in string literal should be ignored");
2665 assert_eq!(ordered, vec![BindValue::Number("9".into())]);
2666 }
2667
2668 #[test]
2669 fn vector_from_sql_f32_f64() {
2670 let vector = QueryValue::Vector(Box::new(Vector::Dense(VectorValues::Float32(vec![
2671 1.0, 2.0, 3.0,
2672 ]))));
2673 assert_eq!(Vec::<f32>::from_sql(&vector).unwrap(), vec![1.0, 2.0, 3.0]);
2674 assert_eq!(Vec::<f64>::from_sql(&vector).unwrap(), vec![1.0, 2.0, 3.0]);
2675 assert_eq!(
2677 vec![1.0_f32, 2.0, 3.0].to_sql(),
2678 BindValue::Vector(Vector::Dense(VectorValues::Float32(vec![1.0, 2.0, 3.0])))
2679 );
2680 }
2681}