Firebird into Apache Arrow with ADBC
Firebird 5, read and written through adbcBridge — the ADBC driver that loads the database's ODBC driver — verified on Linux and Windows x64. Everything below is the compatibility matrix's record for this row.
Status
PASS
driver unavailable: firebird-odbc-driver v3-0-1 ships Linux and Windows assets only
PASS (Firebird 5.0.4 container; Firebird ODBC 3.0.1.18 Firebird ODBC Driver with fbclient.dll from the Firebird 5 zip on PATH) — one bridge fix needed: the driver answers SQL_DRIVER_NAME FirebirdODBC on Windows where Linux sees OdbcFb, so the quirk block that turns parameter arrays off (the driver accepts SQL_ATTR_PARAMSET_SIZE and executes one set) never fired and the first run stopped with ODBC driver accepted a parameter array of 2 sets but reported neither SQL_ATTR_PARAMS_PROCESSED_PTR nor SQL_ATTR_PARAM_STATUS_PTR; matching both names (src/odbc_driver.c) makes the whole workload pass, UNION-ALL bulk form included
Driver and connection string
- ODBC driver
- Firebird ODBC 3.5.0-rc1
- Wire
- native ODBC
- adbcBridge
- 0.1.1 (release)
The connection string the matrix used, as the connection strings reference records it (Databases with their own native protocol and ODBC driver); {drv} is the driver library or its name from odbcinst.ini, and the host, port and credentials are the test server's.
Driver={drv};DBNAME=inet://127.0.0.1:13050//var/lib/firebird/data/adbc.fdb;UID=adbc;PWD=adbc;CHARSET=UTF8;
Python
import adbcbridge
# {drv}: the ODBC driver library (path), or its name from odbcinst.ini
conn = adbcbridge.connect(uri="Driver={drv};DBNAME=inet://127.0.0.1:13050//var/lib/firebird/data/adbc.fdb;UID=adbc;PWD=adbc;CHARSET=UTF8;")
with conn.cursor() as cur:
cur.execute("SELECT * FROM my_table")
table = cur.fetch_arrow_table() # pyarrow.Table
Polars and pandas
import polars as pl, pandas as pd
df = pl.read_database("SELECT * FROM my_table", connection=conn) # Polars, Arrow-native
pdf = pd.read_sql("SELECT * FROM my_table", conn) # pandas 2.2+, ADBC connection
pip install adbcbridge brings the driver library; the Firebird ODBC driver is installed the way its vendor documents, then named in Driver=. Rust, Go, C# and Java use the same connection string through their ADBC driver managers — see the docs.
What the matrix recorded
SQL_C_WCHAR sized in 4-byte wchar_t; parameter arrays are off (no_param_arrays): with column-wise binding OdbcFb steps fixed-length C types at the BufferLength stride, so every row receives row 1's values with SQL_SUCCESS throughout (firebird-odbc-driver#299, reproduced in plain ODBC on 3.0.1 and 3.5.0-rc1).
What this stack needed
- Firebird's dialect has neither multi-row
VALUES(-104Token unknownat the second row-group's comma) nor Oracle'sINSERT ALL, so bulk ingest batches through the third form it does take:INSERT INTO t (cols) SELECT CAST(? AS <type>), ... FROM RDB$DATABASE UNION ALL SELECT ...(multirow_union_from, probed like every other form and only after the standard one is refused). - The
CASTis not a guess and cannot lose anything -- it names the type ingest itself would create for that Arrow type, so a value too wide for the target column raisesstring right truncationon the INSERT's own assignment exactly as a one-row INSERT would; a column with no such exact type gets no batching. - Firebird's 256-context limit caps it at ~250 row-groups.
- Also handled here: once a NULL has been bound to a
SQL_BIGINTparameter withSQL_C_DEFAULT(whose ODBC default C type isSQL_C_CHAR), OdbcFb writes NULL for every laterSQL_C_SBIGINTrebind of that parameter (#300) -- NULLs are boundSQL_C_SBIGINT(NullParamCType). SQL_ATTR_ROWS_FETCHED_PTRset beforeSQLPrepareis discarded (#301); the bridge sets it after execute and was not affected.- Generated ingest DDL used to spell an Arrow string as
BLOB SUB_TYPE TEXT-- whatSQLGetTypeInfo(SQL_LONGVARCHAR)names. - One run read a 100,000-row table with such a column at 8,256 rows/s against 1,004,277 without it; that slowdown could not be reproduced afterwards (2026-08-28: BLOB and
VARCHARcolumns both read at 300k+ rows/s, plain ODBC and through the bridge, driver 3.0.1 and 3.5.0-rc1), so it is not recorded as a driver property. - Strings go in as
VARCHAR(8191), legal in any character set (ddl_string_type_name), because the column can then be filtered and indexed and reads through ordinary bound columns; the same table ingested at 40,670 rows/s instead of 5,705 in that run. - Values over 8,191 characters are refused on insert, as Db2's
VARCHAR(32672)refuses its last 28 bytes.
adbc.odbc.rows_per_insert=1) on the compat table, fetch 294k rows/s