Coverage for src/qdrant_loader/core/state/session.py: 93%
27 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-20 10:15 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-20 10:15 +0000
1from __future__ import annotations
3from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
4from sqlalchemy.pool import StaticPool
6from qdrant_loader.config.state import StateManagementConfig
7from qdrant_loader.core.state.models import Base
8from qdrant_loader.core.state.utils import generate_database_url
11def initialize_engine_and_session(
12 config: StateManagementConfig,
13) -> tuple[AsyncEngine, async_sessionmaker]:
14 """Create the async engine and session factory for the state DB.
16 SQLite uses a StaticPool with check_same_thread disabled (single in-process
17 file/memory DB). PostgreSQL uses a real connection pool sized from
18 config.connection_pool, with pool_pre_ping so RDS idle-connection drops are
19 detected and recycled rather than surfacing as errors.
20 """
21 database_url = generate_database_url(config)
23 if database_url.startswith("sqlite"):
24 is_in_memory = ":memory:" in database_url or "mode=memory" in database_url
25 connect_args = {"check_same_thread": False, "timeout": 30}
27 if is_in_memory:
28 engine = create_async_engine(
29 database_url,
30 poolclass=StaticPool,
31 connect_args=connect_args,
32 echo=False,
33 )
34 else:
35 engine = create_async_engine(
36 database_url,
37 connect_args=connect_args,
38 echo=False,
39 )
40 else:
41 # asyncpg is an optional dependency (the `postgres` extra), imported
42 # lazily only on the Postgres path so SQLite installs don't need it.
43 try:
44 import asyncpg # noqa: F401
45 except ModuleNotFoundError as exc:
46 raise RuntimeError(
47 "PostgreSQL state backend requires the 'postgres' extra: "
48 "install with `pip install qdrant-loader[postgres]`."
49 ) from exc
51 pool_cfg = config.connection_pool or {}
52 engine = create_async_engine(
53 database_url,
54 pool_size=pool_cfg.get("size", 5),
55 pool_timeout=pool_cfg.get("timeout", 30),
56 pool_pre_ping=True,
57 pool_recycle=1800, # recycle connections every 30 min (RDS-friendly)
58 echo=False,
59 )
61 session_factory = async_sessionmaker(engine, expire_on_commit=False)
62 return engine, session_factory
65async def create_tables(engine: AsyncEngine) -> None:
66 """Create database tables if they do not exist."""
67 async with engine.begin() as conn:
68 await conn.run_sync(Base.metadata.create_all)
71async def dispose_engine(engine: AsyncEngine) -> None:
72 """Dispose the async engine and free resources."""
73 await engine.dispose()