Query reference
A DQL query is a document, not a string to parse. That means it can be built, stored, diffed and validated before anything reaches the database.
Classic mode01
The declarative shape: a dataset, a predicate, an ordering and a limit.
from:
dataset: spaces
where:
field: parent_id
op: "=="
value: "$parentId"
orderBy:
- field: sort_order
dir: ascA value beginning with $ is a placeholder the host binds at execution, so one
stored document serves many callers.
Pipe mode02
The same query as a stream of operators applied in order. Pipe mode exists because some questions are a sequence, and forcing them into a single clause tree makes them unreadable.
from:
dataset: events
pipe:
- op: filter
where: { field: status, op: "==", value: "open" }
- op: aggregate
groupBy: [assignee]
aggregate: [{ fn: count, as: total }]
- op: sortLimit
orderBy: [{ field: total, dir: desc }]
limit: 10The operator library covers reshaping, textual operations, quality checks, time handling and set operations.
Textual syntax03
Pipe mode also has a textual surface, which is what a .dql file contains and what
the editor tooling reads.
source events | where x == 1 | limit 10source events
| filter (level == "ERROR")
| aggregate count(id) as total
| limit 10Comments use --, and the operator names match their document-mode
counterparts.
-- the same query, one operator per line
source events
| compute host = split(url, "/")
| rename host as originThe document form is what a program builds and stores. The textual form is what a person types into an editor. Both go through the same parser and produce the same plan.
Planning04
The planner decides which clauses the database can answer and emits SQL for those. Everything else, including computed columns and expression filters, is completed by the processor over the rows that come back.
Splitting work between the database and memory means the planner is responsible for what pushes down. When it is wrong the query still answers correctly, just more slowly than it should.
Partition scoping05
Multi-tenant callers need every query confined to a tenant, and getting that wrong is a data leak rather than a bug. DQL does not guess what partitions your data. You declare it, and the planner and generator apply it to base tables and joins.
sc := scope.Scope{
{Name: "tenant_id", Value: tenantID, Required: true, ScopeJoins: true},
{Name: "project_id", Value: projectID},
}Requiredemits the predicate even when a table does not declare the column.ScopeJoinsalso scopes joined tables, in theONclause instead of theWHERE, so an out-of-scope row fails the join instead of NULL-padding through aLEFTjoin.
A nil scope is refused. An explicitly empty one, scope.Scope{}, is honoured.
Those are different intentions, and only one of them is safe to guess at: a caller
who simply forgot would otherwise get SQL spanning every tenant, quietly.
Expressions06
Computed columns and expression filters are evaluated through an interface, so the expression language is yours to choose.
type ExprEvaluator interface {
Eval(ctx context.Context, expr string, row map[string]any) (any, error)
}DTL satisfies it directly.