Source code for httk.store.backend.duckdb.engine
"""DuckDB database construction for :class:`~httk.store.backend.sql.engine.Backend`."""
import importlib
import os
from typing import TYPE_CHECKING, Any
import sqlalchemy
if TYPE_CHECKING:
from httk.store.backend.sql.engine import Backend
[docs]
def database(
cls: "type[Backend]",
path: str | os.PathLike[str] | None = None,
*,
memory_limit: str | None = None,
read_only: bool = False,
) -> "Backend":
"""Build a DuckDB-backed backend stored in ``path``, or in memory when ``path`` is None.
:param cls: The backend class to instantiate.
:param path: The database file path, or ``None`` for an in-memory database.
:param memory_limit: An optional DuckDB ``memory_limit`` setting such as
``"1GB"``. DuckDB's own default allows every instance up to about 80% of
system RAM, which multiplies dangerously across parallel test or ingest
processes; when this parameter is ``None`` the ``HTTK_DUCKDB_MEMORY_LIMIT``
environment variable (if set) supplies the cap instead, so process trees
can be memory-guarded wholesale.
:param read_only: Open the file in DuckDB ``READ_ONLY`` access mode. A read-only
database takes no write lock, so several processes (and this one) may open the
same file concurrently for reading; write operations on the resulting backend
will fail. Ignored for the in-memory database.
:return: The configured backend wrapper.
:raises ImportError: If the ``duckdb_engine`` SQLAlchemy dialect is not installed;
install the ``httk-store[duckdb]`` extra to use it.
"""
try:
importlib.import_module("duckdb_engine")
except ImportError as error:
raise ImportError(
"the DuckDB backend needs the 'duckdb_engine' SQLAlchemy dialect; "
"install the 'httk-store[duckdb]' extra to use Backend.duckdb()"
) from error
location = ":memory:" if path is None else os.fspath(path)
limit = memory_limit if memory_limit is not None else os.environ.get("HTTK_DUCKDB_MEMORY_LIMIT")
config: dict[str, Any] = {}
if limit:
config["memory_limit"] = limit
if read_only and path is not None:
config["access_mode"] = "READ_ONLY"
options: dict[str, Any] = {"connect_args": {"config": config}} if config else {}
engine = sqlalchemy.create_engine(f"duckdb:///{location}", **options)
# duckdb_engine derives from the psycopg2 dialect, which doubles
# backslashes when rendering inline string literals (PostgreSQL's
# non-standard-conforming-strings legacy). DuckDB always uses
# standard-conforming string literals, so that doubling corrupts e.g.
# the LIKE ... ESCAPE '\' clause the search DSL emits; turn it off.
engine.dialect._backslash_escapes = False # type: ignore[attr-defined]
return cls(engine)