← All notes

Notes · 2026-09-15

PostgreSQL columns declared with reduced precision now read at that precision

Through psqlodbc, TIMESTAMP(0), TIMESTAMPTZ(3) and TIME(3) columns arrived as microsecond Arrow types; adbcBridge 0.1.2 reads the declared unit, and the validation suite went from 265 to 293 of 327.

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 timestamptz column, because psqlodbc's only SQLGetTypeInfo row for the timestamp type is timestamptz and it takes no precision argument. Generated DDL on PostgreSQL now spells TIMESTAMP(n) for a zone-less column and TIMESTAMP(n) WITH TIME ZONE for a zoned one, with n from the Arrow unit, and TIME(n) for every time unit, TIME(0) included, since a bare PostgreSQL TIME is TIME(6).
  • adbc.connection.db_schema was 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.
  • GetObjects at schema depth reported every schema under a NULL catalog, because psqlodbc's SQLTables(SQL_ALL_SCHEMAS) leaves TABLE_CAT empty. The catalog is now filled from SQL_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.