sqllens 1.6.0 → 1.7.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 (53) hide show
  1. package/dist/api.d.ts +5 -1
  2. package/dist/api.js +7 -0
  3. package/dist/bigquery/reserved.generated.d.ts +2 -0
  4. package/dist/bigquery/reserved.generated.js +325 -0
  5. package/dist/completion/complete.d.ts +68 -1
  6. package/dist/completion/complete.js +88 -33
  7. package/dist/databricks/infer.js +24 -6
  8. package/dist/databricks/lower.js +161 -9
  9. package/dist/databricks/reserved.generated.d.ts +2 -0
  10. package/dist/databricks/reserved.generated.js +426 -0
  11. package/dist/dialect-symbols.d.ts +35 -3
  12. package/dist/dialect-symbols.js +54 -2
  13. package/dist/document/document.d.ts +26 -1
  14. package/dist/document/document.js +158 -8
  15. package/dist/document/node-at.js +8 -5
  16. package/dist/duckdb/reserved.generated.d.ts +2 -0
  17. package/dist/duckdb/reserved.generated.js +560 -0
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.js +1 -1
  20. package/dist/infer/infer.js +16 -5
  21. package/dist/ir/ir.d.ts +36 -0
  22. package/dist/ir/walk.d.ts +8 -1
  23. package/dist/ir/walk.js +12 -1
  24. package/dist/mysql/reserved.generated.d.ts +2 -0
  25. package/dist/mysql/reserved.generated.js +1120 -0
  26. package/dist/postgres/reserved.generated.d.ts +2 -0
  27. package/dist/postgres/reserved.generated.js +534 -0
  28. package/dist/redshift/reserved.generated.d.ts +2 -0
  29. package/dist/redshift/reserved.generated.js +827 -0
  30. package/dist/scope/clauses.d.ts +19 -0
  31. package/dist/scope/clauses.js +204 -0
  32. package/dist/scope/frame.d.ts +16 -0
  33. package/dist/scope/frame.js +51 -0
  34. package/dist/scope/scope.d.ts +30 -1
  35. package/dist/scope/scope.js +42 -2
  36. package/dist/scope/setop-arms.d.ts +16 -0
  37. package/dist/scope/setop-arms.js +32 -0
  38. package/dist/scope/walk.js +10 -5
  39. package/dist/session.d.ts +9 -2
  40. package/dist/session.js +12 -2
  41. package/dist/snowflake/lower.js +138 -15
  42. package/dist/snowflake/reserved.generated.d.ts +2 -0
  43. package/dist/snowflake/reserved.generated.js +938 -0
  44. package/dist/sqlite/reserved.generated.d.ts +2 -0
  45. package/dist/sqlite/reserved.generated.js +155 -0
  46. package/dist/symbols/symbols.d.ts +10 -0
  47. package/dist/symbols/symbols.js +192 -76
  48. package/dist/trino/reserved.generated.d.ts +2 -0
  49. package/dist/trino/reserved.generated.js +313 -0
  50. package/dist/tsql/lower.js +235 -4
  51. package/dist/tsql/reserved.generated.d.ts +2 -0
  52. package/dist/tsql/reserved.generated.js +1185 -0
  53. package/package.json +1 -1
@@ -19,6 +19,7 @@ import { Token } from "antlr4ng";
19
19
  import { debugRethrow } from "../debug.js";
20
20
  import { nodeAt } from "../document/node-at.js";
21
21
  import { resolveBehavior } from "../dialect-behavior/registry.js";
22
+ import { partSpanOf } from "../ir/part-span.js";
22
23
  import { DefaultTemplateProvider } from "../qualify/template-provider.js";
23
24
  import { callOf } from "../minijinja/apply-tags.js";
24
25
  import { sourcesMatchingQualifier } from "../scope/scope.js";
@@ -32,9 +33,9 @@ import { completionMeta } from "./parser-factory.js";
32
33
  * fuzzy). Schema-aware when a `Schema` is given (table names + column types). NEVER throws: on
33
34
  * broken / mid-edit input it still returns the keyword candidates the walk can reach.
34
35
  */
