"""Database engine + session factory for prediction logging. Single engine per process, instantiated lazily so the API can boot even when ``DATABASE_URL`` is unset (local dev without Supabase). Use ``init_engine()`` from the FastAPI lifespan to validate connectivity at startup, and ``get_engine()`` from request handlers to grab the cached instance. """ from __future__ import annotations import logging from threading import Lock from sqlalchemy import Engine, create_engine from api import settings logger = logging.getLogger(__name__) _engine: Engine | None = None _lock = Lock() def init_engine(database_url: str | None = None) -> Engine | None: """Initialise the global engine. Returns None when no URL is configured. Called once at app startup. Safe to call multiple times — subsequent calls are no-ops. """ global _engine url = database_url or settings.DATABASE_URL if not url: logger.warning( "DATABASE_URL not set — prediction logging disabled. " "Set the env var to enable Supabase persistence." ) return None with _lock: if _engine is None: _engine = create_engine( url, pool_size=5, max_overflow=10, pool_pre_ping=True, future=True, ) logger.info("Database engine initialised") return _engine def get_engine() -> Engine | None: """Return the cached engine, or None if logging is disabled.""" return _engine def reset_engine() -> None: """Dispose and clear the engine. Useful for tests.""" global _engine with _lock: if _engine is not None: _engine.dispose() _engine = None