adbcBridge is a single plain-C11 shared library, libadbc_driver_odbc.so
(.dylib on macOS, .dll on Windows), built with CMake. It links only an ODBC
(Open Database Connectivity) driver manager and the C standard library; the Arrow
support it needs is vendored (nanoarrow, under vendor/). This page covers the
prerequisites, the build and install commands, every CMake option, the test
suite, the release workflow, and how each language binding is packaged from
source.
You need a C11 compiler, CMake 3.16 or newer, and an ODBC driver manager with its
development headers (sql.h, sqlext.h).
| Platform | Install |
|---|---|
| Debian / Ubuntu | sudo apt install cmake unixodbc-dev |
| Red Hat / Fedora | sudo dnf install cmake unixODBC-devel |
| macOS | brew install cmake unixodbc |
| Windows | CMake and a C toolchain (MSVC or MinGW); the OS ships the ODBC driver manager (odbc32), no separate install needed |
The driver manager can be unixODBC or iODBC on POSIX, and Windows' own on
Windows. CMake finds it via find_package(ODBC), falling back to searching for
sql.h and a library named odbc, odbc32, or iodbc. On macOS, point CMake at
Homebrew's unixODBC with -DCMAKE_PREFIX_PATH="$(brew --prefix unixodbc)".
To use the driver you also need at least one vendor ODBC driver for the database you are connecting to; that is separate from building adbcBridge itself.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel 4
cmake --install build --prefix /your/prefixThe default build type is Release if none is given. The install lays out:
| Component | Destination |
|---|---|
| the shared library | <prefix>/lib (or lib64, per CMake's GNUInstallDirs on Fedora/RHEL-style 64-bit systems; <prefix>/bin on Windows) |
the ADBC driver manifest odbc.toml |
<prefix>/etc/adbc/drivers (configurable) |
For a no-root install into your home directory, install.sh wraps the three
commands above and writes the manifest into the ADBC per-user config directory so
the driver is immediately loadable by the name odbc:
./install.shIt installs the library under ~/.local/lib (lib64 on Fedora/RHEL-style
64-bit systems; install.sh prints the actual path) and the manifest under
~/.config/adbc/drivers (or ~/Library/Application Support/ADBC/Drivers on
macOS). Its behaviour is tunable through environment variables: PREFIX,
MANIFEST_DIR, BUILD_DIR, BUILD_TYPE, and JOBS.
ADBCBRIDGE_INSTALL_MANIFEST (on by default) installs a TOML manifest named
odbc.toml. The ADBC driver manager discovers a driver by finding
<name>.toml in one of its search directories, so installing it as odbc.toml
lets any ADBC binding load adbcBridge simply as driver="odbc" — no
ADBC_DRIVER_PATH or LD_LIBRARY_PATH needed.
The manifest is expanded from adbc_driver_odbc.toml.in at install time (not
configure time), because the absolute library path it must contain is only known
once cmake --install --prefix has chosen the final prefix. It carries a
platform tuple key such as linux_amd64 under [Driver.shared]; CMake derives
the OS, architecture, and a libc suffix (_musl, _mingw) so the key matches
exactly what the driver manager looks up — a mismatch would make discovery
silently fall through to some other library called "odbc".
| Option | Default | Meaning |
|---|---|---|
ADBC_ODBC_BUILD_SHARED |
ON |
Accepted but has no effect in v0.1.0: the library is always built shared. |
ADBC_ODBC_BUILD_TESTS |
ON |
Build the C smoke test and the C unit tests. |
ADBC_ODBC_ASAN |
OFF |
Build with AddressSanitizer and UndefinedBehaviorSanitizer (requires GCC or Clang). |
ADBCBRIDGE_BUILD_SHARED |
ON |
Accepted but has no effect in v0.1.0, like ADBC_ODBC_BUILD_SHARED. |
ADBCBRIDGE_INSTALL_MANIFEST |
ON |
Install the odbc.toml ADBC driver manifest. |
ADBCBRIDGE_MANIFEST_DIR |
etc/adbc/drivers |
Where to install the manifest, relative to the install prefix (or an absolute path, as install.sh uses to place it in the user config dir). |
Standard CMake variables also apply: CMAKE_BUILD_TYPE, CMAKE_INSTALL_PREFIX,
CMAKE_PREFIX_PATH (to find unixODBC), and the install-time
cmake --install --prefix.
Tests are built when ADBC_ODBC_BUILD_TESTS is ON and run with ctest:
ctest --test-dir build --output-on-failure
# on CMake older than 3.20, which has no --test-dir:
cd build && ctest --output-on-failure--test-dir arrived in CMake 3.20; the build itself only needs 3.16, so on an
older ctest run it from inside the build directory as shown. On Windows, where the Visual Studio generator is multi-config, add the
configuration — ctest --test-dir build -C Release --output-on-failure (or
-C Debug) — otherwise ctest runs no tests.
These compile the driver's own sources and exercise pieces of it in isolation,
with no database or ODBC driver required. Each is both a ctest target and a
source file under tests/c/:
| Target | Source | Exercises |
|---|---|---|
test_utf16 |
test_utf16.c |
The reader's SQLWCHAR (UTF-16) to UTF-8 conversion on the fetch path, plus NaN/Inf passthrough for float columns. |
test_types |
test_types.c |
The type-mapping logic (see Type mapping). |
test_sqllen32 |
test_sqllen32.c |
The 32-bit-SQLLEN accessors that read a narrow driver's lengths and indicators. |
test_objects |
test_objects.c |
The GetObjects catalog/schema/table metadata assembly. |
test_errors |
test_errors.c |
Mapping ODBC diagnostic records to ADBC errors and status codes. |
test_multirow |
test_multirow.c |
The multi-row INSERT batching used by bulk ingest. |
test_partition |
test_partition.c |
Splitting a query into partitions for ExecutePartitions. |
When the ODBC headers support SQL_WCHART_CONVERT (unixODBC's sqltypes.h
does), two extra targets — test_utf16_wchar32 and test_multirow_wchar32 —
rebuild those two tests with that macro defined, giving the four-byte SQLWCHAR
iODBC always has, so the wide-character codecs are proven for an iODBC-style
build without needing iODBC present.
adbc_odbc_c_smoke (tests/c/test_driver.c) is a dependency-free test that
dlopens the built driver and drives the ADBC 1.1.0 vtable directly. It is
POSIX-only (it uses dlopen/mkdtemp) and is skipped on Windows. It needs a
SQLite ODBC driver, named by the SQLITE_ODBC_DRIVER environment variable (a
path or a registered driver name); with none set it reports "skipped".
SQLITE_ODBC_DRIVER=/path/to/libsqlite3odbc.so ctest --test-dir buildThe test build also produces helper libraries never installed:
adbc_fake_native_driver (a stand-in native ADBC driver for the delegation
tests) and, on ELF/glibc, a pair of TLS libraries that reproduce a specific
loader failure for tests/test_driver_load_errors.py.
The end-to-end tests under tests/ drive the built library through the ADBC
Python driver manager. test_sqlite.py is the smoke test; test_delegate.py
covers native delegation; test_plug_and_play.py
covers the install; and the feature tests — test_partitions.py,
test_prefetch.py, test_parallel_ingest.py, test_long_columns.py,
test_pg_array_ingest.py, test_driver_load_errors.py, test_windows_text.py
— sit beside them. They read:
ADBC_ODBC_DRIVER— path to the builtlibadbc_driver_odbc.so(default<repo>/build/libadbc_driver_odbc.so);SQLITE_ODBC_DRIVER/POSTGRES_ODBC_DRIVER/ … — the vendor ODBC driver for the database under test.
To run the smoke test by hand, from the repository root:
python -m venv .venv && .venv/bin/pip install 'adbc-driver-manager>=1.7' pyarrow
SQLITE_ODBC_DRIVER=/path/to/libsqlite3odbc.so .venv/bin/python tests/test_sqlite.pyThat the install itself is plug-and-play — install into a temporary prefix,
then load the driver by the name odbc — is covered by test_plug_and_play.py.
It exercises both discovery routes (cmake --install --prefix plus
ADBC_DRIVER_PATH, and install.sh into the per-user config directory with
nothing set in the environment), each connection in a subprocess with an
explicitly built environment so a manifest already installed on the machine
cannot make it pass by accident:
SQLITE_ODBC_DRIVER=/path/to/libsqlite3odbc.so .venv/bin/python tests/test_plug_and_play.pyThe Python package (python/) has its own pytest suite, which also runs
against SQLite:
.venv/bin/pip install -e python pytest
SQLITE_ODBC_DRIVER=/path/to/libsqlite3odbc.so .venv/bin/python -m pytest python/testsNative delegation needs the native ADBC drivers installed and the
adbc_fake_native_driver helper from the test build; each case skips when its
native driver, ODBC driver (POSTGRES_ODBC_DRIVER, MARIADB_ODBC_DRIVER) or
server is missing:
.venv/bin/pip install adbc-driver-postgresql adbc-driver-sqlite
SQLITE_ODBC_DRIVER=/path/to/libsqlite3odbc.so .venv/bin/python tests/test_delegate.pyThe compatibility matrix runner,
tests/compat/test_matrix.py, is what
produces the numbers in Compatibility; it exercises all
53 databases (see Connection strings). It reads:
ADBC_ODBC_DRIVER— the driver under test (default<repo>/build/libadbc_driver_odbc.so);<NAME>_ODBC_DRIVER— each database's vendor ODBC driver, which also gates whether that entry runs;<NAME>_CONN— an optional per-entry connection-string override;ADBC_MATRIX_SUFFIX— a table-name suffix to isolate concurrent runs.
The servers themselves are brought up by tests/compat/docker-compose.yml, which
defines a service for every server-backed database (sqlite, duckdb and
access are file-based and need none) with the ports the matrix templates
connect to.
See tests/compat/README.md for how the fleet is
started and which vendor ODBC drivers each entry expects.
The same smoke test exists for every language binding, each a standalone project
under tests/ that depends only on published packages. All of them read
SQLITE_ODBC_DRIVER (a path or a registered driver name; default SQLite3) and
ADBC_ODBC_DRIVER (default ../../build/libadbc_driver_odbc.so, relative to the
project), and need no DSN, odbc.ini entry or server. Build the driver first.
From Rust (see tests/rust/README.md):
cd tests/rust && SQLITE_ODBC_DRIVER=/path/to/libsqlite3odbc.so cargo testFrom C#, with the .NET 8 SDK and unixODBC on the host
(tests/csharp/README.md has a docker run
one-liner that needs no .NET SDK on the host):
cd tests/csharp && SQLITE_ODBC_DRIVER=/path/to/libsqlite3odbc.so dotnet testFrom R, in a container built from tests/r (see
tests/r/README.md); /path/to/odbc/drivers is the
host directory holding libsqlite3odbc.so:
docker build -t adbcbridge-r tests/r
docker run --rm -v "$PWD:/repo:ro" -v /path/to/odbc/drivers:/odbc:ro \
-e ADBC_ODBC_DRIVER=/repo/build/libadbc_driver_odbc.so \
-e SQLITE_ODBC_DRIVER=/odbc/libsqlite3odbc.so \
adbcbridge-r Rscript /repo/tests/r/smoke.RFrom Java, in a container, so no JDK, Maven or unixODBC is needed on the host
(see tests/java/README.md); the named volume
caches the Maven downloads between runs:
docker run --rm \
-v "$PWD":/work \
-v /path/to/odbc/drivers:/odbc:ro \
-v adbcbridge-m2:/root/.m2 \
-w /work/tests/java \
-e ADBC_ODBC_DRIVER=/work/build/libadbc_driver_odbc.so \
-e SQLITE_ODBC_DRIVER=/odbc/libsqlite3odbc.so \
maven:3-eclipse-temurin-21 \
bash -c 'apt-get update -qq && apt-get install -y -qq unixodbc && mvn -B test'From Go (tests/go/), which skips unless both variables are set:
cd tests/go && ADBC_ODBC_DRIVER=/abs/path/libadbc_driver_odbc.so \
SQLITE_ODBC_DRIVER=/abs/path/libsqlite3odbc.so go test ./....github/workflows/ci.yml runs on every push
to main and every pull request. Its single build job is a matrix over Ubuntu,
macOS, Windows x64 and Windows Win32 (32-bit, where SQLLEN is 32 bits wide, as
the Access, Excel and Text drivers on a 32-bit Office machine require). Each leg
installs the ODBC driver manager where needed, configures (Debug, except Win32,
which builds Release because that is the configuration people ship), builds,
runs the C unit tests with ctest --test-dir build -C Debug --output-on-failure
(-C Release on Win32), installs,
and prints the installed odbc.toml.
The end-to-end tests need an actual ODBC driver, so the Linux leg alone installs
libsqliteodbc and Python 3.12 and additionally runs:
- the SQLite end-to-end test,
tests/test_sqlite.py; - the Python package's pytest suite (
pip install -e python, thenpytest python/tests); - the manifest discovery check: a connection by the name
odbcthroughADBC_DRIVER_PATH, against both the prefix given at configure time and a second prefix given at install time withcmake --install --prefix. The latter is a regression test — the manifest used to freeze the library path at configure time, so a relocated install shipped a manifest pointing at the old prefix.
Continuous integration is described under What CI runs.
.github/workflows/release.yml runs on a v* tag (or as a dry run via
workflow_dispatch) and produces, for each platform, on a GitHub Release:
| Job | Artifact |
|---|---|
linux (x86_64, aarch64, manylinux_2_28) |
an auditwheel-repaired Python wheel and adbcbridge-<tag>-<rid>.tar.gz (containing the .so) |
macos (arm64) |
a delocated wheel and …-osx-arm64.tar.gz (containing the .dylib) |
windows (x64) |
a wheel and …-win-x64.tar.gz (containing the .dll) |
sdist |
a Python source distribution |
crate |
the Rust crate (cargo package) |
nuget |
a NuGet package bundling the four native libraries |
maven |
a Maven jar bundling the four native libraries |
The wheels bundle the driver library but deliberately exclude the OS ODBC
driver manager (libodbc), so the user's own odbcinst.ini still governs which
vendor drivers are visible. Native libraries move between jobs through the Release
itself; a dry run builds everything against a draft release that the final job
deletes. PyPI publishing is a separate workflow (publish-pypi.yml) using trusted
publishing.
Every binding wraps the same C library. They obtain it in one of two ways: some
compile it from a bundled copy of the C sources, others bundle a prebuilt native
library. Each binding locates the library at run time via ADBCBRIDGE_LIBRARY /
ADBC_ODBC_DRIVER, the ADBC manifest, or a bundled copy (see the environment
variables in Options).
The crate's default bundled feature compiles the driver from rust/csrc/ at
build time via a build.rs that mirrors the CMake build (C11, ADBC_EXPORTING,
hidden visibility, linking the ODBC driver manager plus dl/pthread). Because a
crates.io package may only contain files under the crate directory, csrc/ is a
copy of the repository's src/, include/, and vendor/nanoarrow/; refresh it
with rust/sync-csrc.sh after touching those, and a test (tests/csrc_in_sync.rs)
fails until the copy matches. Package with:
cd rust && cargo packageA thin wrapper that ships the prebuilt shared library inside the wheel. setup.py
takes the library path from ADBCBRIDGE_LIBRARY (or finds it under
ADBCBRIDGE_BUILD_DIR, default <repo>/build). Build with:
ADBCBRIDGE_LIBRARY=$PWD/build/libadbc_driver_odbc.so python -m build --wheel pythonThe release repairs the wheel with auditwheel (Linux) or delocate (macOS),
excluding libodbc.
dotnet pack builds the NuGet package; a NativeRoot property points it at a
directory of per-runtime native libraries to bundle under runtimes/:
dotnet pack csharp/AdbcBridge -c Release -p:NativeRoot=/path/to/nativesmvn package builds the jar; the adbcbridge.natives property points it at a
directory of native libraries to bundle under org/adbcbridge/native/:
mvn -f java/pom.xml package -Dadbcbridge.natives=/path/to/nativesAn ordinary go build loads the driver from an external path or the ADBC
manifest at run time. Building with -tags adbcbridge_embed instead embeds the
native libraries staged under go/internal/native/<goos>_<goarch>/ into the
binary and extracts the right one at run time:
go build -tags adbcbridge_embed ./...Without the tag, or with no library staged, the build still compiles and simply falls back to locating the driver via the manifest and install directories.