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.
Files changed (244) hide show
  1. package/LICENSE +0 -10
  2. package/README.md +95 -85
  3. package/THIRD-PARTY-NOTICES.md +70 -5
  4. package/dist/api.d.ts +5 -3
  5. package/dist/api.js +17 -4
  6. package/dist/bigquery/behavior.d.ts +2 -0
  7. package/dist/bigquery/behavior.js +20 -0
  8. package/dist/bigquery/dot-path.d.ts +0 -2
  9. package/dist/bigquery/dot-path.js +0 -1
  10. package/dist/bigquery/fold.d.ts +8 -0
  11. package/dist/bigquery/fold.js +39 -0
  12. package/dist/bigquery/index.d.ts +7 -0
  13. package/dist/bigquery/index.js +10 -0
  14. package/dist/{infer/bigquery.d.ts → bigquery/infer.d.ts} +2 -2
  15. package/dist/{infer/bigquery.js → bigquery/infer.js} +4 -3
  16. package/dist/bigquery/lower.js +35 -12
  17. package/dist/bigquery/signatures.generated.d.ts +6 -0
  18. package/dist/bigquery/signatures.generated.js +1068 -0
  19. package/dist/completion/atn-walk.d.ts +13 -2
  20. package/dist/completion/atn-walk.js +13 -10
  21. package/dist/completion/complete.d.ts +4 -3
  22. package/dist/completion/complete.js +101 -45
  23. package/dist/completion/config.js +66 -0
  24. package/dist/completion/jinja-slot.d.ts +24 -0
  25. package/dist/completion/jinja-slot.js +126 -0
  26. package/dist/completion/parser-factory.d.ts +13 -1
  27. package/dist/completion/parser-factory.js +48 -0
  28. package/dist/databricks/behavior.d.ts +2 -0
  29. package/dist/databricks/behavior.js +19 -0
  30. package/dist/databricks/fold.d.ts +8 -0
  31. package/dist/databricks/fold.js +27 -0
  32. package/dist/databricks/index.d.ts +7 -0
  33. package/dist/databricks/index.js +10 -0
  34. package/dist/databricks/infer.d.ts +6 -0
  35. package/dist/databricks/infer.js +638 -0
  36. package/dist/databricks/signatures.generated.d.ts +6 -0
  37. package/dist/databricks/signatures.generated.js +1745 -0
  38. package/dist/derived-dialects.js +19 -1
  39. package/dist/dialect-behavior/behavior.d.ts +26 -0
  40. package/dist/dialect-behavior/behavior.js +1 -0
  41. package/dist/dialect-behavior/carrier.d.ts +5 -0
  42. package/dist/dialect-behavior/carrier.js +5 -0
  43. package/dist/dialect-behavior/coerce-rules.d.ts +7 -0
  44. package/dist/dialect-behavior/coerce-rules.js +69 -0
  45. package/dist/dialect-behavior/public-fold.d.ts +6 -0
  46. package/dist/dialect-behavior/public-fold.js +9 -0
  47. package/dist/dialect-behavior/registry.d.ts +6 -0
  48. package/dist/dialect-behavior/registry.js +34 -0
  49. package/dist/dialect-symbols.js +22 -17
  50. package/dist/dialect.d.ts +2 -2
  51. package/dist/document/document.d.ts +1 -1
  52. package/dist/document/document.js +8 -7
  53. package/dist/duckdb/behavior.d.ts +2 -0
  54. package/dist/duckdb/behavior.js +21 -0
  55. package/dist/duckdb/fold.d.ts +8 -0
  56. package/dist/duckdb/fold.js +29 -0
  57. package/dist/duckdb/index.d.ts +7 -0
  58. package/dist/duckdb/index.js +10 -0
  59. package/dist/{infer/duckdb.d.ts → duckdb/infer.d.ts} +2 -2
  60. package/dist/{infer/duckdb.js → duckdb/infer.js} +4 -3
  61. package/dist/duckdb/lower.js +47 -15
  62. package/dist/duckdb/signatures.generated.d.ts +6 -0
  63. package/dist/duckdb/signatures.generated.js +1072 -0
  64. package/dist/generated/bigquery/GoogleSQLParser.js +0 -7060
  65. package/dist/generated/databricks/DatabricksParser.js +0 -4800
  66. package/dist/generated/duckdb/DuckdbParser.js +0 -9100
  67. package/dist/generated/minijinja/MinijinjaParser.js +0 -380
  68. package/dist/generated/mysql/MysqlLexer.js +7357 -0
  69. package/dist/generated/mysql/MysqlParser.js +78520 -0
  70. package/dist/generated/postgres/PostgresParser.js +0 -8530
  71. package/dist/generated/redshift/RedshiftParser.js +0 -10970
  72. package/dist/generated/snowflake/SnowflakeParser.js +0 -7320
  73. package/dist/generated/sqlite/SqliteLexer.js +945 -0
  74. package/dist/generated/sqlite/SqliteParser.js +14682 -0
  75. package/dist/generated/trino/TrinoParser.js +0 -3770
  76. package/dist/generated/tsql/TSqlParser.js +0 -8420
  77. package/dist/ident/fold.d.ts +24 -15
  78. package/dist/ident/fold.js +12 -145
  79. package/dist/index.d.ts +6 -2
  80. package/dist/index.js +14 -8
  81. package/dist/infer/functions.d.ts +22 -11
  82. package/dist/infer/functions.js +26 -888
  83. package/dist/infer/infer.js +16 -14
  84. package/dist/infer/nullability.js +3 -4
  85. package/dist/infer/types.d.ts +1 -1
  86. package/dist/infer/types.js +7 -7
  87. package/dist/ir/ir.d.ts +26 -17
  88. package/dist/ir/part-span.d.ts +16 -1
  89. package/dist/ir/part-span.js +38 -10
  90. package/dist/ir/span.js +0 -2
  91. package/dist/ir/walk.js +3 -3
  92. package/dist/lineage/hops.js +13 -10
  93. package/dist/lineage/lineage.js +10 -7
  94. package/dist/minijinja/apply-tags.d.ts +14 -7
  95. package/dist/minijinja/apply-tags.js +66 -121
  96. package/dist/minijinja/parse.js +6 -6
  97. package/dist/minijinja/tag-ast.d.ts +29 -36
  98. package/dist/minijinja/tag-ast.js +210 -90
  99. package/dist/mysql/behavior.d.ts +2 -0
  100. package/dist/mysql/behavior.js +21 -0
  101. package/dist/mysql/fold.d.ts +8 -0
  102. package/dist/mysql/fold.js +49 -0
  103. package/dist/mysql/index.d.ts +7 -0
  104. package/dist/mysql/index.js +10 -0
  105. package/dist/mysql/infer.d.ts +20 -0
  106. package/dist/mysql/infer.js +156 -0
  107. package/dist/mysql/lower.d.ts +13 -0
  108. package/dist/mysql/lower.js +1443 -0
  109. package/dist/mysql/parse.d.ts +10 -0
  110. package/dist/mysql/parse.js +70 -0
  111. package/dist/mysql/signatures.generated.d.ts +6 -0
  112. package/dist/mysql/signatures.generated.js +508 -0
  113. package/dist/postgres/behavior.d.ts +2 -0
  114. package/dist/postgres/behavior.js +19 -0
  115. package/dist/postgres/fold.d.ts +8 -0
  116. package/dist/postgres/fold.js +30 -0
  117. package/dist/postgres/index.d.ts +7 -0
  118. package/dist/postgres/index.js +10 -0
  119. package/dist/{infer/postgres.d.ts → postgres/infer.d.ts} +2 -2
  120. package/dist/{infer/postgres.js → postgres/infer.js} +4 -3
  121. package/dist/postgres/lower.js +2 -2
  122. package/dist/postgres/signatures.generated.d.ts +6 -0
  123. package/dist/postgres/signatures.generated.js +2973 -0
  124. package/dist/qualify/check-calls.js +80 -134
  125. package/dist/qualify/qualify.js +16 -14
  126. package/dist/qualify/schema-provider.js +2 -2
  127. package/dist/qualify/schema.js +3 -3
  128. package/dist/qualify/template-provider.d.ts +45 -12
  129. package/dist/qualify/template-provider.js +69 -38
  130. package/dist/redshift/behavior.d.ts +2 -0
  131. package/dist/redshift/behavior.js +19 -0
  132. package/dist/redshift/fold.d.ts +8 -0
  133. package/dist/redshift/fold.js +31 -0
  134. package/dist/redshift/index.d.ts +7 -0
  135. package/dist/redshift/index.js +10 -0
  136. package/dist/{infer/redshift.d.ts → redshift/infer.d.ts} +2 -2
  137. package/dist/{infer/redshift.js → redshift/infer.js} +4 -3
  138. package/dist/redshift/lower.js +2 -2
  139. package/dist/redshift/signatures.generated.d.ts +6 -0
  140. package/dist/redshift/signatures.generated.js +757 -0
  141. package/dist/references/references.js +17 -12
  142. package/dist/scope/like-pattern.d.ts +2 -0
  143. package/dist/scope/like-pattern.js +15 -0
  144. package/dist/scope/scope.d.ts +5 -5
  145. package/dist/scope/scope.js +48 -42
  146. package/dist/sema/resolve.js +17 -12
  147. package/dist/session.d.ts +2 -2
  148. package/dist/signature/signature.d.ts +14 -6
  149. package/dist/signature/signature.js +30 -22
  150. package/dist/signature/signatures.d.ts +14 -12
  151. package/dist/signature/signatures.js +42 -582
  152. package/dist/snowflake/behavior.d.ts +2 -0
  153. package/dist/snowflake/behavior.js +22 -0
  154. package/dist/snowflake/fold.d.ts +8 -0
  155. package/dist/snowflake/fold.js +25 -0
  156. package/dist/snowflake/index.d.ts +7 -0
  157. package/dist/snowflake/index.js +10 -0
  158. package/dist/{infer/snowflake.d.ts → snowflake/infer.d.ts} +2 -2
  159. package/dist/{infer/snowflake.js → snowflake/infer.js} +4 -3
  160. package/dist/snowflake/lower.js +59 -19
  161. package/dist/snowflake/signatures.generated.d.ts +6 -0
  162. package/dist/snowflake/signatures.generated.js +2080 -0
  163. package/dist/sqlite/behavior.d.ts +2 -0
  164. package/dist/sqlite/behavior.js +19 -0
  165. package/dist/sqlite/fold.d.ts +8 -0
  166. package/dist/sqlite/fold.js +41 -0
  167. package/dist/sqlite/index.d.ts +7 -0
  168. package/dist/sqlite/index.js +10 -0
  169. package/dist/sqlite/infer.d.ts +12 -0
  170. package/dist/sqlite/infer.js +122 -0
  171. package/dist/sqlite/lower.d.ts +11 -0
  172. package/dist/sqlite/lower.js +1093 -0
  173. package/dist/sqlite/parse.d.ts +10 -0
  174. package/dist/sqlite/parse.js +70 -0
  175. package/dist/sqlite/signatures.generated.d.ts +6 -0
  176. package/dist/sqlite/signatures.generated.js +277 -0
  177. package/dist/symbols/symbols.js +14 -12
  178. package/dist/token/classify.js +31 -0
  179. package/dist/token/tokenize.js +4 -0
  180. package/dist/trino/behavior.d.ts +2 -0
  181. package/dist/trino/behavior.js +21 -0
  182. package/dist/trino/fold.d.ts +8 -0
  183. package/dist/trino/fold.js +39 -0
  184. package/dist/trino/index.d.ts +7 -0
  185. package/dist/trino/index.js +10 -0
  186. package/dist/{infer/trino.d.ts → trino/infer.d.ts} +2 -2
  187. package/dist/{infer/trino.js → trino/infer.js} +4 -3
  188. package/dist/trino/lower.js +5 -5
  189. package/dist/trino/signatures.generated.d.ts +6 -0
  190. package/dist/trino/signatures.generated.js +968 -0
  191. package/dist/tsql/behavior.d.ts +2 -0
  192. package/dist/tsql/behavior.js +20 -0
  193. package/dist/tsql/fold.d.ts +8 -0
  194. package/dist/tsql/fold.js +34 -0
  195. package/dist/tsql/index.d.ts +7 -0
  196. package/dist/tsql/index.js +10 -0
  197. package/dist/tsql/infer.d.ts +16 -0
  198. package/dist/tsql/infer.js +289 -0
  199. package/dist/tsql/lower.js +4 -4
  200. package/dist/tsql/signatures.generated.d.ts +6 -0
  201. package/dist/tsql/signatures.generated.js +640 -0
  202. package/package.json +15 -11
  203. package/dist/generated/bigquery/GoogleSQLLexer.d.ts +0 -407
  204. package/dist/generated/bigquery/GoogleSQLParser.d.ts +0 -9558
  205. package/dist/generated/bigquery/GoogleSQLParserListener.d.ts +0 -7777
  206. package/dist/generated/bigquery/GoogleSQLParserListener.js +0 -7070
  207. package/dist/generated/databricks/DatabricksLexer.d.ts +0 -566
  208. package/dist/generated/databricks/DatabricksParser.d.ts +0 -7771
  209. package/dist/generated/databricks/DatabricksParserListener.d.ts +0 -5737
  210. package/dist/generated/databricks/DatabricksParserListener.js +0 -5256
  211. package/dist/generated/duckdb/DuckdbLexer.d.ts +0 -691
  212. package/dist/generated/duckdb/DuckdbParser.d.ts +0 -13932
  213. package/dist/generated/duckdb/DuckdbParserListener.d.ts +0 -10049
  214. package/dist/generated/duckdb/DuckdbParserListener.js +0 -9138
  215. package/dist/generated/minijinja/MinijinjaLexer.d.ts +0 -108
  216. package/dist/generated/minijinja/MinijinjaParser.d.ts +0 -604
  217. package/dist/generated/minijinja/MinijinjaParserListener.d.ts +0 -449
  218. package/dist/generated/minijinja/MinijinjaParserListener.js +0 -410
  219. package/dist/generated/postgres/PostgresLexer.d.ts +0 -663
  220. package/dist/generated/postgres/PostgresParser.d.ts +0 -12963
  221. package/dist/generated/postgres/PostgresParserListener.d.ts +0 -9408
  222. package/dist/generated/postgres/PostgresParserListener.js +0 -8554
  223. package/dist/generated/redshift/RedshiftLexer.d.ts +0 -954
  224. package/dist/generated/redshift/RedshiftParser.d.ts +0 -16939
  225. package/dist/generated/redshift/RedshiftParserListener.d.ts +0 -12092
  226. package/dist/generated/redshift/RedshiftParserListener.js +0 -10994
  227. package/dist/generated/snowflake/SnowflakeLexer.d.ts +0 -1046
  228. package/dist/generated/snowflake/SnowflakeParser.d.ts +0 -14196
  229. package/dist/generated/snowflake/SnowflakeParserListener.d.ts +0 -8063
  230. package/dist/generated/snowflake/SnowflakeParserListener.js +0 -7330
  231. package/dist/generated/trino/TrinoLexer.d.ts +0 -381
  232. package/dist/generated/trino/TrinoParser.d.ts +0 -5340
  233. package/dist/generated/trino/TrinoParserListener.d.ts +0 -4704
  234. package/dist/generated/trino/TrinoParserListener.js +0 -4326
  235. package/dist/generated/tsql/TSqlLexer.d.ts +0 -1278
  236. package/dist/generated/tsql/TSqlParser.d.ts +0 -17267
  237. package/dist/generated/tsql/TSqlParserListener.d.ts +0 -9697
  238. package/dist/generated/tsql/TSqlParserListener.js +0 -8854
  239. package/dist/infer/dialect.d.ts +0 -21
  240. package/dist/infer/dialect.js +0 -74
  241. package/dist/infer/literals.d.ts +0 -6
  242. package/dist/infer/literals.js +0 -44
  243. package/dist/signature/generated/tsql.d.ts +0 -3
  244. 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
+ }