Where the native PostgreSQL and MySQL ADBC drivers stop on wire-compatible databases
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.