"""Typed protocols for the store/searcher query contract.
These protocols define the backend-agnostic query interface shared by httk
data stores: the database layer in httk-data implements them over SQL, and
serving modules (such as *httk-optimade*, whose in-memory store also conforms)
program against them. They mirror the query interface of the httk v1 database
layer (``httk.db`` ``FilteredCollection`` searchers), so lightweight fakes can
stand in for a real store in tests.
"""
from collections.abc import Iterator
from typing import Any, NamedTuple, Protocol
__all__ = [
"SearchExpression",
"SearchField",
"SearchResult",
"SearchVariable",
"Searcher",
"Store",
]
[docs]
class SearchExpression(Protocol):
def __and__(self, other: "SearchExpression") -> "SearchExpression": ...
def __or__(self, other: "SearchExpression") -> "SearchExpression": ...
def __invert__(self) -> "SearchExpression": ...
[docs]
class SearchField(Protocol):
"""A queryable field of a search variable.
In addition to the methods below, fields support the rich comparison
operators (``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=``), returning
:class:`SearchExpression`. The handlers invoke those via
``getattr(field, '__eq__')(value)`` since the comparison dunders cannot be
typed as expression-returning.
The three string-matching methods take **literal** text: no wildcard or
pattern syntax whatsoever crosses this contract, so ``%`` and ``_`` (and
any other metacharacter) match themselves. A backend is therefore free to
implement them with SQL ``LIKE`` over an escaped pattern, with a regular
expression, or with a full-text index — the choice is invisible here.
"""
[docs]
def has_any(self, *values: Any) -> SearchExpression: ...
[docs]
def has_only(self, *values: Any) -> SearchExpression: ...
[docs]
def contains(self, text: str) -> SearchExpression:
"""Match values containing ``text`` as a literal substring."""
...
[docs]
def startswith(self, prefix: str) -> SearchExpression:
"""Match values beginning with the literal ``prefix``."""
...
[docs]
def endswith(self, suffix: str) -> SearchExpression:
"""Match values ending with the literal ``suffix``."""
...
def __getattr__(self, name: str) -> "SearchField":
"""The field ``name`` of the record this field refers to.
A field may hold a *reference* to another stored record, and attribute
access chains into that record's own fields — ``variable.ref.doi`` —
to any depth. Whether a given field is a reference is a property of the
stored class, not of this contract, so the chain is only resolved when
the query is built: a backend raises :class:`AttributeError` for a name
that is not a field of the referenced record, and for chaining off a
field that refers to nothing.
"""
...
[docs]
class SearchVariable(Protocol):
"""A query variable bound to a target type; attribute access yields fields.
``always_true``/``always_false`` are reserved names: they are real methods
of the variable, never stored fields resolved through ``__getattr__``.
They exist so a translation layer can express a constant truth value
without inventing a probe field. A ``field == field`` probe is NULL-unsound,
since it yields NULL (not true) for a NULL field.
"""
[docs]
def always_true(self) -> SearchExpression:
"""An expression that matches every row."""
...
[docs]
def always_false(self) -> SearchExpression:
"""An expression that matches no row."""
...
def __getattr__(self, name: str) -> SearchField: ...
[docs]
class SearchResult(NamedTuple):
"""One match: the declared output values, and the names they were declared under.
``values`` holds one entry per :meth:`Searcher.output` call in declaration
order; it is a tuple, so ``values, names = result`` and ``result[0][0]``
both work.
"""
[docs]
values: tuple[Any, ...]
[docs]
class Searcher(Protocol):
"""A single query under construction, and its results once iterated.
Iteration yields one :class:`SearchResult` per match, so ``item[0][0]`` is
the first declared output of the match (typically the matched row object).
The expressions received by ``add`` are always ones produced by this same
backend's search variables, so implementations may type them as their own
expression class; a backend that needs a second (post-filter) evaluation
position decides that from the expression itself, not from the caller.
"""
[docs]
def variable(self, target: Any) -> Any: ...
[docs]
def output(self, variable: Any, name: str) -> None: ...
[docs]
def add(self, expression: Any) -> None: ...
[docs]
def count(self) -> int: ...
[docs]
def set_limit(self, limit: int) -> None: ...
[docs]
def add_offset(self, offset: int) -> None: ...
[docs]
def add_sort(self, field: Any, descending: bool) -> None: ...
def __iter__(self) -> Iterator[SearchResult]: ...
[docs]
class Store(Protocol):
[docs]
def searcher(self) -> Searcher: ...