Types
Prefer the spellings
DOUBLE and REAL. Firebolt’s own FLOAT
keyword is a 64-bit alias, so writing FLOAT invites confusion about
width; the corpus convention is DOUBLE for 64-bit and REAL for
32-bit.INSERT ... SELECT FROM batch). Only the
scalar types above the line participate in compute — in-memory
expressions and variables. BYTEA and ARRAY cannot enter a row
loop’s in-memory record, and arithmetic on a carry-only type is a
compile error, not a silent coercion.
Literals
- Integer literals are typed by magnitude: a value that fits in 32 bits
is
INT, otherwiseBIGINT. Beyond 64 bits, write it in decimal form (9300000000000000000.0). Scientific notation is not accepted. - A literal with a decimal point is
NUMERICwith the written scale (4.00is NUMERIC scale 2). - Strings are single-quoted:
'shipped'. - Booleans are
TRUEandFALSE. - Dates and timestamps:
DATE '2026-07-01',TIMESTAMP '2026-07-01 12:30:00'. - A NULL that needs a type is written
CAST(NULL AS T).
Variables and assignment
Assignment is:=; plain = is SQL’s comparison and SET operator,
used only inside SQL statements.
2.5 → 2, 3.5 → 4).
Operators
Precedence is conventional (
OR < AND < NOT < comparison <
additive < multiplicative); parenthesize anything you would parenthesize
in SQL.
Inequality has two spellings —
<> and != — and they are one and the
same operator from the lexer on. A ! that is not part of != is not
Sumatra syntax. Chained comparison (a = b = c) is a parse error —
parenthesize what you mean.CASE expression selects a value and works anywhere a value
does:
Numeric semantics
Sumatra’s arithmetic is the bound engine’s arithmetic, bit for bit — Firebolt’s, in these docs’ examples:- Integers are width-checked at every
+ - *and every cast:INTmath staysINTand faults withOVERFLOWpast 32 bits; mixing widths promotes (INT * BIGINTisBIGINT). - Decimals follow the engine’s strict typing: the result scale of
+ - * /is the widest operand scale, precision grows to fit and is capped at 38, and exceeding it is a loud overflow. Multiplication and division are computed in a wide intermediate and rounded half-up to that scale. - Floats are IEEE 754 with no fused-multiply-add surprises. Mixing
DOUBLEwithNUMERICor integers promotes toDOUBLE. A result that would be NaN or Infinity faults instead. - Division by zero faults with
ZERO_DIVIDE— never NULL, never Infinity.
CASE arms, GREATEST, and
LEAST must share a type family. Mixing TEXT with a numeric or
temporal operand is a compile error asking for an explicit CAST,
where the engine alone would coerce at runtime and fault only on bad
data.
Casts that cross into or out of TEXT are not available in compute
position — do text parsing and formatting in the read’s SQL, where the
engine’s full conversion machinery applies.
NULL semantics
Sumatra uses SQL’s three-valued logic, unchanged:- NULL propagates through operators and functions:
NULL + 1is NULL,NULL || 'x'is NULL, comparisons with NULL are NULL. - Test with
IS NULL, or compare null-safely withIS [NOT] DISTINCT FROM. - A condition that evaluates to NULL is not TRUE: an
IFtakes its ELSE path, and a capture filter skips the row.
ORDER KEY columns (a
NULL key is a loud runtime error), keyed-collection keys (NULL never
matches; a NULL-key probe raises NO_DATA_FOUND), and merge-key
matching (a NULL key component never matches a target row).
Control flow
CONTINUE WHEN pred;(or bareCONTINUE;) skips to the next iteration.EXIT WHEN pred;(or bareEXIT;) leaves the innermost loop. Both also work at the batch-loop boundary itself — see stopping a batched read early.RETURN;ends the whole program cleanly (exit 0) from anywhere; writes persist only up to the lastCOMMITthat was reached.
END LOOP; — batch reads, row loops, counted
loops, and WHILE alike. What a counted or WHILE loop’s body may
contain depends on where the loop stands:
- Inside a row loop (compute position): the body is in-memory
compute — a read block or a
COMMITinside it is a compile error by design, so a per-row server operation stays unwriteable. - At the top level of the program: the loop is a pipeline — its
body takes the full top-level alphabet: whole read blocks, writes,
lookups, and its own per-iteration
COMMIT. See pipelines.
Records
Declare a record type explicitly, or derive one from a table. A record variable can be initialized with a positional literal —ev := (1001, 'shipped', 25.00, CURRENT_TIMESTAMP); — matched one-to-one
against its field list (exact arity, each element converted to its
field’s type, a bare NULL taking the field’s type); the literal form
exists only in a DECLARE initializer.
rec in FOR rec IN batch) are records too —
their fields are the SELECT’s columns, and they are assignable.
Collections
Sumatra’s collection is the keyed map: a whole table (or filtered subset) loaded into memory once and probed by key — the lookup-table pattern.INDEX BYnames the key column(s) of the element type — composite keys are supported (INDEX BY wh, sku), and the probe key can be a runtime-computed value, the one correlation no JOIN can express.- A probe on a missing key (or a NULL key) raises
NO_DATA_FOUND— handleable like any fault. - The map is a read-only lookup: there is no iteration over a
collection, and no
.COUNT-style methods. It is resident in memory, so load dimensions, not fact tables. - Key columns may be integer, decimal, text, date, or timestamp — not float.
SELECT ... INTO fetches
scalars directly; zero rows raises NO_DATA_FOUND, and more than one
row is a loud error:
IF/CASE arm, where it runs zero times or once), or directly in a
batch loop’s body — one re-fetch per batch. Inside a row, counted, or
WHILE loop it is rejected: that would be a per-iteration server call.
See lookups inside the batch loop.
Identifiers and names
- Identifiers are ASCII: letters, digits, underscore, not starting with a digit. Case is not significant; the convention is UPPERCASE keywords and lowercase identifiers.
- Table names are single bare identifiers — there is no
schema.tablequalification. A table or column whose name needs quoting in the engine can be reached by aliasing it inside the read’s SQL. - Reserved words cannot name variables, types, record fields,
parameters, or procedures, and the check is case-insensitive. Three
groups are reserved: every language keyword (including the type
names,
TRUE/FALSE,CURRENT_DATE/CURRENT_TIMESTAMP,STOP, the exception names, and the reserved-for-future wordsMERGE,FUNCTION,SCAN,FOLD,MAP,FILTER,COLLAPSE); the C++ keyword table, adopted wholesale and frozen (soclass,template,new, … are rejected — the error says “reserved word” either way); and any name starting with the__sumatra_prefix. The one deliberate non-reserved keyword isBATCH, so the canonicalFOR batch IN (...)idiom can name its loop variablebatch.
