Skip to content

popoto.backends.planning

popoto.backends.planning

Compile a public query call into a protocol :class:QueryPlan (#759 M1b).

Query.filter/count hand a backend a :class:QueryPlan. The Redis backend replays :attr:QueryPlan.source through its moved filter body, so for Redis the query layer sends source alone, exactly as in M1a. Every other backend compiles WHERE/ORDER BY from the plan's own terms, so for those the query layer calls :func:plan_from_call, which populates where, order_by, limit and project from the call -- and keeps source for reference.

It reproduces, from the model's own filter vocabulary (_meta.filter_query_params_by_field), what the Redis path decides:

  • validation, with the same QueryException text -- an unknown parameter or an unknown __operator (name__bogus) is Invalid filter parameters: …, never an exact match on a field of that name; a non-boolean __isnull; a malformed __between; a partitioned SortedField queried without its partition; a non-tuple values=; an order_by that is not a field, or is missing from values=.
  • ordering: explicit order_by > Meta.order_by > the first filtered SortedField's score order (ZRANGEBYSCORE order, which the Redis path uses when nothing else orders the result, and which survives as Meta.order_by's tie-break because Python's sort is stable). A descending primary term reverses the tie-breaks too, as list(reversed(...)) does.
  • plain fields: an unindexed field compared by equality, which Redis does client-side after hydration, is an ordinary Cond.

Pure -- no network, no backend driver -- with one exception: a chained Relationship lookup (person__name="Alice") runs the related model's own query to find the keys it matches, as Redis's filter_query does.

TRUE = And(()) module-attribute

Matches everything (an empty Q()).

validity_cond(field_name, param, suffix, value)

validity__as_of=t / validity__current=… (#759 M3), validated as ValidityField.filter_query validates them -- the same ValueError text -- and compiled to Cond(field, VALID_AT, (t, valid)): valid is False only for __current=False, the complement (members of either interval index not valid now).

Source code in src/popoto/backends/planning.py
def validity_cond(field_name: str, param: str, suffix: str, value: Any) -> Cond:
    """``validity__as_of=t`` / ``validity__current=…`` (#759 M3), validated
    as ``ValidityField.filter_query`` validates them -- the same
    ``ValueError`` text -- and compiled to ``Cond(field, VALID_AT, (t,
    valid))``: ``valid`` is ``False`` only for ``__current=False``, the
    complement (members of either interval index not valid now)."""
    import time

    if suffix == "current":
        if not isinstance(value, bool):
            raise ValueError(f"{param} filter must be True or False, got {value!r}")
        return Cond(field_name, Op.VALID_AT, (time.time(), value))
    try:
        t = float(value)
    except (TypeError, ValueError) as e:
        raise ValueError(
            f"{param} filter must be a number of epoch seconds, got {value!r}"
        ) from e
    return Cond(field_name, Op.VALID_AT, (t, True))

plan_from_call(call)

Validate call the way the query layer does and compile it.

Source code in src/popoto/backends/planning.py
def plan_from_call(call: QueryCall) -> QueryPlan:
    """Validate ``call`` the way the query layer does and compile it."""
    meta = call.query.model_class._meta
    params = _Params(meta)
    kwargs = dict(call.kwargs)
    parts: list[Predicate] = params.leaf(kwargs)
    parts.extend(_q_predicate(params, q) for q in (call.q_objects or ()))
    where: Optional[Predicate]
    if not parts:
        where = None
    elif len(parts) == 1:
        where = parts[0]
    else:
        where = And(tuple(parts))
    if call.kind == "count":
        return QueryPlan(where=where, source=call)

    values = kwargs.get("values")
    if values is not None and not isinstance(values, tuple):
        raise _query_exception(
            "values takes a tuple. eg. query.filter(values=('name',))"
        )
    order_by = kwargs.get("order_by") or meta.order_by or ""
    terms: list[OrderTerm] = []
    if order_by:
        descending = isinstance(order_by, str) and order_by.startswith("-")
        name = order_by[1:] if descending else order_by
        if not isinstance(name, str) or name not in meta.fields:
            raise _query_exception(f"order_by={name} must be a field name (str)")
        if values and name not in values:
            raise _query_exception(
                "field must be included in values=(fieldnames) in order to use "
                "order_by"
            )
        terms.append(OrderTerm(name, descending))
    if not kwargs.get("order_by") and not call.q_objects:
        # The sorted field's score order is the base order; Meta.order_by
        # re-sorts it stably, so it survives as the tie-break.
        sorted_name = params.sorted_default(kwargs)
        if sorted_name and not (terms and terms[0].field == sorted_name):
            terms.append(
                OrderTerm(sorted_name, terms[0].descending if terms else False)
            )
    limit = kwargs.get("limit")
    return QueryPlan(
        where=where,
        order_by=tuple(terms),
        limit=limit if isinstance(limit, int) and limit > 0 else None,
        project=tuple(values) if values else None,
        source=call,
    )

has_filters(call)

Whether call filters at all (anything beyond the result modifiers, or a Q object).

Source code in src/popoto/backends/planning.py
def has_filters(call: QueryCall) -> bool:
    """Whether ``call`` filters at all (anything beyond the result
    modifiers, or a ``Q`` object)."""
    return bool(call.q_objects) or any(k not in RESULT_MODIFIERS for k in call.kwargs)