Skip to content

Commit f4badce

Browse files
committed
Fix ODBC prepare metadata caching
1 parent bb53068 commit f4badce

2 files changed

Lines changed: 122 additions & 19 deletions

File tree

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

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ use crate::odbc::{OdbcStatement, OdbcStatementMetadata};
1313
use futures_core::future::BoxFuture;
1414
use futures_util::future;
1515
use odbc_api::ConnectionTransitions;
16-
use odbc_api::{handles::StatementConnection, Prepared, ResultSetMetadata, SharedConnection};
16+
use odbc_api::Error as OdbcApiError;
17+
use odbc_api::{
18+
handles::{slice_to_cow_utf8, StatementConnection},
19+
Prepared, ResultSetMetadata, SharedConnection,
20+
};
1721
use odbc_bridge::{establish_connection, execute_sql};
1822
use std::borrow::Cow;
1923
use std::sync::{Arc, Mutex};
@@ -23,20 +27,28 @@ mod executor;
2327
type PreparedStatement = Prepared<StatementConnection<SharedConnection<'static>>>;
2428
type SharedPreparedStatement = Arc<Mutex<PreparedStatement>>;
2529

30+
struct CollectedColumns {
31+
columns: Vec<OdbcColumn>,
32+
deferred: bool,
33+
}
34+
2635
fn collect_columns(
2736
prepared: &mut PreparedStatement,
2837
parameter_count: usize,
2938
allow_deferred_result_columns: bool,
30-
) -> Result<Vec<OdbcColumn>, Error> {
39+
) -> Result<CollectedColumns, Error> {
3140
let count = match prepared.num_result_cols() {
3241
Ok(count) => count,
33-
Err(error) if allow_deferred_result_columns && parameter_count > 0 => {
34-
// Some ODBC drivers only expose result columns for parameterized
35-
// statements after values are bound and the statement is executed.
36-
// Row execution still gets authoritative metadata from the cursor;
37-
// describe() uses the strict path and will surface this error.
38-
log::debug!("ODBC prepare did not expose result columns: {error}");
39-
return Ok(Vec::new());
42+
Err(error)
43+
if allow_deferred_result_columns
44+
&& parameter_count > 0
45+
&& is_unbound_parameter_metadata_error(&error) =>
46+
{
47+
log::debug!("ODBC prepare deferred result columns until execution: {error}");
48+
return Ok(CollectedColumns {
49+
columns: Vec::new(),
50+
deferred: true,
51+
});
4052
}
4153
Err(error) => return Err(error.into()),
4254
};
@@ -45,19 +57,38 @@ fn collect_columns(
4557
for i in 1..=count {
4658
columns.push(describe_column(prepared, i as u16)?);
4759
}
48-
Ok(columns)
60+
Ok(CollectedColumns {
61+
columns,
62+
deferred: false,
63+
})
4964
}
5065

5166
fn collect_statement_metadata(
5267
prepared: &mut PreparedStatement,
5368
allow_deferred_result_columns: bool,
54-
) -> Result<OdbcStatementMetadata, Error> {
69+
) -> Result<(OdbcStatementMetadata, bool), Error> {
5570
let parameters = usize::from(prepared.num_params()?);
71+
let collected = collect_columns(prepared, parameters, allow_deferred_result_columns)?;
72+
let metadata_complete =
73+
!collected.deferred && !(parameters > 0 && collected.columns.is_empty());
74+
75+
Ok((
76+
OdbcStatementMetadata {
77+
columns: collected.columns,
78+
parameters,
79+
},
80+
metadata_complete,
81+
))
82+
}
5683

57-
Ok(OdbcStatementMetadata {
58-
columns: collect_columns(prepared, parameters, allow_deferred_result_columns)?,
59-
parameters,
60-
})
84+
fn is_unbound_parameter_metadata_error(error: &OdbcApiError) -> bool {
85+
match error {
86+
OdbcApiError::Diagnostics { record, .. } if record.state.as_str() == "01000" => {
87+
let message = slice_to_cow_utf8(&record.message).to_ascii_lowercase();
88+
message.contains("parameter") && message.contains("bound")
89+
}
90+
_ => false,
91+
}
6192
}
6293

6394
pub(super) fn describe_column<S>(stmt: &mut S, index: u16) -> Result<OdbcColumn, Error>
@@ -241,6 +272,7 @@ impl OdbcConnection {
241272
store_to_cache: bool,
242273
allow_deferred_result_columns: bool,
243274
) -> Result<OdbcStatement<'a>, Error> {
275+
let sql_owned = sql.to_string();
244276
let cached = self
245277
.stmt_cache
246278
.get_mut(sql)
@@ -252,6 +284,7 @@ impl OdbcConnection {
252284
Error::Protocol("ODBC prepare: failed to lock prepared statement".into())
253285
})?;
254286
collect_statement_metadata(&mut prepared, allow_deferred_result_columns)
287+
.map(|(metadata, _)| metadata)
255288
})
256289
.await?;
257290

