Your ODBC Driver Says SQL_SUCCESS and Lies: 24 Bugs Found in 13 Database Projects (adbcBridge Part 2)

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: within the first week, two fixes landed upstream and four more fix PRs went up.
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 came back. Because finding bugs was never the point — adbcBridge needs these drivers to work, so every defect it hits goes back upstream in the form most useful to the maintainer. Within the first week of giving them back, two fixes landed and four more maintainer fix PRs went up.
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
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).
Half of them say SQL_SUCCESS and hand back the wrong answer
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
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
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 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: a fix a day
This section was drafted two days after the bulk filing to manage expectations: most of these projects triage weekly, so expect silence for a while. The week had other plans. Between Saturday and Monday morning four projects moved — a different one each day — and by the time this article went up, two of the twenty-four defects were fixed and four more had maintainer fix PRs open. adbcBridge wrote none of that code. Its contribution was the reproduction each fix was built against; the maintainers did the rest, which is exactly how the give-back loop is supposed to run. Project by project:
CrateDB — reported Saturday, merged Monday, ships in 6.4.4
#20085 reported that CrateDB tags each CommandComplete of a multi-statement query with the leading text of the whole string instead of the per-statement tag, so any client that parses tags for row counts gets none inside a transaction. On Monday morning a core maintainer picked it up: fix PR crate/crate#20088 open at 08:45 UTC, "This will be fixed in 6.4.4" on the thread at 08:57, merged at 09:49 — report to merged fix in about an hour of maintainer time, with backports queued by the merge bot minutes later. Every PostgreSQL-wire client that reads command tags gets correct row counts out of that fix, not just adbcBridge.
psqlodbc — a reply and a fix PR, twenty-eight minutes apart
psqlodbc is the PostgreSQL ODBC driver, which makes it the de-facto ODBC path for everything speaking the PostgreSQL wire — CockroachDB, QuestDB, CrateDB, YugabyteDB and a dozen more of the fleet ride through it. On Sunday its maintainer replied on #208 — the driver savepoint-wraps its own internal queries even at rollback level 0, which breaks servers that speak the wire but not savepoints — and twenty-eight minutes later had PR #209 open with the fix. The reply openly weighed whether non-PostgreSQL servers should be supported at all; the PR answered by supporting them.
QuestDB — two reports, two fix PRs within a day
Both QuestDB pgwire reports — #7566 and #7567 — had dedicated fix PRs (#7568, #7569) open by early Sunday, inside a day of filing: one for a NullPointerException in the connection pool, one fixing the text-format path of simple queries. Two reports in, two fixes in flight, no argument about whether a strict ODBC client is a legitimate caller.
Apache Doris — the Unicode-insert door, reopened
#67301 reported that a server-side prepared INSERT refuses a string parameter typed as BLOB — which is what MySQL Connector/ODBC sends for every SQL_C_WCHAR parameter, so a Unicode ODBC client could not insert through prepared statements at all. By Saturday afternoon fix PR #67306 was up: accept BLOB-typed prepared parameters. One accepted type, and a whole class of clients works again.
The rest of the board
clickhouse-odbc #335 — the affected-row count that has answered 0 since 2021 — picked up triage labels on Monday after five years untouched. OpenSearch's two are labelled "untriaged"; the MySQL, Virtuoso, Firebird, TDengine and Ignite reports have no reply yet — normal for weekly triage, and MySQL's went to the GitHub mirror because a bugs.mysql.com account could not be created. 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 Part 1 mysteries, solved on the way
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
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. And since the 30th the first column keeps itself honest: an hourly CI job re-reads every filed thread and publishes upstream/status.json — state, maintainer events and fix links, machine-readable, always current.
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 first week upstream — two fixes landed, four fix PRs open — 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.
- Source, docs and the upstream ledger: github.com/singhpratech/adbcbridge · adbcbridge.org/docs
- Python:
pip install adbcbridge· Rust:cargo add adbcbridge· C#:dotnet add package AdbcBridge· Go:go get github.com/singhpratech/adbcbridge/go - Part 1, the launch write-up with every benchmark: I Built an Apache Arrow ADBC Driver for Every ODBC Database
- Found one of these in your own stack? The reproduction programs are in each issue; a reproduction of any entry in the second column of UPSTREAM.md is the most useful contribution the project can get right now.
References & Citations
- Singh, P. (2026-08-27). “I Built an Apache Arrow ADBC Driver for Every ODBC Database: 46 Databases, 5 Languages, Every Number Measured.” theaivibe.org — Part 1: what adbcBridge is, the benchmarks and their hosts.
- adbcBridge (2026). “Upstream: what this project found, and gave back.” docs/UPSTREAM.md, read 2026-08-29, statuses re-verified live 2026-08-31 (hourly mirror: upstream/status.json) — the 24 reported entries with links and status, and the 24 documented-not-yet-filed findings with where each is recorded.
- adbcBridge (2026). “Compatibility tracker, 46 × 3.” docs/COMPATIBILITY.md and tests/compat/README.md — per-database non-pass conditions and the driver-keyed route-arounds, re-verified with plain-ODBC probes on 2026-08-28/29 (repo PRs #6–#13).
- unixODBC — lurcher/unixODBC#239 (2026-08-25), maintainer commit a2acae7 “Catch SQLWCHAR size mismatch” (2026-08-25 16:07 UTC); follow-up test report and patch 2026-08-27.
- OpenLink Virtuoso — #1469, #1470, #1471, #1472, #1473 (openlink/virtuoso-opensource, 2026-08-25 and 2026-08-29).
- Dremio Arrow Flight SQL ODBC driver — dremio/warpdrive#16 (2026-08-25).
- Firebird ODBC driver (OdbcFb) — #299, #300, #301 (FirebirdSQL/firebird-odbc-driver, 2026-08-28).
- TDengine taos-odbc — taosdata/taos-connector-odbc#63 (2026-08-28).
- Apache Doris — apache/doris#67301 (2026-08-28); fix PR #67306 opened 2026-08-29.
- ClickHouse ODBC driver — ClickHouse/clickhouse-odbc#582 and comment on #335 (2026-08-29).
- MySQL Connector/ODBC — mysql/mysql-connector-odbc#20, #21 (GitHub mirror, 2026-08-29).
- Apache Ignite ODBC — apache/ignite#13537 (2026-08-29).
- OpenSearch SQL ODBC — opensearch-project/sql-odbc#101, #102 (2026-08-29).
- QuestDB — questdb/questdb#7566, #7567 (2026-08-29); fix PRs #7568, #7569 opened 2026-08-30.
- CrateDB — crate/crate#20085 (2026-08-29); fixed by crate/crate#20088, merged 2026-08-31, ships in 6.4.4.
- psqlodbc (PostgreSQL ODBC driver) — postgresql-interfaces/psqlodbc#207, #208 (2026-08-29); maintainer fix PR #209 opened 2026-08-30.
- MatrixOne — matrixorigin/matrixone#27645 (merged 2026-08-26, issue #27635): the BIT parameter-array abort fixed upstream; verified against the 2026-08-28 nightly.
- DuckDB ODBC driver 1.5.5 and MariaDB Connector/ODBC 3.1.15 / 3.2.9 — parameter-array and SQLBindCol findings documented in docs/UPSTREAM.md and tests/compat/README.md, not yet filed.
- Apache Arrow (2026). “ADBC: Arrow Database Connectivity.” arrow.apache.org/adbc — the specification adbcBridge implements. adbcBridge is an independent implementation, not an Apache project.
- adbcBridge (2026). “Roadmap.” docs/ROADMAP.md — Foundry validation suite, Maven Central, driver bootstrap, Windows prefetch/parallel ingest, JDBC and OLE DB bridges; non-goals.
- Package registries, verified 2026-08-29: pypi.org/project/adbcbridge (0.1.0), crates.io/crates/adbcbridge (0.1.0), nuget.org/packages/AdbcBridge (0.1.0), Go module (go/v0.1.0).
Subscribe to new posts from theaivibe.org
Related Posts
Apache Arrow and Apache DataFusion Now List ArrowMetal: Arrow Compute and a DataFusion Optimizer Rule on the Apple Silicon GPU
ArrowMetal is now on Apache Arrow's Powered By page and on Apache DataFusion's integrations list. What the two listings mean, and what the listed thing does as of 0.5.0, with one real run on each side.
Polars 2.0 Is Faster. With ArrowMetal on the Apple Silicon GPU, 52 of 107 Queries Go 1.35x to 7.94x Faster Still
Polars 2.0.0 shipped on 6 October and runs its group-bys, joins and sorts faster than 1.44. Two days later ArrowMetal's MetalEngine was re-measured against it on an Apple M4 Max: the GPU default takes 52 of 107 queries at 50 million rows, each 1.35x to 7.94x faster than the faster Polars 2.0 engine, and hands 12 back that it used to take. What Polars 2.0 changed, and how a crossover table answers it.
DataFusion on the Apple Silicon GPU: One Optimizer Rule, the Same SQL, Sorts 6.9x to 28.8x Faster
Apache DataFusion runs any physical optimizer rule you register. ArrowMetal 0.4.0 ships one for the Apple silicon GPU: the SQL is unchanged, the answers are DataFusion's, and full sorts of 250,000 to 50 million rows run 6.9x to 28.8x faster on an M4 Max. What it takes, what it leaves, and why.