QueryPlan IR
siderite_orm::QueryPlan is the single backend-neutral representation that
every high-level query compiles into.
- Immutable builder. Each method takes and returns
self.clone()a base plan to branch safely. - Pure data. Building a plan never performs I/O.
- Capability-aware.
required_features()walks the whole tree, including subqueries, derived tables, and set-operation members.check(&caps)turns any mismatch into a typedBackendCapabilityError. Backends check before any I/O, and multi-statement terminals (paginate,get_or_create, the bulk operations) check first as well.
| Field | Meaning | Relational-only? |
|---|---|---|
source |
table, or a derived table (plan) AS alias |
no |
projection |
selected expressions (empty = all columns of the root source) | no |
joins |
joined sources | yes → Feature::Joins |
filter / having |
Expr predicates |
no |
grouping |
group keys | no (maps to $group) |
ordering, limit, offset |
sorting and paging | no |
distinct |
None / All / On(..) |
On → Feature::DistinctOn |
lock |
FOR UPDATE variants |
yes → Feature::RowLocking, LockModifiers |
compound |
UNION / UNION ALL / INTERSECT / EXCEPT members |
no |
origin |
database the plan was built against, set when a QuerySet becomes a subquery; never compiled |
no |
For a plan with compound members, ordering, limit, and offset apply
to the combined result. Every member must project the same columns, and a
member with its own ordering or limit is wrapped in a derived table.
Expressions
Section titled “Expressions”Expr node |
Meaning | Feature |
|---|---|---|
Column, Value |
column reference (Django F), bound literal |
|
Binary, Unary, And, Or |
comparison, arithmetic, boolean logic | |
Lookup |
iexact, contains, startswith, endswith, regex, in, in-subquery, range, isnull | Regex |
Func |
lower, upper, length, coalesce, concat, substr, replace, trim |
|
Cast |
CAST(x AS type) with dialect type names |
|
Case |
searched CASE WHEN .. THEN .. ELSE .. END |
|
DatePart |
year, month, day, week (ISO), quarter, hour, minute, second, date | |
Aggregate |
Count, Sum, Avg, Min, Max, StdDev, Variance, ArrayAgg, StringAgg, with DISTINCT and FILTER |
StatisticalAggregates, Arrays |
Window |
RowNumber, Rank, DenseRank, PercentRank, CumeDist, Ntile, Lag, Lead, FirstValue, LastValue, or an aggregate, with PARTITION BY / ORDER BY |
WindowFunctions |
Exists, Subquery |
EXISTS (..), scalar subquery |
|
OuterRef |
column of the enclosing query, inside a correlated subquery | |
Related |
column reached through foreign keys, resolved into joins | Joins |
Comparing with a NULL literal (Author::age.eq(None::<i32>)) compiles to
IS NULL / IS NOT NULL, as in Django’s filter(x=None).
Compiler rules
Section titled “Compiler rules”- Values are always bound, with one exception: a
NULLvalue is written as the keywordNULL. That keeps the statement text different from the same statement with a value, which matters because SQLx caches PostgreSQL prepared statements by text. Integers that PostgreSQL requires asinteger(LIMIT,OFFSET,NTILE(n),LAGoffsets) are written into SQL text;SUBSTRpositions are bound and cast. - When a plan has joins, unqualified columns are qualified with the root
source, and an empty projection becomes
"root".*. - Parameters are numbered across the whole statement, subqueries included.
UPDATEandDELETEtargets count as the enclosing query of their subqueries, soOuterRefworks in their filters and assignments. A subquery over the same table as its enclosing query must name its table withaliased(..); without it the compiler refuses the plan.- Structural problems (wrong function arity,
OuterRefoutside a subquery, an unresolvedRelatedcolumn) areQueryError::InvalidPlan; they never produce broken SQL.
From QuerySet to plan
Section titled “From QuerySet to plan”QuerySet builds the plan incrementally and keeps it normalised:
- Related traversal.
Book::author.join(Author::name)is aJoinedhandle whose lookups produceExpr::Related.QueryPlan::resolve_relationsturns each into aLEFT JOINwith a stable alias (author,author__team), shared between uses. - Annotations.
annotate("n", expr)names an expression. Laterfilter/order_by/projectcalls refer to it withExpr::col("n"), and the expression is inlined. A predicate containing an aggregate becomesHAVING. A filter on a window function is an error. - Grouping. When the projection contains aggregates, the other
projected expressions become
GROUP BY, and the model’s default ordering is dropped. count,exists,aggregate. Ordering and row locks are removed. A plan with limit, offset, distinct, grouping, or set operations is wrapped:SELECT COUNT(*) FROM (..) AS "counted".updateanddelete. A plain filter is used directly; a queryset with joins, a limit, or distinct becomesWHERE pk IN (SELECT pk ..).- Subquery origin.
QuerySet::subqueryandexists_exprstamp the plan withPlanOrigin.Db::fetchandDb::executerefuse a plan whose nested subquery belongs to another database before any SQL is sent.
Not modelled: window frames (the database default frame applies),
GROUP BY on expressions other than the projection, and set operations
whose members are themselves grouped with different names.