@@ -262,17 +295,16 @@ impl OdbcConnection {
262295
}
263296

264297
let conn = Arc::clone(&self.conn);
265-
let sql_owned = sql.to_string();
266298
let sql_clone = sql_owned.clone();
267-
let (prepared, metadata) = spawn_blocking(move || {
299+
let (prepared, metadata, metadata_complete) = spawn_blocking(move || {
268300
let mut prepared = conn.into_prepared(&sql_clone)?;
269301
let metadata =
270302
collect_statement_metadata(&mut prepared, allow_deferred_result_columns)?;
271-
Ok::<_, Error>((prepared, metadata))
303+
Ok::<_, Error>((prepared, metadata.0, metadata.1))
272304
})
273305
.await?;
274306

275-
if store_to_cache && self.stmt_cache.is_enabled() {
307+
if store_to_cache && metadata_complete && self.stmt_cache.is_enabled() {
276308
self.stmt_cache
277309
.insert(&sql_owned, Arc::new(Mutex::new(prepared)));
278310
}

tests/odbc/odbc.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,77 @@ async fn describe_does_not_succeed_with_missing_columns() -> anyhow::Result<()>
333333
Ok(())
334334
}
335335

336+
const PARAMETERIZED_SELECT_WITH_COLUMN: &str = "SELECT ? AS value";
337+
const MISSING_TABLE_READ: &str = "SELECT contents from sqlx_missing_fs WHERE path = ?";
338+
const MISSING_TABLE_EXISTS: &str = "SELECT 1 from sqlx_missing_fs WHERE path = ?";
339+
const MISSING_TABLE_MODIFIED: &str =
340+
"SELECT 1 from sqlx_missing_fs WHERE last_modified >= ? AND path = ?";
341+
342+
#[tokio::test]
343+
async fn prepare_missing_table_does_not_return_empty_metadata() -> anyhow::Result<()> {
344+
let mut conn = new::<Odbc>().await?;
345+
346+
for sql in [
347+
MISSING_TABLE_READ,
348+
MISSING_TABLE_EXISTS,
349+
MISSING_TABLE_MODIFIED,
350+
] {
351+
if let Ok(statement) = conn.prepare(sql).await {
352+
assert!(
353+
!statement.columns().is_empty(),
354+
"ODBC prepare must not turn a metadata error into zero columns for {sql}"
355+
);
356+
}
357+
}
358+
359+
Ok(())
360+
}
361+
362+
#[tokio::test]
363+
async fn empty_metadata_prepare_is_not_cached() -> anyhow::Result<()> {
364+
let mut conn = new::<Odbc>().await?;
365+
366+
conn.clear_cached_statements().await?;
367+
assert_eq!(conn.cached_statements_size(), 0);
368+
369+
match conn.prepare(PARAMETERIZED_SELECT_WITH_COLUMN).await {
370+
Ok(statement) if statement.columns().is_empty() => assert_eq!(
371+
conn.cached_statements_size(),
372+
0,
373+
"ODBC prepare must not cache statements whose result metadata is deferred"
374+
),
375+
Ok(_) => {}
376+
Err(_) => assert_eq!(
377+
conn.cached_statements_size(),
378+
0,
379+
"ODBC prepare must not cache a statement after result-column metadata failed"
380+
),
381+
}
382+
383+
Ok(())
384+
}
385+
386+
#[tokio::test]
387+
async fn failed_metadata_prepare_does_not_poison_execute() -> anyhow::Result<()> {
388+
let mut conn = new::<Odbc>().await?;
389+
390+
let _ = conn.prepare(MISSING_TABLE_READ).await;
391+
392+
let error = sqlx_oldapi::query::<Odbc>(MISSING_TABLE_READ)
393+
.bind("index.sql")
394+
.fetch_optional(&mut conn)
395+
.await
396+
.expect_err("querying a missing table should fail");
397+
let message = format!("{error:#}");
398+
399+
assert!(
400+
message.contains("sqlx_missing_fs"),
401+
"failed ODBC prepare metadata poisoned a later execute instead of returning a normal missing-table error: {message}"
402+
);
403+
404+
Ok(())
405+
}
406+
336407
#[tokio::test]
337408
async fn it_can_bind_many_params_dynamically() -> anyhow::Result<()> {
338409
let mut conn = new::<Odbc>().await?;

0 commit comments

Comments
 (0)