Relix

Language reference

Operator spellings (Unicode and ASCII)

Syntax

σ amount > 100 (Orders)        -- Unicode
SELECT amount > 100 (Orders)   -- ASCII, identical meaning

A ⋈ B
A JOIN B

Description

Every operator in Relix has two spellings: a Unicode glyph and an ASCII form. They are accepted in exactly the same places and mean exactly the same thing — the two forms parse to the same tree, so nothing downstream can tell which one you typed.

Use whichever suits where you are typing. The glyphs read like the algebra they come from and are what the engine prints back to you; the ASCII forms need no special keyboard and survive a terminal, an email, or a code review comment intact. A single expression may mix them freely.

Unary operators

UnicodeASCII keywordOperation
πPROJECTProjection
σSELECTSelection
ρRENAMERename
γGROUPAggregation
τSORTSort
λLIMITLimit
δDISTINCTDistinct
μUNNESTUnnest (array→rows)
ωWHYWhy (lineage as data)
∀FORALLUniversal quantification (∀ keys : P (R))

Joins

UnicodeASCIIOperation
⋈JOINNatural join
⨝><Theta join
⟕|><Left outer join
⟖><|Right outer join
⟗|><|Full outer join
⋉SEMISemi-join
▷ANTIAnti-join

Set operations

UnicodeASCIIOperation
×CROSSCartesian product
∪UNIONUnion
⊎UALLUnion all
⊔OUNIONOuter-union (heterogeneous merge)
−DIFFSet difference
∆SYMDIFFSymmetric difference
∩INTERIntersection
÷DIVDivision
∘COMPOSEComposition

Logical and comparison operators

UnicodeASCIIOperation
∧ANDLogical and
∨ORLogical or
¬NOTLogical not
≠!=, <>Not equal
≤<=Less or equal
≥>=Greater or equal

Special forms

UnicodeASCIIOperation
→->Alias arrow (aggregation/projection)
↔<->Undirected edge, between a graph operator's two endpoint columns
⊥NULLNull check
∈INElement of
∉NOT INNot element of (two tokens)
⁺CLOSURETransitive closure, written after its relation: E⁺ OVER (a, b) is CLOSURE a, b (E)
*RCLOSUREReflexive-transitive closure, written after its relation: E* OVER (a, b) is RCLOSURE a, b (E)
~MATCHESRegular-expression match
!~NOT MATCHESRegular-expression non-match
~*, !~*—Regular-expression match and non-match, ignoring case

Nullary relation literals

The two relations with the empty (zero-column) heading, written where a relation goes. They have no glyph: like TRUE and FALSE they are literals rather than operators.

KeywordAliasMeaning
UNITDEEEmpty heading, one (empty) tuple — TRUE; the identity of ×
EMPTYDUMEmpty heading, no tuple — FALSE

Operators with no glyph

These operators are written only as words. They have no Unicode form, so there is no second spelling to choose: the word is the operator.

KeywordAliasOperation
USEMI—Universal semi-join: left rows that match every right row
ASOF—As-of join: match each row to the nearest-in-time row
IJOIN—Interval join: match intervals by an Allen relation
LATERAL—Lateral join: evaluate a function call or a relation per left row

SQL-prior aliases

Extra synonyms for the canonical forms above, for readers arriving from SQL. They parse to the identical tree; the canonical form is what gets printed back.

