Files
srbhr--resume-matcher/apps/backend/app/db_engine.py
T
wehub-resource-sync 5bdf4cc89a
Publish Docker Image / publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:36 +08:00

72 lines
2.8 KiB
Python

"""SQLite engine/session plumbing for the SQLAlchemy data layer.
Every ``Database`` instance owns its own engines (one async for the document
tables, one sync for the encrypted ``api_keys`` table read on the synchronous
LLM hot path) built from these factories. Keeping construction here lets tests
spin up fully isolated engines against a temp-file database.
"""
from pathlib import Path
from typing import Any
from sqlalchemy import create_engine, event
from sqlalchemy.engine import Engine
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
from app.models import Base
__all__ = ["Base", "make_async_engine", "make_sync_engine", "init_models_sync"]
def _apply_sqlite_pragmas(dbapi_connection: Any, _connection_record: Any) -> None:
"""Set per-connection SQLite PRAGMAs.
WAL improves concurrent read/write between the async (doc tables) and sync
(api_keys) engines pointed at the same file; ``busy_timeout`` rides out the
brief lock contention that creates; ``foreign_keys`` enforces relational
integrity (off by default in SQLite).
"""
cursor = dbapi_connection.cursor()
try:
cursor.execute("PRAGMA journal_mode=WAL")
cursor.execute("PRAGMA foreign_keys=ON")
cursor.execute("PRAGMA busy_timeout=5000")
finally:
cursor.close()
def _url(path: Path, *, driver: str) -> str:
"""Build a SQLite URL. Absolute paths yield the required four slashes."""
return f"sqlite+{driver}:///{path}" if driver else f"sqlite:///{path}"
def make_async_engine(path: Path) -> AsyncEngine:
"""Create the async engine (``aiosqlite``) for the document tables."""
engine = create_async_engine(_url(path, driver="aiosqlite"), future=True)
event.listen(engine.sync_engine, "connect", _apply_sqlite_pragmas)
return engine
def make_sync_engine(path: Path) -> Engine:
"""Create the sync engine used for the encrypted api_keys table.
Key reads happen synchronously (``get_llm_config`` → ``load_config_file`` →
``resolve_api_key``), so a sync engine avoids threading async through
``llm.py``. It points at the same file as the async engine.
"""
engine = create_engine(_url(path, driver=""), future=True)
event.listen(engine, "connect", _apply_sqlite_pragmas)
return engine
def init_models_sync(engine: Engine) -> None:
"""Create all tables (idempotent) using a sync engine connection."""
Base.metadata.create_all(engine)
# ``create_all`` does not ALTER existing SQLite tables. Keep this additive
# migration idempotent so older local databases can load resumes safely.
with engine.begin() as conn:
columns = conn.exec_driver_sql("PRAGMA table_info(resumes)").mappings().all()
if columns and "interview_prep" not in {column["name"] for column in columns}:
conn.exec_driver_sql("ALTER TABLE resumes ADD COLUMN interview_prep TEXT")