sumatra --dsn my.dsn @file.sumatra.
They build on each other in order, so if this is your first contact
with Sumatra, read top to bottom.
1. The smallest program: set-DML
When the work is already set-shaped, a program is just SQL statements and aCOMMIT — no variables, no loops. Sumatra runs these server-side
exactly as written.
Tables
expire.sumatra
- A body can hold plain
UPDATE/DELETE/INSERT ... SELECTstatements directly. They run on the engine as-is — rows never move to the client. - The single top-level
COMMIT;makes both statements one atomic transaction. - The datetime expression is the engine’s own dialect — inside SQL statements you write the engine’s SQL (Firebolt here), verbatim.
2. Branching per row: CASE and write-back
The canonical Sumatra shape: read in batches, compute per row, write each row back.Tables
grades.sumatra
- The
CASE ... END CASE;statement puts statements in each branch. Its cousin, the CASE expression, selects a value instead — see the language reference. - The read selects every column of the table because
SET ROW = recwrites the whole record back (all columns except the key it matches on). - The compiler turns the loop’s captured writes into one set-based statement per batch — you get row-by-row logic at set-based cost.
3. Carried state: a running balance
The pattern SQL struggles with most: each row’s result depends on the previous row’s result. Variables declared inDECLARE carry across
rows and across batches, and ORDER KEY guarantees the order they
carry in.
Tables
balance.sumatra
50.00, 30.00, 0.00 (the clamp
caught the overdraft); customer 200’s as 10.00, 25.00.
What to notice:
ORDER KEY (customer_id, entry_id)makes each customer’s rows contiguous and ordered — which is exactly what letsrunningandlast_customercarry correctly, even across batch boundaries.- The per-customer reset is runtime logic (
IFon a comparison with carried state), not a window-function trick. BATCH SIZE 50000bounds memory; the answer is identical at any batch size — that invariance is a language guarantee.
4. Runtime-routed writes: insert, update, or delete per row
A per-row branch choosesDELETE, UPDATE, or INSERT based on a
value the data produced. Compile-time templating cannot express this;
in Sumatra it is just an IF.
Tables
apply_changes.sumatra
- All three writes target one table, keyed on
customer_id; the compiler captures whichever branch fired for each row and applies the whole batch’s mixed changes as one set-based statement. - Inside a row loop,
INSERTusesVALUES— per-row inserts are captured and batched like the other writes. - Writing the same key twice in one batch is a program error surfaced loudly at run time — never a silent last-write-wins.
5. Parameters: one program, many runs
PARAM declares program inputs; values arrive on the command line, in
declaration order, and bind into the read’s SQL as real query
parameters.
Tables
region_rollup.sumatra
- A parameter is referenced only inside SQL, as
PARAM('name'), and a non-text parameter needs a cast at the point of use. BATCH SIZE ALLruns the aggregating read as one whole-set batch — the right grain for aGROUP BYsource — and, because the body is order-independent, noORDER KEYis needed.- There is no row loop here: the statements reference
batchas a set, so the whole body runs server-side. The update-then-insert pair is the idiomatic upsert — accumulate existing keys, insert the complement.
6. The incremental watermark: a program that parameterizes itself
Every declared variable is visible to the program’s own SQL asPARAM('name') — so a program can compute a cursor from its target and
filter its read by it. The result is an incremental job with no
command-line arguments at all: every run rolls forward exactly the
days that arrived since the last run.
Tables
watermark_rollup.sumatra
PARAM('wm')inside the read refers to the declared variable — every declared scalar publishes into every server statement’s bind list under its own name, per execution. A non-text value reads back through a cast, exactly like an argument.- The watermark is derived from the target itself (
MAX(day)), so the job needs no state file, no scheduler-managed cursor, and no arguments: re-running it is always safe and always incremental. - A variable that is unset or NULL at the moment a statement runs is
absent from that statement’s binds — referencing it then is the
engine’s own loud error. The
IF wm IS NULLguard makes the first run explicit instead.
7. Exceptions: fail safe, keep the checkpoint
Arithmetic on real data can fault: a zero divisor, an overflow. Rather than guard every row, this program handles faults at the program level and leans on per-batch commits for recovery.Tables
score.sumatra
- The
UPDATEhere sets two named columns rather thanSET ROW— a sparse update. Only those columns are written; everything else is untouched, and the read projects just the columns it needs. - On a fault,
STOPdiscards only the in-flight batch and exits with code2. Already-committed batches remain a valid prefix — and because processed rows are flaggedscored = TRUE, re-running the same program picks up exactly where it stopped. WHEN OTHERS THEN RAISEis the recommended tail: anticipated faults stop gracefully, everything else stays loud.
8. A pipeline: the whole pass, once per slice
A counted loop at the top level repeats an entire read → compute → write pass — here, settling each month in its own transaction. The loop variable is visible to the read’s SQL asPARAM('m').
Tables
settle_year.sumatra
- The pipeline body holds a whole read block — legal precisely because the loop is at the top level; the same loop inside a row loop would be in-memory compute, where a read is a compile error.
- Each iteration’s read binds the current
m(published like any variable), so twelve passes each see only their own month’s rows — and a month with nothing pending is a clean zero-row no-op. - The bound is the program’s own (
1 .. 12), never a per-row server call: twelve read-compute-write passes, each checkpointing per batch. rec.pass := muses the loop variable in compute — it is an ordinary read-onlyINTthere. AWHILEpipeline works the same way, its predicate over carried variables a lookup can refresh — “repeat until the data says done”.
9. Dynamic table names: one program, many targets
A%TABLE parameter makes the table name a program input — the same
logic run against interchangeable, shape-identical tables.
Tables
flag_region.sumatra
- The declaration names a reference table (
sumatra_region_us) — the program compiles against that shape; the run supplies the actual name, and a table whose shape drifted from the reference is refused at run start. - The runtime name must be a plain identifier (at most 128 bytes) — quotes, spaces, and punctuation are unspellable, so a dynamic name can only ever be what a static name could have been.
- The name parameter works in the read’s
FROMand as the captured write’s target — the two places a target table is named.
Where to go deeper
- Programs and batches — the rules behind read blocks, write routing, and commit grains.
- Types, expressions, and control flow — the full language reference, including records and keyed collections.
- Exceptions and recovery — the fault model in detail.
- Stored procedures — turn any of these programs into a named, schedulable procedure.
