Describing filters and orderings¶
Model._meta.get_lookup_info(key) tells what a .filter() key refers to — the relations it crosses, the
field it compares, the lookup and the value the lookup takes — without building or running a query.
get_ordering_info(name) does the same for an .order_by() name, and get_lookups(path, dialect) lists
every lookup of a field that a dialect runs.
It is meant for code that turns outside input into filters: an API layer checking the filters it declares at startup and parsing each query parameter into the right type, a form builder, a table browser. Such code doesn't keep its own copy of hare's lookup lists — it asks the model.
hare uses the same descriptions itself: .filter(), .exclude() and .order_by() check every key
through them when called, and a query checks each lookup against its connection's dialect before building
SQL.
get_lookup_info()¶
from hare.query.enums import Lookup, LookupValueShape
info = Book._meta.get_lookup_info("author__name__icontains")
info.relations # (Book.author,)
info.field # Author.name
info.lookup # Lookup.ICONTAINS
info.value_shape # LookupValueShape.VALUE
info.value_type # str
It accepts every key .filter() accepts:
| Key | relations |
field |
lookup |
value_shape |
value_type |
|---|---|---|---|---|---|
title, title__icontains |
() |
title |
EXACT, ICONTAINS |
VALUE |
str |
id__in |
() |
id |
IN |
LIST |
int |
id__range |
() |
id |
RANGE |
RANGE |
int |
editor_id__isnull |
() |
editor_id |
ISNULL |
VALUE |
bool |
author, author__in (a key or an object) |
(author,) |
Author.id |
EXACT, IN |
VALUE, LIST |
int |
author_id__gte |
() |
author_id |
GTE |
VALUE |
int |
author__pk, author__id__in |
(author,) |
Author.id |
EXACT, IN |
VALUE, LIST |
int |
tags__in (many-to-many) |
(tags,) |
Tag.id |
IN |
LIST |
int |
books__isnull of Author (reverse FK) |
(books,) |
Book.id (Book's primary key) |
ISNULL |
VALUE |
bool |
pk of a composite primary key (Draft) |
() |
(id, version) |
EXACT |
VALUE |
(UUID, int) |
pk__in of a composite primary key |
() |
(id, version) |
IN |
LIST |
(UUID, int) |
draft — a FK to a composite key |
(draft,) |
(Draft.id, Draft.version) |
EXACT |
VALUE |
(UUID, int) |
added_at__year__gte |
() |
added_at |
GTE |
VALUE |
int |
added_at__date |
() |
added_at |
EXACT |
VALUE |
date |
details__publisher__name__icontains (JSON) |
() |
details |
ICONTAINS |
VALUE |
str |
details__has_keys |
() |
details |
HAS_KEYS |
LIST |
str |
keywords__contains (array) |
() |
keywords |
CONTAINS |
LIST |
the element type |
keywords__len__gt |
() |
keywords |
GT |
VALUE |
int |
lending__overlap (range) |
() |
lending |
OVERLAP |
RANGE |
the bound type |
| a generated field | () |
the GeneratedField |
its output_field's type |
||
title__<custom> |
() |
title |
the lookup's name | as registered | as registered |
A lookup on a relation itself (author=, author__in=, tags__isnull=) compares the related model's
key, so field is that key field — the relation's target field(s) for a forward FK/O2O, the related
primary key for a reverse relation or a many-to-many one — and relations ends with the relation.
LookupInfo¶
An immutable dataclass. Descriptions are built once and cached:
- a model's own filter keys, ordering names (
OrderingInfo) andget_lookups()results — per model, andget_lookups()per path and dialect; - a key or name starting with an annotation per queryset, together with the annotations' output fields —
a clone shares them, and
annotate()/alias()replacing an annotation drops them; - a change to any model's fields, lookups or relations (a live model registered or unregistered, a field added or removed) drops every cached description.
A key or name that fails raises its FieldError again on every call — errors aren't cached.
| Attribute | Meaning |
|---|---|
key |
The filter key. |
model |
The model the key starts at. |
relations |
The relations the key crosses, in order — forward FK/O2O, reverse FK/O2O and many-to-many fields. |
field |
The field the value is compared with; a tuple of fields for a composite key; None for an annotation whose type is only known once the query runs. |
transforms |
The path read inside the field's value before the lookup: a date part (("year",)), date/time, a JSON key path (("publisher", "name")), an array index, slice or len, a range bound or flag, unaccent, an hstore key. |
lookup |
A Lookup member, or the name of a lookup registered with register_lookup(). |
value_shape |
LookupValueShape.VALUE (one value), LIST (a list: in, not_in, has_keys, an array's contains...) or RANGE (two items: range, a range field's overlap...). |
value_type |
The type of the value — of each item of a list or range. A tuple of types for a composite key; bool for isnull/not_isnull; int for a date part or len; str for a text lookup or a JSON/hstore key; object for any JSON value. |
crosses_to_many |
Whether a relation the key crosses holds many rows for one row (a reverse FK or a many-to-many relation) — the filter joins it, and the query can return a row more than once. |
requires_extension |
The database extension the lookup needs ("pg_trgm" for the trigram lookups, "unaccent"), else None. |
dialects |
The names of the dialects the field and the lookup exist on (frozenset({"postgresql"}) for an array field), None for every dialect — or for a field whose column type each dialect gives (GeometryField, VectorField), which is_supported() checks against the dialect asked about. |
is_supported(dialect) |
Whether a query on dialect runs the lookup — see below. |
Errors¶
- A path naming no field, relation or annotation raises
FieldError:Unknown filter param 'author__nme': Author has no field 'nme'. - A lookup the field doesn't have raises
FieldError:Book.title has no lookup 'foo'; so does a lookup outside the field'ssupported_lookups(an encrypted field). pkof a composite primary key takes onlypk=,pk__in=,pk__not=andpk__not_in=; anything else is aFieldError.- A lookup other than equality, membership or
isnullon a forward relation to a composite key (draft__gt=) raisesQueryError— it would be ambiguous; filter each key column instead. - A model not bound yet (see Before the connections are set up)
describes nothing:
get_lookup_info(),get_lookups()andget_ordering_info(), onModel._metaand on a queryset alike, raiseConfigurationError:Book is not bound yet: call Hare.bind_models() or Hare.init() before describing its filters or orderings.
Dialect support¶
dialect.filter_operators.supports_lookup(info), or info.is_supported(dialect), tells whether a query
on that dialect runs the lookup. It is not supported when:
- the field doesn't exist on the dialect (a field with
COLUMN_TYPE_FROM_DIALECTthe dialect gives no column type, e.g. an array or range field off PostgreSQL); - the lookup's operator is one only dialects implement (
search, the trigram lookups, the array, range and JSON container lookups, a lookup of one database) and this dialect doesn't implement it; - the lookup needs an extension and the dialect has no extensions.
from hare.dialects.dialect_registry import DialectRegistry
sqlite = DialectRegistry.get_dialect("sqlite")
info = Book._meta.get_lookup_info("title__search")
info.is_supported(sqlite) # False
info.is_supported(DialectRegistry.get_dialect("postgresql")) # True
A query running a lookup its connection's dialect doesn't support raises UnSupportedError before its
SQL is built:
Book.objects.filter(title__search=...) can't run on sqlite: the sqlite dialect doesn't implement the __search lookup
get_lookups()¶
lookups = Book._meta.get_lookups("added_at", connection.dialect)
lookups[""] # plain equality
lookups["year__gte"] # LookupInfo(value_type=int, ...)
path is a field or relation, after any relations ("author__name", "tags", "pk"). The result maps
each lookup's suffix after path — "" for plain equality, "icontains", "year__gte" — to its
description, and holds only the lookups the dialect supports. A relation itself has the lookups of its
key ("", in, not, not_in, isnull, not_isnull); a forward relation with a single key column
has the lookups of that column (author those of author_id, the dialect's alike); pk of a composite
primary key has "" and in.
A lookup can also need a feature of the connection the query runs on, beyond its dialect:
posix_regex/iposix_regex need features.supports_posix_regex — on SQLite a connection whose database
URL has ?install_regexp_functions=true. get_lookups() describes the dialect and lists them; a query
using one on a connection without the feature raises UnSupportedError before it runs.
get_ordering_info()¶
info = Book._meta.get_ordering_info("-author__name")
info.relations # (Book.author,)
info.fields # (Author.name,)
info.paths # ("author__name",)
info.descending # True
OrderingInfo has name, model, relations, fields (the fields ordered by), paths (the names the
query orders by), transforms (a path inside a JSON, array or range value, or a date part:
get_ordering_info("added_at__year").transforms == ("year",)), descending and crosses_to_many. A
forward FK/O2O orders by its own key column(s) with no join ("author" → paths ("author_id",)); the
model's own pk by its key field(s) — every one of a composite primary key, in key order
(("id", "version")); a reverse or many-to-many relation ("tags") and a related pk ("author__pk")
by the related primary key field(s), read through the relation as named. An unknown name raises
FieldError: Unknown field nme for ordering: Book has no field 'nme'.
On a queryset¶
QuerySet.get_lookup_info(), get_lookups() and get_ordering_info() also take the queryset's
annotations: a key may start with one, and it is described by the type of the annotation's value
(Count() is an integer).
queryset = Author.objects.annotate(book_count=Count("books"))
queryset.get_lookup_info("book_count__gte").value_type # int
Before the connections are set up¶
Every description needs the models only, no connection. Hare.bind_models() binds the models of a whole
configuration — the relations between them, swappable models, each model's filters and orderings —
synchronously, and leaves no HareContext current:
from hare import Hare
Hare.bind_models(config=CONFIG) # the same config Hare.init() takes
Book._meta.get_lookup_info("author__name__icontains").value_type # str
Book.objects.all().get_ordering_info("-published_at").descending # True
It takes the configuration in every form Hare.init() does (a dict, a HareConfig, a file path or
"module.VARIABLE") and describes every key a full Hare.init() describes. A framework that builds its
request handlers' signatures and schema when the application is created, before its lifespan opens the
connections, reads the filters from there. Hare.init() — or HareContext.init() in any context, with a
global fallback context as well — sets the connections up later as usual.
Model._meta.is_bound tells whether a model is bound — False right after its class is defined, True
once any of these calls binds it:
Until then every description raises ConfigurationError (see Errors). A queryset made before
binding (BOOKS = Book.objects.all() at module level) describes its keys once the model is bound.
Validation in .filter() and .order_by()¶
.filter(), .exclude() and .order_by() check every key when called — the whole path through
relations, the field and the lookup, keys inside Q objects included:
Book.objects.filter(author__nme="Le Guin")
# FieldError: Book.objects.filter(author__nme=...): Author has no field 'nme'
A queryset built before Hare.init() is checked when init() replays it, and init() reports every
invalid one with the place it was built.