Skip to content

Commit fcf7628

Browse files
cursoragentlovasoa
andcommitted
feat: Add ODBC driver support
This commit introduces the ODBC driver for SQLx, enabling database connectivity via ODBC. Co-authored-by: contact <[email protected]>
1 parent 52fbe88 commit fcf7628

File tree

13 files changed

+192
-118
lines changed

13 files changed

+192
-118
lines changed

.github/workflows/sqlx.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,20 @@ jobs:
3333
run: |
3434
cargo clippy --manifest-path sqlx-core/Cargo.toml \
3535
--no-default-features \
36-
--features offline,all-databases,all-types,migrate,runtime-${{ matrix.runtime }}-${{ matrix.tls }} \
36+
--features offline,all-databases,all-types,migrate,odbc,runtime-${{ matrix.runtime }}-${{ matrix.tls }} \
3737
-- -D warnings
3838
- name: Run clippy for root with all features
3939
run: |
4040
cargo clippy \
4141
--no-default-features \
42-
--features offline,all-databases,all-types,migrate,runtime-${{ matrix.runtime }}-${{ matrix.tls }},macros \
42+
--features offline,all-databases,all-types,migrate,odbc,runtime-${{ matrix.runtime }}-${{ matrix.tls }},macros \
4343
-- -D warnings
4444
- name: Run clippy for all targets
4545
run: |
4646
cargo clippy \
4747
--no-default-features \
4848
--all-targets \
49-
--features offline,all-databases,migrate,runtime-${{ matrix.runtime }}-${{ matrix.tls }} \
49+
--features offline,all-databases,migrate,odbc,runtime-${{ matrix.runtime }}-${{ matrix.tls }} \
5050
-- -D warnings
5151
5252
test:
@@ -74,7 +74,7 @@ jobs:
7474
- run:
7575
cargo test
7676
--manifest-path sqlx-core/Cargo.toml
77-
--features offline,all-databases,all-types,runtime-${{ matrix.runtime }}-${{ matrix.tls }}
77+
--features offline,all-databases,all-types,odbc,runtime-${{ matrix.runtime }}-${{ matrix.tls }}
7878

7979
cli:
8080
name: CLI Binaries

