Four things DuckDB's ODBC driver does that a caller has to know about
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.