Skip to content

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)
19 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)
19 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 )