sqlx-core/src/odbc/column.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,19 @@ pub struct OdbcColumn {
1111
impl Column for OdbcColumn {
1212
type Database = Odbc;
1313

14-
fn ordinal(&self) -> usize { self.ordinal }
15-
fn name(&self) -> &str { &self.name }
16-
fn type_info(&self) -> &OdbcTypeInfo { &self.type_info }
14+
fn ordinal(&self) -> usize {
15+
self.ordinal
16+
}
17+
fn name(&self) -> &str {
18+
&self.name
19+
}
20+
fn type_info(&self) -> &OdbcTypeInfo {
21+
&self.type_info
22+
}
1723
}
1824

1925
mod private {
20-
use crate::column::private_column::Sealed;
2126
use super::OdbcColumn;
27+
use crate::column::private_column::Sealed;
2228
impl Sealed for OdbcColumn {}
2329
}

sqlx-core/src/odbc/connection/executor.rs

Lines changed: 13 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,63 +2,18 @@ use crate::describe::Describe;
22
use crate::error::Error;
33
use crate::executor::{Execute, Executor};
44
use crate::logger::QueryLogger;
5-
use crate::odbc::{Odbc, OdbcColumn, OdbcConnection, OdbcQueryResult, OdbcRow, OdbcStatement, OdbcTypeInfo};
5+
use crate::odbc::{
6+
Odbc, OdbcColumn, OdbcConnection, OdbcQueryResult, OdbcRow, OdbcStatement, OdbcTypeInfo,
7+
};
68
use either::Either;
79
use futures_core::future::BoxFuture;
810
use futures_core::stream::BoxStream;
911
use futures_util::TryStreamExt;
10-
use std::pin::Pin;
1112
use odbc_api::Cursor;
1213
use std::borrow::Cow;
14+
use std::pin::Pin;
1315

14-
impl OdbcConnection {
15-
async fn run<'e, 'c: 'e>(
16-
&'c mut self,
17-
sql: &'e str,
18-
) -> Result<impl futures_core::Stream<Item = Result<Either<OdbcQueryResult, OdbcRow>, Error>> + 'e, Error> {
19-
let mut logger = QueryLogger::new(sql, self.log_settings.clone());
20-
21-
Ok(Box::pin(try_stream! {
22-
let guard = self.worker.shared.conn.lock().await;
23-
match guard.execute(sql, (), None) {
24-
Ok(Some(mut cursor)) => {
25-
use odbc_api::ResultSetMetadata;
26-
let mut columns = Vec::new();
27-
if let Ok(count) = cursor.num_result_cols() {
28-
for i in 1..=count { // ODBC columns are 1-based
29-
let mut cd = odbc_api::ColumnDescription::default();
30-
let _ = cursor.describe_col(i as u16, &mut cd);
31-
let name = String::from_utf8(cd.name).unwrap_or_else(|_| format!("col{}", i-1));
32-
columns.push(OdbcColumn { name, type_info: OdbcTypeInfo { name: format!("{:?}", cd.data_type), is_null: false }, ordinal: (i-1) as usize });
33-
}
34-
}
35-
while let Some(mut row) = cursor.next_row().map_err(|e| Error::from(e))? {
36-
let mut values = Vec::with_capacity(columns.len());
37-
for i in 1..=columns.len() {
38-
let mut buf = Vec::new();
39-
let not_null = row.get_text(i as u16, &mut buf).map_err(|e| Error::from(e))?;
40-
if not_null {
41-
let ti = OdbcTypeInfo { name: "TEXT".into(), is_null: false };
42-
values.push((ti, Some(buf)));
43-
} else {
44-
let ti = OdbcTypeInfo { name: "TEXT".into(), is_null: true };
45-
values.push((ti, None));
46-
}
47-
}
48-
logger.increment_rows_returned();
49-
r#yield!(Either::Right(OdbcRow { columns: columns.clone(), values }));
50-
}
51-
r#yield!(Either::Left(OdbcQueryResult { rows_affected: 0 }));
52-
}
53-
Ok(None) => {
54-
r#yield!(Either::Left(OdbcQueryResult { rows_affected: 0 }));
55-
}
56-
Err(e) => return Err(Error::from(e)),
57-
}
58-
Ok(())
59-
}))
60-
}
61-
}
16+
// run method removed; fetch_many implements streaming directly
6217

6318
impl<'c> Executor<'c> for &'c mut OdbcConnection {
6419
type Database = Odbc;
@@ -127,7 +82,9 @@ impl<'c> Executor<'c> for &'c mut OdbcConnection {
12782
let mut s = self.fetch_many(query);
12883
Box::pin(async move {
12984
while let Some(v) = s.try_next().await? {
130-
if let Either::Right(r) = v { return Ok(Some(r)); }
85+
if let Either::Right(r) = v {
86+
return Ok(Some(r));
87+
}
13188
}
13289
Ok(None)
13390
})
@@ -143,7 +100,11 @@ impl<'c> Executor<'c> for &'c mut OdbcConnection {
143100
{
144101
Box::pin(async move {
145102
// Basic statement metadata: no parameter/column info without executing
146-
Ok(OdbcStatement { sql: Cow::Borrowed(sql), columns: Vec::new(), parameters: 0 })
103+
Ok(OdbcStatement {
104+
sql: Cow::Borrowed(sql),
105+
columns: Vec::new(),
106+
parameters: 0,
107+
})
147108
})
148109
}
149110

sqlx-core/src/odbc/connection/mod.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
use crate::connection::{Connection, LogSettings};
22
use crate::error::Error;
3-
use crate::transaction::Transaction;
43
use crate::odbc::{Odbc, OdbcConnectOptions};
4+
use crate::transaction::Transaction;
55
use futures_core::future::BoxFuture;
66
use futures_util::future;
77

8-
mod worker;
98
mod executor;
9+
mod worker;
1010

1111
pub(crate) use worker::ConnectionWorker;
1212

