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:

  • (~~error: no body – just an optional URI which matches with well-known errors
  • (~~danger: a URI naming the acknowledged danger

(~~docs shares the delimiters but is not an annotation. It is the dedicated definition-document form, and its 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.

invoice(*), total > 0
  (~~config://generation/rule/inlining/view ~~)

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:

invoice(*), total > 20
  (~~config://generation/rule/inlining/view ~~)
  |> (invoice_date, billing_city)

Annotations are usually transparent to SQL generation and so the pipeline above produces identical SQL to:

invoice(*), total > 20 |> (invoice_date, billing_city)

Multiple annotations at the same point are permitted. They appear as siblings in the CST and are processed in order.

Annotation Types

  • error – expects compilation to fail, matches the error URI (see Error Assertions)
  • danger – acknowledges a named safety gate for the current query (see Danger Gates) (see Options)

The dedicated docs form attaches documentation to a definition; see Docs.

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: the statement returns no rows. A statement that compiles anyway is refused (runtime/expectation).

// should fail: table does not exist
nonexistent_table(*) (~~error://semantic/resolution/table ~~)

// should fail: column not in scope
customer(*) |> (no_such_column) (~~error://semantic/resolution/column ~~)

// should fail: any semantic error (prefix match)
invoice(*), total in (1,2,3) (~~error://semantic ~~)

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://semantic/resolution matches semantic/resolution/table, semantic/resolution/column, and any future semantic/resolution/* error. error://semantic/arity matches only semantic/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 Meaning
authored A termination the program itself demanded.
client An incident of the interactive client itself.
configuration A host’s boot settings are invalid.
dml A data-modification query violated DML shape rules.
imprint A blueprint or manifest lifecycle refusal.
internal A defect in DelightQL itself.
namespace A namespace-creation policy refusal.
operational This session refuses to run a valid query.
parse The source text is structurally invalid.
runtime An execution-time failure.
semantic The query is well-formed but semantically invalid.
target The foreign engine rejected or failed the query.

Top-level error URI families (dql explain delightql-error://<family> lists each family’s members)

Coexistence with Data Assertions

Error assertions and data assertions can appear in the same file, documenting both correct and incorrect forms:

// correct: an ordinary assertion effect checks the established relation
invoice(*), total > 0
  !> assert!(exists(*), "a positive-total row exists")(*)

// incorrect: commas produce a multi-column single-row relation
invoice(*), total in (1,2,3) (~~error://semantic/membership ~~)

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:

genre_2024(*) as x (~~danger://semantics/min_multiplicity~~)
  |;| genre_2025(*) as y,
  x.* = y.*

The annotation attaches at a continuation point (after a relation). The URI identifies the specific danger. The annotation takes the URI alone: writing it opens that gate for this query, and nothing else opens one: a state word such as ON has no place in it.

A state word inside the annotation is an error:

// INVALID: the annotation takes no state word
genre_2024(*) as x (~~danger://semantics/min_multiplicity ON~~)
  |;| genre_2025(*) as y,
  x.* = y.*

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: 23 rows, Jazz once
genre_2024(*) as x (~~danger://semantics/min_multiplicity~~)
  |;| genre_2025(*) as y,
  x.* = y.*

// gate is closed again: 47 rows, every matching row of both arms
genre_2024(*) as x |;| genre_2025(*) as y,
  x.* = y.*

A min_multiplicity gate must be spent: written on a statement where no correlated union uses it, it refuses (semantic/setop/min_multiplicity/unspent), and over a correlation that compares only some of the columns the arms share it refuses too (semantic/setop/min_multiplicity/partial) — correlate the whole row (x.* = y.*).

An unaliased self-join is not a danger to acknowledge: two live scopes sharing a name always refuse (semantic/scope/duplicate). Alias one side.

Session Baseline

The program starts with every danger OFF, and only an annotation in the source text opens one, for its query.

Per-query annotations override the session baseline. At query end, the danger reverts to the enclosing scope:

Danger URI Reference

The known dangers are:

URI What it gates
delightql-danger://semantics/min_multiplicity A correlated union pairs duplicate copies by minimum multiplicity (SQL’s INTERSECT ALL) instead of keeping every matching row of both arms; inline only

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

long_tracks(*) :-
  (~~docs
    Tracks longer than ten minutes.

    Returns:
      columns: inherited from track
      cardinality: variable
  ~~)
  track(*), milliseconds > 600000

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:

usa_customers(*) :-
  (~~docs
    Customers whose country is USA.
  ~~)
  customer(*), country = "USA"

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(invoice_date, threshold) :-
  (~~docs
    Filters to rows where invoice_date is
    within threshold days of today.
  ~~)
  invoice_date > 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.entity(*)
  |> ( name, doc )