Files
plainleaf/client/space_lua/ast.ts
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

363 lines
7.6 KiB
TypeScript

type ASTContext = {
ctx: ASTCtx;
};
export type NumericType = "int" | "float";
export type ASTCtx = {
from?: number;
to?: number;
} & Record<string, any>;
export type LuaBlock = {
type: "Block";
statements: LuaStatement[];
hasLabel?: boolean;
hasGoto?: boolean;
// the first duplicated label is recorded here for later eval
dupLabelError?: { name: string; ctx: ASTCtx };
// this block declares top-level locals and therefore needs a new env
needsEnv?: boolean;
// true if this block itself (not nested ones) defines at least one label
hasLabelHere?: boolean;
// true if this block itself (not nested ones) may create to-be-closed values
hasCloseHere?: boolean;
// true if this block's subtree contains any function definition
hasFunctionDef?: boolean;
} & ASTContext;
// STATEMENTS
export type LuaReturnStatement = {
type: "Return";
expressions: LuaExpression[];
} & ASTContext;
export type LuaStatement =
| LuaSemicolonStatement
| LuaLabelStatement
| LuaBreakStatement
| LuaGotoStatement
| LuaReturnStatement
| LuaBlock
| LuaWhileStatement
| LuaRepeatStatement
| LuaIfStatement
| LuaForStatement
| LuaForInStatement
| LuaFunctionStatement
| LuaLocalFunctionStatement
| LuaAssignmentStatement
| LuaLocalStatement
| LuaFunctionCallStatement;
export type LuaSemicolonStatement = {
type: "Semicolon";
} & ASTContext;
export type LuaLabelStatement = {
type: "Label";
name: string;
} & ASTContext;
export type LuaBreakStatement = {
type: "Break";
} & ASTContext;
export type LuaGotoStatement = {
type: "Goto";
name: string;
} & ASTContext;
export type LuaWhileStatement = {
type: "While";
condition: LuaExpression;
block: LuaBlock;
} & ASTContext;
export type LuaRepeatStatement = {
type: "Repeat";
block: LuaBlock;
condition: LuaExpression;
} & ASTContext;
export type LuaIfStatement = {
type: "If";
conditions: { condition: LuaExpression; block: LuaBlock }[];
elseBlock?: LuaBlock;
} & ASTContext;
export type LuaForStatement = {
type: "For";
name: string;
start: LuaExpression;
end: LuaExpression;
step?: LuaExpression;
block: LuaBlock;
// function definition in the block captures the loop variable
capturesLoopVar?: boolean;
} & ASTContext;
export type LuaForInStatement = {
type: "ForIn";
names: string[];
expressions: LuaExpression[];
block: LuaBlock;
// function definition in the block captures any loop variable
capturesLoopVar?: boolean;
} & ASTContext;
export type LuaFunctionStatement = {
type: "Function";
name: LuaFunctionName;
body: LuaFunctionBody;
} & ASTContext;
export type LuaLocalFunctionStatement = {
type: "LocalFunction";
name: string;
body: LuaFunctionBody;
} & ASTContext;
export type LuaFunctionName = {
type: "FunctionName";
propNames: string[];
colonName?: string;
} & ASTContext;
export type LuaFunctionBody = {
type: "FunctionBody";
parameters: string[];
block: LuaBlock;
} & ASTContext;
export type LuaAssignmentStatement = {
type: "Assignment";
variables: LuaLValue[];
expressions: LuaExpression[];
} & ASTContext;
export type LuaLValue =
| LuaVariable
| LuaPropertyAccessExpression
| LuaTableAccessExpression;
export type LuaLocalStatement = {
type: "Local";
names: LuaAttName[];
expressions?: LuaExpression[];
} & ASTContext;
export enum LuaAttribute {
Const = "const",
Close = "close",
}
export type LuaAttName = {
type: "AttName";
name: string;
attribute?: string;
attributes?: LuaAttribute[];
} & ASTContext;
export type LuaFunctionCallStatement = {
type: "FunctionCallStatement";
call: LuaFunctionCallExpression;
} & ASTContext;
// EXPRESSIONS
export type LuaExpression =
| LuaNilLiteral
| LuaBooleanLiteral
| LuaNumberLiteral
| LuaStringLiteral
| LuaPrefixExpression
| LuaBinaryExpression
| LuaUnaryExpression
| LuaTableConstructor
| LuaFunctionDefinition
| LuaQueryExpression
| LuaFilteredCallExpression
| LuaAggregateCallExpression;
export type LuaNilLiteral = {
type: "Nil";
} & ASTContext;
export type LuaBooleanLiteral = {
type: "Boolean";
value: boolean;
} & ASTContext;
export type LuaNumberLiteral = {
type: "Number";
value: number;
numericType: NumericType;
} & ASTContext;
export type LuaStringLiteral = {
type: "String";
value: string;
} & ASTContext;
export type LuaPrefixExpression =
| LuaVariableExpression
| LuaParenthesizedExpression
| LuaFunctionCallExpression;
export type LuaParenthesizedExpression = {
type: "Parenthesized";
expression: LuaExpression;
} & ASTContext;
export type LuaVariableExpression =
| LuaVariable
| LuaPropertyAccessExpression
| LuaTableAccessExpression;
export type LuaVariable = {
type: "Variable";
name: string;
} & ASTContext;
export type LuaPropertyAccessExpression = {
type: "PropertyAccess";
object: LuaPrefixExpression;
property: string;
} & ASTContext;
export type LuaTableAccessExpression = {
type: "TableAccess";
object: LuaPrefixExpression;
key: LuaExpression;
} & ASTContext;
export type LuaFunctionCallExpression = {
type: "FunctionCall";
prefix: LuaPrefixExpression;
name?: string;
args: LuaExpression[];
orderBy?: LuaOrderBy[];
} & ASTContext;
export type LuaBinaryExpression = {
type: "Binary";
operator: string;
left: LuaExpression;
right: LuaExpression;
} & ASTContext;
export type LuaUnaryExpression = {
type: "Unary";
operator: string;
argument: LuaExpression;
} & ASTContext;
export type LuaTableConstructor = {
type: "TableConstructor";
fields: LuaTableField[];
} & ASTContext;
export type LuaTableField = LuaDynamicField | LuaPropField | LuaExpressionField;
export type LuaDynamicField = {
type: "DynamicField";
key: LuaExpression;
value: LuaExpression;
} & ASTContext;
export type LuaPropField = {
type: "PropField";
key: string;
value: LuaExpression;
} & ASTContext;
export type LuaExpressionField = {
type: "ExpressionField";
value: LuaExpression;
} & ASTContext;
export type LuaFunctionDefinition = {
type: "FunctionDefinition";
body: LuaFunctionBody;
} & ASTContext;
// Aggregate with per-row filter
export type LuaFilteredCallExpression = {
type: "FilteredCall";
call: LuaFunctionCallExpression;
filter: LuaExpression;
} & ASTContext;
// Aggregate call with intra-aggregate order by
export type LuaAggregateCallExpression = {
type: "AggregateCall";
call: LuaFunctionCallExpression;
orderBy: LuaOrderBy[];
} & ASTContext;
// Query stuff
export type LuaQueryExpression = {
type: "Query";
clauses: LuaQueryClause[];
} & ASTContext;
export type LuaQueryClause =
| LuaFromClause
| LuaWhereClause
| LuaLimitClause
| LuaOffsetClause
| LuaOrderByClause
| LuaSelectClause
| LuaGroupByClause
| LuaHavingClause;
// Field list used by `from`, `select` and `group by` clauses
export type LuaFromClause = {
type: "From";
fields: LuaTableField[];
} & ASTContext;
export type LuaWhereClause = {
type: "Where";
expression: LuaExpression;
} & ASTContext;
export type LuaLimitClause = {
type: "Limit";
limit: LuaExpression;
offset?: LuaExpression;
} & ASTContext;
export type LuaOffsetClause = {
type: "Offset";
offset: LuaExpression;
} & ASTContext;
export type LuaOrderByClause = {
type: "OrderBy";
orderBy: LuaOrderBy[];
} & ASTContext;
export type LuaOrderBy = {
type: "Order";
expression: LuaExpression;
direction: "asc" | "desc";
nulls?: "first" | "last";
using?: string | LuaFunctionBody;
} & ASTContext;
export type LuaSelectClause = {
type: "Select";
fields: LuaTableField[];
} & ASTContext;
export type LuaGroupByClause = {
type: "GroupBy";
fields: LuaTableField[];
} & ASTContext;
export type LuaHavingClause = {
type: "Having";
expression: LuaExpression;
} & ASTContext;