Skip to content

adbc_poolhouse

adbc_poolhouse

Connection pooling for ADBC drivers from typed warehouse configs.

BaseWarehouseConfig

Bases: BaseSettings, ABC

Base class for all warehouse config models.

Provides pool tuning fields with library defaults. Not intended to be instantiated directly — use a concrete subclass (e.g. DuckDBConfig).

Pool tuning fields are inherited by all concrete configs, and each concrete config's env_prefix applies to these fields automatically. For example, DUCKDB_POOL_SIZE populates DuckDBConfig.pool_size.

pool_size class-attribute instance-attribute

pool_size: int = 5

Number of connections to keep open in the pool. Default: 5.

max_overflow class-attribute instance-attribute

max_overflow: int = 3

Connections allowed above pool_size when pool is exhausted. Default: 3.

timeout class-attribute instance-attribute

timeout: int = 30

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

recycle class-attribute instance-attribute

recycle: int = 3600

Seconds before a connection is closed and replaced. Default: 3600.

to_adbc_kwargs abstractmethod

to_adbc_kwargs() -> dict[str, str]

Convert config to ADBC driver connection kwargs.

Subclasses must override this method to provide backend-specific serialization.

Source code in src/adbc_poolhouse/_base_config.py
@abstractmethod
def to_adbc_kwargs(self) -> dict[str, str]:
    """
    Convert config to ADBC driver connection kwargs.

    Subclasses must override this method to provide backend-specific
    serialization.
    """
    ...

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.

to_adbc_kwargs

to_adbc_kwargs() -> dict[str, str]

Convert config to ADBC driver connection kwargs.

Source code in src/adbc_poolhouse/_base_config.py
def to_adbc_kwargs(self) -> dict[str, str]:
    """Convert config to ADBC driver connection kwargs."""
    ...

BigQueryConfig

Bases: BaseWarehouseConfig

BigQuery warehouse configuration.

Supports SDK default auth (ADC), JSON credential file, JSON credential string, and user authentication flows.

Pool tuning fields are inherited and loaded from BIGQUERY_* env vars.

auth_type class-attribute instance-attribute

auth_type: str | None = None

Auth method: 'bigquery' (SDK default/ADC), 'json_credential_file', 'json_credential_string', 'user_authentication'. Env: BIGQUERY_AUTH_TYPE.

auth_credentials class-attribute instance-attribute

auth_credentials: SecretStr | None = None

JSON credentials file path or encoded credential string, depending on auth_type. Env: BIGQUERY_AUTH_CREDENTIALS.

auth_client_id class-attribute instance-attribute

auth_client_id: str | None = None

OAuth client ID for user_authentication flow. Env: BIGQUERY_AUTH_CLIENT_ID.

auth_client_secret class-attribute instance-attribute

auth_client_secret: SecretStr | None = None

OAuth client secret for user_authentication flow. Env: BIGQUERY_AUTH_CLIENT_SECRET.

auth_refresh_token class-attribute instance-attribute

auth_refresh_token: SecretStr | None = None

OAuth refresh token for user_authentication flow. Env: BIGQUERY_AUTH_REFRESH_TOKEN.

project_id class-attribute instance-attribute

project_id: str | None = None

GCP project ID. Env: BIGQUERY_PROJECT_ID.

dataset_id class-attribute instance-attribute

dataset_id: str | None = None

Default dataset. Env: BIGQUERY_DATASET_ID.

to_adbc_kwargs

to_adbc_kwargs() -> dict[str, str]

Convert config to ADBC driver connection kwargs.

All keys use the adbc.bigquery.sql.* prefix verified from the adbc_driver_bigquery.DatabaseOptions enum. Only non-None fields are included.

Returns:

Type Description
dict[str, str]

Dict of ADBC driver kwargs. Empty when no fields are set.

Source code in src/adbc_poolhouse/_bigquery_config.py
def to_adbc_kwargs(self) -> dict[str, str]:
    """
    Convert config to ADBC driver connection kwargs.

    All keys use the ``adbc.bigquery.sql.*`` prefix verified from the
    ``adbc_driver_bigquery.DatabaseOptions`` enum. Only non-None fields
    are included.

    Returns:
        Dict of ADBC driver kwargs. Empty when no fields are set.
    """
    kwargs: dict[str, str] = {}
    if self.auth_type is not None:
        kwargs["adbc.bigquery.sql.auth_type"] = self.auth_type
    if self.auth_credentials is not None:
        kwargs["adbc.bigquery.sql.auth_credentials"] = self.auth_credentials.get_secret_value()
    if self.auth_client_id is not None:
        kwargs["adbc.bigquery.sql.auth.client_id"] = self.auth_client_id
    if self.auth_client_secret is not None:
        kwargs["adbc.bigquery.sql.auth.client_secret"] = (
            self.auth_client_secret.get_secret_value()
        )
    if self.auth_refresh_token is not None:
        kwargs["adbc.bigquery.sql.auth.refresh_token"] = (
            self.auth_refresh_token.get_secret_value()
        )
    if self.project_id is not None:
        kwargs["adbc.bigquery.sql.project_id"] = self.project_id
    if self.dataset_id is not None:
        kwargs["adbc.bigquery.sql.dataset_id"] = self.dataset_id
    return kwargs

ClickHouseConfig

Bases: BaseWarehouseConfig

ClickHouse warehouse configuration.

Uses the Columnar ADBC ClickHouse driver (Foundry-distributed, not on PyPI). Install via the ADBC Driver Foundry:

dbc install --pre clickhouse

The --pre flag is required — only alpha releases are available (v0.1.0-alpha.1).

Supports two connection modes:

  • URI mode: set uri with the full ClickHouse connection string.
  • Decomposed mode: set host and username together. password, database, and port are optional. port defaults to 8123 (HTTP interface).

At least one mode must be fully specified — construction raises ConfigurationError if neither is provided.

Note: The field name is username, not user. The Columnar ClickHouse driver uses username as the kwarg key. Passing user causes a silent auth failure.

Pool tuning fields are inherited and loaded from CLICKHOUSE_* env vars.

Note: This driver is distributed via the ADBC Driver Foundry, not PyPI. See the installation guide for Foundry setup instructions.

uri class-attribute instance-attribute

uri: SecretStr | None = None

Full ClickHouse connection URI. May contain credentials — stored as SecretStr. Env: CLICKHOUSE_URI.

host class-attribute instance-attribute

host: str | None = None

ClickHouse hostname. Alternative to embedding host in URI. Env: CLICKHOUSE_HOST.

port class-attribute instance-attribute

port: int = 8123

ClickHouse HTTP interface port. Default: 8123. Env: CLICKHOUSE_PORT.

username class-attribute instance-attribute

username: str | None = None

ClickHouse username. Maps to the username driver kwarg (not user). Env: CLICKHOUSE_USERNAME.

password class-attribute instance-attribute

password: SecretStr | None = None

ClickHouse password. Optional. Env: CLICKHOUSE_PASSWORD.

database class-attribute instance-attribute

database: str | None = None

ClickHouse database name. Optional. Env: CLICKHOUSE_DATABASE.

to_adbc_kwargs

to_adbc_kwargs() -> dict[str, str]

Convert config to ADBC driver connection kwargs.

Supports two modes:

  • URI mode (uri set): returns {uri: ...} with the secret value extracted.
  • Decomposed mode (host + username set): returns individual kwargs with port as a string. password and database are omitted when None.

Returns:

Type Description
dict[str, str]

Dict of ADBC driver kwargs for adbc_driver_manager.dbapi.connect().

Source code in src/adbc_poolhouse/_clickhouse_config.py
def to_adbc_kwargs(self) -> dict[str, str]:
    """
    Convert config to ADBC driver connection kwargs.

    Supports two modes:

    - URI mode (``uri`` set): returns ``{uri: ...}`` with the secret
      value extracted.
    - Decomposed mode (``host`` + ``username`` set): returns individual
      kwargs with ``port`` as a string. ``password`` and ``database``
      are omitted when ``None``.

    Returns:
        Dict of ADBC driver kwargs for ``adbc_driver_manager.dbapi.connect()``.
    """
    if self.uri is not None:
        return {"uri": self.uri.get_secret_value()}

    # Decomposed mode -- model_validator guarantees host and username are set
    assert self.host is not None
    assert self.username is not None

    result: dict[str, str] = {
        "username": self.username,
        "host": self.host,
        "port": str(self.port),
    }
    if self.password is not None:
        result["password"] = self.password.get_secret_value()  # pragma: allowlist secret
    if self.database is not None:
        result["database"] = self.database
    return result

check_connection_spec

check_connection_spec() -> Self

Raise ConfigurationError if neither uri nor minimum decomposed fields are set.

Source code in src/adbc_poolhouse/_clickhouse_config.py
@model_validator(mode="after")
def check_connection_spec(self) -> Self:
    """Raise ConfigurationError if neither uri nor minimum decomposed fields are set."""
    has_uri = self.uri is not None
    has_decomposed = self.host is not None and self.username is not None
    if not has_uri and not has_decomposed:
        raise ConfigurationError(
            "ClickHouseConfig requires either 'uri' or at minimum "
            "'host' and 'username'. Got none of these."
        )
    return self

DatabricksConfig

Bases: BaseWarehouseConfig

Databricks warehouse configuration.

Uses the Columnar ADBC Databricks driver (Foundry-distributed, not on PyPI). Install via the ADBC Driver Foundry.

Supports PAT (personal access token) and OAuth (U2M and M2M) auth. Supports two connection modes:

  • URI mode: set uri with the full DSN string.
  • Decomposed mode: set host, http_path, and token together.

At least one mode must be fully specified — construction raises ConfigurationError if neither is provided.

Pool tuning fields are inherited and loaded from DATABRICKS_* env vars.

Note: This driver is distributed via the ADBC Driver Foundry, not PyPI. See the installation guide for Foundry setup instructions.

uri class-attribute instance-attribute

uri: SecretStr | None = None

Full connection URI: databricks://token:@:443/ May contain credentials — stored as SecretStr. Env: DATABRICKS_URI.

host class-attribute instance-attribute

host: str | None = None

Databricks workspace hostname (e.g. 'adb-xxx.azuredatabricks.net'). Alternative to embedding host in URI. Env: DATABRICKS_HOST.

http_path class-attribute instance-attribute

http_path: str | None = None

SQL warehouse HTTP path (e.g. '/sql/1.0/warehouses/abc123'). Env: DATABRICKS_HTTP_PATH.

token class-attribute instance-attribute

token: SecretStr | None = None

Personal access token for PAT auth. Env: DATABRICKS_TOKEN.

auth_type class-attribute instance-attribute

auth_type: str | None = None

OAuth auth type: 'OAuthU2M' (browser-based) or 'OAuthM2M' (service principal). Omit for PAT auth. Env: DATABRICKS_AUTH_TYPE.

client_id class-attribute instance-attribute

client_id: str | None = None

OAuth M2M service principal client ID. Env: DATABRICKS_CLIENT_ID.

client_secret class-attribute instance-attribute

client_secret: SecretStr | None = None

OAuth M2M service principal client secret. Env: DATABRICKS_CLIENT_SECRET.

catalog class-attribute instance-attribute

catalog: str | None = None

Default Unity Catalog. Env: DATABRICKS_CATALOG.

schema_ class-attribute instance-attribute

schema_: str | None = Field(
    default=None, validation_alias="schema", alias="schema"
)

Default schema. Python attribute is schema_ to avoid Pydantic conflicts. Env: DATABRICKS_SCHEMA.

check_connection_spec

check_connection_spec() -> Self

Raise ConfigurationError if neither uri nor all minimum decomposed fields are set.

Source code in src/adbc_poolhouse/_databricks_config.py
@model_validator(mode="after")
def check_connection_spec(self) -> Self:
    """Raise ConfigurationError if neither uri nor all minimum decomposed fields are set."""
    has_uri = self.uri is not None
    has_decomposed = (
        self.host is not None and self.http_path is not None and self.token is not None
    )
    if not has_uri and not has_decomposed:
        raise ConfigurationError(
            "DatabricksConfig requires either 'uri' or all three of "
            "'host', 'http_path', and 'token'. Got none of these."
        )
    return self

to_adbc_kwargs

to_adbc_kwargs() -> dict[str, str]

Convert Databricks config fields to ADBC driver kwargs.

Supports two modes:

  • URI mode (uri set): extracts the SecretStr value and returns it verbatim as {"uri": ...}. The URI is never mutated, so any catalog/schema you want must already be in the DSN.
  • Decomposed mode: builds databricks://token:{encoded}@{host}:443{http_path} from host, http_path, and token. The token is URL-encoded via urllib.parse.quote with safe="". When catalog or schema_ is set, each is appended to the DSN as a URL-encoded query parameter (?catalog=...&schema=...) so unqualified table and view names resolve against that default namespace. When both are unset, no query string is added.

Returns:

Type Description
dict[str, str]

ADBC driver kwargs for adbc_driver_manager.dbapi.connect().

Source code in src/adbc_poolhouse/_databricks_config.py
def to_adbc_kwargs(self) -> dict[str, str]:
    """
    Convert Databricks config fields to ADBC driver kwargs.

    Supports two modes:

    - **URI mode** (`uri` set): extracts the `SecretStr` value and returns
      it verbatim as `{"uri": ...}`. The URI is never mutated, so any
      catalog/schema you want must already be in the DSN.
    - **Decomposed mode**: builds `databricks://token:{encoded}@{host}:443{http_path}`
      from `host`, `http_path`, and `token`. The token is URL-encoded via
      `urllib.parse.quote` with `safe=""`. When `catalog` or `schema_` is
      set, each is appended to the DSN as a URL-encoded query parameter
      (`?catalog=...&schema=...`) so unqualified table and view names resolve
      against that default namespace. When both are unset, no query string
      is added.

    Returns:
        ADBC driver kwargs for `adbc_driver_manager.dbapi.connect()`.
    """
    if self.uri is not None:
        return {"uri": self.uri.get_secret_value()}

    # Decomposed mode -- model_validator guarantees all three are set.
    assert self.host is not None
    assert self.http_path is not None
    assert self.token is not None

    encoded_token = quote(self.token.get_secret_value(), safe="")
    uri = f"databricks://token:{encoded_token}@{self.host}:443{self.http_path}"

    # The Databricks Go SQL driver reads catalog/schema from the DSN query
    # string, so append them when set to honour the default namespace.
    params: dict[str, str] = {}
    if self.catalog is not None:
        params["catalog"] = self.catalog
    if self.schema_ is not None:
        params["schema"] = self.schema_
    if params:
        uri = f"{uri}?{urlencode(params, quote_via=quote)}"

    return {"uri": uri}

DuckDBConfig

Bases: BaseWarehouseConfig

DuckDB warehouse configuration.

Covers all DuckDB ADBC connection parameters. Pool tuning fields (pool_size, max_overflow, timeout, recycle) are inherited from BaseWarehouseConfig and loaded from DUCKDB_* environment variables.

Example
DuckDBConfig(database="/data/warehouse.db", pool_size=5)
DuckDBConfig()  # in-memory, pool_size=1 enforced

database class-attribute instance-attribute

database: str = ':memory:'

File path or ':memory:'. Env: DUCKDB_DATABASE.

pool_size class-attribute instance-attribute

pool_size: int = 1

Number of connections in the pool. Default 1 for in-memory DuckDB.

In-memory DuckDB databases are isolated per connection — each pool connection gets a different empty DB. Use pool_size=1 for ':memory:', or set database to a file path if you need pool_size > 1. Setting pool_size > 1 with database=':memory:' raises ValidationError. Env: DUCKDB_POOL_SIZE.

read_only class-attribute instance-attribute

read_only: bool = False

Open the database in read-only mode. Env: DUCKDB_READ_ONLY.

to_adbc_kwargs

to_adbc_kwargs() -> dict[str, str]

Convert config to ADBC driver connection kwargs.

Returns:

Type Description
dict[str, str]

Dict with 'path' key (always) and 'access_mode' set to

dict[str, str]

'READ_ONLY' when read_only is True.

Source code in src/adbc_poolhouse/_duckdb_config.py
def to_adbc_kwargs(self) -> dict[str, str]:
    """
    Convert config to ADBC driver connection kwargs.

    Returns:
        Dict with ``'path'`` key (always) and ``'access_mode'`` set to
        ``'READ_ONLY'`` when ``read_only`` is True.
    """
    result: dict[str, str] = {"path": self.database}
    if self.read_only:
        result["access_mode"] = "READ_ONLY"
    return result

ConfigurationError

Bases: PoolhouseError, ValueError

Raised when a config model contains invalid field values.

Inherits from both PoolhouseError (library hierarchy) and ValueError (pydantic model_validator compatibility). When raised inside a pydantic @model_validator, pydantic wraps it in ValidationError --- which itself inherits from ValueError --- satisfying 'raises ValueError' test expectations.

Example
DuckDBConfig(database=":memory:", pool_size=2)
# raises pydantic.ValidationError (wraps ConfigurationError)

ConnectionBusyError

ConnectionBusyError(message: str | None = None)

Bases: PoolhouseError

Raised when one async connection is used concurrently from two tasks.

An ADBC connection permits serialized access (one call at a time) but not concurrent access. Each AsyncConnection belongs to exactly one task for its lifetime; sharing it across tasks in a task group is a bug. Rather than silently serialize the calls --- which would still let two tasks' statements interleave inside one transaction (driver-safe, logically corrupt) and hide the bug --- the async layer rejects the second concurrent caller with this error. The fix is to check out a separate connection per task from the pool.

Unlike ConfigurationError, this inherits PoolhouseError only (not ValueError): it signals a runtime misuse of the connection, not an invalid value.

Example
# FORBIDDEN --- aliasing one connection across tasks
async with await pool.connect() as conn:
    cur = conn.cursor()
    async with anyio.create_task_group() as tg:
        tg.start_soon(run_query, cur)  # task A
        tg.start_soon(run_query, cur)  # task B --- raises ConnectionBusyError

Initialize with the canonical message, or an override.

Parameters:

Name Type Description Default
message str | None

Custom error text. Defaults to the canonical connection-aliasing message.

None
Source code in src/adbc_poolhouse/_exceptions.py
def __init__(self, message: str | None = None) -> None:
    """
    Initialize with the canonical message, or an override.

    Args:
        message: Custom error text. Defaults to the canonical
            connection-aliasing message.
    """
    super().__init__(message if message is not None else self._MESSAGE)

PoolhouseError

Bases: Exception

Base exception for all adbc-poolhouse errors.

All library-specific exceptions inherit from this class. Consumers can use except PoolhouseError to catch any library error.

FlightSQLConfig

Bases: BaseWarehouseConfig

FlightSQL warehouse configuration.

Connects to any Apache Arrow Flight SQL server (e.g. Dremio, InfluxDB, DuckDB server mode, custom Flight SQL implementations).

Pool tuning fields are inherited and loaded from FLIGHTSQL_* env vars.

uri class-attribute instance-attribute

uri: str | None = None

gRPC endpoint URI. Env: FLIGHTSQL_URI. Format: grpc://host:port (plaintext) or grpc+tls://host:port (TLS).

username class-attribute instance-attribute

username: str | None = None

Username for HTTP-style basic auth. Env: FLIGHTSQL_USERNAME.

password class-attribute instance-attribute

password: SecretStr | None = None

Password for HTTP-style basic auth. Env: FLIGHTSQL_PASSWORD.

authorization_header class-attribute instance-attribute

authorization_header: SecretStr | None = None

Custom authorization header value (overrides username/password if set). Env: FLIGHTSQL_AUTHORIZATION_HEADER.

mtls_cert_chain class-attribute instance-attribute

mtls_cert_chain: str | None = None

mTLS certificate chain (PEM). Env: FLIGHTSQL_MTLS_CERT_CHAIN.

mtls_private_key class-attribute instance-attribute

mtls_private_key: SecretStr | None = None

mTLS private key (PEM). Env: FLIGHTSQL_MTLS_PRIVATE_KEY.

tls_root_certs class-attribute instance-attribute

tls_root_certs: str | None = None

Root CA certificate(s) in PEM format. Env: FLIGHTSQL_TLS_ROOT_CERTS.

tls_skip_verify class-attribute instance-attribute

tls_skip_verify: bool = False

Disable TLS certificate verification. Env: FLIGHTSQL_TLS_SKIP_VERIFY.

tls_override_hostname class-attribute instance-attribute

tls_override_hostname: str | None = None

Override the TLS hostname for SNI. Env: FLIGHTSQL_TLS_OVERRIDE_HOSTNAME.

connect_timeout class-attribute instance-attribute

connect_timeout: float | None = None

Connection timeout in seconds. Env: FLIGHTSQL_CONNECT_TIMEOUT.

query_timeout class-attribute instance-attribute

query_timeout: float | None = None

Query execution timeout in seconds. Env: FLIGHTSQL_QUERY_TIMEOUT.

fetch_timeout class-attribute instance-attribute

fetch_timeout: float | None = None

Result fetch timeout in seconds. Env: FLIGHTSQL_FETCH_TIMEOUT.

update_timeout class-attribute instance-attribute

update_timeout: float | None = None

DML update timeout in seconds. Env: FLIGHTSQL_UPDATE_TIMEOUT.

authority class-attribute instance-attribute

authority: str | None = None

Override gRPC authority header. Env: FLIGHTSQL_AUTHORITY.

max_msg_size class-attribute instance-attribute

max_msg_size: int | None = None

Maximum gRPC message size in bytes (driver default: 16 MiB). Env: FLIGHTSQL_MAX_MSG_SIZE.

with_cookie_middleware: bool = False

Enable gRPC cookie middleware (required by some servers for session management). Env: FLIGHTSQL_WITH_COOKIE_MIDDLEWARE.

to_adbc_kwargs

to_adbc_kwargs() -> dict[str, str]

Convert config to ADBC driver connection kwargs.

Maps FlightSQL config fields to their adbc.flight.sql.* key equivalents. Boolean defaults (tls_skip_verify, with_cookie_middleware) are always included as 'true'/ 'false' strings. Optional fields are omitted when None.

Returns:

Type Description
dict[str, str]

Dict of ADBC driver kwargs for adbc_driver_manager.dbapi.connect().

Source code in src/adbc_poolhouse/_flightsql_config.py
def to_adbc_kwargs(self) -> dict[str, str]:
    """
    Convert config to ADBC driver connection kwargs.

    Maps FlightSQL config fields to their ``adbc.flight.sql.*`` key
    equivalents. Boolean defaults (``tls_skip_verify``,
    ``with_cookie_middleware``) are always included as ``'true'``/
    ``'false'`` strings. Optional fields are omitted when ``None``.

    Returns:
        Dict of ADBC driver kwargs for ``adbc_driver_manager.dbapi.connect()``.
    """
    kwargs: dict[str, str] = {}

    # Connection endpoint
    if self.uri is not None:
        kwargs["uri"] = self.uri

    # Authentication
    if self.username is not None:
        kwargs["username"] = self.username
    if self.password is not None:
        kwargs["password"] = self.password.get_secret_value()  # pragma: allowlist secret
    if self.authorization_header is not None:
        kwargs["adbc.flight.sql.authorization_header"] = (
            self.authorization_header.get_secret_value()
        )

    # mTLS
    if self.mtls_cert_chain is not None:
        kwargs["adbc.flight.sql.client_option.mtls_cert_chain"] = self.mtls_cert_chain
    if self.mtls_private_key is not None:
        kwargs["adbc.flight.sql.client_option.mtls_private_key"] = (
            self.mtls_private_key.get_secret_value()
        )

    # TLS
    if self.tls_root_certs is not None:
        kwargs["adbc.flight.sql.client_option.tls_root_certs"] = self.tls_root_certs
    kwargs["adbc.flight.sql.client_option.tls_skip_verify"] = str(self.tls_skip_verify).lower()
    if self.tls_override_hostname is not None:
        kwargs["adbc.flight.sql.client_option.tls_override_hostname"] = (
            self.tls_override_hostname
        )

    # Timeouts
    if self.connect_timeout is not None:
        kwargs["adbc.flight.sql.rpc.timeout_seconds.connect"] = str(self.connect_timeout)
    if self.query_timeout is not None:
        kwargs["adbc.flight.sql.rpc.timeout_seconds.query"] = str(self.query_timeout)
    if self.fetch_timeout is not None:
        kwargs["adbc.flight.sql.rpc.timeout_seconds.fetch"] = str(self.fetch_timeout)
    if self.update_timeout is not None:
        kwargs["adbc.flight.sql.rpc.timeout_seconds.update"] = str(self.update_timeout)

    # gRPC options
    if self.authority is not None:
        kwargs["adbc.flight.sql.client_option.authority"] = self.authority
    if self.max_msg_size is not None:
        kwargs["adbc.flight.sql.client_option.with_max_msg_size"] = str(self.max_msg_size)
    kwargs["adbc.flight.sql.rpc.with_cookie_middleware"] = str(
        self.with_cookie_middleware
    ).lower()

    return kwargs