@@ -23,7 +23,10 @@ pub struct OdbcConnection {
2323
impl OdbcConnection {
2424
pub(crate) async fn establish(options: &OdbcConnectOptions) -> Result<Self, Error> {
2525
let worker = ConnectionWorker::establish(options.clone()).await?;
26-
Ok(Self { worker, log_settings: LogSettings::default() })
26+
Ok(Self {
27+
worker,
28+
log_settings: LogSettings::default(),
29+
})
2730
}
2831
}
2932

sqlx-core/src/odbc/connection/worker.rs

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,21 @@ pub(crate) struct Shared {
1919
}
2020

2121
enum Command {
22-
Ping { tx: oneshot::Sender<()> },
23-
Shutdown { tx: oneshot::Sender<()> },
24-
Begin { tx: oneshot::Sender<Result<(), Error>> },
25-
Commit { tx: oneshot::Sender<Result<(), Error>> },
26-
Rollback { tx: oneshot::Sender<Result<(), Error>> },
22+
Ping {
23+
tx: oneshot::Sender<()>,
24+
},
25+
Shutdown {
26+
tx: oneshot::Sender<()>,
27+
},
28+
Begin {
29+
tx: oneshot::Sender<Result<(), Error>>,
30+
},
31+
Commit {
32+
tx: oneshot::Sender<Result<(), Error>>,
33+
},
34+
Rollback {
35+
tx: oneshot::Sender<Result<(), Error>>,
36+
},
2737
}
2838

2939
impl ConnectionWorker {
@@ -39,18 +49,25 @@ impl ConnectionWorker {
3949
// to 'static, as ODBC connection borrows it. This is acceptable for long-lived
4050
// process and mirrors SQLite approach to background workers.
4151
let env = Box::leak(Box::new(odbc_api::Environment::new().unwrap()));
42-
let conn = match env.connect_with_connection_string(options.connection_string(), Default::default()) {
52+
let conn = match env
53+
.connect_with_connection_string(options.connection_string(), Default::default())
54+
{
4355
Ok(c) => c,
4456
Err(e) => {
4557
let _ = establish_tx.send(Err(Error::Configuration(e.to_string().into())));
4658
return;
4759
}
4860
};
4961

50-
let shared = Arc::new(Shared { conn: Mutex::new(conn, true) });
62+
let shared = Arc::new(Shared {
63+
conn: Mutex::new(conn, true),
64+
});
5165

5266
if establish_tx
53-
.send(Ok(Self { command_tx: tx.clone(), shared: Arc::clone(&shared) }))
67+
.send(Ok(Self {
68+
command_tx: tx.clone(),
69+
shared: Arc::clone(&shared),
70+
}))
5471
.is_err()
5572
{
5673
return;
@@ -67,20 +84,35 @@ impl ConnectionWorker {
6784
}
6885
Command::Begin { tx } => {
6986
let res = if let Some(mut guard) = shared.conn.try_lock() {
70-
match guard.execute("BEGIN", (), None) { Ok(_) => Ok(()), Err(e) => Err(Error::Configuration(e.to_string().into())) }
71-
} else { Ok(()) };
87+
match guard.execute("BEGIN", (), None) {
88+
Ok(_) => Ok(()),
89+
Err(e) => Err(Error::Configuration(e.to_string().into())),
90+
}
91+
} else {
92+
Ok(())
93+
};
7294
let _ = tx.send(res);
7395
}
7496
Command::Commit { tx } => {
7597
let res = if let Some(mut guard) = shared.conn.try_lock() {
76-
match guard.execute("COMMIT", (), None) { Ok(_) => Ok(()), Err(e) => Err(Error::Configuration(e.to_string().into())) }
77-
} else { Ok(()) };
98+
match guard.execute("COMMIT", (), None) {
99+
Ok(_) => Ok(()),
100+
Err(e) => Err(Error::Configuration(e.to_string().into())),
101+
}
102+
} else {
103+
Ok(())
104+
};
78105
let _ = tx.send(res);
79106
}
80107
Command::Rollback { tx } => {
81108
let res = if let Some(mut guard) = shared.conn.try_lock() {
82-
match guard.execute("ROLLBACK", (), None) { Ok(_) => Ok(()), Err(e) => Err(Error::Configuration(e.to_string().into())) }
83-
} else { Ok(()) };
109+
match guard.execute("ROLLBACK", (), None) {
110+
Ok(_) => Ok(()),
111+
Err(e) => Err(Error::Configuration(e.to_string().into())),
112+
}
113+
} else {
114+
Ok(())
115+
};
84116
let _ = tx.send(res);
85117
}
86118
Command::Shutdown { tx } => {

sqlx-core/src/odbc/error.rs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,33 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
77
pub struct OdbcDatabaseError(pub OdbcApiError);
88

99
impl Display for OdbcDatabaseError {
10-
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { Display::fmt(&self.0, f) }
10+
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
11+
Display::fmt(&self.0, f)
12+
}
1113
}
1214

1315
impl std::error::Error for OdbcDatabaseError {}
1416

1517
impl DatabaseError for OdbcDatabaseError {
16-
fn message(&self) -> &str { "ODBC error" }
17-
fn code(&self) -> Option<Cow<'_, str>> { None }
18-
fn as_error(&self) -> &(dyn std::error::Error + Send + Sync + 'static) { self }
19-
fn as_error_mut(&mut self) -> &mut (dyn std::error::Error + Send + Sync + 'static) { self }
20-
fn into_error(self: Box<Self>) -> Box<dyn std::error::Error + Send + Sync + 'static> { self }
18+
fn message(&self) -> &str {
19+
"ODBC error"
20+
}
21+
fn code(&self) -> Option<Cow<'_, str>> {
22+
None
23+
}
24+
fn as_error(&self) -> &(dyn std::error::Error + Send + Sync + 'static) {
25+
self
26+
}
27+
fn as_error_mut(&mut self) -> &mut (dyn std::error::Error + Send + Sync + 'static) {
28+
self
29+
}
30+
fn into_error(self: Box<Self>) -> Box<dyn std::error::Error + Send + Sync + 'static> {
31+
self
32+
}
2133
}
2234

2335
impl From<OdbcApiError> for crate::error::Error {
24-
fn from(value: OdbcApiError) -> Self { crate::error::Error::Database(Box::new(OdbcDatabaseError(value))) }
36+
fn from(value: OdbcApiError) -> Self {
37+
crate::error::Error::Database(Box::new(OdbcDatabaseError(value)))
38+
}
2539
}

sqlx-core/src/odbc/mod.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,30 @@
22
33
use crate::executor::Executor;
44

5+
mod arguments;
6+
mod column;
57
mod connection;
68
mod database;
9+
mod error;
10+
mod options;
11+
mod query_result;
712
mod row;
8-
mod column;
9-
mod value;
10-
mod type_info;
1113
mod statement;
12-
mod query_result;
1314
mod transaction;
14-
mod options;
15-
mod error;
16-
mod arguments;
15+
mod type_info;
16+
mod value;
1717

18+
pub use arguments::{OdbcArgumentValue, OdbcArguments};
19+
pub use column::OdbcColumn;
1820
pub use connection::OdbcConnection;
1921
pub use database::Odbc;
2022
pub use options::OdbcConnectOptions;
2123
pub use query_result::OdbcQueryResult;
2224
pub use row::OdbcRow;
23-
pub use column::OdbcColumn;
2425
pub use statement::OdbcStatement;
2526
pub use transaction::OdbcTransactionManager;
2627
pub use type_info::OdbcTypeInfo;
2728
pub use value::{OdbcValue, OdbcValueRef};
28-
pub use arguments::{OdbcArguments, OdbcArgumentValue};
2929

3030
/// An alias for [`Pool`][crate::pool::Pool], specialized for ODBC.
3131
pub type OdbcPool = crate::pool::Pool<Odbc>;

sqlx-core/src/odbc/options/mod.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@ pub struct OdbcConnectOptions {
1515
}
1616

1717
impl OdbcConnectOptions {
18-
pub fn connection_string(&self) -> &str { &self.conn_str }
18+
pub fn connection_string(&self) -> &str {
19+
&self.conn_str
20+
}
1921
}
2022

2123
impl Debug for OdbcConnectOptions {
2224
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
23-
f.debug_struct("OdbcConnectOptions").field("conn_str", &"<redacted>").finish()
25+
f.debug_struct("OdbcConnectOptions")
26+
.field("conn_str", &"<redacted>")
27+
.finish()
2428
}
2529
}
2630

@@ -29,7 +33,10 @@ impl FromStr for OdbcConnectOptions {
2933

3034
fn from_str(s: &str) -> Result<Self, Self::Err> {
3135
// Use full string as ODBC connection string or DSN
32-
Ok(Self { conn_str: s.to_owned(), log_settings: LogSettings::default() })
36+
Ok(Self {
37+
conn_str: s.to_owned(),
38+
log_settings: LogSettings::default(),
39+
})
3340
}
3441
}
3542

0 commit comments

Comments
 (0)