Source code for fsh_lib.loading

"""Eager-load application for list endpoints.

:func:`apply_eager_loads` is the companion of
:func:`fsh_lib.ordering.apply_ordering`: it turns a representation's
:class:`EagerLoad` plan into ``select(...).options(...)`` loader
options.  The one piece of coordination is ``joined`` -- the set of
relationships ``apply_ordering`` already LEFT-joined for an
``ORDER BY``.  A top-level relationship in that set is loaded with
``contains_eager`` (reuse the existing join) instead of being
fetched a second time by ``selectinload`` / ``joinedload``.

Generated list handlers wire the two together explicitly::

    stmt = select(Task)
    stmt, joined = apply_ordering(stmt=stmt, sort_clauses=body.sort, ...)
    stmt = apply_filters(stmt=stmt, node=body.filter, model=Task)
    stmt = apply_eager_loads(
        stmt, [EagerLoad(Task.project, "joined")], joined=joined
    )
"""

from dataclasses import dataclass
from enum import StrEnum
from typing import TYPE_CHECKING

from sqlalchemy.orm import (
    contains_eager,
    joinedload,
    selectinload,
    subqueryload,
)

if TYPE_CHECKING:
    from collections.abc import Callable, Iterable

    from sqlalchemy import Select
    from sqlalchemy.orm import InstrumentedAttribute, RelationshipProperty
    from sqlalchemy.orm.strategy_options import _AbstractLoad


[docs] class EagerStrategy(StrEnum): """Eager-loading strategy for an :class:`EagerLoad`. The three relationship loader strategies that make sense for a representation dump. Each member's value is the string token SQLAlchemy's ``relationship(lazy=...)`` accepts -- a precise subset of SQLAlchemy's (private) ``_LazyLoadArgumentType``. ``contains_eager`` is deliberately not a member: it isn't a per-field choice but is applied automatically by :func:`apply_eager_loads` when a sort already joined the relationship. """ SELECTIN = "selectin" """Separate ``WHERE ... IN`` query -- safe for collections.""" JOINED = "joined" """LEFT JOIN -- right for scalar to-one relationships.""" SUBQUERY = "subquery" """Separate query correlated by subquery."""
# Strategy -> the ``sqlalchemy.orm`` loader function that *starts* # a loader chain, e.g. ``selectinload(Task.project)``. _HEAD_LOADER: dict[EagerStrategy, Callable[..., _AbstractLoad]] = { EagerStrategy.SELECTIN: selectinload, EagerStrategy.JOINED: joinedload, EagerStrategy.SUBQUERY: subqueryload, } # Strategy -> the ``Load`` *method name* that extends an existing # loader to a nested relationship, e.g. # ``head_load.selectinload(Project.owner)``. A standalone function # can't chain -- only a method on the parent ``Load`` can. _CHAIN_METHOD: dict[EagerStrategy, str] = { EagerStrategy.SELECTIN: "selectinload", EagerStrategy.JOINED: "joinedload", EagerStrategy.SUBQUERY: "subqueryload", }
[docs] @dataclass(frozen=True) class EagerLoad: """One relationship to eager-load, with its strategy. Attributes: attr: The relationship attribute, e.g. ``Task.project``. strategy: The :class:`EagerStrategy` to load it with. Overridden by ``contains_eager`` for a top-level relationship a sort already joined. children: Nested eager loads one level deeper, e.g. a ``project``'s own ``owner``. Their strategy is always honoured -- ``contains_eager`` reuse only applies to a top-level relationship a sort actually joined. """ attr: InstrumentedAttribute strategy: EagerStrategy = EagerStrategy.SELECTIN children: tuple[EagerLoad, ...] = ()
[docs] def apply_eager_loads( stmt: Select, loads: Iterable[EagerLoad], joined: set[RelationshipProperty] | None = None, ) -> Select: """Apply eager-load options to a SELECT, reusing ordering's joins. Args: stmt: The SQLAlchemy SELECT to add loader options to. loads: The representation's :class:`EagerLoad` plan. joined: Relationships already LEFT-joined by :func:`fsh_lib.ordering.apply_ordering` (its second return value). A top-level load whose relationship is in this set is loaded with ``contains_eager`` rather than re-fetched. ``None`` means nothing was joined. Returns: The statement with ``.options(...)`` applied. """ joined = joined or set() for load in loads: stmt = stmt.options(_loader(load, joined, head=True)) return stmt
def _loader( load: EagerLoad, joined: set[RelationshipProperty], *, head: bool, ) -> _AbstractLoad: """Build the loader option for *load* and its nested children. A *head* relationship already joined for an ORDER BY is loaded with ``contains_eager`` -- it reuses that join's columns instead of issuing a second fetch. Otherwise its configured strategy applies. Children always use their own strategy. """ if head and load.attr.property in joined: option = contains_eager(load.attr) else: option = _HEAD_LOADER[load.strategy](load.attr) for child in load.children: option = _extend(option, child) return option def _extend(option: _AbstractLoad, child: EagerLoad) -> _AbstractLoad: """Chain a nested *child* eager load onto an existing loader.""" sub: _AbstractLoad = getattr(option, _CHAIN_METHOD[child.strategy])( child.attr ) for grandchild in child.children: sub = _extend(sub, grandchild) return sub