Statements
Every statement the language has, with what each is for and where it is explained.
There is no separate schema language, no migration tool and no administrative API. Defining a table, reading records, declaring a user and taking a backup are all statements, which is why this one list is the whole surface.
Tenancy
| statement | does |
|---|---|
USE NAMESPACE prod DATABASE orders | selects where the statements after it apply — at least one of the two |
DEFINE NAMESPACE prod | creates a namespace; REPLICATION FACTOR 3 or REPLICATION NONE says how many copies a cluster keeps, and MULTI MASTER or SINGLE LEADER says how many nodes may write it; ACKNOWLEDGE LEADER, LOCAL MAJORITY or MAJORITY (OR WEAKER to let a request ask for less) says how many copies hold a write before its caller is answered (When a write is acknowledged) |
ALTER NAMESPACE prod REPLICATION FACTOR 2 | moves the replication in either direction, with no repair step after it; ALTER NAMESPACE prod ACKNOWLEDGE … moves the acknowledgement level |
DEFINE DATABASE orders | creates a database in the selected namespace |
DROP DATABASE orders | removes it — refused while it still holds tables |
DROP NAMESPACE prod | removes it — refused while it still holds databases |
See Your first store.
Structure
| statement | does |
|---|---|
DEFINE TABLE users (name string, email string) | creates a table that declares its columns and refuses a field nobody named; EDGE makes it a relation table, SCHEMALESS keeps the lenient reading, and IDENTITY uuid mints identities that disclose neither order nor count where the default IDENTITY int is a per-table counter; LAST WRITER WINS or REFUSE CONFLICTS says what it does with a write it cannot order against what is stored |
DEFINE TABLE orders (total int) IDENTITY uuid SPLIT AT 'g', 'p' | a table split into shards at those identities — each logged, led and replicated on its own; moved later by ALTER TABLE orders SPLIT AT 'm' and MERGE SHARD 4, 5 while it serves (Splitting a table) |
DEFINE COLLECTION notes | creates a table for records whose shape is not fixed — every field is accepted |
DEFINE TABLE message (body string) EXPIRE AFTER 7d | a table whose records expire, 7d after each is created unless a write says EXPIRE <when> or EXPIRE NONE; ALTER TABLE message SET EXPIRE [AFTER d] and DROP EXPIRE change it, never retroactively (Records that expire) |
DEFINE SPACE sessions | a table whose records hold a single value; MAX 10000 keeps at most that many keys, evicting the least recently written, and MAX 500 EVICT NONE refuses the key past it (A space with a limit) |
DEFINE BUCKET media | a table whose records are files; MAX 5242880 declares the largest file it takes |
DEFINE GRAPH social | a graph, and the collection its own nodes live in under the same name — tables you already have join it with IN social |
DEFINE EDGE works_at IN social FROM person TO company | an edge kind, whose entries are held beside the node rather than as records |
DEFINE VECTOR embeddings DIMENSION 768 DISTANCE cosine | a vector store — both clauses are required and neither has a default |
DEFINE GEO places | a store of shapes — one word, and no clause |
DEFINE VAULT team | a store whose declared fields can be SECRET, sealed under a key minted here — the one declaration that needs the store unsealed |
DEFINE VAULT team PASSPHRASE '…' | the same, with its key under that passphrase instead of the store's — needs nothing unsealed, and the store's passphrase opens nothing in it |
DEFINE QUEUE jobs TIMEOUT 30s ATTEMPTS 5 | a queue; TIMEOUT is required and has no default, ATTEMPTS is optional and absent means unlimited; PRIORITY BY severity hands out the greatest value first and NOT BEFORE send_at holds a record back until that time (from 0.22.0-beta); DEDUPLICATE 10m makes a CREATE of an identity written inside the window write nothing (from 0.33.4-beta) |
DEFINE PARAM $grace VALUE 10m | a named value the database holds, read as $grace by scripts, events and views; DROP PARAM $grace removes it (Parameters) |
DEFINE TOPIC events RETAIN 7d MAX BYTES 4096 | an append-only order of messages with dense positions; RETAIN, RETAIN BYTES n (keep at most n bytes, oldest removed first — from 0.22.0-beta) and MAX BYTES are optional, and PUBLIC RATE 60 PER 1m (which needs MAX BYTES) lets an unsigned caller append and do nothing else (Topics) |
DEFINE SERIES readings RETAIN 30d TIME at | a table whose reads stop at a floor; RETAIN is required and has no default, and TIME at orders it by each event's own at rather than by arrival (Time-series) |
DEFINE ROLLUP hourly FROM readings WINDOW 1h BY sensor COMPUTE count(*) AS n, sum(v) AS total RETAIN 365d | per-window count, sum, min, max, approx_distinct and approx_quantile(v, q) of a TIME series, kept in the transaction that writes it; runs outside BEGIN … COMMIT |
DEFINE VIEW engineers AS SELECT … | names a read — expanded where it is used, resolved against the caller's permissions; MATERIALIZED keeps its answer, current from the table's changes (from 0.22.0-beta) |
DEFINE EVENT audit ON orders FOR CREATE THEN CREATE order_log = { order: $id } | logic run after each write of a record of the table, in the writer's transaction and as the writer; WHEN filters, THEN { … } holds several statements (from 0.22.0-beta, Events) |
DEFINE FIELD email ON users TYPE string | declares a field's type, and what it may demand |
DEFINE INDEX by_email ON users FIELDS email UNIQUE | an index — SEARCH, SPATIAL and VECTOR are the other kinds; VECTOR cosine QUANTIZED keeps one byte per component and reranks on the full vectors (from 0.22.0-beta) |
DEFINE ANALYZER simple FILTERS lowercase | how text becomes terms |
DEFINE SEARCH knowledge ON notes FIELDS title WEIGHT 3, body ON articles FIELDS text ANALYZER plain | several fields of several tables ranked as one collection; per field WEIGHT, SNIPPET, SYNONYMS <set>, NO FUZZY, NO PREFIX, NO PHRASE, and STOPWORDS <set> for the search (One search over several tables) |
DEFINE SYNONYMS machines { engine: ['loom', 'machine'] } | a store-wide set of one-word alternatives, read when a search runs |
DEFINE STOPWORDS common ['the', 'a', 'of'] | a store-wide set of words a search drops from a query outside a quoted phrase |
ALTER TABLE users SET SCHEMAFULL | or SCHEMALESS — whether undeclared fields are allowed |
ALTER TABLE users ADD FIELD city TYPE string | declares a field on a table that exists |
ALTER TABLE users ALTER FIELD age TYPE string | changes what a field demands |
ALTER TABLE users DROP FIELD age | removes the declaration, not the data — refused on a strict table while a record still carries the field |
DROP TABLE users | removes the definition and releases the name — the records are left where they are, and nothing reaches them again |
DROP FIELD email ON users | removes the declaration, not the data |
DROP INDEX by_email ON users | removes the index |
DROP SPACE sessions | removes a space's definition |
DROP BUCKET media | removes a bucket's definition |
DROP GRAPH social | removes the graph and its own node collection — refused while an attached table or an edge kind still belongs to it |
DROP EDGE works_at | removes the edge kind, and every adjacency entry it wrote |
DROP VECTOR embeddings | removes the store's definition — and refuses a table that is not a vector store |
DROP GEO places | the same, for a geo store — and the same refusal |
DROP VAULT team | destroys the key, which is what makes the records unreadable — see Vaults |
DROP QUEUE jobs | removes the queue and its records, held ones included — and refuses a table of another kind |
DROP SERIES readings | removes the table and everything in it, including records past the floor that reads had stopped answering with — refused while a rollup is kept of it |
DROP ROLLUP hourly | removes the rollup and its windows; the series is untouched |
DROP VIEW engineers | removes the definition and leaves the records alone |
DROP EVENT audit ON orders | removes the event; writes after the commit run nothing |
DROP ANALYZER simple | removes it — refused while a field or a search still names it |
DROP SEARCH knowledge | removes the search and every member; the tables are untouched |
DROP SYNONYMS machines | removes the set — refused while a search names it |
DROP STOPWORDS common | the same, for a stop-word set |
REBUILD INDEX by_at ON points | rebuilds it — a no-op except for a vector index |
ANALYZE TABLE users | keeps, beside each non-unique value index, its entry count, distinct values, sixteen most common values and sixty-four buckets, so a plan can estimate how many records a condition reaches; EXPLAIN then carries estimate and estimated_by (from 0.22.0-beta) |
CHECK TABLE users | answers with every stored record that disagrees with what the table declares now — the record, the field, the rule and the reason; an empty list means it holds |
DEFINE NAMESPACE prod;
USE NAMESPACE prod;
DEFINE DATABASE orders;
USE DATABASE orders;
DEFINE TABLE scratch (note string);
DROP TABLE scratch;
A drop removes its own definition and nothing beneath it, and refuses while
anything still points at it — naming and counting what is in the way, so acting
on the refusal needs no second query. There is no CASCADE: a destructive
statement carrying no predicate at all is the widest thing this language can be
asked to run, and it is the same rule that makes DELETE FROM t WHERE c require
a bound.
That sentence is worth reading twice where a table is concerned. The records are not deleted — removing them is bulk work, and a catalog call that did it quietly would hide the cost — but they are not recoverable either, because the id is never reused and a table redefined under the same name is a new table:
DEFINE TABLE keep (note string);
CREATE keep:1 = { note: 'first' };
DROP TABLE keep;
DEFINE TABLE keep (note string);
SELECT * FROM keep;
(0 record(s), via scan)
Reading at a version from before the drop answers the same way. DROP TABLE is
not a recycle bin: what it costs is the whole table, and the only difference
from a delete is where the bytes sit afterwards.
DROP NODE does not exist. DEFINE NODE writes this process's own
configuration, so its inverse is an edit to a file and a restart — a store where
one node could undeclare another's identity over the wire would be answering a
question nobody asked it.
See Definitions.
Records
| statement | does |
|---|---|
CREATE users = { … } | writes a record at an identity the store produces, and answers with it |
CREATE users:1 = { … } | the same, at an identity you name — writes only if nothing is there, never replaces |
INSERT INTO users (name, email) VALUES ( … ), ( … ) | writes several records in one statement and one transaction, at identities the store produces, and answers with them in the order the rows were written |
SELECT * FROM … | reads |
SELECT * FROM … ANSWERED BY LEADER | reads, and admits only the node that decides writes for these records; ANSWERED BY ANY is the default and composes with STALENESS — see Reads, routing and staleness |
UPDATE users:1 = { … } | replaces a record that is there — never invents |
UPDATE users:1 MERGE { … } | changes the fields named, keeps the rest |
UPDATE users:1 SET age = 18 | changes named fields to computed values |
UPDATE users:1 = { … } WHERE version = 4 | the same edit, but only if the condition holds — a false condition refuses and discards the whole transaction, which is what makes it a compare-and-set rather than a filter. The condition may be added to any of the three edit forms |
UPSERT users:1 = { … } | replaces it if it is there, creates it if it is not |
DELETE users:1 | removes one, by identity |
DELETE FROM readings WHERE at < … LIMIT ALL | removes what a condition matches — the bound is required, and ALL is how you say every one |
DELETE FROM readings:1..3 LIMIT ALL | removes a span of identities — .. excludes the upper end, ..= includes it, and the bound is required here too |
RELATE users:1->follows->users:2 | writes an edge |
DELETE users:1->follows->users:2 | removes one — the same arrow, and no FROM |
THROW 'message' | abandons the transaction, with that message |
A write either names one record or states a condition and a bound; neither
DELETE FROM r; nor DELETE FROM r WHERE …; is accepted on its own. RETURN BEFORE and RETURN AFTER make a write answer with the record. Most writes
answer ok without them; the two that already answer with something are CREATE
at an identity the store produced and INSERT, and what they answer with is the
identities they wrote.
See Records, Writing safely and Graphs.
Queues
| statement | does |
|---|---|
CLAIM FROM jobs | takes the head of the queue and holds it until its deadline; nothing claimable answers no records and no error |
CLAIM 5 FROM jobs | takes at most that many; above the store's ceiling is refused rather than trimmed; CLAIM $n FROM jobs takes the count from a parameter |
CLAIM jobs:7 | holds the record you name — a held one answers nothing, one that is not there raises |
RELEASE jobs:1 | ends a hold before its deadline; refused when another consumer holds it |
RELEASE jobs:1 NOT BEFORE 10m | ends the hold and holds the record back until then, in one write — on a queue declaring NOT BEFORE |
RELEASE jobs:1 FOR CONSUMER 'billing' | ends that group's hold on one record, whichever of its instances took it |
RELEASE ALL FROM jobs | ends every hold this session's instance has in one queue, and answers the records |
RELEASE ALL FROM jobs FOR CONSUMER 'billing' | ends every hold that group has there, live sessions included |
USE CONSUMER 'billing' | names this session's consumer group; the store mints its instance |
A claim is an ordinary write in the ordinary transaction, so it replicates and recovers like one. See Queues.
Topics
| statement | does |
|---|---|
CREATE events = { … } | appends a message; its position is decided when it commits |
READ FROM events AFTER 41 LIMIT 100 | reads messages after a position, at most 100 when no LIMIT is written |
READ FROM events FOR CONSUMER 'billing' | reads after the position stored for that reader and moves it, in the reader's own transaction |
DEFINE GROUP 'billing' ON TOPIC events ACK DEADLINE 30s | makes the readers under that name a group that hands messages out and waits for each to be acknowledged; DELIVERIES n, IN FLIGHT n (default 1) and DEAD LETTER TO t (which needs DELIVERIES) are optional |
READ FROM events FOR CONSUMER 'billing' LIMIT 10 | on a group, hands out messages and holds each in flight until its deadline |
ACK events FOR CONSUMER 'billing' AT 41, 42 | those messages are done; answers how many were in flight |
NACK events FOR CONSUMER 'billing' AT 43 DELAY 10s | hands them out again, now or after the delay |
ALTER GROUP 'billing' ON TOPIC events START AT 40 | the group next hands out position 41 and forgets what it held in flight |
DROP GROUP 'billing' ON TOPIC events | removes the group |
INFO FOR TOPIC events | the first and last position, each reader's position and lag, and each group's position, committed position, lag and messages in flight |
DROP TOPIC events | removes the topic |
DEFINE TOPIC payments DEDUPLICATE 5m ON payment_id | a message repeating a payment_id published inside the window is not appended |
UPDATE, UPSERT and DELETE of a message are refused. See
Topics.
Keys and files
| statement | does |
|---|---|
GET sessions:'abc' | reads one key's value |
SET sessions:'abc' = … | writes it, whole |
DEL sessions:'abc' | removes it |
SET sessions:'abc' = … EXPIRE 30m | writes it with an expiry — a duration from now or a datetime |
SET locks:'r' = 'me' IF ABSENT | writes only if there is no key (IF PRESENT, IF = value likewise) and answers whether it wrote |
INCR counters:'hits' BY 10 | adds to a number and answers the result; a missing key counts from zero |
EXPIRE sessions:'abc' 10m · PERSIST sessions:'abc' · TTL sessions:'abc' | moves, clears and reads an expiry |
KEYS FROM sessions RANGE 'a'..'m' | lists a space's keys, optionally bounded |
KEYS FROM sessions PREFIX 'user:42:' AFTER 'k' LIMIT 100 | lists the keys that begin with a prefix, a page at a time |
PUT media:'/logo.png' = 0x0a1b | writes a file, or a range of one |
READ media:'/logo.png' | reads a file, or a range of one |
See Spaces, Expiry, counters and locks and Files.
Transactions
| statement | does |
|---|---|
BEGIN | opens a boundary |
COMMIT | lands everything in it; COMMIT ACKNOWLEDGE MAJORITY waits for that many copies, as a single write may with ACKNOWLEDGE after it |
CANCEL | discards everything in it, without checking — every check that refuses a write runs inside the commit |
VERIFY | runs every check a COMMIT runs, then discards the work — a rehearsal, so a write can be tried without making it |
See Transactions.
Access
| statement | does |
|---|---|
DEFINE USER ada ON prod.orders ROLE editor PASSWORD '…' | declares a user — the first one closes the store |
ALTER USER ada SET PASSWORD '…' | or SET ROLE … — one change at a time |
DROP USER ada | removes one |
DEFINE USER ops AUTHORITIES operate PASSWORD '…' | declares a user by the set of authorities instead of by a role |
GRANT read, write ON orders TO ada | narrows a user to named tables, and optionally named fields |
REVOKE write ON orders FROM ada | takes part of that back — never the last grant |
GRANT manage ON NAMESPACE prod TO kim | hands out one authority over a reach |
REVOKE manage ON NAMESPACE prod FROM kim | takes one back |
REVEAL token FROM team:'github' | opens a vault's sealed fields for a recipient who holds a key |
REVEAL * FROM team:'github' | every sealed field of that record, on the same terms |
ADD RECIPIENT 'ada@example.com' TO team:'github' KEY 0x… | gives a recipient their own wrapped copy of the record's key |
REMOVE RECIPIENT 'ada@example.com' FROM team:'github' | takes it back — the record stays, that recipient can no longer open it |
UNSEAL VAULT WITH '…' | puts the store's master key into this process for the node's period — ten minutes unless it was started with --unseal-for |
SEAL VAULT | takes it back out, and the process can no longer open anything in the store's custody |
CHANGE VAULT PASSPHRASE FROM '…' TO '…' | wraps the same master key under a new passphrase; nothing is re-encrypted |
UNSEAL VAULT team WITH '…' · SEAL VAULT team · CHANGE VAULT team PASSPHRASE FROM '…' TO '…' | the same three for a vault declared with a passphrase of its own, and for that vault alone |
The last two are the GRANT ON REACH and REVOKE ON REACH forms, and they are
separate forms rather than a wider spelling of the two above them: a grant names
a table, while these name a reach — STORE, NAMESPACE x or
DATABASE x.y. Every reach is led by a keyword so that a table name can never be
read as one.
See Users, Authority and Grants.
The node
| statement | does |
|---|---|
DEFINE NODE ROLES serving, writable ENDPOINTS 'host:9000' | what this machine is for — local, and not in a backup |
DEFINE NODE RETAIN 100000 RECORDS / RETAIN NONE | how much of the log this machine keeps — local, 100 000 by default, and a prune is irreversible; see Bounding the log |
DEFINE FAILOVER AWARENESS 10s COLLECTION 10s ROUND 1s CAMPAIGN 1s LEASE 30s | how long the cluster waits before replacing a leader — a replicated row, all five clauses required, written by the node holding the leadership |
DEFINE REPLICA second AT 'host:9001' | a peer — a catalog record, and replicated; NODE '<id>' binds it to one machine, FINGERPRINT '<sha256>' pins the one peer certificate that may bind it, CLIENTS AT and HTTP AT say where clients reach it, ROLES … says what that peer is for, and REPLICATES STORE | NAMESPACE n | DATABASE n.d | SHARD n.d.t k is the subscription that decides what it may collect, and LEADS NAMESPACE | DATABASE | SHARD … names the one range the node stands to lead (Splitting a table); REGION 'eu' names the region the node runs in, unset unless stated, which ACKNOWLEDGE LOCAL MAJORITY counts (When a write is acknowledged) |
ALTER REPLICA second ROLES serving | amends a peer one clause at a time — AT, ROLES, CLIENTS AT / CLIENTS NONE, HTTP AT / HTTP NONE, LEADS, REGION '…' / REGION NONE — keeping the node it is bound to and its subscription |
DROP REPLICA second | removes the peer; a row that named a node also removes the node for good — its greeting is refused at the handshake and no row may name it again |
CREATE JOIN TOKEN FOR REPLICA second EXPIRES 10m | answers, once, a token that binds the row to the first node offering it; the row keeps only its digest (Adding a node) |
REVOKE CERTIFICATE '<sha256>' | refuses a peer certificate on every node, in both directions, and ends a link already open on it — store operators only, never taken back (Encryption in transit) |
BACKUP / BACKUP LOG / BACKUP FROM 42 | a snapshot of the store's current state (the routine backup), or its log, whole or from a sequence |
BACKUP STATE / BACKUP SCRIPT | a snapshot, or the store as TessariQL |
BACKUP SCRIPT OF NAMESPACE crm, prod.orders | a script of chosen namespaces and databases, without users |
BACKUP … TO 'nightly.tessarilog' | written by the node into its backup folder, never over an existing file |
RESTORE SCRIPT FROM 'crm.tessariql' | runs a script from that folder beside what the store holds; it only creates |
See Adding a node, Leadership and failover, Bounding the log and Backup and restore.
Ingestion
| statement | does |
|---|---|
DEFINE TOPIC CONSUMER events_in FROM … GROUP … INTO … IDENTITY … MAP … ON FAILURE … | reads a topic of this store into a table through a group, each message applied exactly once, and the node runs it |
DROP TOPIC CONSUMER events_in | stops it within a second and forgets the declaration; the group keeps its place |
DEFINE KAFKA CONSUMER orders_in FROM … TOPIC … GROUP … FORMAT … INTO … IDENTITY … MAP … ON FAILURE … | reads a Kafka topic into a table, at least once, and a build with the kafka feature runs it |
DROP KAFKA CONSUMER orders_in | forgets the declaration and frees the name |
See Stream ingestion, which also states the delivery guarantee — at-least-once, never exactly-once.
Scripts
| statement | what it does |
|---|---|
LET $city = 'Paris' | binds a value for the statements after it in the same script |
RETURN 1 + 1 | answers with an expression, without reading a table |
A binding is a value, not a fragment of text: it is substituted where a value
goes and never where a table or a field name goes, so it cannot widen what a
statement reaches. RETURN is also the word a write takes to answer with the
record — RETURN BEFORE and RETURN AFTER, listed under Records.
Looking
| statement | does |
|---|---|
EXPLAIN SELECT … | the plan a read would take, without taking it |
INFO FOR STORE | the namespaces |
INFO FOR NAMESPACE | the databases in the selected one |
INFO FOR DATABASE | the tables, and the searches, topics and vaults |
INFO FOR SEARCH knowledge | the analyzer, the stop words and, per member, the table, its record count and each field's weight and options |
INFO FOR TABLE users | fields, indexes and what else the catalog holds |
INFO FOR GRAPH social | the tables and edge kinds that belong to one graph — a graph just declared already lists its own node collection |
INFO FOR VECTOR embeddings | one vector store's width, distance, and the recall it was measured at — or NONE |
INFO FOR GEO places | one geo store's field and index, and what refining that index's candidates last cost |
INFO FOR USER ada | one user's role, scope and grants |
INFO FOR USERS | every user of the tenancy you administer |
INFO FOR ACCESS TO TABLE orders | who reaches one table, and whether they read it or write it |
INFO FOR SEAL / INFO FOR SEAL OF team | whether this process can open secrets and until when; for a vault, also its custody |
INFO FOR VAULT team RECORDS [AFTER team:'x'] [LIMIT n] | a vault's record ids a page at a time, with no value |
INFO FOR NODE | this machine's roles and endpoints, its peers, which of them have actually collected, the time left on its lease, the log window it retains (null is unbounded), and the failover policy the cluster is running (null is nobody has set one) |
INFO FOR VERSIONS OF person:1 | one record's surviving versions, the node that wrote the one an ordinary read answers with, and whether another survives |
INFO FOR HISTORY OF person:1 | what one record became, and when — one entry per write, newest first, with complete saying whether the read reached the start of the log |
INFO FOR TOPIC CONSUMER events_in | one topic consumer: what was declared, what this node is doing with it, and what it guarantees |
INFO FOR KAFKA CONSUMER orders_in | one consumer: what was declared, what this node is doing with it, and what it does not promise |
INFO FOR KAFKA CONSUMERS | every declared consumer, and which are running here |
INFO has a subject per question rather than one with a filter argument,
because each one has exactly one rule deciding what the caller may see — and a
statement whose answer mixed two permission levels could only give a partial
answer or a confusing refusal.
DEFINE TABLE users (name string, email string);
DEFINE INDEX by_email ON users FIELDS email UNIQUE;
INFO FOR DATABASE;
EXPLAIN SELECT * FROM users WHERE email = 'ada@example.com';
{ params: { }, searches: [], tables: ['keep', 'users'], topics: [], vaults: [] }
{ access: 'index', at_most: 1, columns: 1, exact: true, index: 'by_email', shape: 'equality', table: 'users' }
IF NOT EXISTS
Every DEFINE takes it, and without it a name already in use is refused rather
than quietly replaced — so a migration that runs twice tells you, unless you
said it was allowed to:
DEFINE TABLE IF NOT EXISTS users (name string, email string);
DEFINE INDEX IF NOT EXISTS by_email ON users FIELDS email UNIQUE;
That is what makes a start-up script re-runnable, which is what a deployment that restarts actually needs.
The other half is DROP IF EXISTS: every DROP that names one object takes
IF EXISTS and answers ok when the object is not there (from 0.33.4-beta) —
DROP EVENT IF EXISTS audit ON orders, DROP INDEX IF EXISTS by_email ON users,
DROP PARAM IF EXISTS $grace. An event that must change without a moment of not
existing is replaced with DEFINE EVENT OR REPLACE rather than dropped and
defined again.