PostgreSQL columns declared with reduced precision now read at that precision
What a reader would see
Before 0.1.2, a PostgreSQL column declared TIMESTAMP(0) came back from adbcBridge as
timestamp[us], TIMESTAMPTZ(3) as timestamp[us, tz=UTC], and TIME(3) as time64[us].
The values were exact; only the Arrow unit was finer than the column's declaration. The
ADBC Driver Foundry validation suite, which checks the Arrow type of every result against the
declared SQL type, counted 28 failures on PostgreSQL for it, in six groups, on 2026-09-06.
From 0.1.2 the unit matches the column: TIMESTAMP(0) and TIMESTAMPTZ(0) read as
timestamp[s], TIMESTAMPTZ(1..3) as timestamp[ms, tz=UTC], TIME(0) as time32[s] and
TIME(1..3) as time32[ms]. This is the one behaviour change in the release: code that
assumed every PostgreSQL timestamp arrives as microseconds sees the declared unit instead,
with the same values.
Why the driver did not believe the column
psqlodbc reports a TIMESTAMP(0) column truthfully, as scale 0 with the 19-character size of
YYYY-MM-DD hh:mm:ss. The reader treated that pair as unreliable on purpose: MySQL
Connector/ODBC reports exactly the same 0 and 19 for a DATETIME(6) column, and reading that
as whole seconds would silently drop every fraction. Microseconds lose nothing, so they were
the default whenever scale 0 could not be cross-checked against the size.
The fix keeps that caution and narrows it. Only psqlodbc against PostgreSQL itself, identified by
the same version() test the array-ingest path already uses, is trusted on a 0 / 19 pair.
MySQL and MariaDB keep the microsecond default, and so do the PostgreSQL-wire servers that are
not PostgreSQL. A wrong "seconds" loses data; a wrong "microseconds" does not, which is why the
gate points that way.
The zoned path had a second, simpler gap: it always produced microseconds, whatever scale was
reported. It now takes its unit from the scale, and the text parser scales with it. Queries,
ExecuteSchema and GetTableSchema classify columns through the same function, so all
three agree.
Ingest, the current schema, and the catalog name
Three more findings from the same run are in 0.1.2:
- Bulk ingest of a zone-less Arrow timestamp created a
timestamptzcolumn, because psqlodbc's onlySQLGetTypeInforow for the timestamp type istimestamptzand it takes no precision argument. Generated DDL on PostgreSQL now spellsTIMESTAMP(n)for a zone-less column andTIMESTAMP(n) WITH TIME ZONEfor a zoned one, withnfrom the Arrow unit, andTIME(n)for every time unit,TIME(0)included, since a bare PostgreSQLTIMEisTIME(6). adbc.connection.db_schemawas not implemented. ODBC has no attribute for the current schema the way it has one for the catalog, so the option asks the server in its own words,SELECT current_schema(), on PostgreSQL-wire servers and DuckDB.GetObjectsat schema depth reported every schema under a NULL catalog, because psqlodbc'sSQLTables(SQL_ALL_SCHEMAS)leavesTABLE_CATempty. The catalog is now filled fromSQL_ATTR_CURRENT_CATALOG.
The numbers
| Run | PostgreSQL 16 via psqlodbc | SQLite via SQLiteODBC |
|---|---|---|
| 2026-09-06 | 265 pass, 28 fail, 34 skip | 212 pass, 69 fail, 46 skip |
| 2026-09-15 | 293 pass, 0 fail, 34 skip | 212 pass, 69 fail, 46 skip |
The 34 PostgreSQL skips are declared server limits: nanosecond precision, negative decimal
scale, and a 23:59:59.999999999 that rounds to 24:00:00. The SQLite failures are unchanged,
test for test, because SQLiteODBC reports scale 0 and size 32 for every TIME and TIMESTAMP
column whatever was declared, and SQLite has no decimal or float32 type; nothing there is the
driver's to fix without guessing.
Every other database in the compatibility matrix keeps its exact behaviour; 42 entries were run on the same build before the tag, and the two PostgreSQL test files now run in CI against a PostgreSQL 16 service on every push, on top of the Linux, macOS and Windows runs recorded on the pull request.
Also in 0.1.2, found by the Windows run
On Windows, PostgreSQL bulk ingest bound its array literals as narrow text, and the Unicode
psqlodbc read those bytes through the ANSI code page: héllo was stored as héllo. The
literals are now bound wide, as the row-at-a-time path always was. psqlodbc also receives
LFConversion=0 on Windows, where its default rewrites every LF in fetched text to CR LF;
Linux and macOS never had that default.
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/#postgres, and the
validation-suite results, test by test, are in tests/validation/RESULTS.md of the repository.