← All notes

Notes · 2026-09-05

Where the native PostgreSQL and MySQL ADBC drivers stop on wire-compatible databases

One ADBC workload through the native PostgreSQL and MySQL ADBC drivers and through adbcBridge on 28 PostgreSQL-wire and MySQL-wire databases - 14 pass both ways, 14 pass only over ODBC - plus a delegation defect this run found in adbcBridge itself and its workaround.

Fourteen databases in the matrix speak the PostgreSQL wire protocol without being PostgreSQL, and fourteen speak MySQL's without being MySQL. Each of those has a native ADBC driver that can be pointed at it: the Apache Arrow project's PostgreSQL driver (adbc-driver-postgresql 1.12.0) and the ADBC Driver Foundry's MySQL driver (0.6.0). On 2026-09-05 the same seven-step workload ran through both native drivers and through adbcBridge over each database's own ODBC driver, with delegation turned off so the comparison is native protocol against ODBC:

connect · SELECT 1 · the entry's eight-column CREATE TABLE plus two INSERTs and a read · adbc_ingest of 1,000 rows (int32, float64, utf8, date32) with a count check · GetTableSchema · GetObjects · read the ingested table back.

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 —

Where the native driver stops, and the first error it gives:

Database Step Error
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 — answered by ArcadeDB PR #7179 the same day, see below
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

Same-day follow-through from ArcadeDB. Within an hour of the follow-up comment, ArcadeDB's maintainer opened #7178 with their own diagnosis (the emulated pg_type knew neither typreceive nor typsend, and its WHERE parser took a single equality only) and a fix, PR #7179. Built from that PR's CI artifact and probed again the same evening: the native driver connects, builds its type resolver, and GetObjects lists the types at every depth. With the driver's defaults it then stops at the first result set, because ArcadeDB has no COPY (…) TO STDOUT (FORMAT binary), the same stop as CockroachDB, CrateDB and YDB above. With the driver's adbc.postgresql.use_copy=false option, which reads through PQexecParams in binary format instead, SELECT works end to end on that build with values decoded correctly; GetTableSchema was the one call left, its $1::regclass::oid lookup answered with zero rows (#7180); the maintainer fixed that too in #7187, merged the next morning and verified on its build (five and eight fields with the right Arrow types), and split binary COPY into #7188, which the maintainer implemented in #7398 on 2026-09-10; the one framing detail left (the binary header sent as its own CopyData message, where PostgreSQL bundles it with the first row) was fixed in #7607 on 2026-09-15, and on the 26.10.1-SNAPSHOT build that carries it the native driver passes the whole probe with its defaults. Every row in the table above is measured with the drivers' defaults; the section below re-measures the COPY rows with that option. The fix merged on 2026-09-06 and ships in ArcadeDB 26.10.1 (October); the ArcadeDB row will say "connects" once that release is out.

None of these is a defect in the native drivers as such. The PostgreSQL driver reads every result through COPY … TO STDOUT (FORMAT binary) and learns types from pg_catalog, which is the fastest possible path against a real PostgreSQL server and simply is not implemented by the databases that only emulate its wire protocol. The MySQL driver generates MySQL DDL and uses server-side prepared statements, which Doris, StarRocks and Databend do not accept. The ODBC drivers those vendors ship know their own servers; adbcBridge inherits that. The two probes agree with the vendors' own documentation and with the entry notes in the matrix. The probe script and the raw results are kept with the compatibility sources.

Re-measured with the COPY read path turned off (2026-09-06)

The PostgreSQL driver's adbc.postgresql.use_copy statement option (default true) switches result reads from COPY (…) TO STDOUT (FORMAT binary) to PQexecParams with binary result format. The same seven steps, same servers, native driver only, with that option set to false on every statement (bulk ingest still goes through COPY … FROM STDIN, which the option does not cover):

Database Native driver, use_copy=false What changed
CockroachDB passes all seven steps the only stop was the COPY read path
openGauss passes all seven steps the only stop was the binary COPY decode
CrateDB connect, SELECT, DDL and reads pass; ingest and GetTableSchema stop ingest: CrateDB's COPY … FROM STDIN grammar (missing '=' at 'binary'); schema: $1::regclass::oid (Cannot find data type: oid)
YDB connects and every query returns rows, but each column arrives as an opaque binary extension type (unnamed<oid:23>, unnamed<oid:701>); ingest and GetTableSchema stop YDB's pg_type does not describe its own types the way the driver's resolver needs, so nothing is decoded
ArcadeDB (fix build, PR #7179) connect, SELECT, reads and GetObjects pass; GetTableSchema stops described above

So for two of the fourteen the native driver is a one-option fix, and for a third (CrateDB) a read-only one. That is worth knowing before choosing a path: on CockroachDB and openGauss the native driver with use_copy=false and adbcBridge over psqlodbc both do the whole workload, and the choice is about ingest speed (COPY is what makes the native driver's bulk path fast, and it is exactly the part these servers lack), type mapping and what else you connect to. adbcBridge in auto mode keeps its own probe as measured: a SELECT version() through the native driver with its defaults, so on these servers a connection still lands on ODBC unless you pass the option through yourself.

A defect in adbcBridge this run found

adbcBridge can hand a connection to a native ADBC driver instead of opening ODBC at all (adbc.odbc.delegate, default auto). With adbc-driver-postgresql importable in the same environment, auto delegates every connection whose ODBC driver is psqlodbc, and it decides that before knowing what the server is. On CockroachDB, CrateDB, openGauss and YDB the native driver connects and the first SELECT then fails as in the table above; on Spanner reads work but adbc_ingest fails because the native driver's CREATE TABLE has no primary key (adbcBridge's own ingest adds one). Where the native driver cannot connect at all (Materialize, QuestDB, RisingWave, ArcadeDB) auto falls back to ODBC as designed.

Found on this run, fixed the same day. 0.1.1, released 2026-09-05, probes the native driver before handing a connection over — one SELECT version() through it at AdbcDatabaseInit, read back through its own result path — and stays on ODBC when that fails, with the reason in adbc.odbc.delegate.last_error. Re-run after the fix with delegation left on, all fourteen PostgreSQL-wire databases pass all seven steps through adbcBridge.

On a 0.1.0 install the workaround is one database option:

adbc.odbc.delegate=never

(adbcbridge.connect(..., delegate="never") in Python, or ADBC_ODBC_DELEGATE=never in the environment). The matrix's PostgreSQL-wire rows describe the ODBC path either way, which is what they always measured.