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,1093 @@
1
+ import { ParserRuleContext, TerminalNode } from "antlr4ng";
2
+ import { SqliteParser as P } from "../generated/sqlite/SqliteParser.js";
3
+ import { keywordCategory, swallowedCategories, swallowedStatements } from "../ir/statement.js";
4
+ import { partSpansOf } from "../ir/part-span.js";
5
+ import { freezeIR } from "../ir/freeze.js";
6
+ // ---------------------------------------------------------------------------
7
+ // Lowering — SQLite (grammars-v4 sql/sqlite fork, Martin Mirchev's precedence-
8
+ // cascade `expr` variant) CST -> the shared, dialect-neutral IR (src/ir/ir.ts).
9
+ // The semantic layer runs on the IR unchanged; only this file knows SQLite's
10
+ // grammar. Core query path: select_stmt, select_core, join_clause,
11
+ // table_or_subquery, result_column, and the expr_or→…→expr_base cascade.
12
+ //
13
+ // Navigation is by rule index against the generated parser, never by string
14
+ // comparison of rule names. Nested `select_stmt` nodes belong to their own
15
+ // scope, so shallow walks never descend into them.
16
+ //
17
+ // Identifier-delimiter contract (docs/identifier-delimiter-contract.md): SQLite
18
+ // follows the "kept" convention shared by every dialect except BigQuery — every
19
+ // identifier field (ColumnRef.parts / TableSource.name / TableSource.alias /
20
+ // CteDef.name / Projection.name) carries its RAW text with quoting delimiters
21
+ // intact (SQLite quotes are "double", [brackets], `backticks`). Case-folding
22
+ // and delimiter-stripping for identity/display happen downstream in
23
+ // src/ident/fold.ts (foldIdentifier / displayName), never here.
24
+ // ---------------------------------------------------------------------------
25
+ // sqlite.org/lang_aggfunc.html + lang_corefunc's aggregate list — used only to set the
26
+ // `aggregate` heuristic flag (which feeds SelectExpr.aggregated); type inference is separate.
27
+ const AGGREGATES = new Set(["count", "sum", "total", "avg", "min", "max", "group_concat", "string_agg"]);
28
+ /** Lower a parsed SQLite file (`parse`: a `;`-separated sql_stmt_list) into the IR. A single
29
+ * SELECT/VALUES statement lowers fully; anything else (DDL/DML/pragma/utility, multi-statement
30
+ * batches, EXPLAIN) becomes a flagged non-query body — a valid parse never throws. */
31
+ export function lower(tree) {
32
+ const q = lowerImpl(tree);
33
+ q.dialect = "sqlite";
34
+ return freezeIR(q);
35
+ }
36
+ function lowerImpl(tree) {
37
+ const statement = statementCategory(tree);
38
+ const list = firstOfRule(tree, P.RULE_sql_stmt_list);
39
+ const stmts = list ? directChildrenOfRule(list, P.RULE_sql_stmt) : [];
40
+ // Recovery-swallowed statements count toward batch-ness: a broken statement makes recovery dump
41
+ // the rest of the batch as flat error nodes, so the sql_stmt count alone under-reports (issue #21).
42
+ const swallowed = swallowedStatements(tree);
43
+ const total = stmts.length + swallowed;
44
+ if (total !== 1 || stmts.length !== 1) {
45
+ // Anchor a multi-statement span to the FIRST statement, not the whole `parse` container (which
46
+ // reaches EOF), so a downstream AST index read doesn't see a bogus enclosure over statements 2..n.
47
+ const cst = total > 1 && stmts.length > 0 ? stmts[0] : tree;
48
+ const q = nonQuery(cst, total > 1 ? "multi-statement" : total === 1 ? "broken" : "empty");
49
+ q.statement = statement;
50
+ return q;
51
+ }
52
+ const stmt = stmts[0];
53
+ // EXPLAIN / EXPLAIN QUERY PLAN returns bytecode or a plan, not the query's rows → utility, not a query.
54
+ const select = hasDirectToken(stmt, P.EXPLAIN_) ? undefined : directChildrenOfRule(stmt, P.RULE_select_stmt)[0];
55
+ if (select) {
56
+ const q = lowerSelectStmt(select);
57
+ q.statement = statement;
58
+ return q;
59
+ }
60
+ const q = nonQuery(stmt, "non-query");
61
+ q.statement = statement;
62
+ return q;
63
+ }
64
+ // --- statement categories ------------------------------------------------------
65
+ /** Per-statement categories for every `sql_stmt` in a parsed `parse`, in source order — the
66
+ * file-level view behind statementCategory (which folds >1 into "compound"). Parity with the
67
+ * other dialects; feeds the corpus reclassifier. */
68
+ export function statementCategories(tree) {
69
+ const list = firstOfRule(tree, P.RULE_sql_stmt_list);
70
+ const stmts = list ? directChildrenOfRule(list, P.RULE_sql_stmt) : [];
71
+ return [...stmts.map(stmtCategory), ...swallowedCategories(tree)];
72
+ }
73
+ function statementCategory(tree) {
74
+ const cats = statementCategories(tree);
75
+ if (cats.length === 0)
76
+ return "other";
77
+ if (cats.length > 1)
78
+ return "compound";
79
+ return cats[0];
80
+ }
81
+ // SQLite's sql_stmt groups every statement kind as a distinct sub-rule, so the category is exact
82
+ // per rule index. Maintenance commands (ANALYZE / REINDEX / VACUUM) and ATTACH / DETACH / PRAGMA
83
+ // are session/admin utilities; transaction control is tcl; object DML/DDL are the read/write/define
84
+ // paths. EXPLAIN prefixes any of these but yields a plan, not rows → utility.
85
+ const STMT_CATEGORY = [
86
+ [P.RULE_select_stmt, "query"],
87
+ [P.RULE_insert_stmt, "dml"],
88
+ [P.RULE_update_stmt, "dml"],
89
+ [P.RULE_delete_stmt, "dml"],
90
+ [P.RULE_alter_table_stmt, "ddl"],
91
+ [P.RULE_create_index_stmt, "ddl"],
92
+ [P.RULE_create_table_stmt, "ddl"],
93
+ [P.RULE_create_trigger_stmt, "ddl"],
94
+ [P.RULE_create_view_stmt, "ddl"],
95
+ [P.RULE_create_virtual_table_stmt, "ddl"],
96
+ [P.RULE_drop_stmt, "ddl"],
97
+ [P.RULE_begin_stmt, "tcl"],
98
+ [P.RULE_commit_stmt, "tcl"],
99
+ [P.RULE_rollback_stmt, "tcl"],
100
+ [P.RULE_savepoint_stmt, "tcl"],
101
+ [P.RULE_release_stmt, "tcl"],
102
+ [P.RULE_analyze_stmt, "utility"],
103
+ [P.RULE_attach_stmt, "utility"],
104
+ [P.RULE_detach_stmt, "utility"],
105
+ [P.RULE_pragma_stmt, "utility"],
106
+ [P.RULE_reindex_stmt, "utility"],
107
+ [P.RULE_vacuum_stmt, "utility"],
108
+ ];
109
+ function stmtCategory(stmt) {
110
+ if (hasDirectToken(stmt, P.EXPLAIN_))
111
+ return "utility";
112
+ for (const [rule, cat] of STMT_CATEGORY)
113
+ if (directChildrenOfRule(stmt, rule).length)
114
+ return cat;
115
+ // No finer rule matched (mid-edit / recovery) — the leading keyword is the honest fallback.
116
+ return keywordCategory(stmt.start?.text ?? "");
117
+ }
118
+ function nonQuery(cst, reason) {
119
+ return {
120
+ kind: "query",
121
+ ctes: [],
122
+ body: { kind: "select", projections: [], from: [], columns: [], aggregated: false, unsupported: [reason], cst },
123
+ cst,
124
+ };
125
+ }
126
+ // --- SELECT statement ----------------------------------------------------------
127
+ /** select_stmt: with_clause? select_core (compound_operator select_core)* order_clause? limit_clause? */
128
+ function lowerSelectStmt(stmt) {
129
+ const withClause = directChildrenOfRule(stmt, P.RULE_with_clause)[0];
130
+ const ctes = withClause ? directChildrenOfRule(withClause, P.RULE_common_table_expression).map(lowerCte) : [];
131
+ const cores = directChildrenOfRule(stmt, P.RULE_select_core);
132
+ const ops = directChildrenOfRule(stmt, P.RULE_compound_operator);
133
+ const body = foldCompound(cores, ops, stmt);
134
+ const orderClause = directChildrenOfRule(stmt, P.RULE_order_clause)[0];
135
+ const orderBy = orderClause ? lowerOrderBy(orderClause) : undefined;
136
+ // A trailing ORDER BY resolves against the whole compound's output columns. SQLite never
137
+ // produces a pipe body, but QueryBody includes PipeExpr (no `columns`), so narrow first.
138
+ if (orderBy && body.kind !== "pipe")
139
+ for (const o of orderBy)
140
+ columnsOf(o, body.columns, "orderBy");
141
+ const limitClause = directChildrenOfRule(stmt, P.RULE_limit_clause)[0];
142
+ const limit = limitClause ? lowerLimit(limitClause) : undefined;
143
+ return { kind: "query", ctes, body, orderBy, limit, cst: stmt };
144
+ }
145
+ /** Left-fold a compound SELECT (cores interleaved with UNION/INTERSECT/EXCEPT operators).
146
+ * SQLite compound operators are left-associative. */
147
+ function foldCompound(cores, ops, cst) {
148
+ let body = cores[0] ? buildSelectCore(cores[0]) : emptyBody(cst);
149
+ for (let i = 0; i < ops.length; i++) {
150
+ const right = cores[i + 1] ? buildSelectCore(cores[i + 1]) : emptyBody(ops[i]);
151
+ body = {
152
+ kind: "setop",
153
+ op: compoundOp(ops[i]),
154
+ all: hasDirectToken(ops[i], P.ALL_),
155
+ left: body,
156
+ right,
157
+ columns: [],
158
+ cst: ops[i],
159
+ };
160
+ }
161
+ return body;
162
+ }
163
+ /** compound_operator: UNION_ ALL_? | INTERSECT_ | EXCEPT_ */
164
+ function compoundOp(op) {
165
+ if (hasDirectToken(op, P.INTERSECT_))
166
+ return "intersect";
167
+ if (hasDirectToken(op, P.EXCEPT_))
168
+ return "except";
169
+ return "union";
170
+ }
171
+ /** common_table_expression: cte_table_name AS_ (NOT_? MATERIALIZED_)? '(' select_stmt ')'
172
+ * cte_table_name: table_name ('(' column_name (',' column_name)* ')')? */
173
+ function lowerCte(cte) {
174
+ const ctn = directChildrenOfRule(cte, P.RULE_cte_table_name)[0];
175
+ const nameNode = ctn ? directChildrenOfRule(ctn, P.RULE_table_name)[0] : undefined;
176
+ const cols = ctn ? directChildrenOfRule(ctn, P.RULE_column_name).map((c) => c.getText()) : [];
177
+ const sel = directChildrenOfRule(cte, P.RULE_select_stmt)[0];
178
+ return {
179
+ name: nameNode ? nameNode.getText() : "",
180
+ nameCst: nameNode,
181
+ columnAliases: cols.length ? cols : undefined,
182
+ body: sel ? lowerSelectStmt(sel) : emptyQuery(cte),
183
+ cst: cte,
184
+ };
185
+ }
186
+ // --- the SELECT core -----------------------------------------------------------
187
+ /** select_core:
188
+ * SELECT_ (DISTINCT_|ALL_)? result_column (',' result_column)* (FROM_ join_clause)?
189
+ * (WHERE_ expr)? (GROUP_ BY_ expr (',' expr)* (HAVING_ expr)?)? (WINDOW_ …)?
190
+ * | values_clause */
191
+ function buildSelectCore(core) {
192
+ const values = directChildrenOfRule(core, P.RULE_values_clause)[0];
193
+ if (values)
194
+ return buildValues(values);
195
+ const projections = directChildrenOfRule(core, P.RULE_result_column).map(buildProjection);
196
+ const joinClause = directChildrenOfRule(core, P.RULE_join_clause)[0];
197
+ const from = joinClause ? buildSources(joinClause) : [];
198
+ const { joinConditions, onByConstraint, usingByConstraint } = joinClause
199
+ ? collectJoinData(joinClause)
200
+ : { joinConditions: [], onByConstraint: new Map(), usingByConstraint: new Map() };
201
+ const joins = joinClause ? buildJoins(joinClause, from, onByConstraint, usingByConstraint) : [];
202
+ const { whereExpr, groupBy, having } = selectCoreClauses(core);
203
+ const aggregated = (groupBy !== undefined && groupBy.length > 0) ||
204
+ projections.some((p) => hasAggregate(p.expr)) ||
205
+ (having !== undefined && hasAggregate(having));
206
+ const columns = [];
207
+ for (const p of projections)
208
+ columnsOf(p.expr, columns, "projection");
209
+ if (whereExpr)
210
+ columnsOf(whereExpr, columns, "where");
211
+ for (const j of joinConditions)
212
+ columnsOf(j, columns, "join");
213
+ for (const g of groupBy ?? [])
214
+ columnsOf(g, columns, "groupBy");
215
+ if (having)
216
+ columnsOf(having, columns, "having");
217
+ const subqueries = extractExpressionSubqueries(core);
218
+ return {
219
+ kind: "select",
220
+ projections,
221
+ from,
222
+ columns,
223
+ where: whereExpr,
224
+ joinConditions: joinConditions.length ? joinConditions : undefined,
225
+ joins: joins.length ? joins : undefined,
226
+ groupBy,
227
+ having,
228
+ aggregated,
229
+ subqueries: subqueries.length ? subqueries : undefined,
230
+ cst: core,
231
+ };
232
+ }
233
+ // --- expression subqueries (scalar / IN / EXISTS) --------------------------------
234
+ /** Expression subqueries appearing in this select's expressions (SELECT list, WHERE, GROUP BY,
235
+ * HAVING, JOIN ON, TVF args) — every `select_stmt` reachable without crossing another select_stmt,
236
+ * EXCLUDING the FROM subqueries (a `table_or_subquery`'s direct select_stmt child is a Source,
237
+ * scoped separately by buildSource). Mirrors snowflake's extractExpressionSubqueries: the scope
238
+ * pass discovers expression-nested subqueries ONLY through `SelectExpr.subqueries`, so their
239
+ * (possibly correlated) columns resolve as children of this scope. */
240
+ function extractExpressionSubqueries(core) {
241
+ const out = [];
242
+ const walk = (n) => {
243
+ for (const child of kidsOf(n)) {
244
+ if (!(child instanceof ParserRuleContext))
245
+ continue;
246
+ if (child.ruleIndex === P.RULE_select_stmt) {
247
+ // A table_or_subquery's select_stmt is a FROM source, not an expression subquery.
248
+ if (!isRule(child.parent ?? undefined, P.RULE_table_or_subquery))
249
+ out.push(lowerSelectStmt(child));
250
+ continue; // its own scope — don't descend
251
+ }
252
+ walk(child);
253
+ }
254
+ };
255
+ walk(core);
256
+ return out;
257
+ }
258
+ /** The WHERE / GROUP BY / HAVING exprs of a select_core. They are all direct `expr` children of
259
+ * select_core (result_column and join_clause exprs are nested, so never direct children); classify
260
+ * each by the keyword token that most recently preceded it. */
261
+ function selectCoreClauses(core) {
262
+ let mode;
263
+ let whereExpr;
264
+ let having;
265
+ const groupBy = [];
266
+ for (const c of kidsOf(core)) {
267
+ if (c instanceof TerminalNode) {
268
+ const t = c.symbol.type;
269
+ if (t === P.WHERE_)
270
+ mode = "where";
271
+ else if (t === P.GROUP_)
272
+ mode = "group";
273
+ else if (t === P.HAVING_)
274
+ mode = "having";
275
+ else if (t === P.WINDOW_)
276
+ mode = undefined;
277
+ continue;
278
+ }
279
+ if (c instanceof ParserRuleContext && c.ruleIndex === P.RULE_expr) {
280
+ const e = lowerExpr(c);
281
+ if (mode === "where")
282
+ whereExpr = e;
283
+ else if (mode === "group")
284
+ groupBy.push(e);
285
+ else if (mode === "having")
286
+ having = e;
287
+ }
288
+ }
289
+ return { whereExpr, groupBy: groupBy.length ? groupBy : undefined, having };
290
+ }
291
+ /** values_clause: VALUES_ value_row (',' value_row)* / value_row: '(' expr (',' expr)* ')'.
292
+ * Lowers to a modelled select whose projections carry the row exprs, named column1…columnN
293
+ * (SQLite's default VALUES output names). */
294
+ function buildValues(values) {
295
+ const firstRow = directChildrenOfRule(values, P.RULE_value_row)[0];
296
+ const exprs = firstRow ? directChildrenOfRule(firstRow, P.RULE_expr) : [];
297
+ const projections = exprs.map((e, i) => ({
298
+ name: `column${i + 1}`,
299
+ isStar: false,
300
+ expr: lowerExpr(e),
301
+ cst: e,
302
+ }));
303
+ const columns = [];
304
+ for (const p of projections)
305
+ columnsOf(p.expr, columns, "projection");
306
+ const subqueries = extractExpressionSubqueries(values);
307
+ return {
308
+ kind: "select",
309
+ projections,
310
+ from: [],
311
+ columns,
312
+ aggregated: false,
313
+ subqueries: subqueries.length ? subqueries : undefined,
314
+ cst: values,
315
+ };
316
+ }
317
+ // --- projections ---------------------------------------------------------------
318
+ /** result_column: STAR | table_name '.' STAR | expr (AS_? column_alias)? */
319
+ function buildProjection(rc) {
320
+ if (hasDirectToken(rc, P.STAR)) {
321
+ const tableName = directChildrenOfRule(rc, P.RULE_table_name)[0];
322
+ const expr = { kind: "star", qualifier: tableName ? [tableName.getText()] : undefined, cst: rc };
323
+ return { name: undefined, isStar: true, expr, cst: rc };
324
+ }
325
+ const exprNode = directChildrenOfRule(rc, P.RULE_expr)[0];
326
+ const expr = exprNode ? lowerExpr(exprNode) : otherExpr(rc);
327
+ const aliasNode = directChildrenOfRule(rc, P.RULE_column_alias)[0];
328
+ let name = aliasNode ? aliasNode.getText() : undefined;
329
+ if (name === undefined && expr.kind === "column")
330
+ name = expr.parts[expr.parts.length - 1];
331
+ return { name, isStar: false, expr, ...(aliasNode ? { aliasCst: aliasNode } : {}), cst: rc };
332
+ }
333
+ // --- sources -------------------------------------------------------------------
334
+ /** The base sources of a join_clause, in source order. A parenthesized join `( join_clause )` is a
335
+ * grouping (NOT a derived table — its columns stay directly visible), so its inner sources are
336
+ * flattened into this level rather than hidden behind an anonymous subquery. */
337
+ function buildSources(joinClause) {
338
+ const out = [];
339
+ const addSource = (tos) => {
340
+ const sel = directChildrenOfRule(tos, P.RULE_select_stmt)[0];
341
+ const nested = sel ? undefined : directChildrenOfRule(tos, P.RULE_join_clause)[0];
342
+ if (nested)
343
+ out.push(...buildSources(nested));
344
+ else
345
+ out.push(buildSource(tos));
346
+ };
347
+ // join_clause: table_or_subquery join_step*. The left operand is a direct table_or_subquery child;
348
+ // each subsequent operand lives inside a join_step. Direct-then-steps preserves source order.
349
+ for (const tos of directChildrenOfRule(joinClause, P.RULE_table_or_subquery))
350
+ addSource(tos);
351
+ for (const step of directChildrenOfRule(joinClause, P.RULE_join_step)) {
352
+ const tos = directChildrenOfRule(step, P.RULE_table_or_subquery)[0];
353
+ if (tos)
354
+ addSource(tos);
355
+ }
356
+ return out;
357
+ }
358
+ /** table_or_subquery — one of:
359
+ * (schema_name '.')? table_name (AS_ table_alias | table_alias_excluding_joins)? …
360
+ * | (schema_name '.')? table_function_name '(' expr (',' expr)* ')' (AS_? table_alias)?
361
+ * | '(' select_stmt ')' (AS_? table_alias)? */
362
+ function buildSource(tos) {
363
+ const { alias, aliasCst } = tableAliasOf(tos);
364
+ // '(' select_stmt ')' — a derived table.
365
+ const sel = directChildrenOfRule(tos, P.RULE_select_stmt)[0];
366
+ if (sel) {
367
+ return { kind: "subquery", query: lowerSelectStmt(sel), alias, aliasCst, cst: tos };
368
+ }
369
+ // table_function_name(args) — a table-valued function; its output columns need the signature
370
+ // (unknown without a catalog, never wrong — the inference contract).
371
+ const tfn = directChildrenOfRule(tos, P.RULE_table_function_name)[0];
372
+ if (tfn) {
373
+ const schemaName = directChildrenOfRule(tos, P.RULE_schema_name)[0];
374
+ const partNodes = [schemaName, tfn].filter((n) => n !== undefined);
375
+ return {
376
+ kind: "table",
377
+ name: partNodes.map((n) => n.getText()),
378
+ namePartSpans: partSpansOf(partNodes),
379
+ alias,
380
+ aliasCst,
381
+ cst: tos,
382
+ };
383
+ }
384
+ // (schema_name '.')? table_name — a plain table reference.
385
+ const tableName = directChildrenOfRule(tos, P.RULE_table_name)[0];
386
+ const schemaName = directChildrenOfRule(tos, P.RULE_schema_name)[0];
387
+ const partNodes = [schemaName, tableName].filter((n) => n !== undefined);
388
+ const parts = partNodes.map((n) => n.getText());
389
+ return {
390
+ kind: "table",
391
+ name: parts.length ? parts : [tos.getText()],
392
+ namePartSpans: partNodes.length ? partSpansOf(partNodes) : undefined,
393
+ alias,
394
+ aliasCst,
395
+ cst: tos,
396
+ };
397
+ }
398
+ /** The FROM-source alias: `AS table_alias` / bare `table_alias_excluding_joins` (plain table),
399
+ * or `AS? table_alias` (subquery / table function). */
400
+ function tableAliasOf(tos) {
401
+ const a = directChildrenOfRule(tos, P.RULE_table_alias)[0] ??
402
+ directChildrenOfRule(tos, P.RULE_table_alias_excluding_joins)[0];
403
+ return a ? { alias: a.getText(), aliasCst: a } : {};
404
+ }
405
+ // --- joins ---------------------------------------------------------------------
406
+ /** ON exprs and USING column lists for every join_constraint in this select's join tree (descending
407
+ * through parenthesized nested join_clauses, but NOT into subquery select_stmts). The ON exprs are
408
+ * lowered ONCE and keyed by their join_constraint CST so buildJoins shares the same Expr objects
409
+ * (the reference-identity contract between joinConditions and joins). */
410
+ function collectJoinData(joinClause) {
411
+ const joinConditions = [];
412
+ const onByConstraint = new Map();
413
+ const usingByConstraint = new Map();
414
+ for (const jc of shallowCollectOfRule(joinClause, P.RULE_join_constraint)) {
415
+ const onExpr = directChildrenOfRule(jc, P.RULE_expr)[0];
416
+ if (onExpr) {
417
+ const e = lowerExpr(onExpr);
418
+ joinConditions.push(e);
419
+ onByConstraint.set(jc, e);
420
+ }
421
+ else {
422
+ const cols = directChildrenOfRule(jc, P.RULE_column_name).map((c) => c.getText());
423
+ if (cols.length)
424
+ usingByConstraint.set(jc, cols);
425
+ }
426
+ }
427
+ return { joinConditions, onByConstraint, usingByConstraint };
428
+ }
429
+ /** join_clause: table_or_subquery join_step* / join_step: join_operator table_or_subquery
430
+ * join_constraint?. One Join per join_step whose operator is an explicit JOIN (a bare COMMA step is a
431
+ * plain FROM entry, not a join — the IR contract). Each join.source is the reference-identical `from`
432
+ * entry for that step's right table_or_subquery; join.on is the shared Expr from onByConstraint.
433
+ * Join.cst is the `join_step` node itself, so its span is the full `[type] JOIN <table> [ON …|USING …]`
434
+ * construct (the src/ir/ir.ts Join.cst contract) — the reason the grammar carries the join_step rule. */
435
+ function buildJoins(joinClause, from, onByConstraint, usingByConstraint) {
436
+ const joins = [];
437
+ for (const step of directChildrenOfRule(joinClause, P.RULE_join_step)) {
438
+ const op = directChildrenOfRule(step, P.RULE_join_operator)[0];
439
+ if (!op || hasDirectToken(op, P.COMMA))
440
+ continue; // comma → plain from entry, not a join
441
+ const tos = directChildrenOfRule(step, P.RULE_table_or_subquery)[0];
442
+ if (!tos)
443
+ continue;
444
+ const source = sourceFor(from, tos);
445
+ if (!source)
446
+ continue;
447
+ const jc = directChildrenOfRule(step, P.RULE_join_constraint)[0];
448
+ const { kind, natural } = joinKind(op);
449
+ const on = jc ? onByConstraint.get(jc) : undefined;
450
+ const using = jc ? usingByConstraint.get(jc) : undefined;
451
+ joins.push({ kind, source, on, using, natural: natural || undefined, cst: step });
452
+ }
453
+ return joins;
454
+ }
455
+ /** The `from` entry for a join's right table_or_subquery: the source whose cst IS that node, or (a
456
+ * flattened parenthesized-join operand) the first source nested within it. */
457
+ function sourceFor(from, tos) {
458
+ return from.find((s) => s.cst === tos) ?? from.find((s) => isWithin(s.cst, tos));
459
+ }
460
+ /** join_operator: COMMA | NATURAL_? ((LEFT_|RIGHT_|FULL_) OUTER_? | INNER_ | CROSS_)? JOIN_ */
461
+ function joinKind(op) {
462
+ const natural = hasDirectToken(op, P.NATURAL_);
463
+ if (hasDirectToken(op, P.CROSS_))
464
+ return { kind: "cross", natural };
465
+ if (hasDirectToken(op, P.LEFT_))
466
+ return { kind: "left", natural };
467
+ if (hasDirectToken(op, P.RIGHT_))
468
+ return { kind: "right", natural };
469
+ if (hasDirectToken(op, P.FULL_))
470
+ return { kind: "full", natural };
471
+ if (hasDirectToken(op, P.INNER_))
472
+ return { kind: "inner", natural };
473
+ return { kind: natural ? "natural" : "inner", natural };
474
+ }
475
+ // --- ORDER BY / LIMIT ----------------------------------------------------------
476
+ /** order_clause: ORDER_ BY_ ordering_term (',' ordering_term)* / ordering_term: expr … */
477
+ function lowerOrderBy(orderClause) {
478
+ const items = orderingTermExprs(orderClause).map(lowerExpr);
479
+ return items.length ? items : undefined;
480
+ }
481
+ function orderingTermExprs(orderClause) {
482
+ return directChildrenOfRule(orderClause, P.RULE_ordering_term)
483
+ .map((ot) => directChildrenOfRule(ot, P.RULE_expr)[0])
484
+ .filter((e) => e !== undefined);
485
+ }
486
+ /** limit_clause: LIMIT_ expr ((OFFSET_ | COMMA) expr)?. The comma form is SQLite's `LIMIT offset,
487
+ * count`; the OFFSET form is `LIMIT count OFFSET offset`. */
488
+ function lowerLimit(limitClause) {
489
+ const exprs = directChildrenOfRule(limitClause, P.RULE_expr);
490
+ if (exprs.length === 0)
491
+ return undefined;
492
+ const info = {};
493
+ if (hasDirectToken(limitClause, P.COMMA)) {
494
+ info.offset = lowerExpr(exprs[0]);
495
+ if (exprs[1])
496
+ info.top = lowerExpr(exprs[1]);
497
+ }
498
+ else {
499
+ info.top = lowerExpr(exprs[0]);
500
+ if (exprs[1])
501
+ info.offset = lowerExpr(exprs[1]);
502
+ }
503
+ return info;
504
+ }
505
+ // --- the expr precedence cascade ----------------------------------------------
506
+ // expr → expr_or → expr_and → expr_not → expr_binary → expr_comparison → expr_bitwise
507
+ // → expr_addition → expr_multiplication → expr_string → expr_collate → expr_unary
508
+ // → expr_base → expr_recursive
509
+ function lowerExpr(node) {
510
+ switch (node.ruleIndex) {
511
+ case P.RULE_expr: {
512
+ const c = directChildrenOfRule(node, P.RULE_expr_or)[0];
513
+ return c ? lowerExpr(c) : otherExpr(node);
514
+ }
515
+ case P.RULE_expr_or:
516
+ return foldLeft(node, P.RULE_expr_and, (t) => (t === P.OR_ ? "or" : undefined));
517
+ case P.RULE_expr_and:
518
+ return foldLeft(node, P.RULE_expr_not, (t) => (t === P.AND_ ? "and" : undefined));
519
+ case P.RULE_expr_not:
520
+ return lowerNot(node);
521
+ case P.RULE_expr_binary:
522
+ return lowerBinary(node);
523
+ case P.RULE_expr_comparison:
524
+ return foldLeft(node, P.RULE_expr_bitwise, comparisonOp);
525
+ case P.RULE_expr_bitwise:
526
+ return foldLeft(node, P.RULE_expr_addition, bitwiseOp);
527
+ case P.RULE_expr_addition:
528
+ return foldLeft(node, P.RULE_expr_multiplication, (t) => t === P.PLUS ? "+" : t === P.MINUS ? "-" : undefined);
529
+ case P.RULE_expr_multiplication:
530
+ return foldLeft(node, P.RULE_expr_string, (t) => t === P.STAR ? "*" : t === P.DIV ? "/" : t === P.MOD ? "%" : undefined);
531
+ case P.RULE_expr_string:
532
+ return foldLeft(node, P.RULE_expr_collate, (t) => t === P.PIPE2 ? "||" : t === P.JPTR ? "->" : t === P.JPTR2 ? "->>" : undefined);
533
+ case P.RULE_expr_collate:
534
+ return lowerCollate(node);
535
+ case P.RULE_expr_unary:
536
+ return lowerUnary(node);
537
+ case P.RULE_expr_base:
538
+ return lowerExprBase(node);
539
+ case P.RULE_expr_recursive:
540
+ return lowerExprRecursive(node);
541
+ default:
542
+ return otherExpr(node);
543
+ }
544
+ }
545
+ function comparisonOp(t) {
546
+ return t === P.LT ? "<" : t === P.LT_EQ ? "<=" : t === P.GT ? ">" : t === P.GT_EQ ? ">=" : undefined;
547
+ }
548
+ function bitwiseOp(t) {
549
+ return t === P.LT2 ? "<<" : t === P.GT2 ? ">>" : t === P.AMP ? "&" : t === P.PIPE ? "|" : undefined;
550
+ }
551
+ /** A left-associative `child (OP child)*` cascade level: fold the operand rule-nodes into a chain of
552
+ * binary exprs, taking the operator string from the interleaved tokens. A single operand passes
553
+ * through untouched. */
554
+ function foldLeft(node, childRule, opFor) {
555
+ let left;
556
+ let op;
557
+ for (const c of kidsOf(node)) {
558
+ if (c instanceof TerminalNode) {
559
+ const o = opFor(c.symbol.type);
560
+ if (o !== undefined)
561
+ op = o;
562
+ continue;
563
+ }
564
+ if (c instanceof ParserRuleContext && c.ruleIndex === childRule) {
565
+ const e = lowerExpr(c);
566
+ left = left === undefined ? e : { kind: "binary", op: op ?? "", left, right: e, cst: node };
567
+ }
568
+ }
569
+ return left ?? otherExpr(node);
570
+ }
571
+ /** expr_not: NOT_* expr_binary — wrap in a `not` unary once per leading NOT. */
572
+ function lowerNot(node) {
573
+ const inner = directChildrenOfRule(node, P.RULE_expr_binary)[0];
574
+ let e = inner ? lowerExpr(inner) : otherExpr(node);
575
+ for (const c of kidsOf(node)) {
576
+ if (c instanceof TerminalNode && c.symbol.type === P.NOT_)
577
+ e = { kind: "unary", op: "not", operand: e, cst: node };
578
+ }
579
+ return e;
580
+ }
581
+ /** expr_collate: expr_unary (COLLATE_ collation_name)* — COLLATE is a passthrough (no column refs). */
582
+ function lowerCollate(node) {
583
+ const inner = directChildrenOfRule(node, P.RULE_expr_unary)[0];
584
+ return inner ? lowerExpr(inner) : otherExpr(node);
585
+ }
586
+ /** expr_unary: (MINUS | PLUS | TILDE)* expr_base — wrap each leading sign, innermost binds tightest. */
587
+ function lowerUnary(node) {
588
+ const base = directChildrenOfRule(node, P.RULE_expr_base)[0];
589
+ let e = base ? lowerExpr(base) : otherExpr(node);
590
+ const signs = [];
591
+ for (const c of kidsOf(node)) {
592
+ if (c instanceof TerminalNode &&
593
+ (c.symbol.type === P.MINUS || c.symbol.type === P.PLUS || c.symbol.type === P.TILDE)) {
594
+ signs.push(c.symbol.type);
595
+ }
596
+ }
597
+ for (let i = signs.length - 1; i >= 0; i--) {
598
+ const op = signs[i] === P.MINUS ? "-" : signs[i] === P.PLUS ? "+" : "~";
599
+ e = { kind: "unary", op, operand: e, cst: node };
600
+ }
601
+ return e;
602
+ }
603
+ /** expr_base:
604
+ * literal_value | BIND_PARAMETER | (schema_name '.')? table_name '.' column_name
605
+ * | column_name_excluding_string | (NOT_? EXISTS_)? '(' select_stmt ')' | raise_function
606
+ * | expr_recursive */
607
+ function lowerExprBase(node) {
608
+ const lit = directChildrenOfRule(node, P.RULE_literal_value)[0];
609
+ if (lit)
610
+ return { kind: "literal", text: lit.getText(), cst: node };
611
+ if (hasDirectToken(node, P.BIND_PARAMETER))
612
+ return { kind: "literal", text: node.getText(), cst: node };
613
+ // (schema_name '.')? table_name '.' column_name — a qualified column reference.
614
+ const colName = directChildrenOfRule(node, P.RULE_column_name)[0];
615
+ if (colName) {
616
+ const schemaName = directChildrenOfRule(node, P.RULE_schema_name)[0];
617
+ const tableName = directChildrenOfRule(node, P.RULE_table_name)[0];
618
+ const partNodes = [schemaName, tableName, colName].filter((n) => n !== undefined);
619
+ return {
620
+ kind: "column",
621
+ parts: partNodes.map((n) => n.getText()),
622
+ partSpans: partSpansOf(partNodes),
623
+ cst: node,
624
+ };
625
+ }
626
+ // column_name_excluding_string — a bare (unqualified) column reference.
627
+ const bareCol = directChildrenOfRule(node, P.RULE_column_name_excluding_string)[0];
628
+ if (bareCol) {
629
+ return { kind: "column", parts: [bareCol.getText()], partSpans: partSpansOf([bareCol]), cst: node };
630
+ }
631
+ // (NOT_? EXISTS_)? '(' select_stmt ')' — a scalar subquery or an [NOT] EXISTS test.
632
+ const sel = directChildrenOfRule(node, P.RULE_select_stmt)[0];
633
+ if (sel) {
634
+ if (hasDirectToken(node, P.EXISTS_)) {
635
+ const exists = { kind: "exists", query: lowerSelectStmt(sel), cst: node };
636
+ return hasDirectToken(node, P.NOT_) ? { kind: "unary", op: "not", operand: exists, cst: node } : exists;
637
+ }
638
+ return { kind: "subquery", query: lowerSelectStmt(sel), cst: node };
639
+ }
640
+ const rec = directChildrenOfRule(node, P.RULE_expr_recursive)[0];
641
+ if (rec)
642
+ return lowerExprRecursive(rec);
643
+ return otherExpr(node); // raise_function, or an unmodelled shape — columns recovered from the CST
644
+ }
645
+ /** expr_recursive:
646
+ * function_name '(' (DISTINCT_? expr (',' expr)* order_clause? | STAR)? ')' percentile? filter? over?
647
+ * | '(' expr (',' expr)* ')' | CAST_ '(' expr AS_ type_name ')' | CASE_ … */
648
+ function lowerExprRecursive(node) {
649
+ const fn = directChildrenOfRule(node, P.RULE_function_name)[0];
650
+ if (fn)
651
+ return lowerFunction(node, fn);
652
+ if (hasDirectToken(node, P.CAST_)) {
653
+ const inner = directChildrenOfRule(node, P.RULE_expr)[0];
654
+ const tn = directChildrenOfRule(node, P.RULE_type_name)[0];
655
+ return {
656
+ kind: "cast",
657
+ expr: inner ? lowerExpr(inner) : otherExpr(node),
658
+ typeText: tn ? tn.getText() : "",
659
+ cst: node,
660
+ };
661
+ }
662
+ if (hasDirectToken(node, P.CASE_))
663
+ return lowerCase(node);
664
+ // '(' expr (',' expr)* ')' — a parenthesized single expr is grouping (passthrough); a comma tuple
665
+ // has no IR node, so it stays `other` (columnsOf recovers its column refs from the CST).
666
+ const exprs = directChildrenOfRule(node, P.RULE_expr);
667
+ if (exprs.length === 1)
668
+ return lowerExpr(exprs[0]);
669
+ return otherExpr(node);
670
+ }
671
+ function lowerFunction(node, fnNode) {
672
+ const name = fnNode.getText().toLowerCase();
673
+ const argExprs = directChildrenOfRule(node, P.RULE_expr).map(lowerExpr);
674
+ // aggregate ORDER BY (group_concat(x ORDER BY y)), WITHIN GROUP (percentile) and FILTER (WHERE …)
675
+ // all feed the call — conserve their column refs as arguments.
676
+ const orderClause = directChildrenOfRule(node, P.RULE_order_clause)[0];
677
+ const orderArgs = orderClause ? orderingTermExprs(orderClause).map(lowerExpr) : [];
678
+ const pc = directChildrenOfRule(node, P.RULE_percentile_clause)[0];
679
+ const pcArgs = pc ? directChildrenOfRule(pc, P.RULE_expr).map(lowerExpr) : [];
680
+ const fc = directChildrenOfRule(node, P.RULE_filter_clause)[0];
681
+ const fcArgs = fc ? directChildrenOfRule(fc, P.RULE_expr).map(lowerExpr) : [];
682
+ const over = directChildrenOfRule(node, P.RULE_over_clause)[0];
683
+ return {
684
+ kind: "function",
685
+ name,
686
+ args: [...argExprs, ...orderArgs, ...pcArgs, ...fcArgs],
687
+ aggregate: AGGREGATES.has(name),
688
+ distinct: hasDirectToken(node, P.DISTINCT_),
689
+ window: over ? lowerOver(over) : undefined,
690
+ cst: node,
691
+ };
692
+ }
693
+ /** over_clause: OVER_ (window_name | '(' base_window_name? (PARTITION_ BY_ expr (',' expr)*)?
694
+ * order_clause? frame_spec? ')'). PARTITION BY exprs are direct expr children; ORDER BY exprs are
695
+ * nested in the order_clause. */
696
+ function lowerOver(over) {
697
+ const partitionBy = directChildrenOfRule(over, P.RULE_expr).map(lowerExpr);
698
+ const orderClause = directChildrenOfRule(over, P.RULE_order_clause)[0];
699
+ const orderBy = orderClause ? orderingTermExprs(orderClause).map(lowerExpr) : [];
700
+ return { partitionBy, orderBy, cst: over };
701
+ }
702
+ /** CASE_ expr? (WHEN_ expr THEN_ expr)+ (ELSE_ expr)? END_. A simple CASE with a subject desugars to
703
+ * `subject = whenValue` so the subject's columns/types are seen (mirrors the other dialects). */
704
+ function lowerCase(node) {
705
+ let subject;
706
+ let elseExpr;
707
+ let pendingWhen;
708
+ let mode = "subject";
709
+ const whens = [];
710
+ for (const c of kidsOf(node)) {
711
+ if (c instanceof TerminalNode) {
712
+ const t = c.symbol.type;
713
+ if (t === P.WHEN_)
714
+ mode = "when";
715
+ else if (t === P.THEN_)
716
+ mode = "then";
717
+ else if (t === P.ELSE_)
718
+ mode = "else";
719
+ continue;
720
+ }
721
+ if (!(c instanceof ParserRuleContext && c.ruleIndex === P.RULE_expr))
722
+ continue;
723
+ const e = lowerExpr(c);
724
+ if (mode === "subject")
725
+ subject = e;
726
+ else if (mode === "when")
727
+ pendingWhen = e;
728
+ else if (mode === "then") {
729
+ whens.push({ when: pendingWhen ?? otherExpr(node), then: e });
730
+ pendingWhen = undefined;
731
+ }
732
+ else
733
+ elseExpr = e;
734
+ }
735
+ if (subject) {
736
+ const desugared = whens.map((w) => ({
737
+ when: { kind: "binary", op: "=", left: subject, right: w.when, cst: node },
738
+ then: w.then,
739
+ }));
740
+ return { kind: "case", whens: desugared, elseExpr, cst: node };
741
+ }
742
+ return { kind: "case", whens, elseExpr, cst: node };
743
+ }
744
+ // --- expr_binary (the postfix predicate loop) ----------------------------------
745
+ // expr_binary: expr_comparison (
746
+ // (ASSIGN|EQ|NOT_EQ1|NOT_EQ2) expr_comparison
747
+ // | IS_ NOT_? (DISTINCT_ FROM_)? expr_comparison
748
+ // | NOT_? BETWEEN_ expr_comparison AND_ expr_comparison
749
+ // | NOT_? IN_ ( '(' (select_stmt | expr_comparison (',' expr_comparison)*)? ')'
750
+ // | (schema_name '.')? table_name
751
+ // | (schema_name '.')? table_function_name '(' (expr_comparison …)? ')' )
752
+ // | NOT_? ( LIKE_ expr_comparison (ESCAPE_ expr_comparison)? | (GLOB_|REGEXP_|MATCH_) expr_comparison )
753
+ // | ISNULL_ | NOTNULL_ | NOT_ NULL_
754
+ // )*
755
+ function lowerBinary(node) {
756
+ const kids = kidsOf(node);
757
+ const firstR = nextRule(kids, 0, P.RULE_expr_comparison);
758
+ let left = firstR ? lowerExpr(firstR.node) : otherExpr(node);
759
+ let i = firstR ? firstR.index + 1 : kids.length;
760
+ const operand = (from) => {
761
+ const r = nextRule(kids, from, P.RULE_expr_comparison);
762
+ return r ? { expr: lowerExpr(r.node), index: r.index + 1 } : { expr: otherExpr(node), index: from };
763
+ };
764
+ while (i < kids.length) {
765
+ const c = kids[i];
766
+ if (!(c instanceof TerminalNode)) {
767
+ i++;
768
+ continue;
769
+ }
770
+ const t = c.symbol.type;
771
+ if (t === P.ASSIGN || t === P.EQ || t === P.NOT_EQ1 || t === P.NOT_EQ2) {
772
+ const { expr, index } = operand(i + 1);
773
+ left = { kind: "binary", op: c.getText(), left, right: expr, cst: node };
774
+ i = index;
775
+ continue;
776
+ }
777
+ if (t === P.IS_) {
778
+ i++;
779
+ let negated = false;
780
+ if (isToken(kids[i], P.NOT_)) {
781
+ negated = true;
782
+ i++;
783
+ }
784
+ const distinct = isToken(kids[i], P.DISTINCT_);
785
+ if (distinct) {
786
+ i++;
787
+ if (isToken(kids[i], P.FROM_))
788
+ i++;
789
+ }
790
+ const { expr, index } = operand(i);
791
+ i = index;
792
+ left = distinct
793
+ ? { kind: "predicate", op: "distinct from", negated, operand: left, args: [expr], cst: node }
794
+ : { kind: "binary", op: negated ? "is not" : "is", left, right: expr, cst: node };
795
+ continue;
796
+ }
797
+ let negated = false;
798
+ if (t === P.NOT_) {
799
+ // NOT NULL is a nullness predicate; otherwise NOT prefixes BETWEEN / IN / LIKE-family.
800
+ if (isToken(kids[i + 1], P.NULL_)) {
801
+ left = { kind: "predicate", op: "null", negated: true, operand: left, args: [], cst: node };
802
+ i += 2;
803
+ continue;
804
+ }
805
+ negated = true;
806
+ i++;
807
+ }
808
+ const ot = kids[i] instanceof TerminalNode ? kids[i].symbol.type : undefined;
809
+ if (ot === P.BETWEEN_) {
810
+ const lo = operand(i + 1);
811
+ const hi = operand(lo.index);
812
+ left = { kind: "predicate", op: "between", negated, operand: left, args: [lo.expr, hi.expr], cst: node };
813
+ i = hi.index;
814
+ continue;
815
+ }
816
+ if (ot === P.IN_) {
817
+ const { args, nextIndex } = consumeInPayload(kids, i + 1);
818
+ left = { kind: "predicate", op: "in", negated, operand: left, args, cst: node };
819
+ i = nextIndex;
820
+ continue;
821
+ }
822
+ if (ot === P.LIKE_ || ot === P.GLOB_ || ot === P.REGEXP_ || ot === P.MATCH_) {
823
+ const op = ot === P.LIKE_ ? "like" : ot === P.GLOB_ ? "glob" : ot === P.REGEXP_ ? "rlike" : "match";
824
+ const pat = operand(i + 1);
825
+ const args = [pat.expr];
826
+ i = pat.index;
827
+ if (isToken(kids[i], P.ESCAPE_)) {
828
+ const esc = operand(i + 1);
829
+ args.push(esc.expr);
830
+ i = esc.index;
831
+ }
832
+ left = { kind: "predicate", op, negated, operand: left, args, cst: node };
833
+ continue;
834
+ }
835
+ if (ot === P.ISNULL_) {
836
+ left = { kind: "predicate", op: "null", negated: false, operand: left, args: [], cst: node };
837
+ i++;
838
+ continue;
839
+ }
840
+ if (ot === P.NOTNULL_) {
841
+ left = { kind: "predicate", op: "null", negated: true, operand: left, args: [], cst: node };
842
+ i++;
843
+ continue;
844
+ }
845
+ i++; // an unrecognized token — skip, never throw
846
+ }
847
+ return left;
848
+ }
849
+ /** Consume an IN payload starting at `from` (just after the IN_ token): a parenthesized
850
+ * select_stmt / expr_comparison list, a bare (schema.)?table_name, or a table_function_name(args).
851
+ * Returns the collected args and the index past the payload. */
852
+ function consumeInPayload(kids, from) {
853
+ const args = [];
854
+ const push = (c) => {
855
+ if (c.ruleIndex === P.RULE_select_stmt)
856
+ args.push({ kind: "subquery", query: lowerSelectStmt(c), cst: c });
857
+ else if (c.ruleIndex === P.RULE_expr_comparison)
858
+ args.push(lowerExpr(c));
859
+ };
860
+ let i = from;
861
+ if (isToken(kids[i], P.OPEN_PAR)) {
862
+ let depth = 0;
863
+ for (; i < kids.length; i++) {
864
+ const c = kids[i];
865
+ if (c instanceof TerminalNode) {
866
+ if (c.symbol.type === P.OPEN_PAR)
867
+ depth++;
868
+ else if (c.symbol.type === P.CLOSE_PAR && --depth === 0) {
869
+ i++;
870
+ break;
871
+ }
872
+ }
873
+ else if (c instanceof ParserRuleContext)
874
+ push(c);
875
+ }
876
+ return { args, nextIndex: i };
877
+ }
878
+ // Non-paren forms: consume the name rule-nodes (and a following table-function paren group).
879
+ while (i < kids.length) {
880
+ const c = kids[i];
881
+ if (isRule(c, P.RULE_schema_name) || isRule(c, P.RULE_table_name) || isRule(c, P.RULE_table_function_name)) {
882
+ i++;
883
+ continue;
884
+ }
885
+ if (isToken(c, P.DOT)) {
886
+ i++;
887
+ continue;
888
+ }
889
+ if (isToken(c, P.OPEN_PAR)) {
890
+ let depth = 0;
891
+ for (; i < kids.length; i++) {
892
+ const k = kids[i];
893
+ if (k instanceof TerminalNode) {
894
+ if (k.symbol.type === P.OPEN_PAR)
895
+ depth++;
896
+ else if (k.symbol.type === P.CLOSE_PAR && --depth === 0) {
897
+ i++;
898
+ break;
899
+ }
900
+ }
901
+ else if (k instanceof ParserRuleContext)
902
+ push(k);
903
+ }
904
+ }
905
+ break;
906
+ }
907
+ return { args, nextIndex: i };
908
+ }
909
+ // --- column extraction (single source of truth for SelectExpr.columns) ---------
910
+ function columnsOf(expr, acc, clause) {
911
+ switch (expr.kind) {
912
+ case "column":
913
+ acc.push({ kind: "columnref", parts: expr.parts, clause, cst: expr.cst, partSpans: expr.partSpans });
914
+ break;
915
+ case "binary":
916
+ columnsOf(expr.left, acc, clause);
917
+ columnsOf(expr.right, acc, clause);
918
+ break;
919
+ case "unary":
920
+ columnsOf(expr.operand, acc, clause);
921
+ break;
922
+ case "cast":
923
+ columnsOf(expr.expr, acc, clause);
924
+ break;
925
+ case "function":
926
+ expr.args.forEach((a) => columnsOf(a, acc, clause));
927
+ expr.window?.partitionBy.forEach((a) => columnsOf(a, acc, clause));
928
+ expr.window?.orderBy.forEach((a) => columnsOf(a, acc, clause));
929
+ break;
930
+ case "case":
931
+ expr.whens.forEach((w) => {
932
+ columnsOf(w.when, acc, clause);
933
+ columnsOf(w.then, acc, clause);
934
+ });
935
+ if (expr.elseExpr)
936
+ columnsOf(expr.elseExpr, acc, clause);
937
+ break;
938
+ case "predicate":
939
+ columnsOf(expr.operand, acc, clause);
940
+ expr.args.forEach((a) => columnsOf(a, acc, clause));
941
+ break;
942
+ case "subscript":
943
+ columnsOf(expr.base, acc, clause);
944
+ columnsOf(expr.index, acc, clause);
945
+ break;
946
+ case "other":
947
+ cstColumnRefs(expr.cst, acc, clause);
948
+ break;
949
+ // literal / star / subquery / exists / lambda / with → no column refs at this level
950
+ }
951
+ }
952
+ /** Fallback: recover column references from inside an unmodelled `other` node — descend the CST,
953
+ * lowering any expr_base column, but never into a nested select_stmt (its own scope). */
954
+ function cstColumnRefs(node, acc, clause) {
955
+ for (const child of kidsOf(node)) {
956
+ if (!(child instanceof ParserRuleContext))
957
+ continue;
958
+ if (child.ruleIndex === P.RULE_select_stmt)
959
+ continue;
960
+ if (child.ruleIndex === P.RULE_expr_base) {
961
+ const e = lowerExprBase(child);
962
+ if (e.kind === "column") {
963
+ acc.push({ kind: "columnref", parts: e.parts, clause, cst: child, partSpans: e.partSpans });
964
+ continue;
965
+ }
966
+ }
967
+ cstColumnRefs(child, acc, clause);
968
+ }
969
+ }
970
+ function hasAggregate(expr) {
971
+ switch (expr.kind) {
972
+ case "function":
973
+ return (expr.aggregate && !expr.window) || expr.args.some(hasAggregate);
974
+ case "binary":
975
+ return hasAggregate(expr.left) || hasAggregate(expr.right);
976
+ case "unary":
977
+ return hasAggregate(expr.operand);
978
+ case "cast":
979
+ return hasAggregate(expr.expr);
980
+ case "case":
981
+ return (expr.whens.some((w) => hasAggregate(w.when) || hasAggregate(w.then)) ||
982
+ (expr.elseExpr !== undefined && hasAggregate(expr.elseExpr)));
983
+ case "predicate":
984
+ return hasAggregate(expr.operand) || expr.args.some(hasAggregate);
985
+ case "subscript":
986
+ return hasAggregate(expr.base);
987
+ default:
988
+ return false;
989
+ }
990
+ }
991
+ // --- CST navigation helpers ----------------------------------------------------
992
+ function kidsOf(node) {
993
+ if (node instanceof ParserRuleContext)
994
+ return node.children ?? [];
995
+ const out = [];
996
+ for (let i = 0; i < node.getChildCount(); i++) {
997
+ const c = node.getChild(i);
998
+ if (c)
999
+ out.push(c);
1000
+ }
1001
+ return out;
1002
+ }
1003
+ function* descendants(node) {
1004
+ for (const child of kidsOf(node)) {
1005
+ if (child instanceof ParserRuleContext) {
1006
+ yield child;
1007
+ yield* descendants(child);
1008
+ }
1009
+ }
1010
+ }
1011
+ function firstOfRule(node, ruleIndex) {
1012
+ for (const d of descendants(node))
1013
+ if (d.ruleIndex === ruleIndex)
1014
+ return d;
1015
+ return undefined;
1016
+ }
1017
+ function directChildrenOfRule(node, ruleIndex) {
1018
+ const out = [];
1019
+ for (const child of kidsOf(node)) {
1020
+ if (child instanceof ParserRuleContext && child.ruleIndex === ruleIndex)
1021
+ out.push(child);
1022
+ }
1023
+ return out;
1024
+ }
1025
+ /** Collect rule nodes within `node` but not inside a nested select_stmt (its own scope); matched
1026
+ * nodes are not themselves descended into. */
1027
+ function shallowCollectOfRule(node, ruleIndex) {
1028
+ const out = [];
1029
+ const walk = (n) => {
1030
+ for (const child of kidsOf(n)) {
1031
+ if (!(child instanceof ParserRuleContext))
1032
+ continue;
1033
+ if (child.ruleIndex === ruleIndex)
1034
+ out.push(child);
1035
+ else if (child.ruleIndex === P.RULE_select_stmt)
1036
+ continue;
1037
+ else
1038
+ walk(child);
1039
+ }
1040
+ };
1041
+ walk(node);
1042
+ return out;
1043
+ }
1044
+ /** The first `kids[j >= from]` that is a rule node of `ruleIndex`, with its index. */
1045
+ function nextRule(kids, from, ruleIndex) {
1046
+ for (let j = from; j < kids.length; j++) {
1047
+ const c = kids[j];
1048
+ if (c instanceof ParserRuleContext && c.ruleIndex === ruleIndex)
1049
+ return { node: c, index: j };
1050
+ }
1051
+ return undefined;
1052
+ }
1053
+ function isRule(node, ruleIndex) {
1054
+ return node instanceof ParserRuleContext && node.ruleIndex === ruleIndex;
1055
+ }
1056
+ function isToken(node, type) {
1057
+ return node instanceof TerminalNode && node.symbol.type === type;
1058
+ }
1059
+ function hasDirectToken(node, type) {
1060
+ for (const child of kidsOf(node)) {
1061
+ if (child instanceof TerminalNode && child.symbol.type === type)
1062
+ return true;
1063
+ }
1064
+ return false;
1065
+ }
1066
+ /** True when `node` is a descendant of `ancestor` (used to match a flattened parenthesized-join
1067
+ * source back to its enclosing table_or_subquery). */
1068
+ function isWithin(node, ancestor) {
1069
+ let n = node instanceof ParserRuleContext ? node.parent : undefined;
1070
+ while (n) {
1071
+ if (n === ancestor)
1072
+ return true;
1073
+ n = n.parent;
1074
+ }
1075
+ return false;
1076
+ }
1077
+ function otherExpr(node) {
1078
+ return { kind: "other", text: node.getText(), cst: node };
1079
+ }
1080
+ function emptyBody(cst) {
1081
+ return {
1082
+ kind: "select",
1083
+ projections: [],
1084
+ from: [],
1085
+ columns: [],
1086
+ aggregated: false,
1087
+ unsupported: ["unparsed"],
1088
+ cst,
1089
+ };
1090
+ }
1091
+ function emptyQuery(cst) {
1092
+ return { kind: "query", ctes: [], body: emptyBody(cst), cst };
1093
+ }