DQL
1.x
Docs/DQL/Query reference
Open

Reading4 min
Updated2 Aug 2026
Sourcev1/specification.mdx

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: asc

A 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: 10

The 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 10
source events
  | filter (level == "ERROR")
  | aggregate count(id) as total
  | limit 10

Comments 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 origin
Note

The 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.

Warning

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},
}
  • Required emits the predicate even when a table does not declare the column.

  • ScopeJoins also scopes joined tables, in the ON clause instead of the WHERE, so an out-of-scope row fails the join instead of NULL-padding through a LEFT join.

Note

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.