Back to Blog

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

Prateek SinghSeptember 5, 202612 min read
“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 driveradbcBridge over ODBC
Both passPostgreSQL, YugabyteDB, TimescaleDB, Citus, Cloudberry, MySQL, MariaDB, Percona, MariaDB ColumnStore, TiDB, Dolt, MatrixOne, SingleStore, OceanBasesame 14
Native stops, ODBC passesCockroachDB, Materialize, QuestDB, RisingWave, openGauss, CrateDB, Spanner, ArcadeDB, YDB, Databend, GreptimeDB, Doris, StarRocks, MongoDB BI Connector
ODBC stops, native passesnone
One seven-step ADBC workload, 28 databases, 5 September 2026. Native = Arrow's PostgreSQL driver or the Foundry's MySQL driver; ODBC = adbcBridge over the vendor's own ODBC driver, delegation off.

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:

  1. connect — open the database and one connection;
  2. SELECT 1 — the smallest possible round trip through the driver's result path;
  3. DDL and a read — the eight-column CREATE TABLE from the matrix entry, two INSERTs, and a read of the rows back;
  4. bulk ingestadbc_ingest of 1,000 rows of int32, float64, utf8 and date32, followed by a COUNT(*) that must say 1,000;
  5. table schemaGetTableSchema on the ingested table;
  6. catalogGetObjects, the catalog and schema listing;
  7. 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:

DatabaseStepFirst error from the native driver
CockroachDBSELECT 1could not begin COPY: ERROR: unimplemented: binary format for COPY TO not implemented
CrateDBSELECT 1could not begin COPY: ERROR: line 1:6: no viable alternative at input 'COPY ('
YDBSELECT 1could not begin COPY: … RawStmt: alternative is not implemented yet : 138
openGaussSELECT 1ReadRecord failed at row 0: Unexpected end of input (expected 2 bytes but found 0)
Materialize, QuestDB, RisingWaveconnectthe type-resolver bootstrap SELECT oid, typname, typreceive, … FROM pg_catalog.pg_type is rejected
ArcadeDBconnectExpected 5 or 6 columns from type resolver pg_type query but got 0
SpanneringestPrimary key must be defined for table "adbc_ing_np" (reads work)
DatabendingestSyntaxException on the driver's generated CREATE TABLE … INT NULL
StarRocksingestUnexpected input 'NULL' on the same generated DDL
GreptimeDBDDL readinvalid decimal precision/scale (1023, 0); ingest: Missing time index constraint
DorisconnectOnly support prepare SelectStmt or InsertStmt now
MongoDB BI Connectorconnectrecv handshake response error: invalid connection attribute at index 0: EOF
The first error each native driver gave, verbatim, 5 September 2026. adbc-driver-postgresql 1.12.0 for the PostgreSQL-wire rows; the ADBC Driver Foundry MySQL driver 0.6.0 for Databend, StarRocks, GreptimeDB, Doris and MongoDB BI Connector.

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.

Where the native driver stops — 14 databases, by workload step connect 6 Materialize · QuestDB · RisingWave · ArcadeDB · Doris · MongoDB BI first SELECT 4 CockroachDB · CrateDB · YDB · openGauss DDL read 1 GreptimeDB bulk ingest 3 Spanner · Databend · StarRocks schema / catalog / read-back 0 Steps in workload order. The three steps after ingest never failed on a database that reached them.
Ten of the fourteen stop before or at the first query. Every failure after that is a CREATE TABLE the server would not accept.

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_catalog are 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's auto hands 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=never if 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

Subscribe to new posts from theaivibe.org

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

Related Posts

Polars vs DuckDB vs ArrowMetal GPU on Apple Silicon: Sort and Group-By Benchmarks
Data Engineering12 min read

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.

56 views
Read
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
Data Engineering11 min read

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.

95 views
Read
Apache Arrow ADBC Just Listed My ODBC Bridge on Its Official Integrations Page — Seven Days After v0.1.0
Data Engineering6 min read

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.

103 views
Read