@file.sumatra) and the stored procedure
(the same body wrapped in CREATE OR REPLACE PROCEDURE, covered in
Stored procedures). This page walks the anonymous
block top to bottom.
;. The corpus convention is UPPERCASE
keywords and lowercase identifiers; identifier case is not significant.
PARAM: program inputs
PARAM('name'), and it is bound out-of-band as a real
query parameter, never substituted into the SQL text:
- A non-
TEXTparameter needs an explicit cast at the point of use:CAST(PARAM('want_date') AS DATE). - A bare
want_datein compute code is an undeclared-variable error — parameters are read-only inputs to your SQL, not variables.
DECLARE: variables and carried state
:= 0.
DECLARE is also where record and collection types live — see
records and collections.
The read block
All row input comes through one construct:BATCH SIZE
BATCH SIZE is mandatory on every read and sets the working grain:
BATCH SIZE N— fetch the result in chunks of up to N rows, using keyset pagination over theORDER KEY(the sameWHERE key > last ... LIMIT Npattern you would hand-write). Each batch is processed and written as a unit, and memory stays bounded at N rows.BATCH SIZE ALL— one whole-set batch, evaluated once. This is the right choice for aggregating reads (aGROUP BYsource re-evaluates per chunk underBATCH SIZE N) and for order-independent bodies. On the client-compute path the whole set is held in memory.
ORDER KEY
ORDER KEY (...) declares a strict total order over the read. It is
required when:
BATCH SIZE Nchunks the read (the key is the pagination cursor), or- the body is order-dependent — carried state that must see rows in a specific sequence — at any batch size, or
- the loop is a writing client-compute loop.
ALL) work.
Key columns must be INT, BIGINT, or TIMESTAMP (composite keys
are fine, e.g. ORDER KEY (customer_id, entry_id)). The order must
actually be strict: a duplicate key value or a NULL in a key column at
runtime is a loud error — never a silently skipped or double-processed
row.
Row loops and batch statements
Inside the batch body you either iterate rows in memory:How writes are routed
You write only naturalUPDATE, INSERT, and DELETE. The compiler
routes each write to the lightest correct SQL form — there is no
routing keyword, and you never write MERGE, COPY, or staging
tables:
The per-row forms and their rules:
IF that deletes one row, updates another,
and inserts a third produces a single op-tagged change set applied by
one MERGE.
The merge key is whatever the write’s WHERE equates with the
record’s own fields — a single column or a composite AND-chain
(WHERE wh = rec.wh AND sku = rec.sku). Any extra non-key condition on
rec fields acts as a capture filter: the change is captured only
when it is TRUE (WHERE id = rec.id AND rec.qty > 0 skips
non-positive rows).
Three rules the compiler holds you to:
- A write may not change its own key:
SETof a merge-key column, or aSET ROWmatched on anything but the record’s own key field, is a compile error. - One change per key per flush: writing the same key twice in one batch
is a program error. Statically visible cases are compile errors;
data-dependent duplicates surface as the engine’s own MERGE
cardinality error at runtime (catchable with
WHEN OTHERS). Repeated DELETEs of the same key are idempotent and allowed. - An upsert is written as its two natural statements — an accumulate
UPDATEplus a complementINSERT ... WHERE key NOT IN (...)— and both run server-side; together they behave as the upsert.
COMMIT: placement is meaning
COMMIT is always explicit — Sumatra never commits behind your back —
and where you put it defines the transaction grain:
- A writing loop with no
COMMITanywhere is a compile error — there is no implicit commit to fall back on. COMMITcannot appear inside a compute loop (WHILE, count loops, row loops) — per-row commits are inexpressible by grammar.- A server-side read at
BATCH SIZE Nmust commit in the batch body.
COMMIT: a fault or crash
rolls back the open transaction while every already-committed batch
survives. On Firebolt, transactions are ACID and snapshot-isolated, so
readers never see a half-written batch.
What the compiler manages in your database
Two pieces of engine-side plumbing exist so you know what they are:- Staging tables — client-computed changes land in a per-run
scratch table named
sumatra_stage_...before the MERGE applies them. They are created, truncated, and dropped automatically; a crashed run’s orphan is reclaimed by the next run of the same program. A companionsumatra_stage_lock_...marker refuses two concurrent runs of the same program with the same parameters. sumatra_source— the stored-procedure catalog table (only created once you store a procedure).
sumatra_ name prefix as reserved for the toolchain in
databases you point Sumatra at.
Program lifecycle
A run finishes in exactly one of three states, surfaced as the process exit code:0 completed, 2 stopped by a handler with the committed
prefix retained, 1 error. There is no automatic retry or resume —
re-running a program is always your action, and the standard idiom is
to make the read re-runnable (WHERE processed = FALSE, flipping the
flag as part of each row’s write) so a re-run continues where the
committed prefix ends.