Skip to content

Effects and Directives

A directive is a functor whose name ends in an ! exclamation point. It an instruction to the system to change something: rows in user tables, database objects, the session itself, or any mutative script, i.e. it is called for its effect. A directive is still a predicate. It is a relation whose evaluation returns a table called a receipt.

success operation … …
1 mount! … …

Examples of calling directives:

mount!("etl.sqlite", "etl")(*)

enlist!("etl")(*)

_(last_name, first_name @ "Eklund", "Daniel") |> insert!(employee(*))(*)

Directives may be built-in or user authored. In either case, the following are all true:

  • all directives always end with a !
  • all directives always return a single row or no row at all

Receipts

A receipt is a single row table that is the result of calling a directive. A receipt also includes the concept of not being returned, so the more accurate statement is that a receipt is a zero or one row table that is the result of calling a directive.

The presence of a single row is meant to be understood as a successful judgement of a proposition – a “YES”, that the directive was successfull. The lack of a row, a zero cardinality table, indicates “NO”.

This cardinality and semantics allows receipts to be used in composed queries, where the NO short-circuits the rest of the query, simulating an early return. The presence of a YES row merely adds columns to the relation but never explodes the cardinality of the query in which it participates.

Receipt Schema

Every receipt GUARANTEES exactly two things:

  • the first column is success, value 1;
  • the second column is operation, the name of the directive that produced the receipt, as written ('temp_table!', 'insert!'),

Every remaining column is a declared addition: a typed column recorded in the directive’s contract. A creation receipt (table!, temp_table!, temp_view!) declares target, the fully qualified data namespace target the designator selected, and created, the exact catalog path of the object created; they coincide for table!:

success operation target created
1 temp_table! main.staged sys::shadow::main.staged
success operation … …
1 mount! … …

Some receipts from certain directives embed a table within a table (a cheat that allows richer data return, but with the same zero-or-one cardinality that receipts demand).

Those receipts that embed a table within a table assigned to the name returned operate with the TEE-PIPE !> a syntactic sugar for exploding out the table within and removing the receipts columns.

employee(*), title = "IT Staff" !> assert!(count_is(2), "two IT staff")(*)

is exactly equivalent to the interior-drill form:

employee(*), title = "IT Staff" |> assert!(count_is(2), "two IT staff")(*) |> .returned(*)

Effect Rules

An effect rule is a rule where the functor name has a ! in the head. It can be either lower or higher-order.

Effect rules allow the programmer to genereate their own reusable effects.

?- mount!("etl.sqlite", "etl")(*)

quarantine!(Bad(*))(*) :-
    Bad(*) |> insert!(etl.partner_sale_quarantine(*))(*)

stage!(*) :-
    etl.partner_sale_2025_07_31(*) |> temp_table!(staged(*))(*)

load!(Good(*))(*) :-
    Good(*), +customer(, email = buyer_email), quantity > 0
      |> insert!(etl.partner_sale_line(*))(*)

An effect rule must

  1. Have an ! exclamation point as the last character in the rule head name
  2. End with a directive call (either built-in or a user authored effect rule)

This second requirement guarantees that every directive maintain the contractual semantic of returning a receipt.

The Unwrap Pipe

The UNWRAP-PIPE !> is a special pipe syntax used often with certain built-in directives.

employee(*), title = "IT Staff" !> assert!(count_is(2), "two IT staff")(*)

The above notation is sugar for

employee(*), title = "IT Staff" |> assert!(count_is(2), "two IT staff")(*) |> .returned(*)

For certain primitive built-in directives this notation permits a complete relation to be returned from a directive efficiently.

ETL and scripting

Scripting is the act of utilizing effect rules – built-in or authored – to affect some change to a system.

Delightql provides the following semantic guarantees that makes scripting principled:

  • all directive invocations return a receipt – zero or one rules
  • all authored effect rules must end in another directive
  • the COMMA , short-circuits effects after any other effect that returns a zero row receipt
  • certain built-in directives – run! chief among them – have a well-known protocol
?- mount!("etl.sqlite", "etl")(*)
?- enlist!("main")(*)    // track, customer, and staged are read bare

landed(*) :-
    etl.partner_sale_2025_06_30(*)
      |;| etl.partner_sale_2025_07_31(*)
      |> %(*)

quarantine!(Bad(*))(*) :-
    Bad(*) |> insert!(etl.partner_sale_quarantine(*))(*)

stage!(*) :-
    landed(*)
      |> $$(lower:(trim:(buyer_email)) as buyer_email)
      |> temp_table!(staged(*))(*)

load!(Good(*))(*) :-
    Good(*), +track(*.(track_id)), +customer(, email = buyer_email), quantity > 0
      |> insert!(etl.partner_sale_line(*))(*)

main!(*) :-
    stage!(*) : s!
    staged(*), (\+track(*.(track_id)) or \+customer(, email = buyer_email))
      |> quarantine!(*) : q!
    staged(*) |> load!(*) : l!

    s!(*) ; q!(*) ; l!(*)

Built-in Directives

The built-in directives fall into five categories, by what effects they produce:

category directives direct
session directives consult!, consult_tree!, reconsult!, unconsult!, mount!, mount_new!, mount_tree!, unmount!, refresh!, ground!, enlist!, delist!, alias!, expose!, doc! the session’s namespace tree
DDL directives temp_table!, table!, temp_view!, imprint!, imprint_replace! database objects
DML directives insert!, update!, delete! rows in user tables
execution directives run!, run_namespace! starting runs
utility directives exit!, returning!, returning_other!, stdout! the run itself: stop, return, sequence, print

