"""SQLAlchemy engine and session factory. Engine and session factory are cached globally per process. Only one DATABASE_URL is effectively used for the process lifetime. Using a different URL later (e.g. in tests with in-memory SQLite) would still use the first engine. To use a different URL in tests, set env (e.g. DATABASE_URL) before the first import of this module, or clear _engine and _SessionLocal in test fixtures. Prefer session_scope() for all callers so sessions are always closed and rolled back on error. """ from contextlib import contextmanager from typing import Generator from sqlalchemy import create_engine from sqlalchemy.orm import Session, sessionmaker _engine = None _SessionLocal = None @contextmanager def session_scope(database_url: str) -> Generator[Session, None, None]: """Context manager: yields a session, rolls back on exception, closes on exit.""" session = get_session(database_url) try: yield session except Exception: session.rollback() raise finally: session.close() def get_engine(database_url: str): global _engine if _engine is None: _engine = create_engine( database_url, connect_args={"check_same_thread": False} if "sqlite" in database_url else {}, echo=False, ) return _engine def get_session_factory(database_url: str) -> sessionmaker[Session]: global _SessionLocal if _SessionLocal is None: engine = get_engine(database_url) _SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) return _SessionLocal def get_session(database_url: str) -> Session: return get_session_factory(database_url)()