Skip to main content
Sumatra is a procedural language for the database your data lives in. You write row-by-row logic — loops, variables, conditionals, exception handlers — around plain SQL reads and writes, and the toolchain compiles the program into a native binary that runs it against your database, batching reads and routing every write to the lightest correct SQL form automatically. The language is engine-neutral by design. Firebolt is its first supported backend — and the concrete engine behind every example in this documentation — but nothing in the language is Firebolt-specific: the backend is declared per-database by the connection string, and the program’s SQL surface is whatever the connected engine speaks. Here is a complete Sumatra program. It computes a per-customer running balance — each row’s result depends on the previous row’s result, the kind of logic SQL alone expresses only awkwardly:
balance.sumatra
No connection handling, no pagination code, no staging tables, no 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.
If you know PL/SQL, the only real shift is this: the grain is a batch, not a 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 familiar PL/SQL: 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

The sumatra 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 MERGE per batch. You never write MERGE or manage staging tables yourself.
Programs run one-shot from a file (@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 (today, Firebolt’s), 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 KEY fixes a strict total order, state carries across batch boundaries, and the answer is invariant under BATCH 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

  1. Why Sumatra — the case for in-database transformation, and the batch and merge engines in depth.
  2. Quickstart — install, connect, and run your first two programs.
  3. Learning by example — small annotated programs covering the everyday shapes.
  4. Programs and batches — the program skeleton, read blocks, write routing, and commit grains.
  5. Types, expressions, and control flow — the language reference.
  6. Exceptions and recovery · Stored procedures · Built-in functions · The sumatra shell · Limitations