From 289ed79d6e039534bdb9521e382f8e160e657d0f Mon Sep 17 00:00:00 2001 From: Fredrik Fornwall Date: Mon, 6 Jul 2026 17:48:29 +0200 Subject: [PATCH] fix(rust/ffi): set rows_affected to -1 on the query path of ExecuteQuery `statement_execute_query` wrote `rows_affected` only on the update path (`out == NULL`). On the query path it left the caller's out-parameter untouched, so callers read back whatever they passed in. ADBC's `AdbcStatementExecuteQuery` contract (adbc.h) defines `rows_affected` as the row count if known, else -1; write that sentinel in the query branch. Adds a C-ABI regression test (via the dummy driver's exported vtable, since the Rust driver manager passes a NULL `rows_affected` for queries so the value is not observable through the high-level API). Signed-off-by: Fredrik Fornwall --- .../dummy/tests/driver_exporter_dummy.rs | 68 +++++++++++++++++++ rust/ffi/src/driver_exporter.rs | 3 + 2 files changed, 71 insertions(+) diff --git a/rust/driver/dummy/tests/driver_exporter_dummy.rs b/rust/driver/dummy/tests/driver_exporter_dummy.rs index b64d2016c2..b9bc989416 100644 --- a/rust/driver/dummy/tests/driver_exporter_dummy.rs +++ b/rust/driver/dummy/tests/driver_exporter_dummy.rs @@ -19,20 +19,24 @@ /// directly using the Rust API (native) and through the exported driver via the /// driver manager (exported). That allows us to test that data correctly round-trip /// between C and Rust. +use std::ffi::CString; use std::ops::Deref; use std::sync::Arc; +use arrow_array::ffi_stream::FFI_ArrowArrayStream; use arrow_array::{Array, Float64Array, Int64Array, RecordBatch, RecordBatchReader, StringArray}; use arrow_schema::{DataType, Field, Schema}; use arrow_select::concat::concat_batches; use adbc_core::Statement; +use adbc_core::constants::ADBC_STATUS_OK; use adbc_core::options::{ AdbcVersion, InfoCode, IngestMode, IsolationLevel, ObjectDepth, OptionConnection, OptionDatabase, OptionStatement, }; use adbc_core::{Connection, Database, Driver, Optionable, schemas}; use adbc_driver_manager::{ManagedConnection, ManagedDatabase, ManagedDriver, ManagedStatement}; +use adbc_ffi::{FFI_AdbcConnection, FFI_AdbcDatabase, FFI_AdbcError, FFI_AdbcStatement, FFIDriver}; use adbc_dummy::{DummyConnection, DummyDatabase, DummyDriver, DummyStatement, SingleBatchReader}; @@ -590,6 +594,70 @@ fn test_statement_execute_query() { assert_eq!(exported_data, native_data); } +// Driven at the C ABI (unlike the other tests): the driver manager passes a NULL +// `rows_affected` on the query path, so the -1 the exporter must write there is not +// observable through the high-level `Statement::execute`. +#[test] +fn test_statement_execute_query_sets_rows_affected() { + let driver = DummyDriver::ffi_driver(); + let mut error = FFI_AdbcError::default(); + let err = &mut error as *mut FFI_AdbcError; + + unsafe { + let mut database = FFI_AdbcDatabase::default(); + assert_eq!( + driver.DatabaseNew.unwrap()(&mut database, err), + ADBC_STATUS_OK + ); + assert_eq!( + driver.DatabaseInit.unwrap()(&mut database, err), + ADBC_STATUS_OK + ); + + let mut connection = FFI_AdbcConnection::default(); + assert_eq!( + driver.ConnectionNew.unwrap()(&mut connection, err), + ADBC_STATUS_OK + ); + assert_eq!( + driver.ConnectionInit.unwrap()(&mut connection, &mut database, err), + ADBC_STATUS_OK + ); + + let mut statement = FFI_AdbcStatement::default(); + assert_eq!( + driver.StatementNew.unwrap()(&mut connection, &mut statement, err), + ADBC_STATUS_OK + ); + let query = CString::new("SELECT 1").unwrap(); + assert_eq!( + driver.StatementSetSqlQuery.unwrap()(&mut statement, query.as_ptr(), err), + ADBC_STATUS_OK + ); + + // Seed `rows_affected` with a value the exporter must overwrite with -1. + let mut stream = FFI_ArrowArrayStream::empty(); + let mut rows_affected: i64 = 42; + assert_eq!( + driver.StatementExecuteQuery.unwrap()( + &mut statement, + &mut stream, + &mut rows_affected, + err, + ), + ADBC_STATUS_OK + ); + assert_eq!( + rows_affected, -1, + "a query must report rows_affected = -1 (not known), not a stale value" + ); + + driver.StatementRelease.unwrap()(&mut statement, err); + driver.ConnectionRelease.unwrap()(&mut connection, err); + driver.DatabaseRelease.unwrap()(&mut database, err); + } +} + #[test] fn test_statement_execute_schema() { let (_, _, _, mut exported_statement) = get_exported(); diff --git a/rust/ffi/src/driver_exporter.rs b/rust/ffi/src/driver_exporter.rs index 1a41d100b4..b1bb5e90a2 100644 --- a/rust/ffi/src/driver_exporter.rs +++ b/rust/ffi/src/driver_exporter.rs @@ -1728,6 +1728,9 @@ extern "C" fn statement_execute_query( let reader = Box::new(reader); let reader = FFI_ArrowArrayStream::new(reader); unsafe { std::ptr::write_unaligned(out, reader) }; + if !rows_affected.is_null() { + unsafe { std::ptr::write_unaligned(rows_affected, -1) }; + } } else { let rows_affected_value = check_err!(statement.execute_update(), error).unwrap_or(-1); if !rows_affected.is_null() {