Source code for rational.backend.database
"""SQLAlchemy integration: an async engine plus request-scoped sessions.
SQLAlchemy stays the source of truth for ORM semantics; this module only
creates the engine and hands out sessions. Use Alembic for migrations;
``create_all()`` is a development convenience.
"""
from __future__ import annotations
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import Any
try:
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase
except ImportError as exc: # pragma: no cover
raise ImportError("database support needs SQLAlchemy: pip install 'rational[db]'") from exc
class Model(DeclarativeBase):
"""Optional declarative base. Any SQLAlchemy declarative base works with ``Database``."""
[docs]
class Database:
def __init__(self, url: str, *, echo: bool = False, **engine_options: Any) -> None:
self.url = url
self.engine = create_async_engine(url, echo=echo, **engine_options)
self.sessionmaker = async_sessionmaker(self.engine, expire_on_commit=False)
[docs]
def session(self) -> AsyncSession:
"""``async with db.session() as session: ...`` — commit explicitly."""
return self.sessionmaker()
[docs]
@asynccontextmanager
async def transaction(self) -> AsyncIterator[AsyncSession]:
"""A session inside a transaction: commits on success, rolls back on error."""
async with self.sessionmaker() as session, session.begin():
yield session
async def create_all(self, base: type[DeclarativeBase] = Model) -> None:
async with self.engine.begin() as conn:
await conn.run_sync(base.metadata.create_all)
async def dispose(self) -> None:
await self.engine.dispose()