balance.sumatra
MERGE statements — the compiler produces all of that. The program
reads in 50,000-row batches, carries running across every row and
every batch in order, and commits each batch as a durable checkpoint.
The problem it solves
Modern analytical engines — Firebolt among them — deliberately ship no stored procedures and no procedural SQL dialect. Set-based SQL covers enormous ground, but the moment a pipeline needs a step that carries state across rows, branches on a value the data just produced, or loops until a computed condition is met, that step has to leave the database: a Python script here, a Spark job there, dbt orchestrating around the gap, and data movement stitching it together. Sumatra collapses that split. The procedural mid-work is written in one small language and executes as batch-shaped SQL on the engine itself, next to the set-based steps it belongs with.
The full case — what staying in-database saves you operationally, and
what the batch and merge machinery buys you technically — is on
Why Sumatra.
The mental model
Every Sumatra program reduces to one shape: Read → Compute → Write- Read — pull rows from the engine in batches into memory. Never a per-row server query.
- Compute — arbitrary in-memory work: loops, branches, carried variables, record and collection types. No server round-trips.
- Write — push results back in batches. Never a per-row server write.
SELECTs and MERGEs, nothing else.
If you have written stored procedures in a classic procedural SQL
dialect, the only real shift is this: the grain is a batch of rows,
not a single row. Cursor-style row-at-a-time server access — natural
on OLTP databases — is pathological on a distributed analytical engine,
so Sumatra makes it inexpressible: the language’s grammar has no way to
write a per-row server call. In exchange, the batch machinery
(pagination, ordering, staging, write-back, commit grain) is entirely
the compiler’s job, driven by two declarations on the read:
ORDER KEY and BATCH SIZE.
Beyond that frame, the language is deliberately the familiar
stored-procedure idiom: DECLARE/BEGIN/END, := assignment,
IF/ELSIF, CASE, WHILE, FOR loops, records, %ROWTYPE, keyed
collections, EXCEPTION handlers, explicit COMMIT. A handful of new words cover the batch
boundary: BATCH SIZE, ORDER KEY, PARAM, STOP.
How programs run
Thesumatra shell compiles a program against the live catalog of
the connected database — the database itself is the schema — then
generates native code, builds it with g++ on your host, and runs it.
The compiled program makes its own connection from the same DSN file
and speaks batch-shaped SQL to the engine.
Writes are routed, not written by hand:
- A write that is already set-shaped (plain
UPDATE/DELETE/INSERT ... SELECT) runs server-side as-is — rows never leave the engine. - Per-row writes captured inside a loop are staged and applied as one
set-based
MERGEper batch. You never writeMERGEor manage staging tables yourself.
@prog.sumatra), or are stored in
the database as named procedures and run with
EXEC — compiled once, cached per host, and automatically recompiled
when the source, the toolchain, or the table schemas change.
Semantics you can trust
Sumatra’s answer for what a program means is deliberately boring: it means what the engine underneath means. Values, types, rounding, overflow, NULL handling — the in-memory evaluator is bit-exact to the bound engine’s own semantics (Firebolt’s, in these docs’ examples), so moving a computation between the read SQL and the procedural middle never changes the answer, and a program never invents behavior the engine does not have. Three properties are worth calling out:- Determinism. A compute program’s result is declared-order
deterministic:
ORDER KEYfixes a strict total order, state carries across batch boundaries, and the answer is invariant underBATCH SIZE. An order tie or a NULL key at runtime is a loud error, never a silent skip or duplicate. - Loud failure. Overflow, division by zero, and lossy surprises are errors, never silently wrong values. Committed batches always survive a fault as a durable prefix; recovery is explicit and yours.
- Honest rejection. A program the model cannot run well — a nested read, a per-row server op, an unbounded server-dependent loop — is rejected at compile time with a named reason. The failure mode is a compile error, not a production performance cliff.
Status at a glance
Sumatra is built by the Wirekite team and documented here, but it is a
standalone product: it is not part of the Wirekite replication
pipeline, is not invoked by Wirekite, and nothing in this section
depends on Wirekite in any way.
Reading order
The section is laid out like a book — read it front to back:- Why Sumatra — the case for in-database transformation, and the batch and merge engines in depth.
- Quickstart — install, connect, and run your first two programs.
- Types, expressions, and control flow — the language itself: variables, operators, conditionals, loops, records, and keyed collections.
- Programs and batches — Sumatra’s own layer on top: the program skeleton, batched reads, write routing, commit grains and transactions, then pipelines and dynamic table names.
- Exceptions and recovery · Stored procedures — faults, restarts, and shipping programs as named, scheduled procedures.
- Learning by example — nine complete programs, graded from the smallest possible to pipelines and dynamic targets.
- Reference: The sumatra shell · Built-in functions · Limitations.
