Events
Statements a table runs after each write of one of its records — inside the writer's transaction, as the writer, landing with the write or not at all.
DEFINE COLLECTION orders;
DEFINE COLLECTION log;
DEFINE EVENT audit ON orders THEN
CREATE log = { event: $event, order: $id, was: $before.total ?? 0, now: $after.total ?? 0 };
CREATE orders:1 = { total: 40 };
UPDATE orders:1 SET total = 55;
SELECT event, was, now FROM log ORDER BY now;
1: { event: 'CREATE', now: 40, was: 0 }
2: { event: 'UPDATE', now: 55, was: 40 }
(2 record(s), via scan)
An event is statements a table runs after each write of one of its records,
inside the writer's transaction and as the writer. Its effects and the write
are one commit: they land together, or neither does. From 0.22.0-beta.
When it runs, and what it sees
After every CREATE, UPDATE and DELETE of a record — whatever wrote it: a
statement, a script, a client, a Kafka or topic consumer.
FOR CREATE, UPDATE narrows it to those writes, and WHEN to the records a
condition holds for:
DEFINE COLLECTION review;
DEFINE EVENT big ON orders FOR UPDATE WHEN $after.total > 1000 THEN
CREATE review = { order: $id, total: $after.total };
UPDATE orders:1 SET total = 1200;
SELECT order, total FROM review;
1: { order: 1, total: 1200 }
(1 record(s), via scan)
$eventis'CREATE','UPDATE'or'DELETE'.$beforeand$afterare the record as it was and as it is,NONEfor the side that does not exist;$afteris what was stored, defaults applied. Reach into them with a route —$after.total,$before.lines[0]— and a step that reaches nothing answersNONE, so$before.total ?? 0reads a new record as zero.$idis the record's identity, the value after the colon, so a body names its own recordorders:$id.
An UPSERT is a CREATE or an UPDATE depending on whether the record was
there. A DELETE of a record that was not there deleted nothing, and runs
nothing. A table's events run in name order.
Several statements go in braces, and DROP EVENT removes one:
DEFINE EVENT tally ON orders FOR CREATE THEN {
LET $line = { order: $id, total: $after.total };
CREATE log = $line;
};
DROP EVENT big ON orders;
As whom, and what refuses
Every statement in the body is authorized exactly as the writer's own would
be. A writer who may write orders but not log cannot write orders while
audit writes log: the audit is part of the write. $before and $after are
what the writer may read — a writer who may write a table and not read it gives
the body NONE for both, so an event cannot copy a field somewhere the writer
could then read it. Defining or dropping an event needs the authority to define
on the table, as an index does.
A refusal anywhere in the body refuses the write, carrying the body's own refusal.
That makes WHEN … THEN THROW the way to state a rule that reads more than the
record being written — the one an ASSERT cannot say:
DEFINE EVENT positive ON orders WHEN $after.total < 0 THEN THROW 'an order is never negative';
CREATE orders:2 = { total: -5 };
Refused — EventFailed, with the message an order is never negative inside it.
Inside BEGIN … COMMIT that is the statement failing, as any other.
An event's writes run the events of the tables they reach, its own included. A
chain more than 16 deep is refused (EventDepth), so a cycle fails at its first
write instead of stopping halfway with its effects applied. A body that updates
its own record writes a WHEN that excludes its own change.
What a body may say
CREATE, INSERT, UPDATE, UPSERT, every DELETE, RELATE, the key-value
verbs (SET, INCR, DEL, EXPIRE, PERSIST), LET and THROW. A body runs
in its table's namespace and database, already inside a transaction, with nobody
to hand a read's answer to — so USE, BEGIN, COMMIT, a plain read, a
definition and a grant are each refused where the event is written:
DEFINE EVENT chatty ON orders THEN SELECT * FROM log;
Refused — EventBody, naming the statement.
Tables, collections and edge tables carry events. A vault does not — the body would see its secrets in the clear — and neither do the stores with a write path of their own: buckets, views, topics, queues, spaces, vector and geo stores and series.
DEFINE TOPIC order_events;
DEFINE EVENT nope ON order_events THEN CREATE log = { seen: true };
Refused — EventOnKind, naming the kind.
Changing an event, and what else a body reads
DEFINE COLLECTION invoices;
DEFINE PARAM $limit VALUE 1000;
DEFINE EVENT OR REPLACE capped ON invoices WHEN $after.total > $limit THEN THROW 'over the limit';
DEFINE PARAM OR REPLACE $limit VALUE 5000;
CREATE invoices:1 = { total: 2000 };
DROP EVENT IF EXISTS capped ON invoices;
DROP EVENT IF EXISTS capped ON invoices;
DEFINE EVENT OR REPLACE replaces an event's FOR, WHEN and body — or defines
it — in one catalog write, so no commit sees the table without the event: the
form a migration that runs on every start wants. DROP EVENT IF EXISTS removes
one that may not be there. A body may read the database's named values
(DEFINE PARAM, see Parameters)
and reads the value they hold when the write runs, so changing the limit above
changed what the event refuses without redefining it. From 0.33.4-beta.
A failing body reaches the writer as EventFailed, whose class is the class of
what the body met: contention is retry, a record that is not there is
conflict, THROW is invalid. A record removed because its expiry passed fires
no event — the removal belongs to no writer's transaction.
Work after the commit is a topic
An event runs inside the transaction, so it cannot send an email or call another service and still be undone with the write. What it can do is append to a topic — and the message commits with the write or not at all:
DEFINE EVENT outbox ON orders FOR CREATE THEN CREATE order_events = { order: $id, total: $after.total };
CREATE orders:3 = { total: 70 };
READ FROM order_events;
A group, a client's consumer or DEFINE TOPIC CONSUMER then does the rest at its
own pace: exactly once for what it writes into this store, at least once for
anything outside it, with a dead letter for what will not go. There is no second,
"after commit" kind of event, because it would need everything a topic already
is — a durable queue, a position, a retry, a dead letter — built again.
Cost, and what carries it
A write to a table with no events pays nothing measurable (9.4 µs per write with the lookup and without it, release build, in memory). A write that runs an event pays its body: one audit event writing one log record took a write from 10.0 µs to 34.1 µs, one record write of which is the log row itself.
INFO FOR TABLE lists a table's events under events, each as the statement that
defines it. BACKUP SCRIPT writes them after the data, so a restore does not
run them again over the records it replays; a snapshot, the log, a follower and a
restore apply commits whose effects are already in them, and run nothing.