MSSQLConfig

Bases: BaseWarehouseConfig

Microsoft SQL Server / Azure SQL / Azure Fabric / Synapse Analytics configuration.

Uses the Columnar ADBC MSSQL driver (Foundry-distributed, not on PyPI). One class covers all Microsoft SQL variants via optional variant-specific fields: - SQL Server: use host + port + instance (or URI) - Azure SQL: use host + port, optionally fedauth for Entra ID / Azure AD auth - Azure Fabric / Synapse Analytics: use fedauth for managed identity or service principal authentication

Pool tuning fields are inherited and loaded from MSSQL_* env vars.

Note: This driver is distributed via the ADBC Driver Foundry, not PyPI. See the installation guide for Foundry setup instructions.

uri class-attribute instance-attribute

uri: str | None = None

Connection URI. Format: mssql://user:pass@host[:port][/instance][?params] # pragma: allowlist secret Also accepts the sqlserver:// scheme. Env: MSSQL_URI.

host class-attribute instance-attribute

host: str | None = None

Hostname or IP address. Alternative to URI-based connection. Env: MSSQL_HOST.

port class-attribute instance-attribute

port: int | None = None

Port number. Default: 1433. Env: MSSQL_PORT.

instance class-attribute instance-attribute

instance: str | None = None

SQL Server named instance (e.g. 'SQLExpress'). Env: MSSQL_INSTANCE.

user class-attribute instance-attribute

user: str | None = None

SQL auth username. Env: MSSQL_USER.

password class-attribute instance-attribute

password: SecretStr | None = None

SQL auth password. Env: MSSQL_PASSWORD.

database class-attribute instance-attribute

database: str | None = None

Target database name. Env: MSSQL_DATABASE.

trust_server_certificate class-attribute instance-attribute

trust_server_certificate: bool = False

Accept self-signed TLS certificates. Enable for local development. Env: MSSQL_TRUST_SERVER_CERTIFICATE.

connection_timeout class-attribute instance-attribute

connection_timeout: int | None = None

Connection timeout in seconds. Env: MSSQL_CONNECTION_TIMEOUT.

fedauth class-attribute instance-attribute

fedauth: str | None = None

Federated authentication method for Entra ID / Azure AD. Used for Azure SQL, Azure Fabric, and Synapse Analytics. Values: 'ActiveDirectoryPassword', 'ActiveDirectoryMsi', 'ActiveDirectoryServicePrincipal', 'ActiveDirectoryInteractive'. Env: MSSQL_FEDAUTH.

to_adbc_kwargs

to_adbc_kwargs() -> dict[str, str]

Convert config to ADBC driver connection kwargs.

Supports two modes:

  • URI mode (uri set): returns {uri: ...}.
  • Decomposed mode: maps individual fields to their ADBC key equivalents. trust_server_certificate is always included as a 'true'/'false' string.

Returns:

Type Description
dict[str, str]

Dict of ADBC driver kwargs for adbc_driver_manager.dbapi.connect().

Source code in src/adbc_poolhouse/_mssql_config.py
def to_adbc_kwargs(self) -> dict[str, str]:
    """
    Convert config to ADBC driver connection kwargs.

    Supports two modes:

    - URI mode (``uri`` set): returns ``{uri: ...}``.
    - Decomposed mode: maps individual fields to their ADBC key
      equivalents. ``trust_server_certificate`` is always included
      as a ``'true'``/``'false'`` string.

    Returns:
        Dict of ADBC driver kwargs for ``adbc_driver_manager.dbapi.connect()``.
    """
    kwargs: dict[str, str] = {}

    # URI-first: if uri is set, use it as the primary connection spec
    if self.uri is not None:
        kwargs["uri"] = self.uri
        return kwargs

    # Decomposed fields (include only if not None)
    if self.host is not None:
        kwargs["host"] = self.host
    if self.port is not None:
        kwargs["port"] = str(self.port)
    if self.instance is not None:
        kwargs["instance"] = self.instance
    if self.user is not None:
        kwargs["username"] = self.user
    if self.password is not None:
        kwargs["password"] = self.password.get_secret_value()  # pragma: allowlist secret
    if self.database is not None:
        kwargs["database"] = self.database

    # Boolean flag (always include)
    kwargs["trustServerCertificate"] = str(self.trust_server_certificate).lower()

    if self.connection_timeout is not None:
        kwargs["connectionTimeout"] = str(self.connection_timeout)
    if self.fedauth is not None:
        kwargs["fedauth"] = self.fedauth

    return kwargs

MySQLConfig

Bases: BaseWarehouseConfig

MySQL warehouse configuration.

Uses the Columnar ADBC MySQL driver (Foundry-distributed, not on PyPI). Install via the ADBC Driver Foundry (see DEVELOP.md for setup instructions).

Supports two connection modes:

  • URI mode: set uri with the full MySQL connection string.
  • Decomposed mode: set host, user, and database together. password is optional — MySQL supports passwordless connections. port defaults to 3306.

At least one mode must be fully specified — construction raises ConfigurationError if neither is provided.

Pool tuning fields are inherited and loaded from MYSQL_* env vars.

Note: This driver is distributed via the ADBC Driver Foundry, not PyPI. See the installation guide for Foundry setup instructions.

uri class-attribute instance-attribute

uri: SecretStr | None = None

Full MySQL connection URI. May contain credentials — stored as SecretStr. Env: MYSQL_URI.

host class-attribute instance-attribute

host: str | None = None

MySQL hostname. Alternative to embedding host in URI. Env: MYSQL_HOST.

port class-attribute instance-attribute

port: int = 3306

MySQL port. Default: 3306. Env: MYSQL_PORT.

user class-attribute instance-attribute

user: str | None = None

MySQL username. Env: MYSQL_USER.

password class-attribute instance-attribute

password: SecretStr | None = None

MySQL password. Optional — MySQL supports passwordless connections. Env: MYSQL_PASSWORD.

database class-attribute instance-attribute

database: str | None = None

MySQL database name. Env: MYSQL_DATABASE.

check_connection_spec

check_connection_spec() -> Self

Raise ConfigurationError if neither uri nor all minimum decomposed fields are set.

Source code in src/adbc_poolhouse/_mysql_config.py
@model_validator(mode="after")
def check_connection_spec(self) -> Self:
    """Raise ConfigurationError if neither uri nor all minimum decomposed fields are set."""
    has_uri = self.uri is not None
    has_decomposed = (
        self.host is not None and self.user is not None and self.database is not None
    )
    if not has_uri and not has_decomposed:
        raise ConfigurationError(
            "MySQLConfig requires either 'uri' or all of 'host', 'user', "
            "and 'database'. Got none of these."
        )
    return self

to_adbc_kwargs

to_adbc_kwargs() -> dict[str, str]

Convert MySQL config fields to ADBC driver kwargs.

Supports two modes:

  • URI mode (uri set): extracts SecretStr value and returns {"uri": ...}.
  • Decomposed mode: builds a Go DSN from user, password, host, port, and database. Password is URL-encoded via urllib.parse.quote with safe="".

Returns:

Type Description
dict[str, str]

ADBC driver kwargs for adbc_driver_manager.dbapi.connect().

Source code in src/adbc_poolhouse/_mysql_config.py
def to_adbc_kwargs(self) -> dict[str, str]:
    """
    Convert MySQL config fields to ADBC driver kwargs.

    Supports two modes:

    - **URI mode** (``uri`` set): extracts ``SecretStr`` value and returns
      ``{"uri": ...}``.
    - **Decomposed mode**: builds a Go DSN from ``user``, ``password``,
      ``host``, ``port``, and ``database``. Password is URL-encoded via
      `urllib.parse.quote` with ``safe=""``.

    Returns:
        ADBC driver kwargs for ``adbc_driver_manager.dbapi.connect()``.
    """
    if self.uri is not None:
        return {"uri": self.uri.get_secret_value()}

    # Decomposed mode -- model_validator guarantees host, user, database.
    assert self.host is not None
    assert self.user is not None
    assert self.database is not None

    if self.password is not None:
        encoded_pass = quote(self.password.get_secret_value(), safe="")
        uri = f"{self.user}:{encoded_pass}@tcp({self.host}:{self.port})/{self.database}"
    else:
        uri = f"{self.user}@tcp({self.host}:{self.port})/{self.database}"

    return {"uri": uri}

PostgreSQLConfig

Bases: BaseWarehouseConfig

PostgreSQL warehouse configuration.

The PostgreSQL ADBC driver wraps libpq. Specify the connection either as a full URI or via individual fields. If neither is provided, libpq falls back to its own environment variables (PGHOST, PGUSER, etc.).

Pool tuning fields are inherited and loaded from POSTGRESQL_* env vars.

Example
PostgreSQLConfig(uri="postgresql://me:s3cret@host/mydb")  # pragma: allowlist secret
PostgreSQLConfig(host="db.example.com", user="me", database="mydb")

uri class-attribute instance-attribute

uri: str | None = None

libpq connection URI. Takes precedence over individual fields. Format: postgresql://[user[:password]@][host][:port][/dbname][?params] Env: POSTGRESQL_URI.

host class-attribute instance-attribute

host: str | None = None

Database hostname or IP address. Env: POSTGRESQL_HOST.

port class-attribute instance-attribute

port: int | None = None

Database port. Defaults to 5432 when omitted. Env: POSTGRESQL_PORT.

user class-attribute instance-attribute

user: str | None = None

Database username. Env: POSTGRESQL_USER.

password class-attribute instance-attribute

password: SecretStr | None = None

Database password. Env: POSTGRESQL_PASSWORD.

database class-attribute instance-attribute

database: str | None = None

Database name. Env: POSTGRESQL_DATABASE.

sslmode class-attribute instance-attribute

sslmode: str | None = None

SSL mode. Accepted values: disable, allow, prefer, require, verify-ca, verify-full. Env: POSTGRESQL_SSLMODE.

use_copy class-attribute instance-attribute

use_copy: bool = True

Use PostgreSQL COPY protocol for bulk query execution (driver default: True). Disable if COPY triggers permission errors. Env: POSTGRESQL_USE_COPY.

to_adbc_kwargs

to_adbc_kwargs() -> dict[str, str]

Convert PostgreSQL config fields to ADBC driver kwargs.

Supports three modes:

  • URI mode (uri set): passed directly as {"uri": ...}.
  • Decomposed mode: builds a libpq URI from host, port, user, password, database, and sslmode. Password is URL-encoded via urllib.parse.quote with safe="".
  • Empty mode: returns {} so libpq resolves from env vars.

Returns:

Type Description
dict[str, str]

ADBC driver kwargs for adbc_driver_manager.dbapi.connect().

Source code in src/adbc_poolhouse/_postgresql_config.py
def to_adbc_kwargs(self) -> dict[str, str]:
    """
    Convert PostgreSQL config fields to ADBC driver kwargs.

    Supports three modes:

    - **URI mode** (``uri`` set): passed directly as ``{"uri": ...}``.
    - **Decomposed mode**: builds a libpq URI from ``host``, ``port``,
      ``user``, ``password``, ``database``, and ``sslmode``. Password is
      URL-encoded via `urllib.parse.quote` with ``safe=""``.
    - **Empty mode**: returns ``{}`` so libpq resolves from env vars.

    Returns:
        ADBC driver kwargs for ``adbc_driver_manager.dbapi.connect()``.
    """
    if self.uri is not None:
        return {"uri": self.uri}

    # Decomposed mode -- build URI only if at least one field is set.
    has_fields = any([self.host, self.user, self.password, self.database, self.sslmode])
    if not has_fields:
        return {}

    uri = "postgresql://"

    if self.user is not None:
        uri += quote(self.user, safe="")
        if self.password is not None:
            uri += ":" + quote(self.password.get_secret_value(), safe="")
        uri += "@"

    if self.host is not None:
        uri += self.host

    if self.port is not None:
        uri += f":{self.port}"

    if self.database is not None:
        uri += f"/{self.database}"

    if self.sslmode is not None:
        uri += f"?sslmode={self.sslmode}"

    return {"uri": uri}

QuackConfig

Bases: BaseWarehouseConfig

Quack warehouse configuration.

Targets the DuckDB Quack remote protocol via the adbc-driver-quack PyPI driver. The driver is on an alpha release track, so install with the --pre flag (the version constraint is in pyproject.toml):

pip install --pre adbc-poolhouse[quack]

Two connection modes are supported:

  • URI mode: set uri to a full quack://host[:port] string.
  • Decomposed mode: set host and optionally port. The URI is rebuilt as quack://{host}:{port} when port is set, or quack://{host} when port is None.

Exactly one mode must be specified. Setting both uri and host, or neither, raises ConfigurationError (wrapped as a pydantic ValidationError).

The driver's URI cannot embed credentials, so uri is a plain str (not SecretStr). The token field passes via the adbc.quack.token kwarg and is never embedded in the URI. The tls flag emits adbc.quack.tls only when True — when False (the default), the kwarg is omitted entirely so the driver's own default applies.

Pool tuning fields are inherited and loaded from QUACK_* env vars.

Example
from adbc_poolhouse import QuackConfig, create_pool

# URI mode
config = QuackConfig(uri="quack://example.com:1234", token="secret", tls=True)

# Decomposed mode
config = QuackConfig(host="example.com", port=1234, token="secret")

pool = create_pool(config)

See create_pool for pool creation.

uri class-attribute instance-attribute

uri: str | None = None

Full connection URI quack://host[:port]. The driver's URI cannot embed credentials, so this is a plain str (not SecretStr). Env: QUACK_URI.

host class-attribute instance-attribute

host: str | None = None

Quack server hostname. Alternative to URI mode. Env: QUACK_HOST.

port class-attribute instance-attribute

port: int | None = None

Quack server port. Optional even in decomposed mode. Env: QUACK_PORT.

token class-attribute instance-attribute

token: SecretStr | None = None

Bearer token. Passes via adbc.quack.token kwarg, never embedded in the URI, never URL-encoded. Env: QUACK_TOKEN.

tls class-attribute instance-attribute

tls: bool = False

Enable TLS. When False (default), the adbc.quack.tls kwarg is omitted entirely (driver default is "false"). Env: QUACK_TLS.

check_connection_spec

check_connection_spec() -> Self

Raise ConfigurationError when both modes are set or neither is set.

Source code in src/adbc_poolhouse/_quack_config.py
@model_validator(mode="after")
def check_connection_spec(self) -> Self:
    """Raise ConfigurationError when both modes are set or neither is set."""
    has_uri = self.uri is not None
    has_host = self.host is not None
    if has_uri and has_host:
        raise ConfigurationError("QuackConfig accepts either 'uri' or 'host', not both.")
    if not has_uri and not has_host:
        raise ConfigurationError("QuackConfig requires either 'uri' or 'host'. Got neither.")
    return self

to_adbc_kwargs

to_adbc_kwargs() -> dict[str, str]

Convert config to ADBC driver connection kwargs.

Returns:

Type Description
dict[str, str]

A dict with uri always set. adbc.quack.token is included

dict[str, str]

when token is set; adbc.quack.tls is included only when

dict[str, str]

tls=True (omitted on False — driver default is "false").

Source code in src/adbc_poolhouse/_quack_config.py
def to_adbc_kwargs(self) -> dict[str, str]:
    """
    Convert config to ADBC driver connection kwargs.

    Returns:
        A dict with `uri` always set. `adbc.quack.token` is included
        when `token` is set; `adbc.quack.tls` is included only when
        `tls=True` (omitted on False — driver default is "false").
    """
    if self.uri is not None:
        uri = self.uri
    else:
        assert self.host is not None  # model_validator guarantees
        uri = (
            f"quack://{self.host}:{self.port}"
            if self.port is not None
            else f"quack://{self.host}"
        )

    result: dict[str, str] = {"uri": uri}
    if self.token is not None:
        result["adbc.quack.token"] = self.token.get_secret_value()  # pragma: allowlist secret
    if self.tls:
        result["adbc.quack.tls"] = "true"
    return result

RedshiftConfig

Bases: BaseWarehouseConfig

Redshift warehouse configuration.

Uses the Columnar ADBC Redshift driver (Foundry-distributed, not on PyPI). Supports provisioned clusters (standard and IAM auth) and Redshift Serverless.

Pool tuning fields are inherited and loaded from REDSHIFT_* env vars.

Note: This driver is distributed via the ADBC Driver Foundry, not PyPI. See the installation guide for Foundry setup instructions.

uri class-attribute instance-attribute

uri: str | None = None

Connection URI: redshift://[user:password@]host[:port]/dbname[?params] Use redshift:///dbname for automatic endpoint discovery. Env: REDSHIFT_URI.

host class-attribute instance-attribute

host: str | None = None

Redshift cluster hostname. Alternative to URI. Env: REDSHIFT_HOST.

port class-attribute instance-attribute

port: int | None = None

Port number. Default: 5439. Env: REDSHIFT_PORT.

user class-attribute instance-attribute

user: str | None = None

Database username. Env: REDSHIFT_USER.

password class-attribute instance-attribute

password: SecretStr | None = None

Database password. Env: REDSHIFT_PASSWORD.

database class-attribute instance-attribute

database: str | None = None

Target database name. Env: REDSHIFT_DATABASE.

cluster_type class-attribute instance-attribute

cluster_type: str | None = None

Cluster variant: 'redshift' (standard), 'redshift-iam', or 'redshift-serverless'. Env: REDSHIFT_CLUSTER_TYPE.

cluster_identifier class-attribute instance-attribute

cluster_identifier: str | None = None

Provisioned cluster identifier (required for IAM auth). Env: REDSHIFT_CLUSTER_IDENTIFIER.

workgroup_name class-attribute instance-attribute

workgroup_name: str | None = None

Serverless workgroup name. Env: REDSHIFT_WORKGROUP_NAME.

aws_region class-attribute instance-attribute

aws_region: str | None = None

AWS region (e.g. 'us-west-2'). Env: REDSHIFT_AWS_REGION.

aws_access_key_id class-attribute instance-attribute

aws_access_key_id: str | None = None

AWS IAM access key ID. Env: REDSHIFT_AWS_ACCESS_KEY_ID.

aws_secret_access_key class-attribute instance-attribute

aws_secret_access_key: SecretStr | None = None

AWS IAM secret access key. Env: REDSHIFT_AWS_SECRET_ACCESS_KEY.

sslmode class-attribute instance-attribute

sslmode: str | None = None

SSL mode (e.g. 'require', 'verify-full'). Env: REDSHIFT_SSLMODE.

to_adbc_kwargs

to_adbc_kwargs() -> dict[str, str]

Convert Redshift config fields to ADBC driver kwargs.

Supports two connection modes:

  • URI mode (uri set): passed directly as {"uri": ...}.
  • Decomposed mode: builds a redshift:// URI from host, port, user, password, database, and sslmode. Password is URL-encoded via urllib.parse.quote with safe="".

IAM and cluster fields (cluster_type, cluster_identifier, workgroup_name, aws_region, aws_access_key_id, aws_secret_access_key) are always translated as separate driver kwargs when set, regardless of connection mode.

Returns:

Type Description
dict[str, str]

ADBC driver kwargs for adbc_driver_manager.dbapi.connect().

Source code in src/adbc_poolhouse/_redshift_config.py
def to_adbc_kwargs(self) -> dict[str, str]:
    """
    Convert Redshift config fields to ADBC driver kwargs.

    Supports two connection modes:

    - **URI mode** (``uri`` set): passed directly as ``{"uri": ...}``.
    - **Decomposed mode**: builds a ``redshift://`` URI from ``host``,
      ``port``, ``user``, ``password``, ``database``, and ``sslmode``.
      Password is URL-encoded via `urllib.parse.quote` with
      ``safe=""``.

    IAM and cluster fields (``cluster_type``, ``cluster_identifier``,
    ``workgroup_name``, ``aws_region``, ``aws_access_key_id``,
    ``aws_secret_access_key``) are always translated as separate driver
    kwargs when set, regardless of connection mode.

    Returns:
        ADBC driver kwargs for ``adbc_driver_manager.dbapi.connect()``.
    """
    kwargs: dict[str, str] = {}

    # URI: explicit passthrough or build from individual fields
    if self.uri is not None:
        kwargs["uri"] = self.uri
    elif any([self.host, self.user, self.password, self.database, self.sslmode]):
        kwargs["uri"] = self._build_uri()

    # IAM/cluster params
    if self.cluster_type is not None:
        kwargs["redshift.cluster_type"] = self.cluster_type
    if self.cluster_identifier is not None:
        kwargs["redshift.cluster_identifier"] = self.cluster_identifier
    if self.workgroup_name is not None:
        kwargs["redshift.workgroup_name"] = self.workgroup_name
    if self.aws_region is not None:
        kwargs["aws_region"] = self.aws_region
    if self.aws_access_key_id is not None:
        kwargs["aws_access_key_id"] = self.aws_access_key_id
    if self.aws_secret_access_key is not None:
        kwargs["aws_secret_access_key"] = self.aws_secret_access_key.get_secret_value()

    return kwargs

SnowflakeConfig

Bases: BaseWarehouseConfig

Snowflake warehouse configuration.

Supports all authentication methods provided by adbc-driver-snowflake: password, JWT (private_key_path / private_key_pem), external browser, OAuth, MFA, Okta, PAT, and workload identity federation (WIF).

Pool tuning fields (pool_size, max_overflow, timeout, recycle) are inherited and loaded from SNOWFLAKE_* environment variables.

Example
SnowflakeConfig(account="myorg-myaccount", user="me", password="...")
SnowflakeConfig(account="myorg", user="me", private_key_path=Path("/keys/rsa.p8"))

account instance-attribute

account: str

Snowflake account identifier (e.g. 'myorg-myaccount'). Env: SNOWFLAKE_ACCOUNT.

user class-attribute instance-attribute

user: str | None = None

Username. Required for most auth methods. Env: SNOWFLAKE_USER.

password class-attribute instance-attribute

password: SecretStr | None = None

Password for basic auth. Env: SNOWFLAKE_PASSWORD.

auth_type class-attribute instance-attribute

auth_type: str | None = None

Auth method: auth_jwt, auth_ext_browser, auth_oauth, auth_mfa, auth_okta, auth_pat, auth_wif. Env: SNOWFLAKE_AUTH_TYPE.

private_key_path class-attribute instance-attribute

private_key_path: Path | None = None

File path to a PKCS1 or PKCS8 private key file. Mutually exclusive with private_key_pem. Env: SNOWFLAKE_PRIVATE_KEY_PATH.

private_key_pem class-attribute instance-attribute

private_key_pem: SecretStr | None = None

Inline PEM-encoded PKCS8 private key (encrypted or unencrypted). Mutually exclusive with private_key_path. Env: SNOWFLAKE_PRIVATE_KEY_PEM.

private_key_passphrase class-attribute instance-attribute

private_key_passphrase: SecretStr | None = None

Passphrase to decrypt an encrypted PKCS8 key. Env: SNOWFLAKE_PRIVATE_KEY_PASSPHRASE.

jwt_expire_timeout class-attribute instance-attribute

jwt_expire_timeout: str | None = None

JWT expiry duration (e.g. '300ms', '1m30s'). Env: SNOWFLAKE_JWT_EXPIRE_TIMEOUT.

