"""Async SQLAlchemy engine, session factory, and declarative base. Works with SQLite (aiosqlite) or Postgres (asyncpg) depending on DATABASE_URL. """ from collections.abc import AsyncGenerator from sqlalchemy import event from sqlalchemy.ext.asyncio import ( AsyncSession, async_sessionmaker, create_async_engine, ) from sqlalchemy.orm import DeclarativeBase from app.core.config import settings class Base(DeclarativeBase): """Declarative base shared by all models.""" engine = create_async_engine(settings.database_url, echo=settings.debug, future=True) if engine.sync_engine.dialect.name == "sqlite": @event.listens_for(engine.sync_engine, "connect") def _set_sqlite_pragma(dbapi_connection, connection_record) -> None: """WAL mode lets readers/writers avoid blocking each other; busy_timeout makes a connection wait for a lock instead of immediately raising "database is locked" (the pysqlite/aiosqlite default is 0ms). """ cursor = dbapi_connection.cursor() cursor.execute("PRAGMA journal_mode=WAL") cursor.execute("PRAGMA busy_timeout=5000") cursor.close() AsyncSessionLocal = async_sessionmaker( engine, class_=AsyncSession, expire_on_commit=False, autoflush=False, ) async def get_db() -> AsyncGenerator[AsyncSession, None]: """FastAPI dependency yielding a request-scoped session.""" async with AsyncSessionLocal() as session: yield session async def init_db() -> None: """Create all tables. Convenient for dev/SQLite and tests. In production prefer Alembic migrations (``alembic upgrade head``). """ # Import models so they register with Base.metadata before create_all. import app.models # noqa: F401 async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all)