39 lines
1.4 KiB
Python
39 lines
1.4 KiB
Python
from collections.abc import Generator
|
|
from typing import Any, cast
|
|
|
|
from sqlalchemy import CursorResult, Engine, create_engine
|
|
from sqlalchemy.engine import Result
|
|
from sqlalchemy.orm import Session
|
|
|
|
from modelforge_api.persistence.runtime_engine import register_application_engine
|
|
from modelforge_api.settings import get_settings
|
|
|
|
|
|
def rows_affected(result: Result[Any]) -> int:
|
|
"""How many rows a DML statement actually changed.
|
|
|
|
`Session.execute` is typed as returning `Result`, which carries no `rowcount`; a DML statement
|
|
always returns a `CursorResult`, which does. SQLAlchemy 2.0.52 narrowed that return type, so
|
|
reading `.rowcount` directly stopped type-checking — while continuing to work at runtime.
|
|
|
|
The cast is where that knowledge lives, once, rather than at ten call sites. It matters more
|
|
than it looks: every single-use claim in the platform is a conditional UPDATE whose row count
|
|
decides the winner, and that is what makes enrolment tokens and lifecycle claims atomic instead
|
|
of merely usually-correct.
|
|
"""
|
|
|
|
return cast("CursorResult[Any]", result).rowcount
|
|
|
|
|
|
def build_engine(database_url: str | None = None) -> Engine:
|
|
url = database_url or get_settings().database_url
|
|
return register_application_engine(create_engine(url, pool_pre_ping=True))
|
|
|
|
|
|
engine = build_engine()
|
|
|
|
|
|
def get_session() -> Generator[Session, None, None]:
|
|
with Session(engine) as session:
|
|
yield session
|