oauth_token class-attribute instance-attribute

oauth_token: SecretStr | None = None

Bearer token for auth_oauth. Env: SNOWFLAKE_OAUTH_TOKEN.

okta_url class-attribute instance-attribute

okta_url: str | None = None

Okta server URL required for auth_okta. Env: SNOWFLAKE_OKTA_URL.

identity_provider class-attribute instance-attribute

identity_provider: str | None = None

Identity provider for auth_wif. Env: SNOWFLAKE_IDENTITY_PROVIDER.

database class-attribute instance-attribute

database: str | None = None

Default database. Env: SNOWFLAKE_DATABASE.

schema_ class-attribute instance-attribute

schema_: str | None = Field(
    default=None, validation_alias="schema", alias="schema"
)

Default schema. Python attribute is schema_ to avoid Pydantic conflicts; env var is SNOWFLAKE_SCHEMA. Env: SNOWFLAKE_SCHEMA.

warehouse class-attribute instance-attribute

warehouse: str | None = None

Snowflake virtual warehouse. Env: SNOWFLAKE_WAREHOUSE.

role class-attribute instance-attribute

role: str | None = None

Snowflake role. Env: SNOWFLAKE_ROLE.

region class-attribute instance-attribute

region: str | None = None

Snowflake region (if not embedded in account). Env: SNOWFLAKE_REGION.

host class-attribute instance-attribute

host: str | None = None

Explicit hostname (alternative to account-derived URI). Env: SNOWFLAKE_HOST.

port class-attribute instance-attribute

port: int | None = None

Connection port. Env: SNOWFLAKE_PORT.

protocol class-attribute instance-attribute

protocol: str | None = None

Protocol: 'http' or 'https'. Env: SNOWFLAKE_PROTOCOL.

login_timeout class-attribute instance-attribute

login_timeout: str | None = None

Login retry timeout duration string. Env: SNOWFLAKE_LOGIN_TIMEOUT.

request_timeout class-attribute instance-attribute

request_timeout: str | None = None

Request retry timeout duration string. Env: SNOWFLAKE_REQUEST_TIMEOUT.

client_timeout class-attribute instance-attribute

client_timeout: str | None = None

Network roundtrip timeout duration string. Env: SNOWFLAKE_CLIENT_TIMEOUT.

tls_skip_verify class-attribute instance-attribute

tls_skip_verify: bool = False

Disable TLS certificate verification. Env: SNOWFLAKE_TLS_SKIP_VERIFY.

ocsp_fail_open_mode class-attribute instance-attribute

ocsp_fail_open_mode: bool = True

OCSP fail-open mode (True = allow connection on OCSP errors). Env: SNOWFLAKE_OCSP_FAIL_OPEN_MODE.

keep_session_alive class-attribute instance-attribute

keep_session_alive: bool = False

Prevent session expiry during long operations. Env: SNOWFLAKE_KEEP_SESSION_ALIVE.

app_name class-attribute instance-attribute

app_name: str | None = None

Application identifier sent to Snowflake. Env: SNOWFLAKE_APP_NAME.

disable_telemetry class-attribute instance-attribute

disable_telemetry: bool = False

Disable Snowflake usage telemetry. Env: SNOWFLAKE_DISABLE_TELEMETRY.

cache_mfa_token class-attribute instance-attribute

cache_mfa_token: bool = False

Cache MFA token for subsequent connections. Env: SNOWFLAKE_CACHE_MFA_TOKEN.

store_temp_creds class-attribute instance-attribute

store_temp_creds: bool = False

Cache ID token for SSO. Env: SNOWFLAKE_STORE_TEMP_CREDS.

check_private_key_exclusion

check_private_key_exclusion() -> Self

Raise ValidationError if both private_key_path and private_key_pem are set.

Source code in src/adbc_poolhouse/_snowflake_config.py
@model_validator(mode="after")
def check_private_key_exclusion(self) -> Self:
    """Raise ValidationError if both private_key_path and private_key_pem are set."""
    if self.private_key_path is not None and self.private_key_pem is not None:
        raise ValueError(
            "Provide only one of private_key_path (path to a PKCS1/PKCS8 "
            "private key file) or private_key_pem (inline PEM-encoded key "
            "content), not both. Use private_key_path for a key file, or "
            "private_key_pem for inline PEM content."
        )
    return self

to_adbc_kwargs

to_adbc_kwargs() -> dict[str, str]

Convert config to ADBC driver connection kwargs.

Returns a dict[str, str] suitable for passing as db_kwargs to adbc_driver_manager.dbapi.connect(). All values are strings; None fields are omitted. Boolean fields are always included as 'true'/'false' strings.

Key names follow adbc_driver_snowflake DatabaseOptions and AuthType enums. 'username' and 'password' are plain string keys (not prefixed with 'adbc.snowflake.sql.*').

Source code in src/adbc_poolhouse/_snowflake_config.py
def to_adbc_kwargs(self) -> dict[str, str]:
    """
    Convert config to ADBC driver connection kwargs.

    Returns a dict[str, str] suitable for passing as ``db_kwargs`` to
    ``adbc_driver_manager.dbapi.connect()``. All values are strings;
    None fields are omitted. Boolean fields are always included as
    ``'true'``/``'false'`` strings.

    Key names follow adbc_driver_snowflake ``DatabaseOptions`` and
    ``AuthType`` enums. ``'username'`` and ``'password'`` are plain
    string keys (not prefixed with ``'adbc.snowflake.sql.*'``).
    """
    kwargs: dict[str, str] = {}

    # --- Identity (always include) ---
    kwargs["adbc.snowflake.sql.account"] = self.account

    # --- Auth (include only if not None) ---
    if self.user is not None:
        kwargs["username"] = self.user
    if self.password is not None:
        kwargs["password"] = self.password.get_secret_value()  # pragma: allowlist secret
    if self.auth_type is not None:
        kwargs["adbc.snowflake.sql.auth_type"] = self.auth_type

    # --- JWT / private key (include only if not None) ---
    if self.private_key_path is not None:
        kwargs["adbc.snowflake.sql.client_option.jwt_private_key"] = str(self.private_key_path)
    if self.private_key_pem is not None:
        kwargs["adbc.snowflake.sql.client_option.jwt_private_key_pkcs8_value"] = (
            self.private_key_pem.get_secret_value()  # pragma: allowlist secret
        )
    if self.private_key_passphrase is not None:
        kwargs["adbc.snowflake.sql.client_option.jwt_private_key_pkcs8_password"] = (
            self.private_key_passphrase.get_secret_value()  # pragma: allowlist secret
        )
    if self.jwt_expire_timeout is not None:
        kwargs["adbc.snowflake.sql.client_option.jwt_expire_timeout"] = self.jwt_expire_timeout

    # --- OAuth / Okta / WIF (include only if not None) ---
    if self.oauth_token is not None:
        kwargs["adbc.snowflake.sql.client_option.auth_token"] = (
            self.oauth_token.get_secret_value()  # pragma: allowlist secret
        )
    if self.okta_url is not None:
        kwargs["adbc.snowflake.sql.client_option.okta_url"] = self.okta_url
    if self.identity_provider is not None:
        kwargs["adbc.snowflake.sql.client_option.identity_provider"] = self.identity_provider

    # --- Session / scope (include only if not None) ---
    if self.database is not None:
        kwargs["adbc.snowflake.sql.db"] = self.database
    if self.schema_ is not None:
        kwargs["adbc.snowflake.sql.schema"] = self.schema_
    if self.warehouse is not None:
        kwargs["adbc.snowflake.sql.warehouse"] = self.warehouse
    if self.role is not None:
        kwargs["adbc.snowflake.sql.role"] = self.role
    if self.region is not None:
        kwargs["adbc.snowflake.sql.region"] = self.region

    # --- Connection (include only if not None) ---
    if self.host is not None:
        kwargs["adbc.snowflake.sql.uri.host"] = self.host
    if self.port is not None:
        kwargs["adbc.snowflake.sql.uri.port"] = str(self.port)
    if self.protocol is not None:
        kwargs["adbc.snowflake.sql.uri.protocol"] = self.protocol

    # --- Timeouts (include only if not None) ---
    if self.login_timeout is not None:
        kwargs["adbc.snowflake.sql.client_option.login_timeout"] = self.login_timeout
    if self.request_timeout is not None:
        kwargs["adbc.snowflake.sql.client_option.request_timeout"] = self.request_timeout
    if self.client_timeout is not None:
        kwargs["adbc.snowflake.sql.client_option.client_timeout"] = self.client_timeout

    # --- Boolean flags (always include) ---
    kwargs["adbc.snowflake.sql.client_option.tls_skip_verify"] = str(
        self.tls_skip_verify
    ).lower()
    kwargs["adbc.snowflake.sql.client_option.ocsp_fail_open_mode"] = str(
        self.ocsp_fail_open_mode
    ).lower()
    kwargs["adbc.snowflake.sql.client_option.keep_session_alive"] = str(
        self.keep_session_alive
    ).lower()
    kwargs["adbc.snowflake.sql.client_option.disable_telemetry"] = str(
        self.disable_telemetry
    ).lower()
    kwargs["adbc.snowflake.sql.client_option.cache_mfa_token"] = str(
        self.cache_mfa_token
    ).lower()
    kwargs["adbc.snowflake.sql.client_option.store_temp_creds"] = str(
        self.store_temp_creds
    ).lower()

    # --- Misc (include only if not None) ---
    if self.app_name is not None:
        kwargs["adbc.snowflake.sql.client_option.app_name"] = self.app_name

    return kwargs

SQLiteConfig

Bases: BaseWarehouseConfig

SQLite warehouse configuration.

Covers SQLite ADBC connection parameters. Pool tuning fields (pool_size, max_overflow, timeout, recycle) are inherited from BaseWarehouseConfig and loaded from SQLITE_* environment variables.

Unlike DuckDB, an SQLite in-memory database is shared across all connections in the pool. This means pool_size > 1 with database=':memory:' is almost always unintended (connection state races across a single shared DB), so it is rejected by a validator.

Example
SQLiteConfig(database="/data/warehouse.db", pool_size=5)
SQLiteConfig()  # in-memory, pool_size=1 enforced

database class-attribute instance-attribute

database: str = ':memory:'

File path or ':memory:'. Env: SQLITE_DATABASE.

pool_size class-attribute instance-attribute

pool_size: int = 1

Number of connections in the pool. Default 1 for in-memory SQLite.

SQLite in-memory databases are shared across all connections in the pool — unlike DuckDB, where each connection gets its own isolated empty DB. Use pool_size=1 for ':memory:', or set database to a file path if you need pool_size > 1. Setting pool_size > 1 with database=':memory:' raises ValidationError. Env: SQLITE_POOL_SIZE.

to_adbc_kwargs

to_adbc_kwargs() -> dict[str, str]

Convert config to ADBC driver connection kwargs.

Returns:

Type Description
dict[str, str]

Dict with a single 'uri' key set to the database path

dict[str, str]

(or ':memory:').

Source code in src/adbc_poolhouse/_sqlite_config.py
def to_adbc_kwargs(self) -> dict[str, str]:
    """
    Convert config to ADBC driver connection kwargs.

    Returns:
        Dict with a single ``'uri'`` key set to the database path
        (or ``':memory:'``).
    """
    return {"uri": self.database}

TrinoConfig

Bases: BaseWarehouseConfig

Trino warehouse configuration.

Uses the Columnar ADBC Trino driver (Foundry-distributed, not on PyPI). Supports URI-based or decomposed field connection specification.

Pool tuning fields are inherited and loaded from TRINO_* env vars.

Note: This driver is distributed via the ADBC Driver Foundry, not PyPI. See the installation guide for Foundry setup instructions.

uri class-attribute instance-attribute

uri: str | None = None

Connection URI. Format: trino://[user[:password]@]host[:port][/catalog[/schema]][?params] Env: TRINO_URI.

host class-attribute instance-attribute

host: str | None = None

Trino coordinator hostname. Alternative to URI. Env: TRINO_HOST.

port class-attribute instance-attribute

port: int | None = None

Trino coordinator port. Defaults: 8080 (HTTP), 8443 (HTTPS). Env: TRINO_PORT.

user class-attribute instance-attribute

user: str | None = None

Username. Env: TRINO_USER.

password class-attribute instance-attribute

password: SecretStr | None = None

Password (HTTPS connections only). Env: TRINO_PASSWORD.

catalog class-attribute instance-attribute

catalog: str | None = None

Default catalog. Env: TRINO_CATALOG.

schema_ class-attribute instance-attribute

schema_: str | None = Field(
    default=None, validation_alias="schema", alias="schema"
)

Default schema. Python attribute is schema_ to avoid Pydantic conflicts. Env: TRINO_SCHEMA.

ssl class-attribute instance-attribute

ssl: bool = True

Use HTTPS. Disable for local development clusters. Env: TRINO_SSL.

ssl_verify class-attribute instance-attribute

ssl_verify: bool = True

Verify SSL certificate. Env: TRINO_SSL_VERIFY.

source class-attribute instance-attribute

source: str | None = None

Application identifier sent to Trino coordinator. Env: TRINO_SOURCE.

to_adbc_kwargs

to_adbc_kwargs() -> dict[str, str]

Convert config to ADBC driver connection kwargs.

Supports two modes:

  • URI mode (uri set): returns {uri: ...}.
  • Decomposed mode: maps individual fields to their ADBC key equivalents. Boolean defaults (ssl, ssl_verify) are always included as 'true'/'false' strings.

Returns:

Type Description
dict[str, str]

Dict of ADBC driver kwargs for adbc_driver_manager.dbapi.connect().

Source code in src/adbc_poolhouse/_trino_config.py
def to_adbc_kwargs(self) -> dict[str, str]:
    """
    Convert config to ADBC driver connection kwargs.

    Supports two modes:

    - URI mode (``uri`` set): returns ``{uri: ...}``.
    - Decomposed mode: maps individual fields to their ADBC key
      equivalents. Boolean defaults (``ssl``, ``ssl_verify``) are
      always included as ``'true'``/``'false'`` strings.

    Returns:
        Dict of ADBC driver kwargs for ``adbc_driver_manager.dbapi.connect()``.
    """
    kwargs: dict[str, str] = {}

    # URI-first: if uri is set, use it as the primary connection spec
    if self.uri is not None:
        kwargs["uri"] = self.uri
        return kwargs

    # Decomposed fields (include only if not None)
    if self.host is not None:
        kwargs["host"] = self.host
    if self.port is not None:
        kwargs["port"] = str(self.port)
    if self.user is not None:
        kwargs["username"] = self.user
    if self.password is not None:
        kwargs["password"] = self.password.get_secret_value()  # pragma: allowlist secret
    if self.catalog is not None:
        kwargs["catalog"] = self.catalog
    if self.schema_ is not None:
        kwargs["schema"] = self.schema_

    # SSL fields (bool -> 'true'/'false' strings, always included)
    kwargs["ssl"] = str(self.ssl).lower()
    kwargs["ssl_verify"] = str(self.ssl_verify).lower()

    if self.source is not None:
        kwargs["source"] = self.source

    return kwargs

close_pool

close_pool(pool: QueuePool) -> None

Dispose a pool and close its underlying ADBC source connection.

Replaces the two-step pattern pool.dispose() followed by pool._adbc_source.close(). Always call this instead of calling pool.dispose() directly to avoid leaving the ADBC source connection open.

Parameters:

Name Type Description Default
pool QueuePool

A pool returned by create_pool.

required
Example
from adbc_poolhouse import DuckDBConfig, create_pool, close_pool

pool = create_pool(DuckDBConfig(database="/tmp/wh.db"))
close_pool(pool)
Source code in src/adbc_poolhouse/_pool_factory.py
def close_pool(pool: sqlalchemy.pool.QueuePool) -> None:
    """
    Dispose a pool and close its underlying ADBC source connection.

    Replaces the two-step pattern ``pool.dispose()`` followed by
    ``pool._adbc_source.close()``. Always call this instead of calling
    ``pool.dispose()`` directly to avoid leaving the ADBC source connection open.

    Args:
        pool: A pool returned by `create_pool`.

    Example:
        ```python
        from adbc_poolhouse import DuckDBConfig, create_pool, close_pool

        pool = create_pool(DuckDBConfig(database="/tmp/wh.db"))
        close_pool(pool)
        ```
    """
    pool.dispose()
    pool._adbc_source.close()  # type: ignore[attr-defined]

create_pool

create_pool(
    config: WarehouseConfig,
    *,
    pool_size: int = 5,
    max_overflow: int = 3,
    timeout: int = 30,
    recycle: int = 3600,
    pre_ping: bool = False,
) -> sqlalchemy.pool.QueuePool
create_pool(
    *,
    driver_path: str,
    db_kwargs: dict[str, str],
    entrypoint: str | None = None,
    pool_size: int = 5,
    max_overflow: int = 3,
    timeout: int = 30,
    recycle: int = 3600,
    pre_ping: bool = False,
) -> sqlalchemy.pool.QueuePool
create_pool(
    *,
    dbapi_module: str,
    db_kwargs: dict[str, str],
    pool_size: int = 5,
    max_overflow: int = 3,
    timeout: int = 30,
    recycle: int = 3600,
    pre_ping: bool = False,
) -> sqlalchemy.pool.QueuePool

Create a SQLAlchemy QueuePool backed by an ADBC driver.

Three call patterns are supported:

pool = create_pool(DuckDBConfig(...))           # from a config object
pool = create_pool(driver_path="...", ...)       # native ADBC driver
pool = create_pool(dbapi_module="...", ...)      # Python dbapi module

The config path extracts driver information from the config object's methods. The two raw paths accept driver arguments directly, bypassing config objects entirely.

Parameters:

Name Type Description Default
config WarehouseConfig | None

A warehouse config model instance (e.g. DuckDBConfig). Mutually exclusive with driver_path and dbapi_module.

None
driver_path str | None

Path to a native ADBC driver shared library, or a short driver name for manifest-based resolution. Requires db_kwargs. Mutually exclusive with config and dbapi_module.

None
db_kwargs dict[str, str] | None

ADBC connection keyword arguments as dict[str, str]. Required when using driver_path or dbapi_module.

None
entrypoint str | None

ADBC entry-point symbol. Only used with driver_path (e.g. "duckdb_adbc_init" for DuckDB). Default: None.

None
dbapi_module str | None

Dotted module name for a Python package implementing the ADBC dbapi interface (e.g. "adbc_driver_snowflake.dbapi" or a custom "my_driver.dbapi"). Requires db_kwargs. Mutually exclusive with config and driver_path.

None
pool_size int

Number of connections to keep in the pool. Default: 5.

5
max_overflow int

Extra connections allowed above pool_size. Default: 3.

3
timeout int

Seconds to wait for a connection before raising. Default: 30.

30
recycle int

Seconds before a connection is recycled. Default: 3600.

3600
pre_ping bool

Whether to ping connections before checkout. Default: False. Pre-ping does not function on a standalone QueuePool without a SQLAlchemy dialect; recycle is the preferred health mechanism.

False

Returns:

Type Description
QueuePool

A configured sqlalchemy.pool.QueuePool ready for use.

Raises:

Type Description
TypeError

If none of config, driver_path, or dbapi_module is provided, or if both driver_path and dbapi_module are provided.

ImportError

If the required ADBC driver is not installed.

Example

Config path:

from adbc_poolhouse import create_pool, close_pool
from adbc_poolhouse import DuckDBConfig

pool = create_pool(DuckDBConfig(database="/tmp/my.db"))
close_pool(pool)

Raw native driver path:

pool = create_pool(
    driver_path="/path/to/libduckdb.dylib",
    db_kwargs={"path": "/tmp/my.db"},
    entrypoint="duckdb_adbc_init",
)
close_pool(pool)
Source code in src/adbc_poolhouse/_pool_factory.py
def create_pool(
    config: WarehouseConfig | None = None,
    *,
    driver_path: str | None = None,
    db_kwargs: dict[str, str] | None = None,
    entrypoint: str | None = None,
    dbapi_module: str | None = None,
    pool_size: int = 5,
    max_overflow: int = 3,
    timeout: int = 30,
    recycle: int = 3600,
    pre_ping: bool = False,
) -> sqlalchemy.pool.QueuePool:
    """
    Create a SQLAlchemy QueuePool backed by an ADBC driver.

    Three call patterns are supported:

        pool = create_pool(DuckDBConfig(...))           # from a config object
        pool = create_pool(driver_path="...", ...)       # native ADBC driver
        pool = create_pool(dbapi_module="...", ...)      # Python dbapi module

    The config path extracts driver information from the config object's
    methods. The two raw paths accept driver arguments directly, bypassing
    config objects entirely.

    Args:
        config: A warehouse config model instance (e.g. ``DuckDBConfig``).
            Mutually exclusive with ``driver_path`` and ``dbapi_module``.
        driver_path: Path to a native ADBC driver shared library, or a
            short driver name for manifest-based resolution. Requires
            ``db_kwargs``. Mutually exclusive with ``config`` and
            ``dbapi_module``.
        db_kwargs: ADBC connection keyword arguments as ``dict[str, str]``.
            Required when using ``driver_path`` or ``dbapi_module``.
        entrypoint: ADBC entry-point symbol. Only used with ``driver_path``
            (e.g. ``"duckdb_adbc_init"`` for DuckDB). Default: ``None``.
        dbapi_module: Dotted module name for a Python package implementing
            the ADBC dbapi interface (e.g. ``"adbc_driver_snowflake.dbapi"``
            or a custom ``"my_driver.dbapi"``). Requires ``db_kwargs``.
            Mutually exclusive with ``config`` and ``driver_path``.
        pool_size: Number of connections to keep in the pool. Default: 5.
        max_overflow: Extra connections allowed above pool_size. Default: 3.
        timeout: Seconds to wait for a connection before raising. Default: 30.
        recycle: Seconds before a connection is recycled. Default: 3600.
        pre_ping: Whether to ping connections before checkout. Default: False.
            Pre-ping does not function on a standalone QueuePool without a
            SQLAlchemy dialect; recycle is the preferred health mechanism.

    Returns:
        A configured ``sqlalchemy.pool.QueuePool`` ready for use.

    Raises:
        TypeError: If none of ``config``, ``driver_path``, or ``dbapi_module``
            is provided, or if both ``driver_path`` and ``dbapi_module`` are
            provided.
        ImportError: If the required ADBC driver is not installed.

    Example:
        Config path:

        ```python
        from adbc_poolhouse import create_pool, close_pool
        from adbc_poolhouse import DuckDBConfig

        pool = create_pool(DuckDBConfig(database="/tmp/my.db"))
        close_pool(pool)
        ```

        Raw native driver path:

        ```python
        pool = create_pool(
            driver_path="/path/to/libduckdb.dylib",
            db_kwargs={"path": "/tmp/my.db"},
            entrypoint="duckdb_adbc_init",
        )
        close_pool(pool)
        ```
    """
    return _create_pool_impl(
        config,
        driver_path,
        db_kwargs,
        entrypoint,
        dbapi_module,
        pool_size,
        max_overflow,
        timeout,
        recycle,
        pre_ping,
    )

managed_pool

managed_pool(
    config: WarehouseConfig,
    *,
    pool_size: int = 5,
    max_overflow: int = 3,
    timeout: int = 30,
    recycle: int = 3600,
    pre_ping: bool = False,
) -> contextlib.AbstractContextManager[
    sqlalchemy.pool.QueuePool
]
managed_pool(
    *,
    driver_path: str,
    db_kwargs: dict[str, str],
    entrypoint: str | None = None,
    pool_size: int = 5,
    max_overflow: int = 3,
    timeout: int = 30,
    recycle: int = 3600,
    pre_ping: bool = False,
) -> contextlib.AbstractContextManager[
    sqlalchemy.pool.QueuePool
]
managed_pool(
    *,
    dbapi_module: str,
    db_kwargs: dict[str, str],
    pool_size: int = 5,
    max_overflow: int = 3,
    timeout: int = 30,
    recycle: int = 3600,
    pre_ping: bool = False,
) -> contextlib.AbstractContextManager[
    sqlalchemy.pool.QueuePool
]

