Files
plainleaf/client/space_lua/lua.grammar
T
5996e51c6f [LIQ] Unify from, select, and group by clauses to use field lists, implement multi-source cross-join (#1909)
* Website: Make manual build ref configurable

* [LIQ] Unify `from`, `select`, and `group by` clauses to use field lists

Query clauses `from`, `select`, and `group by` now accept the same field
syntax as Lua table constructors (`name = expr`, bare expr, or `[expr]
= expr`), giving them a consistent grammar and enabling named bindings
everywhere. #feature

What changed:

* New `FieldList` rule in `lua.grammar`. All three clauses now parse
  their arguments through it instead of using expression lists or the
  special `Name "=" exp` pattern.

* AST types `LuaFromClause`, `LuaSelectClause`, and `LuaGroupByClause`
  now carry fields: `LuaTableField[]` instead of a single expression or
  an expression array. The parser, static analysis helpers and the
  evaluator are all updated to work with this new shape.

Named bindings in `select`
--------------------------
`select name, age` now derives column names from bare variable or
property access expressions so the result table has string keys (`name`,
`age`) rather than integer indices. Explicit naming still works.

Named bindings in `group by`
----------------------------
`group b`y fields can carry explicit aliases that propagate into the
post-grouping scope. For example:

```sql
group by
  n = name
select {
  label = n
}
```

The alias `n` is now visible in `select`, `having`, and `order by`.
Previously only the raw expression form (`name`) was bound and an
explicit alias was silently lost. #bugfix

A new `LuaGroupByEntry` type pairs each `group by` expression with its
optional alias. The environment builder binds every
declared alias to the corresponding key value, regardless of whether the
group key is a scalar or a multi-key table.

Named bindings in `from`
------------------------
`from p = page` continues to work as before. The clause now also accepts
the full field-list syntax which lays the groundwork for multi-source
from (cross join) in a future change. #todo #feature

Complete LIQ syntax now
-----------------------
Complete LIQ syntax (in Postgres documentation style) could be now
roughly expressed as follows: #todo #documentation

```text
query[[ clause [...] ]]

where clause is one of:

    FROM field [, ...]
    WHERE condition
    GROUP BY field [, ...]
    HAVING condition
    SELECT field [, ...]
    ORDER BY sort_key [, ...]
    LIMIT count [, offset]
    OFFSET start

where field is one of:

    expression
    name = expression
    [expression] = expression

where sort_key is:

    expression [ ASC | DESC ]
               [ NULLS { FIRST | LAST } ]
               [ USING name ]
               [ USING FUNCTION function_body ]

where expression is any valid Lua expression, including:

    aggregate_call
    aggregate_call FILTER ( WHERE condition )
```

Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>

* Fix type error caused by earlier merge conflict resolution

Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>

* [LIQ] Cross-join support for multi-source `from`

Examples of what works now:

```
${query [[
  from
    x = {   1,   2 },
    y = {  10,  20 },
    z = { 100, 200 }
  select {
    sum = x + y + z
  }
]]}
```

Or (more complicated three-way cross-join) to illustrate:

```
${query [[
  from
    s = {
      { id = 1, name = 'Eva',  },
      { id = 2, name = 'Adam', },
      { id = 3, name = 'John', },
      { id = 4, name = 'Zef',  },
    },
    c = {
      { id = 101, title = 'Mathematics',      },
      { id = 102, title = 'Arts',             },
      { id = 103, title = 'Physics',          },
      { id = 104, title = 'Computer Science', },
      { id = 105, title = 'Literature',       },
    },
    e = {
      { sid = 1, cid = 101, },
      { sid = 1, cid = 103, },
      { sid = 2, cid = 101, },
      { sid = 2, cid = 102, },
      { sid = 2, cid = 103, },
      { sid = 3, cid = 102, },
      { sid = 3, cid = 105, },
      { sid = 4, cid = 101, },
      { sid = 4, cid = 103, },
      { sid = 4, cid = 104, },
    }
  where
    s.id  == e.sid and
    e.cid == c.id
  order by
    s.name
  group by
    s.name
  having
    s.name:match('^A') or
    s.name:match('f$')
  select
    student = s.name:upper(),
    courses = string_agg(c.title, ', '
      order by c.title desc
    )
]]}
```

Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>

---------

Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
Co-authored-by: Zef Hemel <zef@zef.me>
2026-03-25 14:48:57 +01:00

222 lines
4.8 KiB
Plaintext

/* Based on: https://github.com/R167/lezer-lua */
@precedence {
call,
power @right,
prefix,
times @left,
plus @left,
concat @right,
shift @left,
bitand @left,
xor @left,
bitor @left,
compare @left,
and @left,
or @left
}
@top Chunk { Block }
kw<term> { @specialize[@name={term}]<identifier, term> }
ckw<term> { @extend[@name={term}]<identifier, term> }
list<term> { term ("," term)* }
Block { statement* ReturnStatement? }
ReturnStatement { kw<"return"> ExpList? ";"?}
@skip { newline | space | Comment }
statement[@isGroup=Statement] {
";" |
Label |
kw<"break"> |
Goto{ kw<"goto"> Name } |
Scope { kw<"do"> Block kw<"end"> } |
WhileStatement { kw<"while"> exp kw<"do"> Block kw<"end"> } |
RepeatStatement { kw<"repeat"> Block kw<"until"> exp } |
IfStatement |
ForStatement |
Function { kw<"function"> FuncName FuncBody } |
LocalFunction { kw<"local"> kw<"function"> Name FuncBody } |
Assign { VarList "=" ExpList } |
Local { kw<"local"> AttNameList ("=" ExpList)? } |
FunctionCall ~fcall
}
IfStatement {
kw<"if"> exp kw<"then"> Block
(kw<"elseif"> exp kw<"then"> Block)*
(kw<"else"> Block)?
kw<"end">
}
ForNumeric { Name "=" exp "," exp ("," exp)? }
ForGeneric { NameList kw<"in"> ExpList }
ForStatement {
kw<"for"> (ForNumeric | ForGeneric) kw<"do"> Block kw<"end">
}
FuncName { Name ("." Name)* (":" Name)? }
FuncBody { "(" ArgList ")" Block kw<"end"> }
NameList { list<Name> }
ExpList { list<exp> }
VarList { list<var> }
ArgList { (list<var | "...">)? }
AttNameList { list<AttName> }
AttName { Name Attrib }
Attrib { ( "<" Name ">" )? }
FieldList { field ("," field)* }
exp {
kw<"nil"> | kw<"true"> | kw<"false"> | "..." |
Number |
LiteralString |
prefixexp |
BinaryExpression |
UnaryExpression |
TableConstructor |
FunctionDef { kw<"function"> FuncBody } |
Query |
FilteredCall { FunctionCall ckw<"filter"> "(" ckw<"where"> exp ")" }
}
Query {
kw<"query"> "[[" QueryClause* "]]"
}
QueryClause {
FromClause |
WhereClause |
OrderByClause |
SelectClause |
LimitClause |
OffsetClause |
GroupByClause |
HavingClause
}
FromClause { ckw<"from"> FieldList }
WhereClause { ckw<"where"> exp }
LimitClause { ckw<"limit"> exp ("," exp)? }
OffsetClause { ckw<"offset"> exp }
OrderByClause { ckw<"order"> ckw<"by"> list<OrderBy> }
OrderBy { exp (ckw<"desc"> | ckw<"asc"> | kw<"using"> (Name | kw<"function"> FuncBody))? (ckw<"nulls"> (ckw<"first"> | ckw<"last">))? }
SelectClause { ckw<"select"> FieldList }
GroupByClause { ckw<"group"> ckw<"by"> FieldList }
HavingClause { ckw<"having"> exp }
AggOrderBy { ckw<"order"> ckw<"by"> list<OrderBy> }
field[@isGroup=Field] {
FieldDynamic { "[" exp "]" "=" exp } |
FieldProp { Name "=" exp } |
FieldExp { exp }
}
prefixexp {
var |
Parens { "(" exp ")" ~parens } |
FunctionCall ~fcall
}
FunctionCall { prefixexp (":" Name)? !call args }
args {
LiteralString |
TableConstructor |
funcParams[@dynamicPrecedence=1] { "(" list<exp>? AggOrderBy? ")" ~parens }
}
var {
Name | Property { (prefixexp "." Name) } | MemberExpression { (prefixexp "[" exp "]") }
}
Name { identifier }
Label { "::" Name "::" }
LiteralString { simpleString }
BinaryExpression {
exp !or kw<"or"> exp |
exp !and kw<"and"> exp |
exp !compare CompareOp exp |
exp !bitor BitOp{"|"} exp |
exp !bitand BitOp{"&"} exp |
exp !xor BitOp{"~"} exp |
exp !shift BitOp{"<<" | ">>"} exp |
exp !concat ".." exp |
exp !plus ArithOp{"+" | minus} exp |
exp !times ArithOp{"*" | "/" | "%" | "//"} exp |
exp !power ArithOp{"^"} exp
}
UnaryExpression {
!prefix kw<"not"> exp |
!prefix (ArithOp{minus} | BitOp{"~"} | LenOp{"#"}) exp
}
TableConstructor { "{" (field (fieldsep field)* fieldsep?)? "}" }
@tokens {
CompareOp { "<" | ">" | $[<>=~/!] "=" }
identifier { (std.asciiLetter | "_") (std.digit | std.asciiLetter | "_")* }
stringEscape {
"\\" ($[abfnrtz"'\\] | digit digit? digit?) |
"\\x" hex hex |
"\\u{" hex+ "}"
}
// Any sequence of characters except two consecutive ]]
longStringContent { (![\]] | $[\]] ![\]])* }
singleEqualStringContent {
(![\]] | $[\]] ![=] ![\]])* $[\]]* ![\]]*
}
doubleEqualStringContent {
(![\]] | $[\]] ![=] ![=] ![\]])* $[\]]* ![\]]*
}
simpleString {
"'" (stringEscape | ![\r\n\\'])* "'" |
'"' (stringEscape | ![\r\n\\"])* '"' |
'[[' longStringContent ']]' |
'[=[' singleEqualStringContent ']=]' |
'[==[' doubleEqualStringContent ']==]'
}
hex { $[0-9a-fA-F] }
digit { std.digit }
Number {
digit+ ("." digit+)? ($[eE] $[+\-]? digit+)? |
"0" $[xX] hex+ ("." hex+)? ($[pP] $[+\-]? digit+)?
}
Comment { "--" ![\n\r]* }
space { ($[ \t\f] | "\\" $[\n\r])+ }
newline { $[\n\r] | "\n\r" | "\r\n" }
"..."[@name=Ellipsis]
".."[@name=Concat]
@precedence { Comment, minus }
minus {"-"}
fieldsep { $[,;] }
"(" ")" "[" "]" "{" "}"
"." "," ";" ":" "::"
}