Annotations
Annotations appear at continuation points in a query pipeline, and permit language directives to operate as inline syntactic elements.
The Annotation Framework
Annotations are distinguished by matching PARENOTATES (~~ ~~) followed immediately (no space) by an <identifier>.
(~~<identifier> body ~~) -- annotation with body
(~~<identifier>:instance body ~~) -- annotation with instance name
The identifier after the (~~ is the ANNOTATION-TYPE.
How the body is parsed within an annotation is a function of the specific ANNOTATION-TYPE. Each recognized type has its own grammar rule:
(~~assert: the body is parsed as a DQL continuation(~~error: no body – just an optional URI which matches with well-known errors(~~danger: a URI and a toggle state(~~option: a URI and a toggle state(~~docs: the body is raw text
A colon form identifier:instance names a specific annotation instance and is also a function of the particular annotation type in question.
users(*)
(~~assert:positive_age , age > 0 |> forall(*) ~~)
Only the annotation types listed above are recognized by the grammar. Unknown annotation names produce a parse error.
Placement
Annotations may appear at any continuation point – before a pipe, before a comma, or at the end of an expression:
users(*)
(~~assert:has_rows |> exists(*) ~~)
, age > 30
(~~emit:filtered ~~)
|> (first_name, email)
Annotations are usually transparent to SQL generation and so the pipeline above produces identical SQL to:
users(*), age > 30 |> (first_name, email)
Multiple annotations at the same point are permitted. They appear as siblings in the CST and are processed in order.
Annotation Types
assert– forks a sub-query, evaluates a predicate, produces a verdict (see Assertions)error– expects compilation to fail, matches the error URI (see Error Assertions)danger– opens or closes a named safety gate for the current query (see Danger Gates)option– selects a strategy or preference for the current query (see Options)docs– attaches documentation to a DDL definition (see Docs)
Assertions
Assertions verify properties of a relation at a given point in a pipeline. They are annotations whose body is parsed as DQL, using interior relation semantics to scope the current relation.
users(*), age > 30
(~~assert , age > 30 |> forall(*) ~~)
|> (first_name, email)
The assertion above verifies that every row has age > 30 at that point in the pipeline. The main pipeline is unaffected – the relation after the assertion is the same as before it.
Assertion Syntax
An assertion uses the annotation syntax with the reserved name assert:
(~~assert <continuation> ~~)
The body after assert is parsed as a DQL continuation – the same syntax used inside functor parentheses for interior relations (see Interior Relations). The (~~assert delimiter scopes a sub-query on the current relation. The leading , or |> is a continuation on the implicit relation, exactly like users(, age > 20).
The sub-query inside the assertion is a fork: it branches from the main pipeline, evaluates independently, and the main pipeline continues with the original relation regardless of the assertion’s outcome.
The assertion body is pure DQL. It may terminate with an assertion view that produces a single-column, single-row boolean relation (see Assertion Views below). If no assertion view is specified, exists(*) is implied – the assertion passes if at least one row survives the body’s filters:
-- these are equivalent
users(*) (~~assert , age > 0 ~~)
users(*) (~~assert , age > 0 |> exists(*) ~~)
The bare form is the common case. An explicit view is only needed for notexists(*), forall(*), or equals(*).
Named Assertions
Assertions may carry a name. The name appears after assert as a colon-delimited string:
(~~assert:"age is positive" , age > 0 |> forall(*) ~~)
(~~assert:"has email" , email != null |> forall(*) ~~)
(~~assert:"at least 3 rows" ~> count:(*) as n, n >= 3 |> exists(*) ~~)
The name should be an author-supplied label that serves as the primary key when recording assertion outcomes. Unnamed assertions still work. They will receive a synthetic key (derived from source location and body hash) but lose cross-run trackability.
Data Assertions
Data assertions check properties of the rows at a point in the pipeline. They end with an assertion view that reduces the relation to a boolean:
-- at least one row with age > 20 exists
users(*) (~~assert , age > 20 |> exists(*) ~~)
-- every row has age > 20
users(*) (~~assert , age > 20 |> forall(*) ~~)
-- no nulls in email
users(*) (~~assert , email is null |> notexists(*) ~~)
-- exactly 3 rows
users(*) (~~assert ~> count:(*) as cnt, cnt == 3 |> exists(*) ~~)
-- id is unique (no duplicates)
users(*) (~~assert ~> %(id ~> count:(*) as n), n > 1 |> notexists(*) ~~)
-- age is always positive
users(*) (~~assert , age > 0 |> forall(*) ~~)
The assertion body is any valid DQL.
Schema Assertions
Schema assertions check structural properties of the relation. They use the meta-ize operator ^ (see Meta-ize Operator) to convert the schema to a queryable relation, then apply standard assertions:
-- column "age" exists
users(*) |> (name, age)
(~~assert ^, colname = "age" |> exists(*) ~~)
-- exactly 3 columns
users(*) |> (a, b, c)
(~~assert ^ ~> count:(*) as n, n == 3 |> exists(*) ~~)
-- no TEXT columns
users(*)
(~~assert ^, coltype = "TEXT" |> notexists(*) ~~)
For exact schema matching, use equals(*) with the reverse pipe <| (see Reverse Pipe) to compare against an expected schema:
users(*)
(~~assert ^ |> equals(*) <| _(colname, colpos
------
"age", 1;
"last_name", 2;
"first_name", 3) ~~)
Relational Equality
The equals(*) view checks bag equality between two relations via the reverse pipe. Bag equality means: same column names in the same order, and the same bag of rows with duplicates and multiplicities preserved.
-- assert query result matches expected rows
users(*), age > 50
(~~assert |> equals(*) <| _(first_name, age
------
"Alice", 55;
"Bob", 62) ~~)
The right operand of <| can be any relational expression – a CTE, a table access, or an anonymous table literal.
Assertion Views
Assertion bodies end with a view from std::prelude that reduces a relation to a single-row, single-column table. The column is named bool and contains true or false. Every assertion view has this same output shape – the runner reads the bool column to determine the verdict.
┌───────┐
│ bool │
├───────┤
│ true │
└───────┘
| View | Semantics | SQL pattern |
|---|---|---|
exists(*) |
At least one row in the input | SELECT EXISTS(...) AS bool |
notexists(*) |
No rows in the input | SELECT NOT EXISTS(...) AS bool |
forall(*) |
All input rows survived filtering | SELECT NOT EXISTS(... WHERE NOT ...) AS bool |
equals(*) |
Bag equality of two relations |
Assertion views (auto-imported from std::prelude)
All four views produce the same relation: one row, one column named bool. This uniformity means the assertion mechanism needs no special dispatch – the pipeline compiles the body, executes it, and reads bool from the single result row.
Error Assertions
Error assertions verify that a query fails compilation or resolution with a specific error. They use a separate annotation with the reserved name error:
(~~error://uri/path ~~)
The URI identifies the expected error category using a hierarchical path. The pipeline attempts to compile the query; if compilation fails and the actual error matches the URI, the assertion passes.
-- should fail: table does not exist
nonexistent_table(*) (~~error://resolution/table_not_found ~~)
-- should fail: column not in scope
users(*) |> (no_such_column) (~~error://resolution/column_not_found ~~)
-- should fail: any validation error (prefix match)
users(*), age in (1,2,3) (~~error://validation ~~)
A bare error annotation with no URI matches any error:
-- should fail with some error, don't care which
bad_query(*) (~~error ~~)
Errors are rarely needed for end users.
URI Prefix Matching
The URI is matched as a prefix against the actual error’s canonical URI. error://resolution matches resolution/table_not_found, resolution/column_not_found, and any future resolution/* error. error://validation/arity matches only validation/arity and its sub-paths.
Error URI Categories
Each DelightQLError variant maps to a canonical URI path. The URI is a stable identifier for the error category, reusable in documentation, tooling, and diagnostics.
| URI | Phase | Meaning |
|---|---|---|
parse |
compile | Syntax-level parse failure |
resolution/table_not_found |
compile | Table not in schema |
resolution/column_not_found |
compile | Column not in scope |
validation/arity |
compile | Wrong number of arguments |
validation/ambiguous |
compile | Ambiguous column reference |
validation/duplicate |
compile | Duplicate name or definition |
build/* |
compile | AST construction errors |
transform/* |
compile | SQL generation errors |
limitation/* |
compile | Known limitations |
runtime/bug |
runtime | Generated SQL rejected by backend (compiler defect) |
runtime/collision |
runtime | Namespace or resource already exists (duplicate mount!/consult!) |
runtime/useafterfree |
runtime | Accessing parted or unavailable resource (use after part!) |
runtime/assertion |
runtime | Data assertion verdict is fail |
Error URI categories
Coexistence with Data Assertions
Error assertions and data assertions can appear in the same file, documenting both correct and incorrect forms:
-- correct: semicolons produce a single-column multi-row relation
users(*), age in (1;2;3)
(~~assert , age > 0 |> exists(*) ~~)
-- incorrect: commas produce a multi-column single-row relation
users(*), age in (1,2,3) (~~error://validation ~~)
Scope
Error assertions are primarily a language development tool. They assert contracts about the compiler’s behavior. End users writing queries against a database should have no use for expected failures. I think
Danger Gates
Certain behaviors are safe in most contexts but dangerous in others. Rather than forbid them outright, delightql gates them behind danger URIs: named safety boundaries that are closed by default and opened explicitly per-query.
Syntax
A danger gate is a danger:// URI inside annotation delimiters:
employee(*) as e (~~danger://dql/cardinality/nulljoin ON~~),
department(*) as d,
e.DepartmentId = d.DepartmentId
The annotation attaches at a continuation point (after a relation). The URI identifies the specific danger. The toggle controls it:
| Toggle | Meaning |
|---|---|
ON |
Enable the dangerous behavior for this query |
OFF |
Restore the safe default (useful to override a CLI baseline) |
1–9 |
Graduated severity levels for host-defined behavior |
A bare form without a toggle is an error:
// INVALID: no toggle
(~~danger://dql/cardinality/nulljoin~~)
Scoping
A danger gate opens for one query and auto-closes at query end. It does not leak into subsequent queries:
-- gate is open for this query
employee(*) as e (~~danger://dql/cardinality/nulljoin ON~~),
department(*) as d,
e.DepartmentId = d.DepartmentId
-- gate is closed again -- safe defaults restored
employee(*) as e, department(*) as d,
e.DepartmentId = d.DepartmentId
Multiple gates may be opened for the same query:
employee(*) as e
(~~danger://dql/cardinality/nulljoin ON~~)
(~~danger://dql/cardinality/cartesian ON~~),
department(*) as d
Session Baseline
The program starts with every danger OFF. A client to the program, lik the CLI, can shift the baseline for guardrail dangers – those that control execution policy (resource limits, safety checks) rather than language semantics:
dql query --danger dql/cardinality/cartesian=ON --db test.db "..."
Dangers that change language semantics (what operators mean) cannot be overridden from the CLI. They must appear in the source text – either as inline per-query annotations.
Per-query annotations override the session baseline. At query end, the danger reverts to the enclosing scope:
Danger URI Reference
The full hierarchy of danger URIs, their defaults, and their semantics is documented in the Danger URI Taxonomy appendix. The initial dangers are:
| URI | What it gates |
|---|---|
dql/cardinality/nulljoin |
NULL-matching joins (= compiles to IS NOT DISTINCT FROM in join position) |
dql/cardinality/cartesian |
Cross joins without explicit conditions |
dql/termination/unbounded |
Recursive CTEs without termination conditions |
Option Annotations
Where danger gates control safety (off by default, opened to permit risky behavior), option annotations control preferences – which code path the compiler uses when multiple paths lead to the same result. A query with an option annotation produces the same logical result regardless of the option state; only the implementation strategy may differ.
Options may be used for non-query reasons too.
Syntax
An option annotation is an option:// URI inside annotation delimiters:
users(*) (~~option://generation/rule/inlining/view ON~~) |> (id, first_name)
The toggle values are identical to danger gates:
| Toggle | Meaning |
|---|---|
ON |
Enable the strategy for this query |
OFF |
Disable the strategy (restore default) |
1–9 |
Graduated preference levels |
Scoping
Like danger gates, option annotations are scoped to a single query and auto-revert at query end. Multiple option annotations may appear on the same query.
Session Baseline
The CLI can shift the baseline for a session:
dql query --option generation/rule/inlining/view=ON --db test.db "..."
Per-query annotations override the session baseline.
Known Options
| URI | Default | What it controls |
|---|---|---|
generation/rule/inlining/view |
OFF | View inlining strategy during SQL generation |
generation/rule/inlining/fact |
OFF | Fact inlining strategy during SQL generation |
Docs
Definitions may carry structured documentation between the neck and the body. The docs block uses the annotation delimiters with the docs identifier:
<HEAD> <NECK> (~~docs ... ~~) <BODY>
Syntax
high_paid_employees(*) :-
(~~docs
Employees with salary above the company median.
Returns:
columns: inherited from employee
cardinality: variable
~~)
employee(*), Salary > 50000
The docs block is a (~~docs ... ~~) annotation. The body is raw text – no DQL parsing is applied. Line breaks, indentation, and blank lines are preserved as written.
The block must appear immediately after the neck, before the first expression of the body. Only one docs block per definition is permitted.
Applicability
The docs block is valid on any rule-form definition:
Views:
active_users(*) :-
(~~docs
Users whose account status is active.
~~)
users(*), status = 'active'
Functions:
tax_amount:(price, rate) :-
(~~docs
Computes tax as price times rate, rounded to two decimal places.
Returns:
type: numeric
~~)
round:(price * rate, 2)
Higher-order rules:
same_schema(T(*), V(*))(*) :-
(~~docs
Compares T's and V's schema for
equality. Equality is reached if the
column names match exactly and are
in the same ordinal position.
Returns:
column: pass
column type: boolean
cardinality: 1
~~)
first_md(*) : T(?)
second_md(*) : V(?)
together(*) :
second_md(*),
first_md(*.(column_name, ordinal))
together(~> count:() as a),
first_md(~> count:() as b),
second_md(~> count:() as c)
|> ( (a == b) and (b == c) as pass)
Sigma predicates:
+is_recent(threshold) :-
(~~docs
Filters to rows where created_at is
within threshold days of today.
~~)
created_at > date:('now', '-' ++ threshold ++ ' days')
The docs block is not valid on facts (which have no neck) or on shadow-neck definitions (which are query-scoped and ephemeral).
Storage
When a definition is loaded via consult!(), the docs text is extracted at parse time and stored alongside the entity in the system catalog.
The docs are queryable through the system metadata:
sys::entities.entities(*)
|> ( name, doc )