Context manager that creates a pool and closes it on exit.

The pool is created when the with block is entered and disposed (via close_pool) when the block exits, whether normally or by exception.

Three call patterns are supported:

with managed_pool(DuckDBConfig(...)) as pool: ...          # config
with managed_pool(driver_path="...", ...) as pool: ...     # native
with managed_pool(dbapi_module="...", ...) as pool: ...    # dbapi

Parameters:

Name Type Description Default
config WarehouseConfig | None

A warehouse config model instance (e.g. DuckDBConfig). Mutually exclusive with driver_path and dbapi_module.

None
driver_path str | None

Path to a native ADBC driver shared library, or a short driver name for manifest-based resolution. Requires db_kwargs. Mutually exclusive with config and dbapi_module.

None
db_kwargs dict[str, str] | None

ADBC connection keyword arguments as dict[str, str]. Required when using driver_path or dbapi_module.

None
entrypoint str | None

ADBC entry-point symbol. Only used with driver_path (e.g. "duckdb_adbc_init" for DuckDB). Default: None.

None
dbapi_module str | None

Dotted module name for a Python package implementing the ADBC dbapi interface (e.g. "adbc_driver_snowflake.dbapi" or a custom "my_driver.dbapi"). Requires db_kwargs. Mutually exclusive with config and driver_path.

None
pool_size int

Number of connections to keep in the pool. Default: 5.

5
max_overflow int

Extra connections allowed above pool_size. Default: 3.

3
timeout int

Seconds to wait for a connection before raising. Default: 30.

30
recycle int

Seconds before a connection is recycled. Default: 3600.

3600
pre_ping bool

Whether to ping connections before checkout. Default: False.

False

Yields:

Type Description
QueuePool

A configured sqlalchemy.pool.QueuePool. The pool is automatically

QueuePool

closed when the with block exits.

Raises:

Type Description
TypeError

If none of config, driver_path, or dbapi_module is provided, or if both driver_path and dbapi_module are provided.

ImportError

If the required ADBC driver is not installed.

Example

Config path:

from adbc_poolhouse import DuckDBConfig, managed_pool

with managed_pool(DuckDBConfig(database="/tmp/wh.db")) as pool:
    with pool.connect() as conn:
        cursor = conn.cursor()
        cursor.execute("SELECT 1")

Raw native driver path:

with managed_pool(
    driver_path="/path/to/libduckdb.dylib",
    db_kwargs={"path": "/tmp/my.db"},
    entrypoint="duckdb_adbc_init",
) as pool:
    with pool.connect() as conn:
        cursor = conn.cursor()
        cursor.execute("SELECT 42")
Source code in src/adbc_poolhouse/_pool_factory.py
@contextlib.contextmanager
def managed_pool(
    config: WarehouseConfig | None = None,
    *,
    driver_path: str | None = None,
    db_kwargs: dict[str, str] | None = None,
    entrypoint: str | None = None,
    dbapi_module: str | None = None,
    pool_size: int = 5,
    max_overflow: int = 3,
    timeout: int = 30,
    recycle: int = 3600,
    pre_ping: bool = False,
) -> collections.abc.Generator[sqlalchemy.pool.QueuePool, None, None]:
    """
    Context manager that creates a pool and closes it on exit.

    The pool is created when the ``with`` block is entered and disposed
    (via `close_pool`) when the block exits, whether normally or by
    exception.

    Three call patterns are supported:

        with managed_pool(DuckDBConfig(...)) as pool: ...          # config
        with managed_pool(driver_path="...", ...) as pool: ...     # native
        with managed_pool(dbapi_module="...", ...) as pool: ...    # dbapi

    Args:
        config: A warehouse config model instance (e.g. ``DuckDBConfig``).
            Mutually exclusive with ``driver_path`` and ``dbapi_module``.
        driver_path: Path to a native ADBC driver shared library, or a
            short driver name for manifest-based resolution. Requires
            ``db_kwargs``. Mutually exclusive with ``config`` and
            ``dbapi_module``.
        db_kwargs: ADBC connection keyword arguments as ``dict[str, str]``.
            Required when using ``driver_path`` or ``dbapi_module``.
        entrypoint: ADBC entry-point symbol. Only used with ``driver_path``
            (e.g. ``"duckdb_adbc_init"`` for DuckDB). Default: ``None``.
        dbapi_module: Dotted module name for a Python package implementing
            the ADBC dbapi interface (e.g. ``"adbc_driver_snowflake.dbapi"``
            or a custom ``"my_driver.dbapi"``). Requires ``db_kwargs``.
            Mutually exclusive with ``config`` and ``driver_path``.
        pool_size: Number of connections to keep in the pool. Default: 5.
        max_overflow: Extra connections allowed above pool_size. Default: 3.
        timeout: Seconds to wait for a connection before raising. Default: 30.
        recycle: Seconds before a connection is recycled. Default: 3600.
        pre_ping: Whether to ping connections before checkout. Default: False.

    Yields:
        A configured ``sqlalchemy.pool.QueuePool``. The pool is automatically
        closed when the ``with`` block exits.

    Raises:
        TypeError: If none of ``config``, ``driver_path``, or ``dbapi_module``
            is provided, or if both ``driver_path`` and ``dbapi_module`` are
            provided.
        ImportError: If the required ADBC driver is not installed.

    Example:
        Config path:

        ```python
        from adbc_poolhouse import DuckDBConfig, managed_pool

        with managed_pool(DuckDBConfig(database="/tmp/wh.db")) as pool:
            with pool.connect() as conn:
                cursor = conn.cursor()
                cursor.execute("SELECT 1")
        ```

        Raw native driver path:

        ```python
        with managed_pool(
            driver_path="/path/to/libduckdb.dylib",
            db_kwargs={"path": "/tmp/my.db"},
            entrypoint="duckdb_adbc_init",
        ) as pool:
            with pool.connect() as conn:
                cursor = conn.cursor()
                cursor.execute("SELECT 42")
        ```
    """
    pool = _create_pool_impl(
        config,
        driver_path,
        db_kwargs,
        entrypoint,
        dbapi_module,
        pool_size,
        max_overflow,
        timeout,
        recycle,
        pre_ping,
    )
    try:
        yield pool
    finally:
        close_pool(pool)

close_async_pool async

close_async_pool(pool: AsyncPool) -> None

Dispose an AsyncPool and close its underlying ADBC source.

The async analog of close_pool. The blocking teardown runs on a worker thread inside a shielded cancel scope (in AsyncPool.close), so a cancellation cannot abandon the pool mid-close and leak driver resources.

Parameters:

Name Type Description Default
pool AsyncPool

A pool returned by create_async_pool.

required
Example
from adbc_poolhouse import DuckDBConfig, create_async_pool, close_async_pool

pool = create_async_pool(DuckDBConfig(database="/tmp/wh.db"))
await close_async_pool(pool)
Source code in src/adbc_poolhouse/_async/_factory.py
async def close_async_pool(pool: AsyncPool) -> None:
    """
    Dispose an `AsyncPool` and close its underlying ADBC source.

    The async analog of [`close_pool`][adbc_poolhouse.close_pool]. The blocking
    teardown runs on a worker thread inside a shielded cancel scope (in
    `AsyncPool.close`), so a cancellation cannot abandon the pool mid-close and
    leak driver resources.

    Args:
        pool: A pool returned by `create_async_pool`.

    Example:
        ```python
        from adbc_poolhouse import DuckDBConfig, create_async_pool, close_async_pool

        pool = create_async_pool(DuckDBConfig(database="/tmp/wh.db"))
        await close_async_pool(pool)
        ```
    """
    await pool.close()

create_async_pool

create_async_pool(
    config: WarehouseConfig,
    *,
    pool_size: int = 5,
    max_overflow: int = 3,
    timeout: int = 30,
    recycle: int = 3600,
    pre_ping: bool = False,
) -> AsyncPool
create_async_pool(
    *,
    driver_path: str,
    db_kwargs: dict[str, str],
    entrypoint: str | None = None,
    pool_size: int = 5,
    max_overflow: int = 3,
    timeout: int = 30,
    recycle: int = 3600,
    pre_ping: bool = False,
) -> AsyncPool
create_async_pool(
    *,
    dbapi_module: str,
    db_kwargs: dict[str, str],
    pool_size: int = 5,
    max_overflow: int = 3,
    timeout: int = 30,
    recycle: int = 3600,
    pre_ping: bool = False,
) -> AsyncPool

Create an AsyncPool backed by an ADBC driver.

The signature mirrors create_pool exactly, with the same three call patterns and keyword defaults. The pool is built synchronously by the shared sync core (_create_pool_impl), then wrapped in an AsyncPool that owns a dedicated anyio.CapacityLimiter(pool_size + max_overflow). There is no per-backend code, so any of the supported warehouse configs works.

Three call patterns are supported:

pool = create_async_pool(DuckDBConfig(...))           # from a config object
pool = create_async_pool(driver_path="...", ...)       # native ADBC driver
pool = create_async_pool(dbapi_module="...", ...)      # Python dbapi module

Parameters:

Name Type Description Default
config WarehouseConfig | None

A warehouse config model instance (e.g. DuckDBConfig). Mutually exclusive with driver_path and dbapi_module.

None
driver_path str | None

Path to a native ADBC driver shared library, or a short driver name for manifest-based resolution. Requires db_kwargs. Mutually exclusive with config and dbapi_module.

None
db_kwargs dict[str, str] | None

ADBC connection keyword arguments as dict[str, str]. Required when using driver_path or dbapi_module.

None
entrypoint str | None

ADBC entry-point symbol. Only used with driver_path (e.g. "duckdb_adbc_init" for DuckDB). Default: None.

None
dbapi_module str | None

Dotted module name for a Python package implementing the ADBC dbapi interface (e.g. "adbc_driver_snowflake.dbapi"). Requires db_kwargs. Mutually exclusive with config and driver_path.

None
pool_size int

Number of connections to keep in the pool. Default: 5.

5
max_overflow int

Extra connections allowed above pool_size. Default: 3.

3
timeout int

Seconds to wait for a connection before raising. Default: 30.

30
recycle int

Seconds before a connection is recycled. Default: 3600.

3600
pre_ping bool

Whether to ping connections before checkout. Default: False.

False

Returns:

Type Description
AsyncPool

A configured AsyncPool ready for use.

Raises:

Type Description
TypeError

If none of config, driver_path, or dbapi_module is provided, or if both driver_path and dbapi_module are provided.

ImportError

If the required ADBC driver is not installed.

Example
import anyio
from adbc_poolhouse import DuckDBConfig, create_async_pool, close_async_pool


async def main():
    pool = create_async_pool(DuckDBConfig(database="/tmp/wh.db"))
    try:
        async with await pool.connect() as conn:
            cur = conn.cursor()
            await cur.execute("SELECT 1")
    finally:
        await close_async_pool(pool)


anyio.run(main)
Source code in src/adbc_poolhouse/_async/_factory.py
def create_async_pool(
    config: WarehouseConfig | None = None,
    *,
    driver_path: str | None = None,
    db_kwargs: dict[str, str] | None = None,
    entrypoint: str | None = None,
    dbapi_module: str | None = None,
    pool_size: int = 5,
    max_overflow: int = 3,
    timeout: int = 30,
    recycle: int = 3600,
    pre_ping: bool = False,
) -> AsyncPool:
    """
    Create an `AsyncPool` backed by an ADBC driver.

    The signature mirrors [`create_pool`][adbc_poolhouse.create_pool] exactly,
    with the same three call patterns and keyword defaults. The pool is built
    synchronously by the shared sync core (`_create_pool_impl`), then wrapped in an
    `AsyncPool` that owns a dedicated `anyio.CapacityLimiter(pool_size +
    max_overflow)`. There is no per-backend code, so any of the supported
    warehouse configs works.

    Three call patterns are supported:

        pool = create_async_pool(DuckDBConfig(...))           # from a config object
        pool = create_async_pool(driver_path="...", ...)       # native ADBC driver
        pool = create_async_pool(dbapi_module="...", ...)      # Python dbapi module

    Args:
        config: A warehouse config model instance (e.g. `DuckDBConfig`).
            Mutually exclusive with `driver_path` and `dbapi_module`.
        driver_path: Path to a native ADBC driver shared library, or a short
            driver name for manifest-based resolution. Requires `db_kwargs`.
            Mutually exclusive with `config` and `dbapi_module`.
        db_kwargs: ADBC connection keyword arguments as `dict[str, str]`. Required
            when using `driver_path` or `dbapi_module`.
        entrypoint: ADBC entry-point symbol. Only used with `driver_path`
            (e.g. `"duckdb_adbc_init"` for DuckDB). Default: `None`.
        dbapi_module: Dotted module name for a Python package implementing the ADBC
            dbapi interface (e.g. `"adbc_driver_snowflake.dbapi"`). Requires
            `db_kwargs`. Mutually exclusive with `config` and `driver_path`.
        pool_size: Number of connections to keep in the pool. Default: 5.
        max_overflow: Extra connections allowed above `pool_size`. Default: 3.
        timeout: Seconds to wait for a connection before raising. Default: 30.
        recycle: Seconds before a connection is recycled. Default: 3600.
        pre_ping: Whether to ping connections before checkout. Default: False.

    Returns:
        A configured `AsyncPool` ready for use.

    Raises:
        TypeError: If none of `config`, `driver_path`, or `dbapi_module` is
            provided, or if both `driver_path` and `dbapi_module` are provided.
        ImportError: If the required ADBC driver is not installed.

    Example:
        ```python
        import anyio
        from adbc_poolhouse import DuckDBConfig, create_async_pool, close_async_pool


        async def main():
            pool = create_async_pool(DuckDBConfig(database="/tmp/wh.db"))
            try:
                async with await pool.connect() as conn:
                    cur = conn.cursor()
                    await cur.execute("SELECT 1")
            finally:
                await close_async_pool(pool)


        anyio.run(main)
        ```
    """
    sync_pool = _create_pool_impl(
        config,
        driver_path,
        db_kwargs,
        entrypoint,
        dbapi_module,
        pool_size,
        max_overflow,
        timeout,
        recycle,
        pre_ping,
    )
    return AsyncPool(sync_pool, pool_size=pool_size, max_overflow=max_overflow)

managed_async_pool async

managed_async_pool(
    config: WarehouseConfig,
    *,
    pool_size: int = 5,
    max_overflow: int = 3,
    timeout: int = 30,
    recycle: int = 3600,
    pre_ping: bool = False,
) -> contextlib.AbstractAsyncContextManager[AsyncPool]
managed_async_pool(
    *,
    driver_path: str,
    db_kwargs: dict[str, str],
    entrypoint: str | None = None,
    pool_size: int = 5,
    max_overflow: int = 3,
    timeout: int = 30,
    recycle: int = 3600,
    pre_ping: bool = False,
) -> contextlib.AbstractAsyncContextManager[AsyncPool]
managed_async_pool(
    *,
    dbapi_module: str,
    db_kwargs: dict[str, str],
    pool_size: int = 5,
    max_overflow: int = 3,
    timeout: int = 30,
    recycle: int = 3600,
    pre_ping: bool = False,
) -> contextlib.AbstractAsyncContextManager[AsyncPool]

Async context manager that creates an AsyncPool and closes it on exit.

The async analog of managed_pool. The pool is created when the async with block is entered and closed (via close_async_pool, whose teardown is shielded from cancellation) when the block exits, whether normally or by exception.

Three call patterns are supported:

async with managed_async_pool(DuckDBConfig(...)) as pool: ...        # config
async with managed_async_pool(driver_path="...", ...) as pool: ...   # native
async with managed_async_pool(dbapi_module="...", ...) as pool: ...  # dbapi

Parameters:

Name Type Description Default
config WarehouseConfig | None

A warehouse config model instance (e.g. DuckDBConfig). Mutually exclusive with driver_path and dbapi_module.

None
driver_path str | None

Path to a native ADBC driver shared library, or a short driver name for manifest-based resolution. Requires db_kwargs. Mutually exclusive with config and dbapi_module.

None
db_kwargs dict[str, str] | None

ADBC connection keyword arguments as dict[str, str]. Required when using driver_path or dbapi_module.

None
entrypoint str | None

ADBC entry-point symbol. Only used with driver_path (e.g. "duckdb_adbc_init" for DuckDB). Default: None.

None
dbapi_module str | None

Dotted module name for a Python package implementing the ADBC dbapi interface (e.g. "adbc_driver_snowflake.dbapi"). Requires db_kwargs. Mutually exclusive with config and driver_path.

None
pool_size int

Number of connections to keep in the pool. Default: 5.

5
max_overflow int

Extra connections allowed above pool_size. Default: 3.

3
timeout int

Seconds to wait for a connection before raising. Default: 30.

30
recycle int

Seconds before a connection is recycled. Default: 3600.

3600
pre_ping bool

Whether to ping connections before checkout. Default: False.

False

Yields:

Type Description
AsyncGenerator[AsyncPool, None]

A configured AsyncPool, closed automatically when the block exits.

Raises:

Type Description
TypeError

If none of config, driver_path, or dbapi_module is provided, or if both driver_path and dbapi_module are provided.

ImportError

If the required ADBC driver is not installed.

Example
from adbc_poolhouse import DuckDBConfig, managed_async_pool

async with managed_async_pool(DuckDBConfig(database="/tmp/wh.db")) as pool:
    async with await pool.connect() as conn:
        cur = conn.cursor()
        await cur.execute("SELECT 42")
Source code in src/adbc_poolhouse/_async/_factory.py
@contextlib.asynccontextmanager
async def managed_async_pool(
    config: WarehouseConfig | None = None,
    *,
    driver_path: str | None = None,
    db_kwargs: dict[str, str] | None = None,
    entrypoint: str | None = None,
    dbapi_module: str | None = None,
    pool_size: int = 5,
    max_overflow: int = 3,
    timeout: int = 30,
    recycle: int = 3600,
    pre_ping: bool = False,
) -> collections.abc.AsyncGenerator[AsyncPool, None]:
    """
    Async context manager that creates an `AsyncPool` and closes it on exit.

    The async analog of [`managed_pool`][adbc_poolhouse.managed_pool]. The pool is
    created when the `async with` block is entered and closed (via
    `close_async_pool`, whose teardown is shielded from cancellation) when the
    block exits, whether normally or by exception.

    Three call patterns are supported:

        async with managed_async_pool(DuckDBConfig(...)) as pool: ...        # config
        async with managed_async_pool(driver_path="...", ...) as pool: ...   # native
        async with managed_async_pool(dbapi_module="...", ...) as pool: ...  # dbapi

    Args:
        config: A warehouse config model instance (e.g. `DuckDBConfig`).
            Mutually exclusive with `driver_path` and `dbapi_module`.
        driver_path: Path to a native ADBC driver shared library, or a short
            driver name for manifest-based resolution. Requires `db_kwargs`.
            Mutually exclusive with `config` and `dbapi_module`.
        db_kwargs: ADBC connection keyword arguments as `dict[str, str]`. Required
            when using `driver_path` or `dbapi_module`.
        entrypoint: ADBC entry-point symbol. Only used with `driver_path`
            (e.g. `"duckdb_adbc_init"` for DuckDB). Default: `None`.
        dbapi_module: Dotted module name for a Python package implementing the ADBC
            dbapi interface (e.g. `"adbc_driver_snowflake.dbapi"`). Requires
            `db_kwargs`. Mutually exclusive with `config` and `driver_path`.
        pool_size: Number of connections to keep in the pool. Default: 5.
        max_overflow: Extra connections allowed above `pool_size`. Default: 3.
        timeout: Seconds to wait for a connection before raising. Default: 30.
        recycle: Seconds before a connection is recycled. Default: 3600.
        pre_ping: Whether to ping connections before checkout. Default: False.

    Yields:
        A configured `AsyncPool`, closed automatically when the block exits.

    Raises:
        TypeError: If none of `config`, `driver_path`, or `dbapi_module` is
            provided, or if both `driver_path` and `dbapi_module` are provided.
        ImportError: If the required ADBC driver is not installed.

    Example:
        ```python
        from adbc_poolhouse import DuckDBConfig, managed_async_pool

        async with managed_async_pool(DuckDBConfig(database="/tmp/wh.db")) as pool:
            async with await pool.connect() as conn:
                cur = conn.cursor()
                await cur.execute("SELECT 42")
        ```
    """
    sync_pool = _create_pool_impl(
        config,
        driver_path,
        db_kwargs,
        entrypoint,
        dbapi_module,
        pool_size,
        max_overflow,
        timeout,
        recycle,
        pre_ping,
    )
    pool = AsyncPool(sync_pool, pool_size=pool_size, max_overflow=max_overflow)
    try:
        yield pool
    finally:
        await close_async_pool(pool)

Async API (experimental)

You never construct these classes yourself. They are returned by the async entry-point functions above (create_async_pool returns an AsyncPool, whose connect yields an AsyncConnection, whose cursor yields an AsyncCursor). They are documented here at their real module paths because the async surface is experimental and is not part of the constructible public API.

adbc_poolhouse._async._pool.AsyncPool

AsyncPool(
    sync_pool: QueuePool,
    *,
    pool_size: int,
    max_overflow: int,
)

Async wrapper over a synchronous ADBC QueuePool.

Created by create_async_pool. The pool construction itself is synchronous (it does no per-call I/O); only connect and close are offloaded to worker threads.

The pool owns one dedicated anyio.CapacityLimiter sized to pool_size + max_overflow, shared by every offloaded call on the pool and its connections. A limiter token is borrowed only for the duration of each individual call (transient-token model, D-24-01), so a checked-out connection holds no token between calls and the worst-case in-flight offload count exactly fits the limiter.

Attributes:

Name Type Description
_limiter

The pool's dedicated anyio.CapacityLimiter. Exposed for tests that assert token accounting (e.g. pool._limiter.borrowed_tokens).

Example
import anyio
from adbc_poolhouse import DuckDBConfig, create_async_pool, close_async_pool


async def main():
    pool = create_async_pool(DuckDBConfig(database="/tmp/wh.db"))
    try:
        async with await pool.connect() as conn:
            cur = conn.cursor()  # synchronous, no await
            await cur.execute("SELECT 42")
            table = await cur.fetch_arrow_table()
            print(table.num_rows)
    finally:
        await close_async_pool(pool)


anyio.run(main)

Wrap a sync pool and build its dedicated limiter.

Parameters:

Name Type Description Default
sync_pool QueuePool

The synchronous QueuePool built by the sync factory.

required
pool_size int

The pool's steady-state connection count. Must match the value passed to the sync pool.

required
max_overflow int

Extra connections allowed above pool_size. Must match the value passed to the sync pool.

required
Source code in src/adbc_poolhouse/_async/_pool.py
def __init__(
    self,
    sync_pool: sqlalchemy.pool.QueuePool,
    *,
    pool_size: int,
    max_overflow: int,
) -> None:
    """
    Wrap a sync pool and build its dedicated limiter.

    Args:
        sync_pool: The synchronous `QueuePool` built by the sync factory.
        pool_size: The pool's steady-state connection count. Must match the
            value passed to the sync pool.
        max_overflow: Extra connections allowed above `pool_size`. Must match
            the value passed to the sync pool.
    """
    self._pool = sync_pool
    # Dedicated per-pool limiter sized to the checkout ceiling --- never the
    # anyio global 40-token default (CORE-02).
    self._limiter = anyio.CapacityLimiter(pool_size + max_overflow)

connect async

connect() -> AsyncConnection

Check out a connection from the pool on a worker thread.

The blocking QueuePool.connect() runs through the offload chokepoint under the pool limiter; the borrowed token is released as soon as the checkout returns (transient-token model). The returned AsyncConnection belongs to exactly one task --- do not share it across concurrent tasks.

Returns:

Type Description
AsyncConnection

An AsyncConnection wrapping the checked-out sync connection.

