“PostgreSQL-compatible” Is Not PostgreSQL: What Arrow's Native ADBC Driver Does on 14 Wire-Compatible Databases

I ran the native PostgreSQL and MySQL ADBC drivers against 28 databases that speak their protocols. Half stopped. Then I found a bug in my own driver.
Every “PostgreSQL-compatible” database promises that your existing tools just work. For psql, JDBC and ODBC that is largely true. For Arrow's ADBC it is half true, and the half that fails is worth understanding before you build on it.
ADBC, Arrow Database Connectivity, is the Arrow project's database API: you hand a driver a query and get Arrow record batches back, no row-by-row conversion. The Arrow project ships a native PostgreSQL driver, and the ADBC Driver Foundry ships a MySQL one. Both are excellent against the database they were written for. My question was narrower: what happens when you point them at the fourteen PostgreSQL-wire and fourteen MySQL-wire databases that are not PostgreSQL or MySQL?
I maintain adbcBridge, an ADBC driver that wraps ODBC drivers, so I already had all 28 running and a workload that exercises the whole ADBC surface: connect, a trivial SELECT, a CREATE TABLE plus inserts plus a read, a 1,000-row bulk ingest with a count check, table schema, catalog listing, and a read of the ingested table. On 5 September 2026 I ran it two ways per database: the native driver, and adbcBridge over the vendor's own ODBC driver with native delegation switched off, so the comparison is native protocol against ODBC.
The result
| Native driver | adbcBridge over ODBC | |
|---|---|---|
| Both pass | PostgreSQL, YugabyteDB, TimescaleDB, Citus, Cloudberry, MySQL, MariaDB, Percona, MariaDB ColumnStore, TiDB, Dolt, MatrixOne, SingleStore, OceanBase | same 14 |
| Native stops, ODBC passes | — | CockroachDB, Materialize, QuestDB, RisingWave, openGauss, CrateDB, Spanner, ArcadeDB, YDB, Databend, GreptimeDB, Doris, StarRocks, MongoDB BI Connector |
| ODBC stops, native passes | none | — |
Fourteen and fourteen, and nothing in the third row.
What “pass” means here
A pass is all seven steps, in order, with the results checked, not a successful connect. The workload is the one I use for every entry in the compatibility matrix, so the bar is the same for the native drivers as it is for adbcBridge:
- connect — open the database and one connection;
SELECT 1— the smallest possible round trip through the driver's result path;- DDL and a read — the eight-column
CREATE TABLEfrom the matrix entry, twoINSERTs, and a read of the rows back; - bulk ingest —
adbc_ingestof 1,000 rows ofint32,float64,utf8anddate32, followed by aCOUNT(*)that must say 1,000; - table schema —
GetTableSchemaon the ingested table; - catalog —
GetObjects, the catalog and schema listing; - read-back — the ingested table read as Arrow batches.
The native drivers were the current releases on the day: the Arrow project's adbc-driver-postgresql 1.12.0 and the ADBC Driver Foundry's MySQL driver 0.6.0. adbcBridge ran over each vendor's own ODBC driver, the same one the matrix row names, with adbc.odbc.delegate=never so that nothing in the ODBC column could secretly be the native driver. That last setting turned out to matter, as the second half of this post explains.
Why the native driver stops
The pattern is clean. Arrow's PostgreSQL driver reads every result through COPY … TO STDOUT (FORMAT binary) and learns its types from pg_catalog. Against a real PostgreSQL server that is the fastest path there is. CockroachDB, CrateDB and YDB do not implement binary COPY, so the very first SELECT fails. Materialize, QuestDB and RisingWave reject the pg_type bootstrap query, so the driver never gets past connect. openGauss encodes one binary field slightly differently and the row decode fails. Spanner requires a primary key on every table and the driver's generated CREATE TABLE has none. On the MySQL side, Doris will not prepare the statement the Foundry driver sends on connect, and Databend and StarRocks reject the DDL it generates for bulk ingest.
Here is the first error each one gave, verbatim, because “does not work” is not a useful finding and the exact text is what you will search for at two in the morning:
| Database | Step | First error from the native driver |
|---|---|---|
| CockroachDB | SELECT 1 | could not begin COPY: ERROR: unimplemented: binary format for COPY TO not implemented |
| CrateDB | SELECT 1 | could not begin COPY: ERROR: line 1:6: no viable alternative at input 'COPY (' |
| YDB | SELECT 1 | could not begin COPY: … RawStmt: alternative is not implemented yet : 138 |
| openGauss | SELECT 1 | ReadRecord failed at row 0: Unexpected end of input (expected 2 bytes but found 0) |
| Materialize, QuestDB, RisingWave | connect | the type-resolver bootstrap SELECT oid, typname, typreceive, … FROM pg_catalog.pg_type is rejected |
| ArcadeDB | connect | Expected 5 or 6 columns from type resolver pg_type query but got 0 |
| Spanner | ingest | Primary key must be defined for table "adbc_ing_np" (reads work) |
| Databend | ingest | SyntaxException on the driver's generated CREATE TABLE … INT NULL |
| StarRocks | ingest | Unexpected input 'NULL' on the same generated DDL |
| GreptimeDB | DDL read | invalid decimal precision/scale (1023, 0); ingest: Missing time index constraint |
| Doris | connect | Only support prepare SelectStmt or InsertStmt now |
| MongoDB BI Connector | connect | recv handshake response error: invalid connection attribute at index 0: EOF |
Read down the step column and the fourteen sort into four families. No binary COPY: CockroachDB, CrateDB and YDB parse the statement and refuse it, three different ways. No usable pg_type: Materialize, QuestDB and RisingWave reject the bootstrap query outright, and ArcadeDB answers it with zero columns, which the driver rightly treats as no answer. DDL the server will not take: Spanner wants a primary key, Databend and StarRocks choke on INT NULL, and GreptimeDB rejects a decimal of precision 1023 at the DDL read and wants a time index at ingest. Handshake and prepare: Doris only prepares SELECT and INSERT, and the MongoDB BI Connector drops the connection at the first connection attribute. openGauss is the odd one out, a real binary-COPY implementation whose encoding of one field comes up two bytes short of what the driver expects.
None of this is a defect in those drivers. Each is built for one server and uses that server's fastest path. The wire-compatible databases emulate enough of the protocol for the common clients and stop there, which is a reasonable place to stop. The vendor's own ODBC driver, meanwhile, knows exactly what its server can do, and adbcBridge inherits that knowledge for free. That is the whole reason it exists.
The fourteen that pass natively are also informative. YugabyteDB, TimescaleDB, Citus and Cloudberry are PostgreSQL underneath, so the native path works. TiDB, Dolt, MatrixOne, SingleStore and OceanBase implement MySQL's prepared statements and DDL faithfully enough. If your database is on that list, use the native driver; it is faster.
The bug I did not expect
adbcBridge has a feature I was proud of: when it recognises a database that has a native ADBC driver, it hands the connection to that driver instead of opening ODBC at all, on the theory that native beats ODBC whenever native works. The first time I ran the probe I had left that feature on, and the CockroachDB column marked “bridge” failed with a libpq error. libpq is not something ODBC ever produces.
The bridge had seen psqlodbc in the connection string, decided this must be PostgreSQL, delegated to the native driver, and inherited its failure. The native driver's initialisation succeeds on CockroachDB, because libpq connects and the catalog bootstrap answers, so the bridge had no signal that anything was wrong until the first query. The same thing happened on CrateDB, openGauss and YDB, and on Spanner for writes. Anyone who had installed adbc-driver-postgresql alongside adbcBridge and pointed it at one of those five databases would have hit it with default settings.
The fix is obvious in hindsight. Decide after you know what the server can do, not before. adbcBridge now opens one connection through the native driver, runs SELECT version(), and reads the result. If that fails, the native database is released and the connection stays on ODBC, with the reason recorded where the caller can read it. One round trip at database initialisation, and the failure mode becomes a clean fallback. That shipped as 0.1.1 the same day; on 0.1.0 a single option, adbc.odbc.delegate=never, turns delegation off.
How delegation decides now
Delegation exists because a native ADBC driver builds Arrow straight off the wire and an ODBC driver cannot. On the benchmark box, a laptop-class Intel Core i9-13900HK running PostgreSQL 16 in Docker, the same million-row four-column query takes 0.19 s through adbc-driver-postgresql and 0.57 s through adbcBridge over psqlodbc, medians of interleaved runs with the machine otherwise busy (bench/BENCHMARKS.md has the full conditions). Three times faster is worth having, so when adbcBridge sees a target a native driver handles, it loads that driver, rebuilds the connection from the ODBC keywords, and forwards every call to it. A delegated fetch measures the same as the native driver called directly.
The option is adbc.odbc.delegate with three values. auto, the default, delegates when a native driver can be found and falls back to ODBC when it cannot, recording why in adbc.odbc.delegate.last_error. never stays on ODBC. always makes a missing native driver an error rather than a slow path. Before 0.1.1, auto already fell back correctly whenever the native driver failed to initialise, which is why Materialize, QuestDB, RisingWave and ArcadeDB were never affected: the native driver cannot get past connect on them, so the bridge went to ODBC as designed. The gap was the other five, where initialisation succeeds and the first query fails.
0.1.1 closes that gap with one probe at AdbcDatabaseInit: open a connection through the native driver, run SELECT version(), and read the result back through the native driver's own result path, the same path a real query would use. On CockroachDB that probe hits the binary-COPY error, the native driver is released, the connection stays on ODBC, and last_error says so. Re-run with delegation left on, all fourteen PostgreSQL-wire databases pass all seven steps through adbcBridge.
Two things you can do about it on any version. First, you can always see who served a connection: adbc_get_info()["driver_name"] answers ADBC PostgreSQL Driver or ADBC ODBC Driver, and the option adbc.odbc.delegated_to names the native driver or answers odbc. Second, on 0.1.0 you can turn delegation off for one database or a whole deployment:
# off, for this database (Python DBAPI)
dbapi.connect(driver="odbc", db_kwargs={"uri": uri, "adbc.odbc.delegate": "never"})
# off, for a whole deployment
export ADBC_ODBC_DELEGATE=never
Windows users were never exposed. Delegation needs the ADBC driver manager's loader in the process, which is not implemented on Windows, so auto always takes the ODBC path there.
I mention the bug at some length because it is the useful part. A compatibility table that only lists other people's failures is marketing. The probe found a real defect in my own driver on the first run, and the same table that shows where the native drivers stop is what caught it.
What to do with this
If you are choosing a path for one of these 28 databases, the table gives you the answer, but the reasoning generalises to any database that sells itself as compatible with something.
- Your database is in the top row. Use the native driver. YugabyteDB, TimescaleDB, Citus and Cloudberry are PostgreSQL with extensions or a distributed layer, so binary COPY and
pg_catalogare the real thing. TiDB, Dolt, MatrixOne, SingleStore and OceanBase implement enough of MySQL's prepared-statement protocol and DDL. On the PostgreSQL-wire ones, adbcBridge'sautohands you to the native driver anyway, and on 0.1.1 it checks first; there is no MySQL delegation target, so the MySQL-wire ones stay on ODBC unless you call the Foundry driver yourself. - Your database is in the second row. Point adbcBridge at the vendor's ODBC driver. On 0.1.1 leave delegation on; the probe keeps you on ODBC where native cannot serve you. On 0.1.0 set
adbc.odbc.delegate=neverif the native PostgreSQL driver is installed in the same environment, or upgrade. - Your database is not on either list. The matrix covers 53 databases on Linux, 45 of them on macOS and 48 on Windows, through 24 ODBC drivers. If it has an ODBC driver, it is probably there; if it is not, the compatibility harness in the repository is how to add it.
- Test the client path, not the protocol label. “PostgreSQL-compatible” is a statement about psql and JDBC. Every client uses a different subset of the protocol, and the fast ones use the corners. The seven-step workload above answers the question for your client, whichever it is.
A last practical note. The fourteen failures in the second row are in the servers, not in the drivers and not in adbcBridge, and the two probes agree with the vendors' own documentation and with the notes on each matrix entry. Several are stated design decisions rather than bugs: CockroachDB's binary COPY and Spanner's primary-key rule among them. The findings that look unintended are recorded, with reproductions, in docs/UPSTREAM.md.
Where the details are
The exact first error from each native driver on each database, the driver versions, and the workaround are on the adbcBridge site: adbcbridge.org/notes/native-adbc-drivers-on-wire-compatible-databases. The per-database compatibility status across Linux, macOS and Windows is at adbcbridge.org/matrix. The code, the probe script and the fix are at github.com/singhpratech/adbcbridge.
The long tail of databases is long. The Foundry will keep adding native drivers for the databases with enough users to justify one, and that is the right outcome. Everything else will still have an ODBC driver, and that is where a bridge earns its keep.
Earlier in this series: Part 1 — what adbcBridge is and every number measured; Part 2 — 24 ODBC driver bugs found upstream; the Apache Arrow ADBC integrations listing. adbcBridge is an independent, Apache-2.0-licensed implementation of the ADBC standard, not an Apache project.
References & Citations
- adbcBridge (2026-09-05). “Where the native PostgreSQL and MySQL ADBC drivers stop on wire-compatible databases.” adbcbridge.org/notes — the probe, the per-database first errors, driver versions (adbc-driver-postgresql 1.12.0; ADBC Driver Foundry MySQL driver 0.6.0) and the workaround. Verified live (unauthenticated) 2026-09-05.
- adbcBridge (2026). Compatibility matrix — per-database status on Linux, macOS and Windows.
- adbcBridge v0.1.1 — GitHub release 2026-09-05 (crates.io the same day): native-driver probe at database initialisation before delegating; falls back to ODBC with the reason in
adbc.odbc.delegate.last_error. - adbcBridge (2026). “Native delegation.” docs/how-it-works/delegation.md, and “Options.” docs/reference/options.md —
adbc.odbc.delegate(auto / never / always),delegate.last_error,delegated_to, Windows behaviour. - adbcBridge (2026). “Benchmarks.” bench/BENCHMARKS.md — the 0.19 s native vs 0.57 s over psqlodbc figure: 1,000,000 rows of (int4, float8, varchar(20), date), PostgreSQL 16 in Docker, psqlodbc 16.00, Intel Core i9-13900HK, Linux, medians of 7–9 interleaved runs, machine otherwise busy.
- adbcBridge (2026). “Compatibility.” docs/COMPATIBILITY.md — 53 entries on Linux, 45 on macOS, 48 on Windows, 24 ODBC drivers, as of the 2026-09-03 batch.
- Apache Arrow (2026). “PostgreSQL Driver.” arrow.apache.org/adbc — the native driver's
COPY … (FORMAT binary)read path andpg_catalogtype resolution. - ADBC Driver Foundry — home of the community MySQL ADBC driver used in the probe.
- Singh, P. (2026-08-27). “I Built an Apache Arrow ADBC Driver for Every ODBC Database.” theaivibe.org — Part 1.
- Singh, P. (2026-08-30). “Your ODBC Driver Says SQL_SUCCESS and Lies: 24 Bugs Found in 13 Database Projects.” theaivibe.org — Part 2.
- Singh, P. (2026-09-01). “Apache Arrow ADBC Just Listed My ODBC Bridge on Its Official Integrations Page.” theaivibe.org.
- Apache Arrow (2026). “ADBC: Arrow Database Connectivity.” — the specification adbcBridge implements. adbcBridge is an independent implementation, not an Apache project.
Subscribe to new posts from theaivibe.org
Related Posts

