Skip to content

Custom backends

adbc-poolhouse ships config classes for 13 ADBC backends (14 config classes in total, the extra one being the non-ADBC DatabricksPythonConfig). If your ADBC driver is not on that list you have two options: pass raw driver arguments to create_pool() directly, or write a config class as described below.

Both of those routes assume a driver that speaks ADBC, and a connector that does not is out of scope: non-ADBC connectors are not a supported extension point. adbc-poolhouse does pool one itself through DatabricksPythonConfig, but the seam that makes that work is private, undocumented, and free to change in any release, so nothing here describes it and no third-party config should build against it. If you need a native connector backend, please open an issue rather than reaching for internals.

The short version

Inherit from BaseWarehouseConfig and implement two methods:

from adbc_poolhouse import BaseWarehouseConfig, create_pool
from pydantic_settings import SettingsConfigDict


class MyDriverConfig(BaseWarehouseConfig):
    model_config = SettingsConfigDict(env_prefix="MYDRIVER_")

    host: str
    port: int = 5000
    database: str = "default"

    def _driver_path(self) -> str:
        return self._resolve_driver_path("adbc_driver_mydriver")

    def to_adbc_kwargs(self) -> dict[str, str]:
        return {
            "uri": f"mydriver://{self.host}:{self.port}/{self.database}",
        }


pool = create_pool(MyDriverConfig(host="db.example.com"))

That is the complete implementation. BaseWarehouseConfig provides the pool tuning fields (pool_size, max_overflow, timeout, recycle, pre_ping) with defaults, and the _adbc_entrypoint() and _dbapi_module() methods return None, the right default for most drivers.

Library authors who prefer not to add a dependency on adbc-poolhouse (or pydantic) can implement the WarehouseConfig protocol directly instead. See Without BaseWarehouseConfig below.

What each method does

_driver_path()

Called by create_pool() to locate the native ADBC driver. Return None if the driver uses a Python dbapi module instead (see _dbapi_module() below). At least one of _driver_path or _dbapi_module must return a non-None value.

Return one of:

  • An absolute path to a shared library (.so / .dylib / .dll)
  • A short package name like "adbc_driver_mydriver" that adbc_driver_manager resolves through its manifest
  • None — when using _dbapi_module() instead

See the ADBC driver manifests docs for details about driver path resolution.

The PyPI package for many drivers has a method like _driver_path() that returns this value. A _resolve_driver_path("adbc_driver_mydriver") helper is provided on BaseWarehouseConfig. It tries importlib.util.find_spec -> import -> call the package's own _driver_path(), and falls back to returning the package name if the package is not installed.

to_adbc_kwargs()

Called by create_pool() to get the connection keyword arguments passed to the ADBC driver. Return a dict[str, str] mapping ADBC option keys to values.

The exact keys depend on your driver. Common patterns:

Driver style Key Example value
URI-based "uri" "postgresql://host:5432/db"
Path-based "path" "/data/warehouse.db"
Key-value "adbc.snowflake.sql.account" "myorg-myaccount"

_adbc_entrypoint()

Return the driver's init symbol name, or None for the default.

Most ADBC drivers use a default init function. Override this only when your driver requires a non-standard symbol. Among the 13 ADBC backends, only DuckDB and SQLite override this method.

def _adbc_entrypoint(self) -> str | None:
    return "my_driver_custom_init"

_dbapi_module()

Some Python ADBC packages are not full ADBC drivers, but do expose the Python dbapi2-compatible ADBC interface. In that case you can implement this method.

Return a dotted Python module path (e.g. "adbc_driver_mydb.dbapi") to the module containing a connect() function. Return None otherwise.

When set, create_pool() imports the module and calls connect() directly instead of loading a native shared library through adbc_driver_manager.

import importlib.util


def _dbapi_module(self) -> str | None:
    if importlib.util.find_spec("adbc_driver_mydb") is not None:
        return "adbc_driver_mydb.dbapi"
    return None

Dispatch contract

When _dbapi_module() returns a string, create_pool() imports that module and inspects connect()'s signature to pick the call shape. Your driver's connect() must fall into one of these shapes:

Signature shape Call form Examples
No db_kwargs parameter connect(**to_adbc_kwargs()) DuckDB, SQLite
Has db_kwargs; uri absent or with a default connect(db_kwargs=to_adbc_kwargs()) Snowflake, BigQuery
Has db_kwargs; uri required, positional-or-keyword connect(uri, db_kwargs={...}) with uri popped from kwargs first PostgreSQL, FlightSQL, Quack
Has db_kwargs; uri required, keyword-only connect(uri=uri, db_kwargs={...}) with uri popped from kwargs first (none in the wild yet)