Source code in src/adbc_poolhouse/_async/_pool.py
async def connect(self) -> AsyncConnection:
    """
    Check out a connection from the pool on a worker thread.

    The blocking `QueuePool.connect()` runs through the offload chokepoint
    under the pool limiter; the borrowed token is released as soon as the
    checkout returns (transient-token model). The returned `AsyncConnection`
    belongs to exactly one task --- do not share it across concurrent tasks.

    Returns:
        An `AsyncConnection` wrapping the checked-out sync connection.
    """
    fairy = await offload(self._pool.connect, limiter=self._limiter)
    return AsyncConnection(fairy, self._limiter)

close async

close() -> None

Dispose the pool and close its ADBC source, shielded from cancellation.

The blocking teardown (close_pool, which disposes the pool and closes the underlying ADBC source connection) is offloaded under the pool limiter and wrapped in anyio.CancelScope(shield=True), so a cancellation arriving mid-close cannot abandon the pool with leaked driver resources.

Source code in src/adbc_poolhouse/_async/_pool.py
async def close(self) -> None:
    """
    Dispose the pool and close its ADBC source, shielded from cancellation.

    The blocking teardown (`close_pool`, which disposes the pool and closes
    the underlying ADBC source connection) is offloaded under the pool limiter
    and wrapped in `anyio.CancelScope(shield=True)`, so a cancellation
    arriving mid-close cannot abandon the pool with leaked driver resources.
    """
    with anyio.CancelScope(shield=True):
        await offload(close_pool, self._pool, limiter=self._limiter)

adbc_poolhouse._async._connection.AsyncConnection

AsyncConnection(
    fairy: PoolProxiedConnection, limiter: CapacityLimiter
)

Async wrapper over a pooled ADBC connection.

Returned by AsyncPool.connect. It holds a checked-out sync pool connection (a SQLAlchemy PoolProxiedConnection) for its lifetime but borrows a limiter token only for the duration of each individual offloaded call (transient-token model, D-24-01) --- it retains no token between calls.

One AsyncConnection belongs to exactly one task. A cheap _in_use guard rejects a second concurrent caller (another task using a cursor on this connection, or a concurrent commit/close) with ConnectionBusyError rather than silently serializing (D-24-03). The check-in (close / __aexit__) is shielded from cancellation so a connection is never returned to the pool in an unknown state.

Two-tier entry guard (D-29-10). The entry method layers a persistent reader-lifetime tier on top of the per-call _in_use tier:

  • Foreign callers (execute, commit, another cursor, a new fetch_record_batch) are rejected while a reader is live --- they hit _in_use or _reader_open and get ConnectionBusyError.
  • A live reader's own per-batch pulls pass from_reader=True, which exempts them from the _reader_open tier only (the reentrancy exemption --- a reader must not deadlock on its own lifetime lock) while STILL taking the per-call _in_use C-access tier.

Attributes:

Name Type Description
_in_use

True while an offloaded call on this connection (or one of its cursors) is in flight. The single-task aliasing guard; deliberately a plain bool, never a serializing lock, so a second concurrent caller is rejected rather than queued (D-24-03).

_reader_open

True for the WHOLE lifetime of a live Arrow reader on this connection (Model B, D-29-08/09), distinct from the per-call _in_use. A live reader locks the connection so no foreign op can touch the still -open C stream between pulls, closing the gap _in_use alone leaves (STREAM-06). Set by fetch_record_batch and cleared by reader.close() / checkin (plan 03); _exit_offload NEVER touches it. A stale _reader_open == True is harmless because a fresh AsyncConnection wraps each connect(), so checkin is the backstop eraser (D-29-13).

_teardown_limiter CapacityLimiter

A dedicated 1-token anyio.CapacityLimiter the poison-recovery invalidate offloads through, kept separate from the shared pool limiter so recovery never contends for the pool token the just-aborted worker is still releasing (WR-03).

Example
import adbc_poolhouse

pool = await adbc_poolhouse.create_async_pool(config)
async with await pool.connect() as conn:
    cursor = conn.cursor()  # synchronous, no await
    await cursor.execute("SELECT 42")
    table = await cursor.fetch_arrow_table()
    await conn.commit()
await adbc_poolhouse.close_async_pool(pool)

Bind a checked-out sync connection to its pool limiter.

Parameters:

Name Type Description Default
fairy PoolProxiedConnection

The checked-out sync pool connection (SQLAlchemy PoolProxiedConnection) returned by the underlying QueuePool.connect().

required
limiter CapacityLimiter

The owning pool's anyio.CapacityLimiter, threaded into every offloaded call on this connection and its cursors.

required
Source code in src/adbc_poolhouse/_async/_connection.py
def __init__(
    self,
    fairy: PoolProxiedConnection,
    limiter: CapacityLimiter,
) -> None:
    """
    Bind a checked-out sync connection to its pool limiter.

    Args:
        fairy: The checked-out sync pool connection (SQLAlchemy
            `PoolProxiedConnection`) returned by the underlying
            `QueuePool.connect()`.
        limiter: The owning pool's `anyio.CapacityLimiter`, threaded into
            every offloaded call on this connection and its cursors.
    """
    self._fairy = fairy
    self._limiter = limiter
    self._in_use = False
    # D-29-09: a live Arrow reader locks the connection for its WHOLE lifetime,
    # not just during an in-flight pull. Distinct from the per-call `_in_use`
    # (which reads False between pulls, leaving the STREAM-06 gap this closes).
    # Owned by fetch_record_batch (set) / reader.close() + checkin (clear) in
    # plan 03; `_exit_offload` must never touch it (D-29-11/13).
    self._reader_open = False
    # Poison-recovery (`invalidate`) runs off a DEDICATED 1-token limiter, not
    # the pool's shared `limiter` (WR-03). Teardown is not throughput-bounded,
    # and on a `pool_size + max_overflow == 1` pool the just-aborted worker
    # still holds the single pool token until its thread returns; borrowing the
    # pool token here would make recovery wait on the very worker it just
    # aborted. A private limiter sidesteps that ordering dependency entirely.
    self._teardown_limiter: CapacityLimiter = anyio.CapacityLimiter(1)

_enter_offload

_enter_offload(*, from_reader: bool = False) -> None

Claim this connection for one offloaded call, or reject an aliased caller.

Applies the two-tier entry guard (D-29-10). First the per-call C-access tier: raise ConnectionBusyError if the connection is already executing a call (from this task or another) --- this tier is unconditional, so from_reader never bypasses it. Then the reader-lifetime tier: raise ConnectionBusyError if a reader is live (_reader_open) UNLESS the caller is that reader's own pull (from_reader=True), which is exempt from this tier only (the reentrancy exemption --- a reader must not deadlock on its own lifetime lock). If both tiers pass, mark the connection busy.

The read of _in_use and the write that sets it run in one synchronous span with NO await between them, so on the single-threaded event loop two tasks can never both observe _in_use == False and both proceed (Pitfall 3 / the check-and-set race). Always paired with _exit_offload in a finally.

Parameters:

Name Type Description Default
from_reader bool

True only for a live reader's own per-batch pull, exempting it from the _reader_open tier (not the _in_use tier). Defaults False, so every existing (foreign) call site is unchanged.

False

Raises:

Type Description
ConnectionBusyError

If an offloaded call on this connection is already in flight (_in_use), or a reader is live and the caller is foreign (_reader_open and not from_reader).

Source code in src/adbc_poolhouse/_async/_connection.py
def _enter_offload(self, *, from_reader: bool = False) -> None:
    """
    Claim this connection for one offloaded call, or reject an aliased caller.

    Applies the two-tier entry guard (D-29-10). First the per-call C-access tier:
    raise `ConnectionBusyError` if the connection is already executing a call
    (from this task or another) --- this tier is unconditional, so `from_reader`
    never bypasses it. Then the reader-lifetime tier: raise `ConnectionBusyError`
    if a reader is live (`_reader_open`) UNLESS the caller is that reader's own
    pull (`from_reader=True`), which is exempt from this tier only (the
    reentrancy exemption --- a reader must not deadlock on its own lifetime
    lock). If both tiers pass, mark the connection busy.

    The read of `_in_use` and the write that sets it run in one synchronous span
    with NO `await` between them, so on the single-threaded event loop two tasks
    can never both observe `_in_use == False` and both proceed (Pitfall 3 / the
    check-and-set race). Always paired with `_exit_offload` in a `finally`.

    Args:
        from_reader: True only for a live reader's own per-batch pull, exempting
            it from the `_reader_open` tier (not the `_in_use` tier). Defaults
            False, so every existing (foreign) call site is unchanged.

    Raises:
        ConnectionBusyError: If an offloaded call on this connection is already
            in flight (`_in_use`), or a reader is live and the caller is foreign
            (`_reader_open and not from_reader`).
    """
    if self._in_use:
        raise ConnectionBusyError
    # Reader-lifetime tier: foreign callers are rejected while a reader is live.
    # The reader's own pulls pass from_reader=True to skip ONLY this tier
    # (D-29-10); they still took the _in_use tier above.
    if self._reader_open and not from_reader:
        raise ConnectionBusyError
    self._in_use = True

_exit_offload

_exit_offload() -> None

Release the connection after an offloaded call (call from finally).

Source code in src/adbc_poolhouse/_async/_connection.py
def _exit_offload(self) -> None:
    """Release the connection after an offloaded call (call from `finally`)."""
    self._in_use = False

_offloading

_offloading(
    *, from_reader: bool = False
) -> Generator[None]

Hold the two-tier entry guard for the span of one offloaded call.

Claims the connection on entry (raising ConnectionBusyError if it is already executing a call, or if a reader is live and the caller is foreign) and releases the per-call _in_use tier on exit, so every offloading method on this connection --- and on its cursors --- brackets its work identically without repeating the try/finally (D-24-03). If the guard rejects the caller the body never runs and the flags are left untouched for the call that legitimately holds them.

_reader_open is deliberately NOT cleared here: its lifetime spans many offload calls and is owned by reader.close() / checkin (D-29-11), so _exit_offload only clears _in_use.

Parameters:

Name Type Description Default
from_reader bool

Forwarded to _enter_offload. True only for a live reader's own per-batch pull (the reentrancy exemption); defaults False so every existing call site keeps its foreign-tier semantics with no edit.

False

Yields:

Type Description
Generator[None]

None. The guarded offload runs inside the with body.

Raises:

Type Description
ConnectionBusyError

If an offloaded call on this connection is already in flight, or a reader is live and the caller is foreign.

Source code in src/adbc_poolhouse/_async/_connection.py
@contextlib.contextmanager
def _offloading(self, *, from_reader: bool = False) -> Generator[None]:
    """
    Hold the two-tier entry guard for the span of one offloaded call.

    Claims the connection on entry (raising `ConnectionBusyError` if it is
    already executing a call, or if a reader is live and the caller is foreign)
    and releases the per-call `_in_use` tier on exit, so every offloading method
    on this connection --- and on its cursors --- brackets its work identically
    without repeating the `try`/`finally` (D-24-03). If the guard rejects the
    caller the body never runs and the flags are left untouched for the call
    that legitimately holds them.

    `_reader_open` is deliberately NOT cleared here: its lifetime spans many
    offload calls and is owned by `reader.close()` / checkin (D-29-11), so
    `_exit_offload` only clears `_in_use`.

    Args:
        from_reader: Forwarded to `_enter_offload`. True only for a live
            reader's own per-batch pull (the reentrancy exemption); defaults
            False so every existing call site keeps its foreign-tier semantics
            with no edit.

    Yields:
        `None`. The guarded offload runs inside the `with` body.

    Raises:
        ConnectionBusyError: If an offloaded call on this connection is already
            in flight, or a reader is live and the caller is foreign.
    """
    self._enter_offload(from_reader=from_reader)
    try:
        yield
    finally:
        self._exit_offload()

cursor

cursor() -> AsyncCursor

Open a cursor on this connection.

This is a plain synchronous accessor (NOT async): the dbapi cursor() does no I/O, so there is nothing to offload and no token to borrow. The returned cursor guards this connection's _in_use flag, so concurrent use of two cursors on one connection --- like concurrent use of the connection itself --- raises ConnectionBusyError.

Returns:

Type Description
AsyncCursor

An AsyncCursor bound to a fresh dbapi cursor and to this connection.

Source code in src/adbc_poolhouse/_async/_connection.py
def cursor(self) -> AsyncCursor:
    """
    Open a cursor on this connection.

    This is a plain synchronous accessor (NOT `async`): the dbapi `cursor()`
    does no I/O, so there is nothing to offload and no token to borrow. The
    returned cursor guards this connection's `_in_use` flag, so concurrent use
    of two cursors on one connection --- like concurrent use of the connection
    itself --- raises `ConnectionBusyError`.

    Returns:
        An `AsyncCursor` bound to a fresh dbapi cursor and to this connection.
    """
    # SQLAlchemy types the fairy's cursor as the generic PEP 249 DBAPICursor;
    # at runtime it is the ADBC cursor, which adds `fetch_arrow_table`. Bridge
    # the narrow static type to the structural ADBC surface the wrapper needs.
    sync_cursor = cast("_SyncCursor", self._fairy.cursor())
    return AsyncCursor(sync_cursor, self._limiter, self)

commit async

commit() -> None

Commit the current transaction on a worker thread.

Offloads fairy.commit() through the pool limiter while holding the _in_use guard, so a concurrent commit/query on this connection is rejected with ConnectionBusyError.

Unlike the cursor's execute/fetch*, this call is not cooperatively cancellable: it runs through the non-interruptible offload with no adbc_cancel hook, so a surrounding timeout or cancellation cannot abort an in-flight commit. The loop defers the cancellation and waits for the driver call to return before raising it. Keep commits short, or enforce a server-side statement timeout, if you need a bound on how long a commit can block.

Raises:

Type Description
ConnectionBusyError

If another offloaded call on this connection is already in flight.

Source code in src/adbc_poolhouse/_async/_connection.py
async def commit(self) -> None:
    """
    Commit the current transaction on a worker thread.

    Offloads `fairy.commit()` through the pool limiter while holding the
    `_in_use` guard, so a concurrent commit/query on this connection is
    rejected with `ConnectionBusyError`.

    Unlike the cursor's `execute`/`fetch*`, this call is **not** cooperatively
    cancellable: it runs through the non-interruptible `offload` with no
    `adbc_cancel` hook, so a surrounding timeout or cancellation cannot abort an
    in-flight commit. The loop defers the cancellation and waits for the driver
    call to return before raising it. Keep commits short, or enforce a
    server-side statement timeout, if you need a bound on how long a commit can
    block.

    Raises:
        ConnectionBusyError: If another offloaded call on this connection is
            already in flight.
    """
    with self._offloading():
        await offload(self._fairy.commit, limiter=self._limiter)

rollback async

rollback() -> None

Roll back the current transaction on a worker thread.

Offloads fairy.rollback() through the pool limiter while holding the _in_use guard.

Like commit, this call is not cooperatively cancellable: a surrounding timeout or cancellation cannot abort an in-flight rollback. The loop defers the cancellation and waits for the driver call to return before raising it.

Raises:

Type Description
ConnectionBusyError

If another offloaded call on this connection is already in flight.

Source code in src/adbc_poolhouse/_async/_connection.py
async def rollback(self) -> None:
    """
    Roll back the current transaction on a worker thread.

    Offloads `fairy.rollback()` through the pool limiter while holding the
    `_in_use` guard.

    Like `commit`, this call is **not** cooperatively cancellable: a surrounding
    timeout or cancellation cannot abort an in-flight rollback. The loop defers
    the cancellation and waits for the driver call to return before raising it.

    Raises:
        ConnectionBusyError: If another offloaded call on this connection is
            already in flight.
    """
    with self._offloading():
        await offload(self._fairy.rollback, limiter=self._limiter)

adbc_get_info async

adbc_get_info() -> dict[str | int, Any]

Read the driver and vendor info codes on a worker thread.

Offloads the sync adbc_get_info() through the pool limiter while holding the _in_use guard, so a concurrent call on this connection is rejected with ConnectionBusyError. The driver's own dict is returned unchanged --- keys are ADBC info codes (str or int), values the backend's reported metadata.

Like commit, this call is not cooperatively cancellable: it runs through the non-interruptible offload with no cancel hook, so a surrounding timeout or cancellation cannot abort an in-flight metadata read. The loop defers the cancellation and waits for the driver call to return before raising it.

Returns:

Type Description
dict[str | int, Any]

The driver's info mapping, keyed by ADBC info code. Returned unchanged.

Raises:

Type Description
ConnectionBusyError

If another offloaded call on this connection is already in flight.

Example
async with await pool.connect() as conn:
    info = await conn.adbc_get_info()
    print(info)  # a dict of driver/vendor codes
Source code in src/adbc_poolhouse/_async/_connection.py
async def adbc_get_info(self) -> dict[str | int, Any]:
    """
    Read the driver and vendor info codes on a worker thread.

    Offloads the sync `adbc_get_info()` through the pool limiter while holding
    the `_in_use` guard, so a concurrent call on this connection is rejected with
    `ConnectionBusyError`. The driver's own `dict` is returned unchanged --- keys
    are ADBC info codes (`str` or `int`), values the backend's reported metadata.

    Like `commit`, this call is **not** cooperatively cancellable: it runs
    through the non-interruptible `offload` with no cancel hook, so a surrounding
    timeout or cancellation cannot abort an in-flight metadata read. The loop
    defers the cancellation and waits for the driver call to return before
    raising it.

    Returns:
        The driver's info mapping, keyed by ADBC info code. Returned unchanged.

    Raises:
        ConnectionBusyError: If another offloaded call on this connection is
            already in flight.

    Example:
        ```python
        async with await pool.connect() as conn:
            info = await conn.adbc_get_info()
            print(info)  # a dict of driver/vendor codes
        ```
    """
    sync_conn = cast("_SyncConnection", self._fairy)
    with self._offloading():
        return await offload(sync_conn.adbc_get_info, limiter=self._limiter)

adbc_get_table_types async

adbc_get_table_types() -> list[str]

List the backend's table-type names on a worker thread.

Offloads the sync adbc_get_table_types() through the pool limiter while holding the _in_use guard, so a concurrent call on this connection is rejected with ConnectionBusyError. The driver's own list of table-type strings (for example "table", "view") is returned unchanged.

Like commit, this call is not cooperatively cancellable: a surrounding timeout or cancellation cannot abort an in-flight metadata read. The loop defers the cancellation and waits for the driver call to return before raising it.

Returns:

Type Description
list[str]

The backend's table-type strings, in the driver's order. Returned

list[str]

unchanged.

Raises:

Type Description
ConnectionBusyError

If another offloaded call on this connection is already in flight.

Example
async with await pool.connect() as conn:
    types = await conn.adbc_get_table_types()
    print(types)  # e.g. ["table", "view"]
Source code in src/adbc_poolhouse/_async/_connection.py
async def adbc_get_table_types(self) -> list[str]:
    """
    List the backend's table-type names on a worker thread.

    Offloads the sync `adbc_get_table_types()` through the pool limiter while
    holding the `_in_use` guard, so a concurrent call on this connection is
    rejected with `ConnectionBusyError`. The driver's own `list` of table-type
    strings (for example `"table"`, `"view"`) is returned unchanged.

    Like `commit`, this call is **not** cooperatively cancellable: a surrounding
    timeout or cancellation cannot abort an in-flight metadata read. The loop
    defers the cancellation and waits for the driver call to return before
    raising it.

    Returns:
        The backend's table-type strings, in the driver's order. Returned
        unchanged.

    Raises:
        ConnectionBusyError: If another offloaded call on this connection is
            already in flight.

    Example:
        ```python
        async with await pool.connect() as conn:
            types = await conn.adbc_get_table_types()
            print(types)  # e.g. ["table", "view"]
        ```
    """
    sync_conn = cast("_SyncConnection", self._fairy)
    with self._offloading():
        return await offload(sync_conn.adbc_get_table_types, limiter=self._limiter)

adbc_get_table_schema async

adbc_get_table_schema(
    table_name: str,
    *,
    catalog_filter: str | None = None,
    db_schema_filter: str | None = None,
) -> pyarrow.Schema

Read a single table's Arrow schema on a worker thread.

Offloads the sync adbc_get_table_schema() through the pool limiter while holding the _in_use guard, so a concurrent call on this connection is rejected with ConnectionBusyError. The driver's own pyarrow.Schema is returned unchanged. The arguments forward through functools.partial so the keyword-only filters reach the driver arity-checked.

Like commit, this call is not cooperatively cancellable: a surrounding timeout or cancellation cannot abort an in-flight metadata read. The loop defers the cancellation and waits for the driver call to return before raising it.

Parameters:

Name Type Description Default
table_name str

The table to describe. Passed straight to the driver as an identifier; poolhouse does not quote or sanitize it.

required
catalog_filter str | None

Restrict the lookup to this catalog. Forwarded to the driver unchanged; None (the default) leaves it unrestricted.

None
db_schema_filter str | None

Restrict the lookup to this schema. Forwarded to the driver unchanged; None (the default) leaves it unrestricted.

None

Returns:

Type Description
Schema

The table's pyarrow.Schema, returned unchanged from the driver.

Raises:

Type Description
ConnectionBusyError

If another offloaded call on this connection is already in flight.

Example
async with await pool.connect() as conn:
    schema = await conn.adbc_get_table_schema("people")
    print(schema.names)  # e.g. ["id", "name"]
Source code in src/adbc_poolhouse/_async/_connection.py
async def adbc_get_table_schema(
    self,
    table_name: str,
    *,
    catalog_filter: str | None = None,
    db_schema_filter: str | None = None,
) -> pyarrow.Schema:
    """
    Read a single table's Arrow schema on a worker thread.

    Offloads the sync `adbc_get_table_schema()` through the pool limiter while
    holding the `_in_use` guard, so a concurrent call on this connection is
    rejected with `ConnectionBusyError`. The driver's own `pyarrow.Schema` is
    returned unchanged. The arguments forward through `functools.partial` so the
    keyword-only filters reach the driver arity-checked.

    Like `commit`, this call is **not** cooperatively cancellable: a surrounding
    timeout or cancellation cannot abort an in-flight metadata read. The loop
    defers the cancellation and waits for the driver call to return before
    raising it.

    Args:
        table_name: The table to describe. Passed straight to the driver as an
            identifier; poolhouse does not quote or sanitize it.
        catalog_filter: Restrict the lookup to this catalog. Forwarded to the
            driver unchanged; `None` (the default) leaves it unrestricted.
        db_schema_filter: Restrict the lookup to this schema. Forwarded to the
            driver unchanged; `None` (the default) leaves it unrestricted.

    Returns:
        The table's `pyarrow.Schema`, returned unchanged from the driver.

    Raises:
        ConnectionBusyError: If another offloaded call on this connection is
            already in flight.

    Example:
        ```python
        async with await pool.connect() as conn:
            schema = await conn.adbc_get_table_schema("people")
            print(schema.names)  # e.g. ["id", "name"]
        ```
    """
    sync_conn = cast("_SyncConnection", self._fairy)
    with self._offloading():
        return await offload(
            functools.partial(
                sync_conn.adbc_get_table_schema,
                table_name,
                catalog_filter=catalog_filter,
                db_schema_filter=db_schema_filter,
            ),
            limiter=self._limiter,
        )

adbc_get_objects async

adbc_get_objects(
    *,
    depth: Literal[
        "all", "catalogs", "db_schemas", "tables", "columns"
    ] = "all",
    catalog_filter: str | None = None,
    db_schema_filter: str | None = None,
    table_name_filter: str | None = None,
    table_types_filter: list[str] | None = None,
    column_name_filter: str | None = None,
) -> AsyncRecordBatchReader

Stream the catalog/schema/table hierarchy as an AsyncRecordBatchReader.

Offloads the sync adbc_get_objects() through the pool limiter to create the native pyarrow.RecordBatchReader, then wraps it in an AsyncRecordBatchReader whose every batch pull is itself offloaded --- the result is never materialized to a pyarrow.Table. The filter arguments forward through functools.partial so they reach the driver arity-checked.

Like commit, creating the reader is not cooperatively cancellable: a surrounding timeout or cancellation cannot abort the in-flight metadata call, which runs through the non-interruptible offload.

