# app/core/database.py """ Database engine, session factory, and base model. Supports SQLite (dev) and PostgreSQL (prod) via DATABASE_URL. Production-hardened: connection pool tuning, pre-ping, recycle. """ import logging from sqlalchemy import create_engine from sqlalchemy.orm import declarative_base, sessionmaker from app.core.config import settings logger = logging.getLogger("hale.database") # ── Connection args ──────────────────────────────────────────────────────── connect_args: dict = {} pool_kwargs: dict = {} if settings.is_sqlite: # SQLite: single-thread safety, no connection pool connect_args["check_same_thread"] = False else: # PostgreSQL: production-grade connection pooling pool_kwargs = { "pool_size": 20, # Base connections kept alive across workers "max_overflow": 10, # Burst connections allowed above pool_size "pool_timeout": 30, # Seconds to wait if pool is exhausted "pool_recycle": 1800, # Recycle connections every 30 min (prevents stale TCP) } logger.info( "PostgreSQL pool: pool_size=%d, max_overflow=%d, recycle=%ds", pool_kwargs["pool_size"], pool_kwargs["max_overflow"], pool_kwargs["pool_recycle"], ) engine = create_engine( settings.DATABASE_URL, connect_args=connect_args, echo=False, pool_pre_ping=True, # Validates connection before use — handles DB restarts **pool_kwargs, ) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() def init_db() -> None: """Create all tables (idempotent — safe for existing tables).""" # Import all models so SQLAlchemy registers them before create_all from app.models import user, goal, progress # noqa: F401 Base.metadata.create_all(bind=engine) def get_db(): """FastAPI dependency that yields a database session.""" db = SessionLocal() try: yield db finally: db.close()