Back to Blog

adbcBridge Part 2 — Your ODBC Driver Says SQL_SUCCESS and Lies: 24 Bugs Found Across 13 Database Projects

Prateek SinghAugust 30, 202615 min read
adbcBridge Part 2 — Your ODBC Driver Says SQL_SUCCESS and Lies: 24 Bugs Found Across 13 Database Projects

Part 2 of the adbcBridge story. Running one workload through 46 databases on three operating systems turned up 24 defects that belong to other projects — twelve of them return wrong or lost data under SQL_SUCCESS. Every one is filed upstream with a reproduction that needs no adbcBridge in the stack. Here is the ledger, what the bugs have in common, and what happened when the maintainers read them.

Part 1 of this story — I Built an Apache Arrow ADBC Driver for Every ODBC Database — was about what adbcBridge is: one plain-C11 shared library that turns whatever ODBC driver is on your machine into an Apache Arrow ADBC driver, columnar record batches out and bulk ingest in, from Python, Rust, Go, Java and C#. 46 databases verified on Linux, 41 on macOS, 45 on Windows, every number published with the laptop and the load it was measured under. This part is about what that exercise turned up in the drivers and servers it runs on.

Here is the short version. Running one workload — types, NULLs, Unicode in parameters and in statement text, bulk ingest, batched reads, catalog calls, error mapping — through 46 databases on three operating systems produced 24 defect reports against 13 other projects in five days, plus 24 more findings documented and waiting their turn. Twelve of the 24 reports are the hardest kind of bug to notice: the call returns SQL_SUCCESS and the data is wrong. Every report went upstream with a reproduction that does not involve adbcBridge at all — a standalone unixODBC C program, a libpq call, a wire capture — because a maintainer should never have to install my project to see their bug. Here is the ledger, what the bugs have in common, and what happened when the maintainers read them.

Why a bridge finds bugs nobody else does

Every ODBC driver on that list has been in production for years. Most of them work well for the applications their vendors test them with, which is usually one: a BI tool, an office suite, the vendor's own console. adbcBridge is a different kind of caller. It drives every driver through the same code path, in the same order, with the same edge cases — a parameter array of 999 rows with NULLs scattered through it, a BIGINT at INT64_MAX, an astral-plane emoji in a VARCHAR, a block cursor with the rows-fetched pointer set before SQLPrepare, a statement freed inside an open transaction. Any one of those is rare in a spreadsheet. All of them, on 46 drivers, on Linux, macOS and Windows, is a conformance suite most of these drivers have never been run through.

The compatibility campaign recorded every non-pass with its first error and the conditions around it (docs/COMPATIBILITY.md). What turned a list of quirks into a list of bug reports was a second pass, this week, with adbcBridge taken out of the picture: for each entry, a small C program against unixODBC alone, or psycopg and libpq against the server alone, to prove the defect belongs where I was about to say it belongs. Nine entries were re-verified that way on the 28th, twenty-two more on the 29th, and the wording in the compatibility file was tightened to what the plain-ODBC probe actually showed. Only then did anything get filed.

The ledger: 24 reports, 13 projects, five days

24 reports, 13 projects — filed 25–29 August 2026, each with a standalone reproduction OpenLink Virtuoso (virtodbc) 5 #1469 – #1473 Firebird ODBC (OdbcFb) 3 #299 #300 #301 clickhouse-odbc 2 #582, comment on #335 MySQL Connector/ODBC 2 #20 #21 OpenSearch SQL ODBC 2 #101 #102 QuestDB (pgwire) 2 #7566 #7567 psqlodbc 2 #207 #208 unixODBC 1 #239 — fix committed Arrow Flight SQL ODBC (Dremio) 1 #16 taos-odbc (TDengine) 1 #63 Apache Doris 1 #67301 Apache Ignite ODBC 1 #13537 CrateDB (pgwire) 1 #20085
Twenty-four reports by project. Twenty-three are new issues; the twenty-fourth is a comment with fresh evidence on a clickhouse-odbc issue that has been open since 2020.

