adbcBridge is documented upstream as the way to reach a database over ADBC when it has an ODBC driver but no native ADBC one.
Integrated in omniload since 2026-09-17, as its fourth SQL backend, shipping in v0.17.0.
Every ODBC driver, an Arrow driver.
adbcBridge is one plain-C shared library that turns whatever ODBC driver is on your machine into an ADBC driver β columnar Arrow batches out, bulk ingest in, from Python, Rust, Go, Java, C# and R. No per-database driver to write, no rows to convert by hand.
ODBC (Open Database Connectivity) is the decades-old C API almost every database ships a driver for; ADBC (Arrow Database Connectivity) is the newer C API whose result sets are Apache Arrow columnar batches instead of row-at-a-time buffers. adbcBridge is the piece in between.
- You need Arrow-native access to a database that has no native ADBC driver, and you would rather not hand-roll ODBC-to-Arrow conversion.
- You already have ODBC drivers installed and configured, and want every language's ADBC client to use them.
- You are building a data tool and want one code path that reaches hundreds of databases instead of one integration per vendor.
Where a database already has a first-class native ADBC driver (PostgreSQL, SQLite, DuckDB, and others), adbcBridge can hand the whole connection over to it: native delegation.

Rows on the near bank, columns on the far one. Nothing is lost on the way across.
The long tail of databases has an ODBC driver. Now it has an Arrow one.
Native ADBC drivers exist for a handful of databases. The other few hundred β Db2, Oracle, Informix, Vertica, Firebird, Access, every warehouse behind a PostgreSQL or MySQL wire β ship an ODBC driver and nothing else. adbcBridge sits between the ADBC driver manager and that ODBC driver, and does the columnar work once, in C, for all of them.
Where a native ADBC driver is installed (PostgreSQL, SQLite, DuckDB), the same install hands the connection to it, so you get native speed without changing a line.
Python / R / Go / Rust / Java / C#
β ADBC driver manager
βΌ
libadbc_driver_odbc.so β adbcBridge
β ODBC API (unixODBC / iODBC / Windows DM)
βΌ
Db2 Β· Oracle Β· SQL Server Β· Vertica Β· Informix Β·
Access Β· Snowflake Β· Redshift Β· SQLite Β· anything
with an ODBC driver
Three figures, each with its conditions attached.
Measured on four ordinary machines, never idle:
- Linux laptop β Linux Mint 22.3, Core i9-13900HK, 14 cores, 31 GiB, unixODBC 2.3.12; the 53-database fleet on the same box β Docker containers and local files, Db2 for i alone on a public host β, ~23 GiB of other work resident.
- Apple M4 Max β 16 cores, 64 GB, load 2.4β10.5 during runs, some servers under amd64 emulation.
- Windows 11 laptop, Core i9-13900HK β 14 cores, 32 GB, Docker Desktop on WSL2, up to eight benchmark runs and ~30 idle containers at once; the full 46-entry re-measure.
- Windows 11 laptop, Core i7-8550U β 4 cores, 7.7 GB, 26% run-to-run variance; the first campaign. Both Windows builds without prefetch or fan-out.
Cross-machine numbers are not comparable; each benchmark file opens with its host state.
INSERT writes twice the WAL of a COPY.| Database (AβZ) | Linux | macOS arm64 | Windows x64 |
|---|---|---|---|
| β53 | β45β7β1 | β48β3β2 |
53 databases, one workload, three operating systems: Linux 53 pass; macOS 45 pass, 7 without a vendor driver for the OS, 1 whose server cannot run on that machine; Windows 48 pass, 3 recorded with the driver-side reason, 2 without a vendor driver. Every row below deep-links to its matrix entry.
Show all 53 rows
- βpasses the full compat workload
- βfails β hover the cell for the error, or open the row in the matrix
- βthe vendor ships no driver for this OS
- βthe server cannot run on this machine
- Β·not attempted β hover for why, or open the row in the matrix
Every cell is a recorded fact from docs/COMPATIBILITY.md; all three columns are closed; every non-pass names its reason.
Method (Linux and macOS): the PostgreSQL ratio is the mean of three interleaved runs, a fresh process per timed run, sides alternated so load drift lands on both, every read checksum-compared against a reference; per-database and per-language rates are the median of three. The Windows column is single samples under contention, read as floors. Full tables, spreads and the host's load average for each: bench/. Full benchmark tables.
Block cursors in, Arrow buffers out, and a few things only this driver does.
Reads
Columns are bound once with SQLBindCol into rowsets of up to 8 MiB and copied into Arrow buffers column-at-a-time; UTF-16 becomes UTF-8, decimals become decimal128, longs are chunked through SQLGetData. A column a driver describes as "unbounded" is bound at 2 KiB and re-read only when a value overflows, so one wide column no longer collapses the whole read to a row at a time.
Partitioned reads
AdbcStatementExecutePartitions splits one query into K slices β by ctid on a PostgreSQL heap, by key range on a leading primary-key column anywhere else, by yb_hash_code() on YugabyteDB β and reads them on K connections. The key column's NOT NULL comes from the catalog, never from "it is a primary key", so no row can fall between slices.
Writes
Bulk ingest binds parameter arrays where the driver honours them, and otherwise batches the stream into multi-row INSERT β¦ VALUES, Oracle's INSERT ALL, or Firebird's UNION ALL SELECT β each form probed once per connection, and only after the plain form is refused. On PostgreSQL it sends INSERT β¦ SELECT FROM unnest(arrays), 10,000 rows a statement, across up to 64 connections.
Delegation
adbc.odbc.delegate=auto looks for a native ADBC driver for the database behind the connection string and hands the whole driver over when one is found. The connection you configured is the connection you get; nothing is re-parsed.
Download the release, or build from source.
v0.1.3 carries the driver library for Linux x86_64 and aarch64, macOS arm64 and Windows x64, a Python wheel for each, the source distribution, the Rust crate, the NuGet package and the Maven jar with its sources and javadoc jars β fourteen files, every one built and tested by the release workflow from the tagged commit, with a SHA256SUMS to check them against and the release key's signature of it (SHA256SUMS.asc). Releases after v0.1.0 add a build-provenance attestation per asset (gh attestation verify) and an SPDX SBOM; the security policy, the support window and the private reporting channel are in Security and verification.
Linux verified
sudo apt install unixodbc-dev cmake git clone https://github.com/singhpratech/adbcbridge cd adbcbridge ./install.sh # into ~/.local, no root pip install adbc-driver-manager pyarrow
The reference host: Linux Mint 22.3 (Ubuntu 24.04 base), unixODBC 2.3.12, the 53-database matrix on the same machine (Docker containers and local files; Db2 for i alone on a public IBM i host over TLS). install.sh builds the library into ~/.local and writes the driver manifest the ADBC driver manager already searches, so driver="odbc" resolves with nothing else set. Step by step: docs/getting-started/install-linux.md.
macOS verified
brew install unixodbc cmake sqliteodbc cmake -S . -B build \ -DCMAKE_PREFIX_PATH="$(brew --prefix unixodbc)" cmake --build build
Apple M4 Max, macOS 26.5, arm64: 45 of 53 databases pass the compat workload, Oracle, Db2 and Informix on the vendors' arm64 drivers among them; vendor drivers that ship only iODBC-width builds run through a bridge built against iODBC. SQLite reads 2.3 M rows/s, DuckDB 4.2 M. The eight entries without a macOS driver or a runnable server are recorded with the reason. Details: bench/BENCHMARKS-macos.md, docs/getting-started/install-macos.md.
Windows verified
cmake -S . -B build cmake --build build --config Release # -> build\Release\libadbc_driver_odbc.dll
Windows 11 x64, MSVC 2022, the OS's own driver manager: 48 of 53 databases pass the complete workload under Docker Desktop on WSL2; the five remaining cells are recorded one by one: three driver-side failures, two databases whose vendor ships no Windows driver. CI builds and tests 64- and 32-bit Windows on every push to main and every pull request. Five Windows-only driver quirks are handled in the bridge β Firebird's driver name, Informix parameters, Arrow Flight SQL's text path (read as binary, 1.8Γ faster), Apache Ignite's narrow UTF-8 route and OpenLink Virtuoso's 4-byte indicator stride. The prefetch pipeline and parallel ingest are compiled out on Windows until a Win32 thread shim lands (on the roadmap). Details: bench/BENCHMARKS-windows.md, docs/getting-started/install-windows.md.
First query
The shortest working program, from Your first query: connect through the bridge by name, run a query, get a pyarrow.Table.
import adbc_driver_manager.dbapi as dbapi
import pyarrow as pa
# 1. Connect through the bridge to a SQLite file. `driver="odbc"` resolves the
# bridge by name from the manifest an install wrote; `uri` is an ODBC
# connection string.
conn = dbapi.connect(
driver="odbc",
db_kwargs={"uri": "Driver=SQLite3;Database=/tmp/first.db;"},
)
# 2. Run a query and get the result as a pyarrow.Table.
with conn.cursor() as cur:
cur.execute("SELECT 42 AS answer, 'hello' AS greeting")
table = cur.fetch_arrow_table() # pyarrow.Table
print(table)
The same program in Rust, C#, Java, Go and C, plus parameters and bulk ingest: Your first query.
One binary, loaded by name from five languages, and R.
Each binding dlopens the same libadbc_driver_odbc.so and calls AdbcDriverInit. The numbers are PostgreSQL 16, 10,000 rows ingested in one transaction and 100,000 read back into that language's own Arrow batches, adbc.odbc.delegate=never so every row really travels over ODBC.
| Language | ADBC binding | ingest rows/s | fetch rows/s | Name the driver |
|---|---|---|---|---|
| Python | adbc_driver_manager.dbapi | 278,075 | 1,892,520 | dbapi.connect(driver="odbc", db_kwargs={"uri": "Driver=SQLite3;Database=my.db;"}) |
| Rust | adbc_driver_manager crate | 482,822 | 2,078,465 | ManagedDriver::load_from_name("odbc", None, AdbcVersion::V110, LOAD_FLAG_DEFAULT, None) |
| Go | go/adbc/drivermgr | 395,941 | 1,987,447 | drivermgr.Driver{}.NewDatabase(map[string]string{"driver": "odbc", "uri": ...}) |
| Java | adbc-driver-jni | 450,072 | 1,617,704 | JniDriver.PARAM_DRIVER.set(params, "odbc") |
| C# | Apache.Arrow.Adbc | 401,694 | 1,937,816 | AdbcDriverManager.FindLoadDriver("odbc") |
| R | adbcdrivermanager | β | β | adbc_database_init(adbc_driver("/path/to/libadbc_driver_odbc.so", entrypoint = "AdbcDriverInit"), uri = ...) |
Five packages, one library inside each.
Each package finds the library the same way β an explicit override (ADBC_ODBC_DRIVER everywhere, ADBCBRIDGE_LIBRARY too in Rust, C#, Java and Go), the copy shipped inside the package, the ADBC driver manifest, the usual install directories β and Rust, C#, Java and Go list every place they looked in the error. All five are built and tested in the repository and attached to every release β v0.1.0 was the first β and published on PyPI, crates.io, NuGet, Maven Central and as a Go module.
pip install adbcbridgeOn PyPI, and on the release page. The library rides inside a py3-none-<platform> wheel for Linux x86_64 and aarch64, macOS arm64 and Windows x64 β one wheel for every Python version, no extension module.
adbcbridge.connect(uri="Driver=SQLite3;Database=my.db;") cargo add adbcbridgeThe crate carries the C sources; cargo build compiles the driver on the spot (unixODBC headers needed), and a test keeps the bundled copy in sync with the repository.
let driver = adbcbridge::load()?;dotnet add package AdbcBridgenetstandard2.0 and net8.0; the library ships as runtimes/<rid>/native assets and loads through Apache.Arrow.Adbc.
Driver.Connect("Driver=SQLite3;Database=my.db;") org.adbcbridge:adbcbridge:0.1.3 Over adbc-driver-jni; the natives live inside the jar and are extracted once per JVM. Sources and javadoc jars included.
AdbcBridge.open(allocator, "Driver=SQLite3;Database=my.db;", Map.of()) go get github.com/singhpratech/adbcbridge/go Over drivermgr (cgo). A Go module cannot carry a prebuilt library, so it comes from install.sh, a release binary or the manifest β or a build tag embeds one.
adbcbridge.Open(ctx, alloc, "Driver=SQLite3;Database=my.db;", nil) R has a smoke test but no benchmark yet. The 53-database version of this table, per language, is bench/LANGUAGE_BENCHMARKS.md. Note that Java here is Java as a client of this ODBC driver (through adbc-driver-jni); the JDBC bridge on the roadmap β reaching JDBC-only databases from any language β is a separate, future library.
Not a database: the loaders and movers that drive it.
adbcBridge is an ADBC driver, so anything that speaks ADBC can use it without a connector of its own. Three that have been tried, with the measurements written up.
omniload ingest --sql-backend adbcbridgeA fourth SQL backend for the dlt-based loader, built on its maintainer's request: SQLAlchemy reflects the table, the database's ODBC driver reads it, Arrow batches come out, and incremental append and merge work as before. Tested in omniload's own harness on PostgreSQL, CrateDB and SQL Server, and inside its container image. How it was built and tested.
Merged and released on 2026-09-17: panodata/omniload#362, reviewed by both maintainers, shipped the same day in omniload v0.17.0.
βPerfect. Thank you so much!β βYour contribution was included in v0.17.0, thank you again.βAndreas Motl, omniload maintainer, on the pull request
register_table_loader_backend("adbcbridge", β¦)Two shapes, no change to dlt: a plain resource yielding record batches from adbcbridge.connect(), or a registered sql_table backend so incremental loading works as usual. One million PostgreSQL rows into DuckDB in 2.1 s, and the backend that keeps a timestamp(0) column at second precision. the measurements.
--source-driver <libadbc_driver_odbc>A Go command line that moves tables between databases over ADBC. adbcBridge plugs in as a custom driver, with the ODBC connection string as the source URI: one million rows from PostgreSQL into SQLite in 1.3 s, and 200,000 rows out of CrateDB, where the native PostgreSQL driver stops at its first COPY, in 0.23 s. the measurements.
53 databases, 24 ODBC drivers, one workload.
"Reachable" would be the few hundred sources with an ODBC driver. These are the ones that passed the full compat workload against a real server or file, grouped by the driver that fronts them β because the driver, not the database, is where the surprises live.
PostgreSQL wire (psqlodbc)
PostgreSQL Β· CockroachDB Β· YugabyteDB Β· TimescaleDB Β· Citus Β· Apache Cloudberry Β· openGauss Β· CrateDB Β· QuestDB Β· RisingWave Β· Materialize Β· YDB Β· Google Spanner (PGAdapter) Β· ArcadeDB
MySQL wire (Connector/ODBC, MariaDB Connector)
MySQL Β· MariaDB Β· MariaDB ColumnStore Β· TiDB Β· Percona Server Β· Dolt Β· Databend Β· Apache Doris Β· StarRocks Β· OceanBase Β· MatrixOne Β· GreptimeDB Β· MongoDB BI Connector Β· SingleStore
Their own drivers
SQLite Β· DuckDB Β· SQL Server Β· Azure SQL Edge Β· Oracle Β· IBM Db2 Β· IBM Informix Β· MonetDB Β· Firebird Β· OpenLink Virtuoso Β· Vertica Β· Apache Ignite Β· TDengine Β· OpenSearch Β· ClickHouse Β· Microsoft Access Β· SAP HANA Β· Exasol Β· Altibase Β· Kinetica Β· Actian Ingres Β· IBM Db2 for i
Arrow Flight SQL ODBC
sqlflite Β· Dremio Β· InfluxDB 3
Explore the interactive matrix β every cell, every quirk βOne page per database β
Stated rather than hidden. Three of those drivers were built from source for Linux here (Ignite, OpenSearch, TDengine); Ignite and OpenSearch publish no Linux build at all. Oracle's driver segfaults if a rowset is resized on an open LOB cursor, so the reader never does. Firebird's driver was silently writing NULLs over 64-bit integers after the first NULL, and our own DDL was giving it BLOBs for strings, which it reads one row at a time (fixed: VARCHAR, ~35Γ faster to read back β 8,256 to 289kβ295k rows/s). Db2's free driver has 32-bit SQLLEN. MySQL's driver cannot be loaded after pyarrow is imported, and unixODBC reports that as "file not found". All of it is in COMPATIBILITY.md and TROUBLESHOOTING.md.
What the sweep found in other projects, reported with a reproduction.
Driving 53 databases through one driver on three operating systems turns up defects that belong upstream. Each goes back to its project with a reproduction that needs nothing from this one.
The driver manager overwrites its own stack and heap, and the process aborts, on the first SQL error from a driver whose SQLWCHAR is 4 bytes. Found on macOS with two vendor drivers; reproduced on Linux with a 40-line fake driver; affects 2.3.12 and 2.3.14.
lurcher/unixODBC#239Affects: OpenLink Virtuoso, sqlflite, Dremio, InfluxDB 3 (the two macOS drivers built to the 4-byte width) Β· Read the note
The macOS driver is built to iODBC's 4-byte SQLWCHAR and nothing says so β through unixODBC every application dies on its first SQL error. On Linux the ANSI driver treats SQL_C_WCHAR as wchar_t on both sides, stores and reads SQL_C_SBIGINT as 0, steps date parameter arrays by ColumnSize, and aborts on any Charset= but the literal UTF-8 β four programs. Two of them are now sent as fixes (2026-09-18): the abort turned out to be a double free of the charset name, and the date stride comes from the ODBC 3 type names missing from one size switch.
openlink/virtuoso-opensource#1469 Β· #1470 Β· #1471 Β· #1472 Β· #1473 Β· PR #1477 Β· PR #1478Affects: OpenLink Virtuoso Β· Read the note
The Apple Silicon build has the same undocumented width, a logger that breaks SQLAllocHandle when enabled, and docs that contradict each other on Apple Silicon support.
dremio/warpdrive#16Affects: sqlflite, Dremio, InfluxDB 3 Β· Read the note
Column-wise parameter arrays step fixed-length C types at the BufferLength stride, so every row receives row 1's values; a SQL_BIGINT parameter once bound as SQL_C_DEFAULT keeps writing NULL; SQLPrepare discards the rows-fetched pointer set before it. Three plain-ODBC programs, reproduced on 3.0.1 and 3.5.0-rc1.
firebird-odbc-driver#299 Β· #300 Β· #301Affects: Firebird Β· Read the note
Timestamp columns cannot be bound (SQLGetData converts them, SQLBindCol does not), timestamp parameters and most BOOL parameter types are refused, and a DECIMAL column fails the whole SELECT β each with the driver's own diagnostic and a program.
taosdata/taos-connector-odbc#63Affects: TDengine Β· Read the note
Server-side prepare refuses a string parameter sent with MySQL type BLOB β what MySQL Connector/ODBC sends for every SQL_C_WCHAR parameter β so Unicode ODBC clients cannot insert through prepared statements: a NullPointerException on 2.1.0, a clean refusal on 4.1.3.
apache/doris#67301Affects: Apache Doris Β· Read the note
A bound SQL_NULL_DATA is ignored whenever a value buffer is attached, and parameter arrays stop after set 1 β the driver's one-set-per-SQLMoreResults protocol answers SQL_NO_DATA on the first call. Two programs; the affected-row count thread from 2021 re-measured on 1.5.5. The ignored SQL_NULL_DATA is now sent as a fix (2026-09-18): two lines in the driver decided nullability from the value pointer alone.
ClickHouse/clickhouse-odbc#582 Β· #335 Β· PR #586Affects: ClickHouse Β· Read the note
An out-of-range int8 is narrowed to SQL_C_SLONG silently β 9223372036854775807 reads as β1 under SQL_SUCCESS; and with Protocol=7.4-0 a statement freed inside a transaction still sends SAVEPOINT;DEALLOCATE;RELEASE, whose failure on a server without savepoints is swallowed while COMMIT reports success. Wire captures included.
postgresql-interfaces/psqlodbc#207 Β· #208Affects: PostgreSQL, QuestDB, Materialize Β· Read the note
A BINARY column in a simple-query result is sent with format code 1 where PostgreSQL always sends text, so libpq and psqlodbc both cut the value at its first 0x00; and DEALLOCATE ALL throws a NullPointerException even on a fresh connection.
questdb/questdb#7566 Β· #7567Affects: QuestDB Β· Read the note
A multi-statement simple query returns every CommandComplete tagged with the whole string's leading text β BEGIN;INSERT 0, BEGIN;INSERT 1 β so tag-parsing clients get no row count inside a transaction. Side by side with PostgreSQL at the wire level.
crate/crate#20085Affects: CrateDB Β· Read the note
Any character value converted to SQL_C_TYPE_TIMESTAMP comes back as today's date under SQL_SUCCESS; and SQLErrorW never clears the connection error, so unixODBC loops forever on any failed connect. Both traced to the source line.
opensearch-project/sql-odbc#101 Β· #102Affects: OpenSearch Β· Read the note
Column-wise parameter arrays test row 0's NULL indicator for every row: a NULL below it stores an empty string, or segfaults the client on a binary column when WriteInt8Array is handed β1 as a length. The offending lines in parameter.cpp, with a fix.
apache/ignite#13537Affects: Apache Ignite Β· Read the note
SQLDescribeCol describes a DATETIME(6) as 19 characters with 0 digits while returning 26; and a MySQL-wire server reporting a zero charset width makes SHOW VARIABLES kill the client with SIGFPE in get_column_size().
mysql/mysql-connector-odbc#20 Β· #21Affects: MySQL, SingleStore, MatrixOne Β· Read the note
DEALLOCATE "Name" keeps the quotes in its lookup, so a statement prepared at the protocol level with a mixed-case name β psqlodbc's _PLAN0xβ¦ β can never be released, and inside a transaction the failure aborts it. The server half of the psqlodbc row-loss pair; filed with a psycopg side-by-side against PostgreSQL 16, which releases it.
MaterializeInc/materialize#38605Affects: Materialize Β· Read the note
BEGIN, COMMIT and the savepoint statements were answered with a zero-field RowDescription before the command tag, so libpq reported PGRES_TUPLES_OK and the Arrow PostgreSQL ADBC driver stopped at Failed to begin transaction with autocommit off. Sent as a fix with a wire-level test and merged by the maintainer the same day (2026-09-17), who re-ran the module's integration suite and extended the fix to prefixed statements such as {sql}BEGIN.
Written up by ArcadeDB on 2026-09-21: ArcadeDB in the Apache Arrow Ecosystem, Through Its PostgreSQL Wire Protocol β their own account of all four findings, the two Arrow routes and the language benchmark.
ArcadeData/arcadedb#7815Affects: ArcadeDB Β· Read the note
SQLGetInfo(SQL_DBMS_VER) segfaults under unixODBC: inside the driver the call to SQLAllocHandle binds to the driver manager, fails, and the error path frees an uninitialized handle. Linked directly it answers SQL_SUCCESS with an empty string, read into one buffer and returned from another. Sent as a fix rather than a report, and merged by a DuckDB maintainer the same day (2026-09-17): the version now comes straight from the engine, with a test against pragma_version(). A second fix: a parameter array stored set 0's value in every row for every fixed-length type under SQL_SUCCESS; each set now gets its own value, with a test. Merged the same day too.
duckdb/duckdb-odbc#523 Β· #524Affects: DuckDB Β· Read the note
SQLGetTypeInfo fails with 42S22 Unknown column 'json' whenever sql_mode contains ANSI_QUOTES: the type-info result set is built from a SQL template with double-quoted string literals, which that mode turns into identifiers. And SQLColumns with a NULL catalog name segfaults the client on a connection opened without Database= β strdup(NULL) on the client library's empty current-database field. Both reproduced through pyodbc alone, with the source lines named.
memsql/singlestore-odbc-connector#45 Β· #46 Β· fixes: #48 Β· #47Affects: SingleStore Β· Read the note
Twenty-seven reports across fifteen projects so far; forty more findings β connector versions, driver bindings, server behaviours, container images, and one (MatrixOne's BIT arrays) fixed on their main before it could be filed β are documented with their first error and conditions in docs/UPSTREAM.md.
Everything written down, in the order you will need it.
The documentation follows one path β install the driver manager and a driver, run a first query, then go deep in your language β and ends in a reference that names every option, every connection string and every driver quirk with the measured reason behind it. Every command is copy-paste ready; every limitation is stated where you would hit it.