| from __future__ import annotations | |
| from collections.abc import AsyncIterator | |
| from functools import lru_cache | |
| from sqlalchemy.ext.asyncio import ( | |
| AsyncEngine, | |
| AsyncSession, | |
| async_sessionmaker, | |
| create_async_engine, | |
| ) | |
| from app.core.config import get_settings | |
| def get_engine() -> AsyncEngine: | |
| """Process-wide async SQLAlchemy engine. | |
| ``pool_pre_ping`` checks a pooled connection is still alive before handing it out and | |
| transparently reconnects if not — needed for serverless Postgres (Neon) that suspends | |
| and drops idle connections. Harmless for local SQLite. | |
| """ | |
| return create_async_engine( | |
| get_settings().database_url, echo=False, future=True, pool_pre_ping=True | |
| ) | |
| def get_session_factory() -> async_sessionmaker[AsyncSession]: | |
| return async_sessionmaker(get_engine(), expire_on_commit=False, class_=AsyncSession) | |
| async def get_session() -> AsyncIterator[AsyncSession]: | |
| """Yield a single AsyncSession; rollback on error, commit responsibility is on caller.""" | |
| factory = get_session_factory() | |
| async with factory() as session: | |
| try: | |
| yield session | |
| except Exception: | |
| await session.rollback() | |
| raise | |