Source code for fsh_lib.ordering

"""ORDER BY application from typed Pydantic sort clauses."""

from typing import TYPE_CHECKING, cast

from fsh_lib._relationships import resolve_sort_target, sort_expr

if TYPE_CHECKING:
    from collections.abc import Sequence

    from pydantic import BaseModel
    from sqlalchemy import Select
    from sqlalchemy.orm import RelationshipProperty

    from fsh_lib._relationships import SortDirection

__all__ = ["apply_ordering"]


[docs] def apply_ordering( stmt: Select, sort_clauses: Sequence[BaseModel] | None, model: type, default_field: str, default_dir: SortDirection = "asc", ) -> tuple[Select, set[RelationshipProperty]]: """Apply one or more sort clauses to a SELECT statement. Each clause is a Pydantic model with ``field`` (an enum whose ``.value`` is the sort field) and ``dir`` (:data:`~fsh_lib._relationships.SortDirection`). A sort field naming a column sorts by that column. A field naming a (to-one) *relationship* -- ``"vendor"`` on a model with a ``vendor`` many-to-one -- is intuited as the related row's representative column (``vendor.name``, falling back to its primary key), with the related table LEFT-joined in so rows with a null relationship still appear. The sort field stays the bare relationship name end to end -- callers never see or send ``"vendor.name"``. When *sort_clauses* is ``None`` or empty, the default field and direction are used. Args: stmt: The SQLAlchemy SELECT statement to sort. sort_clauses: List of sort clause models, or ``None``. model: The SQLAlchemy model class providing columns. default_field: Field to sort by when no clauses are provided. default_dir: Direction for the default sort. Returns: ``(stmt, joined)`` -- the statement with ORDER BY applied, and the set of relationships LEFT-joined to resolve a relationship sort field. Hand the *joined* set to :func:`fsh_lib.loading.apply_eager_loads` so an eager load of one of those relationships reuses the join (``contains_eager``) instead of fetching it again. """ # Relationships already joined -- a second sort clause through # the same relationship must not join it twice. joined: set[RelationshipProperty] = set() def order_by_field( stmt: Select, field: str, direction: SortDirection, ) -> Select: col, join_attr = resolve_sort_target(model, field) if join_attr is not None: rel = cast("RelationshipProperty", join_attr.property) if rel not in joined: joined.add(rel) stmt = stmt.join(join_attr, isouter=True) return stmt.order_by(sort_expr(col, direction)) if not sort_clauses: return order_by_field(stmt, default_field, default_dir), joined for clause in sort_clauses: field_enum = getattr(clause, "field", None) if field_enum is None: continue direction: SortDirection = getattr(clause, "direction", "asc") stmt = order_by_field(stmt, field_enum.value, direction) return stmt, joined