From 094d43cbccae38def82b9efc2ce981268288da0a Mon Sep 17 00:00:00 2001 From: lovasoa Date: Thu, 25 Sep 2025 13:56:23 +0200 Subject: [PATCH 01/21] refactor(odbc): implement bulk fetch using columnar buffers This commit introduces a new bulk fetch implementation in the ODBC bridge, utilizing columnar buffers for improved performance. The changes include the addition of a `build_bindings` function to create column bindings, a new `stream_rows` function to handle batch processing, and the extraction of values from buffers. This refactor enhances the efficiency of data retrieval by reducing the overhead of row-by-row fetching. --- sqlx-core/src/odbc/connection/odbc_bridge.rs | 536 ++++++++++++------- 1 file changed, 357 insertions(+), 179 deletions(-) diff --git a/sqlx-core/src/odbc/connection/odbc_bridge.rs b/sqlx-core/src/odbc/connection/odbc_bridge.rs index d0e20262e3..965a0c3ff2 100644 --- a/sqlx-core/src/odbc/connection/odbc_bridge.rs +++ b/sqlx-core/src/odbc/connection/odbc_bridge.rs @@ -1,12 +1,51 @@ use crate::error::Error; use crate::odbc::{ connection::MaybePrepared, OdbcArgumentValue, OdbcArguments, OdbcColumn, OdbcQueryResult, - OdbcRow, OdbcTypeInfo, + OdbcRow, OdbcTypeInfo, OdbcValue, }; use either::Either; use flume::{SendError, Sender}; -use odbc_api::handles::{AsStatementRef, Statement}; -use odbc_api::{Cursor, CursorRow, IntoParameter, Nullable, ResultSetMetadata}; +use odbc_api::buffers::{AnySlice, BufferDesc, ColumnarAnyBuffer}; +use odbc_api::handles::{AsStatementRef, Nullability, Statement}; +use odbc_api::DataType; +use odbc_api::{Cursor, IntoParameter, ResultSetMetadata}; +use std::cmp::min; + +// Bulk fetch implementation using columnar buffers instead of row-by-row fetching +// This provides significant performance improvements by fetching rows in batches +// and avoiding the slow `next_row()` method from odbc-api +const BATCH_SIZE: usize = 128; +const DEFAULT_TEXT_LEN: usize = 512; +const DEFAULT_BINARY_LEN: usize = 1024; +const DEFAULT_NUMERIC_TEXT_LEN: usize = 128; +const MAX_TEXT_LEN: usize = 1024 * 1024; +const MAX_BINARY_LEN: usize = 1024 * 1024; + +struct ColumnBinding { + column: OdbcColumn, + buffer_desc: BufferDesc, +} + +fn build_bindings(cursor: &mut C) -> Result, Error> +where + C: ResultSetMetadata, +{ + let column_count = cursor.num_result_cols().unwrap_or(0); + let mut bindings = Vec::with_capacity(column_count as usize); + for index in 1..=column_count { + let column = create_column(cursor, index as u16); + let nullable = cursor + .col_nullability(index as u16) + .unwrap_or(Nullability::Unknown) + .could_be_nullable(); + let buffer_desc = map_buffer_desc(cursor, index as u16, &column.type_info, nullable)?; + bindings.push(ColumnBinding { + column, + buffer_desc, + }); + } + Ok(bindings) +} pub type ExecuteResult = Result, Error>; pub type ExecuteSender = Sender; @@ -32,15 +71,15 @@ pub fn execute_sql( let affected = match maybe_prepared { MaybePrepared::Prepared(prepared) => { let mut prepared = prepared.lock().expect("prepared statement lock"); - if let Some(mut cursor) = prepared.execute(¶ms[..])? { - handle_cursor(&mut cursor, tx); + if let Some(cursor) = prepared.execute(¶ms[..])? { + handle_cursor(cursor, tx); } extract_rows_affected(&mut *prepared) } MaybePrepared::NotPrepared(sql) => { let mut preallocated = conn.preallocate().map_err(Error::from)?; - if let Some(mut cursor) = preallocated.execute(&sql, ¶ms[..])? { - handle_cursor(&mut cursor, tx); + if let Some(cursor) = preallocated.execute(&sql, ¶ms[..])? { + handle_cursor(cursor, tx); } extract_rows_affected(&mut preallocated) } @@ -86,13 +125,19 @@ fn to_param(arg: OdbcArgumentValue) -> Box(cursor: &mut C, tx: &ExecuteSender) +fn handle_cursor(mut cursor: C, tx: &ExecuteSender) where C: Cursor + ResultSetMetadata, { - let columns = collect_columns(cursor); + let bindings = match build_bindings(&mut cursor) { + Ok(b) => b, + Err(e) => { + send_error(tx, e); + return; + } + }; - match stream_rows(cursor, &columns, tx) { + match stream_rows(cursor, bindings, tx) { Ok(true) => { let _ = send_done(tx, 0); } @@ -115,16 +160,6 @@ fn send_row(tx: &ExecuteSender, row: OdbcRow) -> Result<(), SendError(cursor: &mut C) -> Vec -where - C: ResultSetMetadata, -{ - let count = cursor.num_result_cols().unwrap_or(0); - (1..=count) - .map(|i| create_column(cursor, i as u16)) - .collect() -} - fn create_column(cursor: &mut C, index: u16) -> OdbcColumn where C: ResultSetMetadata, @@ -143,185 +178,328 @@ fn decode_column_name(name_bytes: Vec, index: u16) -> String { String::from_utf8(name_bytes).unwrap_or_else(|_| format!("col{}", index - 1)) } -fn stream_rows(cursor: &mut C, columns: &[OdbcColumn], tx: &ExecuteSender) -> Result +fn map_buffer_desc( + _cursor: &mut C, + _column_index: u16, + type_info: &OdbcTypeInfo, + nullable: bool, +) -> Result where - C: Cursor, + C: ResultSetMetadata, { - let mut receiver_open = true; - - while let Some(mut row) = cursor.next_row()? { - let values = collect_row_values(&mut row, columns)?; - let row_data = OdbcRow { - columns: columns.to_vec(), - values: values.into_iter().map(|(_, value)| value).collect(), - }; - - if send_row(tx, row_data).is_err() { - receiver_open = false; - break; - } - } - Ok(receiver_open) -} - -fn collect_row_values( - row: &mut CursorRow<'_>, - columns: &[OdbcColumn], -) -> Result, Error> { - columns - .iter() - .enumerate() - .map(|(i, column)| collect_column_value(row, i, column)) - .collect() -} - -fn collect_column_value( - row: &mut CursorRow<'_>, - index: usize, - column: &OdbcColumn, -) -> Result<(OdbcTypeInfo, crate::odbc::OdbcValue), Error> { - use odbc_api::DataType; - - let col_idx = (index + 1) as u16; - let type_info = column.type_info.clone(); let data_type = type_info.data_type(); - - let value = match data_type { - DataType::TinyInt - | DataType::SmallInt - | DataType::Integer - | DataType::BigInt - | DataType::Bit => extract_int(row, col_idx, &type_info)?, - - DataType::Real => extract_float::(row, col_idx, &type_info)?, - DataType::Float { .. } | DataType::Double => { - extract_float::(row, col_idx, &type_info)? + let buffer_desc = match data_type { + DataType::TinyInt | DataType::SmallInt | DataType::Integer | DataType::BigInt => { + BufferDesc::I64 { nullable } + } + DataType::Real => BufferDesc::F32 { nullable }, + DataType::Float { .. } | DataType::Double => BufferDesc::F64 { nullable }, + DataType::Bit => BufferDesc::Bit { nullable }, + DataType::Date => BufferDesc::Date { nullable }, + DataType::Time { .. } => BufferDesc::Time { nullable }, + DataType::Timestamp { .. } => BufferDesc::Timestamp { nullable }, + DataType::Binary { .. } | DataType::Varbinary { .. } | DataType::LongVarbinary { .. } => { + BufferDesc::Binary { + length: DEFAULT_BINARY_LEN, + } } - DataType::Char { .. } - | DataType::Varchar { .. } - | DataType::LongVarchar { .. } | DataType::WChar { .. } + | DataType::Varchar { .. } | DataType::WVarchar { .. } + | DataType::LongVarchar { .. } | DataType::WLongVarchar { .. } - | DataType::Date - | DataType::Time { .. } - | DataType::Timestamp { .. } - | DataType::Decimal { .. } - | DataType::Numeric { .. } => extract_text(row, col_idx, &type_info)?, - - DataType::Binary { .. } | DataType::Varbinary { .. } | DataType::LongVarbinary { .. } => { - extract_binary(row, col_idx, &type_info)? - } - - DataType::Unknown | DataType::Other { .. } => { - match extract_text(row, col_idx, &type_info) { - Ok(v) => v, - Err(_) => extract_binary(row, col_idx, &type_info)?, - } - } - }; - - Ok((type_info, value)) -} - -fn extract_int( - row: &mut CursorRow<'_>, - col_idx: u16, - type_info: &OdbcTypeInfo, -) -> Result { - let mut nullable = Nullable::::null(); - row.get_data(col_idx, &mut nullable)?; - - let (is_null, int) = match nullable.into_opt() { - None => (true, None), - Some(v) => (false, Some(v)), + | DataType::Other { .. } + | DataType::Unknown => BufferDesc::Text { + max_str_len: MAX_TEXT_LEN, + }, + DataType::Decimal { .. } | DataType::Numeric { .. } => BufferDesc::Text { + max_str_len: min(DEFAULT_NUMERIC_TEXT_LEN, MAX_TEXT_LEN), + }, }; - Ok(crate::odbc::OdbcValue { - type_info: type_info.clone(), - is_null, - text: None, - blob: None, - int, - float: None, - }) + Ok(buffer_desc) } -fn extract_float( - row: &mut CursorRow<'_>, - col_idx: u16, - type_info: &OdbcTypeInfo, -) -> Result +fn stream_rows( + cursor: C, + bindings: Vec, + tx: &ExecuteSender, +) -> Result where - T: Into + Default, - odbc_api::Nullable: odbc_api::parameter::CElement + odbc_api::handles::CDataMut, + C: Cursor + ResultSetMetadata, { - let mut nullable = Nullable::::null(); - row.get_data(col_idx, &mut nullable)?; - - let (is_null, float) = match nullable.into_opt() { - None => (true, None), - Some(v) => (false, Some(v.into())), - }; + let buffer_descriptions: Vec<_> = bindings.iter().map(|b| b.buffer_desc).collect(); + let buffer = ColumnarAnyBuffer::from_descs(BATCH_SIZE, buffer_descriptions); + let mut row_set_cursor = cursor.bind_buffer(buffer)?; - Ok(crate::odbc::OdbcValue { - type_info: type_info.clone(), - is_null, - text: None, - blob: None, - int: None, - float, - }) -} + let mut receiver_open = true; -fn extract_text( - row: &mut CursorRow<'_>, - col_idx: u16, - type_info: &OdbcTypeInfo, -) -> Result { - let mut buf = Vec::new(); - let is_some = row.get_text(col_idx, &mut buf)?; + while let Some(batch) = row_set_cursor.fetch()? { + let columns: Vec<_> = bindings.iter().map(|b| b.column.clone()).collect(); + + for row_index in 0..batch.num_rows() { + let row_values: Vec<_> = bindings + .iter() + .enumerate() + .map(|(col_index, binding)| { + let type_info = binding.column.type_info.clone(); + let value = + extract_value_from_buffer(batch.column(col_index), row_index, &type_info); + (type_info, value) + }) + .collect(); + + let row = OdbcRow { + columns: columns.clone(), + values: row_values.into_iter().map(|(_, value)| value).collect(), + }; + + if send_row(tx, row).is_err() { + receiver_open = false; + break; + } + } - let (is_null, text) = if !is_some { - (true, None) - } else { - match String::from_utf8(buf) { - Ok(s) => (false, Some(s)), - Err(e) => return Err(Error::Decode(e.into())), + if !receiver_open { + break; } - }; + } - Ok(crate::odbc::OdbcValue { - type_info: type_info.clone(), - is_null, - text, - blob: None, - int: None, - float: None, - }) + Ok(receiver_open) } -fn extract_binary( - row: &mut CursorRow<'_>, - col_idx: u16, +fn extract_value_from_buffer( + slice: AnySlice<'_>, + row_index: usize, type_info: &OdbcTypeInfo, -) -> Result { - let mut buf = Vec::new(); - let is_some = row.get_binary(col_idx, &mut buf)?; - - let (is_null, blob) = if !is_some { - (true, None) - } else { - (false, Some(buf)) - }; - - Ok(crate::odbc::OdbcValue { - type_info: type_info.clone(), - is_null, - text: None, - blob, - int: None, - float: None, - }) +) -> OdbcValue { + match slice { + AnySlice::I8(s) => OdbcValue { + type_info: type_info.clone(), + is_null: false, + text: None, + blob: None, + int: Some(s[row_index] as i64), + float: None, + }, + AnySlice::I16(s) => OdbcValue { + type_info: type_info.clone(), + is_null: false, + text: None, + blob: None, + int: Some(s[row_index] as i64), + float: None, + }, + AnySlice::I32(s) => OdbcValue { + type_info: type_info.clone(), + is_null: false, + text: None, + blob: None, + int: Some(s[row_index] as i64), + float: None, + }, + AnySlice::I64(s) => OdbcValue { + type_info: type_info.clone(), + is_null: false, + text: None, + blob: None, + int: Some(s[row_index]), + float: None, + }, + AnySlice::F32(s) => OdbcValue { + type_info: type_info.clone(), + is_null: false, + text: None, + blob: None, + int: None, + float: Some(s[row_index] as f64), + }, + AnySlice::F64(s) => OdbcValue { + type_info: type_info.clone(), + is_null: false, + text: None, + blob: None, + int: None, + float: Some(s[row_index]), + }, + AnySlice::Bit(s) => OdbcValue { + type_info: type_info.clone(), + is_null: false, + text: None, + blob: None, + int: Some(s[row_index].0 as i64), + float: None, + }, + AnySlice::Text(s) => { + let text = s + .get(row_index) + .map(|bytes| String::from_utf8_lossy(bytes).to_string()); + OdbcValue { + type_info: type_info.clone(), + is_null: text.is_none(), + text, + blob: None, + int: None, + float: None, + } + } + AnySlice::Binary(s) => { + let blob = s.get(row_index).map(|bytes| bytes.to_vec()); + OdbcValue { + type_info: type_info.clone(), + is_null: blob.is_none(), + text: None, + blob, + int: None, + float: None, + } + } + AnySlice::NullableI8(s) => { + let (is_null, int) = if let Some(&val) = s.get(row_index) { + (false, Some(val as i64)) + } else { + (true, None) + }; + OdbcValue { + type_info: type_info.clone(), + is_null, + text: None, + blob: None, + int, + float: None, + } + } + AnySlice::NullableI16(s) => { + let (is_null, int) = if let Some(&val) = s.get(row_index) { + (false, Some(val as i64)) + } else { + (true, None) + }; + OdbcValue { + type_info: type_info.clone(), + is_null, + text: None, + blob: None, + int, + float: None, + } + } + AnySlice::NullableI32(s) => { + let (is_null, int) = if let Some(&val) = s.get(row_index) { + (false, Some(val as i64)) + } else { + (true, None) + }; + OdbcValue { + type_info: type_info.clone(), + is_null, + text: None, + blob: None, + int, + float: None, + } + } + AnySlice::NullableI64(s) => { + let (is_null, int) = if let Some(&val) = s.get(row_index) { + (false, Some(val)) + } else { + (true, None) + }; + OdbcValue { + type_info: type_info.clone(), + is_null, + text: None, + blob: None, + int, + float: None, + } + } + AnySlice::NullableF32(s) => { + let (is_null, float) = if let Some(&val) = s.get(row_index) { + (false, Some(val as f64)) + } else { + (true, None) + }; + OdbcValue { + type_info: type_info.clone(), + is_null, + text: None, + blob: None, + int: None, + float, + } + } + AnySlice::NullableF64(s) => { + let (is_null, float) = if let Some(&val) = s.get(row_index) { + (false, Some(val)) + } else { + (true, None) + }; + OdbcValue { + type_info: type_info.clone(), + is_null, + text: None, + blob: None, + int: None, + float, + } + } + AnySlice::NullableBit(s) => { + let (is_null, int) = if let Some(&val) = s.get(row_index) { + (false, Some(val.0 as i64)) + } else { + (true, None) + }; + OdbcValue { + type_info: type_info.clone(), + is_null, + text: None, + blob: None, + int, + float: None, + } + } + AnySlice::Date(s) => { + let text = s.get(row_index).map(|date| format!("{:?}", date)); + OdbcValue { + type_info: type_info.clone(), + is_null: text.is_none(), + text, + blob: None, + int: None, + float: None, + } + } + AnySlice::Time(s) => { + let text = s.get(row_index).map(|time| format!("{:?}", time)); + OdbcValue { + type_info: type_info.clone(), + is_null: text.is_none(), + text, + blob: None, + int: None, + float: None, + } + } + AnySlice::Timestamp(s) => { + let text = s.get(row_index).map(|ts| format!("{:?}", ts)); + OdbcValue { + type_info: type_info.clone(), + is_null: text.is_none(), + text, + blob: None, + int: None, + float: None, + } + } + _ => OdbcValue { + type_info: type_info.clone(), + is_null: true, + text: None, + blob: None, + int: None, + float: None, + }, + } } From 36d1516a5301de8f1d67ed55f682a6dcd063a576 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Thu, 25 Sep 2025 16:41:04 +0200 Subject: [PATCH 02/21] refactor(odbc): enhance OdbcValue and ColumnData structures This commit introduces significant changes to the ODBC module, including the addition of the `OdbcValueVec` enum to encapsulate various ODBC types and the `ColumnData` struct to hold column values along with their type information. The `OdbcValue` and `OdbcValueRef` structures are updated to utilize `ColumnData`, improving memory management through the use of `Arc`. Additionally, utility methods for value extraction and conversion are added, streamlining the handling of ODBC data types and enhancing overall code clarity. --- sqlx-core/src/odbc/connection/odbc_bridge.rs | 270 +---------- sqlx-core/src/odbc/mod.rs | 2 +- sqlx-core/src/odbc/types/bigdecimal.rs | 31 +- sqlx-core/src/odbc/types/bool.rs | 52 +-- sqlx-core/src/odbc/types/bytes.rs | 52 ++- sqlx-core/src/odbc/types/chrono.rs | 49 +- sqlx-core/src/odbc/types/decimal.rs | 53 +-- sqlx-core/src/odbc/types/float.rs | 29 +- sqlx-core/src/odbc/types/int.rs | 85 ++-- sqlx-core/src/odbc/types/json.rs | 24 +- sqlx-core/src/odbc/types/str.rs | 14 +- sqlx-core/src/odbc/types/time.rs | 24 +- sqlx-core/src/odbc/types/uuid.rs | 17 +- sqlx-core/src/odbc/value.rs | 442 +++++++++++++++++-- tests/odbc/odbc.rs | 2 - tests/odbc/types.rs | 17 +- 16 files changed, 648 insertions(+), 515 deletions(-) diff --git a/sqlx-core/src/odbc/connection/odbc_bridge.rs b/sqlx-core/src/odbc/connection/odbc_bridge.rs index 965a0c3ff2..73df3b37a0 100644 --- a/sqlx-core/src/odbc/connection/odbc_bridge.rs +++ b/sqlx-core/src/odbc/connection/odbc_bridge.rs @@ -1,7 +1,7 @@ use crate::error::Error; use crate::odbc::{ - connection::MaybePrepared, OdbcArgumentValue, OdbcArguments, OdbcColumn, OdbcQueryResult, - OdbcRow, OdbcTypeInfo, OdbcValue, + connection::MaybePrepared, ColumnData, OdbcArgumentValue, OdbcArguments, OdbcColumn, + OdbcQueryResult, OdbcRow, OdbcTypeInfo, OdbcValue, }; use either::Either; use flume::{SendError, Sender}; @@ -10,6 +10,7 @@ use odbc_api::handles::{AsStatementRef, Nullability, Statement}; use odbc_api::DataType; use odbc_api::{Cursor, IntoParameter, ResultSetMetadata}; use std::cmp::min; +use std::sync::Arc; // Bulk fetch implementation using columnar buffers instead of row-by-row fetching // This provides significant performance improvements by fetching rows in batches @@ -18,8 +19,8 @@ const BATCH_SIZE: usize = 128; const DEFAULT_TEXT_LEN: usize = 512; const DEFAULT_BINARY_LEN: usize = 1024; const DEFAULT_NUMERIC_TEXT_LEN: usize = 128; -const MAX_TEXT_LEN: usize = 1024 * 1024; -const MAX_BINARY_LEN: usize = 1024 * 1024; +const MAX_TEXT_LEN: usize = 1024; +const MAX_BINARY_LEN: usize = 1024; struct ColumnBinding { column: OdbcColumn, @@ -238,21 +239,24 @@ where while let Some(batch) = row_set_cursor.fetch()? { let columns: Vec<_> = bindings.iter().map(|b| b.column.clone()).collect(); + // Create ColumnData instances that can be shared across rows + let column_data_vec: Vec<_> = bindings + .iter() + .enumerate() + .map(|(col_index, binding)| { + create_column_data(batch.column(col_index), &binding.column.type_info) + }) + .collect(); + for row_index in 0..batch.num_rows() { - let row_values: Vec<_> = bindings + let row_values: Vec<_> = column_data_vec .iter() - .enumerate() - .map(|(col_index, binding)| { - let type_info = binding.column.type_info.clone(); - let value = - extract_value_from_buffer(batch.column(col_index), row_index, &type_info); - (type_info, value) - }) + .map(|column_data| OdbcValue::new(Arc::clone(column_data), row_index)) .collect(); let row = OdbcRow { columns: columns.clone(), - values: row_values.into_iter().map(|(_, value)| value).collect(), + values: row_values, }; if send_row(tx, row).is_err() { @@ -269,237 +273,11 @@ where Ok(receiver_open) } -fn extract_value_from_buffer( - slice: AnySlice<'_>, - row_index: usize, - type_info: &OdbcTypeInfo, -) -> OdbcValue { - match slice { - AnySlice::I8(s) => OdbcValue { - type_info: type_info.clone(), - is_null: false, - text: None, - blob: None, - int: Some(s[row_index] as i64), - float: None, - }, - AnySlice::I16(s) => OdbcValue { - type_info: type_info.clone(), - is_null: false, - text: None, - blob: None, - int: Some(s[row_index] as i64), - float: None, - }, - AnySlice::I32(s) => OdbcValue { - type_info: type_info.clone(), - is_null: false, - text: None, - blob: None, - int: Some(s[row_index] as i64), - float: None, - }, - AnySlice::I64(s) => OdbcValue { - type_info: type_info.clone(), - is_null: false, - text: None, - blob: None, - int: Some(s[row_index]), - float: None, - }, - AnySlice::F32(s) => OdbcValue { - type_info: type_info.clone(), - is_null: false, - text: None, - blob: None, - int: None, - float: Some(s[row_index] as f64), - }, - AnySlice::F64(s) => OdbcValue { - type_info: type_info.clone(), - is_null: false, - text: None, - blob: None, - int: None, - float: Some(s[row_index]), - }, - AnySlice::Bit(s) => OdbcValue { - type_info: type_info.clone(), - is_null: false, - text: None, - blob: None, - int: Some(s[row_index].0 as i64), - float: None, - }, - AnySlice::Text(s) => { - let text = s - .get(row_index) - .map(|bytes| String::from_utf8_lossy(bytes).to_string()); - OdbcValue { - type_info: type_info.clone(), - is_null: text.is_none(), - text, - blob: None, - int: None, - float: None, - } - } - AnySlice::Binary(s) => { - let blob = s.get(row_index).map(|bytes| bytes.to_vec()); - OdbcValue { - type_info: type_info.clone(), - is_null: blob.is_none(), - text: None, - blob, - int: None, - float: None, - } - } - AnySlice::NullableI8(s) => { - let (is_null, int) = if let Some(&val) = s.get(row_index) { - (false, Some(val as i64)) - } else { - (true, None) - }; - OdbcValue { - type_info: type_info.clone(), - is_null, - text: None, - blob: None, - int, - float: None, - } - } - AnySlice::NullableI16(s) => { - let (is_null, int) = if let Some(&val) = s.get(row_index) { - (false, Some(val as i64)) - } else { - (true, None) - }; - OdbcValue { - type_info: type_info.clone(), - is_null, - text: None, - blob: None, - int, - float: None, - } - } - AnySlice::NullableI32(s) => { - let (is_null, int) = if let Some(&val) = s.get(row_index) { - (false, Some(val as i64)) - } else { - (true, None) - }; - OdbcValue { - type_info: type_info.clone(), - is_null, - text: None, - blob: None, - int, - float: None, - } - } - AnySlice::NullableI64(s) => { - let (is_null, int) = if let Some(&val) = s.get(row_index) { - (false, Some(val)) - } else { - (true, None) - }; - OdbcValue { - type_info: type_info.clone(), - is_null, - text: None, - blob: None, - int, - float: None, - } - } - AnySlice::NullableF32(s) => { - let (is_null, float) = if let Some(&val) = s.get(row_index) { - (false, Some(val as f64)) - } else { - (true, None) - }; - OdbcValue { - type_info: type_info.clone(), - is_null, - text: None, - blob: None, - int: None, - float, - } - } - AnySlice::NullableF64(s) => { - let (is_null, float) = if let Some(&val) = s.get(row_index) { - (false, Some(val)) - } else { - (true, None) - }; - OdbcValue { - type_info: type_info.clone(), - is_null, - text: None, - blob: None, - int: None, - float, - } - } - AnySlice::NullableBit(s) => { - let (is_null, int) = if let Some(&val) = s.get(row_index) { - (false, Some(val.0 as i64)) - } else { - (true, None) - }; - OdbcValue { - type_info: type_info.clone(), - is_null, - text: None, - blob: None, - int, - float: None, - } - } - AnySlice::Date(s) => { - let text = s.get(row_index).map(|date| format!("{:?}", date)); - OdbcValue { - type_info: type_info.clone(), - is_null: text.is_none(), - text, - blob: None, - int: None, - float: None, - } - } - AnySlice::Time(s) => { - let text = s.get(row_index).map(|time| format!("{:?}", time)); - OdbcValue { - type_info: type_info.clone(), - is_null: text.is_none(), - text, - blob: None, - int: None, - float: None, - } - } - AnySlice::Timestamp(s) => { - let text = s.get(row_index).map(|ts| format!("{:?}", ts)); - OdbcValue { - type_info: type_info.clone(), - is_null: text.is_none(), - text, - blob: None, - int: None, - float: None, - } - } - _ => OdbcValue { - type_info: type_info.clone(), - is_null: true, - text: None, - blob: None, - int: None, - float: None, - }, - } +fn create_column_data(slice: AnySlice<'_>, type_info: &OdbcTypeInfo) -> Arc { + use crate::odbc::value::convert_any_slice_to_value_vec; + + Arc::new(ColumnData { + values: convert_any_slice_to_value_vec(slice), + type_info: type_info.clone(), + }) } diff --git a/sqlx-core/src/odbc/mod.rs b/sqlx-core/src/odbc/mod.rs index 492cc370b6..c45bff63da 100644 --- a/sqlx-core/src/odbc/mod.rs +++ b/sqlx-core/src/odbc/mod.rs @@ -47,7 +47,7 @@ pub use row::OdbcRow; pub use statement::{OdbcStatement, OdbcStatementMetadata}; pub use transaction::OdbcTransactionManager; pub use type_info::{DataTypeExt, OdbcTypeInfo}; -pub use value::{OdbcValue, OdbcValueRef}; +pub use value::{ColumnData, OdbcValue, OdbcValueRef, OdbcValueType, OdbcValueVec}; /// An alias for [`Pool`][crate::pool::Pool], specialized for ODBC. pub type OdbcPool = crate::pool::Pool; diff --git a/sqlx-core/src/odbc/types/bigdecimal.rs b/sqlx-core/src/odbc/types/bigdecimal.rs index 7b15f65e15..abdd8cee24 100644 --- a/sqlx-core/src/odbc/types/bigdecimal.rs +++ b/sqlx-core/src/odbc/types/bigdecimal.rs @@ -3,7 +3,7 @@ use crate::encode::Encode; use crate::error::BoxDynError; use crate::odbc::{DataTypeExt, Odbc, OdbcArgumentValue, OdbcTypeInfo, OdbcValueRef}; use crate::types::Type; -use bigdecimal::{BigDecimal, FromPrimitive}; +use bigdecimal::BigDecimal; use odbc_api::DataType; use std::str::FromStr; @@ -36,19 +36,22 @@ impl<'q> Encode<'q, Odbc> for BigDecimal { impl<'r> Decode<'r, Odbc> for BigDecimal { fn decode(value: OdbcValueRef<'r>) -> Result { - if let Some(int) = value.int { - return Ok(BigDecimal::from(int)); + if let Some(int) = value.int::() { + Ok(BigDecimal::from(int)) + } else if let Some(float) = value.float::() { + Ok(BigDecimal::try_from(float)?) + } else if let Some(text) = value.text() { + let text = text.trim(); + Ok(BigDecimal::from_str(text).map_err(|e| format!("bad decimal text: {}", e))?) + } else if let Some(bytes) = value.blob() { + if let Ok(s) = std::str::from_utf8(bytes) { + Ok(BigDecimal::parse_bytes(s.as_bytes(), 10) + .ok_or(format!("bad base10 bytes: {:?}", bytes))?) + } else { + Err(format!("bad utf8 bytes: {:?}", bytes).into()) + } + } else { + Err(format!("ODBC: cannot decode BigDecimal: {:?}", value).into()) } - if let Some(float) = value.float { - return Ok(BigDecimal::from_f64(float).ok_or(format!("bad float: {}", float))?); - } - if let Some(text) = value.text { - return Ok(BigDecimal::from_str(text).map_err(|e| format!("bad decimal text: {}", e))?); - } - if let Some(bytes) = value.blob { - return Ok(BigDecimal::parse_bytes(bytes, 10) - .ok_or(format!("bad base10 bytes: {:?}", bytes))?); - } - Err(format!("ODBC: cannot decode BigDecimal: {:?}", value).into()) } } diff --git a/sqlx-core/src/odbc/types/bool.rs b/sqlx-core/src/odbc/types/bool.rs index d5620235ec..4cb0000524 100644 --- a/sqlx-core/src/odbc/types/bool.rs +++ b/sqlx-core/src/odbc/types/bool.rs @@ -40,49 +40,43 @@ impl<'q> Encode<'q, Odbc> for bool { impl<'r> Decode<'r, Odbc> for bool { fn decode(value: OdbcValueRef<'r>) -> Result { - if let Some(i) = value.int { + if let Some(i) = value.int::() { return Ok(i != 0); } - // Handle float values (from DECIMAL/NUMERIC types) - if let Some(f) = value.float { + if let Some(f) = value.float::() { return Ok(f != 0.0); } - if let Some(text) = value.text { + if let Some(text) = value.text() { let text = text.trim(); - // Try exact string matches first return Ok(match text { "0" | "0.0" | "false" | "FALSE" | "f" | "F" => false, "1" | "1.0" | "true" | "TRUE" | "t" | "T" => true, _ => { - // Try parsing as number first if let Ok(num) = text.parse::() { num != 0.0 } else if let Ok(num) = text.parse::() { num != 0 } else { - // Fall back to string parsing text.parse()? } } }); } - if let Some(bytes) = value.blob { + if let Some(bytes) = value.blob() { let s = std::str::from_utf8(bytes)?; let s = s.trim(); return Ok(match s { "0" | "0.0" | "false" | "FALSE" | "f" | "F" => false, "1" | "1.0" | "true" | "TRUE" | "t" | "T" => true, _ => { - // Try parsing as number first if let Ok(num) = s.parse::() { num != 0.0 } else if let Ok(num) = s.parse::() { num != 0 } else { - // Fall back to string parsing s.parse()? } } @@ -96,41 +90,29 @@ impl<'r> Decode<'r, Odbc> for bool { #[cfg(test)] mod tests { use super::*; - use crate::odbc::{OdbcTypeInfo, OdbcValueRef}; + use crate::odbc::{ColumnData, OdbcTypeInfo, OdbcValueVec}; use crate::type_info::TypeInfo; use odbc_api::DataType; - fn create_test_value_text(text: &'static str, data_type: DataType) -> OdbcValueRef<'static> { - OdbcValueRef { + fn make_ref(value_vec: OdbcValueVec, data_type: DataType) -> OdbcValueRef<'static> { + let column = ColumnData { + values: value_vec, type_info: OdbcTypeInfo::new(data_type), - is_null: false, - text: Some(text), - blob: None, - int: None, - float: None, - } + }; + let ptr = Box::leak(Box::new(column)); + OdbcValueRef::new(ptr, 0) + } + + fn create_test_value_text(text: &'static str, data_type: DataType) -> OdbcValueRef<'static> { + make_ref(OdbcValueVec::Text(vec![Some(text.to_string())]), data_type) } fn create_test_value_int(value: i64, data_type: DataType) -> OdbcValueRef<'static> { - OdbcValueRef { - type_info: OdbcTypeInfo::new(data_type), - is_null: false, - text: None, - blob: None, - int: Some(value), - float: None, - } + make_ref(OdbcValueVec::NullableBigInt(vec![Some(value)]), data_type) } fn create_test_value_float(value: f64, data_type: DataType) -> OdbcValueRef<'static> { - OdbcValueRef { - type_info: OdbcTypeInfo::new(data_type), - is_null: false, - text: None, - blob: None, - int: None, - float: Some(value), - } + make_ref(OdbcValueVec::NullableDouble(vec![Some(value)]), data_type) } #[test] diff --git a/sqlx-core/src/odbc/types/bytes.rs b/sqlx-core/src/odbc/types/bytes.rs index 6ad56a7554..f3a6937014 100644 --- a/sqlx-core/src/odbc/types/bytes.rs +++ b/sqlx-core/src/odbc/types/bytes.rs @@ -40,19 +40,22 @@ impl<'q> Encode<'q, Odbc> for &'q [u8] { impl<'r> Decode<'r, Odbc> for Vec { fn decode(value: OdbcValueRef<'r>) -> Result { - Ok(<&[u8] as Decode<'r, Odbc>>::decode(value)?.to_vec()) + if let Some(bytes) = value.blob() { + Ok(bytes.to_vec()) + } else if let Some(text) = value.text() { + Ok(text.as_bytes().to_vec()) + } else { + Err("ODBC: cannot decode as Vec".into()) + } } } impl<'r> Decode<'r, Odbc> for &'r [u8] { fn decode(value: OdbcValueRef<'r>) -> Result { - if let Some(bytes) = value.blob { - return Ok(bytes); - } - if let Some(text) = value.text { - return Ok(text.as_bytes()); - } - Err(format!("ODBC: cannot decode {:?} as &[u8]", value).into()) + value + .blob() + .or_else(|| value.text().map(|text| text.as_bytes())) + .ok_or(format!("ODBC: cannot decode as &[u8]: {:?}", value).into()) } } @@ -68,30 +71,25 @@ impl Type for [u8] { #[cfg(test)] mod tests { use super::*; - use crate::odbc::{OdbcTypeInfo, OdbcValueRef}; + use crate::odbc::{ColumnData, OdbcTypeInfo, OdbcValueVec}; use crate::type_info::TypeInfo; use odbc_api::DataType; - fn create_test_value_text(text: &'static str, data_type: DataType) -> OdbcValueRef<'static> { - OdbcValueRef { + fn make_ref(value_vec: OdbcValueVec, data_type: DataType) -> OdbcValueRef<'static> { + let column = ColumnData { + values: value_vec, type_info: OdbcTypeInfo::new(data_type), - is_null: false, - text: Some(text), - blob: None, - int: None, - float: None, - } + }; + let ptr = Box::leak(Box::new(column)); + OdbcValueRef::new(ptr, 0) + } + + fn create_test_value_text(text: &'static str, data_type: DataType) -> OdbcValueRef<'static> { + make_ref(OdbcValueVec::Text(vec![Some(text.to_string())]), data_type) } fn create_test_value_blob(data: &'static [u8], data_type: DataType) -> OdbcValueRef<'static> { - OdbcValueRef { - type_info: OdbcTypeInfo::new(data_type), - is_null: false, - text: None, - blob: Some(data), - int: None, - float: None, - } + make_ref(OdbcValueVec::Binary(vec![Some(data.to_vec())]), data_type) } #[test] @@ -160,11 +158,11 @@ mod tests { fn test_vec_u8_encode() { let mut buf = Vec::new(); let data = vec![65, 66, 67, 68, 69]; // "ABCDE" - let result = as Encode>::encode(data, &mut buf); + let result = as Encode>::encode(data.clone(), &mut buf); assert!(matches!(result, crate::encode::IsNull::No)); assert_eq!(buf.len(), 1); if let OdbcArgumentValue::Bytes(bytes) = &buf[0] { - assert_eq!(*bytes, vec![65, 66, 67, 68, 69]); + assert_eq!(*bytes, data); } else { panic!("Expected Bytes argument"); } diff --git a/sqlx-core/src/odbc/types/chrono.rs b/sqlx-core/src/odbc/types/chrono.rs index 178885dacd..d1069e3a1e 100644 --- a/sqlx-core/src/odbc/types/chrono.rs +++ b/sqlx-core/src/odbc/types/chrono.rs @@ -192,11 +192,11 @@ fn parse_yyyymmdd_text_as_naive_date(s: &str) -> Option { } fn get_text_from_value(value: &OdbcValueRef<'_>) -> Result, BoxDynError> { - if let Some(text) = value.text { + if let Some(text) = value.text() { let trimmed = text.trim_matches('\u{0}').trim(); return Ok(Some(trimmed.to_string())); } - if let Some(bytes) = value.blob { + if let Some(bytes) = value.blob() { let s = std::str::from_utf8(bytes)?; let trimmed = s.trim_matches('\u{0}').trim(); return Ok(Some(trimmed.to_string())); @@ -207,8 +207,8 @@ fn get_text_from_value(value: &OdbcValueRef<'_>) -> Result, BoxDy impl<'r> Decode<'r, Odbc> for NaiveDate { fn decode(value: OdbcValueRef<'r>) -> Result { // Handle text values first (most common for dates) - if let Some(text) = get_text_from_value(&value)? { - if let Some(date) = parse_yyyymmdd_text_as_naive_date(&text) { + if let Some(text) = value.text() { + if let Some(date) = parse_yyyymmdd_text_as_naive_date(text) { return Ok(date); } if let Ok(date) = text.parse() { @@ -217,7 +217,7 @@ impl<'r> Decode<'r, Odbc> for NaiveDate { } // Handle numeric YYYYMMDD format (for databases that return as numbers) - if let Some(int_val) = value.int { + if let Some(int_val) = value.int() { if let Some(date) = parse_yyyymmdd_as_naive_date(int_val) { return Ok(date); } @@ -229,7 +229,7 @@ impl<'r> Decode<'r, Odbc> for NaiveDate { } // Handle float values similarly - if let Some(float_val) = value.float { + if let Some(float_val) = value.float::() { if let Some(date) = parse_yyyymmdd_as_naive_date(float_val as i64) { return Ok(date); } @@ -242,7 +242,7 @@ impl<'r> Decode<'r, Odbc> for NaiveDate { Err(format!( "ODBC: cannot decode NaiveDate from value with type '{}'", - value.type_info.name() + value.column_data.type_info.name() ) .into()) } @@ -368,26 +368,29 @@ mod tests { use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc}; use odbc_api::DataType; - fn create_test_value_text(text: &'static str, data_type: DataType) -> OdbcValueRef<'static> { - OdbcValueRef { + fn make_ref(value_vec: OdbcValueVec, data_type: DataType) -> OdbcValueRef<'static> { + let column = ColumnData { + values: value_vec, type_info: OdbcTypeInfo::new(data_type), - is_null: false, - text: Some(text), - blob: None, - int: None, - float: None, - } + }; + let ptr = Box::leak(Box::new(column)); + OdbcValueRef::new(ptr, 0) + } + + fn create_test_value_text(text: &'static str, data_type: DataType) -> OdbcValueRef<'static> { + make_ref(OdbcValueVec::Text(vec![Some(text.to_string())]), data_type) } fn create_test_value_int(value: i64, data_type: DataType) -> OdbcValueRef<'static> { - OdbcValueRef { - type_info: OdbcTypeInfo::new(data_type), - is_null: false, - text: None, - blob: None, - int: Some(value), - float: None, - } + make_ref(OdbcValueVec::NullableBigInt(vec![Some(value)]), data_type) + } + + fn create_test_value_float(value: f64, data_type: DataType) -> OdbcValueRef<'static> { + make_ref(OdbcValueVec::NullableDouble(vec![Some(value)]), data_type) + } + + fn create_test_value_blob(data: &'static [u8], data_type: DataType) -> OdbcValueRef<'static> { + make_ref(OdbcValueVec::Binary(vec![Some(data.to_vec())]), data_type) } #[test] diff --git a/sqlx-core/src/odbc/types/decimal.rs b/sqlx-core/src/odbc/types/decimal.rs index ba796e9b9d..fe9562458a 100644 --- a/sqlx-core/src/odbc/types/decimal.rs +++ b/sqlx-core/src/odbc/types/decimal.rs @@ -36,12 +36,13 @@ impl<'q> Encode<'q, Odbc> for Decimal { // Helper function for getting text from value for decimal parsing fn get_text_for_decimal_parsing(value: &OdbcValueRef<'_>) -> Result, BoxDynError> { - if let Some(text) = value.text { + if let Some(text) = value.text() { return Ok(Some(text.trim().to_string())); } - if let Some(bytes) = value.blob { - let s = std::str::from_utf8(bytes)?; - return Ok(Some(s.trim().to_string())); + if let Some(bytes) = value.blob() { + if let Ok(text) = std::str::from_utf8(bytes) { + return Ok(Some(text.trim().to_string())); + } } Ok(None) } @@ -49,12 +50,12 @@ fn get_text_for_decimal_parsing(value: &OdbcValueRef<'_>) -> Result Decode<'r, Odbc> for Decimal { fn decode(value: OdbcValueRef<'r>) -> Result { // Try integer conversion first (most precise) - if let Some(int_val) = value.int { + if let Some(int_val) = value.int::() { return Ok(Decimal::from(int_val)); } // Try direct float conversion for better precision - if let Some(float_val) = value.float { + if let Some(float_val) = value.float::() { if let Ok(decimal) = Decimal::try_from(float_val) { return Ok(decimal); } @@ -77,37 +78,29 @@ mod tests { use odbc_api::DataType; use std::str::FromStr; - fn create_test_value_text(text: &str, data_type: DataType) -> OdbcValueRef<'_> { - OdbcValueRef { + fn make_ref(value_vec: OdbcValueVec, data_type: DataType) -> OdbcValueRef<'static> { + let column = ColumnData { + values: value_vec, type_info: OdbcTypeInfo::new(data_type), - is_null: false, - text: Some(text), - blob: None, - int: None, - float: None, - } + }; + let ptr = Box::leak(Box::new(column)); + OdbcValueRef::new(ptr, 0) + } + + fn create_test_value_text(text: &'static str, data_type: DataType) -> OdbcValueRef<'static> { + make_ref(OdbcValueVec::Text(vec![Some(text.to_string())]), data_type) + } + + fn create_test_value_bytes(bytes: &'static [u8], data_type: DataType) -> OdbcValueRef<'static> { + make_ref(OdbcValueVec::Binary(vec![Some(bytes.to_vec())]), data_type) } fn create_test_value_int(value: i64, data_type: DataType) -> OdbcValueRef<'static> { - OdbcValueRef { - type_info: OdbcTypeInfo::new(data_type), - is_null: false, - text: None, - blob: None, - int: Some(value), - float: None, - } + make_ref(OdbcValueVec::NullableBigInt(vec![Some(value)]), data_type) } fn create_test_value_float(value: f64, data_type: DataType) -> OdbcValueRef<'static> { - OdbcValueRef { - type_info: OdbcTypeInfo::new(data_type), - is_null: false, - text: None, - blob: None, - int: None, - float: Some(value), - } + make_ref(OdbcValueVec::NullableDouble(vec![Some(value)]), data_type) } #[test] diff --git a/sqlx-core/src/odbc/types/float.rs b/sqlx-core/src/odbc/types/float.rs index 09ed1fcb90..3c7f4d0c0b 100644 --- a/sqlx-core/src/odbc/types/float.rs +++ b/sqlx-core/src/odbc/types/float.rs @@ -71,21 +71,40 @@ impl<'q> Encode<'q, Odbc> for f64 { impl<'r> Decode<'r, Odbc> for f64 { fn decode(value: OdbcValueRef<'r>) -> Result { - if let Some(f) = value.float { + if let Some(f) = value.float::() { return Ok(f); } - if let Some(int) = value.int { + + if let Some(int) = value.int::() { return Ok(int as f64); } - if let Some(s) = value.text { - return Ok(s.trim().parse()?); + + if let Some(s) = value.text() { + if let Ok(parsed) = s.trim().parse::() { + return Ok(parsed); + } } + Err(format!("ODBC: cannot decode f64: {:?}", value).into()) } } impl<'r> Decode<'r, Odbc> for f32 { fn decode(value: OdbcValueRef<'r>) -> Result { - Ok(>::decode(value)? as f32) + if let Some(f) = value.float::() { + return Ok(f as f32); + } + + if let Some(int) = value.int::() { + return Ok(int as f32); + } + + if let Some(s) = value.text() { + if let Ok(parsed) = s.trim().parse::() { + return Ok(parsed); + } + } + + Err(format!("ODBC: cannot decode f32: {:?}", value).into()) } } diff --git a/sqlx-core/src/odbc/types/int.rs b/sqlx-core/src/odbc/types/int.rs index 485d963194..121b2deb6b 100644 --- a/sqlx-core/src/odbc/types/int.rs +++ b/sqlx-core/src/odbc/types/int.rs @@ -263,10 +263,10 @@ fn parse_numeric_as_i64(s: &str) -> Option { } fn get_text_for_numeric_parsing(value: &OdbcValueRef<'_>) -> Result, BoxDynError> { - if let Some(text) = value.text { + if let Some(text) = value.text() { return Ok(Some(text.trim().to_string())); } - if let Some(bytes) = value.blob { + if let Some(bytes) = value.blob() { let s = std::str::from_utf8(bytes)?; return Ok(Some(s.trim().to_string())); } @@ -275,64 +275,49 @@ fn get_text_for_numeric_parsing(value: &OdbcValueRef<'_>) -> Result Decode<'r, Odbc> for i64 { fn decode(value: OdbcValueRef<'r>) -> Result { - if let Some(i) = value.int { - return Ok(i); - } - if let Some(f) = value.float { - return Ok(f as i64); - } - if let Some(text) = get_text_for_numeric_parsing(&value)? { - if let Some(parsed) = parse_numeric_as_i64(&text) { - return Ok(parsed); - } - } - Err("ODBC: cannot decode i64".into()) + Ok(value.try_int::()?) } } impl<'r> Decode<'r, Odbc> for i32 { fn decode(value: OdbcValueRef<'r>) -> Result { - Ok(>::decode(value)? as i32) + Ok(value.try_int::()?) } } impl<'r> Decode<'r, Odbc> for i16 { fn decode(value: OdbcValueRef<'r>) -> Result { - Ok(>::decode(value)? as i16) + Ok(value.try_int::()?) } } impl<'r> Decode<'r, Odbc> for i8 { fn decode(value: OdbcValueRef<'r>) -> Result { - Ok(>::decode(value)? as i8) + Ok(value.try_int::()?) } } impl<'r> Decode<'r, Odbc> for u8 { fn decode(value: OdbcValueRef<'r>) -> Result { - let i = >::decode(value)?; - Ok(u8::try_from(i)?) + Ok(value.try_int::()?) } } impl<'r> Decode<'r, Odbc> for u16 { fn decode(value: OdbcValueRef<'r>) -> Result { - let i = >::decode(value)?; - Ok(u16::try_from(i)?) + Ok(value.try_int::()?) } } impl<'r> Decode<'r, Odbc> for u32 { fn decode(value: OdbcValueRef<'r>) -> Result { - let i = >::decode(value)?; - Ok(u32::try_from(i)?) + Ok(value.try_int::()?) } } impl<'r> Decode<'r, Odbc> for u64 { fn decode(value: OdbcValueRef<'r>) -> Result { - let i = >::decode(value)?; - Ok(u64::try_from(i)?) + Ok(value.try_int::()?) } } @@ -342,37 +327,29 @@ mod tests { use crate::odbc::{OdbcTypeInfo, OdbcValueRef}; use odbc_api::DataType; - fn create_test_value_text(text: &'static str, data_type: DataType) -> OdbcValueRef<'static> { - OdbcValueRef { + fn make_ref(value_vec: OdbcValueVec, data_type: DataType) -> OdbcValueRef<'static> { + let column = ColumnData { + values: value_vec, type_info: OdbcTypeInfo::new(data_type), - is_null: false, - text: Some(text), - blob: None, - int: None, - float: None, - } + }; + let ptr = Box::leak(Box::new(column)); + OdbcValueRef::new(ptr, 0) + } + + fn create_test_value_text(text: &'static str, data_type: DataType) -> OdbcValueRef<'static> { + make_ref(OdbcValueVec::Text(vec![Some(text.to_string())]), data_type) + } + + fn create_test_value_blob(data: &'static [u8], data_type: DataType) -> OdbcValueRef<'static> { + make_ref(OdbcValueVec::Binary(vec![Some(data.to_vec())]), data_type) } fn create_test_value_int(value: i64, data_type: DataType) -> OdbcValueRef<'static> { - OdbcValueRef { - type_info: OdbcTypeInfo::new(data_type), - is_null: false, - text: None, - blob: None, - int: Some(value), - float: None, - } + make_ref(OdbcValueVec::NullableBigInt(vec![Some(value)]), data_type) } fn create_test_value_float(value: f64, data_type: DataType) -> OdbcValueRef<'static> { - OdbcValueRef { - type_info: OdbcTypeInfo::new(data_type), - is_null: false, - text: None, - blob: None, - int: None, - float: Some(value), - } + make_ref(OdbcValueVec::NullableDouble(vec![Some(value)]), data_type) } #[test] @@ -520,14 +497,12 @@ mod tests { #[test] fn test_decode_error_handling() { - let value = OdbcValueRef { + let column = ColumnData { + values: OdbcValueVec::Text(vec![Some("not_a_number".to_string())]), type_info: OdbcTypeInfo::INTEGER, - is_null: false, - text: None, - blob: None, - int: None, - float: None, }; + let ptr = Box::leak(Box::new(column)); + let value = OdbcValueRef::new(ptr, 0); let result = >::decode(value); assert!(result.is_err()); diff --git a/sqlx-core/src/odbc/types/json.rs b/sqlx-core/src/odbc/types/json.rs index dcd7db14c4..23f530cbd5 100644 --- a/sqlx-core/src/odbc/types/json.rs +++ b/sqlx-core/src/odbc/types/json.rs @@ -3,7 +3,6 @@ use crate::encode::Encode; use crate::error::BoxDynError; use crate::odbc::{DataTypeExt, Odbc, OdbcArgumentValue, OdbcTypeInfo, OdbcValueRef}; use crate::types::Type; -use serde::de::Error; use serde_json::Value; impl Type for Value { @@ -35,18 +34,19 @@ impl<'q> Encode<'q, Odbc> for Value { impl<'r> Decode<'r, Odbc> for Value { fn decode(value: OdbcValueRef<'r>) -> Result { - if let Some(bytes) = value.blob { - serde_json::from_slice(bytes) - } else if let Some(text) = value.text { - serde_json::from_str(text) - } else if let Some(i) = value.int { - Ok(serde_json::Value::from(i)) - } else if let Some(f) = value.float { - Ok(serde_json::Value::from(f)) - } else { - Err(serde_json::Error::custom("not a valid json type")) + if let Some(bytes) = value.blob() { + return serde_json::from_slice(bytes) + .map_err(|e| format!("ODBC: cannot decode JSON from {:?}: {}", value, e).into()); + } else if let Some(text) = value.text() { + return serde_json::from_str(text) + .map_err(|e| format!("ODBC: cannot decode JSON from {:?}: {}", value, e).into()); + } else if let Some(i) = value.int::() { + return Ok(Value::from(i)); + } else if let Some(f) = value.float::() { + return Ok(Value::from(f)); } - .map_err(|e| format!("ODBC: cannot decode JSON from {:?}: {}", value, e).into()) + + Err(format!("ODBC: cannot decode JSON from {:?}", value).into()) } } diff --git a/sqlx-core/src/odbc/types/str.rs b/sqlx-core/src/odbc/types/str.rs index 32207efb6c..ae568d2341 100644 --- a/sqlx-core/src/odbc/types/str.rs +++ b/sqlx-core/src/odbc/types/str.rs @@ -48,11 +48,11 @@ impl<'q> Encode<'q, Odbc> for &'q str { impl<'r> Decode<'r, Odbc> for String { fn decode(value: OdbcValueRef<'r>) -> Result { - if let Some(text) = value.text { - return Ok(text.to_owned()); + if let Some(text) = value.text() { + return Ok(text.to_string()); } - if let Some(bytes) = value.blob { - return Ok(std::str::from_utf8(bytes)?.to_owned()); + if let Some(bytes) = value.blob() { + return Ok(String::from_utf8(bytes.to_vec())?); } Err("ODBC: cannot decode String".into()) } @@ -60,12 +60,12 @@ impl<'r> Decode<'r, Odbc> for String { impl<'r> Decode<'r, Odbc> for &'r str { fn decode(value: OdbcValueRef<'r>) -> Result { - if let Some(text) = value.text { + if let Some(text) = value.text() { return Ok(text); } - if let Some(bytes) = value.blob { + if let Some(bytes) = value.blob() { return Ok(std::str::from_utf8(bytes)?); } - Err("ODBC: cannot decode &str".into()) + Err(format!("ODBC: cannot decode &str: {:?}", value).into()) } } diff --git a/sqlx-core/src/odbc/types/time.rs b/sqlx-core/src/odbc/types/time.rs index 3d0d0d0d44..9414763f4f 100644 --- a/sqlx-core/src/odbc/types/time.rs +++ b/sqlx-core/src/odbc/types/time.rs @@ -103,20 +103,20 @@ fn parse_unix_timestamp_as_offset_datetime(timestamp: i64) -> Option Decode<'r, Odbc> for OffsetDateTime { fn decode(value: OdbcValueRef<'r>) -> Result { // Handle numeric timestamps (Unix epoch seconds) first - if let Some(int_val) = value.int { + if let Some(int_val) = value.int() { if let Some(dt) = parse_unix_timestamp_as_offset_datetime(int_val) { return Ok(dt); } } - if let Some(float_val) = value.float { + if let Some(float_val) = value.float::() { if let Some(dt) = parse_unix_timestamp_as_offset_datetime(float_val as i64) { return Ok(dt); } } // Handle text values - if let Some(text) = value.text { + if let Some(text) = value.text() { let trimmed = text.trim(); // Try parsing as ISO-8601 timestamp with timezone if let Ok(dt) = OffsetDateTime::parse( @@ -148,14 +148,14 @@ impl<'r> Decode<'r, Odbc> for OffsetDateTime { impl<'r> Decode<'r, Odbc> for PrimitiveDateTime { fn decode(value: OdbcValueRef<'r>) -> Result { // Handle numeric timestamps (Unix epoch seconds) first - if let Some(int_val) = value.int { + if let Some(int_val) = value.int() { if let Some(offset_dt) = parse_unix_timestamp_as_offset_datetime(int_val) { let utc_dt = offset_dt.to_offset(time::UtcOffset::UTC); return Ok(PrimitiveDateTime::new(utc_dt.date(), utc_dt.time())); } } - if let Some(float_val) = value.float { + if let Some(float_val) = value.float::() { if let Some(offset_dt) = parse_unix_timestamp_as_offset_datetime(float_val as i64) { let utc_dt = offset_dt.to_offset(time::UtcOffset::UTC); return Ok(PrimitiveDateTime::new(utc_dt.date(), utc_dt.time())); @@ -163,7 +163,7 @@ impl<'r> Decode<'r, Odbc> for PrimitiveDateTime { } // Handle text values - if let Some(text) = value.text { + if let Some(text) = value.text() { let trimmed = text.trim(); // Try parsing as ISO-8601 if let Ok(dt) = PrimitiveDateTime::parse( @@ -228,7 +228,7 @@ fn parse_yyyymmdd_text_as_time_date(s: &str) -> Option { impl<'r> Decode<'r, Odbc> for Date { fn decode(value: OdbcValueRef<'r>) -> Result { // Handle numeric YYYYMMDD format first - if let Some(int_val) = value.int { + if let Some(int_val) = value.int() { if let Some(date) = parse_yyyymmdd_as_time_date(int_val) { return Ok(date); } @@ -243,14 +243,14 @@ impl<'r> Decode<'r, Odbc> for Date { } // Handle float values - if let Some(float_val) = value.float { + if let Some(float_val) = value.float::() { if let Some(date) = parse_yyyymmdd_as_time_date(float_val as i64) { return Ok(date); } } // Handle text values - if let Some(text) = value.text { + if let Some(text) = value.text() { let trimmed = text.trim(); if let Some(date) = parse_yyyymmdd_text_as_time_date(trimmed) { return Ok(date); @@ -290,20 +290,20 @@ fn parse_seconds_as_time(seconds: i64) -> Option