The returned reader is a live stream bound to this connection's C Arrow stream, so it locks the connection for its WHOLE lifetime: a foreign op (commit, a cursor, another metadata call) raises ConnectionBusyError until the reader is closed (STREAM-06), and a read after the reader is closed / the connection is checked in surfaces the driver's native pyarrow.lib.ArrowInvalid (never a segfault). The _reader_open lifetime lock is set AFTER the _offloading() span exits and ONLY on the success path, so a failed creation leaves the connection usable.

The reader has no cancel of its own (the connection has no adbc_cancel), so a cancelled per-batch pull cannot abort the in-flight C call: the worker finishes its read, the cancellation is then re-raised, and the pulled batch is discarded. The connection is NOT invalidated --- it was never poisoned, and invalidating would race a second thread against the still-running read on the same connection (CR-34-01) --- so it returns to the pool on the normal checkin.

Parameters:

Name Type Description Default
depth Literal['all', 'catalogs', 'db_schemas', 'tables', 'columns']

How deep to descend the hierarchy --- "all" (the default), "catalogs", "db_schemas", "tables", or "columns". Forwarded to the driver unchanged.

'all'
catalog_filter str | None

Restrict to this catalog. Forwarded unchanged; None (the default) leaves it unrestricted.

None
db_schema_filter str | None

Restrict to this schema. Forwarded unchanged; None (the default) leaves it unrestricted.

None
table_name_filter str | None

Restrict to this table name. Forwarded unchanged; None (the default) leaves it unrestricted.

None
table_types_filter list[str] | None

Restrict to these table types. Forwarded unchanged; None (the default) leaves it unrestricted.

None
column_name_filter str | None

Restrict to this column name. Forwarded unchanged; None (the default) leaves it unrestricted.

None

Returns:

Type Description
AsyncRecordBatchReader

An AsyncRecordBatchReader streaming the object hierarchy, each pull

AsyncRecordBatchReader

offloaded through the pool limiter.

Raises:

Type Description
ConnectionBusyError

If another offloaded call on this connection is already in flight.

Example
async with await pool.connect() as conn:
    async with await conn.adbc_get_objects(depth="tables") as reader:
        async for batch in reader:
            process(batch)  # a pyarrow.RecordBatch, each pull offloaded
Source code in src/adbc_poolhouse/_async/_connection.py
async def adbc_get_objects(
    self,
    *,
    depth: Literal["all", "catalogs", "db_schemas", "tables", "columns"] = "all",
    catalog_filter: str | None = None,
    db_schema_filter: str | None = None,
    table_name_filter: str | None = None,
    table_types_filter: list[str] | None = None,
    column_name_filter: str | None = None,
) -> AsyncRecordBatchReader:
    """
    Stream the catalog/schema/table hierarchy as an `AsyncRecordBatchReader`.

    Offloads the sync `adbc_get_objects()` through the pool limiter to create the
    native `pyarrow.RecordBatchReader`, then wraps it in an
    `AsyncRecordBatchReader` whose every batch pull is itself offloaded --- the
    result is never materialized to a `pyarrow.Table`. The filter arguments
    forward through `functools.partial` so they reach the driver arity-checked.

    Like `commit`, creating the reader is **not** cooperatively cancellable: a
    surrounding timeout or cancellation cannot abort the in-flight metadata call,
    which runs through the non-interruptible `offload`.

    The returned reader is a live stream bound to this connection's C Arrow
    stream, so it locks the connection for its WHOLE lifetime: a foreign op
    (`commit`, a cursor, another metadata call) raises `ConnectionBusyError`
    until the reader is closed (STREAM-06), and a read after the reader is closed
    / the connection is checked in surfaces the driver's native
    `pyarrow.lib.ArrowInvalid` (never a segfault). The `_reader_open` lifetime
    lock is set AFTER the `_offloading()` span exits and ONLY on the success
    path, so a failed creation leaves the connection usable.

    The reader has no cancel of its own (the connection has no `adbc_cancel`), so
    a cancelled per-batch pull cannot abort the in-flight C call: the worker
    finishes its read, the cancellation is then re-raised, and the pulled batch is
    discarded. The connection is NOT invalidated --- it was never poisoned, and
    invalidating would race a second thread against the still-running read on the
    same connection (CR-34-01) --- so it returns to the pool on the normal checkin.

    Args:
        depth: How deep to descend the hierarchy --- `"all"` (the default),
            `"catalogs"`, `"db_schemas"`, `"tables"`, or `"columns"`. Forwarded
            to the driver unchanged.
        catalog_filter: Restrict to this catalog. Forwarded unchanged; `None`
            (the default) leaves it unrestricted.
        db_schema_filter: Restrict to this schema. Forwarded unchanged; `None`
            (the default) leaves it unrestricted.
        table_name_filter: Restrict to this table name. Forwarded unchanged;
            `None` (the default) leaves it unrestricted.
        table_types_filter: Restrict to these table types. Forwarded unchanged;
            `None` (the default) leaves it unrestricted.
        column_name_filter: Restrict to this column name. Forwarded unchanged;
            `None` (the default) leaves it unrestricted.

    Returns:
        An `AsyncRecordBatchReader` streaming the object hierarchy, each pull
        offloaded through the pool limiter.

    Raises:
        ConnectionBusyError: If another offloaded call on this connection is
            already in flight.

    Example:
        ```python
        async with await pool.connect() as conn:
            async with await conn.adbc_get_objects(depth="tables") as reader:
                async for batch in reader:
                    process(batch)  # a pyarrow.RecordBatch, each pull offloaded
        ```
    """
    sync_conn = cast("_SyncConnection", self._fairy)
    with self._offloading():
        sync_reader = await offload(
            functools.partial(
                sync_conn.adbc_get_objects,
                depth=depth,
                catalog_filter=catalog_filter,
                db_schema_filter=db_schema_filter,
                table_name_filter=table_name_filter,
                table_types_filter=table_types_filter,
                column_name_filter=column_name_filter,
            ),
            limiter=self._limiter,
        )
    # Lock the connection for the reader's WHOLE lifetime (D-29-08/09). Set AFTER
    # the _offloading() span exits (it already cleared _in_use) and ONLY on the
    # success path, so a cancelled/failed creation leaves _reader_open False and
    # the connection usable (Pitfall 3).
    self._reader_open = True
    return AsyncRecordBatchReader(
        sync_reader, self._limiter, self, _noop_cancel, poison_on_cancel=False
    )

adbc_get_statistics async

adbc_get_statistics(
    *,
    catalog_filter: str | None = None,
    db_schema_filter: str | None = None,
    table_name_filter: str | None = None,
    approximate: bool = True,
) -> AsyncRecordBatchReader

Stream table statistics as an AsyncRecordBatchReader.

Offloads the sync adbc_get_statistics() through the pool limiter to create the native pyarrow.RecordBatchReader, then wraps it in an AsyncRecordBatchReader whose every batch pull is itself offloaded --- never materialized. The filter arguments forward through functools.partial. A backend that does not implement statistics (for example DuckDB) surfaces the driver's native NotSupportedError unchanged (locked decision #6).

Like commit, creating the reader is not cooperatively cancellable: a surrounding timeout or cancellation cannot abort the in-flight metadata call.

The returned reader locks the connection for its WHOLE lifetime: a foreign op raises ConnectionBusyError until the reader is closed (STREAM-06), and a read after close / check-in surfaces the driver's native pyarrow.lib.ArrowInvalid. The _reader_open lifetime lock is set AFTER the _offloading() span exits and ONLY on the success path. The reader has no cancel of its own, so a cancelled per-batch pull cannot abort the C call: the worker finishes its read and the cancellation is re-raised. The connection is NOT invalidated (it was never poisoned) --- it returns to the pool on the normal checkin (CR-34-01).

Parameters:

Name Type Description Default
catalog_filter str | None

Restrict to this catalog. Forwarded unchanged; None (the default) leaves it unrestricted.

None
db_schema_filter str | None

Restrict to this schema. Forwarded unchanged; None (the default) leaves it unrestricted.

None
table_name_filter str | None

Restrict to this table name. Forwarded unchanged; None (the default) leaves it unrestricted.

None
approximate bool

Allow the backend to return approximate statistics. True (the default) is forwarded to the driver unchanged.

True

Returns:

Type Description
AsyncRecordBatchReader

An AsyncRecordBatchReader streaming the statistics, each pull offloaded

AsyncRecordBatchReader

through the pool limiter.

Raises:

Type Description
ConnectionBusyError

If another offloaded call on this connection is already in flight.

Example
async with await pool.connect() as conn:
    async with await conn.adbc_get_statistics() as reader:
        async for batch in reader:
            process(batch)  # a pyarrow.RecordBatch, each pull offloaded
Source code in src/adbc_poolhouse/_async/_connection.py
async def adbc_get_statistics(
    self,
    *,
    catalog_filter: str | None = None,
    db_schema_filter: str | None = None,
    table_name_filter: str | None = None,
    approximate: bool = True,
) -> AsyncRecordBatchReader:
    """
    Stream table statistics as an `AsyncRecordBatchReader`.

    Offloads the sync `adbc_get_statistics()` through the pool limiter to create
    the native `pyarrow.RecordBatchReader`, then wraps it in an
    `AsyncRecordBatchReader` whose every batch pull is itself offloaded --- never
    materialized. The filter arguments forward through `functools.partial`. A
    backend that does not implement statistics (for example DuckDB) surfaces the
    driver's native `NotSupportedError` unchanged (locked decision #6).

    Like `commit`, creating the reader is **not** cooperatively cancellable: a
    surrounding timeout or cancellation cannot abort the in-flight metadata call.

    The returned reader locks the connection for its WHOLE lifetime: a foreign op
    raises `ConnectionBusyError` until the reader is closed (STREAM-06), and a
    read after close / check-in surfaces the driver's native
    `pyarrow.lib.ArrowInvalid`. The `_reader_open` lifetime lock is set AFTER the
    `_offloading()` span exits and ONLY on the success path. The reader has no
    cancel of its own, so a cancelled per-batch pull cannot abort the C call: the
    worker finishes its read and the cancellation is re-raised. The connection is
    NOT invalidated (it was never poisoned) --- it returns to the pool on the
    normal checkin (CR-34-01).

    Args:
        catalog_filter: Restrict to this catalog. Forwarded unchanged; `None`
            (the default) leaves it unrestricted.
        db_schema_filter: Restrict to this schema. Forwarded unchanged; `None`
            (the default) leaves it unrestricted.
        table_name_filter: Restrict to this table name. Forwarded unchanged;
            `None` (the default) leaves it unrestricted.
        approximate: Allow the backend to return approximate statistics. `True`
            (the default) is forwarded to the driver unchanged.

    Returns:
        An `AsyncRecordBatchReader` streaming the statistics, each pull offloaded
        through the pool limiter.

    Raises:
        ConnectionBusyError: If another offloaded call on this connection is
            already in flight.

    Example:
        ```python
        async with await pool.connect() as conn:
            async with await conn.adbc_get_statistics() as reader:
                async for batch in reader:
                    process(batch)  # a pyarrow.RecordBatch, each pull offloaded
        ```
    """
    sync_conn = cast("_SyncConnection", self._fairy)
    with self._offloading():
        sync_reader = await offload(
            functools.partial(
                sync_conn.adbc_get_statistics,
                catalog_filter=catalog_filter,
                db_schema_filter=db_schema_filter,
                table_name_filter=table_name_filter,
                approximate=approximate,
            ),
            limiter=self._limiter,
        )
    # Set AFTER the _offloading() span exits, success path ONLY (see
    # `adbc_get_objects` for the lifetime-lock rationale, Pitfall 3).
    self._reader_open = True
    return AsyncRecordBatchReader(
        sync_reader, self._limiter, self, _noop_cancel, poison_on_cancel=False
    )

adbc_get_statistic_names async

adbc_get_statistic_names() -> AsyncRecordBatchReader

Stream the backend's statistic names as an AsyncRecordBatchReader.

Offloads the sync adbc_get_statistic_names() through the pool limiter to create the native pyarrow.RecordBatchReader, then wraps it in an AsyncRecordBatchReader whose every batch pull is itself offloaded --- never materialized. A backend that does not implement statistics (for example DuckDB) surfaces the driver's native NotSupportedError unchanged (locked decision #6).

Like commit, creating the reader is not cooperatively cancellable: a surrounding timeout or cancellation cannot abort the in-flight metadata call.

The returned reader locks the connection for its WHOLE lifetime: a foreign op raises ConnectionBusyError until the reader is closed (STREAM-06), and a read after close / check-in surfaces the driver's native pyarrow.lib.ArrowInvalid. The _reader_open lifetime lock is set AFTER the _offloading() span exits and ONLY on the success path. The reader has no cancel of its own, so a cancelled per-batch pull cannot abort the C call: the worker finishes its read and the cancellation is re-raised. The connection is NOT invalidated (it was never poisoned) --- it returns to the pool on the normal checkin (CR-34-01).

Returns:

Type Description
AsyncRecordBatchReader

An AsyncRecordBatchReader streaming the statistic names, each pull

AsyncRecordBatchReader

offloaded through the pool limiter.

Raises:

Type Description
ConnectionBusyError

If another offloaded call on this connection is already in flight.

Example
async with await pool.connect() as conn:
    async with await conn.adbc_get_statistic_names() as reader:
        async for batch in reader:
            process(batch)  # a pyarrow.RecordBatch, each pull offloaded
Source code in src/adbc_poolhouse/_async/_connection.py
async def adbc_get_statistic_names(self) -> AsyncRecordBatchReader:
    """
    Stream the backend's statistic names as an `AsyncRecordBatchReader`.

    Offloads the sync `adbc_get_statistic_names()` through the pool limiter to
    create the native `pyarrow.RecordBatchReader`, then wraps it in an
    `AsyncRecordBatchReader` whose every batch pull is itself offloaded --- never
    materialized. A backend that does not implement statistics (for example
    DuckDB) surfaces the driver's native `NotSupportedError` unchanged (locked
    decision #6).

    Like `commit`, creating the reader is **not** cooperatively cancellable: a
    surrounding timeout or cancellation cannot abort the in-flight metadata call.

    The returned reader locks the connection for its WHOLE lifetime: a foreign op
    raises `ConnectionBusyError` until the reader is closed (STREAM-06), and a
    read after close / check-in surfaces the driver's native
    `pyarrow.lib.ArrowInvalid`. The `_reader_open` lifetime lock is set AFTER the
    `_offloading()` span exits and ONLY on the success path. The reader has no
    cancel of its own, so a cancelled per-batch pull cannot abort the C call: the
    worker finishes its read and the cancellation is re-raised. The connection is
    NOT invalidated (it was never poisoned) --- it returns to the pool on the
    normal checkin (CR-34-01).

    Returns:
        An `AsyncRecordBatchReader` streaming the statistic names, each pull
        offloaded through the pool limiter.

    Raises:
        ConnectionBusyError: If another offloaded call on this connection is
            already in flight.

    Example:
        ```python
        async with await pool.connect() as conn:
            async with await conn.adbc_get_statistic_names() as reader:
                async for batch in reader:
                    process(batch)  # a pyarrow.RecordBatch, each pull offloaded
        ```
    """
    sync_conn = cast("_SyncConnection", self._fairy)
    with self._offloading():
        sync_reader = await offload(sync_conn.adbc_get_statistic_names, limiter=self._limiter)
    # Set AFTER the _offloading() span exits, success path ONLY (see
    # `adbc_get_objects` for the lifetime-lock rationale, Pitfall 3).
    self._reader_open = True
    return AsyncRecordBatchReader(
        sync_reader, self._limiter, self, _noop_cancel, poison_on_cancel=False
    )

close async

close() -> None

Return the connection to the pool (shielded check-in).

Offloads fairy.close() --- which returns the connection to the sync pool and fires the pool's reset event (_release_arrow_allocators) unchanged --- inside anyio.CancelScope(shield=True), so a cancellation arriving mid-check-in cannot abandon the connection in an unknown state (ACONN-02). The _in_use guard is held across the shielded offload.

Raises:

Type Description
ConnectionBusyError

If another offloaded call on this connection is already in flight.

Source code in src/adbc_poolhouse/_async/_connection.py
async def close(self) -> None:
    """
    Return the connection to the pool (shielded check-in).

    Offloads `fairy.close()` --- which returns the connection to the sync pool
    and fires the pool's `reset` event (`_release_arrow_allocators`) unchanged
    --- inside `anyio.CancelScope(shield=True)`, so a cancellation arriving
    mid-check-in cannot abandon the connection in an unknown state (ACONN-02).
    The `_in_use` guard is held across the shielded offload.

    Raises:
        ConnectionBusyError: If another offloaded call on this connection is
            already in flight.
    """
    with self._offloading(), anyio.CancelScope(shield=True):
        await offload(self._fairy.close, limiter=self._limiter)

invalidate async

invalidate() -> None

Drop a poisoned connection from the pool (offloaded, shielded).

A connection whose in-flight call was cancelled is genuinely poisoned --- the driver leaves it with an aborted transaction, so reusing it fails (D-25-03). This drops it instead of returning it: it offloads the fairy's invalidate(), which detaches the underlying dbapi connection and drives the sync pool's checkedout() to 0, inside anyio.CancelScope(shield=True) so a second cancellation arriving mid-recovery cannot leave the pool accounting wrong (CANCEL-02 / D-25-07).

Like __aexit__, it bypasses the _in_use guard: the cursor method that drives this recovery still holds _in_use across its own try/finally, so a connection left marked busy by the cancelled call is still reclaimed.

It offloads through a DEDICATED 1-token teardown limiter, NOT the pool's shared limiter (WR-03): on a single-token pool the just-aborted worker still holds the only pool token until its thread returns, so borrowing the pool token here would deadlock recovery behind the very worker it just aborted. Teardown is not throughput-bounded, so giving it a private token is correct and removes the unenforced scheduler-ordering dependency.

It is the poison-recovery counterpart to close: invalidate is the cancel path, close the normal check-in. A close() after an invalidate() is a safe no-op (probe-confirmed).

Releasing a live reader's lifetime lock (D-29-16): if a streaming AsyncRecordBatchReader held this connection (_reader_open == True) when its pull was cancelled, dropping the connection also clears _reader_open. The connection is being detached from the pool, so its reader-lifetime lock is meaningless --- and a subsequent explicit close() (which takes the foreign tier) must stay the documented safe no-op rather than raise ConnectionBusyError on the now-defunct lock. The per-connect() fresh AsyncConnection is the ultimate backstop (D-29-13), but clearing it here keeps close-after-invalidate correct on the same handle.

Source code in src/adbc_poolhouse/_async/_connection.py
async def invalidate(self) -> None:
    """
    Drop a poisoned connection from the pool (offloaded, shielded).

    A connection whose in-flight call was cancelled is genuinely poisoned ---
    the driver leaves it with an aborted transaction, so reusing it fails
    (D-25-03). This drops it instead of returning it: it offloads the fairy's
    `invalidate()`, which detaches the underlying dbapi connection and drives
    the sync pool's `checkedout()` to 0, inside `anyio.CancelScope(shield=True)`
    so a second cancellation arriving mid-recovery cannot leave the pool
    accounting wrong (CANCEL-02 / D-25-07).

    Like `__aexit__`, it bypasses the `_in_use` guard: the cursor method that
    drives this recovery still holds `_in_use` across its own `try`/`finally`,
    so a connection left marked busy by the cancelled call is still reclaimed.

    It offloads through a DEDICATED 1-token teardown limiter, NOT the pool's
    shared `limiter` (WR-03): on a single-token pool the just-aborted worker
    still holds the only pool token until its thread returns, so borrowing the
    pool token here would deadlock recovery behind the very worker it just
    aborted. Teardown is not throughput-bounded, so giving it a private token
    is correct and removes the unenforced scheduler-ordering dependency.

    It is the poison-recovery counterpart to `close`: invalidate is the cancel
    path, `close` the normal check-in. A `close()` after an `invalidate()` is a
    safe no-op (probe-confirmed).

    Releasing a live reader's lifetime lock (D-29-16): if a streaming
    `AsyncRecordBatchReader` held this connection (`_reader_open == True`) when its
    pull was cancelled, dropping the connection also clears `_reader_open`. The
    connection is being detached from the pool, so its reader-lifetime lock is
    meaningless --- and a subsequent explicit `close()` (which takes the foreign
    tier) must stay the documented safe no-op rather than raise
    `ConnectionBusyError` on the now-defunct lock. The per-`connect()` fresh
    `AsyncConnection` is the ultimate backstop (D-29-13), but clearing it here
    keeps close-after-invalidate correct on the same handle.
    """
    self._reader_open = False
    with anyio.CancelScope(shield=True):
        await offload(self._fairy.invalidate, limiter=self._teardown_limiter)

adbc_poolhouse._async._cursor.AsyncCursor

AsyncCursor(
    sync_cursor: _SyncCursor,
    limiter: CapacityLimiter,
    owner: AsyncConnection,
)

Async wrapper over a sync ADBC cursor.

Opened synchronously by AsyncConnection.cursor. One AsyncCursor belongs to exactly one task, mirroring the synchronous connection-per-thread convention. Every blocking call is offloaded through the owning pool's limiter and guards the parent connection's _in_use flag, so a second concurrent caller is rejected with ConnectionBusyError.

description, rowcount, and arraysize are synchronous property reads (no await, no offload). fetch_arrow_table returns a fully-materialized pyarrow.Table, safe after the connection is checked in.

Example
import adbc_poolhouse

pool = await adbc_poolhouse.create_async_pool(config)
async with await pool.connect() as conn:
    cursor = conn.cursor()
    await cursor.execute("SELECT * FROM events WHERE day = ?", ["2026-06-27"])
    table = await cursor.fetch_arrow_table()  # materialized pyarrow.Table
    print(cursor.rowcount)  # sync property, no await
await adbc_poolhouse.close_async_pool(pool)

Bind a sync cursor to its limiter and owning connection.

Parameters:

Name Type Description Default
sync_cursor _SyncCursor

The underlying ADBC dbapi cursor to wrap.

required
limiter CapacityLimiter

The owning pool's anyio.CapacityLimiter, used to bound every offloaded call. Identical to owner's limiter; held directly so the cursor never reaches through the connection on the hot path.

required
owner AsyncConnection

The AsyncConnection this cursor was opened on. Every offloaded call brackets itself with owner._enter_offload() / owner._exit_offload(), so concurrent cursor use raises ConnectionBusyError via the connection's single-task guard.

required
Source code in src/adbc_poolhouse/_async/_cursor.py
def __init__(
    self,
    sync_cursor: _SyncCursor,
    limiter: CapacityLimiter,
    owner: AsyncConnection,
) -> None:
    """
    Bind a sync cursor to its limiter and owning connection.

    Args:
        sync_cursor: The underlying ADBC dbapi cursor to wrap.
        limiter: The owning pool's `anyio.CapacityLimiter`, used to bound
            every offloaded call. Identical to `owner`'s limiter; held directly
            so the cursor never reaches through the connection on the hot path.
        owner: The `AsyncConnection` this cursor was opened on. Every offloaded
            call brackets itself with `owner._enter_offload()` /
            `owner._exit_offload()`, so concurrent cursor use raises
            `ConnectionBusyError` via the connection's single-task guard.
    """
    self._cursor = sync_cursor
    self._limiter = limiter
    self._owner = owner

description property

description: object

The dbapi description of the last query (synchronous; no offload).

Returns:

Type Description
object

The underlying cursor's description (a sequence of column-metadata

object

tuples, or None before any query). Read directly --- it touches no

object

I/O, so it is not offloaded.

rowcount property

rowcount: int

The dbapi rowcount of the last operation (synchronous; no offload).

Returns:

Type Description
int

The underlying cursor's rowcount (-1 when undetermined). Read

int

directly --- it touches no I/O, so it is not offloaded.

arraysize property

arraysize: int

The dbapi arraysize (default fetchmany batch size; synchronous).

Returns:

Type Description
int

The underlying cursor's arraysize. Read directly --- it touches no

int

I/O, so it is not offloaded.

_adbc_cancel

_adbc_cancel() -> None

Fire the driver's thread-safe adbc_cancel to abort an in-flight call.

