Three ways Firebird's ODBC driver loses your parameters without saying so
Firebird ODBC — libOdbcFb.so on Linux, FirebirdODBC.dll on Windows, the same driver
answering SQL_DRIVER_NAME as OdbcFb and FirebirdODBC respectively — has three defects
in this area: two in parameter binding, one in statement attributes. All three reproduce on
3.0.1 and on 3.5.0-rc1, in plain ODBC, with plain-ODBC C programs and no ADBC in the picture.
1. Column-wise parameter arrays step fixed C types by BufferLength
The ODBC specification says BufferLength is ignored for fixed-length C types; the stride for
a column-wise array of them is sizeof(the C type). OdbcFb steps them by BufferLength
anyway, for SQL_C_SLONG, SQL_C_SBIGINT, SQL_C_DOUBLE, dates and timestamps alike. Every
row receives row 1's values, and the NULL indicator lands in the wrong row, with SQL_SUCCESS
throughout. SQL_C_CHAR arrays are correct.
There is no diagnostic to catch and no status element to check — the driver accepts
SQL_ATTR_PARAMSET_SIZE, executes what looks like the whole array, and the data is quietly
wrong. Row-wise binding (SQL_ATTR_PARAM_BIND_TYPE set to the row struct's size) works around
it; turning parameter arrays off entirely and sending one row per execute is the safer choice
for a general client, which is what the matrix entry does.
FirebirdSQL/firebird-odbc-driver#299, open.
2. A NULL BIGINT bound as character poisons the parameter for good
SQL_BIGINT is the one SQL type whose default C type in the ODBC specification is not the
matching SQL_C_SBIGINT but SQL_C_CHAR. So a NULL bound with SQL_C_DEFAULT on a
SQL_BIGINT parameter arrives as a character parameter. OdbcFb takes that literally, retypes
the parameter, and never re-derives it on the next SQLBindParameter: every later
SQL_C_SBIGINT rebind of that parameter writes NULL. The same pattern on INTEGER is fine.
What it looks like from the outside — every 64-bit integer after the first NULL in a column written as NULL, silently:
ingested [0, 1000000007, 2000000014, 3000000021, 4000000028, None, 6000000042, …]
read back [0, 1000000007, 2000000014, 3000000021, 4000000028, None, None, None, …]
The fix on the client side is to bind NULLs of SQL_BIGINT as SQL_C_SBIGINT explicitly
rather than letting SQL_C_DEFAULT resolve them, which is what a NULL of every other integer
type already resolves to. A test payload that puts a NULL in the last row will never catch
this; put a value after every NULL.
FirebirdSQL/firebird-odbc-driver#300, open.
3. SQL_ATTR_ROWS_FETCHED_PTR set before SQLPrepare is discarded
SQLPrepare and SQLExecDirect throw away SQL_ATTR_ROWS_FETCHED_PTR and
SQL_ATTR_ROW_STATUS_PTR set on the statement before them, so a block cursor never learns how
many rows a fetch returned. Set after them, both work. The order is the whole workaround.
FirebirdSQL/firebird-odbc-driver#301, open.
The DDL choice that decides how a string column reads
SQLGetTypeInfo(SQL_LONGVARCHAR) names BLOB SUB_TYPE TEXT, so a client that generates its
CREATE TABLE from the driver's own type table will spell an unbounded string column that
way. OdbcFb reads a BLOB one row at a time. On the language harnesses' first pass a
100,000-row table with such a column read back at 5.5k–7.7k rows/s, and odbc-api at the same
7.5k; spelling the column VARCHAR(8191) instead, all five agree at 259k–301k rows/s on
fetch and 34k–40k on ingest, where they had been 5–8k both ways. One earlier run put the same
contrast at 8,256 rows/s against 1,004,277, and ingest at 5,705 against 40,670.
That extreme could not be reproduced afterwards — on 2026-08-28 a BLOB column and a VARCHAR
column both read at 300k+ rows/s, in plain ODBC and through the bridge, on 3.0.1 and
3.5.0-rc1 — so treat the exact ratio as measurement-dependent rather than as a fixed property
of the driver. The DDL choice stands on its own merits regardless: a VARCHAR(8191) is legal
in any character set, can be filtered and indexed, and reads through ordinary bound columns.
Its price is that values over 8,191 characters are refused on insert.
Two more things to know
Firebird's dialect has no multi-row VALUES — -104 Token unknown at the second row-group's
comma — and no Oracle-style INSERT ALL. The third form it does take is
INSERT INTO t (cols) SELECT CAST(? AS <type>), … FROM RDB$DATABASE UNION ALL SELECT …. A
bare ? alone in a select list has no type Firebird can infer, hence the CAST. The engine
allows 256 relation contexts per statement, which caps such a statement at about 250
row-groups.
And there is no macOS build: the driver's releases ship Linux and Windows assets only, so the macOS column of the matrix records the entry as driver-unavailable rather than as a failure. Bootstrapping a test server has its own obstacle — Firebird 5's sample security database ships with no SYSDBA, and creating one needs an administrator.
Upstream
All three defects were found on 2026-08-28 and reported the same day, each with its plain-ODBC reproduction and both driver versions' output: FirebirdSQL/firebird-odbc-driver#299, FirebirdSQL/firebird-odbc-driver#300 and FirebirdSQL/firebird-odbc-driver#301. All three are open.
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/#firebird, and the finding is on file with its reproduction in docs/UPSTREAM.md of the repository.