Polars vs DuckDB vs ArrowMetal GPU on Apple Silicon: Sort and Group-By Benchmarks
Polars, DuckDB and ArrowMetal on an Apple M4 Max: sort and group-by benchmarks at 10M and 50M rows, wall time next to CPU time, and the rows where the CPU is still ahead.
Apache Arrow Compute on the Apple Silicon GPU: The First Arrow Project I Created That Does It, With 173 Operations Measured Against Polars, pyarrow and pandas
I built ArrowMetal, the first Apache Arrow project I could find that runs compute on the Apple silicon GPU. Apple silicon has one memory for CPU and GPU, and an Arrow buffer in shared Metal memory is already a GPU buffer; no Arrow project used that. ArrowMetal does: 307 of Arrow's 307 compute functions, seven languages, take at 24.2x pyarrow on an M4 Max, and 339 benchmark rows against the fastest CPU idiom of Polars, pyarrow, pandas and numpy, including the 77 where the CPU is still ahead.

Apache Arrow ADBC Just Listed My ODBC Bridge on Its Official Integrations Page — Seven Days After v0.1.0
The Apache Arrow ADBC documentation now lists adbcBridge on its Tools & Integrations page — seven days after I released v0.1.0. I filed the listing request on August 29; on August 31 a project member invited a PR, and it was merged six hours after the invitation. What the entry says, how the week that earned it went, and what it changes for anyone with an ODBC-only database.