A function returning QueryBuilder[T] defines an unevaluated read template.
The compiler splices it into each consumer and renders one prepared statement.
There is no runtime SQL string or runtime QueryBuilder object.
fn active_bookings(): QueryBuilder[Booking] {
return FROM Booking
WHERE active
ORDER BY created_at DESC
}
fn recent(): []Booking ! QueryError {
return active_bookings()
WHERE created_at >= cutoff
LIMIT 20?
}This is static composition. The producer is checked as a query template, each call is specialized at its use site, and only the final combined query reaches the runtime. It cannot be stored, returned as an opaque runtime value, or assembled from strings.
Producers
The producer query must be an unterminated FROM read whose row type agrees
with QueryBuilder[T]. Terminal LIMIT, FOR UPDATE, AS FRAME, ONE FROM,
and producer-side projections are rejected because they would decide a final
result shape before the consumer is known.
Predicates and stable ordering are useful producer responsibilities:
fn visible(): QueryBuilder[Booking] {
return FROM Booking
WHERE deleted_at == None
ORDER BY created_at DESC
}Keep a builder focused on one reusable policy such as tenancy, visibility, or base joins. A builder does not bypass model visibility or datasource routing.
Parameters
Parameterized builders are implemented:
fn priced(minimum: int, enabled: bool): QueryBuilder[Booking] {
return FROM Booking
WHERE price >= minimum if enabled
}
fn affordable(maximum: int): []Booking ! QueryError {
return priced(10, true)
WHERE price <= maximum?
}Arguments are substituted by symbol identity, so a same-named local at the consumer does not capture the producer parameter.
The arguments are ordinary Atoll expressions. Their values become bound statement parameters when the combined query executes; they are not rendered as SQL text. Each call site receives its own substitution, so two consumers can specialize the same builder differently without sharing mutable state.
Execution
A bare builder call followed by ? executes the template:
rows := active_bookings()?Adding clauses continues the query until a terminal or ? consumes it.
Composition can extend predicates, joins, ordering, grouping, paging, and
supported projections on the consumer side.
Clauses are merged structurally. Producer and consumer predicates both apply; consumer ordering and paging follow the same ordering rules as a query written in one place. The compiler validates the final plan after expansion, so an otherwise valid producer can still fail at a consumer whose added clauses make the complete plan ambiguous or unsupported.
fn by_customer(wanted_customer: int): QueryBuilder[Booking] {
return FROM Booking
WHERE customer_id == wanted_customer
}
fn recent_open(customer_id: int): []Booking ! QueryError {
return by_customer(customer_id)
WHERE status == Open
ORDER BY created_at DESC
LIMIT 10?
}Result typing
The builder’s T must match the model read by its producer. Consumption
returns the same shapes as a direct read. A matching checked struct context can
also trigger single-row aggregate collapse; see
Aggregates.
The builder declaration itself does not execute, suspend, or produce
QueryError. Those properties appear at the consuming expression, which has
the same Result and suspension behavior as a direct query.
Restrictions
Composition inherits every standalone query gate. Guarded joins, conditional assignments, unsupported engine functions, and query shapes without a runtime carrier still diagnose at compilation. A template cannot conceal an unsafe or unrenderable fragment.
Recursive builder calls and dynamic selection among builders are not a runtime
query-construction mechanism. When an entire query must differ by storage
engine, use a when block;
when only values differ, pass parameters.