"""Async SQLAlchemy engine and session management (架构文档 §5, §12.2). The engine is created lazily so that importing this module never opens a connection — important for unit tests and for Celery workers that fork. """ from __future__ import annotations from collections.abc import AsyncIterator from contextlib import asynccontextmanager from sqlalchemy.ext.asyncio import ( AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine, ) from packages.shared.config import Settings, get_settings _engine: AsyncEngine | None = None _session_factory: async_sessionmaker[AsyncSession] | None = None def create_engine(settings: Settings | None = None) -> AsyncEngine: cfg = settings or get_settings() return create_async_engine( cfg.database_url, echo=cfg.db_echo, pool_size=cfg.db_pool_size, max_overflow=cfg.db_max_overflow, pool_pre_ping=True, pool_recycle=1800, ) def get_engine() -> AsyncEngine: global _engine if _engine is None: _engine = create_engine() return _engine def get_session_factory() -> async_sessionmaker[AsyncSession]: global _session_factory if _session_factory is None: _session_factory = async_sessionmaker( bind=get_engine(), expire_on_commit=False, autoflush=False, ) return _session_factory @asynccontextmanager async def session_scope() -> AsyncIterator[AsyncSession]: """Transactional scope: commit on success, rollback on error.""" factory = get_session_factory() async with factory() as session: try: yield session await session.commit() except Exception: await session.rollback() raise async def dispose_engine() -> None: global _engine, _session_factory if _engine is not None: await _engine.dispose() _engine = None _session_factory = None