Language reference
Struct Construction ({ ... })
Syntax
{ field: expr, ... }
{ name } -- shorthand for { name: name }
π id, { name, age } → person (Users)
π id, { full: name } → person (Users)
Description
Struct construction builds a nested object value inside a projection — bundling several columns into one structured column. Instead of flat name and age columns you can produce a single person column holding { name, age }. It is how you reshape flat rows into nested, document-style output.
Technical Description
{ field: expr, … } builds a StructValue (StructConstruction operand); the shorthand { name } expands to { name: name }. It is parsed wherever an operand may appear: a projection, a grouping key, a predicate, a function argument. After IN, braces are a set literal instead. It evaluates to a nested struct value and is never pushed down to SQL. Its type is a StructType of its fields' own inferred types, computed to any depth: {base: salary, banded: salary * 1.1} infers as {base: N, banded: N} because both expressions infer as NUMBER.
Examples
Bundle columns into a struct (shorthand):
π id, { name, age } → person (Users)
PROJECT id, { name, age } -> person (Users)
Explicit field names:
π id, { full: name, years: age } → person (Users)
PROJECT id, { full: name, years: age } -> person (Users)
Combine with COLLECT to build an array of structs:
Built := { π customer, { order_id, amount } → o (Orders) };
query { γ customer, COLLECT(o) → orders (Built) };
Built := { PROJECT customer, { order_id, amount } -> o (Orders) };
query { GROUP customer, COLLECT(o) -> orders (Built) };
Worked Example
Flat columns folded into a struct, and read back out again.
People := [
| id | first | last | city |
|----|-------|-------|--------|
| 1 | Ada | Byron | London |
| 2 | Grace | Hop | Boston |
];
Nested := { π id, {first: first, last: last, home: {city: city}} → person (People) };
query { Nested };
People := [
| id | first | last | city |
|----|-------|-------|--------|
| 1 | Ada | Byron | London |
| 2 | Grace | Hop | Boston |
];
Nested := { PROJECT id, {first: first, last: last, home: {city: city}} -> person (People) };
query { Nested };
id person
── ──────────────────────────────
1 {first: Ada, last: Byron, hom…
2 {first: Grace, last: Hop, hom…
(2 rows)
What the engine did
Data flow
| id | first | last | city |
|---|---|---|---|
| 1 | Ada | Byron | London |
| 2 | Grace | Hop | Boston |
| id | person |
|---|---|
| 1 | {first: Ada, last: Byron, hom… |
| 2 | {first: Grace, last: Hop, hom… |
Rewrites applied
INLINE-001View body inlined into the referencing query view 'Nested' inlinedRENAME-002Unreferenced rename removed (ρ alias names nothing) rename removed — nothing references the alias Nested
Physical plan
Project ~2 rows
└─ Scan People ~2 rows
One column instead of three, and the engine knows its shape. Constructing a value does not put it beyond inference: each field takes the type of the expression that built it, to any depth. The catalog is where that claim can be read back rather than taken on trust:
query { π column, type (σ relation = "Nested" (relix.columns)) };
query { PROJECT column, type (SELECT relation = "Nested" (relix.columns)) };
column type
────── ──────────────────────────────
id N
person {first:S,last:S,home:{city:S}}
(2 rows)
What the engine did
Rewrites applied
PROJ-004Columns pruned (required-columns walk narrows the leaves) columns pruned at relix.columns: 3 of 6 read
Physical plan
Project ~2 rows
└─ Select ~2 rows
└─ Project ~6 rows
└─ Scan columns ~6 rows
person is a struct of three known field types, not ANY. That matters downstream, because a known shape is a shape the engine can check a field name against.
Reading a field is a dotted projection, and it nests as far as the data does:
query { π id, person.first → first, person.home.city → city (Nested) };
query { PROJECT id, person.first -> first, person.home.city -> city (Nested) };
id first city
── ───── ──────
1 Ada London
2 Grace Boston
(2 rows)
What the engine did
Data flow
| id | first | last | city |
|---|---|---|---|
| 1 | Ada | Byron | London |
| 2 | Grace | Hop | Boston |
| id | first | city |
|---|---|---|
| 1 | Ada | London |
| 2 | Grace | Boston |
Rewrites applied
INLINE-001View body inlined into the referencing query view 'Nested' inlinedRENAME-002Unreferenced rename removed (ρ alias names nothing) rename removed — nothing references the alias NestedPROJ-004Columns pruned (required-columns walk narrows the leaves) projection narrowed to the 1 of 2 column(s) read above it
Physical plan
Project ~2 rows
└─ Project ~2 rows
└─ Scan People ~2 rows
Without an alias the output column takes the field's name rather than the whole path, so π person.first produces a column called first.
A field reference works anywhere an ordinary column reference does — inside a predicate, a function call, an aggregate:
query { γ MAX(Len(person.first)) → longest (σ person.home.city = 'London' (Nested)) };
query { GROUP MAX(Len(person.first)) -> longest (SELECT person.home.city = 'London' (Nested)) };
longest
───────
3
(1 row)
What the engine did
Data flow
| id | first | last | city |
|---|---|---|---|
| 1 | Ada | Byron | London |
| 2 | Grace | Hop | Boston |
| longest |
|---|
| 3 |
Rewrites applied
INLINE-001View body inlined into the referencing query view 'Nested' inlinedRENAME-002Unreferenced rename removed (ρ alias names nothing) rename removed — nothing references the alias NestedSEL-003Selection pushed below projection selection pushed below projection
Physical plan
Aggregate ~1 rows
└─ Project ~1 rows
└─ Select ~1 rows
└─ Scan People ~2 rows
A field name the struct does not declare is an analysis error, not a surprise at run time — the same check a misspelled column gets.
Limitations
Produces a nested value that does not push down to SQL. A field whose expression is itself untyped (a path through an ANY column, say) contributes ANY to the struct's type — the shape is only as precise as what it is built from.
Alternatives
Array construction ([ … ]) for ordered lists rather than named fields. Keep columns flat if the consumer cannot handle nested output.
See Also
Notes
Because a struct is an operand, braces around a γ grouping key are not an error. γ {region}, SUM(amount) → total (Sales) groups by a one-field struct and returns one struct column called group in place of region. See group for the example.
Access a struct field downstream with dotted projection (e.g. π person.name) — worked through above. The same spelling is used for a relation qualifier above a join (π rooms.name), and that reading is tried first, so a struct column can never shadow one. A path whose last segment happens to name a column of the same relation still reads as a path: over a heading of (name, skills) where a skill has its own name, skills.name is the skill's, because no column named name originates from a relation called skills.