Resolved lazily and called only by cancellable_offload when a genuinely-running offload is cancelled (never on the success path). The ADBC dbapi cursor always exposes adbc_cancel; a replay/cassette backend that does not block (and so never aborts mid-flight) need not provide it, so its absence is tolerated as a no-op rather than surfaced --- the abort path is unreachable for an instant, non-blocking backend.

Source code in src/adbc_poolhouse/_async/_cursor.py
def _adbc_cancel(self) -> None:
    """
    Fire the driver's thread-safe `adbc_cancel` to abort an in-flight call.

    Resolved lazily and called only by `cancellable_offload` when a
    genuinely-running offload is cancelled (never on the success path). The
    ADBC dbapi cursor always exposes `adbc_cancel`; a replay/cassette backend
    that does not block (and so never aborts mid-flight) need not provide it,
    so its absence is tolerated as a no-op rather than surfaced --- the abort
    path is unreachable for an instant, non-blocking backend.
    """
    cancel = getattr(self._cursor, "adbc_cancel", None)
    if cancel is not None:
        cancel()

execute async

execute(operation: str, parameters: object = None) -> None

Execute a statement on a worker thread.

Offloads the dbapi execute through the pool limiter while holding the parent connection's _in_use guard, so a concurrent call on the same connection is rejected with ConnectionBusyError (EDGE-15).

Parameters:

Name Type Description Default
operation str

The SQL text to execute.

required
parameters object

Optional bound parameters, forwarded to the dbapi cursor.

None

If the surrounding scope is cancelled or times out while the statement is in flight, the in-flight C call is aborted with cursor.adbc_cancel, the now-poisoned connection is invalidated (shielded), and the cancellation is re-raised --- the connection never returns to the pool busy (CANCEL-01/02).

Raises:

Type Description
ConnectionBusyError

If another offloaded call on the owning connection is already in flight.

Source code in src/adbc_poolhouse/_async/_cursor.py
async def execute(self, operation: str, parameters: object = None) -> None:
    """
    Execute a statement on a worker thread.

    Offloads the dbapi `execute` through the pool limiter while holding the
    parent connection's `_in_use` guard, so a concurrent call on the same
    connection is rejected with `ConnectionBusyError` (EDGE-15).

    Args:
        operation: The SQL text to execute.
        parameters: Optional bound parameters, forwarded to the dbapi cursor.

    If the surrounding scope is cancelled or times out while the statement is
    in flight, the in-flight C call is aborted with `cursor.adbc_cancel`, the
    now-poisoned connection is invalidated (shielded), and the cancellation is
    re-raised --- the connection never returns to the pool busy (CANCEL-01/02).

    Raises:
        ConnectionBusyError: If another offloaded call on the owning connection
            is already in flight.
    """
    with self._owner._offloading():  # noqa: SLF001 (intentional parent guard, see module docstring)
        await cancellable_offload(
            self._adbc_cancel,
            self._cursor.execute,
            operation,
            parameters,
            limiter=self._limiter,
            on_abort=self._owner.invalidate,  # poison recovery on a real abort (D-25-03)
        )

executemany async

executemany(
    operation: str, seq_of_parameters: object
) -> None

Execute a statement once per parameter set on a worker thread.

Offloads the dbapi executemany through the pool limiter while holding the parent connection's _in_use guard.

Parameters:

Name Type Description Default
operation str

The SQL text to execute.

required
seq_of_parameters object

The sequence of parameter sets, forwarded to the dbapi cursor.

required

If the surrounding scope is cancelled mid-flight, the in-flight call is aborted with cursor.adbc_cancel, the poisoned connection is invalidated (shielded), and the cancellation is re-raised (CANCEL-01/02).

Raises:

Type Description
ConnectionBusyError

If another offloaded call on the owning connection is already in flight.

Source code in src/adbc_poolhouse/_async/_cursor.py
async def executemany(self, operation: str, seq_of_parameters: object) -> None:
    """
    Execute a statement once per parameter set on a worker thread.

    Offloads the dbapi `executemany` through the pool limiter while holding the
    parent connection's `_in_use` guard.

    Args:
        operation: The SQL text to execute.
        seq_of_parameters: The sequence of parameter sets, forwarded to the
            dbapi cursor.

    If the surrounding scope is cancelled mid-flight, the in-flight call is
    aborted with `cursor.adbc_cancel`, the poisoned connection is invalidated
    (shielded), and the cancellation is re-raised (CANCEL-01/02).

    Raises:
        ConnectionBusyError: If another offloaded call on the owning connection
            is already in flight.
    """
    with self._owner._offloading():  # noqa: SLF001
        await cancellable_offload(
            self._adbc_cancel,
            self._cursor.executemany,
            operation,
            seq_of_parameters,
            limiter=self._limiter,
            on_abort=self._owner.invalidate,  # poison recovery on a real abort (D-25-03)
        )

fetchone async

fetchone() -> object

Fetch the next row on a worker thread.

If the surrounding scope is cancelled mid-fetch, the in-flight call is aborted with cursor.adbc_cancel, the poisoned connection is invalidated (shielded), and the cancellation is re-raised (CANCEL-01/02).

Returns:

Type Description
object

The next row (a tuple), or None when the result set is exhausted.

Raises:

Type Description
ConnectionBusyError

If another offloaded call on the owning connection is already in flight.

Source code in src/adbc_poolhouse/_async/_cursor.py
async def fetchone(self) -> object:
    """
    Fetch the next row on a worker thread.

    If the surrounding scope is cancelled mid-fetch, the in-flight call is
    aborted with `cursor.adbc_cancel`, the poisoned connection is invalidated
    (shielded), and the cancellation is re-raised (CANCEL-01/02).

    Returns:
        The next row (a tuple), or `None` when the result set is exhausted.

    Raises:
        ConnectionBusyError: If another offloaded call on the owning connection
            is already in flight.
    """
    with self._owner._offloading():  # noqa: SLF001
        return await cancellable_offload(
            self._adbc_cancel,
            self._cursor.fetchone,
            limiter=self._limiter,
            on_abort=self._owner.invalidate,  # poison recovery on a real abort (D-25-03)
        )

fetchmany async

fetchmany(size: int | None = None) -> object

Fetch the next batch of rows on a worker thread.

Parameters:

Name Type Description Default
size int | None

The number of rows to fetch. When None, the dbapi cursor's arraysize is used.

None

If the surrounding scope is cancelled mid-fetch, the in-flight call is aborted with cursor.adbc_cancel, the poisoned connection is invalidated (shielded), and the cancellation is re-raised (CANCEL-01/02).

Returns:

Type Description
object

A sequence of rows (possibly empty when the result set is exhausted).

Raises:

Type Description
ConnectionBusyError

If another offloaded call on the owning connection is already in flight.

Source code in src/adbc_poolhouse/_async/_cursor.py
async def fetchmany(self, size: int | None = None) -> object:
    """
    Fetch the next batch of rows on a worker thread.

    Args:
        size: The number of rows to fetch. When `None`, the dbapi cursor's
            `arraysize` is used.

    If the surrounding scope is cancelled mid-fetch, the in-flight call is
    aborted with `cursor.adbc_cancel`, the poisoned connection is invalidated
    (shielded), and the cancellation is re-raised (CANCEL-01/02).

    Returns:
        A sequence of rows (possibly empty when the result set is exhausted).

    Raises:
        ConnectionBusyError: If another offloaded call on the owning connection
            is already in flight.
    """
    with self._owner._offloading():  # noqa: SLF001
        # Forward `size` only when given, so the dbapi cursor falls back to its
        # own `arraysize` default. The two arms differ only by that argument; a
        # single `*tuple`-spread call would lose the `TypeVarTuple` arity the
        # `cancellable_offload` signature enforces, so the branch stays explicit.
        if size is None:
            return await cancellable_offload(
                self._adbc_cancel,
                self._cursor.fetchmany,
                limiter=self._limiter,
                on_abort=self._owner.invalidate,  # poison recovery on a real abort (D-25-03)
            )
        return await cancellable_offload(
            self._adbc_cancel,
            self._cursor.fetchmany,
            size,
            limiter=self._limiter,
            on_abort=self._owner.invalidate,  # poison recovery on a real abort (D-25-03)
        )

fetchall async

fetchall() -> object

Fetch all remaining rows on a worker thread.

If the surrounding scope is cancelled mid-fetch, the in-flight call is aborted with cursor.adbc_cancel, the poisoned connection is invalidated (shielded), and the cancellation is re-raised (CANCEL-01/02).

Returns:

Type Description
object

A sequence of all remaining rows.

Raises:

Type Description
ConnectionBusyError

If another offloaded call on the owning connection is already in flight.

Source code in src/adbc_poolhouse/_async/_cursor.py
async def fetchall(self) -> object:
    """
    Fetch all remaining rows on a worker thread.

    If the surrounding scope is cancelled mid-fetch, the in-flight call is
    aborted with `cursor.adbc_cancel`, the poisoned connection is invalidated
    (shielded), and the cancellation is re-raised (CANCEL-01/02).

    Returns:
        A sequence of all remaining rows.

    Raises:
        ConnectionBusyError: If another offloaded call on the owning connection
            is already in flight.
    """
    with self._owner._offloading():  # noqa: SLF001
        return await cancellable_offload(
            self._adbc_cancel,
            self._cursor.fetchall,
            limiter=self._limiter,
            on_abort=self._owner.invalidate,  # poison recovery on a real abort (D-25-03)
        )

fetch_arrow_table async

fetch_arrow_table() -> pyarrow.Table

Materialize the full result set as a pyarrow.Table on a worker thread.

Offloads the dbapi fetch_arrow_table through the pool limiter and returns its result unchanged: a fully-materialized pyarrow.Table that owns its own buffers. The table is safe to read after the connection is checked in --- it is never a streaming RecordBatchReader bound to the (soon-closed) cursor (EDGE-21 / Pitfall 7).

If the surrounding scope is cancelled or times out while the result is being materialized, the in-flight C call is aborted with cursor.adbc_cancel, the now-poisoned connection is invalidated (shielded), and the cancellation is re-raised (CANCEL-01/02).

Returns:

Type Description
Table

The materialized pyarrow.Table for the current result set.

Raises:

Type Description
ConnectionBusyError

If another offloaded call on the owning connection is already in flight.

Source code in src/adbc_poolhouse/_async/_cursor.py
async def fetch_arrow_table(self) -> pyarrow.Table:
    """
    Materialize the full result set as a `pyarrow.Table` on a worker thread.

    Offloads the dbapi `fetch_arrow_table` through the pool limiter and returns
    its result unchanged: a fully-materialized `pyarrow.Table` that owns its own
    buffers. The table is safe to read after the connection is checked in --- it
    is never a streaming `RecordBatchReader` bound to the (soon-closed) cursor
    (EDGE-21 / Pitfall 7).

    If the surrounding scope is cancelled or times out while the result is
    being materialized, the in-flight C call is aborted with
    `cursor.adbc_cancel`, the now-poisoned connection is invalidated
    (shielded), and the cancellation is re-raised (CANCEL-01/02).

    Returns:
        The materialized `pyarrow.Table` for the current result set.

    Raises:
        ConnectionBusyError: If another offloaded call on the owning connection
            is already in flight.
    """
    with self._owner._offloading():  # noqa: SLF001
        return await cancellable_offload(
            self._adbc_cancel,
            self._cursor.fetch_arrow_table,
            limiter=self._limiter,
            on_abort=self._owner.invalidate,  # poison recovery on a real abort (D-25-03)
        )

fetch_df async

fetch_df() -> pandas.DataFrame

Materialize the full result set as a pandas.DataFrame on a worker thread.

Offloads the driver's native fetch_df through the pool limiter and returns its result unchanged: a fully-materialized pandas.DataFrame that owns its own buffers. The frame is safe to read after the connection is checked in --- it is never bound to the (soon-closed) cursor's C stream (EDGE-21 / Pitfall 7).

pandas is not a poolhouse dependency --- you install it yourself. poolhouse never imports it: the driver imports pandas inside the worker, so a missing install surfaces the native ModuleNotFoundError unchanged, with no pre-check and no wrapping.

If the surrounding scope is cancelled or times out while the result is being materialized, the in-flight C call is aborted with cursor.adbc_cancel, the now-poisoned connection is invalidated (shielded), and the cancellation is re-raised (CANCEL-01/02).

Returns:

Type Description
DataFrame

The materialized pandas.DataFrame for the current result set.

Raises:

Type Description
ConnectionBusyError

If another offloaded call on the owning connection is already in flight.

ModuleNotFoundError

If pandas is not installed. Raised by the driver in the worker and propagated unchanged.

Example
async with await pool.connect() as conn:
    cursor = conn.cursor()
    await cursor.execute("SELECT * FROM events")
    df = await cursor.fetch_df()  # a pandas.DataFrame
Source code in src/adbc_poolhouse/_async/_cursor.py
async def fetch_df(self) -> pandas.DataFrame:
    """
    Materialize the full result set as a `pandas.DataFrame` on a worker thread.

    Offloads the driver's native `fetch_df` through the pool limiter and returns
    its result unchanged: a fully-materialized `pandas.DataFrame` that owns its
    own buffers. The frame is safe to read after the connection is checked in ---
    it is never bound to the (soon-closed) cursor's C stream (EDGE-21 / Pitfall 7).

    `pandas` is not a poolhouse dependency --- you install it yourself. poolhouse
    never imports it: the driver imports `pandas` inside the worker, so a missing
    install surfaces the native `ModuleNotFoundError` unchanged, with no
    pre-check and no wrapping.

    If the surrounding scope is cancelled or times out while the result is
    being materialized, the in-flight C call is aborted with
    `cursor.adbc_cancel`, the now-poisoned connection is invalidated
    (shielded), and the cancellation is re-raised (CANCEL-01/02).

    Returns:
        The materialized `pandas.DataFrame` for the current result set.

    Raises:
        ConnectionBusyError: If another offloaded call on the owning connection
            is already in flight.
        ModuleNotFoundError: If `pandas` is not installed. Raised by the driver
            in the worker and propagated unchanged.

    Example:
        ```python
        async with await pool.connect() as conn:
            cursor = conn.cursor()
            await cursor.execute("SELECT * FROM events")
            df = await cursor.fetch_df()  # a pandas.DataFrame
        ```
    """
    with self._owner._offloading():  # noqa: SLF001
        # `_SyncCursor.fetch_df` is typed `-> object` to keep the Protocol
        # driver-agnostic and free of a pandas stub dependency (D-31-06); cast
        # back to the public return type. No runtime effect --- the driver
        # materializes and returns the pandas.DataFrame itself.
        return cast(
            "pandas.DataFrame",
            await cancellable_offload(
                self._adbc_cancel,
                self._cursor.fetch_df,
                limiter=self._limiter,
                on_abort=self._owner.invalidate,  # poison recovery on a real abort (D-25-03)
            ),
        )

fetch_polars async

fetch_polars() -> polars.DataFrame

Materialize the full result set as a polars.DataFrame on a worker thread.

Offloads the driver's native fetch_polars through the pool limiter and returns its result unchanged: a fully-materialized polars.DataFrame that owns its own buffers. The frame is safe to read after the connection is checked in --- it is never bound to the (soon-closed) cursor's C stream (EDGE-21 / Pitfall 7).

polars is not a poolhouse dependency --- you install it yourself. poolhouse never imports it: the driver imports polars inside the worker, so a missing install surfaces the native ModuleNotFoundError unchanged, with no pre-check and no wrapping.

If the surrounding scope is cancelled or times out while the result is being materialized, the in-flight C call is aborted with cursor.adbc_cancel, the now-poisoned connection is invalidated (shielded), and the cancellation is re-raised (CANCEL-01/02).

Returns:

Type Description
DataFrame

The materialized polars.DataFrame for the current result set.

Raises:

Type Description
ConnectionBusyError

If another offloaded call on the owning connection is already in flight.

ModuleNotFoundError

If polars is not installed. Raised by the driver in the worker and propagated unchanged.

Example
async with await pool.connect() as conn:
    cursor = conn.cursor()
    await cursor.execute("SELECT * FROM events")
    df = await cursor.fetch_polars()  # a polars.DataFrame
Source code in src/adbc_poolhouse/_async/_cursor.py
async def fetch_polars(self) -> polars.DataFrame:
    """
    Materialize the full result set as a `polars.DataFrame` on a worker thread.

    Offloads the driver's native `fetch_polars` through the pool limiter and
    returns its result unchanged: a fully-materialized `polars.DataFrame` that
    owns its own buffers. The frame is safe to read after the connection is
    checked in --- it is never bound to the (soon-closed) cursor's C stream
    (EDGE-21 / Pitfall 7).

    `polars` is not a poolhouse dependency --- you install it yourself. poolhouse
    never imports it: the driver imports `polars` inside the worker, so a missing
    install surfaces the native `ModuleNotFoundError` unchanged, with no
    pre-check and no wrapping.

    If the surrounding scope is cancelled or times out while the result is
    being materialized, the in-flight C call is aborted with
    `cursor.adbc_cancel`, the now-poisoned connection is invalidated
    (shielded), and the cancellation is re-raised (CANCEL-01/02).

    Returns:
        The materialized `polars.DataFrame` for the current result set.

    Raises:
        ConnectionBusyError: If another offloaded call on the owning connection
            is already in flight.
        ModuleNotFoundError: If `polars` is not installed. Raised by the driver
            in the worker and propagated unchanged.

    Example:
        ```python
        async with await pool.connect() as conn:
            cursor = conn.cursor()
            await cursor.execute("SELECT * FROM events")
            df = await cursor.fetch_polars()  # a polars.DataFrame
        ```
    """
    with self._owner._offloading():  # noqa: SLF001
        # `_SyncCursor.fetch_polars` is typed `-> object` to keep the Protocol
        # driver-agnostic (D-31-06); cast back to the public return type. No
        # runtime effect --- the driver materializes and returns the
        # polars.DataFrame itself.
        return cast(
            "polars.DataFrame",
            await cancellable_offload(
                self._adbc_cancel,
                self._cursor.fetch_polars,
                limiter=self._limiter,
                on_abort=self._owner.invalidate,  # poison recovery on a real abort (D-25-03)
            ),
        )

adbc_ingest async

adbc_ingest(
    table_name: str,
    data: RecordBatch
    | Table
    | RecordBatchReader
    | CapsuleType,
    *,
    mode: Literal[
        "create", "append", "replace", "create_append"
    ] = "create",
    catalog_name: str | None = None,
    db_schema_name: str | None = None,
    temporary: bool = False,
) -> int

Bulk-load an Arrow dataset into a table on a worker thread.

Offloads the dbapi adbc_ingest through the pool limiter while holding the parent connection's _in_use guard, so a concurrent call on the same connection is rejected with ConnectionBusyError (EDGE-15). This is a single whole-operation offload --- the connection checks back in the moment the ingest returns, unlike fetch_record_batch, which holds the connection for a reader's whole lifetime.

data is handed to the driver untouched: poolhouse performs no conversion and no validation. The driver owns the Arrow binding and the table identifier, so a malformed dataset or a bad identifier surfaces the driver's native error unchanged.

If the surrounding scope is cancelled or times out while the ingest is in flight, the in-flight C call is aborted with cursor.adbc_cancel, the now-poisoned connection is invalidated (shielded), and the cancellation is re-raised --- the connection never returns to the pool busy (CANCEL-01/02). Recovery restores the connection, not the table: an aborted bulk load can leave rows already written, and poolhouse does not roll that back. Treat a cancelled ingest as leaving the table in an undefined state.

Parameters:

Name Type Description Default
table_name str

The target table. Passed straight to the driver as a SQL identifier; poolhouse does not quote or sanitize it.

required
data RecordBatch | Table | RecordBatchReader | CapsuleType

The Arrow dataset to load --- a pyarrow.Table, RecordBatch, RecordBatchReader, or an Arrow C-stream capsule. Forwarded to the driver with zero conversion.

required
mode Literal['create', 'append', 'replace', 'create_append']

How to write the data. "create" (the default) makes a new table and fails if it exists; "append" adds rows to an existing table; "create_append" creates the table if needed, then appends; "replace" drops the existing table and recreates it --- the old rows are lost, so it is not a row-level upsert. Forwarded to the driver verbatim.

'create'
catalog_name str | None

EXPERIMENTAL. Target catalog for the table. No stability guarantee; surfaced as-is from the driver.

None
db_schema_name str | None

EXPERIMENTAL. Target schema for the table. No stability guarantee; surfaced as-is from the driver.

None
temporary bool

EXPERIMENTAL. Create the table as temporary. No stability guarantee; surfaced as-is from the driver.

False

Returns:

Type Description
int

The number of rows written, or -1 when the driver cannot report a

int

count. Poolhouse returns the driver's value unchanged.

Raises:

Type Description
ConnectionBusyError

If another offloaded call on the owning connection is already in flight.

Example
import pyarrow as pa

people = pa.table({"id": [1, 2, 3], "name": ["a", "b", "c"]})
async with await pool.connect() as conn:
    cursor = conn.cursor()
    await cursor.adbc_ingest("people", people, mode="create")  # returns 3
    await cursor.adbc_ingest("people", people, mode="append")  # returns 3 (6 total)
Source code in src/adbc_poolhouse/_async/_cursor.py
async def adbc_ingest(
    self,
    table_name: str,
    data: pyarrow.RecordBatch | pyarrow.Table | pyarrow.RecordBatchReader | CapsuleType,
    *,
    mode: Literal["create", "append", "replace", "create_append"] = "create",
    catalog_name: str | None = None,
    db_schema_name: str | None = None,
    temporary: bool = False,
) -> int:
    """
    Bulk-load an Arrow dataset into a table on a worker thread.

    Offloads the dbapi `adbc_ingest` through the pool limiter while holding the
    parent connection's `_in_use` guard, so a concurrent call on the same
    connection is rejected with `ConnectionBusyError` (EDGE-15). This is a
    single whole-operation offload --- the connection checks back in the moment
    the ingest returns, unlike `fetch_record_batch`, which holds the connection
    for a reader's whole lifetime.

    `data` is handed to the driver untouched: poolhouse performs no conversion
    and no validation. The driver owns the Arrow binding and the table
    identifier, so a malformed dataset or a bad identifier surfaces the driver's
    native error unchanged.

    If the surrounding scope is cancelled or times out while the ingest is in
    flight, the in-flight C call is aborted with `cursor.adbc_cancel`, the
    now-poisoned connection is invalidated (shielded), and the cancellation is
    re-raised --- the connection never returns to the pool busy (CANCEL-01/02).
    Recovery restores the *connection*, not the *table*: an aborted bulk load
    can leave rows already written, and poolhouse does not roll that back. Treat
    a cancelled ingest as leaving the table in an undefined state.

    Args:
        table_name: The target table. Passed straight to the driver as a SQL
            identifier; poolhouse does not quote or sanitize it.
        data: The Arrow dataset to load --- a `pyarrow.Table`, `RecordBatch`,
            `RecordBatchReader`, or an Arrow C-stream capsule. Forwarded to the
            driver with zero conversion.
        mode: How to write the data. `"create"` (the default) makes a new table
            and fails if it exists; `"append"` adds rows to an existing table;
            `"create_append"` creates the table if needed, then appends;
            `"replace"` **drops** the existing table and recreates it --- the old
            rows are lost, so it is not a row-level upsert. Forwarded to the
            driver verbatim.
        catalog_name: EXPERIMENTAL. Target catalog for the table. No stability
            guarantee; surfaced as-is from the driver.
        db_schema_name: EXPERIMENTAL. Target schema for the table. No stability
            guarantee; surfaced as-is from the driver.
        temporary: EXPERIMENTAL. Create the table as temporary. No stability
            guarantee; surfaced as-is from the driver.

    Returns:
        The number of rows written, or `-1` when the driver cannot report a
        count. Poolhouse returns the driver's value unchanged.

    Raises:
        ConnectionBusyError: If another offloaded call on the owning connection
            is already in flight.

    Example:
        ```python
        import pyarrow as pa

        people = pa.table({"id": [1, 2, 3], "name": ["a", "b", "c"]})
        async with await pool.connect() as conn:
            cursor = conn.cursor()
            await cursor.adbc_ingest("people", people, mode="create")  # returns 3
            await cursor.adbc_ingest("people", people, mode="append")  # returns 3 (6 total)
        ```
    """
    with self._owner._offloading():  # noqa: SLF001
        return await cancellable_offload(
            self._adbc_cancel,
            functools.partial(
                self._cursor.adbc_ingest,
                table_name,
                data,
                mode=mode,
                catalog_name=catalog_name,
                db_schema_name=db_schema_name,
                temporary=temporary,
            ),
            limiter=self._limiter,
            on_abort=self._owner.invalidate,  # poison recovery on a real abort (D-25-03)
        )

