Skip to content

Transactions

transaction() is an async context manager that runs everything inside the block on one connection, committing on exit and rolling back on exception. It yields a Transaction handle for raw SQL that must run on the transaction's connection. It can route by using= or by an explicit/ambient session (ferro.engines.session(...)). See the Transactions guide for semantics and patterns.

transaction(using=None, *, session=None) async

Run database operations inside a transaction context.

Yields a :class:~ferro.raw.Transaction handle bound to this transaction's connection. The handle exposes execute / fetch_all / fetch_one for raw SQL on the same connection — useful for setting Postgres GUCs, advisory locks, and any one-off statement that doesn't fit a Model.

Examples:

>>> async with transaction() as tx:
...     user = await User.create(name="Taylor")
...     await tx.execute(
...         "select set_config('request.jwt.claims', $1, true)",
...         claims_json,
...     )

Existing callers that don't bind the yielded value continue to work; the handle is simply discarded::

>>> async with transaction():
...     user = await User.create(name="Taylor")
...     await user.save()
Source code in src/ferro/models.py
@asynccontextmanager
async def transaction(using: str | None = None, *, session: "Session | None" = None):
    """Run database operations inside a transaction context.

    Yields a :class:`~ferro.raw.Transaction` handle bound to this transaction's
    connection. The handle exposes ``execute`` / ``fetch_all`` / ``fetch_one``
    for raw SQL on the same connection — useful for setting Postgres GUCs,
    advisory locks, and any one-off statement that doesn't fit a Model.

    Examples:
        >>> async with transaction() as tx:
        ...     user = await User.create(name="Taylor")
        ...     await tx.execute(
        ...         "select set_config('request.jwt.claims', $1, true)",
        ...         claims_json,
        ...     )

    Existing callers that don't bind the yielded value continue to work; the
    handle is simply discarded::

        >>> async with transaction():
        ...     user = await User.create(name="Taylor")
        ...     await user.save()
    """
    from .raw import Transaction

    route = resolve_transaction_scope(using=using, session=session)
    tx_id = await begin_transaction(route)
    connection_name = transaction_connection_name(tx_id, session_id=route.session_id)
    child_route = route_for_transaction(connection_name, tx_id, route.session_id)
    token = _CURRENT_TRANSACTION.set(tx_id)
    connection_token = _CURRENT_TRANSACTION_CONNECTION.set(connection_name)
    try:
        yield Transaction(child_route)
        await commit_transaction(tx_id, session_id=route.session_id)
    except Exception:
        await rollback_transaction(tx_id, session_id=route.session_id)
        raise
    finally:
        _CURRENT_TRANSACTION.reset(token)
        _CURRENT_TRANSACTION_CONNECTION.reset(connection_token)

Transaction

Handle for a live transaction.

Obtained via async with transaction() as tx. Methods delegate to the top-level :func:execute / :func:fetch_all / :func:fetch_one with this transaction's route (a :class:RouteHandle built by models.transaction()) passed explicitly, so they don't depend on the ContextVar state. This makes the connection-affinity invariant structurally impossible to violate from inside the async with block.

The handle becomes invalid once the async with block exits — any subsequent call raises :class:RuntimeError.

Source code in src/ferro/raw.py
class Transaction:
    """Handle for a live transaction.

    Obtained via ``async with transaction() as tx``. Methods delegate to the
    top-level :func:`execute` / :func:`fetch_all` / :func:`fetch_one` with this
    transaction's route (a :class:`RouteHandle` built by
    ``models.transaction()``) passed explicitly, so they don't depend on the
    ContextVar state. This makes the connection-affinity invariant
    structurally impossible to violate from inside the ``async with`` block.

    The handle becomes invalid once the ``async with`` block exits — any
    subsequent call raises :class:`RuntimeError`.
    """

    __slots__ = ("_route",)

    def __init__(self, route: RouteHandle) -> None:
        self._route = route

    async def execute(self, sql: str, *args: Any) -> int:
        _check_sql(sql)
        marshalled = [_marshal(a) for a in args]
        return await _raw_execute(sql, marshalled, self._route)

    async def fetch_all(self, sql: str, *args: Any) -> list[dict[str, Any]]:
        _check_sql(sql)
        marshalled = [_marshal(a) for a in args]
        return await _raw_fetch_all(sql, marshalled, self._route)

    async def fetch_one(self, sql: str, *args: Any) -> dict[str, Any] | None:
        _check_sql(sql)
        marshalled = [_marshal(a) for a in args]
        return await _raw_fetch_one(sql, marshalled, self._route)

Attributes

__slots__ = ('_route',) class-attribute instance-attribute

Functions

__init__(route)

Source code in src/ferro/raw.py
def __init__(self, route: RouteHandle) -> None:
    self._route = route

execute(sql, *args) async

Source code in src/ferro/raw.py
async def execute(self, sql: str, *args: Any) -> int:
    _check_sql(sql)
    marshalled = [_marshal(a) for a in args]
    return await _raw_execute(sql, marshalled, self._route)

fetch_all(sql, *args) async

Source code in src/ferro/raw.py
async def fetch_all(self, sql: str, *args: Any) -> list[dict[str, Any]]:
    _check_sql(sql)
    marshalled = [_marshal(a) for a in args]
    return await _raw_fetch_all(sql, marshalled, self._route)

fetch_one(sql, *args) async

Source code in src/ferro/raw.py
async def fetch_one(self, sql: str, *args: Any) -> dict[str, Any] | None:
    _check_sql(sql)
    marshalled = [_marshal(a) for a in args]
    return await _raw_fetch_one(sql, marshalled, self._route)