# app/core/database.py """ Database configuration and session management. This module provides classes and functions for: - PostgreSQL database engine creation and configuration - Session management with connection pooling - Database dependency for FastAPI routes Note: This project uses PostgreSQL only. SQLite is not supported. """ import logging from sqlalchemy import create_engine, event from sqlalchemy.orm import declarative_base, sessionmaker, with_loader_criteria from sqlalchemy.pool import QueuePool from .config import settings, validate_database_url # Validate database URL on import validate_database_url() # Create PostgreSQL engine with connection pooling engine = create_engine( settings.database_url, poolclass=QueuePool, pool_size=10, max_overflow=20, pool_pre_ping=True, echo=False, ) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- # Soft-delete automatic query filter # --------------------------------------------------------------------------- # Any model that inherits SoftDeleteMixin will automatically have # `WHERE deleted_at IS NULL` appended to SELECT queries. # Bypass with: db.execute(stmt, execution_options={"include_deleted": True}) # or db.query(Model).execution_options(include_deleted=True).all() # --------------------------------------------------------------------------- def register_soft_delete_filter(session_factory): """Register the soft-delete query filter on a session factory. Call this for any sessionmaker that should auto-exclude soft-deleted records. Used for both the production SessionLocal and test session factories. """ @event.listens_for(session_factory, "do_orm_execute") def _soft_delete_filter(orm_execute_state): if ( orm_execute_state.is_select and not orm_execute_state.execution_options.get("include_deleted", False) ): from models.database.base import SoftDeleteMixin orm_execute_state.statement = orm_execute_state.statement.options( with_loader_criteria( SoftDeleteMixin, lambda cls: cls.deleted_at.is_(None), include_aliases=True, ) ) return _soft_delete_filter # Register on the production session factory register_soft_delete_filter(SessionLocal) def get_db(): """ Database session dependency for FastAPI routes. Yields a database session and ensures proper cleanup. Handles exceptions and rolls back transactions on error. """ db = SessionLocal() try: yield db except Exception as e: logger.error(f"Database session error: {e}") db.rollback() raise finally: db.close()