Where PostgreSQL-wire servers part company with PostgreSQL
The same driver, seven servers, seven surprises
Speaking the PostgreSQL wire protocol is not the same as behaving like PostgreSQL. Running one identical workload through psqlodbc against seven servers that speak that wire — and against stock PostgreSQL 16 as the control — showed exactly where each of them parts company. Every item below was found in the 2026-08-25 campaign or the work around it; the ones that were filed were filed on 2026-08-29 and 2026-09-01.
CrateDB: one command tag for a whole multi-statement query
With autocommit off, psqlodbc sends BEGIN;<statement> as a single query string. PostgreSQL
answers with a CommandComplete per statement — BEGIN, then INSERT 0 1. CrateDB tagged
each result with the leading text of the whole string instead: BEGIN;INSERT 0,
BEGIN;INSERT 1. A tag-parsing client gets no row count, so SQLRowCount succeeded without
writing its out-parameter for any write or DDL inside a transaction.
Filed as crate/crate#20085. Fixed by crate/crate#20088, merged 2026-08-31 and shipping in 6.4.4.
QuestDB: a binary column truncated at its first NUL
A BINARY value round-trips only as long as it holds no 0x00. QuestDB returns a BYTEA
column with format code 1 — raw bytes — even in the simple query protocol, where PostgreSQL
always sends text. psqlodbc reads the field as a NUL-terminated string and hands back only
the bytes before the first 0x00, silently, under plain SQL_SUCCESS, whatever the C type
or binding: 0x0001 comes back empty, 0x010002 as 01. The server keeps the whole value —
its own length() reports the full length. psqlodbc is not alone: psycopg 3 over libpq reads
the same simple-query field as text too, since the protocol promises text there. Any bound
parameter, which forces the extended protocol, returns all three bytes.
Filed as questdb/questdb#7566; fix PR
questdb/questdb#7569 open. Found on the way:
DEALLOCATE ALL fails with
Cannot invoke "io.questdb.cutlass.pgwire.PGPipelineEntry.close()" because "pe" is null
even on a fresh autocommit connection with nothing prepared, while a named DEALLOCATE
works — questdb/questdb#7567, fix PR
questdb/questdb#7568 open.
Materialize: DEALLOCATE "Name" does not unquote
psqlodbc names every server-side statement _PLAN0x<statement-handle address> — mixed case,
so it must be quoted. Materialize does not unquote the identifier before looking it up, so
a statement prepared at the protocol level cannot be released:
26000 prepared statement "\"_PLAN0x7\"" does not exist
Inside a transaction that failure aborts it, which is the server half of a row-loss pair with a psqlodbc defect. PostgreSQL 16 releases the same statement. Filed with a psycopg side-by-side as MaterializeInc/materialize#38605; fix in review as MaterializeInc/materialize#38606, opened 2026-09-01, which unquotes prepared-statement, portal and cursor names before lookup.
Related: Two psqlodbc bugs that lost rows and narrowed integers, and two traps
RisingWave: a statement name outlives its statement
Over the extended protocol, RisingWave 3.0 keeps a server-side statement name after the
client closes the statement, so psqlodbc's _PLAN0x<handle> names collide the second time
the same handle address is reused:
XX000 Failed to prepare the statement: Duplicated statement name
The ordinary allocate/prepare/execute/free loop hits it on its second query, with or without
parameters; handles kept open concurrently get distinct names and all succeed, and
SQLExecDirect never collides. Setting psqlodbc's UseServerSidePrepare=0 — the simple
protocol has no statement names — avoids it entirely. Documented here, not yet filed.
ArcadeDB: a timestamp literal stored as NULL
'2024-02-29 13:45:10.123456' written into a DATETIME_MICROS property is stored as
NULL, silently, under SQL_SUCCESS. The ISO-8601 form '2024-02-29T13:45:10.123456'
round-trips to the microsecond. This is not an exotic corner: the space form is exactly how
psqlodbc renders a bound SQL_TYPE_TIMESTAMP parameter, so an ordinary bound timestamp hits
it. Write timestamps as ISO text against this server. In the same family, a NULL BOOLEAN
arrives over the wire as false with a non-NULL indicator.
Spanner's emulator: a killed client wedges it
A read-write transaction left open by a client that was killed wedges the Spanner emulator:
every later connection is refused with FATAL: UNAVAILABLE. It was recorded when a
benchmark run hung after the compatibility pass (the emulator reporting
cross-database references are not implemented) and killing it left nothing able to
connect. The entry's own passes were recorded on fresh emulators; recreate it.
YDB: a driver-version incompatibility
psqlodbc 18.00.0002 sends SHOW DateStyle; at connect, and YDB's PostgreSQL layer answers
unrecognized configuration parameter "datestyle", so the connect fails. psqlodbc 16 does
not issue it, and the YDB entry passes through 16 everywhere.
Related: Two psqlodbc bugs that lost rows and narrowed integers, and two traps
Where this comes from: the adbcBridge compatibility matrix runs one workload through every database's ODBC driver on Linux, macOS and Windows and records each failure with its first error; the row for this database is at https://adbcbridge.org/matrix/#cratedb, and the finding is on file with its reproduction in docs/UPSTREAM.md of the repository.