If you ship a new ADBC driver, the simplest safe choice is to keep uri optional (give it a default of None). The dispatcher then routes through the second row above and never has to pop uri out of db_kwargs. Note that db_kwargs is always passed by name, so it is fine for your connect() to declare it keyword-only.

Return None from _dbapi_module() when the driver should route through adbc_driver_manager.dbapi.connect() instead, which is the path the ADBC Driver Foundry drivers take. That is the right default for native shared-library drivers without a Python dbapi submodule.

Pool tuning

BaseWarehouseConfig is a Pydantic BaseSettings model that defines five pool fields, which your subclass inherits: pool_size, max_overflow, timeout, recycle, and pre_ping. Their defaults, meanings, and the guidance for choosing a recycle value live in one place, under Pool tuning in the configuration guide.

Your config's env_prefix applies to these fields automatically. With env_prefix="MYDRIVER_", setting MYDRIVER_POOL_SIZE=10 in the environment overrides the default. That works because the fields are declared on BaseWarehouseConfig itself, so your subclass's prefix covers them alongside your own fields.

Without BaseWarehouseConfig

You do not have to inherit from BaseWarehouseConfig. Any class that satisfies the WarehouseConfig protocol works. The protocol asks for the four tuning fields below, but not pre_ping: create_pool reads pre_ping as False when a config does not declare it.

class StandaloneConfig:
    def __init__(self, uri: str) -> None:
        self.uri = uri
        self.pool_size = 5
        self.max_overflow = 3
        self.timeout = 30
        self.recycle = 3600

    def _driver_path(self) -> str | None:
        return "adbc_driver_mydriver"

    def _adbc_entrypoint(self) -> str | None:
        return None

    def _dbapi_module(self) -> str | None:
        return None

    def to_adbc_kwargs(self) -> dict[str, str]:
        return {"uri": self.uri}

This is more boilerplate than inheriting BaseWarehouseConfig. The standalone approach is useful for library authors who do not want to pull in adbc-poolhouse or pydantic as a transitive dependency.

Protocol reference

The full contract, including underscore-prefixed methods normally hidden in the API reference:

adbc_poolhouse.WarehouseConfig

Bases: Protocol

Structural type for warehouse config objects.

Any class with these attributes and methods can be passed to create_pool or managed_pool. The built-in config classes all satisfy this protocol through BaseWarehouseConfig.

Third-party authors: inherit from BaseWarehouseConfig for pool-tuning defaults and _resolve_driver_path, or implement the full protocol from scratch.

pool_size instance-attribute

pool_size: int

Number of connections to keep open in the pool.

max_overflow instance-attribute

max_overflow: int

Connections allowed above pool_size when the pool is exhausted.

timeout instance-attribute

timeout: int

Seconds to wait for a connection before raising sqlalchemy.exc.TimeoutError.

recycle instance-attribute

recycle: int

Seconds before a connection is closed and replaced.

_adbc_entrypoint

_adbc_entrypoint() -> str | None

Return the ADBC driver init symbol, or None for the default.

Most ADBC drivers use a default init function and this method should return None. Override only when the driver requires a non-standard symbol -- for example, DuckDB needs "duckdb_adbc_init".

_driver_path

_driver_path() -> str | None

Return the ADBC driver path or short name, or None.

Called by create_pool to locate the native driver library. Two return styles are supported:

  • An absolute filesystem path to a shared library (e.g. /usr/lib/libadbc_driver_snowflake.so).
  • A short package name (e.g. "adbc_driver_snowflake") that adbc_driver_manager resolves via its manifest.

Return None when the driver uses a Python dbapi module instead (see _dbapi_module). At least one of _driver_path or _dbapi_module must return a non-None value.

Most implementations delegate to BaseWarehouseConfig._resolve_driver_path.

_dbapi_module

_dbapi_module() -> str | None

Return a dotted Python module path exposing connect(), or None.

When set, create_pool imports this module and calls its connect() function instead of loading a native shared library through adbc_driver_manager. Typical value: "adbc_driver_snowflake.dbapi".

Return None (the default) for drivers that do not ship a Python package with a dbapi sub-module.

to_adbc_kwargs

to_adbc_kwargs() -> dict[str, str]

Convert config to ADBC driver connection kwargs.

See also