Skip to content

Commit c32b40c

Browse files
committed
odbc: separate prepare from describe metadata
1 parent 7c76a8c commit c32b40c

3 files changed

Lines changed: 59 additions & 9 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ impl<'c> Executor<'c> for &'c mut OdbcConnection {
6060
'c: 'e,
6161
{
6262
Box::pin(async move {
63-
let statement = self.prepare(sql).await?;
63+
let statement = self.describe_statement(sql).await?;
6464
let nullable = vec![None; statement.metadata.columns.len()];
6565

6666
Ok(Describe {

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

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,24 @@ mod executor;
2323
type PreparedStatement = Prepared<StatementConnection<SharedConnection<'static>>>;
2424
type SharedPreparedStatement = Arc<Mutex<PreparedStatement>>;
2525

26-
fn collect_columns(prepared: &mut PreparedStatement) -> Result<Vec<OdbcColumn>, Error> {
27-
let count = prepared.num_result_cols()?;
26+
fn collect_columns(
27+
prepared: &mut PreparedStatement,
28+
parameter_count: usize,
29+
allow_deferred_result_columns: bool,
30+
) -> Result<Vec<OdbcColumn>, Error> {
31+
let count = match prepared.num_result_cols() {
32+
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());
40+
}
41+
Err(error) => return Err(error.into()),
42+
};
43+
2844
let mut columns = Vec::with_capacity(count as usize);
2945
for i in 1..=count {
3046
columns.push(describe_column(prepared, i as u16)?);
@@ -34,10 +50,13 @@ fn collect_columns(prepared: &mut PreparedStatement) -> Result<Vec<OdbcColumn>,
3450

3551
fn collect_statement_metadata(
3652
prepared: &mut PreparedStatement,
53+
allow_deferred_result_columns: bool,
3754
) -> Result<OdbcStatementMetadata, Error> {
55+
let parameters = usize::from(prepared.num_params()?);
56+
3857
Ok(OdbcStatementMetadata {
39-
columns: collect_columns(prepared)?,
40-
parameters: usize::from(prepared.num_params()?),
58+
columns: collect_columns(prepared, parameters, allow_deferred_result_columns)?,
59+
parameters,
4160
})
4261
}
4362

@@ -216,7 +235,12 @@ impl OdbcConnection {
216235
Ok(())
217236
}
218237

219-
pub async fn prepare<'a>(&mut self, sql: &'a str) -> Result<OdbcStatement<'a>, Error> {
238+
async fn prepare_with_metadata_policy<'a>(
239+
&mut self,
240+
sql: &'a str,
241+
store_to_cache: bool,
242+
allow_deferred_result_columns: bool,
243+
) -> Result<OdbcStatement<'a>, Error> {
220244
let cached = self
221245
.stmt_cache
222246
.get_mut(sql)
@@ -227,7 +251,7 @@ impl OdbcConnection {
227251
let mut prepared = prepared.lock().map_err(|_| {
228252
Error::Protocol("ODBC prepare: failed to lock prepared statement".into())
229253
})?;
230-
collect_statement_metadata(&mut prepared)
254+
collect_statement_metadata(&mut prepared, allow_deferred_result_columns)
231255
})
232256
.await?;
233257

@@ -242,12 +266,13 @@ impl OdbcConnection {
242266
let sql_clone = sql_owned.clone();
243267
let (prepared, metadata) = spawn_blocking(move || {
244268
let mut prepared = conn.into_prepared(&sql_clone)?;
245-
let metadata = collect_statement_metadata(&mut prepared)?;
269+
let metadata =
270+
collect_statement_metadata(&mut prepared, allow_deferred_result_columns)?;
246271
Ok::<_, Error>((prepared, metadata))
247272
})
248273
.await?;
249274

250-
if self.stmt_cache.is_enabled() {
275+
if store_to_cache && self.stmt_cache.is_enabled() {
251276
self.stmt_cache
252277
.insert(&sql_owned, Arc::new(Mutex::new(prepared)));
253278
}
@@ -257,6 +282,17 @@ impl OdbcConnection {
257282
metadata,
258283
})
259284
}
285+
286+
pub async fn prepare<'a>(&mut self, sql: &'a str) -> Result<OdbcStatement<'a>, Error> {
287+
self.prepare_with_metadata_policy(sql, true, true).await
288+
}
289+
290+
pub(crate) async fn describe_statement<'a>(
291+
&mut self,
292+
sql: &'a str,
293+
) -> Result<OdbcStatement<'a>, Error> {
294+
self.prepare_with_metadata_policy(sql, false, false).await
295+
}
260296
}
261297

262298
pub(crate) enum MaybePrepared {

tests/odbc/odbc.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,20 @@ async fn it_can_prepare_then_query_with_params_integer_float_text() -> anyhow::R
319319
Ok(())
320320
}
321321

322+
#[tokio::test]
323+
async fn describe_does_not_succeed_with_missing_columns() -> anyhow::Result<()> {
324+
let mut conn = new::<Odbc>().await?;
325+
326+
if let Ok(describe) = Executor::describe(&mut conn, "SELECT ? AS value").await {
327+
assert!(
328+
!describe.columns().is_empty(),
329+
"ODBC describe must error instead of silently reporting no columns"
330+
);
331+
}
332+
333+
Ok(())
334+
}
335+
322336
#[tokio::test]
323337
async fn it_can_bind_many_params_dynamically() -> anyhow::Result<()> {
324338
let mut conn = new::<Odbc>().await?;

0 commit comments

Comments
 (0)