Skip to main content
Every program on this page is complete and runnable as written, and each comes from — or directly mirrors — the toolchain’s live-verified example corpus. Each example lists the tables it needs — create them (with a few sample rows) in your Firebolt database, save the program to a file, and run it with 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 a COMMIT — no variables, no loops. Sumatra runs these server-side exactly as written.
Tables
expire.sumatra
What to notice:
  • A body can hold plain UPDATE/DELETE/INSERT ... SELECT statements 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
What to notice:
  • 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 = rec writes 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 in DECLARE carry across rows and across batches, and ORDER KEY guarantees the order they carry in.
Tables
balance.sumatra
Customer 100’s balances land as 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 lets running and last_customer carry correctly, even across batch boundaries.
  • The per-customer reset is runtime logic (IF on a comparison with carried state), not a window-function trick.
  • BATCH SIZE 50000 bounds 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 chooses DELETE, 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
What to notice:
  • 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, INSERT uses VALUES — 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
What to notice:
  • 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 ALL runs the aggregating read as one whole-set batch — the right grain for a GROUP BY source — and, because the body is order-independent, no ORDER KEY is needed.
  • There is no row loop here: the statements reference batch as 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 as PARAM('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
What to notice:
  • 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 NULL guard 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
What to notice:
  • The UPDATE here sets two named columns rather than SET 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, STOP discards only the in-flight batch and exits with code 2. Already-committed batches remain a valid prefix — and because processed rows are flagged scored = TRUE, re-running the same program picks up exactly where it stopped.
  • WHEN OTHERS THEN RAISE is 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 as PARAM('m').
Tables
settle_year.sumatra
What to notice:
  • 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 := m uses the loop variable in compute — it is an ordinary read-only INT there. A WHILE pipeline 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
What to notice:
  • 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 FROM and as the captured write’s target — the two places a target table is named.

Where to go deeper