Specification¶
The specification module implements the Specification pattern with multiple query backends.
Semantics¶
A specification has two readers that must agree: EvaluateVisitor, which
decides in memory, and the database reading the query PostgresqlVisitor
compiles the same tree to. So the evaluator’s semantics are PostgreSQL’s, and
infrastructure/tests/integration/test_postgresql_agreement.py holds the
two against each other on a live database: the same value or the same kind
of failure for each constant expression, the same rows selected for each
specification over rows with nulls, embedded and relational.
Nulls. A comparison or a computation with a null operand is null.
AND,OR,NOTare three-valued:NULL AND FALSEis false,NULL OR TRUEis true,NOT NULLis null.IS,IS NULL,IS NOT NULLand a wildcard are true or false, never null. A candidate satisfies a specification whose value is true; a null does not, as a row with a null condition is not selected.ANDandORdo not evaluate their right operand once the left has decided, and a wildcard stops at its first witness.Arithmetic of
intandfloat(specification.domain.arithmetic): integer division truncates towards zero, the remainder has the sign of the dividend, a result outsidebigintis anOverflowErrorand so is a float overflow, the count of a shift is taken modulo 64;boolandstrhave no arithmetic. Any other type - a Value Object,Decimal,datetime- computes as it defines.Comparisons of
bool, numbers andstr(specification.domain.comparison): values of different kinds do not compare -"a" == 1andTrue == 1are aTypeError, as they are “operator does not exist” in the database;NaNequals itself and is greater than any other number; false comes before true. Any other type compares as it defines. One exception, the server’s own: a string constant beside adatetime, adateor aUUIDis read as one (specification.domain.reading), as the server reads an untyped parameter by the column.A null is tested, not compared. In the tree
x = NULLis null and true of nothing. The notations in which null is a value like any other mean IS NULL by it, and are parsed so:@.a == nullof a template, spelled out or bound to a placeholder, andu.a is None/u.a == Noneof a lambda becomeIsNull;!=andis notbecomeIsNotNull.Names that go into a query are letters, digits and
_, not starting with a digit - of fields, collections, tables, columns and aliases alike; anything else is aValueError, not quoted. Only values are parameters.A collection is named in a schema by its whole path,
"Categories.Items"; a relational collection is joined to what its path starts at - the enclosing item, or the root row.A template is a function of its parameters, and a specification has no word for a placeholder.
parse()keeps what it parsed as the function that builds the tree once the parameters are there;bind(params)calls it and returns the specification, with aValuewhere a placeholder stood, to evaluate, to transform or to compile to SQL;match()isbind()and the evaluator. So there is no tree with a placeholder in it for a reader to be handed by mistake, and aVisitorhas no method for one. A placeholder used to be aValuewhose value was a marker tuple, which went into a query as a parameter.A composite is not a node.
CompositeExpressionis what a transform context may return instead of a node,Mapped, for a field or a value that is several columns;=and!=of two composites become nodes, part by part.transform(context, expression)returns a node: a composite left over - under any other operator, beside a node, as a predicate, as the whole specification - is itsValueError. It used to be aVisitablewhoseacceptraised from inside whatever visited the tree next.A mapping is an interface to inherit,
IMapping, of two abstract methods:attr_node(path)for the members,value_node(val)for the values. A mapping is of the aggregate’s members and knows nothing of any query: it is asked about a member by its whole path from the candidate,["categories", "products", "price"], a collection being a member like any other, and answers a path from the candidate’s row. Where the answer goes - from which item, how far out - is the tree’s, and the transformer puts it there: a member of an item is the answer less the collection’s, from the item. It used to ask about the members of “the item” by their names alone,item_attr_node, so a mapping could not tell the items of one collection from another’s, and about where a collection is kept,collection_node, which is the schema’s to say.A schema is the foreign keys of the storage, as
\dshows them, and nothing of any aggregate or query:foreign_key(table, column, referenced_table, referenced_column), a composite key byforeign_key_composite, a key named as PostgreSQL names it -store_items_store_id_fkey- or byconstraint_name. A tree names a collection by its table, and where two keys of that table reference the row it is named from, by the key’s name; an object kept in a table of its own by the key’s column,owner_id; a row of an array, which has no table, by the array’s column,"stores.items". What the schema does not mention is an array or a composite in the row. It used to key a collection by its path in the aggregate, then by a name of the query’s, and carry an alias for the subquery, which is the compiler’s to make.
Lambda Filter¶
See Lambda Filter Parser for Specification Pattern for the Lambda Filter parser documentation.
Native JSONPath¶
See Native JSONPath Parser (No External Dependencies) for the Native JSONPath parser documentation.
One Parser¶
The template language has one parser, the native one. There were two more,
which converted the tree of a library - jsonpath-rfc9535 and jsonpath2 - and
with them the dependencies on both. They were removed: every change of the
language had to be made three times; they had defects of their own (the path
to a collection lost, placeholders after a nested collection misbound, &&
and || read left to right); and one defect was of their construction - a
placeholder was a magic value put into the text before the library read it,
so a template with that value for a literal was refused or, worse, answered
wrongly. Their tests of the language pass against the native parser and are
kept: test_jsonpath_parser_rfc9535.py, test_jsonpath_parser_edge_cases.py.
See the API Reference for auto-generated API documentation.