SQL-prior formCanonical formAST
INTERSECTINTER / ∩IntersectionNode
MINUS, EXCEPTDIFF / −DifferenceNode
x IS NULL / x IS NOT NULLx = NULL / x ≠ NULL (⊥)Null predicate
x BETWEEN a AND bx >= a ∧ x <= btwo comparisons
x NOT BETWEEN a AND bx < a ∨ x > btwo comparisons
x ILIKE p / x NOT ILIKE pLOWER(x) LIKE LOWER(p) / LOWER(x) NOT LIKE LOWER(p)Pattern predicate
a IS DISTINCT FROM b(a ≠ b ∧ a ≠ ⊥ ∧ b ≠ ⊥) ∨ (a = ⊥ ∧ b ≠ ⊥) ∨ (a ≠ ⊥ ∧ b = ⊥)null tests and a comparison
a IS NOT DISTINCT FROM b(a = b ∧ a ≠ ⊥ ∧ b ≠ ⊥) ∨ (a = ⊥ ∧ b = ⊥)null tests and a comparison
b IS TRUE / b IS NOT TRUEb = TRUE ∧ b ≠ ⊥ / b = FALSE ∨ b = ⊥a comparison and a null test
b IS UNKNOWNb = ⊥Null predicate
(a, b) = (1, 2)a = 1 ∧ b = 2comparisons
(a, b) < (1, 2)a < 1 ∨ (a = 1 ∧ b < 2)comparisons
(a, b) IN ((1, 2), (3, 4))(a = 1 ∧ b = 2) ∨ (a = 3 ∧ b = 4)comparisons
x IN (1, 2)x ∈ {1, 2}ElementOfPredicate
x > ALL (1, 2)x > 1 ∧ x > 2comparisons
x = ANY (1, 2), x = SOME (1, 2)x = 1 ∨ x = 2comparisons
ORDER, ORDER BYSORT / τSortNode
GROUP BYGROUP / γAggregationNode
LJOIN / RJOIN / FJOIN|>< / ><| / |><|outer joins
LIMIT 10 OFFSET 20, OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLYλ 20, 10LimitNode
CURRENT_TIMESTAMP, LOCALTIMESTAMPNOW()function call
COUNT(*)COUNT(1)AggregateFunction(COUNT, number 1)
a || ba + barithmetic +
x % yMod(x, y)function call
CASE WHEN x > 1 THEN a ELSE b ENDSwitch(x > 1, a, b)function call
CASE x WHEN 1 THEN a ENDSwitch(x = 1, a)function call
'it''s'"it's"string literal
1e6, 2.5E-31000000, 0.0025number literal

BY is an optional noise word after ORDER and GROUP. The MINUS keyword is set difference while the arithmetic - character is unchanged, so a - b and A MINUS B never collide.

|| binds more loosely than +, as in PostgreSQL, and % as tightly as *. Since || is +, it joins two strings and adds two numbers. A doubled quote and an exponent are read in the lexer, so the tree holds the string and the number they stand for.

COUNT(*) is SQL's row-count spelling: a literal argument is never NULL, so counting it counts every row, where COUNT(expr) counts non-NULL values. * is special only as the sole argument to COUNT — SUM(*) is a syntax error and COUNT(a * b) is still multiplication.

Technical Description

The choice of spelling is made in the lexer and is gone by the time a tree exists: σ and SELECT produce the same token, so no phase above the lexer — inference, validation, optimisation, planning, execution — can distinguish them. Two scripts differing only in spelling optimise to the same plan and push down the same query.

A tree rendered back to text — by a session's own report, or by the pretty-printer behind it — comes out in a canonical form: by default the glyph for an operator that has one, and the canonical keyword for an alias. A script written in ASCII and printed back therefore comes back in Unicode, and a script written with INTERSECT comes back with ∩. A printer asked for the ASCII spelling writes the keyword in this table's second column instead — SELECT, INTER — and nothing else changes. The round trip preserves the tree, not the typing.

The ASCII forms are ordinary keywords, which is why a relation or column whose name collides with one needs backticks — see the delimited identifier page. The glyphs are not keywords and never collide, so σ is always the operator and a relation may be called select as long as it is written `select`.

Examples

The same filter, both ways:

Adults := { σ age > 18 (Users) };
AdultsAscii := { SELECT age > 18 (Users) };

A natural join, both ways:

Joined := { A ⋈ B };
JoinedAscii := { A JOIN B };

Mixing the two in one expression is allowed, and is what most scripts end up doing — the glyph for the operator, a word for the connective:

ActiveAdults := { SELECT age > 18 ∧ active = "yes" (Users) };

An outer join written in ASCII, with an explicit condition:

Everyone := { Users |><| Users.id = Orders.user_id Orders };

The SQL-prior aliases, which read naturally to someone arriving from SQL:

ByRegion := { GROUP BY region, SUM(amount) → revenue (Orders) };
Placed := { σ shipped_at IS NULL (Orders) };

Limitations

∉ is two tokens in its ASCII form (NOT IN) where the glyph is one. This makes no difference to what parses, but it is the one row of the table where the ASCII column is not a single word.

There is no ASCII spelling that is only ASCII: every form in the left column of these tables is accepted everywhere, so a script cannot be made to reject glyphs. A team that wants ASCII-only sources enforces that in review, not in the grammar.

Aggregate names (SUM, COUNT, MIN, …) have no glyph. They are function names rather than operators, so they are written the same way whichever spelling the surrounding expression uses.

See Also

  • grammar (EBNF) — every spelling on this page in its place in the whole language, one rule at a time
  • comments — the other lexical rule worth knowing before writing a script, and the one place // will surprise you
  • delimited identifier — backticking a name that collides with one of the ASCII keywords above
  • truth relations — the full page for UNIT and EMPTY