OpenLink Virtuoso leads the count with five, and they are not five variants of one thing. #1469 is the macOS driver built to iODBC's 4-byte SQLWCHAR with nothing saying so; #1470 is the Linux driver doing the same on the parameter and fetch side, so a UTF-16 hello stores as ??; #1471 is any Charset= value other than the exact literal UTF-8 aborting the process inside SQLDriverConnect — including every charset Virtuoso itself lists; #1472 is SQL_C_SBIGINT parameters storing 0 and SQL_C_SBIGINT fetches leaving the buffer untouched; #1473 is a date parameter array stepped by ColumnSize instead of sizeof(SQL_DATE_STRUCT). The Firebird ODBC driver has three (#299, #300, #301), all in the parameter-binding and block-cursor paths that a spreadsheet never touches. The rest are one or two each: clickhouse-odbc (#582 and the #335 comment), MySQL Connector/ODBC (#20, #21), OpenSearch SQL ODBC (#101, #102), QuestDB (#7566, #7567), psqlodbc (#207, #208), and one each for unixODBC (#239), Dremio's Arrow Flight SQL ODBC (#16), TDengine's taos-odbc (#63), Apache Doris (#67301), Apache Ignite (#13537) and CrateDB (#20085).

Five days from first report to twenty-four — cumulative count 3824 25 Aug28 Aug29 Aug +3: unixODBC, Virtuoso, Dremio+5: Firebird ×3, TDengine, Doris+16 across nine projects
Three reports on 25 August, five on the 28th, sixteen on the 29th. The gap in the middle is the re-verification pass: nothing was filed until it had been reproduced without adbcBridge.

Half of them say SQL_SUCCESS and hand back the wrong answer

Half of them say SQL_SUCCESS and hand back the wrong answer Wrong or lost data, reported as SQL_SUCCESS 12 Firebird ×3 · Virtuoso #1470 #1472 #1473 · clickhouse #582 · Ignite #13537 · OpenSearch #101 · QuestDB #7566 · psqlodbc #207 #208 Process crash or abort 5 unixODBC #239 · Virtuoso #1469 #1471 · Dremio #16 · MySQL #21 Wrong metadata or row count 3 clickhouse #335 · MySQL #20 · CrateDB #20085 Refused where PostgreSQL / MySQL accept 2 Doris #67301 · QuestDB #7567 Hang 1 OpenSearch #102 Not implemented 1 taos-odbc #63
Twelve of the 24 return wrong or lost data under SQL_SUCCESS. Five crash or abort the process. Three report wrong metadata or a wrong row count. Two refuse something PostgreSQL or MySQL accept. One hangs; one is honestly not implemented.

A crash is a bad day, but it is an honest one: you know something went wrong. The twelve in the first bar are the ones that keep me up. Firebird #299: bind a column-wise parameter array of SQL_C_SLONG, SQL_C_SBIGINT, SQL_C_DOUBLE, dates or timestamps and the driver steps through the array at the BufferLength stride — which the specification says is ignored for fixed-length types — so every row is written with row 1's values and the NULL indicator lands in the wrong row, SQL_SUCCESS throughout. psqlodbc #207: read 9223372036854775807::bigint into a 32-bit SQL_C_SLONG and you get -1, not the 22003 numeric-out-of-range the specification requires; pg_atol stores the low bits and moves on. OpenSearch #101: any character value converted to SQL_C_TYPE_TIMESTAMP comes back as today's date, because convert.c fills an unparsed time struct from localtime. psqlodbc #208 is the one I would put at the top of anyone's list: free a prepared statement inside a transaction on a server without SAVEPOINT and the driver's own savepoint request fails, the error is swallowed, the server rolls the transaction back, and SQLEndTran(SQL_COMMIT) returns SQL_SUCCESS for rows that no longer exist. The wire capture is in the report.

