A NULL column that reads back as the previous row's value
The symptom is quiet and specific. You fetch a block of rows, and a column that is NULL in
row 2 comes back holding row 1's text, padded out to the bound width. A NULL FLOAT8
comes back as 1e-323. A NULL date comes back as year 0.
What is actually happening
The driver was compiled with a 32-bit SQLLEN/SQLULEN on 64-bit Linux, while
unixODBC, pyodbc and the calling application all use an 8-byte one. Every SQLLEN the
driver writes is half as wide as the caller's storage, in two different ways:
- indicator and length arrays bound with
SQLBindColcome back asint32[]with stride 4, so NULLs go undetected and string lengths are garbage past the first row — the classic shape being row 2's string being row 1's text followed by NUL bytes; - scalar out-parameters —
SQLRowCount,SQLDescribeCol's column size,SQLColAttribute's numeric attribute,SQLGetData'sStrLen_or_Ind,SQL_ATTR_ROWS_FETCHED_PTR,SQL_ATTR_PARAMS_PROCESSED_PTR— get only their low four bytes, so the negative sentinelsSQL_NULL_DATA(-1) andSQL_NO_TOTAL(-4) surface as4294967295and4294967292.
Reading a single column one row at a time mostly looks correct, which is why simple probes pass and the damage only shows up with several columns or several rows per fetch.
Which drivers
IBM's Db2 CLI driver. The clidriver package's libdb2.so is built this way; IBM's
64-bit-SQLLEN build is the separate libdb2o.so, shipped only in the full server and
data-server-driver packages. The same libdb2.so is how Informix is reached over DRDA,
so Informix inherits it exactly.
Actian Ingres. libiiodbcdriver.1.so has a four-byte SQLLEN on 64-bit Linux too,
found on 2026-09-03 when Ingres 10.1 was added: a NULL VARCHAR came back as row 1's text
padded out to the bound width, a NULL FLOAT8 as 1e-323, a NULL ANSIDATE as year 0.
MDB Tools, the Microsoft Access driver, writes bound-column indicators four bytes wide
and leaves the high half untouched, so after SQLFetch a NULL column reads
0x……ffffffff and every NULL looks like a 4 GB value — the same shape, from a completely
unrelated driver.
Clients that assume the standard width do not survive it and say so in their own
vocabulary. Rust's arrow-odbc panics while describing a column:
Failed to retrieve data type from ODBC driver. The SQLLEN could not be converted
to a 16 Bit integer
and .NET's System.Data.Odbc raises Arithmetic operation resulted in an overflow. on
the same result set, while Go's alexbrainman/odbc reports
SQLGetDiagRec failed: ret=-2.
What to do about it
There is no fix on the driver side to wait for; the layout is what the vendor shipped. A
client has to read every driver-written SQLLEN at four-byte width — sign-extending the
scalars, and walking indicator arrays with stride 4 — and zero-initialise those
out-variables before the call, so a driver that answers without writing is not read as a
stale value.
adbcBridge does this behind one flag, sqllen_32bit, documented in
docs/reference/quirks.md. It is autodetected from SQL_DRIVER_NAME: the quirk turns on
when the name contains db2 (excluding libdb2o/db2o., IBM's wide build), mdbtools
or iiodbcdriver. The option adbc.odbc.sqllen_32bit pins it either way, on the database,
connection or statement — useful for any other driver built the same way, and useful for
confirming that the quirk is what is fixing a result set. The fast path is unchanged when
it is off.
The related case: Virtuoso on Win64
There is a narrower version of the same bug. OpenLink Virtuoso's virtodbc.dll 7.2.17 on
Windows x64 strides the bound-column indicator array by 4 bytes per row on a block
cursor, while the rest of its SQLLEN use is normal. It is a read-side stride only, not a
whole-driver width mismatch, and it is carried by its own flag (ind_stride_32bit) rather
than by sqllen_32bit. The same driver also advertises the SQL_GD_* extensions and then
fails SQLSetPos(SQL_POSITION) on a block cursor, so a truncated value cannot be repaired
the documented way. Both are recorded in bench/BENCHMARKS-windows.md.
No vendor issue is filed for the SQLLEN width itself, so there is no fix version to
name: the Db2, Informix and Ingres cases are recorded with their conditions in
docs/COMPATIBILITY.md and docs/reference/quirks.md, and the Virtuoso stride sits in
the "documented here, not yet reported" table of docs/UPSTREAM.md.
Related: Five things OpenLink Virtuoso's ODBC driver does silently and wrongly
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/#db2, and the finding is on file with its reproduction in docs/UPSTREAM.md of the repository.