"""SQLAlchemy engine, session factory, and an explicit schema-creation helper. The engine is built from `settings.DATABASE_URL` (`app.config`, already fail-loud-validated at import time) -- no credentials are hardcoded or logged anywhere in this module. Connection-failure messages name the database host/port/name being targeted but intentionally never include `DB_PASSWORD` or the raw `DATABASE_URL`. `create_all()` is NOT called at import time or anywhere else in this module automatically. Table creation is a deliberate, explicit step gated on human confirmation of `migrations/schema.sql` (DB-02) -- importing this module must never mutate the database. """ from __future__ import annotations from collections.abc import Generator from contextlib import contextmanager from sqlalchemy import create_engine from sqlalchemy.engine import Engine from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import Session, sessionmaker from app.config import settings from app.db.models import Base engine: Engine = create_engine(settings.DATABASE_URL, pool_pre_ping=True, future=True) SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True) def get_session() -> Generator[Session, None, None]: """FastAPI-dependency-style session generator. Usage: `db: Session = Depends(get_session)` in a router. Closes the session after the request regardless of outcome. """ session = SessionLocal() try: yield session finally: session.close() @contextmanager def session_scope() -> Generator[Session, None, None]: """Context-manager session for scripts (training/, one-off maintenance) that run outside FastAPI's dependency-injection system. Commits on success, rolls back on any exception, always closes. """ session = SessionLocal() try: yield session session.commit() except Exception: session.rollback() raise finally: session.close() def create_all() -> None: """Create every table defined on `Base.metadata` against the configured MySQL database. This is the explicit, human-approved migration step for phase 01-02 (DB-02): only invoke this after `migrations/schema.sql` has been reviewed and confirmed by the user. Never called automatically by importing this module. Raises `RuntimeError` with a clear, credential-free message if the database is unreachable or rejects the DDL -- the password is never included, only the host/port/database name being targeted. """ try: Base.metadata.create_all(bind=engine) except SQLAlchemyError as exc: raise RuntimeError( "Could not create the database schema: the configured MySQL " f"database (host={settings.DB_HOST}, port={settings.DB_PORT}, " f"db={settings.DB_NAME}) is unreachable or rejected the " "operation. Check DB_HOST/DB_PORT/DB_NAME/DB_USER/DB_PASSWORD " "in .env -- credential values are intentionally omitted from " "this message." ) from exc