The others in that bar are the same shape with different details: Virtuoso storing 0 for a 64-bit integer (#1472) and ?? for a wide string (#1470); clickhouse-odbc sending an empty string where you bound SQL_NULL_DATA, so '' lands in a String column and the epoch in a DateTime64 (#582); Ignite testing row 0's NULL indicator for every row of an array (#13537); QuestDB sending a BINARY column in a simple-query result with format code 1 where PostgreSQL always sends text, so libpq, psycopg and psqlodbc all truncate the value at its first 0x00 (#7566); Firebird writing NULL into every later SQL_C_SBIGINT rebind of a parameter that once held a character-typed NULL (#300), and discarding a rows-fetched pointer set before SQLPrepare so a block cursor never learns how many rows it got (#301).

Where the bugs live

Where the defect lives — the driver is the weak layer 1194 driver managervendor ODBC driverserver unixODBC Virtuoso, Firebird, ClickHouse, MySQL, OpenSearch, psqlodbc, Dremio, TDengine, Ignite Doris, QuestDB ×2,CrateDB Each report names the layer it was reproduced in: a plain-ODBC C program for drivers, libpq / psycopg or a wire capture for servers.
One in the driver manager, nineteen in vendor ODBC drivers, four in servers. The reproductions were built for the layer they name — plain ODBC for drivers, libpq or a wire capture for servers.

The server-side four are worth separating because they are not ODBC bugs at all; they are PostgreSQL- and MySQL-wire servers disagreeing with the reference implementation in ways only a strict client notices. CrateDB #20085 tags each CommandComplete of a multi-statement query with the leading text of the whole string (BEGIN;INSERT 0) instead of the per-statement tag (INSERT 0 1), so any client that parses tags for a row count gets none inside a transaction. QuestDB #7567 fails DEALLOCATE ALL on a fresh connection with nothing prepared. Apache Doris #67301 refuses a server-side prepared INSERT whose string parameter arrives typed as BLOB — which is exactly what MySQL Connector/ODBC sends for every SQL_C_WCHAR parameter, so a Unicode ODBC client cannot insert through prepared statements at all: a NullPointerException in the frontend on 2.1.0, Unsupported MySQL type: BLOB on 4.1.3. The one driver-manager bug is the unixODBC story below.

One bug, seven implementations

One bug family, seven implementations: SQL_ATTR_PARAMSET_SIZE > 1 Column-wise parameter arrays are the fast path for bulk ingest. Almost nobody's application exercises them, so almost nobody's driver gets them right. Firebird ODBC #299 fixed-length C types stepped at BufferLength filed Virtuoso #1473 DATE array stepped at ColumnSize, not sizeof(SQL_DATE_STRUCT) filed Apache Ignite #13537 row 0's NULL indicator tested for every row filed clickhouse-odbc #582 array stops after set 1 (SQLMoreResults answers SQL_NO_DATA) filed DuckDB ODBC 1.5.5 (Linux) set 0's value stored in every row, SQL_SUCCESS documented MariaDB Connector/ODBC ≥ 3.2 segfault on an array holding a NULL DATE documented MatrixOne 4.2 (server) BIT array with NULLs aborts the server fixed upstream adbcBridge probes each driver once and falls back to row-wise binding or single-row execute where the array path is wrong.
Column-wise parameter arrays (SQL_ATTR_PARAMSET_SIZE > 1) are the fast path for bulk ingest. Seven implementations diverge from the specification in seven different ways; four are filed, two are documented and waiting, one was fixed upstream before it could be filed.

If there is one finding to take away, it is this chart. Array binding is how an ODBC client inserts a million rows without a million round trips, and it is the path adbcBridge takes by default for ingest. It is also, on this evidence, the least-exercised path in the ecosystem — understandably, since few ODBC callers use it. Firebird steps at the wrong stride; Virtuoso steps at a different wrong stride; Ignite reads one row's NULL indicator for all of them; clickhouse-odbc executes set 1 and then answers SQL_NO_DATA for the rest; DuckDB's ODBC driver 1.5.5 on Linux stores set 0's value in every row for every fixed-length type with SQL_SUCCESS and the right rows-processed count; MariaDB Connector/ODBC 3.2 and later segfault on an array holding a NULL DATE. And MatrixOne 4.2 — the one server on the list — aborted with malloc(): unaligned fastbin chunk detected once a BIT array held NULLs. adbcBridge probes each driver once, records the answer, and falls back to row-wise binding or a single-row execute where the array path is wrong; that is why all seven pass the compatibility run even though six of the seven bugs are still in the wild.

What unfolded: the unixODBC thread

unixODBC #239 — from report to a committed fix in one day 25 Aug 00:52 UTC Report filed A 40-line fake driver reproduces the abort with no vendor driver; lldb frames from two real macOS drivers 25 Aug 08:16 Maintainer diagnoses a driver-side mismatch The 4-byte SQLWCHAR driver, not the manager, is writing past the buffer — a correct call; issue closed 25 Aug 15:03 A second maintainer proposes a way through An odbcinst.ini setting so the manager can adapt, rather than changing every driver 25 Aug 16:03 Reply: agreed on the cause; the concern is the silent SIGABRT Two patches offered as PRs 25 Aug 16:07 Commit a2acae7 “Catch SQLWCHAR size mismatch” Doubled buffers, a sentinel test, a message before the abort 27 Aug 01:22 Test report on the commit Four builds of the fake driver + sqliteodbc; one regression found (missing memset); patch attached The issue is closed with the fix on the maintainer's tree; the follow-up patch is awaiting review.
unixODBC #239, hour by hour. Filed at 00:52 UTC; diagnosed as a driver-side mismatch and closed at 08:16; a commit at 16:07 the same day. The follow-up test report found one regression in the fix and attached a patch.

unixODBC is the ODBC driver manager on every Linux box and most Macs, and #239 is the one report that has had a real conversation so far. The bug: a driver built with a 4-byte SQLWCHAR (the iODBC convention, which is what several vendors ship for macOS) writes past the driver manager's 2-byte-unit diagnostic buffers on the first SQL_ERROR, the stack protector fires, and the process dies with a silent SIGABRT — after connecting and running queries fine. I filed it with a 40-line fake driver that reproduces it with no vendor code involved, lldb frames from two real drivers, and a suggested fix.

The maintainer's first reply, eight hours later, was a correct diagnosis: the driver manager was not overwriting its own stack, the driver was. A driver that decides SQLWCHAR is four bytes has stepped outside the contract the manager is built on, and that makes it a driver bug — so he closed the issue, and I agreed with him; the two vendors were told the same thing in Virtuoso #1469 and Dremio #16. The point I raised in reply was about the user rather than the cause: someone on macOS installs the vendor's only build, sees a silent abort on the first error, and has no way to tell the driver from the manager from their own code. A second maintainer proposed an odbcinst.ini setting so the manager could adapt instead of every driver changing. And at 16:07 the same day the first maintainer committed a2acae7, "Catch SQLWCHAR size mismatch": doubled buffers, a sentinel that detects a 4-byte write, and a clear message before the abort — the mismatch is now caught before anything is corrupted. Diagnosing, closing and shipping a defence for a report on the same day is about as good as an open-source response gets.

I built that commit on the 2.3.14 tree and sent back a test report rather than a thumbs-up: the fake driver compiled four ways (2-byte and 4-byte, with and without the SQLGetDiagRecW entry points) plus sqliteodbc as a control. The 4-byte driver now stops cleanly. One regression turned up: a normal 2-byte driver that exports only SQLErrorW also trips the new fatal error, because that loop compares the sentinel without first setting it. The patch for that — a memset — is in the thread, with an optional second change that reports the mismatch as an HY000 diagnostic and returns instead of aborting. Both are offered as PRs. The issue is still marked closed, the fix is on the maintainer's tree, and the follow-up is awaiting a reply.

What unfolded: everyone else

Where they stand — 29 August 2026, 21:30 UTC 1 23 open, no reply yet fix committed (unixODBC) 20 of the 23 were filed in the last 48 hours; OpenSearch's two are labelled “untriaged” +1 fixed upstream before it could be filed: the MatrixOne BIT-array abort, matrixorigin/matrixone#27645 (26 Aug), verified on the 28 Aug nightly. The released 4.2.0 still aborts, so adbcBridge keeps ingesting booleans as TINYINT there until the fix ships.
As of 29 August 2026, 21:30 UTC: one fix committed, twenty-three open with no reply — twenty of them filed in the previous 48 hours. MatrixOne's abort was fixed upstream on the 26th before it could be filed.

Honesty about the other 23: nothing yet. Twenty of them were filed today; the Firebird, TDengine and Doris reports are two days old; the two vendor tickets from the 25th have no comment. That is normal — most of these projects triage weekly, and MySQL's went to the GitHub mirror because a bugs.mysql.com account could not be created. I will update the ledger as replies come in. One finding resolved itself before I got to it: the MatrixOne server abort was fixed on main by matrixorigin/matrixone#27645 on 26 August; I re-verified against the 28 August nightly (NULL stores as NULL, the 999-set array survives). The released 4.2.0 still aborts, so adbcBridge keeps ingesting booleans as TINYINT there until the fix ships.

Two of the reports solved mysteries from Part 1 rather than adding to them. The OpenSearch connect hang that the compatibility table could only describe now has a cause (#102): the driver's SQLError returns the same connection error on every call and never SQL_NO_DATA, and unixODBC's SQLDriverConnect prefers SQLError for a failed connect's diagnostics, so it loops forever — psqlodbc never hangs because it does not export SQLError at all. And the clickhouse-odbc affected-row count that has been 0 since 2021 (#335) turns out to be knowable: the server already sends written_rows in the X-ClickHouse-Summary header of every HTTP response, checked with a plain curl. Zero, as the comment notes, is a stated count, not the ODBC "unknown" value of -1; a generic caller that trusts it concludes nothing was written.

The rest of the ledger

docs/UPSTREAM.md — what was found, where it went, what came back 2424 reported upstreamdocumented, not yet filed each with a standalone reproduction conditions recorded; verified the same way before filing (one already fixed upstream: MatrixOne)
docs/UPSTREAM.md is the running record: 24 reported, 24 documented with the conditions that reproduce them and not yet filed. Each of the second column is verified the same way before it moves to the first.

The second column of docs/UPSTREAM.md is the queue: 24 more findings with their first error and the file that records the conditions. Some will become reports next week — MySQL Connector/ODBC on Windows needing NO_SSPS=1 against every non-MySQL server and reading astral characters as ??? from servers without character-set session variables; the Windows build of Arrow Flight SQL ODBC returning a non-BMP character with only its low 16 bits; a Go ODBC package that access-violates on its first diagnostic; MariaDB Connector/ODBC 3.1.15 not NUL-terminating a bound SQL_C_CHAR that exactly fills its buffer; RisingWave never releasing a server-side statement name, so psqlodbc's _PLAN names collide on the second prepare. Some are arguably nobody's bug — psqlodbc 18 sending SHOW DateStyle at connect, which YDB's PostgreSQL layer rejects. And some are just conditions worth knowing: no Firebird or Ignite ODBC build for Apple Silicon, an OpenSearch macOS package that is x86_64-only and links iODBC. A reproduction contributed by anyone moves an entry from the second column to the first.

What adbcBridge does about them

Filing a bug and waiting is not a strategy for a driver people install today, so every one of these has a route-around recorded next to it, keyed on the driver, with no global state. Firebird's parameter arrays are bound row-wise (#299). DuckDB is bound row by row and fetched in 2048-row rowsets. MariaDB 3.2+ gets no_param_arrays. RisingWave gets UseServerSidePrepare=0. MatrixOne booleans go in as TINYINT; its TEXT columns, which SQLDescribeCol sizes at a third of their real width, are bound at long_bind_bytes. A driver whose SQLBindCol omits the terminator is read by its indicators, not its bytes. Virtuoso's Windows driver writes its indicator array at a 4-byte stride on a block cursor, so it is read at that stride. The compatibility run is green on all 46 because of those entries, and each one is a line in tests/compat/README.md that says which upstream bug it exists for, so it can be deleted the day the bug is.

That is also the reason to keep filing. A bridge that silently papers over 48 driver defects forever is a bridge that owns 48 defects. A bridge whose route-arounds each point at an open upstream issue is a to-do list for the ecosystem, and the unixODBC thread shows the list gets shorter when the reproduction is good enough.

What adbcBridge is, and where it is going

For anyone arriving here first: adbcBridge is an independent, Apache-2.0 driver that implements the Apache Arrow ADBC standard — it is not an Apache project — and it exists for the few hundred databases that have an ODBC driver and will never get a native ADBC one: Db2, Oracle, Informix, Vertica, Teradata, SAP HANA, Firebird, Access, and every warehouse behind a PostgreSQL or MySQL wire. Where a native ADBC driver is installed (PostgreSQL, SQLite, DuckDB, Flight SQL), the same install hands the connection over, so you get native speed from the same code. The read path lands within 7% of the raw SQLBindCol/SQLFetch floor on the Linux laptop the benchmarks name (a Core i9-13900HK with the 46-database fleet running in Docker on the same box), and partitioned reads split a table across connections — 1.54× the native PostgreSQL driver at eight partitions on ten million rows there, 0.36× on one. The full numbers, hosts and loads are in Part 1 and on adbcbridge.org/bench.

The roadmap in one paragraph: the ODBC bridge is 0.1.0 and on PyPI, crates.io and nuget.org, with the Go module fetched from the tag and the jar on the release page until Maven Central. Next are the ADBC Driver Foundry validation suite and a Foundry listing, a driver bootstrap so install.sh fetches the open-licence ODBC drivers a first run needs, and the Windows prefetch and parallel-ingest pipelines. After that the name earns its plural: a JDBC bridge that loads a JVM in-process and returns Arrow batches from any JDBC driver, and an OLE DB bridge for the last few Windows-only sources. The non-goal has not moved — replacing native ADBC drivers where they exist. Those will always be faster. adbcBridge is for everything else.

References & Citations

Subscribe to new posts from theaivibe.org

No spam — just new posts. One-click unsubscribe.
Share this article

Related Posts

I Built an Apache Arrow ADBC Driver for Every ODBC Database: 46 Databases, 5 Languages, Every Number Measured
Data Engineering21 min read

I Built an Apache Arrow ADBC Driver for Every ODBC Database: 46 Databases, 5 Languages, Every Number Measured

Native Apache Arrow ADBC drivers exist for a handful of databases. The other few hundred ship an ODBC driver and nothing else. adbcBridge is one plain-C11 shared library that turns every ODBC driver on your machine into an Arrow-native ADBC driver — columnar record batches out, bulk ingest in — from Python, Rust, Go, Java and C#. Today it is public: 46 databases verified on Linux, 41 on macOS, 45 on Windows, five languages measured against all of them, and every figure named with the laptop and the load it was taken under.

43 views
Read
The First SQL Engine for Apple Silicon GPUs Is Now a DuckDB Community Extension
Data Engineering8 min read

The First SQL Engine for Apple Silicon GPUs Is Now a DuckDB Community Extension

In May 2026 I shipped gpudb v0.1 — the first SQL execution engine targeting Apple Silicon GPUs, built as a DuckDB extension with a CUDA backend on Linux. Three releases later, the project crossed two lines at once. v0.3.0's streaming-aggregate rewrite reached parity with native DuckDB on end-to-end TPC-H queries — the worst cell improved roughly 100×, from 11.05 s to 0.109 s. And gpudb became an official DuckDB Community Extension: INSTALL gpudb FROM community now works in any DuckDB ≥ 1.5.5, signed, no flags. This is the full arc — what v0.1 proved, what v0.2 honestly lost, what v0.3 fixed, and why the next GPU frontier is joins.

64 views
Read
The Agent-Written Data Pipeline: The Review Bottleneck Nobody Priced In
Data Engineering10 min read

The Agent-Written Data Pipeline: The Review Bottleneck Nobody Priced In

AI agents can now write dbt models, SQL transforms, and backfills that pass CI and ship. The catch: a wrong number doesn't crash, it quietly poisons every dashboard downstream. The hard part moved from authoring to verification.

20 views
Read