The assert! directive

The assert! directive takes three arguments:

  • a HO-judgment rule
  • a human readable label
  • the input relation

and runs the input relation through the judgment rule. If the judgement returns any rows then the assertion has passed. If the judgement fails to return rows, then assert! aborts the script or one REPL invocation and prints the human-readable label.

Example Passing

_(a@10;22;42)
  !> assert!(
       same_bag(
       _(a@10;22;42)),
       "Anon tables should match")(*)
  |> ( a*2 as post_assert)
post_assert
20
44
84

Example Failing

_(a@10;22;42)
  !> assert!(
       same_bag(
       _(a@ 99999999 )),
       "Anon tables should match")(*)
  |> ( a*2 as post_assert)

Error: [delightql-error://authored/abort] Permission: Anon tables should match

Judgment Rules

A judgment rule is any higher-order rule that takes a table parameter as its last parameter. Structurally, this is denoted in higher-order parameter syntax as P(... T(*))(*).

at_least(n,T(*))(*) :-
    T(*) ~> count:(*) as count,
    count >= $.n

is_greater(n, T(*))(*) :-
    T(*) ~> count:(*) as count,
    count > $.n

not_empty(T(*))(*) :-
    T(*) ~> count:(*) as count,
    count > 0

greater_than(n, T(*))(*) :-
    T(*) ~> count:(*) as count,
    count > $.n

count_between(lo, hi, T(*))(*) :-
    T(*) ~> count:(*) as count,
    count >= $.lo,
    count <= $.hi

A judgment rule should encode some simple logic about when a condition is met or not and return zero or one rows: zero to cause the system to abort, and one to allow the chain to continue.

Denotational Assert! Definition

The assert rule is assumed to obey the following denotation:

assert!(P(... T(*))(*), label, I(*))(*) :-
    I(*) |> P(*) : witness
    witness(*) + : judgment
    judgment(*), met=1 |> returning_other!(I(*))(*) : pass!
    judgment(*), met!=1, abort!($.label) : fail!

    fail!(*); pass!(*)

Roughly this shows three higher-order parameters:

  • the judgment rule ( P(...T(*))(*)) that structurally still requires one last table-value parameter
  • the label to be printed on failure and
  • the input relation I(*)

The invocation I(*) |> P(*) is the heart of the asserts.

Consult

consult!

consult!(path, ns) reads the definitions file at path into a new namespace ns and executes the file’s liminal space as it loads. The definitions are addressable by their qualified names; enlist! makes them available without qualification.

Consultation and name visibility are distinct from grounding: a groundable library’s free table references are bound by ground! (see Grounding). The rule below instead names its data dependency explicitly as main.track.

A relative path resolves against the session’s base directory, not the directory of the file that names it.

// mylib.dql
long_tracks(*) :- main.track(*), milliseconds > 600000
consult!("mylib.dql", "mylib")(*)
enlist!("mylib")(*)
long_tracks(*) |> (name, milliseconds)

One consulted source owns one namespace. Consulting into a name that already holds a consulted namespace refuses rather than merging; reload it with reconsult! or remove it with unconsult!.

reconsult!

reconsult!(ns) reads the namespace’s file again and replaces the namespace whole: the next statement sees the new definitions, while a statement already running keeps the ones it began with. reconsult!(ns, path) reads a different file into the same namespace.

consult!("mylib.dql", "mylib")(*)
reconsult!("mylib")(*)

unconsult!

unconsult!(ns) removes a consulted namespace. It refuses, naming the blockers, while a definition outside the namespace still depends on it or another namespace stands beneath it.

consult!("mylib.dql", "mylib")(*)
unconsult!("mylib")(*)

consult_tree!

consult_tree!(path, ns) consults every .dql file under the directory path, one namespace per file beneath ns: consult_tree!("lib", "lib")(*) makes lib/top.dql the namespace lib::top and lib/sub/speed.dql the namespace lib::sub::speed. The receipt’s returned column lists each file with the namespace it became.

Mount

Mounting is the process of importing knowledge of tables and views into a delightql namespace. No table, view, function or higher-order table can be accessed without it having been mounted into a namespace.

During the mounting process, delightql will scan the target connection for a list of all entities at that location and store them into internal delightql system tables. This effectively creates a delightql-side information schema that becomes the system of record

mount!

mount!("etl.sqlite", "data::prod")(*)
success operation path namespace
1 mount! etl.sqlite data::prod

The directive mount! is a higher-order directive. It takes a pair of higher-order parameters both of which are strings:

  • the target specific connection string
  • the namespace into which this data will become visibile

The namespace must not exist prior to the mount call.

unmount!

mount!("etl.sqlite", "data::prod")(*)
unmount!("data::prod")(*)
success operation namespace
1 unmount! data::prod

refresh!

The refresh! directive asks delightql to re-connect to the connection associated with the data namespace and to discover new or changed entities. Use this when other programs may modify the database entities underneath you.

mount!("etl.sqlite", "data::prod")(*)
refresh!("data::prod")(*)
success operation namespace
1 refresh! data::prod

mount_new!

Use mount_new! when the connection refers to a non-existent database. Delightql will create an empty namespace connected to the target’s own empty namespace.

mount_new!("test.db", "data::test")(*)
success operation path namespace
1 mount_new! test.db data::test

mount_tree!

Use mount_tree! when the connection has an internal hierarchy that can be turned into sub-namespaces on the delightql side.