sqllens 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -10
- package/README.md +95 -85
- package/THIRD-PARTY-NOTICES.md +70 -5
- package/dist/api.d.ts +5 -3
- package/dist/api.js +17 -4
- package/dist/bigquery/behavior.d.ts +2 -0
- package/dist/bigquery/behavior.js +20 -0
- package/dist/bigquery/dot-path.d.ts +0 -2
- package/dist/bigquery/dot-path.js +0 -1
- package/dist/bigquery/fold.d.ts +8 -0
- package/dist/bigquery/fold.js +39 -0
- package/dist/bigquery/index.d.ts +7 -0
- package/dist/bigquery/index.js +10 -0
- package/dist/{infer/bigquery.d.ts → bigquery/infer.d.ts} +2 -2
- package/dist/{infer/bigquery.js → bigquery/infer.js} +4 -3
- package/dist/bigquery/lower.js +35 -12
- package/dist/bigquery/signatures.generated.d.ts +6 -0
- package/dist/bigquery/signatures.generated.js +1068 -0
- package/dist/completion/atn-walk.d.ts +13 -2
- package/dist/completion/atn-walk.js +13 -10
- package/dist/completion/complete.d.ts +4 -3
- package/dist/completion/complete.js +101 -45
- package/dist/completion/config.js +66 -0
- package/dist/completion/jinja-slot.d.ts +24 -0
- package/dist/completion/jinja-slot.js +126 -0
- package/dist/completion/parser-factory.d.ts +13 -1
- package/dist/completion/parser-factory.js +48 -0
- package/dist/databricks/behavior.d.ts +2 -0
- package/dist/databricks/behavior.js +19 -0
- package/dist/databricks/fold.d.ts +8 -0
- package/dist/databricks/fold.js +27 -0
- package/dist/databricks/index.d.ts +7 -0
- package/dist/databricks/index.js +10 -0
- package/dist/databricks/infer.d.ts +6 -0
- package/dist/databricks/infer.js +638 -0
- package/dist/databricks/signatures.generated.d.ts +6 -0
- package/dist/databricks/signatures.generated.js +1745 -0
- package/dist/derived-dialects.js +19 -1
- package/dist/dialect-behavior/behavior.d.ts +26 -0
- package/dist/dialect-behavior/behavior.js +1 -0
- package/dist/dialect-behavior/carrier.d.ts +5 -0
- package/dist/dialect-behavior/carrier.js +5 -0
- package/dist/dialect-behavior/coerce-rules.d.ts +7 -0
- package/dist/dialect-behavior/coerce-rules.js +69 -0
- package/dist/dialect-behavior/public-fold.d.ts +6 -0
- package/dist/dialect-behavior/public-fold.js +9 -0
- package/dist/dialect-behavior/registry.d.ts +6 -0
- package/dist/dialect-behavior/registry.js +34 -0
- package/dist/dialect-symbols.js +22 -17
- package/dist/dialect.d.ts +2 -2
- package/dist/document/document.d.ts +1 -1
- package/dist/document/document.js +8 -7
- package/dist/duckdb/behavior.d.ts +2 -0
- package/dist/duckdb/behavior.js +21 -0
- package/dist/duckdb/fold.d.ts +8 -0
- package/dist/duckdb/fold.js +29 -0
- package/dist/duckdb/index.d.ts +7 -0
- package/dist/duckdb/index.js +10 -0
- package/dist/{infer/duckdb.d.ts → duckdb/infer.d.ts} +2 -2
- package/dist/{infer/duckdb.js → duckdb/infer.js} +4 -3
- package/dist/duckdb/lower.js +47 -15
- package/dist/duckdb/signatures.generated.d.ts +6 -0
- package/dist/duckdb/signatures.generated.js +1072 -0
- package/dist/generated/bigquery/GoogleSQLParser.js +0 -7060
- package/dist/generated/databricks/DatabricksParser.js +0 -4800
- package/dist/generated/duckdb/DuckdbParser.js +0 -9100
- package/dist/generated/minijinja/MinijinjaParser.js +0 -380
- package/dist/generated/mysql/MysqlLexer.js +7357 -0
- package/dist/generated/mysql/MysqlParser.js +78520 -0
- package/dist/generated/postgres/PostgresParser.js +0 -8530
- package/dist/generated/redshift/RedshiftParser.js +0 -10970
- package/dist/generated/snowflake/SnowflakeParser.js +0 -7320
- package/dist/generated/sqlite/SqliteLexer.js +945 -0
- package/dist/generated/sqlite/SqliteParser.js +14682 -0
- package/dist/generated/trino/TrinoParser.js +0 -3770
- package/dist/generated/tsql/TSqlParser.js +0 -8420
- package/dist/ident/fold.d.ts +24 -15
- package/dist/ident/fold.js +12 -145
- package/dist/index.d.ts +6 -2
- package/dist/index.js +14 -8
- package/dist/infer/functions.d.ts +22 -11
- package/dist/infer/functions.js +26 -888
- package/dist/infer/infer.js +16 -14
- package/dist/infer/nullability.js +3 -4
- package/dist/infer/types.d.ts +1 -1
- package/dist/infer/types.js +7 -7
- package/dist/ir/ir.d.ts +26 -17
- package/dist/ir/part-span.d.ts +16 -1
- package/dist/ir/part-span.js +38 -10
- package/dist/ir/span.js +0 -2
- package/dist/ir/walk.js +3 -3
- package/dist/lineage/hops.js +13 -10
- package/dist/lineage/lineage.js +10 -7
- package/dist/minijinja/apply-tags.d.ts +14 -7
- package/dist/minijinja/apply-tags.js +66 -121
- package/dist/minijinja/parse.js +6 -6
- package/dist/minijinja/tag-ast.d.ts +29 -36
- package/dist/minijinja/tag-ast.js +210 -90
- package/dist/mysql/behavior.d.ts +2 -0
- package/dist/mysql/behavior.js +21 -0
- package/dist/mysql/fold.d.ts +8 -0
- package/dist/mysql/fold.js +49 -0
- package/dist/mysql/index.d.ts +7 -0
- package/dist/mysql/index.js +10 -0
- package/dist/mysql/infer.d.ts +20 -0
- package/dist/mysql/infer.js +156 -0
- package/dist/mysql/lower.d.ts +13 -0
- package/dist/mysql/lower.js +1443 -0
- package/dist/mysql/parse.d.ts +10 -0
- package/dist/mysql/parse.js +70 -0
- package/dist/mysql/signatures.generated.d.ts +6 -0
- package/dist/mysql/signatures.generated.js +508 -0
- package/dist/postgres/behavior.d.ts +2 -0
- package/dist/postgres/behavior.js +19 -0
- package/dist/postgres/fold.d.ts +8 -0
- package/dist/postgres/fold.js +30 -0
- package/dist/postgres/index.d.ts +7 -0
- package/dist/postgres/index.js +10 -0
- package/dist/{infer/postgres.d.ts → postgres/infer.d.ts} +2 -2
- package/dist/{infer/postgres.js → postgres/infer.js} +4 -3
- package/dist/postgres/lower.js +2 -2
- package/dist/postgres/signatures.generated.d.ts +6 -0
- package/dist/postgres/signatures.generated.js +2973 -0
- package/dist/qualify/check-calls.js +80 -134
- package/dist/qualify/qualify.js +16 -14
- package/dist/qualify/schema-provider.js +2 -2
- package/dist/qualify/schema.js +3 -3
- package/dist/qualify/template-provider.d.ts +45 -12
- package/dist/qualify/template-provider.js +69 -38
- package/dist/redshift/behavior.d.ts +2 -0
- package/dist/redshift/behavior.js +19 -0
- package/dist/redshift/fold.d.ts +8 -0
- package/dist/redshift/fold.js +31 -0
- package/dist/redshift/index.d.ts +7 -0
- package/dist/redshift/index.js +10 -0
- package/dist/{infer/redshift.d.ts → redshift/infer.d.ts} +2 -2
- package/dist/{infer/redshift.js → redshift/infer.js} +4 -3
- package/dist/redshift/lower.js +2 -2
- package/dist/redshift/signatures.generated.d.ts +6 -0
- package/dist/redshift/signatures.generated.js +757 -0
- package/dist/references/references.js +17 -12
- package/dist/scope/like-pattern.d.ts +2 -0
- package/dist/scope/like-pattern.js +15 -0
- package/dist/scope/scope.d.ts +5 -5
- package/dist/scope/scope.js +48 -42
- package/dist/sema/resolve.js +17 -12
- package/dist/session.d.ts +2 -2
- package/dist/signature/signature.d.ts +14 -6
- package/dist/signature/signature.js +30 -22
- package/dist/signature/signatures.d.ts +14 -12
- package/dist/signature/signatures.js +42 -582
- package/dist/snowflake/behavior.d.ts +2 -0
- package/dist/snowflake/behavior.js +22 -0
- package/dist/snowflake/fold.d.ts +8 -0
- package/dist/snowflake/fold.js +25 -0
- package/dist/snowflake/index.d.ts +7 -0
- package/dist/snowflake/index.js +10 -0
- package/dist/{infer/snowflake.d.ts → snowflake/infer.d.ts} +2 -2
- package/dist/{infer/snowflake.js → snowflake/infer.js} +4 -3
- package/dist/snowflake/lower.js +59 -19
- package/dist/snowflake/signatures.generated.d.ts +6 -0
- package/dist/snowflake/signatures.generated.js +2080 -0
- package/dist/sqlite/behavior.d.ts +2 -0
- package/dist/sqlite/behavior.js +19 -0
- package/dist/sqlite/fold.d.ts +8 -0
- package/dist/sqlite/fold.js +41 -0
- package/dist/sqlite/index.d.ts +7 -0
- package/dist/sqlite/index.js +10 -0
- package/dist/sqlite/infer.d.ts +12 -0
- package/dist/sqlite/infer.js +122 -0
- package/dist/sqlite/lower.d.ts +11 -0
- package/dist/sqlite/lower.js +1093 -0
- package/dist/sqlite/parse.d.ts +10 -0
- package/dist/sqlite/parse.js +70 -0
- package/dist/sqlite/signatures.generated.d.ts +6 -0
- package/dist/sqlite/signatures.generated.js +277 -0
- package/dist/symbols/symbols.js +14 -12
- package/dist/token/classify.js +31 -0
- package/dist/token/tokenize.js +4 -0
- package/dist/trino/behavior.d.ts +2 -0
- package/dist/trino/behavior.js +21 -0
- package/dist/trino/fold.d.ts +8 -0
- package/dist/trino/fold.js +39 -0
- package/dist/trino/index.d.ts +7 -0
- package/dist/trino/index.js +10 -0
- package/dist/{infer/trino.d.ts → trino/infer.d.ts} +2 -2
- package/dist/{infer/trino.js → trino/infer.js} +4 -3
- package/dist/trino/lower.js +5 -5
- package/dist/trino/signatures.generated.d.ts +6 -0
- package/dist/trino/signatures.generated.js +968 -0
- package/dist/tsql/behavior.d.ts +2 -0
- package/dist/tsql/behavior.js +20 -0
- package/dist/tsql/fold.d.ts +8 -0
- package/dist/tsql/fold.js +34 -0
- package/dist/tsql/index.d.ts +7 -0
- package/dist/tsql/index.js +10 -0
- package/dist/tsql/infer.d.ts +16 -0
- package/dist/tsql/infer.js +289 -0
- package/dist/tsql/lower.js +4 -4
- package/dist/tsql/signatures.generated.d.ts +6 -0
- package/dist/tsql/signatures.generated.js +640 -0
- package/package.json +15 -11
- package/dist/generated/bigquery/GoogleSQLLexer.d.ts +0 -407
- package/dist/generated/bigquery/GoogleSQLParser.d.ts +0 -9558
- package/dist/generated/bigquery/GoogleSQLParserListener.d.ts +0 -7777
- package/dist/generated/bigquery/GoogleSQLParserListener.js +0 -7070
- package/dist/generated/databricks/DatabricksLexer.d.ts +0 -566
- package/dist/generated/databricks/DatabricksParser.d.ts +0 -7771
- package/dist/generated/databricks/DatabricksParserListener.d.ts +0 -5737
- package/dist/generated/databricks/DatabricksParserListener.js +0 -5256
- package/dist/generated/duckdb/DuckdbLexer.d.ts +0 -691
- package/dist/generated/duckdb/DuckdbParser.d.ts +0 -13932
- package/dist/generated/duckdb/DuckdbParserListener.d.ts +0 -10049
- package/dist/generated/duckdb/DuckdbParserListener.js +0 -9138
- package/dist/generated/minijinja/MinijinjaLexer.d.ts +0 -108
- package/dist/generated/minijinja/MinijinjaParser.d.ts +0 -604
- package/dist/generated/minijinja/MinijinjaParserListener.d.ts +0 -449
- package/dist/generated/minijinja/MinijinjaParserListener.js +0 -410
- package/dist/generated/postgres/PostgresLexer.d.ts +0 -663
- package/dist/generated/postgres/PostgresParser.d.ts +0 -12963
- package/dist/generated/postgres/PostgresParserListener.d.ts +0 -9408
- package/dist/generated/postgres/PostgresParserListener.js +0 -8554
- package/dist/generated/redshift/RedshiftLexer.d.ts +0 -954
- package/dist/generated/redshift/RedshiftParser.d.ts +0 -16939
- package/dist/generated/redshift/RedshiftParserListener.d.ts +0 -12092
- package/dist/generated/redshift/RedshiftParserListener.js +0 -10994
- package/dist/generated/snowflake/SnowflakeLexer.d.ts +0 -1046
- package/dist/generated/snowflake/SnowflakeParser.d.ts +0 -14196
- package/dist/generated/snowflake/SnowflakeParserListener.d.ts +0 -8063
- package/dist/generated/snowflake/SnowflakeParserListener.js +0 -7330
- package/dist/generated/trino/TrinoLexer.d.ts +0 -381
- package/dist/generated/trino/TrinoParser.d.ts +0 -5340
- package/dist/generated/trino/TrinoParserListener.d.ts +0 -4704
- package/dist/generated/trino/TrinoParserListener.js +0 -4326
- package/dist/generated/tsql/TSqlLexer.d.ts +0 -1278
- package/dist/generated/tsql/TSqlParser.d.ts +0 -17267
- package/dist/generated/tsql/TSqlParserListener.d.ts +0 -9697
- package/dist/generated/tsql/TSqlParserListener.js +0 -8854
- package/dist/infer/dialect.d.ts +0 -21
- package/dist/infer/dialect.js +0 -74
- package/dist/infer/literals.d.ts +0 -6
- package/dist/infer/literals.js +0 -44
- package/dist/signature/generated/tsql.d.ts +0 -3
- package/dist/signature/generated/tsql.js +0 -260
|
@@ -0,0 +1,1443 @@
|
|
|
1
|
+
import { ParserRuleContext, TerminalNode } from "antlr4ng";
|
|
2
|
+
import { MysqlParser as P } from "../generated/mysql/MysqlParser.js";
|
|
3
|
+
import { keywordCategory, swallowedCategories, swallowedStatements } from "../ir/statement.js";
|
|
4
|
+
import { collapsePartSpans, dotIdPartSpanOf, partSpanOf } from "../ir/part-span.js";
|
|
5
|
+
import { freezeIR } from "../ir/freeze.js";
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Lowering — MySQL (grammars-v4 sql/mysql/Positive-Technologies fork, Ivan
|
|
8
|
+
// Kochurkin's split MySqlLexer/MySqlParser) CST -> the shared, dialect-neutral
|
|
9
|
+
// IR (src/ir/ir.ts). The semantic layer runs on the IR unchanged; only this
|
|
10
|
+
// file knows MySQL's grammar. Core query path: selectStatement,
|
|
11
|
+
// querySpecification, tableSources/tableSourceItem, joinPart, selectElements,
|
|
12
|
+
// and the left-recursive expression -> predicate -> expressionAtom cascade.
|
|
13
|
+
//
|
|
14
|
+
// Navigation is by rule index against the generated parser, never by string
|
|
15
|
+
// comparison of rule names. Nested `selectStatement` nodes belong to their own
|
|
16
|
+
// scope, so shallow walks never descend into them.
|
|
17
|
+
//
|
|
18
|
+
// Two grammar realities shape this lowering (see the R3 report):
|
|
19
|
+
// 1. WITH/SELECT split — MySQL-PT models `WITH cte AS (...) SELECT ...` as a bare
|
|
20
|
+
// `withStatement` (the CTE clause only) immediately followed by its query. The
|
|
21
|
+
// batch rule requires a SEMI between every other statement pair, so this is the
|
|
22
|
+
// one no-semicolon adjacency, carried by statementItem's `withStatement
|
|
23
|
+
// sqlStatement` alternative; lowerImpl rejoins the two into one CTE query. Set
|
|
24
|
+
// operations are a flat linearized chain (UNION / EXCEPT / INTERSECT since the
|
|
25
|
+
// 8.0.19+ fork restructure); INTERSECT's higher precedence is realized in foldSetop.
|
|
26
|
+
// 2. Bare `LEFT`/`RIGHT` before JOIN now parse as an outer join, not a swallowed
|
|
27
|
+
// table alias — our fork removed the reserved words LEFT/RIGHT from the
|
|
28
|
+
// keyword-as-identifier path (upstream let them alias); joinPart carries the token.
|
|
29
|
+
//
|
|
30
|
+
// Identifier-delimiter contract (docs/identifier-delimiter-contract.md): MySQL
|
|
31
|
+
// follows the "kept" convention shared by every dialect except BigQuery — every
|
|
32
|
+
// identifier field (ColumnRef.parts / TableSource.name / TableSource.alias /
|
|
33
|
+
// CteDef.name / Projection.name) carries its RAW text with quoting delimiters
|
|
34
|
+
// intact. MySQL quotes are `backticks` and (in ANSI_QUOTES mode) "double
|
|
35
|
+
// quotes"; both lex to STRING_LITERAL and reach the `uid` rule, so getText()
|
|
36
|
+
// returns the delimiters verbatim. Case-folding / delimiter-stripping for
|
|
37
|
+
// identity/display happen downstream in src/ident/fold.ts, never here.
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// dev.mysql.com/doc/refman/8.0/en/aggregate-functions.html — used only for the `aggregate`
|
|
40
|
+
// heuristic on scalar-parsed calls; structural aggregates (aggregateWindowedFunction) set it directly.
|
|
41
|
+
const AGGREGATES = new Set([
|
|
42
|
+
"avg",
|
|
43
|
+
"bit_and",
|
|
44
|
+
"bit_or",
|
|
45
|
+
"bit_xor",
|
|
46
|
+
"count",
|
|
47
|
+
"group_concat",
|
|
48
|
+
"json_arrayagg",
|
|
49
|
+
"json_objectagg",
|
|
50
|
+
"max",
|
|
51
|
+
"min",
|
|
52
|
+
"std",
|
|
53
|
+
"stddev",
|
|
54
|
+
"stddev_pop",
|
|
55
|
+
"stddev_samp",
|
|
56
|
+
"sum",
|
|
57
|
+
"var_pop",
|
|
58
|
+
"var_samp",
|
|
59
|
+
"variance",
|
|
60
|
+
]);
|
|
61
|
+
/** The core query "primary" rules the union linearizer treats as branch cores. Beyond the four
|
|
62
|
+
* upstream select shapes: the 8.0.19+ query primaries (explicitTable / tableValueConstructor, i.e.
|
|
63
|
+
* `TABLE t` and `VALUES ROW(...)`) and their statement wrappers (tableStatement / valuesStatement,
|
|
64
|
+
* reachable as parenthesized operands `(TABLE t ORDER BY x)`). */
|
|
65
|
+
const CORE_RULES = new Set([
|
|
66
|
+
P.RULE_querySpecification,
|
|
67
|
+
P.RULE_querySpecificationNointo,
|
|
68
|
+
P.RULE_queryExpression,
|
|
69
|
+
P.RULE_queryExpressionNointo,
|
|
70
|
+
P.RULE_explicitTable,
|
|
71
|
+
P.RULE_tableValueConstructor,
|
|
72
|
+
P.RULE_tableStatement,
|
|
73
|
+
P.RULE_valuesStatement,
|
|
74
|
+
]);
|
|
75
|
+
/** Lower a parsed MySQL file (`root`: a `;`-separated batch of statements) into the IR. A single
|
|
76
|
+
* SELECT / VALUES / TABLE statement (plus the WITH/SELECT rejoin) lowers fully; anything else
|
|
77
|
+
* (DDL/DML/admin/utility, multi-statement batches) becomes a flagged non-query body — a valid
|
|
78
|
+
* parse never throws, and neither does broken/partial input. */
|
|
79
|
+
export function lower(tree) {
|
|
80
|
+
const q = lowerImpl(tree);
|
|
81
|
+
q.dialect = "mysql";
|
|
82
|
+
return freezeIR(q);
|
|
83
|
+
}
|
|
84
|
+
function lowerImpl(tree) {
|
|
85
|
+
const statementsNode = firstOfRule(tree, P.RULE_sqlStatements);
|
|
86
|
+
const stmts = statementsNode ? collectStatements(statementsNode) : [];
|
|
87
|
+
// Recovery-swallowed statements count toward batch-ness (a broken statement dumps the rest of the
|
|
88
|
+
// batch as flat error nodes, under-reporting the sqlStatement count).
|
|
89
|
+
const swallowed = swallowedStatements(tree);
|
|
90
|
+
// WITH/SELECT rejoin: MySQL-PT models `WITH cte AS (...) SELECT ...` as a bare withStatement (the CTE
|
|
91
|
+
// clause only) immediately followed by its query — the one no-semicolon statement adjacency real
|
|
92
|
+
// MySQL has, carried by statementItem's `withStatement sqlStatement` alternative. collectStatements
|
|
93
|
+
// surfaces it as the two adjacent statements stmts[0] (the withStatement) and stmts[1] (the query);
|
|
94
|
+
// merge them into one CTE query so the CTE resolves downstream. Because a SEMI is now REQUIRED between
|
|
95
|
+
// every other statement pair, this adjacency is unambiguous: `WITH ...; SELECT ...` (semicolon
|
|
96
|
+
// between — invalid MySQL, the WITH is left dangling) arrives instead as a lone withStatement + a
|
|
97
|
+
// separate statement (stmts[0] is a sqlStatement, not a bare withStatement), so it is NOT rejoined.
|
|
98
|
+
if (stmts.length === 2 && swallowed === 0) {
|
|
99
|
+
const withStmt = stmts[0].ruleIndex === P.RULE_withStatement ? stmts[0] : undefined;
|
|
100
|
+
const trailingDml = dmlOf(stmts[1]);
|
|
101
|
+
if (withStmt && trailingDml && isQueryDml(trailingDml)) {
|
|
102
|
+
const q = lowerQueryDml(trailingDml);
|
|
103
|
+
q.ctes = collectCteNodes(withStmt).map(lowerCteNode);
|
|
104
|
+
q.statement = "query";
|
|
105
|
+
return q;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const total = stmts.length + swallowed;
|
|
109
|
+
if (total !== 1 || stmts.length !== 1) {
|
|
110
|
+
// Anchor a multi-statement span to the FIRST statement, not the whole `root` container (which
|
|
111
|
+
// reaches EOF), so a downstream AST index read doesn't see a bogus enclosure over statements 2..n.
|
|
112
|
+
const cst = total > 1 && stmts.length > 0 ? stmts[0] : tree;
|
|
113
|
+
const q = nonQuery(cst, total > 1 ? "multi-statement" : total === 1 ? "broken" : "empty");
|
|
114
|
+
q.statement = statementCategory(tree);
|
|
115
|
+
return q;
|
|
116
|
+
}
|
|
117
|
+
const stmt = stmts[0];
|
|
118
|
+
const dml = dmlOf(stmt);
|
|
119
|
+
if (dml) {
|
|
120
|
+
const sel = directChildrenOfRule(dml, P.RULE_selectStatement)[0];
|
|
121
|
+
if (sel) {
|
|
122
|
+
const q = lowerSelectStatement(sel);
|
|
123
|
+
q.statement = "query";
|
|
124
|
+
return q;
|
|
125
|
+
}
|
|
126
|
+
const vs = directChildrenOfRule(dml, P.RULE_valuesStatement)[0];
|
|
127
|
+
if (vs) {
|
|
128
|
+
const q = lowerValuesQuery(vs);
|
|
129
|
+
q.statement = "query";
|
|
130
|
+
return q;
|
|
131
|
+
}
|
|
132
|
+
const tblStmt = directChildrenOfRule(dml, P.RULE_tableStatement)[0];
|
|
133
|
+
if (tblStmt) {
|
|
134
|
+
const q = buildTableQuery(tblStmt);
|
|
135
|
+
q.statement = "query";
|
|
136
|
+
return q;
|
|
137
|
+
}
|
|
138
|
+
const withStmt = directChildrenOfRule(dml, P.RULE_withStatement)[0];
|
|
139
|
+
if (withStmt) {
|
|
140
|
+
// A lone WITH with no trailing query captured (mid-edit / broken) — keep the CTEs, empty body.
|
|
141
|
+
const q = {
|
|
142
|
+
kind: "query",
|
|
143
|
+
ctes: collectCteNodes(withStmt).map(lowerCteNode),
|
|
144
|
+
body: emptyBody(withStmt),
|
|
145
|
+
cst: stmt,
|
|
146
|
+
};
|
|
147
|
+
q.statement = "query";
|
|
148
|
+
return q;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const q = nonQuery(stmt, "non-query");
|
|
152
|
+
q.statement = statementCategory(tree);
|
|
153
|
+
return q;
|
|
154
|
+
}
|
|
155
|
+
// --- statement categories ------------------------------------------------------
|
|
156
|
+
/** Per-statement categories for every `sqlStatement` in a parsed `root`, in source order — the
|
|
157
|
+
* file-level view behind statementCategory (which folds >1 into "compound"). Parity with the other
|
|
158
|
+
* dialects; feeds the corpus reclassifier. Note the WITH/SELECT split shows here as two "query"
|
|
159
|
+
* entries even though lowerImpl rejoins them into one query. */
|
|
160
|
+
export function statementCategories(tree) {
|
|
161
|
+
const statementsNode = firstOfRule(tree, P.RULE_sqlStatements);
|
|
162
|
+
const stmts = statementsNode ? collectStatements(statementsNode) : [];
|
|
163
|
+
return [...stmts.map(stmtCategory), ...swallowedCategories(tree)];
|
|
164
|
+
}
|
|
165
|
+
/** The batch's statement contexts, in source order, flattened out of the statementItem wrappers
|
|
166
|
+
* (sqlStatements is now a SEMI-separated list of statementItem — see the grammar). A `withStatement
|
|
167
|
+
* sqlStatement` item (MySQL's one no-semicolon adjacency: a CTE clause bound to its query) contributes
|
|
168
|
+
* BOTH nodes — the bare withStatement then its query — so a lone `WITH cte AS (...) SELECT ...` still
|
|
169
|
+
* arrives as the two adjacent statements the WITH/SELECT rejoin expects. A bare withStatement head
|
|
170
|
+
* categorizes as "query" via stmtCategory's keyword fallback, matching the split's prior two entries. */
|
|
171
|
+
function collectStatements(statementsNode) {
|
|
172
|
+
const out = [];
|
|
173
|
+
for (const item of directChildrenOfRule(statementsNode, P.RULE_statementItem)) {
|
|
174
|
+
const withHead = directChildrenOfRule(item, P.RULE_withStatement)[0];
|
|
175
|
+
if (withHead)
|
|
176
|
+
out.push(withHead);
|
|
177
|
+
const stmt = directChildrenOfRule(item, P.RULE_sqlStatement)[0];
|
|
178
|
+
if (stmt)
|
|
179
|
+
out.push(stmt);
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
function statementCategory(tree) {
|
|
184
|
+
const cats = statementCategories(tree);
|
|
185
|
+
if (cats.length === 0)
|
|
186
|
+
return "other";
|
|
187
|
+
if (cats.length > 1)
|
|
188
|
+
return "compound";
|
|
189
|
+
return cats[0];
|
|
190
|
+
}
|
|
191
|
+
/** MySQL's sqlStatement groups statement kinds behind the ddl/dml/transaction/administration/utility/
|
|
192
|
+
* replication/prepared meta-rules, so the big buckets are structural; admin/utility/replication/
|
|
193
|
+
* prepared refine by their leading keyword (GRANT→dcl, SET/SHOW→utility, …), mirroring snowflake. */
|
|
194
|
+
function stmtCategory(stmt) {
|
|
195
|
+
if (directChildrenOfRule(stmt, P.RULE_ddlStatement).length)
|
|
196
|
+
return "ddl";
|
|
197
|
+
const dml = dmlOf(stmt);
|
|
198
|
+
if (dml)
|
|
199
|
+
return dmlCategory(dml);
|
|
200
|
+
if (directChildrenOfRule(stmt, P.RULE_transactionStatement).length)
|
|
201
|
+
return "tcl";
|
|
202
|
+
if (directChildrenOfRule(stmt, P.RULE_administrationStatement).length ||
|
|
203
|
+
directChildrenOfRule(stmt, P.RULE_replicationStatement).length ||
|
|
204
|
+
directChildrenOfRule(stmt, P.RULE_preparedStatement).length) {
|
|
205
|
+
return keywordCategory(stmt.start?.text ?? "");
|
|
206
|
+
}
|
|
207
|
+
if (directChildrenOfRule(stmt, P.RULE_utilityStatement).length)
|
|
208
|
+
return "utility";
|
|
209
|
+
return keywordCategory(stmt.start?.text ?? "");
|
|
210
|
+
}
|
|
211
|
+
function dmlCategory(dml) {
|
|
212
|
+
if (isQueryDml(dml) || directChildrenOfRule(dml, P.RULE_withStatement).length)
|
|
213
|
+
return "query";
|
|
214
|
+
// CALL invokes a stored procedure — a utility, not a data write.
|
|
215
|
+
if (directChildrenOfRule(dml, P.RULE_callStatement).length)
|
|
216
|
+
return "utility";
|
|
217
|
+
// INSERT / UPDATE / DELETE / REPLACE / LOAD DATA / LOAD XML / DO / HANDLER → data movement.
|
|
218
|
+
return "dml";
|
|
219
|
+
}
|
|
220
|
+
function isQueryDml(dml) {
|
|
221
|
+
return (directChildrenOfRule(dml, P.RULE_selectStatement).length > 0 ||
|
|
222
|
+
directChildrenOfRule(dml, P.RULE_valuesStatement).length > 0 ||
|
|
223
|
+
directChildrenOfRule(dml, P.RULE_tableStatement).length > 0);
|
|
224
|
+
}
|
|
225
|
+
function dmlOf(stmt) {
|
|
226
|
+
return directChildrenOfRule(stmt, P.RULE_dmlStatement)[0];
|
|
227
|
+
}
|
|
228
|
+
function nonQuery(cst, reason) {
|
|
229
|
+
return { kind: "query", ctes: [], body: nonQuerySelect(cst, reason), cst };
|
|
230
|
+
}
|
|
231
|
+
function nonQuerySelect(cst, reason) {
|
|
232
|
+
return { kind: "select", projections: [], from: [], columns: [], aggregated: false, unsupported: [reason], cst };
|
|
233
|
+
}
|
|
234
|
+
// --- SELECT statement ----------------------------------------------------------
|
|
235
|
+
/** A dmlStatement that is a query primary (select / VALUES / TABLE) → a QueryExpr. */
|
|
236
|
+
function lowerQueryDml(dml) {
|
|
237
|
+
const sel = directChildrenOfRule(dml, P.RULE_selectStatement)[0];
|
|
238
|
+
if (sel)
|
|
239
|
+
return lowerSelectStatement(sel);
|
|
240
|
+
const vs = directChildrenOfRule(dml, P.RULE_valuesStatement)[0];
|
|
241
|
+
if (vs)
|
|
242
|
+
return lowerValuesQuery(vs);
|
|
243
|
+
const tblStmt = directChildrenOfRule(dml, P.RULE_tableStatement)[0];
|
|
244
|
+
if (tblStmt)
|
|
245
|
+
return buildTableQuery(tblStmt);
|
|
246
|
+
return { kind: "query", ctes: [], body: nonQuerySelect(dml, "non-query"), cst: dml };
|
|
247
|
+
}
|
|
248
|
+
/** selectStatement (all five labelled alternatives). A single querySpecification lowers to a select
|
|
249
|
+
* body; a UNION chain folds left into a setop body; the trailing ORDER BY / LIMIT hoist to the
|
|
250
|
+
* QueryExpr level (from the querySpecification for a single select, from the selectStatement for a
|
|
251
|
+
* union). */
|
|
252
|
+
function lowerSelectStatement(selectStmt) {
|
|
253
|
+
const { cores, ops } = collectUnion(selectStmt);
|
|
254
|
+
const body = cores.length > 1
|
|
255
|
+
? foldSetop(cores, ops, selectStmt)
|
|
256
|
+
: cores[0]
|
|
257
|
+
? buildCoreBody(cores[0])
|
|
258
|
+
: emptyBody(selectStmt);
|
|
259
|
+
// ORDER BY / LIMIT: at the selectStatement level (union case) or inside the single core.
|
|
260
|
+
const single = cores.length === 1 && cores[0] ? unwrapCore(cores[0]) : undefined;
|
|
261
|
+
const orderBy = extractOrderBy(selectStmt) ?? (single ? extractOrderBy(single) : undefined);
|
|
262
|
+
if (orderBy && body.kind !== "pipe")
|
|
263
|
+
for (const o of orderBy)
|
|
264
|
+
columnsOf(o, body.columns, "orderBy");
|
|
265
|
+
const limit = extractLimit(selectStmt) ?? (single ? extractLimit(single) : undefined);
|
|
266
|
+
return { kind: "query", ctes: [], body, orderBy, limit, cst: selectStmt };
|
|
267
|
+
}
|
|
268
|
+
function opKindOf(n) {
|
|
269
|
+
if (hasDirectToken(n, P.EXCEPT))
|
|
270
|
+
return "except";
|
|
271
|
+
if (hasDirectToken(n, P.INTERSECT))
|
|
272
|
+
return "intersect";
|
|
273
|
+
return "union";
|
|
274
|
+
}
|
|
275
|
+
/** Linearize the set-operation chain of a selectStatement (or a parenthesized query-expression core —
|
|
276
|
+
* buildCoreBody folds those as their own unit) into ordered branch cores + operator entries. The
|
|
277
|
+
* grammar nests the operator both directly under selectStatement AND as the trailing child of a
|
|
278
|
+
* querySpecificationNointo core, so ONLY the querySpecification(Nointo) cores are followed into a
|
|
279
|
+
* trailing operator (a parenthesized core's operators belong to ITS chain, never this level).
|
|
280
|
+
* unionSelect/unionParenthesisSelect additionally allow ONE trailing `(UNION|EXCEPT|INTERSECT)
|
|
281
|
+
* (ALL|DISTINCT)? (querySpecification|queryExpression)` into-tail arm whose operator/ALL are LOOSE
|
|
282
|
+
* direct tokens of selectStatement and whose branch is a bare direct core — collected here as a
|
|
283
|
+
* branch of its own (previously silently dropped; B-R3 review finding). */
|
|
284
|
+
function collectUnion(selectStmt) {
|
|
285
|
+
const cores = [];
|
|
286
|
+
const ops = [];
|
|
287
|
+
const isOp = (n) => n.ruleIndex === P.RULE_unionStatement || n.ruleIndex === P.RULE_unionParenthesis;
|
|
288
|
+
const processCore = (core) => {
|
|
289
|
+
cores.push(core);
|
|
290
|
+
// Only querySpecification(Nointo) may carry a trailing set operator as a DIRECT child.
|
|
291
|
+
if (core.ruleIndex === P.RULE_querySpecification || core.ruleIndex === P.RULE_querySpecificationNointo) {
|
|
292
|
+
for (const c of kidsOf(core))
|
|
293
|
+
if (c instanceof ParserRuleContext && isOp(c))
|
|
294
|
+
processOp(c);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
const processOp = (op) => {
|
|
298
|
+
ops.push({ op: opKindOf(op), all: hasDirectToken(op, P.ALL), cst: op });
|
|
299
|
+
const core = firstCoreChild(op);
|
|
300
|
+
if (core)
|
|
301
|
+
processCore(core);
|
|
302
|
+
};
|
|
303
|
+
let pendingAll = false; // the loose ALL token preceding the trailing into-tail branch
|
|
304
|
+
let pendingOp = "union"; // the loose operator token preceding it
|
|
305
|
+
for (const c of kidsOf(selectStmt)) {
|
|
306
|
+
if (c instanceof TerminalNode) {
|
|
307
|
+
if (c.symbol.type === P.ALL)
|
|
308
|
+
pendingAll = true;
|
|
309
|
+
else if (c.symbol.type === P.EXCEPT)
|
|
310
|
+
pendingOp = "except";
|
|
311
|
+
else if (c.symbol.type === P.INTERSECT)
|
|
312
|
+
pendingOp = "intersect";
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (!(c instanceof ParserRuleContext))
|
|
316
|
+
continue;
|
|
317
|
+
if (isOp(c)) {
|
|
318
|
+
processOp(c);
|
|
319
|
+
pendingAll = false;
|
|
320
|
+
pendingOp = "union";
|
|
321
|
+
}
|
|
322
|
+
else if (CORE_RULES.has(c.ruleIndex)) {
|
|
323
|
+
if (cores.length > 0)
|
|
324
|
+
ops.push({ op: pendingOp, all: pendingAll, cst: c }); // the trailing into-tail branch
|
|
325
|
+
processCore(c);
|
|
326
|
+
pendingAll = false;
|
|
327
|
+
pendingOp = "union";
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return { cores, ops };
|
|
331
|
+
}
|
|
332
|
+
/** Fold the linearized branch bodies with MySQL's set-operator precedence: INTERSECT binds tighter
|
|
333
|
+
* than UNION / EXCEPT (dev.mysql.com/doc/refman/8.4/en/set-operations.html); equal-precedence
|
|
334
|
+
* operators group left-to-right. Pass 1 collapses INTERSECT runs into their left neighbor; pass 2
|
|
335
|
+
* left-folds the remaining UNION/EXCEPT chain. A missing op entry (never expected — cores always
|
|
336
|
+
* lead ops by one) folds as the UNION DISTINCT default. */
|
|
337
|
+
function foldSetop(cores, ops, cst) {
|
|
338
|
+
const bodies = cores.map(buildCoreBody);
|
|
339
|
+
const groups = [bodies[0] ?? emptyBody(cst)];
|
|
340
|
+
const outer = [];
|
|
341
|
+
for (let i = 1; i < bodies.length; i++) {
|
|
342
|
+
const op = ops[i - 1];
|
|
343
|
+
if (op?.op === "intersect") {
|
|
344
|
+
groups[groups.length - 1] = setopNode(op, groups[groups.length - 1], bodies[i], cst);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
outer.push(op);
|
|
348
|
+
groups.push(bodies[i]);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
let body = groups[0];
|
|
352
|
+
for (let i = 1; i < groups.length; i++) {
|
|
353
|
+
body = setopNode(outer[i - 1], body, groups[i], cst);
|
|
354
|
+
}
|
|
355
|
+
return body;
|
|
356
|
+
}
|
|
357
|
+
function setopNode(op, left, right, fallback) {
|
|
358
|
+
return {
|
|
359
|
+
kind: "setop",
|
|
360
|
+
op: op?.op ?? "union",
|
|
361
|
+
all: op?.all ?? false,
|
|
362
|
+
left,
|
|
363
|
+
right,
|
|
364
|
+
columns: [],
|
|
365
|
+
cst: op?.cst ?? fallback,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
/** A branch core -> QueryBody. querySpecification(Nointo) builds a select; explicitTable /
|
|
369
|
+
* tableValueConstructor (and their statement wrappers, reachable as parenthesized operands) build
|
|
370
|
+
* TABLE/VALUES bodies; queryExpression(Nointo) folds its OWN parenthesized chain as a unit (parens
|
|
371
|
+
* grouping survives the flat outer linearization) or unwraps a single inner core. A parenthesized
|
|
372
|
+
* branch's own ORDER BY / LIMIT (`(TABLE t ORDER BY x) UNION ...`) have no per-branch IR slot — an
|
|
373
|
+
* accepted boundary (the refs stay CST-addressable). */
|
|
374
|
+
function buildCoreBody(core) {
|
|
375
|
+
if (core.ruleIndex === P.RULE_querySpecification || core.ruleIndex === P.RULE_querySpecificationNointo) {
|
|
376
|
+
return buildSelect(core);
|
|
377
|
+
}
|
|
378
|
+
if (core.ruleIndex === P.RULE_explicitTable)
|
|
379
|
+
return buildTableBody(core);
|
|
380
|
+
if (core.ruleIndex === P.RULE_tableStatement) {
|
|
381
|
+
const et = directChildrenOfRule(core, P.RULE_explicitTable)[0];
|
|
382
|
+
return et ? buildTableBody(et) : emptyBody(core);
|
|
383
|
+
}
|
|
384
|
+
if (core.ruleIndex === P.RULE_tableValueConstructor || core.ruleIndex === P.RULE_valuesStatement) {
|
|
385
|
+
return buildValues(core);
|
|
386
|
+
}
|
|
387
|
+
// queryExpression(Nointo): fold the parenthesized chain (single inner core -> plain recursion).
|
|
388
|
+
const { cores, ops } = collectUnion(core);
|
|
389
|
+
if (cores.length > 1)
|
|
390
|
+
return foldSetop(cores, ops, core);
|
|
391
|
+
return cores[0] ? buildCoreBody(cores[0]) : emptyBody(core);
|
|
392
|
+
}
|
|
393
|
+
function unwrapCore(core) {
|
|
394
|
+
if (core.ruleIndex === P.RULE_querySpecification ||
|
|
395
|
+
core.ruleIndex === P.RULE_querySpecificationNointo ||
|
|
396
|
+
core.ruleIndex === P.RULE_tableStatement ||
|
|
397
|
+
core.ruleIndex === P.RULE_valuesStatement) {
|
|
398
|
+
return core;
|
|
399
|
+
}
|
|
400
|
+
const inner = firstCoreChild(core);
|
|
401
|
+
return inner ? unwrapCore(inner) : core;
|
|
402
|
+
}
|
|
403
|
+
function firstCoreChild(node) {
|
|
404
|
+
for (const c of kidsOf(node))
|
|
405
|
+
if (c instanceof ParserRuleContext && CORE_RULES.has(c.ruleIndex))
|
|
406
|
+
return c;
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
// --- CTEs ---------------------------------------------------------------------
|
|
410
|
+
/** The commonTableExpressions nodes of a withStatement, in order. The rule is right-recursive (each
|
|
411
|
+
* cte's trailing `(',' commonTableExpressions)?` holds the next), and withStatement also lists them
|
|
412
|
+
* directly — following the direct-child chain covers both, without descending into the CTE bodies. */
|
|
413
|
+
function collectCteNodes(withStmt) {
|
|
414
|
+
const out = [];
|
|
415
|
+
const visit = (n) => {
|
|
416
|
+
for (const cte of directChildrenOfRule(n, P.RULE_commonTableExpressions)) {
|
|
417
|
+
out.push(cte);
|
|
418
|
+
visit(cte);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
visit(withStmt);
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
424
|
+
/** commonTableExpressions: cteName ('(' cteColumnName (',' cteColumnName)* ')')? AS '(' dmlStatement ')' … */
|
|
425
|
+
function lowerCteNode(cte) {
|
|
426
|
+
const nameNode = directChildrenOfRule(cte, P.RULE_cteName)[0];
|
|
427
|
+
const nameUid = nameNode ? directChildrenOfRule(nameNode, P.RULE_uid)[0] : undefined;
|
|
428
|
+
const cols = directChildrenOfRule(cte, P.RULE_cteColumnName).map((c) => c.getText());
|
|
429
|
+
const dml = directChildrenOfRule(cte, P.RULE_dmlStatement)[0];
|
|
430
|
+
return {
|
|
431
|
+
name: nameUid ? nameUid.getText() : (nameNode?.getText() ?? ""),
|
|
432
|
+
nameCst: nameUid ?? nameNode,
|
|
433
|
+
columnAliases: cols.length ? cols : undefined,
|
|
434
|
+
body: dml ? lowerQueryDml(dml) : emptyQuery(cte),
|
|
435
|
+
cst: cte,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
// --- the SELECT body ----------------------------------------------------------
|
|
439
|
+
/** querySpecification: SELECT selectSpec* selectElements selectIntoExpression? fromClause? groupByClause?
|
|
440
|
+
* havingClause? windowClause? orderByClause? limitClause? (WHERE lives INSIDE fromClause). */
|
|
441
|
+
function buildSelect(qspec) {
|
|
442
|
+
const projections = buildProjections(qspec);
|
|
443
|
+
const fromClause = directChildrenOfRule(qspec, P.RULE_fromClause)[0];
|
|
444
|
+
const { from, joins, joinConditions, fromSubqueries } = fromClause
|
|
445
|
+
? buildFrom(fromClause)
|
|
446
|
+
: {
|
|
447
|
+
from: [],
|
|
448
|
+
joins: [],
|
|
449
|
+
joinConditions: [],
|
|
450
|
+
fromSubqueries: new Set(),
|
|
451
|
+
};
|
|
452
|
+
// fromClause: (FROM tableSources)? (WHERE whereExpr=expression)? — the WHERE is fromClause's own
|
|
453
|
+
// direct expression child (join ON / source exprs are nested deeper).
|
|
454
|
+
const whereNode = fromClause ? directChildrenOfRule(fromClause, P.RULE_expression)[0] : undefined;
|
|
455
|
+
const where = whereNode ? lowerExpr(whereNode) : undefined;
|
|
456
|
+
const groupByClause = directChildrenOfRule(qspec, P.RULE_groupByClause)[0];
|
|
457
|
+
const groupBy = groupByClause ? extractGroupBy(groupByClause) : undefined;
|
|
458
|
+
const havingClause = directChildrenOfRule(qspec, P.RULE_havingClause)[0];
|
|
459
|
+
const havingNode = havingClause ? directChildrenOfRule(havingClause, P.RULE_expression)[0] : undefined;
|
|
460
|
+
const having = havingNode ? lowerExpr(havingNode) : undefined;
|
|
461
|
+
const subqueries = extractExpressionSubqueries(qspec, fromSubqueries);
|
|
462
|
+
const aggregated = (groupBy !== undefined && groupBy.length > 0) ||
|
|
463
|
+
projections.some((p) => hasAggregate(p.expr)) ||
|
|
464
|
+
(having !== undefined && hasAggregate(having));
|
|
465
|
+
const columns = [];
|
|
466
|
+
for (const p of projections)
|
|
467
|
+
columnsOf(p.expr, columns, "projection");
|
|
468
|
+
if (where)
|
|
469
|
+
columnsOf(where, columns, "where");
|
|
470
|
+
for (const j of joinConditions)
|
|
471
|
+
columnsOf(j, columns, "join");
|
|
472
|
+
for (const g of groupBy ?? [])
|
|
473
|
+
columnsOf(g, columns, "groupBy");
|
|
474
|
+
if (having)
|
|
475
|
+
columnsOf(having, columns, "having");
|
|
476
|
+
return {
|
|
477
|
+
kind: "select",
|
|
478
|
+
projections,
|
|
479
|
+
from,
|
|
480
|
+
columns,
|
|
481
|
+
where,
|
|
482
|
+
joinConditions: joinConditions.length ? joinConditions : undefined,
|
|
483
|
+
joins: joins.length ? joins : undefined,
|
|
484
|
+
groupBy,
|
|
485
|
+
having,
|
|
486
|
+
aggregated,
|
|
487
|
+
subqueries: subqueries.length ? subqueries : undefined,
|
|
488
|
+
cst: qspec,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
/** selectElements: (star='*' | selectElement) (',' selectElement)*. */
|
|
492
|
+
function buildProjections(qspec) {
|
|
493
|
+
const se = directChildrenOfRule(qspec, P.RULE_selectElements)[0];
|
|
494
|
+
if (!se)
|
|
495
|
+
return [];
|
|
496
|
+
const out = [];
|
|
497
|
+
// A bare leading '*' is a direct token of selectElements, not wrapped in a selectElement.
|
|
498
|
+
if (hasDirectToken(se, P.STAR))
|
|
499
|
+
out.push({ name: undefined, isStar: true, expr: { kind: "star", cst: se }, cst: se });
|
|
500
|
+
for (const el of directChildrenOfRule(se, P.RULE_selectElement))
|
|
501
|
+
out.push(buildProjection(el));
|
|
502
|
+
return out;
|
|
503
|
+
}
|
|
504
|
+
/** selectElement:
|
|
505
|
+
* fullId '.' '*' (qualified star)
|
|
506
|
+
* | fullColumnName (AS? uid)?
|
|
507
|
+
* | functionCall (AS? uid)?
|
|
508
|
+
* | (LOCAL_ID VAR_ASSIGN)? expression (AS? uid)? */
|
|
509
|
+
function buildProjection(el) {
|
|
510
|
+
if (hasDirectToken(el, P.STAR)) {
|
|
511
|
+
const fullId = directChildrenOfRule(el, P.RULE_fullId)[0];
|
|
512
|
+
const qualifier = fullId ? dottedParts(fullId).parts : [];
|
|
513
|
+
return {
|
|
514
|
+
name: undefined,
|
|
515
|
+
isStar: true,
|
|
516
|
+
expr: { kind: "star", qualifier: qualifier.length ? qualifier : undefined, cst: el },
|
|
517
|
+
cst: el,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
const alias = directChildrenOfRule(el, P.RULE_uid)[0]; // the (AS? uid) alias, a direct child
|
|
521
|
+
const fcn = directChildrenOfRule(el, P.RULE_fullColumnName)[0];
|
|
522
|
+
const fc = directChildrenOfRule(el, P.RULE_functionCall)[0];
|
|
523
|
+
const exprNode = directChildrenOfRule(el, P.RULE_expression)[0];
|
|
524
|
+
const expr = fcn ? columnRef(fcn) : fc ? lowerFunctionCall(fc) : exprNode ? lowerExpr(exprNode) : otherExpr(el);
|
|
525
|
+
let name = alias ? alias.getText() : undefined;
|
|
526
|
+
if (name === undefined && expr.kind === "column")
|
|
527
|
+
name = expr.parts[expr.parts.length - 1];
|
|
528
|
+
return { name, isStar: false, expr, ...(alias ? { aliasCst: alias } : {}), cst: el };
|
|
529
|
+
}
|
|
530
|
+
/** groupByClause: GROUP BY groupByItem (',' groupByItem)* (WITH ROLLUP)? / groupByItem: expression … */
|
|
531
|
+
function extractGroupBy(clause) {
|
|
532
|
+
const items = directChildrenOfRule(clause, P.RULE_groupByItem)
|
|
533
|
+
.map((gi) => directChildrenOfRule(gi, P.RULE_expression)[0])
|
|
534
|
+
.filter((e) => e !== undefined)
|
|
535
|
+
.map(lowerExpr);
|
|
536
|
+
return items.length ? items : undefined;
|
|
537
|
+
}
|
|
538
|
+
// --- VALUES / TABLE -----------------------------------------------------------
|
|
539
|
+
/** A valuesStatement (tableValueConstructor orderByClause? limitClause?) or a bare
|
|
540
|
+
* tableValueConstructor: VALUES ROW(1,-2,3), ROW(5,7,9) — the 8.0.19 VALUES statement
|
|
541
|
+
* (dev.mysql.com/doc/refman/8.4/en/values.html; ROW? also admits upstream's bare `VALUES (...)`
|
|
542
|
+
* shape). Lowers to a modelled select whose projections carry the FIRST row's exprs, named
|
|
543
|
+
* column_0…column_N (MySQL's default table-value-constructor output names). */
|
|
544
|
+
function buildValues(vs) {
|
|
545
|
+
const host = directChildrenOfRule(vs, P.RULE_tableValueConstructor)[0] ?? vs;
|
|
546
|
+
const firstRow = directChildrenOfRule(host, P.RULE_expressionsWithDefaults)[0];
|
|
547
|
+
const items = firstRow ? directChildrenOfRule(firstRow, P.RULE_expressionOrDefault) : [];
|
|
548
|
+
const projections = items.map((eod, i) => {
|
|
549
|
+
const e = directChildrenOfRule(eod, P.RULE_expression)[0];
|
|
550
|
+
return {
|
|
551
|
+
name: `column_${i}`,
|
|
552
|
+
isStar: false,
|
|
553
|
+
expr: e ? lowerExpr(e) : { kind: "literal", text: eod.getText(), cst: eod },
|
|
554
|
+
cst: eod,
|
|
555
|
+
};
|
|
556
|
+
});
|
|
557
|
+
const columns = [];
|
|
558
|
+
for (const p of projections)
|
|
559
|
+
columnsOf(p.expr, columns, "projection");
|
|
560
|
+
return { kind: "select", projections, from: [], columns, aggregated: false, cst: vs };
|
|
561
|
+
}
|
|
562
|
+
/** explicitTable: TABLE tableName — the 8.0.19 explicit-table query primary, equivalent to
|
|
563
|
+
* `SELECT * FROM t` (dev.mysql.com/doc/refman/8.4/en/table.html). */
|
|
564
|
+
function buildTableBody(et) {
|
|
565
|
+
const tn = directChildrenOfRule(et, P.RULE_tableName)[0];
|
|
566
|
+
const from = tn ? [tableSourceFromName(tn, undefined, et)] : [];
|
|
567
|
+
return {
|
|
568
|
+
kind: "select",
|
|
569
|
+
projections: [{ name: undefined, isStar: true, expr: { kind: "star", cst: et }, cst: et }],
|
|
570
|
+
from,
|
|
571
|
+
columns: [],
|
|
572
|
+
aggregated: false,
|
|
573
|
+
cst: et,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
/** tableStatement: explicitTable orderByClause? limitClause? — the standalone TABLE statement. */
|
|
577
|
+
function buildTableQuery(ts) {
|
|
578
|
+
const et = directChildrenOfRule(ts, P.RULE_explicitTable)[0];
|
|
579
|
+
const body = et ? buildTableBody(et) : emptyBody(ts);
|
|
580
|
+
const orderBy = extractOrderBy(ts);
|
|
581
|
+
if (orderBy)
|
|
582
|
+
for (const o of orderBy)
|
|
583
|
+
columnsOf(o, body.columns, "orderBy");
|
|
584
|
+
return { kind: "query", ctes: [], body, orderBy, limit: extractLimit(ts), cst: ts };
|
|
585
|
+
}
|
|
586
|
+
/** valuesStatement as a standalone statement: the VALUES body plus hoisted ORDER BY / LIMIT
|
|
587
|
+
* (dev.mysql.com/doc/refman/8.4/en/values.html). */
|
|
588
|
+
function lowerValuesQuery(vs) {
|
|
589
|
+
const body = buildValues(vs);
|
|
590
|
+
const orderBy = extractOrderBy(vs);
|
|
591
|
+
if (orderBy)
|
|
592
|
+
for (const o of orderBy)
|
|
593
|
+
columnsOf(o, body.columns, "orderBy");
|
|
594
|
+
return { kind: "query", ctes: [], body, orderBy, limit: extractLimit(vs), cst: vs };
|
|
595
|
+
}
|
|
596
|
+
/** subqueryBody (withClause? selectStatement | tableStatement | valuesStatement) — the query forms
|
|
597
|
+
* admissible inside subquery/derived-table parentheses — to a QueryExpr. A withClause's CTEs attach
|
|
598
|
+
* to the query they prefix (CTEs are legal in derived tables/subqueries,
|
|
599
|
+
* dev.mysql.com/doc/refman/8.4/en/with.html). */
|
|
600
|
+
function lowerSubqueryBody(sb) {
|
|
601
|
+
const sel = directChildrenOfRule(sb, P.RULE_selectStatement)[0];
|
|
602
|
+
if (sel) {
|
|
603
|
+
const q = lowerSelectStatement(sel);
|
|
604
|
+
const wc = directChildrenOfRule(sb, P.RULE_withClause)[0];
|
|
605
|
+
if (wc)
|
|
606
|
+
q.ctes = collectCteNodes(wc).map(lowerCteNode);
|
|
607
|
+
return q;
|
|
608
|
+
}
|
|
609
|
+
const ts = directChildrenOfRule(sb, P.RULE_tableStatement)[0];
|
|
610
|
+
if (ts)
|
|
611
|
+
return buildTableQuery(ts);
|
|
612
|
+
const vs = directChildrenOfRule(sb, P.RULE_valuesStatement)[0];
|
|
613
|
+
if (vs)
|
|
614
|
+
return lowerValuesQuery(vs);
|
|
615
|
+
return emptyQuery(sb);
|
|
616
|
+
}
|
|
617
|
+
/** The subquery of an operand node — its direct subqueryBody ('(' … ')' forms) or bare selectStatement
|
|
618
|
+
* child — with the node that anchors it. */
|
|
619
|
+
function subqueryOf(node) {
|
|
620
|
+
const sb = directChildrenOfRule(node, P.RULE_subqueryBody)[0];
|
|
621
|
+
if (sb)
|
|
622
|
+
return { query: lowerSubqueryBody(sb), cst: sb };
|
|
623
|
+
const sel = directChildrenOfRule(node, P.RULE_selectStatement)[0];
|
|
624
|
+
return sel ? { query: lowerSelectStatement(sel), cst: sel } : undefined;
|
|
625
|
+
}
|
|
626
|
+
// --- sources ------------------------------------------------------------------
|
|
627
|
+
function buildFrom(fromClause) {
|
|
628
|
+
const from = [];
|
|
629
|
+
const joins = [];
|
|
630
|
+
const joinConditions = [];
|
|
631
|
+
const fromSubqueries = new Set();
|
|
632
|
+
const tableSources = directChildrenOfRule(fromClause, P.RULE_tableSources)[0];
|
|
633
|
+
if (!tableSources)
|
|
634
|
+
return { from, joins, joinConditions, fromSubqueries };
|
|
635
|
+
for (const ts of directChildrenOfRule(tableSources, P.RULE_tableSource)) {
|
|
636
|
+
// tableSource: tableSourceItem joinPart* | '(' tableSourceItem joinPart* ')' | jsonTable.
|
|
637
|
+
const jt = directChildrenOfRule(ts, P.RULE_jsonTable)[0];
|
|
638
|
+
if (jt) {
|
|
639
|
+
from.push({ kind: "table", name: [jt.getText()], cst: jt });
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
const base = directChildrenOfRule(ts, P.RULE_tableSourceItem)[0];
|
|
643
|
+
if (base)
|
|
644
|
+
from.push(...buildSourceItem(base, fromSubqueries));
|
|
645
|
+
for (const jp of directChildrenOfRule(ts, P.RULE_joinPart)) {
|
|
646
|
+
const jItem = directChildrenOfRule(jp, P.RULE_tableSourceItem)[0];
|
|
647
|
+
const jSources = jItem ? buildSourceItem(jItem, fromSubqueries) : [];
|
|
648
|
+
from.push(...jSources);
|
|
649
|
+
const target = jSources[0];
|
|
650
|
+
if (!target)
|
|
651
|
+
continue;
|
|
652
|
+
const { kind, natural } = joinKind(jp);
|
|
653
|
+
const { on, using } = joinSpecOf(jp, joinConditions);
|
|
654
|
+
joins.push({ kind, source: target, on, using, natural: natural || undefined, cst: jp });
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return { from, joins, joinConditions, fromSubqueries };
|
|
658
|
+
}
|
|
659
|
+
/** tableSourceItem:
|
|
660
|
+
* tableName (PARTITION …)? (AS? alias=uid)? (indexHint …)* (atomTableItem)
|
|
661
|
+
* | sequenceFunctionName '(' DECIMAL_LITERAL ')' (AS? alias)? (sequenceTableItem)
|
|
662
|
+
* | (selectStatement | '(' subqueryBody ')') AS? alias=uid ('(' uidList ')')? (subqueryTableItem)
|
|
663
|
+
* | '(' tableSources ')' (tableSourcesItem — flattened)
|
|
664
|
+
* | jsonTable (jsonTableItem) */
|
|
665
|
+
function buildSourceItem(item, fromSubqueries) {
|
|
666
|
+
const sb = directChildrenOfRule(item, P.RULE_subqueryBody)[0];
|
|
667
|
+
const sel = sb ?? directChildrenOfRule(item, P.RULE_selectStatement)[0];
|
|
668
|
+
if (sel) {
|
|
669
|
+
fromSubqueries.add(sel);
|
|
670
|
+
const alias = directChildrenOfRule(item, P.RULE_uid)[0];
|
|
671
|
+
// The derived-table column-alias list: `(SELECT 1, 2) AS dt (a, b)` — its uidList is a direct
|
|
672
|
+
// child (the PARTITION uidList lives on atomTableItem, a different branch).
|
|
673
|
+
const ul = directChildrenOfRule(item, P.RULE_uidList)[0];
|
|
674
|
+
const columnAliases = ul ? directChildrenOfRule(ul, P.RULE_uid).map((u) => u.getText()) : undefined;
|
|
675
|
+
return [
|
|
676
|
+
{
|
|
677
|
+
kind: "subquery",
|
|
678
|
+
query: sb ? lowerSubqueryBody(sb) : lowerSelectStatement(sel),
|
|
679
|
+
alias: alias?.getText(),
|
|
680
|
+
aliasCst: alias,
|
|
681
|
+
...(columnAliases?.length ? { columnAliases } : {}),
|
|
682
|
+
cst: item,
|
|
683
|
+
},
|
|
684
|
+
];
|
|
685
|
+
}
|
|
686
|
+
// '(' tableSources ')' — a parenthesized source group; flatten its members (any inner joins are not
|
|
687
|
+
// separately modelled, an accepted boundary).
|
|
688
|
+
const inner = directChildrenOfRule(item, P.RULE_tableSources)[0];
|
|
689
|
+
if (inner) {
|
|
690
|
+
const out = [];
|
|
691
|
+
for (const ts of directChildrenOfRule(inner, P.RULE_tableSource)) {
|
|
692
|
+
const b = directChildrenOfRule(ts, P.RULE_tableSourceItem)[0];
|
|
693
|
+
if (b)
|
|
694
|
+
out.push(...buildSourceItem(b, fromSubqueries));
|
|
695
|
+
for (const jp of directChildrenOfRule(ts, P.RULE_joinPart)) {
|
|
696
|
+
const ji = directChildrenOfRule(jp, P.RULE_tableSourceItem)[0];
|
|
697
|
+
if (ji)
|
|
698
|
+
out.push(...buildSourceItem(ji, fromSubqueries));
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return out;
|
|
702
|
+
}
|
|
703
|
+
const tn = directChildrenOfRule(item, P.RULE_tableName)[0];
|
|
704
|
+
if (tn) {
|
|
705
|
+
const alias = directChildrenOfRule(item, P.RULE_uid)[0];
|
|
706
|
+
return [tableSourceFromName(tn, alias, item)];
|
|
707
|
+
}
|
|
708
|
+
// sequenceFunctionName(n) — a TVF-ish source; its columns need the signature (unknown, never wrong).
|
|
709
|
+
const seq = directChildrenOfRule(item, P.RULE_sequenceFunctionName)[0];
|
|
710
|
+
if (seq) {
|
|
711
|
+
const alias = directChildrenOfRule(item, P.RULE_uid)[0];
|
|
712
|
+
return [{ kind: "table", name: [seq.getText()], alias: alias?.getText(), aliasCst: alias, cst: item }];
|
|
713
|
+
}
|
|
714
|
+
return [{ kind: "table", name: [item.getText()], cst: item }];
|
|
715
|
+
}
|
|
716
|
+
function tableSourceFromName(tn, alias, cst) {
|
|
717
|
+
const fullId = directChildrenOfRule(tn, P.RULE_fullId)[0] ?? tn;
|
|
718
|
+
const { parts, spans } = dottedParts(fullId);
|
|
719
|
+
return {
|
|
720
|
+
kind: "table",
|
|
721
|
+
name: parts.length ? parts : [tn.getText()],
|
|
722
|
+
namePartSpans: collapsePartSpans(spans),
|
|
723
|
+
alias: alias?.getText(),
|
|
724
|
+
aliasCst: alias,
|
|
725
|
+
cst,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
/** joinPart:
|
|
729
|
+
* (INNER | CROSS)? JOIN LATERAL? tableSourceItem joinSpec* (innerJoin)
|
|
730
|
+
* | STRAIGHT_JOIN tableSourceItem (ON expression)* (straightJoin → inner)
|
|
731
|
+
* | (LEFT | RIGHT) OUTER? JOIN LATERAL? tableSourceItem joinSpec* (outerJoin)
|
|
732
|
+
* | NATURAL ((LEFT | RIGHT) OUTER?)? JOIN tableSourceItem (naturalJoin) */
|
|
733
|
+
function joinKind(jp) {
|
|
734
|
+
const natural = hasDirectToken(jp, P.NATURAL);
|
|
735
|
+
if (hasDirectToken(jp, P.CROSS))
|
|
736
|
+
return { kind: "cross", natural: false };
|
|
737
|
+
if (hasDirectToken(jp, P.LEFT))
|
|
738
|
+
return { kind: "left", natural };
|
|
739
|
+
if (hasDirectToken(jp, P.RIGHT))
|
|
740
|
+
return { kind: "right", natural };
|
|
741
|
+
return { kind: natural ? "natural" : "inner", natural };
|
|
742
|
+
}
|
|
743
|
+
/** ON exprs (lowered ONCE, pushed to joinConditions AND returned as join.on for reference identity)
|
|
744
|
+
* and USING columns for a joinPart. STRAIGHT_JOIN carries ON as a bare expression child; the others
|
|
745
|
+
* wrap it in a joinSpec. */
|
|
746
|
+
function joinSpecOf(jp, joinConditions) {
|
|
747
|
+
let on;
|
|
748
|
+
let using;
|
|
749
|
+
for (const js of directChildrenOfRule(jp, P.RULE_joinSpec)) {
|
|
750
|
+
const e = directChildrenOfRule(js, P.RULE_expression)[0];
|
|
751
|
+
if (e) {
|
|
752
|
+
const lowered = lowerExpr(e);
|
|
753
|
+
joinConditions.push(lowered);
|
|
754
|
+
if (!on)
|
|
755
|
+
on = lowered;
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
const ul = directChildrenOfRule(js, P.RULE_uidList)[0];
|
|
759
|
+
if (ul) {
|
|
760
|
+
const cols = directChildrenOfRule(ul, P.RULE_uid).map((u) => u.getText());
|
|
761
|
+
if (cols.length && !using)
|
|
762
|
+
using = cols;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
// straightJoin's ON is a direct expression child of joinPart (no joinSpec wrapper).
|
|
766
|
+
for (const e of directChildrenOfRule(jp, P.RULE_expression)) {
|
|
767
|
+
const lowered = lowerExpr(e);
|
|
768
|
+
joinConditions.push(lowered);
|
|
769
|
+
if (!on)
|
|
770
|
+
on = lowered;
|
|
771
|
+
}
|
|
772
|
+
return { on, using };
|
|
773
|
+
}
|
|
774
|
+
// --- ORDER BY / LIMIT ---------------------------------------------------------
|
|
775
|
+
/** orderByClause: ORDER BY orderByExpression (',' orderByExpression)* / orderByExpression: expression order? */
|
|
776
|
+
function extractOrderBy(node) {
|
|
777
|
+
const obc = directChildrenOfRule(node, P.RULE_orderByClause)[0];
|
|
778
|
+
if (!obc)
|
|
779
|
+
return undefined;
|
|
780
|
+
const items = orderByExprs(obc);
|
|
781
|
+
return items.length ? items : undefined;
|
|
782
|
+
}
|
|
783
|
+
function orderByExprs(obc) {
|
|
784
|
+
return directChildrenOfRule(obc, P.RULE_orderByExpression)
|
|
785
|
+
.map((oe) => directChildrenOfRule(oe, P.RULE_expression)[0])
|
|
786
|
+
.filter((e) => e !== undefined)
|
|
787
|
+
.map(lowerExpr);
|
|
788
|
+
}
|
|
789
|
+
/** limitClause: LIMIT ((offset=limitClauseAtom ',')? limit=limitClauseAtom | limit OFFSET offset).
|
|
790
|
+
* The comma form is MySQL's `LIMIT offset, count`; the OFFSET form is `LIMIT count OFFSET offset`. */
|
|
791
|
+
function extractLimit(node) {
|
|
792
|
+
const lc = directChildrenOfRule(node, P.RULE_limitClause)[0];
|
|
793
|
+
if (!lc)
|
|
794
|
+
return undefined;
|
|
795
|
+
const atoms = directChildrenOfRule(lc, P.RULE_limitClauseAtom);
|
|
796
|
+
if (atoms.length === 0)
|
|
797
|
+
return undefined;
|
|
798
|
+
const info = {};
|
|
799
|
+
if (hasDirectToken(lc, P.OFFSET)) {
|
|
800
|
+
info.top = limitAtom(atoms[0]);
|
|
801
|
+
if (atoms[1])
|
|
802
|
+
info.offset = limitAtom(atoms[1]);
|
|
803
|
+
}
|
|
804
|
+
else if (atoms.length >= 2) {
|
|
805
|
+
// `offset, count`
|
|
806
|
+
info.offset = limitAtom(atoms[0]);
|
|
807
|
+
info.top = limitAtom(atoms[1]);
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
info.top = limitAtom(atoms[0]);
|
|
811
|
+
}
|
|
812
|
+
return info;
|
|
813
|
+
}
|
|
814
|
+
function limitAtom(atom) {
|
|
815
|
+
return { kind: "literal", text: atom.getText(), cst: atom };
|
|
816
|
+
}
|
|
817
|
+
// --- expressions (left-recursive: expression -> predicate -> expressionAtom) ---
|
|
818
|
+
function lowerExpr(node) {
|
|
819
|
+
switch (node.ruleIndex) {
|
|
820
|
+
case P.RULE_expression:
|
|
821
|
+
return lowerExpression(node);
|
|
822
|
+
case P.RULE_predicate:
|
|
823
|
+
return lowerPredicate(node);
|
|
824
|
+
case P.RULE_expressionAtom:
|
|
825
|
+
return lowerExpressionAtom(node);
|
|
826
|
+
case P.RULE_functionCall:
|
|
827
|
+
return lowerFunctionCall(node);
|
|
828
|
+
case P.RULE_constant:
|
|
829
|
+
return { kind: "literal", text: node.getText(), cst: node };
|
|
830
|
+
case P.RULE_fullColumnName:
|
|
831
|
+
return columnRef(node);
|
|
832
|
+
case P.RULE_expressionOrDefault: {
|
|
833
|
+
const e = directChildrenOfRule(node, P.RULE_expression)[0];
|
|
834
|
+
return e ? lowerExpr(e) : { kind: "literal", text: node.getText(), cst: node };
|
|
835
|
+
}
|
|
836
|
+
default:
|
|
837
|
+
return otherExpr(node);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
/** expression:
|
|
841
|
+
* (NOT | '!') expression (notExpression)
|
|
842
|
+
* | expression logicalOperator expression (logicalExpression)
|
|
843
|
+
* | predicate IS NOT? (TRUE | FALSE | UNKNOWN) (isExpression)
|
|
844
|
+
* | predicate (predicateExpression) */
|
|
845
|
+
function lowerExpression(node) {
|
|
846
|
+
const exprs = directChildrenOfRule(node, P.RULE_expression);
|
|
847
|
+
if (exprs.length === 2) {
|
|
848
|
+
const lop = directChildrenOfRule(node, P.RULE_logicalOperator)[0];
|
|
849
|
+
return {
|
|
850
|
+
kind: "binary",
|
|
851
|
+
op: lop ? logicalOpText(lop) : "",
|
|
852
|
+
left: lowerExpr(exprs[0]),
|
|
853
|
+
right: lowerExpr(exprs[1]),
|
|
854
|
+
cst: node,
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
if (exprs.length === 1) {
|
|
858
|
+
// notExpression — a leading NOT / '!' over one expression.
|
|
859
|
+
return { kind: "unary", op: "not", operand: lowerExpr(exprs[0]), cst: node };
|
|
860
|
+
}
|
|
861
|
+
const pred = directChildrenOfRule(node, P.RULE_predicate)[0];
|
|
862
|
+
if (pred && hasDirectToken(node, P.IS)) {
|
|
863
|
+
// isExpression: IS [NOT] TRUE/FALSE/UNKNOWN.
|
|
864
|
+
const op = hasDirectToken(node, P.TRUE) ? "true" : hasDirectToken(node, P.FALSE) ? "false" : "unknown";
|
|
865
|
+
return {
|
|
866
|
+
kind: "predicate",
|
|
867
|
+
op,
|
|
868
|
+
negated: hasDirectToken(node, P.NOT),
|
|
869
|
+
operand: lowerExpr(pred),
|
|
870
|
+
args: [],
|
|
871
|
+
cst: node,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
return pred ? lowerExpr(pred) : otherExpr(node);
|
|
875
|
+
}
|
|
876
|
+
function logicalOpText(lop) {
|
|
877
|
+
if (hasDirectToken(lop, P.AND))
|
|
878
|
+
return "and";
|
|
879
|
+
if (hasDirectToken(lop, P.OR))
|
|
880
|
+
return "or";
|
|
881
|
+
if (hasDirectToken(lop, P.XOR))
|
|
882
|
+
return "xor";
|
|
883
|
+
// '&&' -> and, '||' -> or (the operator-token forms).
|
|
884
|
+
const t = lop.getText();
|
|
885
|
+
return t === "&&" ? "and" : t === "||" ? "or" : t;
|
|
886
|
+
}
|
|
887
|
+
/** predicate (labelled left-recursive alternatives — see the grammar). Detected structurally by the
|
|
888
|
+
* operator token / sub-rule shapes; a bare expressionAtom passes through. */
|
|
889
|
+
function lowerPredicate(node) {
|
|
890
|
+
const preds = directChildrenOfRule(node, P.RULE_predicate);
|
|
891
|
+
if (preds.length === 0) {
|
|
892
|
+
// expressionAtomPredicate
|
|
893
|
+
const atom = directChildrenOfRule(node, P.RULE_expressionAtom)[0];
|
|
894
|
+
return atom ? lowerExpressionAtom(atom) : otherExpr(node);
|
|
895
|
+
}
|
|
896
|
+
const operand = lowerExpr(preds[0]);
|
|
897
|
+
const negated = hasDirectToken(node, P.NOT);
|
|
898
|
+
// predicate NOT? IN '(' (subqueryBody | expressions) ')'
|
|
899
|
+
if (hasDirectToken(node, P.IN)) {
|
|
900
|
+
const sub = subqueryOf(node);
|
|
901
|
+
const args = sub
|
|
902
|
+
? [{ kind: "subquery", query: sub.query, cst: sub.cst }]
|
|
903
|
+
: expressionsExprs(node).map(lowerExpr);
|
|
904
|
+
return { kind: "predicate", op: "in", negated, operand, args, cst: node };
|
|
905
|
+
}
|
|
906
|
+
// predicate IS nullNotnull
|
|
907
|
+
const nn = directChildrenOfRule(node, P.RULE_nullNotnull)[0];
|
|
908
|
+
if (nn) {
|
|
909
|
+
return { kind: "predicate", op: "null", negated: hasDirectToken(nn, P.NOT), operand, args: [], cst: node };
|
|
910
|
+
}
|
|
911
|
+
// predicate NOT? BETWEEN predicate AND predicate
|
|
912
|
+
if (hasDirectToken(node, P.BETWEEN)) {
|
|
913
|
+
return {
|
|
914
|
+
kind: "predicate",
|
|
915
|
+
op: "between",
|
|
916
|
+
negated,
|
|
917
|
+
operand,
|
|
918
|
+
args: [preds[1] ? lowerExpr(preds[1]) : otherExpr(node), preds[2] ? lowerExpr(preds[2]) : otherExpr(node)],
|
|
919
|
+
cst: node,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
// predicate SOUNDS LIKE predicate
|
|
923
|
+
if (hasDirectToken(node, P.SOUNDS)) {
|
|
924
|
+
return {
|
|
925
|
+
kind: "predicate",
|
|
926
|
+
op: "like",
|
|
927
|
+
negated: false,
|
|
928
|
+
operand,
|
|
929
|
+
args: preds[1] ? [lowerExpr(preds[1])] : [],
|
|
930
|
+
cst: node,
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
// predicate NOT? LIKE predicate (ESCAPE STRING_LITERAL)?
|
|
934
|
+
if (hasDirectToken(node, P.LIKE)) {
|
|
935
|
+
return {
|
|
936
|
+
kind: "predicate",
|
|
937
|
+
op: "like",
|
|
938
|
+
negated,
|
|
939
|
+
operand,
|
|
940
|
+
args: preds[1] ? [lowerExpr(preds[1])] : [],
|
|
941
|
+
cst: node,
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
// predicate NOT? (REGEXP | RLIKE) predicate
|
|
945
|
+
if (hasDirectToken(node, P.REGEXP) || hasDirectToken(node, P.RLIKE)) {
|
|
946
|
+
return {
|
|
947
|
+
kind: "predicate",
|
|
948
|
+
op: "rlike",
|
|
949
|
+
negated,
|
|
950
|
+
operand,
|
|
951
|
+
args: preds[1] ? [lowerExpr(preds[1])] : [],
|
|
952
|
+
cst: node,
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
// predicate MEMBER OF '(' predicate ')'
|
|
956
|
+
if (hasDirectToken(node, P.MEMBER)) {
|
|
957
|
+
return {
|
|
958
|
+
kind: "predicate",
|
|
959
|
+
op: "member of",
|
|
960
|
+
negated: false,
|
|
961
|
+
operand,
|
|
962
|
+
args: preds[1] ? [lowerExpr(preds[1])] : [],
|
|
963
|
+
cst: node,
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
// binaryComparisonPredicate / subqueryComparisonPredicate: left comparisonOperator (right | ANY/ALL/SOME (subqueryBody))
|
|
967
|
+
const cmp = directChildrenOfRule(node, P.RULE_comparisonOperator)[0];
|
|
968
|
+
if (cmp) {
|
|
969
|
+
const sub = subqueryOf(node);
|
|
970
|
+
const right = sub
|
|
971
|
+
? { kind: "subquery", query: sub.query, cst: sub.cst }
|
|
972
|
+
: preds[1]
|
|
973
|
+
? lowerExpr(preds[1])
|
|
974
|
+
: otherExpr(node);
|
|
975
|
+
return { kind: "binary", op: cmp.getText(), left: operand, right, cst: node };
|
|
976
|
+
}
|
|
977
|
+
return operand;
|
|
978
|
+
}
|
|
979
|
+
/** expressionAtom (labelled left-recursive alternatives — see the grammar). */
|
|
980
|
+
function lowerExpressionAtom(node) {
|
|
981
|
+
const atoms = directChildrenOfRule(node, P.RULE_expressionAtom);
|
|
982
|
+
// constantExpressionAtom
|
|
983
|
+
const c = directChildrenOfRule(node, P.RULE_constant)[0];
|
|
984
|
+
if (c && atoms.length === 0)
|
|
985
|
+
return { kind: "literal", text: c.getText(), cst: node };
|
|
986
|
+
// matchAgainstExpressionAtom: MATCH '(' cols ')' AGAINST '(' expr searchModifier? ')' — modelled as
|
|
987
|
+
// a function call so EVERY matched column and the against-expression contribute column refs (a MATCH
|
|
988
|
+
// atom has direct fullColumnName children, so this must run before the bare-column branch below).
|
|
989
|
+
if (hasDirectToken(node, P.MATCH)) {
|
|
990
|
+
const cols = directChildrenOfRule(node, P.RULE_fullColumnName).map(columnRef);
|
|
991
|
+
const against = directChildrenOfRule(node, P.RULE_expression)[0];
|
|
992
|
+
return {
|
|
993
|
+
kind: "function",
|
|
994
|
+
name: "match",
|
|
995
|
+
args: [...cols, ...(against ? [lowerExpr(against)] : [])],
|
|
996
|
+
aggregate: false,
|
|
997
|
+
distinct: false,
|
|
998
|
+
cst: node,
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
// fullColumnNameExpressionAtom
|
|
1002
|
+
const fcn = directChildrenOfRule(node, P.RULE_fullColumnName)[0];
|
|
1003
|
+
if (fcn && atoms.length === 0)
|
|
1004
|
+
return columnRef(fcn);
|
|
1005
|
+
// functionCallExpressionAtom
|
|
1006
|
+
const fc = directChildrenOfRule(node, P.RULE_functionCall)[0];
|
|
1007
|
+
if (fc && atoms.length === 0)
|
|
1008
|
+
return lowerFunctionCall(fc);
|
|
1009
|
+
// mysqlVariableExpressionAtom
|
|
1010
|
+
const mv = directChildrenOfRule(node, P.RULE_mysqlVariable)[0];
|
|
1011
|
+
if (mv && atoms.length === 0)
|
|
1012
|
+
return { kind: "literal", text: mv.getText(), cst: node };
|
|
1013
|
+
// existsExpressionAtom: EXISTS '(' subqueryBody ')'
|
|
1014
|
+
if (hasDirectToken(node, P.EXISTS)) {
|
|
1015
|
+
const sub = subqueryOf(node);
|
|
1016
|
+
return sub ? { kind: "exists", query: sub.query, cst: node } : otherExpr(node);
|
|
1017
|
+
}
|
|
1018
|
+
// subqueryExpressionAtom: '(' subqueryBody ')'
|
|
1019
|
+
const sub = subqueryOf(node);
|
|
1020
|
+
if (sub)
|
|
1021
|
+
return { kind: "subquery", query: sub.query, cst: node };
|
|
1022
|
+
// collateExpressionAtom / binaryExpressionAtom / variableAssignExpressionAtom — passthroughs.
|
|
1023
|
+
if (atoms.length === 1 &&
|
|
1024
|
+
(hasDirectToken(node, P.COLLATE) || hasDirectToken(node, P.BINARY) || hasDirectToken(node, P.VAR_ASSIGN))) {
|
|
1025
|
+
return lowerExpr(atoms[0]);
|
|
1026
|
+
}
|
|
1027
|
+
// unaryExpressionAtom: unaryOperator expressionAtom
|
|
1028
|
+
const unary = directChildrenOfRule(node, P.RULE_unaryOperator)[0];
|
|
1029
|
+
if (unary && atoms.length === 1) {
|
|
1030
|
+
return { kind: "unary", op: unary.getText(), operand: lowerExpr(atoms[0]), cst: node };
|
|
1031
|
+
}
|
|
1032
|
+
// intervalExpressionAtom: INTERVAL expression intervalType — keep the inner expr's columns.
|
|
1033
|
+
if (hasDirectToken(node, P.INTERVAL)) {
|
|
1034
|
+
const e = directChildrenOfRule(node, P.RULE_expression)[0];
|
|
1035
|
+
return e ? lowerExpr(e) : otherExpr(node);
|
|
1036
|
+
}
|
|
1037
|
+
// bitExpressionAtom / mathExpressionAtom / jsonExpressionAtom: left OP right
|
|
1038
|
+
if (atoms.length === 2) {
|
|
1039
|
+
return {
|
|
1040
|
+
kind: "binary",
|
|
1041
|
+
op: binaryAtomOp(node),
|
|
1042
|
+
left: lowerExpr(atoms[0]),
|
|
1043
|
+
right: lowerExpr(atoms[1]),
|
|
1044
|
+
cst: node,
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
// nestedExpressionAtom: '(' expression (',' expression)* ')' — a single expr is grouping
|
|
1048
|
+
// (passthrough); a tuple stays `other` (columnsOf recovers its refs from the CST).
|
|
1049
|
+
const innerExprs = directChildrenOfRule(node, P.RULE_expression);
|
|
1050
|
+
if (innerExprs.length === 1 && atoms.length === 0)
|
|
1051
|
+
return lowerExpr(innerExprs[0]);
|
|
1052
|
+
if (atoms.length === 1)
|
|
1053
|
+
return lowerExpr(atoms[0]);
|
|
1054
|
+
return otherExpr(node);
|
|
1055
|
+
}
|
|
1056
|
+
function binaryAtomOp(node) {
|
|
1057
|
+
for (const r of [P.RULE_bitOperator, P.RULE_multOperator, P.RULE_addOperator, P.RULE_jsonOperator]) {
|
|
1058
|
+
const op = directChildrenOfRule(node, r)[0];
|
|
1059
|
+
if (op)
|
|
1060
|
+
return op.getText();
|
|
1061
|
+
}
|
|
1062
|
+
return "";
|
|
1063
|
+
}
|
|
1064
|
+
/** The direct expression children of the `expressions` list inside an IN payload. */
|
|
1065
|
+
function expressionsExprs(node) {
|
|
1066
|
+
const list = directChildrenOfRule(node, P.RULE_expressions)[0];
|
|
1067
|
+
return list ? directChildrenOfRule(list, P.RULE_expression) : [];
|
|
1068
|
+
}
|
|
1069
|
+
// --- function calls -----------------------------------------------------------
|
|
1070
|
+
/** functionCall:
|
|
1071
|
+
* specificFunction | aggregateWindowedFunction | nonAggregateWindowedFunction
|
|
1072
|
+
* | scalarFunctionName '(' functionArgs? ')' | fullId '(' functionArgs? ')' | passwordFunctionClause */
|
|
1073
|
+
function lowerFunctionCall(fc) {
|
|
1074
|
+
const agg = directChildrenOfRule(fc, P.RULE_aggregateWindowedFunction)[0];
|
|
1075
|
+
if (agg)
|
|
1076
|
+
return lowerWindowedFunction(agg, fc, true);
|
|
1077
|
+
const nonAgg = directChildrenOfRule(fc, P.RULE_nonAggregateWindowedFunction)[0];
|
|
1078
|
+
if (nonAgg)
|
|
1079
|
+
return lowerWindowedFunction(nonAgg, fc, false);
|
|
1080
|
+
const spec = directChildrenOfRule(fc, P.RULE_specificFunction)[0];
|
|
1081
|
+
if (spec)
|
|
1082
|
+
return lowerSpecificFunction(spec);
|
|
1083
|
+
const scalarName = directChildrenOfRule(fc, P.RULE_scalarFunctionName)[0];
|
|
1084
|
+
const fullId = directChildrenOfRule(fc, P.RULE_fullId)[0];
|
|
1085
|
+
const name = scalarName
|
|
1086
|
+
? scalarName.getText()
|
|
1087
|
+
: fullId
|
|
1088
|
+
? lastPart(dottedParts(fullId).parts)
|
|
1089
|
+
: (leftmostToken(fc) ?? "");
|
|
1090
|
+
return {
|
|
1091
|
+
kind: "function",
|
|
1092
|
+
name: name.toLowerCase(),
|
|
1093
|
+
args: argsOf(fc),
|
|
1094
|
+
aggregate: AGGREGATES.has(name.toLowerCase()),
|
|
1095
|
+
distinct: false,
|
|
1096
|
+
cst: fc,
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
/** aggregate / non-aggregate windowed function → a function expr; carries DISTINCT and an OVER window. */
|
|
1100
|
+
function lowerWindowedFunction(fn, fc, aggregate) {
|
|
1101
|
+
const name = (leftmostToken(fn) ?? "").toLowerCase();
|
|
1102
|
+
const over = directChildrenOfRule(fn, P.RULE_overClause)[0] ?? directChildrenOfRule(fc, P.RULE_overClause)[0];
|
|
1103
|
+
return {
|
|
1104
|
+
kind: "function",
|
|
1105
|
+
name,
|
|
1106
|
+
args: argsOf(fn),
|
|
1107
|
+
// A windowed aggregate is not a grouping aggregate for the `aggregated` heuristic — hasAggregate
|
|
1108
|
+
// already ignores calls with a window, so `aggregate: true` here is safe and correct.
|
|
1109
|
+
aggregate: aggregate || AGGREGATES.has(name),
|
|
1110
|
+
distinct: hasDirectToken(fn, P.DISTINCT),
|
|
1111
|
+
window: over ? lowerOver(over) : undefined,
|
|
1112
|
+
cst: fc,
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
/** specificFunction — CAST/CONVERT lower to a cast; CASE to a case; the rest to a generic function. */
|
|
1116
|
+
function lowerSpecificFunction(spec) {
|
|
1117
|
+
if (hasDirectToken(spec, P.CASE))
|
|
1118
|
+
return lowerCaseFunc(spec);
|
|
1119
|
+
if (hasDirectToken(spec, P.CAST) || hasDirectToken(spec, P.CONVERT)) {
|
|
1120
|
+
const inner = directChildrenOfRule(spec, P.RULE_expression)[0];
|
|
1121
|
+
const dt = directChildrenOfRule(spec, P.RULE_convertedDataType)[0];
|
|
1122
|
+
return {
|
|
1123
|
+
kind: "cast",
|
|
1124
|
+
expr: inner ? lowerExpr(inner) : otherExpr(spec),
|
|
1125
|
+
typeText: dt ? dt.getText() : "",
|
|
1126
|
+
cst: spec,
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
const name = (leftmostToken(spec) ?? "").toLowerCase();
|
|
1130
|
+
return { kind: "function", name, args: argsOf(spec), aggregate: false, distinct: false, cst: spec };
|
|
1131
|
+
}
|
|
1132
|
+
/** CASE: `CASE expr? (WHEN fArg THEN fArg)+ (ELSE fArg)? END`. A simple CASE (with a subject expr)
|
|
1133
|
+
* desugars to `subject = whenValue` so the subject's columns are seen (mirrors the other dialects). */
|
|
1134
|
+
function lowerCaseFunc(spec) {
|
|
1135
|
+
const subjectNode = directChildrenOfRule(spec, P.RULE_expression)[0];
|
|
1136
|
+
const subject = subjectNode ? lowerExpr(subjectNode) : undefined;
|
|
1137
|
+
const whens = directChildrenOfRule(spec, P.RULE_caseFuncAlternative).map((alt) => {
|
|
1138
|
+
const fargs = directChildrenOfRule(alt, P.RULE_functionArg);
|
|
1139
|
+
const whenE = fargs[0] ? lowerArg(fargs[0]) : otherExpr(alt);
|
|
1140
|
+
const thenE = fargs[1] ? lowerArg(fargs[1]) : otherExpr(alt);
|
|
1141
|
+
return subject
|
|
1142
|
+
? { when: { kind: "binary", op: "=", left: subject, right: whenE, cst: alt }, then: thenE }
|
|
1143
|
+
: { when: whenE, then: thenE };
|
|
1144
|
+
});
|
|
1145
|
+
// The ELSE functionArg is a direct child of specificFunction (WHEN/THEN ones live in caseFuncAlternative).
|
|
1146
|
+
const elseNode = directChildrenOfRule(spec, P.RULE_functionArg)[0];
|
|
1147
|
+
return { kind: "case", whens, elseExpr: elseNode ? lowerArg(elseNode) : undefined, cst: spec };
|
|
1148
|
+
}
|
|
1149
|
+
/** functionArgs / functionArg args of a call node → lowered Expr[]. Both wrap constant / fullColumnName
|
|
1150
|
+
* / functionCall / expression; some callers (aggregates, CAST) attach these arg rules directly. */
|
|
1151
|
+
function argsOf(host) {
|
|
1152
|
+
const out = [];
|
|
1153
|
+
for (const c of kidsOf(host)) {
|
|
1154
|
+
if (!(c instanceof ParserRuleContext))
|
|
1155
|
+
continue;
|
|
1156
|
+
if (c.ruleIndex === P.RULE_functionArgs) {
|
|
1157
|
+
for (const a of directArgRuleChildren(c))
|
|
1158
|
+
out.push(lowerArg(a));
|
|
1159
|
+
}
|
|
1160
|
+
else if (c.ruleIndex === P.RULE_functionArg) {
|
|
1161
|
+
const a = directArgRuleChildren(c)[0];
|
|
1162
|
+
if (a)
|
|
1163
|
+
out.push(lowerArg(a));
|
|
1164
|
+
}
|
|
1165
|
+
else if (ARG_RULES.has(c.ruleIndex)) {
|
|
1166
|
+
out.push(lowerArg(c));
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return out;
|
|
1170
|
+
}
|
|
1171
|
+
const ARG_RULES = new Set([P.RULE_constant, P.RULE_fullColumnName, P.RULE_functionCall, P.RULE_expression]);
|
|
1172
|
+
function directArgRuleChildren(node) {
|
|
1173
|
+
const out = [];
|
|
1174
|
+
for (const c of kidsOf(node))
|
|
1175
|
+
if (c instanceof ParserRuleContext && ARG_RULES.has(c.ruleIndex))
|
|
1176
|
+
out.push(c);
|
|
1177
|
+
return out;
|
|
1178
|
+
}
|
|
1179
|
+
function lowerArg(node) {
|
|
1180
|
+
switch (node.ruleIndex) {
|
|
1181
|
+
case P.RULE_constant:
|
|
1182
|
+
return { kind: "literal", text: node.getText(), cst: node };
|
|
1183
|
+
case P.RULE_fullColumnName:
|
|
1184
|
+
return columnRef(node);
|
|
1185
|
+
case P.RULE_functionCall:
|
|
1186
|
+
return lowerFunctionCall(node);
|
|
1187
|
+
default:
|
|
1188
|
+
return lowerExpr(node);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
/** overClause: OVER ('(' windowSpec? ')' | windowName) / windowSpec: windowName? partitionClause?
|
|
1192
|
+
* orderByClause? frameClause? / partitionClause: PARTITION BY expression (',' expression)*. */
|
|
1193
|
+
function lowerOver(over) {
|
|
1194
|
+
const spec = directChildrenOfRule(over, P.RULE_windowSpec)[0];
|
|
1195
|
+
const pc = spec ? directChildrenOfRule(spec, P.RULE_partitionClause)[0] : undefined;
|
|
1196
|
+
const partitionBy = pc ? directChildrenOfRule(pc, P.RULE_expression).map(lowerExpr) : [];
|
|
1197
|
+
const obc = spec ? directChildrenOfRule(spec, P.RULE_orderByClause)[0] : undefined;
|
|
1198
|
+
const orderBy = obc ? orderByExprs(obc) : [];
|
|
1199
|
+
return { partitionBy, orderBy, cst: over };
|
|
1200
|
+
}
|
|
1201
|
+
// --- identifiers --------------------------------------------------------------
|
|
1202
|
+
/** A fullColumnName / fullId as a column Expr — RAW parts (delimiters intact, dots stripped). */
|
|
1203
|
+
function columnRef(node) {
|
|
1204
|
+
const { parts, spans } = dottedParts(node);
|
|
1205
|
+
return {
|
|
1206
|
+
kind: "column",
|
|
1207
|
+
parts: parts.length ? parts : [node.getText()],
|
|
1208
|
+
partSpans: collapsePartSpans(spans),
|
|
1209
|
+
cst: node,
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
/** The dotted parts of a fullColumnName / fullId, each with its own `PartSpan` (all-or-nothing per
|
|
1213
|
+
* ref, collapsed by the caller). A `uid` rule and a `.uid` dottedId go through the shared
|
|
1214
|
+
* `partSpanOf`; a glued `DOT_ID` token (the unspaced `a.b` style, `DOT_ID: '.' ID_LITERAL`) is a
|
|
1215
|
+
* single lexer token, not a node, so its identifier span is computed past the leading dot via
|
|
1216
|
+
* `dotIdPartSpanOf`. `DOT_ID` reaches this two ways, both handled: as a direct terminal child of
|
|
1217
|
+
* `fullId` (`uid (DOT_ID | '.' uid)?`), and nested inside a `dottedId` of `fullColumnName`
|
|
1218
|
+
* (`uid (dottedId dottedId?)?` where `dottedId: DOT_ID | '.' uid`). Both strip the leading dot from
|
|
1219
|
+
* the emitted part text. A span is `undefined` only for a part that genuinely carries no token. */
|
|
1220
|
+
function dottedParts(node) {
|
|
1221
|
+
const parts = [];
|
|
1222
|
+
const spans = [];
|
|
1223
|
+
const pushDotId = (sym) => {
|
|
1224
|
+
const t = sym.text ?? "";
|
|
1225
|
+
parts.push(t.startsWith(".") ? t.slice(1) : t);
|
|
1226
|
+
spans.push(dotIdPartSpanOf(sym));
|
|
1227
|
+
};
|
|
1228
|
+
for (const c of kidsOf(node)) {
|
|
1229
|
+
if (c instanceof ParserRuleContext) {
|
|
1230
|
+
if (c.ruleIndex === P.RULE_uid) {
|
|
1231
|
+
parts.push(c.getText());
|
|
1232
|
+
spans.push(partSpanOf(c));
|
|
1233
|
+
}
|
|
1234
|
+
else if (c.ruleIndex === P.RULE_dottedId) {
|
|
1235
|
+
const iu = directChildrenOfRule(c, P.RULE_uid)[0];
|
|
1236
|
+
const dotId = dotIdTerminalOf(c);
|
|
1237
|
+
if (iu) {
|
|
1238
|
+
parts.push(iu.getText());
|
|
1239
|
+
spans.push(partSpanOf(iu));
|
|
1240
|
+
}
|
|
1241
|
+
else if (dotId) {
|
|
1242
|
+
pushDotId(dotId.symbol);
|
|
1243
|
+
}
|
|
1244
|
+
else {
|
|
1245
|
+
const t = c.getText();
|
|
1246
|
+
parts.push(t.startsWith(".") ? t.slice(1) : t);
|
|
1247
|
+
spans.push(undefined);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
else if (c instanceof TerminalNode && c.symbol.type === P.DOT_ID) {
|
|
1252
|
+
pushDotId(c.symbol);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return { parts, spans };
|
|
1256
|
+
}
|
|
1257
|
+
/** The `DOT_ID` terminal directly under a `dottedId` (its `: DOT_ID` alternative), else `undefined`
|
|
1258
|
+
* (the `'.' uid` alternative). */
|
|
1259
|
+
function dotIdTerminalOf(node) {
|
|
1260
|
+
for (const c of kidsOf(node))
|
|
1261
|
+
if (c instanceof TerminalNode && c.symbol.type === P.DOT_ID)
|
|
1262
|
+
return c;
|
|
1263
|
+
return undefined;
|
|
1264
|
+
}
|
|
1265
|
+
function lastPart(parts) {
|
|
1266
|
+
return parts.length ? parts[parts.length - 1] : "";
|
|
1267
|
+
}
|
|
1268
|
+
// --- expression subqueries (scalar / IN / EXISTS) -----------------------------
|
|
1269
|
+
/** Scalar / IN / EXISTS subqueries in this query spec's expressions — every nested `subqueryBody` /
|
|
1270
|
+
* `selectStatement` that is NOT a FROM source. Rooted at the querySpecification, so SELECT-list /
|
|
1271
|
+
* WHERE / GROUP BY / HAVING / ORDER BY / LIMIT expressions are all covered. `continue` at each
|
|
1272
|
+
* boundary so an inner query collects its own expression subqueries in its own scope (a subqueryBody
|
|
1273
|
+
* is the boundary for the '(' … ')' operand forms — its inner selectStatement is never re-visited). */
|
|
1274
|
+
function extractExpressionSubqueries(qspec, fromSubqueries) {
|
|
1275
|
+
const out = [];
|
|
1276
|
+
const walk = (n) => {
|
|
1277
|
+
for (const c of kidsOf(n)) {
|
|
1278
|
+
if (!(c instanceof ParserRuleContext))
|
|
1279
|
+
continue;
|
|
1280
|
+
if (c.ruleIndex === P.RULE_selectStatement || c.ruleIndex === P.RULE_subqueryBody) {
|
|
1281
|
+
if (!fromSubqueries.has(c)) {
|
|
1282
|
+
out.push(c.ruleIndex === P.RULE_subqueryBody ? lowerSubqueryBody(c) : lowerSelectStatement(c));
|
|
1283
|
+
}
|
|
1284
|
+
continue; // its own scope — don't descend
|
|
1285
|
+
}
|
|
1286
|
+
walk(c);
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
walk(qspec);
|
|
1290
|
+
return out;
|
|
1291
|
+
}
|
|
1292
|
+
// --- column extraction (single source of truth for SelectExpr.columns) --------
|
|
1293
|
+
function columnsOf(expr, acc, clause) {
|
|
1294
|
+
switch (expr.kind) {
|
|
1295
|
+
case "column":
|
|
1296
|
+
acc.push({ kind: "columnref", parts: expr.parts, clause, cst: expr.cst, partSpans: expr.partSpans });
|
|
1297
|
+
break;
|
|
1298
|
+
case "binary":
|
|
1299
|
+
columnsOf(expr.left, acc, clause);
|
|
1300
|
+
columnsOf(expr.right, acc, clause);
|
|
1301
|
+
break;
|
|
1302
|
+
case "unary":
|
|
1303
|
+
columnsOf(expr.operand, acc, clause);
|
|
1304
|
+
break;
|
|
1305
|
+
case "cast":
|
|
1306
|
+
columnsOf(expr.expr, acc, clause);
|
|
1307
|
+
break;
|
|
1308
|
+
case "function":
|
|
1309
|
+
expr.args.forEach((a) => columnsOf(a, acc, clause));
|
|
1310
|
+
expr.window?.partitionBy.forEach((a) => columnsOf(a, acc, clause));
|
|
1311
|
+
expr.window?.orderBy.forEach((a) => columnsOf(a, acc, clause));
|
|
1312
|
+
break;
|
|
1313
|
+
case "case":
|
|
1314
|
+
expr.whens.forEach((w) => {
|
|
1315
|
+
columnsOf(w.when, acc, clause);
|
|
1316
|
+
columnsOf(w.then, acc, clause);
|
|
1317
|
+
});
|
|
1318
|
+
if (expr.elseExpr)
|
|
1319
|
+
columnsOf(expr.elseExpr, acc, clause);
|
|
1320
|
+
break;
|
|
1321
|
+
case "predicate":
|
|
1322
|
+
columnsOf(expr.operand, acc, clause);
|
|
1323
|
+
expr.args.forEach((a) => columnsOf(a, acc, clause));
|
|
1324
|
+
break;
|
|
1325
|
+
case "subscript":
|
|
1326
|
+
columnsOf(expr.base, acc, clause);
|
|
1327
|
+
columnsOf(expr.index, acc, clause);
|
|
1328
|
+
break;
|
|
1329
|
+
case "other":
|
|
1330
|
+
cstColumnRefs(expr.cst, acc, clause);
|
|
1331
|
+
break;
|
|
1332
|
+
// literal / star / subquery / exists / lambda / with → no column refs at this level
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
/** Fallback: recover column references from inside an unmodelled `other` node — descend the CST,
|
|
1336
|
+
* lowering any fullColumnName, but never into a nested selectStatement / subqueryBody (own scope). */
|
|
1337
|
+
function cstColumnRefs(node, acc, clause) {
|
|
1338
|
+
for (const child of kidsOf(node)) {
|
|
1339
|
+
if (!(child instanceof ParserRuleContext))
|
|
1340
|
+
continue;
|
|
1341
|
+
if (child.ruleIndex === P.RULE_selectStatement || child.ruleIndex === P.RULE_subqueryBody)
|
|
1342
|
+
continue;
|
|
1343
|
+
if (child.ruleIndex === P.RULE_fullColumnName) {
|
|
1344
|
+
const e = columnRef(child);
|
|
1345
|
+
if (e.kind === "column") {
|
|
1346
|
+
acc.push({ kind: "columnref", parts: e.parts, clause, cst: child, partSpans: e.partSpans });
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
cstColumnRefs(child, acc, clause);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
function hasAggregate(expr) {
|
|
1354
|
+
switch (expr.kind) {
|
|
1355
|
+
case "function":
|
|
1356
|
+
return (expr.aggregate && !expr.window) || expr.args.some(hasAggregate);
|
|
1357
|
+
case "binary":
|
|
1358
|
+
return hasAggregate(expr.left) || hasAggregate(expr.right);
|
|
1359
|
+
case "unary":
|
|
1360
|
+
return hasAggregate(expr.operand);
|
|
1361
|
+
case "cast":
|
|
1362
|
+
return hasAggregate(expr.expr);
|
|
1363
|
+
case "case":
|
|
1364
|
+
return (expr.whens.some((w) => hasAggregate(w.when) || hasAggregate(w.then)) ||
|
|
1365
|
+
(expr.elseExpr !== undefined && hasAggregate(expr.elseExpr)));
|
|
1366
|
+
case "predicate":
|
|
1367
|
+
return hasAggregate(expr.operand) || expr.args.some(hasAggregate);
|
|
1368
|
+
case "subscript":
|
|
1369
|
+
return hasAggregate(expr.base);
|
|
1370
|
+
default:
|
|
1371
|
+
return false;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
// --- CST navigation helpers ---------------------------------------------------
|
|
1375
|
+
function kidsOf(node) {
|
|
1376
|
+
if (node instanceof ParserRuleContext)
|
|
1377
|
+
return node.children ?? [];
|
|
1378
|
+
const out = [];
|
|
1379
|
+
for (let i = 0; i < node.getChildCount(); i++) {
|
|
1380
|
+
const c = node.getChild(i);
|
|
1381
|
+
if (c)
|
|
1382
|
+
out.push(c);
|
|
1383
|
+
}
|
|
1384
|
+
return out;
|
|
1385
|
+
}
|
|
1386
|
+
function* descendants(node) {
|
|
1387
|
+
for (const child of kidsOf(node)) {
|
|
1388
|
+
if (child instanceof ParserRuleContext) {
|
|
1389
|
+
yield child;
|
|
1390
|
+
yield* descendants(child);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
function firstOfRule(node, ruleIndex) {
|
|
1395
|
+
for (const d of descendants(node))
|
|
1396
|
+
if (d.ruleIndex === ruleIndex)
|
|
1397
|
+
return d;
|
|
1398
|
+
return undefined;
|
|
1399
|
+
}
|
|
1400
|
+
function directChildrenOfRule(node, ruleIndex) {
|
|
1401
|
+
const out = [];
|
|
1402
|
+
for (const child of kidsOf(node)) {
|
|
1403
|
+
if (child instanceof ParserRuleContext && child.ruleIndex === ruleIndex)
|
|
1404
|
+
out.push(child);
|
|
1405
|
+
}
|
|
1406
|
+
return out;
|
|
1407
|
+
}
|
|
1408
|
+
function hasDirectToken(node, type) {
|
|
1409
|
+
for (const child of kidsOf(node)) {
|
|
1410
|
+
if (child instanceof TerminalNode && child.symbol.type === type)
|
|
1411
|
+
return true;
|
|
1412
|
+
}
|
|
1413
|
+
return false;
|
|
1414
|
+
}
|
|
1415
|
+
function leftmostToken(node) {
|
|
1416
|
+
let n = node;
|
|
1417
|
+
while (n.getChildCount() > 0) {
|
|
1418
|
+
const first = n.getChild(0);
|
|
1419
|
+
if (!first)
|
|
1420
|
+
return undefined;
|
|
1421
|
+
if (first instanceof TerminalNode)
|
|
1422
|
+
return first.getText();
|
|
1423
|
+
n = first;
|
|
1424
|
+
}
|
|
1425
|
+
return undefined;
|
|
1426
|
+
}
|
|
1427
|
+
function otherExpr(node) {
|
|
1428
|
+
return { kind: "other", text: node.getText(), cst: node };
|
|
1429
|
+
}
|
|
1430
|
+
function emptyBody(cst) {
|
|
1431
|
+
return {
|
|
1432
|
+
kind: "select",
|
|
1433
|
+
projections: [],
|
|
1434
|
+
from: [],
|
|
1435
|
+
columns: [],
|
|
1436
|
+
aggregated: false,
|
|
1437
|
+
unsupported: ["unparsed"],
|
|
1438
|
+
cst,
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
function emptyQuery(cst) {
|
|
1442
|
+
return { kind: "query", ctes: [], body: emptyBody(cst), cst };
|
|
1443
|
+
}
|