Skip to main content
Every program on this page is complete and runnable as written, and each comes from 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. 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.

Where to go deeper