adbc_prepare async

adbc_prepare(
    operation: bytes | str,
) -> pyarrow.Schema | None

Prepare a statement without executing it, on a worker thread.

Offloads the dbapi adbc_prepare through the pool limiter while holding the parent connection's _in_use guard, so a concurrent call on the same connection is rejected with ConnectionBusyError (EDGE-15). The query is prepared but NOT executed --- no rows are read and no data is written.

Returns the schema of the query's BIND PARAMETERS, or None when the driver cannot determine it. Poolhouse forwards the driver's value unchanged; treat a None result as "the backend does not report a parameter schema", not an error.

If the surrounding scope is cancelled or times out while the prepare is in flight, the in-flight C call is aborted with cursor.adbc_cancel and the cancellation is re-raised. Because a prepare writes no table data, the connection is NOT invalidated --- it is cancellable but non-poisoning, returning clean to the pool (D-35-04). This is the deliberate difference from execute, which invalidates on abort. The non-poisoning guarantee assumes the driver leaves no lingering session state after a cancelled prepare (true for DuckDB); a backend whose cancel aborts the surrounding transaction may need the connection rolled back before reuse.

Parameters:

Name Type Description Default
operation bytes | str

The SQL text to prepare. Passed to the driver verbatim; poolhouse constructs no SQL and adds no sanitization.

required

Returns:

Type Description
Schema | None

The pyarrow.Schema describing the query's bind parameters, or None

Schema | None

when the driver cannot determine a parameter schema.

Raises:

Type Description
ConnectionBusyError

If another offloaded call on the owning connection is already in flight.

Example
async with await pool.connect() as conn:
    cursor = conn.cursor()
    schema = await cursor.adbc_prepare("SELECT * FROM t WHERE id = ?")
    # `schema` describes the `?` bind parameters (or is None).
Source code in src/adbc_poolhouse/_async/_cursor.py
async def adbc_prepare(self, operation: bytes | str) -> pyarrow.Schema | None:
    """
    Prepare a statement without executing it, on a worker thread.

    Offloads the dbapi `adbc_prepare` through the pool limiter while holding the
    parent connection's `_in_use` guard, so a concurrent call on the same
    connection is rejected with `ConnectionBusyError` (EDGE-15). The query is
    prepared but NOT executed --- no rows are read and no data is written.

    Returns the schema of the query's BIND PARAMETERS, or `None` when the driver
    cannot determine it. Poolhouse forwards the driver's value unchanged; treat a
    `None` result as "the backend does not report a parameter schema", not an
    error.

    If the surrounding scope is cancelled or times out while the prepare is in
    flight, the in-flight C call is aborted with `cursor.adbc_cancel` and the
    cancellation is re-raised. Because a prepare writes no table data, the
    connection is NOT invalidated --- it is cancellable but non-poisoning, returning
    clean to the pool (D-35-04). This is the deliberate difference from `execute`,
    which invalidates on abort. The non-poisoning guarantee assumes the driver
    leaves no lingering session state after a cancelled prepare (true for DuckDB); a
    backend whose cancel aborts the surrounding transaction may need the connection
    rolled back before reuse.

    Args:
        operation: The SQL text to prepare. Passed to the driver verbatim;
            poolhouse constructs no SQL and adds no sanitization.

    Returns:
        The `pyarrow.Schema` describing the query's bind parameters, or `None`
        when the driver cannot determine a parameter schema.

    Raises:
        ConnectionBusyError: If another offloaded call on the owning connection
            is already in flight.

    Example:
        ```python
        async with await pool.connect() as conn:
            cursor = conn.cursor()
            schema = await cursor.adbc_prepare("SELECT * FROM t WHERE id = ?")
            # `schema` describes the `?` bind parameters (or is None).
        ```
    """
    with self._owner._offloading():  # noqa: SLF001
        # `_SyncCursor.adbc_prepare` is typed `-> object` to keep the Protocol
        # driver-agnostic (D-35-05); cast back to the public return type. No
        # runtime effect --- the driver returns the pyarrow.Schema (or None).
        return cast(
            "pyarrow.Schema | None",
            await cancellable_offload(
                self._adbc_cancel,
                self._cursor.adbc_prepare,
                operation,
                limiter=self._limiter,
                # on_abort OMITTED (D-35-04): cancellable but non-poisoning ---
                # a prepare writes no state, so an aborted call leaves the
                # connection clean; do NOT invalidate (unlike execute).
            ),
        )

adbc_execute_schema async

adbc_execute_schema(
    operation: bytes | str, parameters: object = None
) -> pyarrow.Schema

Get a query's result-set schema without executing it, on a worker thread.

Offloads the dbapi adbc_execute_schema through the pool limiter while holding the parent connection's _in_use guard, so a concurrent call on the same connection is rejected with ConnectionBusyError (EDGE-15). The query is planned but NOT run --- no rows are fetched and no side effects occur; only the result-set schema is returned.

If the surrounding scope is cancelled or times out while the call is in flight, the in-flight C call is aborted with cursor.adbc_cancel and the cancellation is re-raised. Because the query never executes, the connection is NOT invalidated --- it is cancellable but non-poisoning, returning clean to the pool (D-35-04). As with adbc_prepare, this assumes the driver leaves no lingering session state after a cancelled call (true for DuckDB); a backend whose cancel aborts the surrounding transaction may need a rollback before reuse.

An unsupported backend surfaces the driver's native error unchanged: poolhouse does not catch, wrap, or find_spec-pre-check it (D-35-06). DuckDB, for example, does not implement result-schema introspection and raises NotSupportedError straight through the single offload chokepoint (EDGE-17).

Parameters:

Name Type Description Default
operation bytes | str

The SQL text whose result schema to resolve. Passed to the driver verbatim; poolhouse constructs no SQL and adds no sanitization.

required
parameters object

Optional bound parameters, forwarded to the dbapi cursor.

None

Returns:

Type Description
Schema

The pyarrow.Schema describing the query's result set, resolved without

Schema

executing the query.

Raises:

Type Description
ConnectionBusyError

If another offloaded call on the owning connection is already in flight.

NotSupportedError

If the driver does not implement result-schema introspection (e.g. DuckDB). Raised by the driver in the worker and propagated unchanged through the offload chokepoint (EDGE-17).

Example
async with await pool.connect() as conn:
    cursor = conn.cursor()
    schema = await cursor.adbc_execute_schema("SELECT id, name FROM t")
    # `schema` is the result-set schema; the query never ran.
Source code in src/adbc_poolhouse/_async/_cursor.py
async def adbc_execute_schema(
    self, operation: bytes | str, parameters: object = None
) -> pyarrow.Schema:
    """
    Get a query's result-set schema without executing it, on a worker thread.

    Offloads the dbapi `adbc_execute_schema` through the pool limiter while
    holding the parent connection's `_in_use` guard, so a concurrent call on the
    same connection is rejected with `ConnectionBusyError` (EDGE-15). The query is
    planned but NOT run --- no rows are fetched and no side effects occur; only the
    result-set schema is returned.

    If the surrounding scope is cancelled or times out while the call is in
    flight, the in-flight C call is aborted with `cursor.adbc_cancel` and the
    cancellation is re-raised. Because the query never executes, the connection is
    NOT invalidated --- it is cancellable but non-poisoning, returning clean to the
    pool (D-35-04). As with `adbc_prepare`, this assumes the driver leaves no
    lingering session state after a cancelled call (true for DuckDB); a backend
    whose cancel aborts the surrounding transaction may need a rollback before reuse.

    An unsupported backend surfaces the driver's native error unchanged: poolhouse
    does not catch, wrap, or `find_spec`-pre-check it (D-35-06). DuckDB, for
    example, does not implement result-schema introspection and raises
    `NotSupportedError` straight through the single offload chokepoint (EDGE-17).

    Args:
        operation: The SQL text whose result schema to resolve. Passed to the
            driver verbatim; poolhouse constructs no SQL and adds no sanitization.
        parameters: Optional bound parameters, forwarded to the dbapi cursor.

    Returns:
        The `pyarrow.Schema` describing the query's result set, resolved without
        executing the query.

    Raises:
        ConnectionBusyError: If another offloaded call on the owning connection
            is already in flight.
        NotSupportedError: If the driver does not implement result-schema
            introspection (e.g. DuckDB). Raised by the driver in the worker and
            propagated unchanged through the offload chokepoint (EDGE-17).

    Example:
        ```python
        async with await pool.connect() as conn:
            cursor = conn.cursor()
            schema = await cursor.adbc_execute_schema("SELECT id, name FROM t")
            # `schema` is the result-set schema; the query never ran.
        ```
    """
    with self._owner._offloading():  # noqa: SLF001
        # `_SyncCursor.adbc_execute_schema` is typed `-> object` to keep the
        # Protocol driver-agnostic (D-35-05); cast back to the public return type.
        # No runtime effect --- the driver returns the pyarrow.Schema itself.
        return cast(
            "pyarrow.Schema",
            await cancellable_offload(
                self._adbc_cancel,
                self._cursor.adbc_execute_schema,
                operation,
                parameters,
                limiter=self._limiter,
                # on_abort OMITTED (D-35-04): cancellable but non-poisoning ---
                # the query never executes, so an aborted call leaves the
                # connection clean; do NOT invalidate (unlike execute).
            ),
        )

fetch_record_batch async

fetch_record_batch() -> AsyncRecordBatchReader

Stream the result set as an AsyncRecordBatchReader off a worker thread.

Offloads the dbapi fetch_record_batch through the pool limiter to create the sync pyarrow.RecordBatchReader, then wraps it in an AsyncRecordBatchReader whose every batch pull is itself offloaded (D-29-03). Unlike fetch_arrow_table, the result is a live stream bound to this connection's C Arrow stream, so the reader locks the connection for its WHOLE lifetime: a foreign op raises ConnectionBusyError until the reader is closed (STREAM-06), and a read after the reader is closed / the connection is checked in surfaces the driver's native pyarrow.lib.ArrowInvalid (never a segfault, T-29-01).

The _reader_open lifetime lock is set AFTER the _offloading() span exits (which already cleared the per-call _in_use) and ONLY on the success path, so a cancelled or failed creation leaves the connection usable rather than permanently busy (Pitfall 5).

If the surrounding scope is cancelled or times out while the reader is being created, the in-flight C call is aborted with cursor.adbc_cancel, the now-poisoned connection is invalidated (shielded), and the cancellation is re-raised --- the connection never returns to the pool busy (CANCEL-01/02).

Returns:

Type Description
AsyncRecordBatchReader

An AsyncRecordBatchReader streaming the current result set, threaded

AsyncRecordBatchReader

with this cursor's adbc_cancel so a cancelled pull aborts through the

AsyncRecordBatchReader

cursor (the reader has no cancel of its own, Pitfall 4).

Raises:

Type Description
ConnectionBusyError

If another offloaded call on the owning connection is already in flight.

Example
await cursor.execute("SELECT * FROM events")
async with await cursor.fetch_record_batch() as reader:
    async for batch in reader:
        process(batch)  # a pyarrow.RecordBatch, each pull offloaded
Source code in src/adbc_poolhouse/_async/_cursor.py
async def fetch_record_batch(self) -> AsyncRecordBatchReader:
    """
    Stream the result set as an `AsyncRecordBatchReader` off a worker thread.

    Offloads the dbapi `fetch_record_batch` through the pool limiter to create
    the sync `pyarrow.RecordBatchReader`, then wraps it in an
    `AsyncRecordBatchReader` whose every batch pull is itself offloaded (D-29-03).
    Unlike
    `fetch_arrow_table`, the result is a live stream bound to this connection's C
    Arrow stream, so the reader locks the connection for its WHOLE lifetime: a
    foreign op raises `ConnectionBusyError` until the reader is closed (STREAM-06),
    and a read after the reader is closed / the connection is checked in surfaces
    the driver's native `pyarrow.lib.ArrowInvalid` (never a segfault, T-29-01).

    The `_reader_open` lifetime lock is set AFTER the `_offloading()` span exits
    (which already cleared the per-call `_in_use`) and ONLY on the success path,
    so a cancelled or failed creation leaves the connection usable rather than
    permanently busy (Pitfall 5).

    If the surrounding scope is cancelled or times out while the reader is being
    created, the in-flight C call is aborted with `cursor.adbc_cancel`, the
    now-poisoned connection is invalidated (shielded), and the cancellation is
    re-raised --- the connection never returns to the pool busy (CANCEL-01/02).

    Returns:
        An `AsyncRecordBatchReader` streaming the current result set, threaded
        with this cursor's `adbc_cancel` so a cancelled pull aborts through the
        cursor (the reader has no cancel of its own, Pitfall 4).

    Raises:
        ConnectionBusyError: If another offloaded call on the owning connection
            is already in flight.

    Example:
        ```python
        await cursor.execute("SELECT * FROM events")
        async with await cursor.fetch_record_batch() as reader:
            async for batch in reader:
                process(batch)  # a pyarrow.RecordBatch, each pull offloaded
        ```
    """
    with self._owner._offloading():  # noqa: SLF001  foreign-tier guard: _in_use OR _reader_open
        sync_reader = await cancellable_offload(
            self._adbc_cancel,
            self._cursor.fetch_record_batch,
            limiter=self._limiter,
            on_abort=self._owner.invalidate,  # poison recovery on a real abort (D-25-03)
        )
    # Lock the connection for the reader's WHOLE lifetime (D-29-08/09). Set AFTER
    # the _offloading() span exits (it already cleared _in_use) and ONLY on the
    # success path, so a cancelled/failed creation leaves _reader_open False and
    # the connection usable (Pitfall 5).
    self._owner._reader_open = True  # noqa: SLF001
    # Pass this cursor's own `_adbc_cancel` as the 4th arg: a cancelled pull must
    # abort through the cursor, since the reader has no cancel of its own (Pitfall 4).
    return AsyncRecordBatchReader(
        sync_reader,
        self._limiter,
        self._owner,
        self._adbc_cancel,
    )

close async

close() -> None

Close the underlying cursor on a worker thread (shielded).

Offloads the dbapi close inside anyio.CancelScope(shield=True), so a cancellation arriving mid-close cannot abandon an open cursor (which would pin Arrow readers). The parent connection's _in_use guard is held across the shielded offload.

Raises:

Type Description
ConnectionBusyError

If another offloaded call on the owning connection is already in flight.

Source code in src/adbc_poolhouse/_async/_cursor.py
async def close(self) -> None:
    """
    Close the underlying cursor on a worker thread (shielded).

    Offloads the dbapi `close` inside `anyio.CancelScope(shield=True)`, so a
    cancellation arriving mid-close cannot abandon an open cursor (which would
    pin Arrow readers). The parent connection's `_in_use` guard is held across
    the shielded offload.

    Raises:
        ConnectionBusyError: If another offloaded call on the owning connection
            is already in flight.
    """
    with self._owner._offloading(), anyio.CancelScope(shield=True):  # noqa: SLF001
        await offload(
            self._cursor.close,
            limiter=self._limiter,
        )

adbc_poolhouse._async._reader.AsyncRecordBatchReader

AsyncRecordBatchReader(
    sync_reader: _SyncReader,
    limiter: CapacityLimiter,
    owner: AsyncConnection,
    adbc_cancel: Callable[[], None],
    *,
    poison_on_cancel: bool = True,
)

Async wrapper over a sync pyarrow.RecordBatchReader.

Created by AsyncCursor.fetch_record_batch and reached only through await cursor.fetch_record_batch(). Holds the sync reader by composition (never subclasses it, D-29-01) and offloads each batch pull and the final close through the owning pool's limiter, so no blocking Arrow C call ever runs on the event loop.

A live reader locks its owning connection for its WHOLE lifetime, so a foreign op on the same connection raises ConnectionBusyError while the reader is open (STREAM-06). The lock is cleared by close() --- NOT at drain (D-29-11) --- so the reader must be closed; async with is the canonical usage and its finalizer only warns.

schema is a synchronous property read (no await, no offload). Reading after the reader is closed --- or after the connection is checked in, which closes the underlying cursor --- surfaces the driver's native pyarrow.lib.ArrowInvalid, never a poolhouse error and never a segfault (T-29-01).

Example
import adbc_poolhouse

pool = await adbc_poolhouse.create_async_pool(config)
async with await pool.connect() as conn:
    cursor = conn.cursor()
    await cursor.execute("SELECT * FROM events WHERE day = ?", ["2026-06-27"])
    async with await cursor.fetch_record_batch() as reader:
        async for batch in reader:  # each pull offloaded off-loop
            process(batch)  # a pyarrow.RecordBatch
await adbc_poolhouse.close_async_pool(pool)

Bind a sync reader to its limiter, owning connection, and the cursor's cancel.

Parameters:

Name Type Description Default
sync_reader _SyncReader

The underlying sync pyarrow.RecordBatchReader to wrap.

required
limiter CapacityLimiter

The owning pool's anyio.CapacityLimiter, used to bound every offloaded pull and the close. Identical to owner's limiter; held directly so the reader never reaches through the connection on the hot path.

required
owner AsyncConnection

The AsyncConnection this reader streams from. Each pull brackets itself with owner._offloading(from_reader=True) (the reentrancy exemption, so the reader does not deadlock on its own lifetime lock), and close clears owner._reader_open.

required
adbc_cancel Callable[[], None]

The cancel callback fired to abort an in-flight pull. For a cursor-backed reader this is the owning CURSOR's adbc_cancel bound method (the reader has no adbc_cancel of its own, Pitfall 4). For a connection-level metadata reader there is no adbc_cancel to thread in, so a no-op is passed and poison_on_cancel is set False (see below) --- the two go together.

required
poison_on_cancel bool

Whether a cancelled pull invalidates the owning connection. True (the default, the cursor path) is correct when adbc_cancel genuinely aborts the in-flight C call: the driver call returns poisoned, so poison-recovery must run. False (the connection-level metadata path) is REQUIRED when adbc_cancel is a no-op: the worker thread cannot be aborted and stays in the driver's read_next_batch until it returns on its own, so invalidating here would drive fairy.invalidate() on a SECOND thread concurrently with that still-running read --- the concurrent single-connection access ADBC forbids. With False, a cancelled pull instead re-raises the cancellation once the joined worker finishes, leaving the (never poisoned) connection to return to the pool through the normal checkin.

True
Source code in src/adbc_poolhouse/_async/_reader.py
def __init__(
    self,
    sync_reader: _SyncReader,
    limiter: CapacityLimiter,
    owner: AsyncConnection,
    adbc_cancel: Callable[[], None],
    *,
    poison_on_cancel: bool = True,
) -> None:
    """
    Bind a sync reader to its limiter, owning connection, and the cursor's cancel.

    Args:
        sync_reader: The underlying sync `pyarrow.RecordBatchReader` to wrap.
        limiter: The owning pool's `anyio.CapacityLimiter`, used to bound every
            offloaded pull and the close. Identical to `owner`'s limiter; held
            directly so the reader never reaches through the connection on the
            hot path.
        owner: The `AsyncConnection` this reader streams from. Each pull brackets
            itself with `owner._offloading(from_reader=True)` (the reentrancy
            exemption, so the reader does not deadlock on its own lifetime lock),
            and `close` clears `owner._reader_open`.
        adbc_cancel: The cancel callback fired to abort an in-flight pull. For a
            cursor-backed reader this is the owning CURSOR's `adbc_cancel` bound
            method (the reader has no `adbc_cancel` of its own, Pitfall 4). For a
            connection-level metadata reader there is no `adbc_cancel` to thread
            in, so a no-op is passed and `poison_on_cancel` is set `False` (see
            below) --- the two go together.
        poison_on_cancel: Whether a cancelled pull invalidates the owning
            connection. `True` (the default, the cursor path) is correct when
            `adbc_cancel` genuinely aborts the in-flight C call: the driver call
            returns poisoned, so poison-recovery must run. `False` (the
            connection-level metadata path) is REQUIRED when `adbc_cancel` is a
            no-op: the worker thread cannot be aborted and stays in the driver's
            `read_next_batch` until it returns on its own, so invalidating here
            would drive `fairy.invalidate()` on a SECOND thread concurrently with
            that still-running read --- the concurrent single-connection access
            ADBC forbids. With `False`, a cancelled pull instead re-raises the
            cancellation once the joined worker finishes, leaving the (never
            poisoned) connection to return to the pool through the normal checkin.
    """
    self._reader = sync_reader
    self._limiter = limiter
    self._owner = owner
    # The CURSOR's cancel, NOT `self._reader.adbc_cancel` (which does not exist
    # --- a pyarrow reader has no cancel hook). See Pitfall 4. May be a no-op for a
    # connection-level metadata reader, in which case `poison_on_cancel` is False.
    self._adbc_cancel = adbc_cancel
    # False for connection-level metadata readers whose `adbc_cancel` is a no-op:
    # the worker cannot be aborted, so invalidating on cancel would race a second
    # thread against the still-running read on the same connection (CR-34-01).
    self._poison_on_cancel = poison_on_cancel
    # Idempotency + finalizer latch: True once closed (or detached via close), so
    # `close` is a safe no-op on a second call and `__del__` warns only when the
    # reader was abandoned unclosed.
    self._detached = False

schema property

schema: Schema

The reader's Arrow schema (synchronous; no offload --- touches no I/O).

Returns:

Type Description
Schema

The wrapped sync reader's pyarrow.Schema. Read directly --- reading a

Schema

schema touches no I/O, so it is not offloaded and not async (a

Schema

coroutine property would be the "coroutine was never awaited" footgun).

close async

close() -> None

Close the underlying sync reader on a worker thread (shielded, idempotent).

Offloads the sync reader's close inside anyio.CancelScope(shield=True), so a cancellation arriving mid-close cannot abandon an open C stream (which would pin the connection's Arrow allocators). Idempotent via _detached: a second close --- or a close after the connection was already checked in --- is a safe no-op.

The owning connection's _reader_open lock is cleared in a finally, so even a close whose offloaded sync_reader.close raises still releases the connection (never stranding it busy) and chains the raised body error via __context__ (EDGE-20 / D-29-16).

Raises:

Type Description
Exception

Whatever the sync reader's close raises is propagated unchanged, after the lock is released.

Source code in src/adbc_poolhouse/_async/_reader.py
async def close(self) -> None:
    """
    Close the underlying sync reader on a worker thread (shielded, idempotent).

    Offloads the sync reader's `close` inside `anyio.CancelScope(shield=True)`, so
    a cancellation arriving mid-close cannot abandon an open C stream (which would
    pin the connection's Arrow allocators). Idempotent via `_detached`: a second
    `close` --- or a `close` after the connection was already checked in --- is a
    safe no-op.

    The owning connection's `_reader_open` lock is cleared in a `finally`, so even
    a `close` whose offloaded `sync_reader.close` raises still releases the
    connection (never stranding it busy) and chains the raised body error via
    `__context__` (EDGE-20 / D-29-16).

    Raises:
        Exception: Whatever the sync reader's `close` raises is propagated
            unchanged, after the lock is released.
    """
    if self._detached:
        return  # idempotent: close-after-close / close-after-checkin no-op
    self._detached = True
    with anyio.CancelScope(shield=True):
        try:
            await offload(self._reader.close, limiter=self._limiter)
        finally:
            # Clear the lifetime lock even if close raised (EDGE-20): a failed
            # cleanup must never leave the connection permanently busy.
            self._owner._reader_open = False  # noqa: SLF001