Notes · 2026-08-23

Four things DuckDB's ODBC driver does that a caller has to know about

DuckDB ODBC 1.5.5 - SQLGetInfo(SQL_DBMS_VER) segfaults, a parameter array stores set 0 in every row, a C++ exception across the FFI aborts Rust, and :memory: is a new database per connection.

DuckDB is one of the fastest entries in a compatibility matrix that runs one workload through 53 databases' ODBC drivers — 3.05M rows/s fetch on Linux in the five-language run — and it passes the workload on Linux, macOS and Windows alike. Getting there needed four separate accommodations in the client, all in DuckDB ODBC 1.5.5. The :memory: one dates from the Rust benchmark run of 2026-08-23; the C++ exception is recorded from the five-language Windows campaign of 2026-08-25 as well as from Linux and macOS.

SQLGetInfo(SQL_DBMS_VER) segfaults inside the driver

Not an error return — the process dies, on Linux, on a call a client makes while identifying the server it has just connected to. On Windows the same call answered nothing at all on the first machine of the Windows campaign, whose entry records DuckDB (via ODBC) with the version missing; the second machine's run of the same entry records DuckDB (via ODBC) 1.5.5.

A parameter array stores set 0's value in every row

Set SQL_ATTR_PARAMSET_SIZE above 1, bind columns of a parameter array, execute — and the driver writes set 0's value into every row for every fixed-length C type. Only SQL_C_CHAR steps through the array correctly. There is no diagnostic to catch this on: the call returns SQL_SUCCESS, and SQL_ATTR_PARAMS_PROCESSED_PTR comes back with the right count. In a measured 20,000-row insert with arrays forced on, the visible symptom was a wrong row count.

The workaround is to bind one execute per row, which sounds like a heavy price and is not. Two things pay it back. Turning autocommit off for the duration and committing once takes that path from 10,131 to 16,421 rows/s on its own. Rewriting the insert as a multi-row INSERT INTO t (…) VALUES (?,?,?,?), (?,?,?,?), … — which needs no ODBC feature at all, only ordinary SQL — takes DuckDB from 24,057 to 344,597 rows/s at 10,000 rows, and from 20,342 to 403,228 at 50,000 — a 14× win, and it lands here precisely because the parameter arrays that would otherwise close that gap cannot be used.

Three more parameter-side notes on the same driver, all handled the same way — by not using the feature. SQLDescribeParam aborts the process, so a caller has to know its own types. SQL_BIT parameters are rejected, so booleans go as integers. And SQL_DECIMAL parameters are mis-scaled, so decimals go as SQL_VARCHAR text.

A C++ exception across the FFI boundary

fatal runtime error: Rust cannot catch foreign exceptions, aborting

The driver throws a C++ exception out of SQLExecute on the plain-ODBC path. A Rust process cannot unwind a foreign exception and aborts; std::terminate ends the process before a single line of output. Reproduced on Linux, macOS and Windows, and on macOS Go's own binding takes a SIGBUS on the same driver. On macOS Python, Java and C# are fine on it — the difference is in how each runtime handles an exception crossing the boundary, not in what the driver does.

Database=:memory: is a fresh database per connection

Catalog Error: Table with name adbc_bench_rs does not exist!

Every ODBC connection to an in-memory DuckDB gets its own empty database. A program that ingests on one connection and reads on another finds nothing: no error at write time, a catalog error at read time. That is how a benchmark harness here ingested on one connection and found no table on the next. Point Database= at a file — the matrix entry uses one in the run's temp directory, the way its SQLite entry always did.

Two read-side details

The driver writes a full 2048-row vector into bound buffers whatever the rowset size you asked for. Bind for SQL_ATTR_ROW_ARRAY_SIZE = 100 and it will write 2048 rows into a 100-row buffer. Allocate at least 2048 rows, or fetch in 2048-row rowsets and stop asking for anything smaller.

And it advertises all three SQL_GETDATA_EXTENSIONS bits — SQL_GD_BLOCK, SQL_GD_BOUND, SQL_GD_ANY_ORDER — while SQLSetPos fails outright, and SQLFetchScroll(SQL_FETCH_ABSOLUTE) is refused as well. Both textbook routes back to a value that truncated in a bound buffer are therefore closed, so a column with no declared width cannot safely be bound at a guess: there is no repair.

None of these are filed upstream yet; each is recorded with its conditions and its first error, and a reproduction contributed by anyone is welcome.

Where this comes from: the adbcBridge compatibility matrix runs one workload through every database's ODBC driver on Linux, macOS and Windows and records each failure with its first error; the row for this database is at https://adbcbridge.org/matrix/#duckdb, and the finding is on file with its reproduction in docs/UPSTREAM.md of the repository.