PostgreSQL: functions¶
Functions only PostgreSQL runs, from hare.dialects.postgresql.functions; they go into annotate(),
filter() and order_by() like the database functions of every database.
Full-text search is hare.search.
from hare.dialects.postgresql.functions import StringAgg, Subpath, TransactionNow
from hare.query.functions import ArrayAgg, ArrayItem
The functions¶
| Function | SQL |
|---|---|
STDistance(field, point) |
ST_DISTANCE(...) |
STDWithin(field, point, radius_m) |
ST_DWITHIN(...) — radius_m is a finite number of zero or more or an expression, else ValidationError |
ArrayAgg(field, distinct=False, _filter=None, order_by=()) |
ARRAY_AGG(...) — each element decoded through the field; hare's own aggregate of hare.query.functions, groupArray on ClickHouse |
StringAgg(field, delimiter, distinct=False, _filter=None, order_by=()) |
STRING_AGG(CAST(field AS TEXT), ...) — delimiter is text or an expression; another value raises QueryError |
JSONBAgg(field, distinct=False, _filter=None, order_by=()) |
JSONB_AGG(...) — each element decoded through the field |
BoolAnd(field) / BoolOr(field) |
BOOL_AND/BOOL_OR |
BitAnd(field) / BitOr(field) / BitXor(field) |
BIT_AND/BIT_OR/BIT_XOR of an integer column |
RangeAgg(field) |
RANGE_AGG(...) — the ranges of each group merged into a multirange, read through the multirange field of the range field (Multirange fields) |
Corr(y, x) |
CORR(y, x) — the correlation coefficient |
CovarPop(y, x, sample=False) |
COVAR_POP, or COVAR_SAMP with sample=True |
RegrAvgX(y, x) / RegrAvgY(y, x) |
REGR_AVGX/REGR_AVGY — the average of x/y |
RegrCount(y, x) |
REGR_COUNT — the rows where both are non-NULL (an integer) |
RegrIntercept(y, x) / RegrSlope(y, x) / RegrR2(y, x) |
the least-squares line's intercept, slope and R² |
RegrSXX(y, x) / RegrSXY(y, x) / RegrSYY(y, x) |
REGR_SXX/REGR_SXY/REGR_SYY |
TransactionNow() |
CURRENT_TIMESTAMP — the start of the transaction, the same for all its statements |
RandomUUID() |
gen_random_uuid() — a random version 4 UUID per row |
ArraySubquery(queryset, *, shares_outer_scope=False) |
ARRAY(SELECT ...) — the one column a values()/values_list() queryset selects, of every row, as a list in the queryset's order_by() ([] for no row); each element decoded through the column's field; shares_outer_scope as for Subquery |
Subpath(field, offset, length=None) |
subpath(...) — a part of an LtreeField path, read as a path (LtreeField) |
ArrayItem(field, index) |
field[index + 1] (0-indexed) — decoded through the array's base_field; on a nested array, a whole row ([[1, 2], [3, 4]] → [1, 2]) |
await Store.objects.annotate(distance_m=STDistance("location", (55.75, 37.62))).order_by("distance_m")
await Book.objects.all().annotate(tag_names=ArrayAgg("tags__name")).group_by("id")
await Book.objects.annotate(first_keyword=ArrayItem("keywords", 0)).values("first_keyword")
await Book.objects.annotate(
tag_names=ArraySubquery(Tag.objects.filter(books=OuterReference("pk")).order_by("name").values("name"))
)
ArraySubquery¶
ArraySubquery takes a queryset selecting exactly one column — a model queryset or one selecting several
raises QueryError. Its value is an ArrayField of the column's field, so the ArrayField lookups
filter it. On another dialect it raises UnSupportedError before the query is sent.
Date functions¶
TruncYear/TruncMonth/TruncDay and ExtractYear/ExtractMonth/ExtractDay are the date functions
of hare.query.functions; they work on every database — see
Date functions.
Aggregating into arrays and JSON¶
ArrayAgg/JSONBAgg decode each element through the aggregated field (a JSONField element is parsed,
of its declared field_type when it has one; a DatetimeField element is in hare's time zone, a
TimeDeltaField element is a timedelta). A non-finite float, which jsonb holds as its text ("NaN",
"Infinity"), is a float again in JSONBAgg of a FloatField and of an array of floats.
JSONBAgg of a DecimalField gives a Decimal quantized to the field's decimal_places — JSON itself
carries the number with float precision, so use ArrayAgg when exact digits matter. JSONBAgg of a
range field gives Range values (an empty range, an unbounded side and inclusive/exclusive bounds
included). JSONBAgg of an IntervalField or a BinaryField raises FieldError — JSON holds no such
value, only text of the server's IntervalStyle/bytea_output; ArrayAgg gives them exactly.
The refusal applies before SQL to arrays of these fields at any depth and to generated fields with
these output types as well.
ArrayAgg of an ArrayField builds one multi-dimensional array, whose rows PostgreSQL
requires of one length: arrays of different lengths, or an empty one among others, raise
OperationalError (cannot accumulate arrays of different dimensionality). JSONBAgg aggregates them
as a list of lists — [['a', 'b'], [], ['c']] — as ClickHouse's ArrayAgg does.
_filter=Q(...) leaves a row that fails it out of the aggregate (FILTER (WHERE ...)) — ArrayAgg/
JSONBAgg collect only the matching rows, and a group with none gives None.
An ArrayAgg annotation is filtered with the ArrayField lookups — contains, contained_by,
overlap, len, equality — and a JSONBAgg one with the JSONField lookups:
await Author.objects.annotate(titles=ArrayAgg("books__title")).filter(titles__overlap=["War and Peace"])
await Author.objects.annotate(titles=ArrayAgg("books__title")).filter(titles__len=2)
Such a lookup on an annotation whose value isn't an array/range/JSON value raises FieldError.
order_by= orders the rows ArrayAgg/StringAgg/JSONBAgg read — a field name ("-name" for
descending), F("name"), an expression (Lower("name"), Coalesce("rank", Value(0)).desc()) or
F("name").desc(nulls_last=True), or a list of them, also across a relation. Any other aggregate raises
QueryError for it — its result doesn't depend on the order. StringAgg(..., distinct=True) ordered by
the field it joins orders a text field's values by the text they are joined as; a non-text field there
raises QueryError — PostgreSQL orders a DISTINCT aggregate only by its argument, the field's text,
which would put 10 before 9.
await Author.objects.annotate(titles=ArrayAgg("books__title", order_by="-books__published")).group_by("id")
await Tag.objects.all().aggregate(names=StringAgg("name", ", ", order_by=["name", "id"]))
Two-column statistics¶
The two-column statistics take the dependent value first (y), as PostgreSQL does, and leave out a row
where either is NULL; they are floats, RegrCount an integer.