Skip to content

Commit

Permalink
blob [WIP]
Browse files Browse the repository at this point in the history
  • Loading branch information
pacman82 committed Jul 10, 2021
1 parent 8ccfc11 commit 36025f0
Show file tree
Hide file tree
Showing 11 changed files with 247 additions and 82 deletions.
5 changes: 5 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 0.24.0

* Add `DataType::LongVarchar`.
* `BufferKind::from_data_type` now suggests a TextBuffer for columns with data type `LongVarchar`.

## 0.23.2

* Adds `Connection::columns`
Expand Down
3 changes: 2 additions & 1 deletion odbc-api/src/buffers/description.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,8 @@ impl BufferKind {
| DataType::WVarchar { length }
// Currently no special buffers for fixed lengths text implemented.
| DataType::WChar {length }
| DataType::Char { length } => BufferKind::Text { max_str_len : length },
| DataType::Char { length }
| DataType::LongVarchar { length } => BufferKind::Text { max_str_len : length },
// Specialized buffers for Numeric and decimal are not yet supported.
| DataType::Numeric { precision: _, scale: _ }
| DataType::Decimal { precision: _, scale: _ }
Expand Down
13 changes: 11 additions & 2 deletions odbc-api/src/handles/data_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ pub enum DataType {
Varbinary { length: usize },
/// `BINARY(n)`. Type for fixed sized binary data.
Binary { length: usize },
/// `text`. Type for long text.
LongVarchar { length: usize },
/// The driver returned a type, but it is not among the other types of these enumeration. This
/// is a catchall, in case the library is incomplete, or the data source supports custom or
/// non-standard types.
Expand Down Expand Up @@ -150,6 +152,9 @@ impl DataType {
SqlDataType::EXT_W_CHAR => DataType::WChar {
length: column_size,
},
SqlDataType::EXT_LONG_VARCHAR => DataType::LongVarchar {
length: column_size
},
other => DataType::Other {
data_type: other,
column_size,
Expand Down Expand Up @@ -181,6 +186,7 @@ impl DataType {
DataType::Bit => SqlDataType::EXT_BIT,
DataType::WVarchar { .. } => SqlDataType::EXT_W_VARCHAR,
DataType::WChar { .. } => SqlDataType::EXT_W_CHAR,
DataType::LongVarchar { .. } => SqlDataType::EXT_LONG_VARCHAR,
DataType::Other { data_type, .. } => *data_type,
}
}
Expand All @@ -206,7 +212,8 @@ impl DataType {
| DataType::Varbinary { length }
| DataType::Binary { length }
| DataType::WChar { length }
| DataType::WVarchar { length } => *length,
| DataType::WVarchar { length }
| DataType::LongVarchar { length }=> *length,
DataType::Numeric { precision, .. } | DataType::Decimal { precision, .. } => *precision,
DataType::Other { column_size, .. } => *column_size,
}
Expand All @@ -225,6 +232,7 @@ impl DataType {
| DataType::Varchar { .. }
| DataType::WVarchar { .. }
| DataType::WChar { .. }
| DataType::LongVarchar { .. }
| DataType::Varbinary { .. }
| DataType::Binary { .. }
| DataType::Date
Expand Down Expand Up @@ -255,7 +263,8 @@ impl DataType {
DataType::Varchar { length }
| DataType::WVarchar { length }
| DataType::WChar { length }
| DataType::Char { length } => Some(*length),
| DataType::Char { length }
| DataType::LongVarchar { length }=> Some(*length),
// The precision of the column plus 2 (a sign, precision digits, and a decimal point).
// For example, the display size of a column defined as NUMERIC(10,3) is 12.
DataType::Numeric {
Expand Down
6 changes: 4 additions & 2 deletions odbc-api/src/handles/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ impl Environment {
SqlReturn::SUCCESS | SqlReturn::SUCCESS_WITH_INFO => Ok(()),
other => panic!(
"Unexpected Return value ('{:?}') for SQLSetEnvAttr then trying to set connection \
pooling to {:?}", other, scheme
pooling to {:?}",
other, scheme
),
}
}
Expand All @@ -92,7 +93,8 @@ impl Environment {
SqlReturn::SUCCESS | SqlReturn::SUCCESS_WITH_INFO => Ok(()),
other => panic!(
"Unexpected Return value ('{:?}') for SQLSetEnvAttr then trying to set \
connection pooling maching to {:?}", other, matching
connection pooling maching to {:?}",
other, matching
),
}
}
Expand Down
8 changes: 5 additions & 3 deletions odbc-api/src/handles/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ pub enum Error {
/// A user dialog to complete the connection string has been aborted.
#[error("The dialog shown to provide or complete the connection string has been aborted.")]
AbortedConnectionStringCompletion,
#[error("ODBC diver manager does not seem to support the required ODBC version 3.80. (Most
likely you need to update unixODBC if you run on a Linux. Diagnostic record returned by
SQLSetEnvAttr:\n{0}")]
#[error(
"ODBC diver manager does not seem to support the required ODBC version 3.80. (Most likely
you need to update unixODBC if you run on a Linux. Diagnostic record returned by
SQLSetEnvAttr:\n{0}"
)]
OdbcApiVersionUnsupported(DiagnosticRecord),
}

Expand Down
69 changes: 54 additions & 15 deletions odbc-api/src/handles/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,7 @@ use super::{
drop_handle,
error::{Error, IntoResult},
};
use odbc_sys::{
Desc, FreeStmtOption, HDbc, HStmt, Handle, HandleType, Len, ParamType, Pointer, SQLBindCol,
SQLBindParameter, SQLCloseCursor, SQLColAttributeW, SQLColumnsW, SQLDescribeColW,
SQLDescribeParam, SQLExecDirectW, SQLExecute, SQLFetch, SQLFreeStmt, SQLGetData,
SQLNumResultCols, SQLParamData, SQLPrepareW, SQLPutData, SQLSetStmtAttrW, SqlDataType,
SqlReturn, StatementAttribute, ULen,
};
use odbc_sys::{CDataType, Desc, FreeStmtOption, HDbc, HStmt, Handle, HandleType, Len, ParamType, Pointer, SQLBindCol, SQLBindParameter, SQLCloseCursor, SQLColAttributeW, SQLColumnsW, SQLDescribeColW, SQLDescribeParam, SQLExecDirectW, SQLExecute, SQLFetch, SQLFreeStmt, SQLGetData, SQLNumResultCols, SQLParamData, SQLPrepareW, SQLPutData, SQLSetStmtAttrW, SqlDataType, SqlReturn, StatementAttribute, ULen};
use std::{convert::TryInto, ffi::c_void, marker::PhantomData, mem::ManuallyDrop, ptr::null_mut};
use widestring::U16Str;

Expand Down Expand Up @@ -337,6 +331,24 @@ pub trait Statement {
///
/// Panics if batch is empty.
fn put_binary_batch(&mut self, batch: &[u8]) -> Result<bool, Error>;

/// Binds an input parameter whose data is send to the data source during statement execution
/// time. This is typically used to send long data to the data source. For other input
/// parameters see [`Statement::bind_input_parameter`].
///
/// See <https://docs.microsoft.com/en-us/sql/odbc/reference/syntax/sqlbindparameter-function>.
///
/// # Safety
///
/// * It is up to the caller to ensure the lifetimes of the bound parameters.
/// * Calling this function may influence other statements that share the APD.
unsafe fn bind_input_blob(
&mut self,
parameter_number: u16,
stream_id: *mut c_void,
parameter_type: DataType,
indicator: &isize,
) -> Result<(), Error>;
}

impl<'o> Statement for StatementImpl<'o> {
Expand Down Expand Up @@ -612,6 +624,38 @@ impl<'o> Statement for StatementImpl<'o> {
.into_result(self)
}

/// Binds an input parameter whose data is send to the data source during statement execution
/// time. This is typically used to send long data to the data source. For other input
/// parameters see [`Statement::bind_input_parameter`].
///
/// See <https://docs.microsoft.com/en-us/sql/odbc/reference/syntax/sqlbindparameter-function>.
///
/// # Safety
///
/// * It is up to the caller to ensure the lifetimes of the bound parameters.
/// * Calling this function may influence other statements that share the APD.
unsafe fn bind_input_blob(
&mut self,
parameter_number: u16,
stream_id: *mut c_void,
parameter_type: DataType,
indicator: &isize,
) -> Result<(), Error> {
SQLBindParameter(
self.handle,
parameter_number,
ParamType::Input,
CDataType::Char,
parameter_type.data_type(),
parameter_type.column_size(),
parameter_type.decimal_digits(),
stream_id,
0,
indicator as * const isize as * mut isize,
)
.into_result(self)
}

/// `true` if a given column in a result set is unsigned or not a numeric type, `false`
/// otherwise.
///
Expand Down Expand Up @@ -661,6 +705,9 @@ impl<'o> Statement for StatementImpl<'o> {
SqlDataType::EXT_W_CHAR => DataType::WChar {
length: self.col_display_size(column_number)?.try_into().unwrap(),
},
SqlDataType::EXT_LONG_VARCHAR => DataType::LongVarchar {
length: self.col_display_size(column_number)?.try_into().unwrap(),
},
SqlDataType::CHAR => DataType::Char {
length: self.col_display_size(column_number)?.try_into().unwrap(),
},
Expand Down Expand Up @@ -890,7 +937,6 @@ impl<'o> Statement for StatementImpl<'o> {
///
/// Panics if batch is empty.
fn put_binary_batch(&mut self, batch: &[u8]) -> Result<bool, Error> {

// Probably not strictly necessary. MSSQL returns an error than inserting empty batches.
// Still strikes me as a programming error. Maybe we could also do nothing instead.
if batch.is_empty() {
Expand Down Expand Up @@ -920,10 +966,3 @@ pub struct ParameterDescription {
/// The SQL Type associated with that parameter.
pub data_type: DataType,
}

/// Can stream a sequence of binary batches. Use this to put large data.
pub trait BlobInputStream {
/// Fetches the next batch from the stream. The batch may not be valid once the next call to
/// `next` (as enforced by the signature).
fn next(&mut self) -> Option<&[u8]>;
}
2 changes: 2 additions & 0 deletions odbc-api/src/parameter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,11 +223,13 @@
//! work? Well, in that case please open an issue or a pull request. [`crate::IntoParameter`] can usually be
//! implemented entirely in safe code, and is a suitable spot to enable support for your custom
//! types.
mod blob;
mod c_string;
mod varbin;
mod varchar;

pub use self::{
blob::{BlobInputParameter, BlobInputStream},
varbin::{VarBinary, VarBinaryArray, VarBinaryBox, VarBinarySlice, VarBinarySliceMut},
varchar::{VarChar, VarCharArray, VarCharBox, VarCharSlice, VarCharSliceMut},
};
Expand Down
64 changes: 64 additions & 0 deletions odbc-api/src/parameter/blob.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use std::convert::TryInto;

use crate::{handles::Statement, DataType, Parameter};

/// Can stream a sequence of binary batches. Use this to put large data.
///
/// # Safety
///
/// Should a size hint be implemented (i.e. `len` returns `Some` value), then it must be correct.
pub unsafe trait BlobInputStream {
/// Fetches the next batch from the stream. The batch may not be valid once the next call to
/// `next` (as enforced by the signature).
fn next(&mut self) -> Option<&[u8]>;

/// Total length of all batches combined. If available this information will be send to the
/// driver before the first call to next. It depends on the driver wether or not something
/// clever happens with this.
fn len(&self) -> Option<usize>;

/// SQL Parameter Data Type as which the stream is going to be bound. E.g.
/// [`DataType::LongVarchar`] for narrow text data.
fn data_type(&self) -> DataType;
}

/// Can bind stream input data at statement execution time. Useful for sending long data to the
/// connected data source.
pub struct BlobInputParameter<B> {
stream: B,
indicator: isize,
}

impl<B> BlobInputParameter<B>
where
B: BlobInputStream,
{
/// Create a new blob input parameter from a blob stream.
pub fn new(stream: B) -> Self {
let indicator = if let Some(len) = stream.len() {
odbc_sys::len_data_at_exec(len.try_into().unwrap())
} else {
odbc_sys::DATA_AT_EXEC
};

Self { stream, indicator }
}
}

unsafe impl<B> Parameter for &mut BlobInputParameter<B>
where
B: BlobInputStream,
{
unsafe fn bind_parameter(
self,
parameter_number: u16,
stmt: &mut crate::handles::StatementImpl<'_>,
) -> Result<(), crate::Error> {
stmt.bind_input_blob(
parameter_number,
parameter_number as odbc_sys::Pointer,
self.stream.data_type(),
&self.indicator,
)
}
}
10 changes: 7 additions & 3 deletions odbc-api/tests/common.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
use lazy_static::lazy_static;
use odbc_api::{Connection, Cursor, Environment, Error, RowSetBuffer, U16Str, buffers, buffers::TextColumn, handles::{CDataMut, Statement}};
use odbc_api::{
buffers,
buffers::TextColumn,
handles::{CDataMut, Statement},
Connection, Cursor, Environment, Error, RowSetBuffer, U16Str,
};

// Rust by default executes tests in parallel. Yet only one environment is allowed at a time.
lazy_static! {
Expand All @@ -20,7 +25,6 @@ pub struct Profile {
}

impl Profile {

/// Open a new connection using the connection string of the profile
pub fn connection(&self) -> Result<Connection<'static>, Error> {
ENV.connect_with_connection_string(self.connection_string)
Expand All @@ -36,7 +40,7 @@ pub fn setup_empty_table(
) -> Result<(), odbc_api::Error> {
let drop_table = &format!("DROP TABLE IF EXISTS {}", table_name);

let column_names = &["a", "b", "c", "d", "e", "f", "g", "h"];
let column_names = &["a", "b", "c", "d", "e", "f", "g", "h", "i"];
let cols = column_types
.iter()
.zip(column_names)
Expand Down
71 changes: 38 additions & 33 deletions odbc-api/tests/connection_pooling.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,38 @@
//! Since connection pooling mode is a process level attribute these tests have to run in their own
//! process.

use lazy_static::lazy_static;
use odbc_api::Environment;
use odbc_sys::{AttrConnectionPooling, AttrCpMatch};

const MSSQL_CONNECTION: &str =
"Driver={ODBC Driver 17 for SQL Server};Server=localhost;UID=SA;PWD=<YourStrong@Passw0rd>;";

// Rust by default executes tests in parallel. Yet only one environment is allowed at a time.
lazy_static! {
pub static ref ENV: Environment = unsafe {
let _ = env_logger::builder().is_test(true).try_init();
Environment::set_connection_pooling(AttrConnectionPooling::DriverAware).unwrap();
let mut env = Environment::new().unwrap();
env.set_connection_pooling_matching(AttrCpMatch::Strict).unwrap();
env
};
}

#[test]
fn connect() {
// First connection should be created on demand
{
let conn = ENV.connect_with_connection_string(MSSQL_CONNECTION).unwrap();
assert!(!conn.is_dead().unwrap());
}

// Second connection should be from the pool
let conn = ENV.connect_with_connection_string(MSSQL_CONNECTION).unwrap();
assert!(!conn.is_dead().unwrap());
}
//! Since connection pooling mode is a process level attribute these tests have to run in their own
//! process.

use lazy_static::lazy_static;
use odbc_api::Environment;
use odbc_sys::{AttrConnectionPooling, AttrCpMatch};

const MSSQL_CONNECTION: &str =
"Driver={ODBC Driver 17 for SQL Server};Server=localhost;UID=SA;PWD=<YourStrong@Passw0rd>;";

// Rust by default executes tests in parallel. Yet only one environment is allowed at a time.
lazy_static! {
pub static ref ENV: Environment = unsafe {
let _ = env_logger::builder().is_test(true).try_init();
Environment::set_connection_pooling(AttrConnectionPooling::DriverAware).unwrap();
let mut env = Environment::new().unwrap();
env.set_connection_pooling_matching(AttrCpMatch::Strict)
.unwrap();
env
};
}

#[test]
fn connect() {
// First connection should be created on demand
{
let conn = ENV
.connect_with_connection_string(MSSQL_CONNECTION)
.unwrap();
assert!(!conn.is_dead().unwrap());
}

// Second connection should be from the pool
let conn = ENV
.connect_with_connection_string(MSSQL_CONNECTION)
.unwrap();
assert!(!conn.is_dead().unwrap());
}
Loading

0 comments on commit 36025f0

Please sign in to comment.