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, value1; - 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.
is exactly equivalent to the interior-drill form:
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
- Have an
!exclamation point as the last character in the rule head name - 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.
The above notation is sugar for
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
labelto 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.
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.
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_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!¶
| 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!¶
| 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.
| 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.
| 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.