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.
- package/dist/api.d.ts +5 -1
- package/dist/api.js +7 -0
- package/dist/bigquery/reserved.generated.d.ts +2 -0
- package/dist/bigquery/reserved.generated.js +325 -0
- package/dist/completion/complete.d.ts +68 -1
- package/dist/completion/complete.js +88 -33
- package/dist/databricks/infer.js +24 -6
- package/dist/databricks/lower.js +161 -9
- package/dist/databricks/reserved.generated.d.ts +2 -0
- package/dist/databricks/reserved.generated.js +426 -0
- package/dist/dialect-symbols.d.ts +35 -3
- package/dist/dialect-symbols.js +54 -2
- package/dist/document/document.d.ts +26 -1
- package/dist/document/document.js +158 -8
- package/dist/document/node-at.js +8 -5
- package/dist/duckdb/reserved.generated.d.ts +2 -0
- package/dist/duckdb/reserved.generated.js +560 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/infer/infer.js +16 -5
- package/dist/ir/ir.d.ts +36 -0
- package/dist/ir/walk.d.ts +8 -1
- package/dist/ir/walk.js +12 -1
- package/dist/mysql/reserved.generated.d.ts +2 -0
- package/dist/mysql/reserved.generated.js +1120 -0
- package/dist/postgres/reserved.generated.d.ts +2 -0
- package/dist/postgres/reserved.generated.js +534 -0
- package/dist/redshift/reserved.generated.d.ts +2 -0
- package/dist/redshift/reserved.generated.js +827 -0
- package/dist/scope/clauses.d.ts +19 -0
- package/dist/scope/clauses.js +204 -0
- package/dist/scope/frame.d.ts +16 -0
- package/dist/scope/frame.js +51 -0
- package/dist/scope/scope.d.ts +30 -1
- package/dist/scope/scope.js +42 -2
- package/dist/scope/setop-arms.d.ts +16 -0
- package/dist/scope/setop-arms.js +32 -0
- package/dist/scope/walk.js +10 -5
- package/dist/session.d.ts +9 -2
- package/dist/session.js +12 -2
- package/dist/snowflake/lower.js +138 -15
- package/dist/snowflake/reserved.generated.d.ts +2 -0
- package/dist/snowflake/reserved.generated.js +938 -0
- package/dist/sqlite/reserved.generated.d.ts +2 -0
- package/dist/sqlite/reserved.generated.js +155 -0
- package/dist/symbols/symbols.d.ts +10 -0
- package/dist/symbols/symbols.js +192 -76
- package/dist/trino/reserved.generated.d.ts +2 -0
- package/dist/trino/reserved.generated.js +313 -0
- package/dist/tsql/lower.js +235 -4
- package/dist/tsql/reserved.generated.d.ts +2 -0
- package/dist/tsql/reserved.generated.js +1185 -0
- 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
|
-
|
|
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
|
|
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
|
|
154
|
-
add(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/databricks/infer.js
CHANGED
|
@@ -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
|
|
255
|
-
// (
|
|
256
|
-
|
|
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",
|
package/dist/databricks/lower.js
CHANGED
|
@@ -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
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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()] : [];
|