35
- export function completeAt(doc, offset, schema) {
36
+ export function completeAt(doc, offset, schema, opts) {
36
37
  try {
37
- return collect(doc, offset, schema);
38
+ return collect(doc, offset, schema, opts?.decorate);
38
39
  }
39
40
  catch (e) {
40
41
  // Total by contract: a walk/parse hiccup must not surface to the editor.
@@ -44,7 +45,7 @@ export function completeAt(doc, offset, schema) {
44
45
  }
45
46
  /** @deprecated Use completeAt — same function, uniform cursor-verb naming. */
46
47
  export const complete = completeAt;
47
- function collect(doc, offset, schema) {
48
+ function collect(doc, offset, schema, decorate) {
48
49
  const dialect = doc.dialect;
49
50
  // Inside a jinja tag ({{ ref('| }}, {% if | %}, {{ a ~ | }}) the caret is not in SQL at all, so SQL
50
51
  // completion is wrong: the tag was blanked to a placeholder sitting in some SQL slot, so the walk
@@ -56,7 +57,7 @@ function collect(doc, offset, schema) {
56
57
  if (tags) {
57
58
  const slot = jinjaSlotAt(tags, doc.text, offset);
58
59
  if (slot)
59
- return templateCompletions(slot, schema);
60
+ return templateCompletions(slot, schema, decorate);
60
61
  if (tags.some((t) => offset > t.tagSpan.start && offset < t.tagSpan.end))
61
62
  return [];
62
63
  }
@@ -89,18 +90,22 @@ function collect(doc, offset, schema) {
89
90
  const cand = collectCandidates(meta.atn, meta.entryRuleIndex, walkTokens, caretIdx, cfg.preferredRules, cfg.ignoredTokens);
90
91
  const out = [];
91
92
  const seen = new Set(); // dedup by `${kind}\0${label}`
92
- const add = (c) => {
93
+ // Per-candidate structural identity (CandidateIdentity), keyed by the Completion object itself —
94
+ // consulted only when `decorate` is set (decorateResult, at the end of this function).
95
+ const identities = new Map();
96
+ const add = (c, identity) => {
93
97
  const key = `${c.kind}\0${c.label}`;
94
98
  if (seen.has(key))
95
99
  return;
96
100
  seen.add(key);
97
101
  out.push(c);
102
+ identities.set(c, identity);
98
103
  };
99
104
  // keywords — from candidate token types whose grammar literal is a word.
100
105
  for (const type of cand.tokens) {
101
106
  const label = keywordLabel(meta.vocabulary, type);
102
107
  if (label)
103
- add({ label, kind: "keyword" });
108
+ add({ label, kind: "keyword" }, { kind: "keyword" });
104
109
  }
105
110
  // A RELATION PATH position (#38 stage 6): a dotted chain right before the caret whose anchor
106
111
  // token is FROM/JOIN-family (cfg.relationKeywordTokens — the same per-dialect set the
@@ -116,7 +121,7 @@ function collect(doc, offset, schema) {
116
121
  if (atRelationPath) {
117
122
  if (schema?.childrenOf) {
118
123
  for (const child of schema.childrenOf(path.parts, dialect))
119
- add({ label: child.name, kind: child.kind });
124
+ add({ label: child.name, kind: child.kind }, { kind: child.kind });
120
125
  }
121
126
  }
122
127
  else if (path.parts.length > 0) {
@@ -126,41 +131,73 @@ function collect(doc, offset, schema) {
126
131
  // mid-edit the walk often reports nothing, but the dot chain itself is the member-position
127
132
  // evidence (it is the completion trigger character), and an unmatched qualifier answers [].
128
133
  const scoped = qualifiedSourceColumns(cellScopes, cellAst, cellOffset, path.parts, dialect, schema);
129
- for (const c of scoped)
130
- add(c);
134
+ for (const c of scoped.items)
135
+ add(c, { kind: "column", source: scoped.source });
131
136
  // Mid-edit the dangling dot often breaks the FROM parse and the scope is EMPTY — the same
132
137
  // failure the bare-slot fallback covers. Its member twin: read `FROM/JOIN name [alias]`
133
138
  // pairs off the token stream and answer the matching relation's schema columns.
134
- if (scoped.length === 0 && schema) {
139
+ if (scoped.items.length === 0 && schema) {
135
140
  const fallback = qualifiedFallbackColumns(walkTokens, cfg, path.parts, schema, dialect, doc.templated?.tags, doc.text);
136
141
  for (const c of fallback)
137
- add(c);
142
+ add(c, { kind: "column" });
138
143
  }
139
144
  }
140
145
  else {
141
146
  if (atTable) {
142
147
  // Bare relation slot: in-scope CTE names FIRST (they shadow same-named catalog tables),
143
148
  // then the catalog's tables.
144
- for (const name of visibleCteNames(cellScopes, cellAst, cellOffset))
145
- add({ label: name, kind: "cte" });
149
+ for (const cte of visibleCteNames(cellScopes, cellAst, cellOffset))
150
+ add({ label: cte.name, kind: "cte" }, { kind: "cte", declarationSpan: cte.declarationSpan });
146
151
  if (schema)
147
152
  for (const t of schema.tables(dialect))
148
- add({ label: t, kind: "table" });
153
+ add({ label: t, kind: "table" }, { kind: "table" });
149
154
  }
150
155
  // columns — value/column slot: the columns visible from the enclosing scope, plus a
151
156
  // broken-input fallback reading FROM/JOIN relation names straight off the token stream.
152
157
  if (atColumn) {
153
- for (const c of visibleColumns(cellScopes, cellAst, dialect, cellOffset, schema))
154
- add(c);
158
+ for (const { completion, source } of visibleColumns(cellScopes, cellAst, dialect, cellOffset, schema))
159
+ add(completion, { kind: "column", source });
155
160
  if (schema)
156
161
  for (const c of fromRelationColumns(walkTokens, cfg, schema, dialect, doc.templated?.tags, doc.text))
157
- add(c);
162
+ add(c, { kind: "column" });
158
163
  // functions — value/column slot: the dialect's inference-registry function names.
159
164
  for (const fn of Object.keys(resolveBehavior(dialect).functions))
160
- add({ label: fn, kind: "function" });
165
+ add({ label: fn, kind: "function" }, { kind: "function" });
161
166
  }
162
167
  }
163
- return pruneByPrefix(out, walkTokens, offset, cfg, doc.text, dialect);
168
+ const result = pruneByPrefix(out, walkTokens, offset, cfg, doc.text, dialect);
169
+ return decorate ? decorateResult(result, identities, decorate) : result;
170
+ }
171
+ /** Apply `decorate` to one candidate, merging its `detail`/`documentation` onto a COPY (the input is
172
+ * never mutated). Total-safe: a throwing hook degrades to the undecorated candidate unchanged;
173
+ * SQLLENS_DEBUG=1 rethrows (src/debug.ts), the same posture every other completeAt internal failure
174
+ * gets. */
175
+ function applyDecoration(candidate, identity, decorate) {
176
+ try {
177
+ const d = decorate(candidate, identity);
178
+ if (!d)
179
+ return candidate;
180
+ return {
181
+ ...candidate,
182
+ ...(d.detail !== undefined ? { detail: d.detail } : {}),
183
+ ...(d.documentation !== undefined ? { documentation: d.documentation } : {}),
184
+ };
185
+ }
186
+ catch (e) {
187
+ debugRethrow(e);
188
+ return candidate;
189
+ }
190
+ }
191
+ /** Decorate every candidate in the assembled SQL-slot result (post prefix-pruning — a pruned-out
192
+ * candidate never reaches the consumer's hook), preserving `replaceRange` (Array.prototype.map
193
+ * drops it — it is not an own array index). `identities` was populated by `add()` during collect();
194
+ * a candidate somehow missing an entry (never happens in practice — every `add()` call site provides
195
+ * one) falls back to its bare kind rather than crashing. */
196
+ function decorateResult(result, identities, decorate) {
197
+ const decorated = result.map((c) => applyDecoration(c, identities.get(c) ?? { kind: c.kind }, decorate));
198
+ if (result.replaceRange)
199
+ decorated.replaceRange = result.replaceRange;
200
+ return decorated;
164
201
  }
165
202
  /** SQL identifier-quoting delimiters recognized across all ten dialects
166
203
  * (docs/identifier-delimiter-contract.md) — used only to recognize a PARTIAL, possibly
@@ -252,15 +289,21 @@ function pruneByPrefix(out, toks, offset, cfg, text, dialect) {
252
289
  * so this reads the `schema` when it is one (a DbtTemplateProvider IS a SchemaProvider, and the host
253
290
  * already passes it here for column/table completion); the neutral provider offers none. A jinja slot
254
291
  * with no candidates still returns [], never SQL keywords, so a caret inside a tag never leaks SQL
255
- * completion. */
256
- function templateCompletions(slot, schema) {
292
+ * completion. Every item's structural identity is `{ kind: "template", call: slot.call }` — the SAME
293
+ * TemplateCall `templateCandidates` was asked with (a dbt model/source candidate resolves through
294
+ * this call, so a `decorate` hook names it without re-parsing the call text). */
295
+ function templateCompletions(slot, schema, decorate) {
257
296
  if (!(schema instanceof DefaultTemplateProvider))
258
297
  return [];
259
- return schema.templateCandidates(slot.call, slot.argIndex).map((c) => ({
298
+ const items = schema.templateCandidates(slot.call, slot.argIndex).map((c) => ({
260
299
  label: c.label,
261
300
  kind: "template",
262
301
  ...(c.detail !== undefined ? { detail: c.detail } : {}),
263
302
  }));
303
+ if (!decorate)
304
+ return items;
305
+ const identity = { kind: "template", call: slot.call };
306
+ return items.map((c) => applyDecoration(c, identity, decorate));
264
307
  }
265
308
  /** The dotted qualifier immediately before the caret (#38): `analytics.` → ["analytics"],
266
309
  * `analytics.sales.` → ["analytics","sales"], `analytics.sa|` (typing a segment) → ["analytics"].
@@ -294,13 +337,18 @@ function dottedPrefixAt(toks, caretIdx) {
294
337
  }
295
338
  return { parts, anchorIdx };
296
339
  }
297
- /** The CTE names visible from the caret's enclosing scope, as declared (display text). */
340
+ /** The CTE names visible from the caret's enclosing scope, as declared (display text), each with its
341
+ * declaration span (`CteDef.nameCst`, falling back to the whole `CteDef.cst`) — the same span
342
+ * `SqlDocument.unionCtes` keys its own per-CTE aggregation on, so a `decorate` hook can pin "12
343
+ * columns" to the right declaration even when a nested scope shadows the name. */
298
344
  function visibleCteNames(scopes, ast, offset) {
299
345
  const scope = enclosingScope(scopes, ast, offset);
300
346
  const out = [];
301
- for (let s = scope; s; s = s.parent)
302
- for (const cte of s.ctes.values())
303
- out.push(cte.def.name);
347
+ for (let s = scope; s; s = s.parent) {
348
+ for (const cte of s.ctes.values()) {
349
+ out.push({ name: cte.def.name, declarationSpan: partSpanOf(cte.def.nameCst ?? cte.def.cst) });
350
+ }
351
+ }
304
352
  return out;
305
353
  }
306
354
  /** The walk's caret token index. Two rules, in order (anvil 2026-07-15; antlr4-c3's own caret
@@ -395,21 +443,27 @@ function fromRelationColumns(walkTokens, cfg, schema, dialect, tags, text) {
395
443
  /** The columns of the ONE source a dotted qualifier matches from the caret's scope (anvil item 2):
396
444
  * `o.|` answers o's columns only. Matching is sourcesMatchingQualifier — the same validated,
397
445
  * any-depth primitive column BINDING uses — walking enclosing scopes nearest-first. Ambiguous or
398
- * unmatched qualifiers answer nothing (never a fabricated union). */
446
+ * unmatched qualifiers answer nothing (never a fabricated union). `source` rides along (the matched
447
+ * ResolvedSource, when exactly one matched) so the caller can attach it as the column candidates'
448
+ * structural identity. */
399
449
  function qualifiedSourceColumns(scopes, ast, offset, qualParts, dialect, schema) {
400
450
  const scope = enclosingScope(scopes, ast, offset);
401
451
  if (!scope)
402
- return [];
452
+ return { items: [] };
403
453
  const behavior = resolveBehavior(dialect);
404
454
  for (let s = scope; s; s = s.parent) {
405
455
  const matches = sourcesMatchingQualifier(s, qualParts);
406
456
  if (matches.length > 1)
407
- return [];
457
+ return { items: [] };
408
458
  if (matches.length === 1) {
409
- return columnsOf(matches[0], dialect, schema).map((c) => ({ ...c, label: behavior.displayName(c.label) }));
459
+ const source = matches[0];
460
+ return {
461
+ items: columnsOf(source, dialect, schema).map((c) => ({ ...c, label: behavior.displayName(c.label) })),
462
+ source,
463
+ };
410
464
  }
411
465
  }
412
- return [];
466
+ return { items: [] };
413
467
  }
414
468
  /** The member-position twin of `fromRelationColumns` (#38): when the scope is empty (the dangling
415
469
  * dot broke the FROM parse), read `FROM/JOIN name(.name)* [AS] [alias]` off the token stream and
@@ -479,7 +533,8 @@ function qualifiedFallbackColumns(walkTokens, cfg, qualParts, schema, dialect, t
479
533
  }
480
534
  /** The columns visible from the scope enclosing `offset` (a CELL-relative offset into `scopes`).
481
535
  * Derived sources / CTEs expose their own output column names; base-table sources get their columns
482
- * (and types) from the schema. */
536
+ * (and types) from the schema. Each item rides alongside the ResolvedSource it came from — the
537
+ * column candidate's structural identity (a `decorate` hook's "source relation"). */
483
538
  function visibleColumns(scopes, ast, dialect, offset, schema) {
484
539
  const scope = enclosingScope(scopes, ast, offset);
485
540
  if (!scope)
@@ -494,7 +549,7 @@ function visibleColumns(scopes, ast, dialect, offset, schema) {
494
549
  if (seen.has(key))
495
550
  continue;
496
551
  seen.add(key);
497
- out.push({ ...col, label: behavior.displayName(col.label) });
552
+ out.push({ completion: { ...col, label: behavior.displayName(col.label) }, source: src });
498
553
  }
499
554
  }
500
555
  return out;
@@ -178,12 +178,14 @@ export const DATABRICKS_FUNCTION_RETURNS = {
178
178
  "date_format",
179
179
  "dayname",
180
180
  "monthname",
181
- "string_agg",
182
- "listagg",
183
181
  "to_json",
184
182
  "get_json_object",
185
183
  "string",
186
184
  ]),
185
+ // string_agg/listagg (sql-ref-functions listagg: "the datatype of the return is the same as the
186
+ // datatype of expr if expr is BINARY type, otherwise STRING" — v4.2.0 goldens: listagg(CAST(col
187
+ // AS BINARY)) -> binary, not string).
188
+ ...group((args) => (args[0]?.kind === "scalar" && args[0].name === "binary" ? BIN : S), ["string_agg", "listagg"]),
187
189
  ...group(fixed(I), [
188
190
  "ascii",
189
191
  "instr",
@@ -251,9 +253,21 @@ export const DATABRICKS_FUNCTION_RETURNS = {
251
253
  "bigint",
252
254
  "long",
253
255
  ]),
254
- // avg — sql-ref: DECIMAL input stays DECIMAL, an interval stays its interval, else DOUBLE
255
- // (v4.2.0 goldens: AVG over a decimal window column is decimal, not double).
256
- ...group((args) => args[0]?.kind === "scalar" && (args[0].name === "decimal" || isIntervalName(args[0].name)) ? args[0] : D, ["avg", "try_avg", "mean"]),
256
+ // avg — sql-ref: DECIMAL input stays DECIMAL, an interval WIDENS to its family's full range
257
+ // (year-to-month / day-to-second — same fullInterval() promotion Multiply/DivideInterval already
258
+ // use; v4.2.0 goldens: avg(interval month) -> interval year to month, avg(interval day/second) ->
259
+ // interval day to second), else DOUBLE (v4.2.0 goldens: AVG over a decimal window column is
260
+ // decimal, not double).
261
+ ...group((args) => {
262
+ const a0 = args[0];
263
+ if (a0?.kind !== "scalar")
264
+ return D;
265
+ if (a0.name === "decimal")
266
+ return a0;
267
+ if (isIntervalName(a0.name))
268
+ return fullInterval(a0);
269
+ return D;
270
+ }, ["avg", "try_avg", "mean"]),
257
271
  ...group(fixed(D), [
258
272
  "sqrt",
259
273
  "cbrt",
@@ -341,6 +355,11 @@ export const DATABRICKS_FUNCTION_RETURNS = {
341
355
  "make_date",
342
356
  "next_day",
343
357
  "last_day",
358
+ // trunc(expr, unit) is DATE-truncation only (sql-ref-functions/trunc: "Returns date with the
359
+ // time portion of the day truncated..."; Databricks has no separate numeric-trunc overload,
360
+ // unlike Oracle/Postgres) — always DATE regardless of expr's own type (v4.2.0 goldens:
361
+ // trunc('2018-01-01', a) -> date, not the string argument's own type).
362
+ "trunc",
344
363
  ]),
345
364
  ...group(fixed(TS), [
346
365
  "current_timestamp",
@@ -374,7 +393,6 @@ export const DATABRICKS_FUNCTION_RETURNS = {
374
393
  ...group(firstArg, [
375
394
  "round",
376
395
  "bround",
377
- "trunc",
378
396
  "mod",
379
397
  "pmod",
380
398
  "shiftleft",
@@ -1,7 +1,7 @@
1
1
  import { ParserRuleContext, TerminalNode } from "antlr4ng";
2
- import { ArithmeticBinaryContext, ArithmeticUnaryContext, CastByColonContext, CastContext, CollationForContext, ColumnReferenceContext, ComparisonContext, ConstantDefaultContext, CurrentLikeContext, DatabricksParser as P, DereferenceContext, ExistsContext, FunctionCallContext, IdentifierLiteralContext, LambdaContext, LogicalBinaryContext, LogicalNotContext, NamedParameterLiteralContext, ParenthesizedExpressionContext, PosParameterLiteralContext, PredicatedContext, PrimaryExpressionContext, RowConstructorContext, SearchedCaseContext, ShiftExpressionContext, SimpleCaseContext, StarContext, StringLiteralContext, SubqueryExpressionContext, SubscriptContext, TimestampaddContext, TimestampdiffContext, TryCastByColonContext, TypeAscriptionContext, } from "../generated/databricks/DatabricksParser.js";
2
+ import { ArithmeticBinaryContext, ArithmeticUnaryContext, CastByColonContext, CastContext, CollationForContext, ColumnReferenceContext, ComparisonContext, ConstantDefaultContext, CreateVariableContext, CurrentLikeContext, DatabricksParser as P, DereferenceContext, ExistsContext, FunctionCallContext, IdentifierLiteralContext, LambdaContext, LogicalBinaryContext, LogicalNotContext, NamedParameterLiteralContext, ParenthesizedExpressionContext, PosParameterLiteralContext, PredicatedContext, PrimaryExpressionContext, RowConstructorContext, SearchedCaseContext, ShiftExpressionContext, SimpleCaseContext, StarContext, StringLiteralContext, SubqueryExpressionContext, SubscriptContext, TimestampaddContext, TimestampdiffContext, TryCastByColonContext, TypeAscriptionContext, } from "../generated/databricks/DatabricksParser.js";
3
3
  import { keywordCategory, swallowedCategories, swallowedStatements } from "../ir/statement.js";
4
- import { partSpansOf } from "../ir/part-span.js";
4
+ import { partSpanOf, partSpansOf } from "../ir/part-span.js";
5
5
  import { freezeIR } from "../ir/freeze.js";
6
6
  import { qualifiedNameOf } from "../ir/qualified-name.js";
7
7
  import { DATABRICKS_NAME_CONFIG } from "./fold.js";
@@ -136,10 +136,23 @@ function lowerImpl(tree) {
136
136
  return flagged(tree, "other", swallowed > 0 ? "broken" : "empty");
137
137
  const stmt = elements[0] ?? tree; // the single element, or a legacy single-statement root
138
138
  const statement = statementCategory(stmt);
139
- // A BEGIN…END scripting compound is a statement *sequence*, not a query — flag the
140
- // whole thing rather than modelling whichever SELECT happens to come first inside it.
141
- if (isCompound(stmt))
142
- return flagged(stmt, statement, "compound");
139
+ // A BEGIN…END scripting compound is a statement *sequence*, not a query — flag the whole thing
140
+ // rather than modelling whichever SELECT happens to come first inside it, PLUS its own inner
141
+ // statements (applyCompoundFrame — the routine-frame slice's Databricks counterpart, see tsql's
142
+ // applyRoutineFrame / snowflake's applyScriptingFrame).
143
+ if (isCompound(stmt)) {
144
+ const q = flagged(stmt, statement, "compound");
145
+ applyCompoundFrame(q, stmt);
146
+ return q;
147
+ }
148
+ return lowerStatement(stmt);
149
+ }
150
+ /** Lower a single statement-shaped node into a QueryExpr: a real query gets its real body;
151
+ * anything else gets the flagged "non-query" stub. Shared by lowerImpl's single-element case and
152
+ * each compound inner statement whose own alternative is a nested `statement`
153
+ * (lowerInnerCompoundStatement). */
154
+ function lowerStatement(stmt) {
155
+ const statement = statementCategory(stmt);
143
156
  const query = firstOfRule(stmt, P.RULE_query);
144
157
  if (!query)
145
158
  return flagged(stmt, statement, "non-query");
@@ -148,12 +161,89 @@ function lowerImpl(tree) {
148
161
  return lowered;
149
162
  }
150
163
  /** A BEGIN…END scripting compound: the batch element's BEGIN-led alternative, or a legacy
151
- * `singleCompoundStatement` root (other entries into this lowering still work). */
164
+ * `singleCompoundStatement` root (other entries into this lowering still work). A NESTED
165
+ * `beginEndCompoundBlock` inside another compound's own body is detected directly by
166
+ * lowerInnerCompoundStatement instead (it is never routed through `statement`/lowerStatement, so
167
+ * it never reaches this function). */
152
168
  function isCompound(stmt) {
153
169
  if (stmt.ruleIndex === P.RULE_multiStatementElement && stmt.start?.type === P.BEGIN)
154
170
  return true;
155
171
  return !!firstOfRule(stmt, P.RULE_singleCompoundStatement);
156
172
  }
173
+ /** The `compoundBody` of a compound-shaped node: a direct child for the two shapes that carry one
174
+ * right on themselves (a BEGIN-led `multiStatementElement`, or a nested `beginEndCompoundBlock`),
175
+ * else a deep search for the legacy `singleCompoundStatement` root's own child. */
176
+ function compoundBodyOf(stmt) {
177
+ const direct = directChildrenOfRule(stmt, P.RULE_compoundBody)[0];
178
+ if (direct)
179
+ return direct;
180
+ const single = firstOfRule(stmt, P.RULE_singleCompoundStatement);
181
+ return single ? directChildrenOfRule(single, P.RULE_compoundBody)[0] : undefined;
182
+ }
183
+ /** Layers a BEGIN...END scripting compound's inner statements (statements[]) onto the container's
184
+ * existing flagged "compound" QueryExpr. Databricks' compoundBody has no variable-DECLARE
185
+ * statement of its own (declareConditionStatement declares a named SQLSTATE handler condition, not
186
+ * a typed variable — grammars/databricks/DatabricksParser.g4); the DECLARE-VARIABLE form
187
+ * (`#createVariable`) is a plain `statement` alternative reachable from inside a compound body too,
188
+ * so it rides that inner statement's own `declarations` (lowerInnerCompoundStatement) — there is no
189
+ * container-level declarations counterpart here, unlike tsql/snowflake's signature-like preamble. */
190
+ function applyCompoundFrame(q, stmt) {
191
+ const body = compoundBodyOf(stmt);
192
+ if (!body)
193
+ return;
194
+ const units = directChildrenOfRule(body, P.RULE_compoundStatement);
195
+ if (units.length)
196
+ q.statements = units.map(lowerInnerCompoundStatement);
197
+ }
198
+ /** One `compoundStatement` of a scripting body -> its own QueryExpr. A nested `beginEndCompoundBlock`
199
+ * gets the same flagged "compound" stub + recursive statements[] (applyCompoundFrame called again,
200
+ * so nesting layers naturally to any depth). A `statement` gets its real body via the shared
201
+ * per-statement lowering (lowerStatement); its `#createVariable` alternative (DECLARE [OR REPLACE] [VARIABLE]
202
+ * name [, name...] [data_type] [DEFAULT|= expr] — sql-ref-syntax-ddl-declare-variable) ALSO carries
203
+ * its own `declarations`, mirroring a tsql body DECLARE / a Snowflake scripting LET: mid-body, not
204
+ * container-level (rootDeclarations pools the whole tree regardless of nesting depth). Every other
205
+ * scripting-only control-flow alternative (declareConditionStatement, setStatementInsideSqlScript,
206
+ * declareHandlerStatement, ifElseStatement, caseStatement, whileStatement, repeatStatement,
207
+ * leaveStatement, iterateStatement, loopStatement, forStatement) has no query body to lower — an
208
+ * honest flagged "non-query" stub, categorised by leading keyword like the top-level grab-bag
209
+ * commands. */
210
+ function lowerInnerCompoundStatement(unit) {
211
+ const nested = directChildrenOfRule(unit, P.RULE_beginEndCompoundBlock)[0];
212
+ if (nested) {
213
+ const q = flagged(nested, "compound", "compound");
214
+ applyCompoundFrame(q, nested);
215
+ return q;
216
+ }
217
+ const stmt = directChildrenOfRule(unit, P.RULE_statement)[0];
218
+ if (stmt) {
219
+ const q = lowerStatement(stmt);
220
+ if (stmt instanceof CreateVariableContext)
221
+ q.declarations = lowerCreateVariable(stmt);
222
+ return q;
223
+ }
224
+ return flagged(unit, keywordCategory(unit.start?.text ?? ""), "non-query");
225
+ }
226
+ /** `DECLARE [OR REPLACE] [VARIABLE] name [, name...] [data_type] [{DEFAULT|=} expr]` (Databricks
227
+ * session variables, sql-ref-syntax-ddl-declare-variable): ONE shared data_type/default expression
228
+ * across every name in the list — duplicated onto EACH name's own VariableDecl, matching the doc's
229
+ * own multi-name example (`DECLARE var1, var2 DOUBLE DEFAULT rand()`: both get type DOUBLE and
230
+ * their own evaluation of the same expression). */
231
+ function lowerCreateVariable(node) {
232
+ const dt = node.dataType();
233
+ const typeText = dt?.getText();
234
+ const defaultExpr = node.variableDefaultExpression();
235
+ const init = defaultExpr ? lowerExpression(defaultExpr.expression()) : undefined;
236
+ return node.identifierReference().map((idRef) => ({
237
+ name: idRef.getText(),
238
+ nameSpan: partSpanOf(idRef) ?? partSpanOf(node) ?? ZERO_PART_SPAN,
239
+ typeText,
240
+ init,
241
+ cst: idRef,
242
+ }));
243
+ }
244
+ /** A fallback span for the never-should-happen case a declared variable's own name token is missing
245
+ * (a broken/partial parse): stays a valid `PartSpan` rather than `undefined`. */
246
+ const ZERO_PART_SPAN = { start: 0, end: 0, line: 0, column: 0, endLine: 0, endColumn: 0 };
157
247
  /**
158
248
  * The statement category, from the parse — not the source text. Spark's `statement` rule labels its
159
249
  * alternatives, so the structural cases are exact: a `#dmlStatement` (`ctes? dmlStatementNoWith`) is
@@ -207,7 +297,8 @@ function lowerQuery(query) {
207
297
  if (orderBy && body.kind !== "pipe")
208
298
  for (const o of orderBy)
209
299
  columnsOf(o, body.columns, "orderBy");
210
- return { kind: "query", ctes, body, orderBy, cst: query };
300
+ const limit = extractLimit(query);
301
+ return { kind: "query", ctes, body, orderBy, limit, cst: query };
211
302
  }
212
303
  /** The ORDER BY sort expressions from the query's queryOrganization (not SORT/CLUSTER/DISTRIBUTE BY). */
213
304
  function extractOrderBy(query) {
@@ -238,6 +329,48 @@ function extractOrderBy(query) {
238
329
  }
239
330
  return items.length ? items : undefined;
240
331
  }
332
+ /** The LIMIT / OFFSET row-limiting clause from the query's queryOrganization (grammar:
333
+ * `(LIMIT (ALL | expression))? (OFFSET expression)?`) — learn.microsoft.com/en-us/azure/databricks/
334
+ * sql/language-manual/sql-ref-syntax-qry-select-limit. `LIMIT ALL` is Spark's documented no-cap
335
+ * spelling (no row-count expression to carry); recorded as a present-but-unbounded clause, the
336
+ * same convention as postgres' extractLimit's `LIMIT ALL` handling. */
337
+ function extractLimit(query) {
338
+ const qo = directChildrenOfRule(query, P.RULE_queryOrganization)[0];
339
+ if (!qo)
340
+ return undefined;
341
+ const info = {};
342
+ let any = false;
343
+ let afterLimit = false;
344
+ let afterOffset = false;
345
+ for (let i = 0; i < qo.getChildCount(); i++) {
346
+ const child = qo.getChild(i);
347
+ if (!(child instanceof ParserRuleContext)) {
348
+ const t = child?.symbol?.type;
349
+ if (t === P.LIMIT) {
350
+ any = true;
351
+ afterLimit = true;
352
+ }
353
+ else if (t === P.ALL) {
354
+ afterLimit = false; // LIMIT ALL — no row cap; recorded as present-but-unbounded
355
+ }
356
+ else if (t === P.OFFSET) {
357
+ any = true;
358
+ afterOffset = true;
359
+ afterLimit = false;
360
+ }
361
+ continue;
362
+ }
363
+ if (afterLimit && child.ruleIndex === P.RULE_expression) {
364
+ info.top = lowerExpression(child);
365
+ afterLimit = false;
366
+ }
367
+ else if (afterOffset && child.ruleIndex === P.RULE_expression) {
368
+ info.offset = lowerExpression(child);
369
+ afterOffset = false;
370
+ }
371
+ }
372
+ return any ? info : undefined;
373
+ }
241
374
  /** A queryTerm is a pipe (`queryTerm |> rhs`), a set operation (two queryTerm branches), or a select. */
242
375
  function lowerQueryTerm(queryTerm) {
243
376
  // Pipe (Spark 4.0 `|>`): the queryTerm left-recurses — `((base |> op1) |> op2)`. Unwind the left
@@ -1690,7 +1823,11 @@ function topRelationPrimaries(node) {
1690
1823
  return out;
1691
1824
  }
1692
1825
  function buildSource(relationPrimary) {
1693
- const tableAlias = directChildrenOfRule(relationPrimary, P.RULE_tableAlias)[0];
1826
+ // `VALUES (...), (...) AS alias(cols)` as a relationPrimary (#inlineTableDefault2) nests its own
1827
+ // tableAlias INSIDE the inlineTable node, one level below relationPrimary's direct children.
1828
+ const inlineTable = firstOfRule(relationPrimary, P.RULE_inlineTable);
1829
+ const tableAlias = directChildrenOfRule(relationPrimary, P.RULE_tableAlias)[0] ??
1830
+ (inlineTable ? directChildrenOfRule(inlineTable, P.RULE_tableAlias)[0] : undefined);
1694
1831
  const aliasCst = tableAlias ? firstOfRule(tableAlias, P.RULE_strictIdentifier) : undefined;
1695
1832
  const alias = aliasCst?.getText();
1696
1833
  const columnAliases = tableAlias ? columnAliasList(tableAlias) : undefined;
@@ -1706,6 +1843,21 @@ function buildSource(relationPrimary) {
1706
1843
  cst: relationPrimary,
1707
1844
  };
1708
1845
  }
1846
+ // An inline table constructor used as a FROM-clause relation: lower its rows the same way as a
1847
+ // top-level VALUES query body (buildInlineTable already names each projection from the alias's
1848
+ // own column list) and expose the result as a subquery source, so `SELECT *` expands against ITS
1849
+ // OWN named projections (schema-free) instead of falling into the generic multipartIdentifier
1850
+ // branch below — which has nothing to read here and previously produced an anonymous EMPTY-named
1851
+ // "table" source that any real schema misses as unknown-table (Spark-goldens gate, 2026-07-21).
1852
+ if (inlineTable) {
1853
+ return {
1854
+ kind: "subquery",
1855
+ query: { kind: "query", ctes: [], body: buildInlineTable(inlineTable), cst: inlineTable },
1856
+ alias,
1857
+ aliasCst,
1858
+ cst: relationPrimary,
1859
+ };
1860
+ }
1709
1861
  const multipart = firstOfRule(relationPrimary, P.RULE_multipartIdentifier);
1710
1862
  const partNodes = multipart ? directChildrenOfRule(multipart, P.RULE_errorCapturingIdentifier) : [];
1711
1863
  const parts = partNodes.length ? partNodes.map((p) => p.getText()) : multipart ? [multipart.getText()] : [];
@@ -0,0 +1,2 @@
1
+ import type { KeywordReservation } from "../dialect-symbols.js";
2
+ export declare const DATABRICKS_RESERVED: Record<string, KeywordReservation>;