Query DSL
Forze uses one expression language for filtering, sorting, and
aggregating, shared by document query ports (find, find_many, count, …)
and search requests. Learn it once; it applies everywhere — including
authorization scope filters.
Filter expressions¶
A filter expression is exactly one of these shapes. Combinators
($and/$or/$not) cannot share a dict with constraints ($values/$fields) —
that raises at parse time.
| Form | Shape |
|---|---|
| Literal constraints | {"$values": {…}} |
| Field-to-field constraints | {"$fields": {…}} |
| Combined (implicit AND) | {"$values": {…}, "$fields": {…}} |
| Conjunction | {"$and": [expr, …]} |
| Disjunction | {"$or": [expr, …]} |
| Negation | {"$not": expr} (a single child object, not a list) |
Field constraints — $values¶
Inside $values, each field maps to a literal shortcut or an explicit operator
map:
| Shortcut | Expands to | Meaning |
|---|---|---|
"active" |
{"$eq": "active"} |
equality |
["a", "b"] |
{"$in": ["a", "b"]} |
membership |
None |
{"$null": true} |
is null |
filters = {
"$values": {
"status": "active", # → $eq
"tags": ["backend", "api"], # → $in
"deleted_at": None, # → $null
}
}
Operators¶
| Group | Operators | Operand |
|---|---|---|
| Comparison | $eq $neq $gt $gte $lt $lte |
a scalar |
| Membership | $in $nin |
a list — value is / isn't in it |
| Text | $like $ilike $regex |
a pattern (or a list of patterns → OR) |
| Null | $null |
true: is null · false: is not null |
| Empty | $empty |
true: empty array · false: non-empty |
| Set relations | $superset $subset $overlaps $disjoint |
a list (see Array fields) |
| Quantifiers | $any $all $none |
an element predicate (see Array fields) |
| Hierarchy | $descendant_of $ancestor_of |
a path (or a list of paths → OR) on a TreePath-typed field — inclusive, materialized-path semantics; backend-specific and capability-gated |
$empty tests array length (not string emptiness). $like/$ilike use
%/_ wildcards (\ escapes); a list of patterns becomes an OR on that field.
Combining operators on one field¶
Multiple operators on the same field are ANDed:
{"$values": {"score": {"$gte": 1, "$lt": 10}}} # score >= 1 AND score < 10
Two operators stand alone — {"$null": true} and {"$empty": true} can't be
combined with anything else on that field (their false forms can). A
quantifier is also exclusive with other operators on its
field.
Nested fields¶
A field key is a dot-separated path into a nested/embedded object — usable in
$values, $fields, sorts, $groups, aggregate fields, and the fields list of
project* calls (a dotted projection returns a nested shape, and a path crossing a
list maps over each element). Depth is unbounded;
each segment walks one level deeper:
{"$values": {
"address.city": "Berlin",
"address.geo.lat": {"$gte": 52.0},
}}
The root segment must be a real column / read-model field; deeper segments traverse a JSON/JSONB column. A few rules to know (Postgres):
- When the leaf type can't be inferred statically (a dynamic mapping, an
Any), declare it via the adapter'snested_field_hints={"address.geo.lat": float}. - Set operators (
$superset/$subset/$overlaps/$disjoint/$empty) are not supported on nested JSON paths — use a top-level array column for those.
Array fields¶
Two distinct ways to query an array column.
Set relations¶
The whole array, compared against a list:
| Operator | Matches when the field… |
|---|---|
$superset |
contains all the listed values |
$subset |
has no values outside the list |
$overlaps |
intersects the list |
$disjoint |
does not intersect the list |
{"$values": {"roles": {"$superset": ["admin", "ops"]}}} # has both roles
Element quantifiers¶
$any / $all / $none apply a predicate to individual elements. $all and
$none are vacuously true on a missing, null, or empty array. A quantifier holds
exactly one of three operand forms:
# 1. scalar shortcut → element equality
{"$values": {"tags": {"$any": "urgent"}}}
# 2. a single element operator (only $eq $neq $gt $gte $lt $lte $like $ilike $regex)
{"$values": {"scores": {"$all": {"$gte": 1}}}}
# 3. $values map for an array of objects — fields are element-relative
{"$values": {"line_items": {"$any": {"$values": {
"product_id": "p-42",
"quantity": {"$gt": 0},
}}}}}
Quantifiers do not nest ($any inside $any is rejected), and inside an
object-array $values the null / list / quantifier shortcuts aren't allowed —
only literal operators (which still AND together per field).
Comparing fields — $fields¶
Compare one field to another, not to a literal. A bare string value is a field path, and only the equality/ordering operators apply (no membership, text, or set operators here):
filters = {
"$values": {"is_deleted": False},
"$fields": {"starts_at": {"$lte": "ends_at"}},
}
Combining expressions — $and / $or / $not¶
{"$and": [
{"$values": {"status": ["active", "trial"]}}, # $in shortcut
{"$or": [
{"$values": {"region": "eu"}},
{"$not": {"$values": {"deleted_at": {"$null": False}}}},
]},
{"$fields": {"updated_at": {"$gt": "created_at"}}},
]}
$and/$or take a list of expressions; $not takes a single expression.
Nesting depth is capped (see Limits).
Sorting¶
A map of field → direction ("asc" / "desc"); keys may be nested paths, and
map order is sort priority:
sorts = {"created_at": "desc", "id": "asc"}
A key may instead be an object to control null placement —
{"dir": "asc", "nulls": "first"} ("first" / "last"). Omitted, nulls sort as
the smallest value (asc → first, desc → last); some backends (Mongo, Firestore)
support only that default and reject an explicit override. For
cursor pagination directions may be mixed — each
key seeks in its own direction — and an id tie-breaker is appended automatically.
Cursor tokens can be HMAC-signed (<payload>.<hmac>) or AEAD-encrypted (a leading
~) — see Cursor tokens.
Aggregates¶
An aggregate expression groups and computes over matched rows. Group keys go in
$groups (alias → source path, or a list of paths); outputs go in $computed
(alias → function). Functions: $count (use None for row counts),
$count_distinct, $sum, $avg, $min, $max, $median, $percentile
(requires the application form with a p quantile), $stddev_pop, $stddev_samp,
$var_pop, $var_samp. $median / $percentile are exact on Postgres and the
mock but approximate on Mongo.
aggregates = {
"$groups": {"category": "category"},
"$computed": {
"products": {"$count": None},
"revenue": {"$sum": "price"},
"premium_revenue": {
"$sum": {"field": "price", "filter": {"$values": {"price": {"$gte": 20}}}},
},
},
}
A computed metric's filter is a per-metric row pre-filter (it narrows the
rows that feed that aggregate). For a post-aggregate stage, add $having — a
filter (same grammar) over the aggregated rows, referencing only the output
aliases (group keys and computed metrics). $count takes no field; every other
function requires one.
Calendar bucketing uses $trunc as a group value — unit is one of hour /
day / week (Monday-start) / month; timezone is an IANA name or fixed
offset (default UTC):
"$groups": {"day_start": {"$trunc": {"field": "ts", "unit": "day", "timezone": "+3"}}}
Where you pass them¶
Document query ports take filters, sorts, pagination, and (where supported)
aggregates:
doc = ctx.document.query(project_spec)
page = await doc.find_many(
filters=filters,
sorts=sorts,
pagination={"limit": 20, "offset": 0},
)
rows = page.hits
Search requests take the same filter and sort expressions alongside the query text:
page = await ctx.search.query(project_search_spec).search(
"roadmap",
filters=filters,
pagination={"limit": 20, "offset": 0},
)
Limits¶
Filters are validated at parse time, before any query reaches the database.
Defaults (override per gateway via filter_limits):
| Limit | Default | Applies to |
|---|---|---|
max_depth |
32 | nesting of $and / $or / $not |
max_clauses |
256 | combinator children, $values / $fields keys, per-field operators |
max_in_size |
1000 | $in / $nin, array shortcuts, set-relation operands |
max_pattern_length |
256 | each $like / $ilike / $regex pattern |
max_pattern_or_branches |
32 | patterns when a text operand is a sequence |
A violation — or an empty operator map, an unknown operator, a type mismatch, or a
regex with unsafe nesting/repetition — raises a precondition CoreException
(HTTP 400; the caller supplied a bad query) before the query runs.
Backend notes¶
Semantics are shared; rendering is backend-specific. Text-pattern support varies:
| Operator | Postgres | MongoDB | Firestore |
|---|---|---|---|
$like |
LIKE |
$regex |
not supported |
$ilike |
ILIKE |
$regex + i |
not supported |
$regex |
~ |
$regex |
not supported |
Leading-% patterns may need a trigram index (Postgres pg_trgm) to stay fast on
large tables. On MongoDB, $null: true matches both explicit null and missing
fields; object-array quantifiers render as $elemMatch.