sqllens 1.5.0 → 1.6.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 +2 -2
- package/dist/api.js +2 -2
- package/dist/bigquery/lower.js +28 -2
- package/dist/completion/complete.d.ts +28 -7
- package/dist/completion/complete.js +135 -16
- package/dist/completion/config.js +25 -4
- package/dist/databricks/behavior.js +3 -1
- package/dist/databricks/infer.d.ts +4 -0
- package/dist/databricks/infer.js +257 -38
- package/dist/databricks/lower.js +296 -29
- package/dist/debug.d.ts +2 -0
- package/dist/debug.js +20 -0
- package/dist/dialect-behavior/behavior.d.ts +8 -1
- package/dist/dialect-symbols.d.ts +14 -0
- package/dist/dialect-symbols.js +34 -0
- package/dist/document/document.d.ts +26 -20
- package/dist/document/document.js +170 -43
- package/dist/document/split.js +3 -1
- package/dist/duckdb/lower.js +99 -34
- package/dist/generated/databricks/DatabricksParser.js +5852 -5822
- package/dist/generated/duckdb/DuckdbParser.js +15042 -15030
- package/dist/generated/mysql/MysqlLexer.js +5131 -5128
- package/dist/generated/mysql/MysqlParser.js +1616 -1604
- package/dist/generated/postgres/PostgresParser.js +2305 -2292
- package/dist/generated/redshift/RedshiftLexer.js +3478 -3465
- package/dist/generated/redshift/RedshiftParser.js +17788 -17463
- package/dist/generated/snowflake/SnowflakeLexer.js +5683 -5673
- package/dist/generated/snowflake/SnowflakeParser.js +20361 -19877
- package/dist/generated/tsql/TSqlLexer.js +6241 -6178
- package/dist/generated/tsql/TSqlParser.js +13255 -11071
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/infer/coerce.d.ts +5 -0
- package/dist/infer/coerce.js +39 -4
- package/dist/infer/infer.js +163 -17
- package/dist/infer/nullability.js +3 -3
- package/dist/infer/types.d.ts +11 -0
- package/dist/infer/types.js +73 -0
- package/dist/ir/ir.d.ts +42 -2
- package/dist/ir/walk.js +7 -2
- package/dist/lineage/hops.js +10 -3
- package/dist/lineage/lineage.js +7 -2
- package/dist/minijinja/apply-tags.js +3 -1
- package/dist/minijinja/parse.js +3 -1
- package/dist/minijinja/variants.js +3 -1
- package/dist/mysql/lower.js +36 -8
- package/dist/postgres/infer.d.ts +0 -1
- package/dist/postgres/infer.js +74 -15
- package/dist/postgres/lower.js +79 -12
- package/dist/qualify/check-calls.js +8 -2
- package/dist/qualify/qualify.js +3 -1
- package/dist/redshift/lower.js +111 -13
- package/dist/references/references.js +46 -1
- package/dist/scope/scope.js +6 -2
- package/dist/session.d.ts +2 -2
- package/dist/signature/signature.js +3 -1
- package/dist/snowflake/lower.js +23 -4
- package/dist/sqlite/lower.js +25 -2
- package/dist/symbols/symbols.d.ts +1 -1
- package/dist/symbols/symbols.js +37 -6
- package/dist/trino/lower.js +19 -4
- package/dist/tsql/lower.js +104 -4
- package/package.json +3 -3
package/dist/api.d.ts
CHANGED
|
@@ -116,14 +116,14 @@ export type { Token, TokenRole } from "./token/token.js";
|
|
|
116
116
|
export { SqlDocument, type DocumentAnalysis, type StatementCell, type DocumentVariant, type UnionCte, } from "./document/document.js";
|
|
117
117
|
export { LineIndex } from "./document/line-index.js";
|
|
118
118
|
export type { StatementCellSpan } from "./document/split.js";
|
|
119
|
-
export { complete, completeAt, type Completion } from "./completion/complete.js";
|
|
119
|
+
export { complete, completeAt, type Completion, type CompletionResult, type ReplaceRange, } from "./completion/complete.js";
|
|
120
120
|
export { jinjaSlotAt, type JinjaSlot } from "./completion/jinja-slot.js";
|
|
121
121
|
export { signatureAt, type SignatureHelpInfo, type SignatureLabel } from "./signature/signature.js";
|
|
122
122
|
export { SIGNATURES, lookupSignature, hasSignature, type FnSignature, type ParamSig } from "./signature/signatures.js";
|
|
123
123
|
export { renderSignature, type RenderSignatureOptions } from "./signature/render.js";
|
|
124
124
|
export { FN_DOCS, lookupFnDoc, type FnDoc } from "./signature/docs.js";
|
|
125
125
|
export { referencesAt, type Occurrence, type Occurrences } from "./references/references.js";
|
|
126
|
-
export { dialectSymbols, type DialectSymbols } from "./dialect-symbols.js";
|
|
126
|
+
export { dialectSymbols, dialectVocabulary, type DialectSymbols, type DialectVocabulary } from "./dialect-symbols.js";
|
|
127
127
|
export { CallbackSchema, type SchemaProvider, type TableResolver } from "./qualify/schema-provider.js";
|
|
128
128
|
export { DERIVED_DIALECTS, resolveDialect } from "./derived-dialects.js";
|
|
129
129
|
export { foldIdentifier, displayName } from "./dialect-behavior/public-fold.js";
|
package/dist/api.js
CHANGED
|
@@ -204,7 +204,7 @@ export { SqlDocument, } from "./document/document.js";
|
|
|
204
204
|
export { LineIndex } from "./document/line-index.js";
|
|
205
205
|
// Scope-aware completion over a SqlDocument — the broken-input editor feature (keywords + schema
|
|
206
206
|
// tables/columns + function names at the caret). Total: never throws.
|
|
207
|
-
export { complete, completeAt } from "./completion/complete.js";
|
|
207
|
+
export { complete, completeAt, } from "./completion/complete.js";
|
|
208
208
|
// The NEUTRAL half of jinja completion: which call + arg slot the caret sits in inside a jinja tag
|
|
209
209
|
// (`{{ ref('cu│` → { callee: "ref", argIndex: 0, prefix: "cu" }). Carries no dbt vocabulary; a
|
|
210
210
|
// consumer maps callee + argIndex to a role and supplies candidates. Reuses the parsed tags.
|
|
@@ -225,7 +225,7 @@ export { FN_DOCS, lookupFnDoc } from "./signature/docs.js";
|
|
|
225
225
|
export { referencesAt } from "./references/references.js";
|
|
226
226
|
// Per-dialect function/keyword/type-name membership sets — for lint-style "is this identifier a
|
|
227
227
|
// known X for this dialect?" checks. Computed once per dialect and cached.
|
|
228
|
-
export { dialectSymbols } from "./dialect-symbols.js";
|
|
228
|
+
export { dialectSymbols, dialectVocabulary } from "./dialect-symbols.js";
|
|
229
229
|
// The catalog interface the whole pipeline resolves against, plus its resolve-on-demand
|
|
230
230
|
// implementation. `Schema` (a full upfront mapping) and `CallbackSchema` (a host-driven
|
|
231
231
|
// lazy resolver whose prime() bumps a version to invalidate SqlDocument.analyze's memo) both
|
package/dist/bigquery/lower.js
CHANGED
|
@@ -1482,9 +1482,11 @@ function lowerLeaf(node) {
|
|
|
1482
1482
|
case P.RULE_floating_point_literal:
|
|
1483
1483
|
case P.RULE_date_or_time_literal:
|
|
1484
1484
|
case P.RULE_range_literal:
|
|
1485
|
+
return { kind: "literal", text: node.getText(), cst: node };
|
|
1485
1486
|
case P.RULE_parameter_expression:
|
|
1487
|
+
return lowerParameterExpression(node);
|
|
1486
1488
|
case P.RULE_system_variable_expression:
|
|
1487
|
-
return
|
|
1489
|
+
return lowerSystemVariableExpression(node);
|
|
1488
1490
|
case P.RULE_identifier:
|
|
1489
1491
|
return { kind: "column", parts: [identText(node)], partSpans: partSpansOf([node]), cst: node };
|
|
1490
1492
|
case P.RULE_path_expression:
|
|
@@ -1844,7 +1846,12 @@ function columnsOf(expr, acc, clause) {
|
|
|
1844
1846
|
break;
|
|
1845
1847
|
case "subscript":
|
|
1846
1848
|
columnsOf(expr.base, acc, clause);
|
|
1847
|
-
|
|
1849
|
+
if (expr.index)
|
|
1850
|
+
columnsOf(expr.index, acc, clause);
|
|
1851
|
+
if (expr.end)
|
|
1852
|
+
columnsOf(expr.end, acc, clause);
|
|
1853
|
+
if (expr.step)
|
|
1854
|
+
columnsOf(expr.step, acc, clause);
|
|
1848
1855
|
break;
|
|
1849
1856
|
case "lambda":
|
|
1850
1857
|
columnsOf(expr.body, acc, clause);
|
|
@@ -1949,6 +1956,25 @@ function extractExpressionSubqueries(select, fromQueries) {
|
|
|
1949
1956
|
return out;
|
|
1950
1957
|
}
|
|
1951
1958
|
// --- name helpers ----------------------------------------------------------------
|
|
1959
|
+
/** parameter_expression: named_parameter_expression | QUESTION_SYMBOL
|
|
1960
|
+
* (cloud.google.com/bigquery/docs/parameterized-queries). Bare `?` carries no name/ordinal —
|
|
1961
|
+
* BigQuery's positional form has no explicit index, so `ordinal` stays a consumer derivation,
|
|
1962
|
+
* never fabricated here. `@name` resolves through named_parameter_expression's single
|
|
1963
|
+
* dot_identifier; reusing pathParts is safe since that rule holds exactly one dot_identifier
|
|
1964
|
+
* and no identifier head, so it degenerates to a one-element (dot-split, backtick-stripped) path. */
|
|
1965
|
+
function lowerParameterExpression(node) {
|
|
1966
|
+
const named = firstOfRule(node, P.RULE_named_parameter_expression);
|
|
1967
|
+
if (!named)
|
|
1968
|
+
return { kind: "parameter", text: node.getText(), cst: node };
|
|
1969
|
+
return { kind: "parameter", text: node.getText(), name: pathParts(named).join("."), cst: node };
|
|
1970
|
+
}
|
|
1971
|
+
/** system_variable_expression: ATAT_SYMBOL dot_identifier (DOT_SYMBOL dot_identifier)* — a script-
|
|
1972
|
+
* level system variable (cloud.google.com/bigquery/docs/reference/system-variables), e.g.
|
|
1973
|
+
* `@@dataset_id` or the dotted `@@a.b`. `name` is the dotted path with the `@@` sigil stripped;
|
|
1974
|
+
* pathParts already handles this shape (no identifier head, one-or-more dot_identifier tail). */
|
|
1975
|
+
function lowerSystemVariableExpression(node) {
|
|
1976
|
+
return { kind: "variable", text: node.getText(), name: pathParts(node).join("."), system: true, cst: node };
|
|
1977
|
+
}
|
|
1952
1978
|
/** path_expression: identifier (DOT dot_identifier)* — the dotted parts. The head is an identifier;
|
|
1953
1979
|
* later parts are dot_identifier (which may be a reserved keyword after the dot). A single
|
|
1954
1980
|
* backtick-quoted identifier may itself hold a dotted path (`proj.ds.t`), so split each part on `.`. */
|
|
@@ -1,19 +1,40 @@
|
|
|
1
1
|
import type { SqlDocument } from "../document/document.js";
|
|
2
2
|
import type { SchemaProvider } from "../qualify/schema-provider.js";
|
|
3
|
-
/** One completion candidate
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
/** One completion candidate, already pruned to the typed prefix (2026-07-12 ruling) and applied at
|
|
4
|
+
* the caret / `CompletionResult.replaceRange`. The `"template"` kind is a host candidate for a
|
|
5
|
+
* jinja call slot (a dbt model for a ref's arg) — its own, separately-decided contract (the
|
|
6
|
+
* consumer still filters those by the typed prefix; see complete.jinja-candidates.test.ts). */
|
|
6
7
|
export interface Completion {
|
|
7
8
|
label: string;
|
|
8
9
|
kind: "keyword" | "column" | "table" | "cte" | "namespace" | "function" | "template";
|
|
9
10
|
/** Extra display info, e.g. a column's type when the schema knows it. */
|
|
10
11
|
detail?: string;
|
|
11
12
|
}
|
|
13
|
+
/** The caret-anchored span of the partial identifier/keyword the candidates were pruned against —
|
|
14
|
+
* `text.slice(start, end)` is what's already typed. `start` includes an opening delimiter when the
|
|
15
|
+
* caret sits inside a quoted/bracketed/backtick-quoted identifier (`"my_t`, `` `my_t ``, `[my_t`),
|
|
16
|
+
* so an editor that replaces this span never leaves a stray leading quote. `end` never extends past
|
|
17
|
+
* the caret (`offset`) — even where a dialect's lexer greedily swallows an unterminated quoted
|
|
18
|
+
* identifier past the caret, only the already-typed portion is ever reported or matched. */
|
|
19
|
+
export interface ReplaceRange {
|
|
20
|
+
start: number;
|
|
21
|
+
end: number;
|
|
22
|
+
}
|
|
23
|
+
/** completeAt()'s result: an ordinary `Completion[]` (`.map`/`.filter`/iteration/`.length` all work
|
|
24
|
+
* exactly as before — every existing consumer sees no change) carrying one optional extra
|
|
25
|
+
* property, the same "array with named extras" shape TypeScript's own `RegExpMatchArray` uses for
|
|
26
|
+
* `String.prototype.match`. `replaceRange` is present only when the caret sits inside a partially
|
|
27
|
+
* typed word; an empty-prefix caret (a token boundary — nothing typed yet) returns a plain array
|
|
28
|
+
* with no `replaceRange`, byte-identical to the pre-pruning contract. */
|
|
29
|
+
export interface CompletionResult extends Array<Completion> {
|
|
30
|
+
replaceRange?: ReplaceRange;
|
|
31
|
+
}
|
|
12
32
|
/**
|
|
13
|
-
* Completion candidates for the caret at `offset` in `doc
|
|
14
|
-
*
|
|
15
|
-
*
|
|
33
|
+
* Completion candidates for the caret at `offset` in `doc`, pruned to the identifier/keyword
|
|
34
|
+
* fragment already typed there (case-insensitive, dialect-fold-aware; plain prefix match — never
|
|
35
|
+
* fuzzy). Schema-aware when a `Schema` is given (table names + column types). NEVER throws: on
|
|
36
|
+
* broken / mid-edit input it still returns the keyword candidates the walk can reach.
|
|
16
37
|
*/
|
|
17
|
-
export declare function completeAt(doc: SqlDocument, offset: number, schema?: SchemaProvider):
|
|
38
|
+
export declare function completeAt(doc: SqlDocument, offset: number, schema?: SchemaProvider): CompletionResult;
|
|
18
39
|
/** @deprecated Use completeAt — same function, uniform cursor-verb naming. */
|
|
19
40
|
export declare const complete: typeof completeAt;
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// throws — broken input still yields at least the keyword candidates.
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
import { Token } from "antlr4ng";
|
|
19
|
+
import { debugRethrow } from "../debug.js";
|
|
19
20
|
import { nodeAt } from "../document/node-at.js";
|
|
20
21
|
import { resolveBehavior } from "../dialect-behavior/registry.js";
|
|
21
22
|
import { DefaultTemplateProvider } from "../qualify/template-provider.js";
|
|
@@ -26,9 +27,10 @@ import { jinjaSlotAt } from "./jinja-slot.js";
|
|
|
26
27
|
import { COMPLETION_CONFIG } from "./config.js";
|
|
27
28
|
import { completionMeta } from "./parser-factory.js";
|
|
28
29
|
/**
|
|
29
|
-
* Completion candidates for the caret at `offset` in `doc
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* Completion candidates for the caret at `offset` in `doc`, pruned to the identifier/keyword
|
|
31
|
+
* fragment already typed there (case-insensitive, dialect-fold-aware; plain prefix match — never
|
|
32
|
+
* fuzzy). Schema-aware when a `Schema` is given (table names + column types). NEVER throws: on
|
|
33
|
+
* broken / mid-edit input it still returns the keyword candidates the walk can reach.
|
|
32
34
|
*/
|
|
33
35
|
export function completeAt(doc, offset, schema) {
|
|
34
36
|
try {
|
|
@@ -36,8 +38,7 @@ export function completeAt(doc, offset, schema) {
|
|
|
36
38
|
}
|
|
37
39
|
catch (e) {
|
|
38
40
|
// Total by contract: a walk/parse hiccup must not surface to the editor.
|
|
39
|
-
|
|
40
|
-
throw e;
|
|
41
|
+
debugRethrow(e);
|
|
41
42
|
return [];
|
|
42
43
|
}
|
|
43
44
|
}
|
|
@@ -84,7 +85,7 @@ function collect(doc, offset, schema) {
|
|
|
84
85
|
...(cell ? cell.tokens : doc.tokens),
|
|
85
86
|
{ type: Token.EOF, channel: Token.DEFAULT_CHANNEL, start: end, text: "" },
|
|
86
87
|
];
|
|
87
|
-
const caretIdx = caretTokenIndex(walkTokens, offset);
|
|
88
|
+
const caretIdx = caretTokenIndex(walkTokens, offset, cfg);
|
|
88
89
|
const cand = collectCandidates(meta.atn, meta.entryRuleIndex, walkTokens, caretIdx, cfg.preferredRules, cfg.ignoredTokens);
|
|
89
90
|
const out = [];
|
|
90
91
|
const seen = new Set(); // dedup by `${kind}\0${label}`
|
|
@@ -131,7 +132,8 @@ function collect(doc, offset, schema) {
|
|
|
131
132
|
// failure the bare-slot fallback covers. Its member twin: read `FROM/JOIN name [alias]`
|
|
132
133
|
// pairs off the token stream and answer the matching relation's schema columns.
|
|
133
134
|
if (scoped.length === 0 && schema) {
|
|
134
|
-
|
|
135
|
+
const fallback = qualifiedFallbackColumns(walkTokens, cfg, path.parts, schema, dialect, doc.templated?.tags, doc.text);
|
|
136
|
+
for (const c of fallback)
|
|
135
137
|
add(c);
|
|
136
138
|
}
|
|
137
139
|
}
|
|
@@ -158,7 +160,93 @@ function collect(doc, offset, schema) {
|
|
|
158
160
|
add({ label: fn, kind: "function" });
|
|
159
161
|
}
|
|
160
162
|
}
|
|
161
|
-
return out;
|
|
163
|
+
return pruneByPrefix(out, walkTokens, offset, cfg, doc.text, dialect);
|
|
164
|
+
}
|
|
165
|
+
/** SQL identifier-quoting delimiters recognized across all ten dialects
|
|
166
|
+
* (docs/identifier-delimiter-contract.md) — used only to recognize a PARTIAL, possibly
|
|
167
|
+
* unterminated quoted identifier under the caret; a dialect's own quoting/case rules stay put in
|
|
168
|
+
* src/<dialect>/fold.ts. */
|
|
169
|
+
const CLOSE_FOR_OPEN = { '"': '"', "`": "`", "[": "]" };
|
|
170
|
+
/** The identifier/keyword fragment under (or immediately preceding) the caret, read off the
|
|
171
|
+
* document's OWN token stream — never re-lexed. Two shapes recognized:
|
|
172
|
+
* - a bare word (keyword or identifier) the caret sits inside/at the end of (`SEL|`, `ifn|`) —
|
|
173
|
+
* word-START only (`[A-Za-z_]`), so a numeric literal (`… WHERE x > 10|`) is never mistaken
|
|
174
|
+
* for an identifier prefix;
|
|
175
|
+
* - a delimited identifier: either ONE token spanning both delimiters (`cfg.nameTokens` — a
|
|
176
|
+
* complete `"a"`/`` `a` ``/`[a]`, or an unterminated one some dialects lex greedily to EOF), or
|
|
177
|
+
* a lone opening-delimiter character immediately followed by a bare identifier token (the
|
|
178
|
+
* lexer error-recovery split other dialects produce for an unterminated `` `a `` / `"a` — the
|
|
179
|
+
* delimiter alone fails to match any token and is skipped, the identifier body then lexes
|
|
180
|
+
* cleanly on its own).
|
|
181
|
+
* Returns undefined at a token boundary — nothing partially typed (the empty-prefix case, left
|
|
182
|
+
* completely unpruned). The returned span never extends past `offset`: even where a dialect's
|
|
183
|
+
* lexer greedily swallows an unterminated quoted identifier to EOF, only the already-typed portion
|
|
184
|
+
* is ever reported. */
|
|
185
|
+
function identifierPrefixAt(toks, offset, cfg, text) {
|
|
186
|
+
for (let i = 0; i < toks.length; i++) {
|
|
187
|
+
const t = toks[i];
|
|
188
|
+
if (!t || t.channel !== Token.DEFAULT_CHANNEL)
|
|
189
|
+
continue;
|
|
190
|
+
const nameLike = /^[A-Za-z_]/.test(t.text) || cfg.nameTokens.has(t.type);
|
|
191
|
+
if (nameLike && t.start < offset && offset <= t.start + t.text.length) {
|
|
192
|
+
let start = t.start;
|
|
193
|
+
// A lone opening-delimiter token immediately before this one (the split-recovery case) —
|
|
194
|
+
// fold it into the same partial identifier so the replace range covers it too.
|
|
195
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
196
|
+
const p = toks[j];
|
|
197
|
+
if (!p || p.channel !== Token.DEFAULT_CHANNEL)
|
|
198
|
+
continue;
|
|
199
|
+
if (p.start + p.text.length === start && Object.hasOwn(CLOSE_FOR_OPEN, p.text))
|
|
200
|
+
start = p.start;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
return { start, raw: text.slice(start, offset) };
|
|
204
|
+
}
|
|
205
|
+
if (t.start >= offset)
|
|
206
|
+
return undefined; // moved past the caret — nothing partially typed here
|
|
207
|
+
}
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
/** Fold a (possibly partial/unterminated) identifier fragment to the same identity space
|
|
211
|
+
* `behavior.fold` gives a COMPLETE identifier. A partial `"a`/`` `a `` /`[a` has no closing
|
|
212
|
+
* delimiter yet — `fold()`'s own unwrap requires one to recognize it as quoted — so this
|
|
213
|
+
* synthesizes the matching close for the fold call only; the raw text / replace range reported
|
|
214
|
+
* elsewhere are untouched. Folding an already-complete candidate label through this is a no-op. */
|
|
215
|
+
function foldForPrefixMatch(behavior, raw, kind) {
|
|
216
|
+
const open = raw[0];
|
|
217
|
+
const close = open ? CLOSE_FOR_OPEN[open] : undefined;
|
|
218
|
+
const closed = close && !raw.endsWith(close) ? raw + close : raw;
|
|
219
|
+
return behavior.fold(closed, kind);
|
|
220
|
+
}
|
|
221
|
+
/** Post-filters the assembled SQL-slot candidates by the identifier/keyword fragment under the
|
|
222
|
+
* caret (2026-07-12 ruling: "completeAt must return only candidates that validly complete at the
|
|
223
|
+
* caret right now, pruned by the word already typed" — the library holds the token stream, CST,
|
|
224
|
+
* and dialect fold rules, so it prunes here instead of handing the consumer the whole per-slot
|
|
225
|
+
* set). An empty-prefix caret (a token boundary — nothing partially typed) returns `out`
|
|
226
|
+
* completely unchanged: byte-identical to the pre-pruning contract, no `replaceRange`. Keywords
|
|
227
|
+
* fold plain ASCII-case-insensitive (every dialect's keywords are case-insensitive, unlike
|
|
228
|
+
* identifiers); identifier-kind candidates (column/table/cte/namespace/function) fold through the
|
|
229
|
+
* dialect's own identity-key rule, `"table"` kind for `table` candidates (only BigQuery's table
|
|
230
|
+
* case rule differs from the rest). Plain-prefix `startsWith` only — never fuzzy (never-wrong: no
|
|
231
|
+
* guessed matches). Jinja template/call-slot candidates (returned earlier in `collect`, from
|
|
232
|
+
* `templateCompletions`) are OUT of this — that path has its own, separately-decided "editor
|
|
233
|
+
* filters" contract (tests/completion/complete.jinja-candidates.test.ts). */
|
|
234
|
+
function pruneByPrefix(out, toks, offset, cfg, text, dialect) {
|
|
235
|
+
const prefix = identifierPrefixAt(toks, offset, cfg, text);
|
|
236
|
+
if (!prefix)
|
|
237
|
+
return out;
|
|
238
|
+
const behavior = resolveBehavior(dialect);
|
|
239
|
+
const keywordPrefix = prefix.raw.toLowerCase();
|
|
240
|
+
const otherPrefix = foldForPrefixMatch(behavior, prefix.raw, "other");
|
|
241
|
+
const tablePrefix = foldForPrefixMatch(behavior, prefix.raw, "table");
|
|
242
|
+
const pruned = out.filter((c) => {
|
|
243
|
+
if (c.kind === "keyword")
|
|
244
|
+
return c.label.toLowerCase().startsWith(keywordPrefix);
|
|
245
|
+
const kind = c.kind === "table" ? "table" : "other";
|
|
246
|
+
return foldForPrefixMatch(behavior, c.label, kind).startsWith(kind === "table" ? tablePrefix : otherPrefix);
|
|
247
|
+
});
|
|
248
|
+
pruned.replaceRange = { start: prefix.start, end: offset };
|
|
249
|
+
return pruned;
|
|
162
250
|
}
|
|
163
251
|
/** The host's candidates for a jinja call slot, as completions. The template provider carries them,
|
|
164
252
|
* so this reads the `schema` when it is one (a DbtTemplateProvider IS a SchemaProvider, and the host
|
|
@@ -219,18 +307,22 @@ function visibleCteNames(scopes, ast, offset) {
|
|
|
219
307
|
* convention):
|
|
220
308
|
* 1. the token being TYPED — a word-like token whose span CONTAINS the caret (start < offset <=
|
|
221
309
|
* end). A caret at the end of `ifn` completes `ifn`; it does not mean the slot is filled.
|
|
222
|
-
* Word-like
|
|
310
|
+
* Word-like OR a delimited identifier token (cfg.nameTokens: `` `a` ``/`"a"`/`[a]`) counts too —
|
|
311
|
+
* without it, a caret inside a quoted identifier fell through to rule 2 and reported the
|
|
312
|
+
* position of whatever token happened to follow it. Punctuation is never partially typed,
|
|
313
|
+
* so `abs(|` keeps rule 2.
|
|
223
314
|
* 2. between tokens — the first default-channel token whose `.start >= offset`; for an
|
|
224
315
|
* end-of-input caret that is the EOF sentinel's index (last entry).
|
|
225
316
|
* `toks` is the document's own token stream (doc coordinates) with the EOF sentinel appended.
|
|
226
317
|
* Source order makes one pass sufficient: a containing token starts before any `.start >= offset`
|
|
227
318
|
* token, so rule 1 fires first whenever it applies. */
|
|
228
|
-
function caretTokenIndex(toks, offset) {
|
|
319
|
+
function caretTokenIndex(toks, offset, cfg) {
|
|
229
320
|
for (let i = 0; i < toks.length; i++) {
|
|
230
321
|
const t = toks[i];
|
|
231
322
|
if (!t || t.channel !== Token.DEFAULT_CHANNEL)
|
|
232
323
|
continue;
|
|
233
|
-
|
|
324
|
+
const wordLike = /^\w/.test(t.text) || cfg.nameTokens.has(t.type);
|
|
325
|
+
if (wordLike && t.start < offset && offset <= t.start + t.text.length)
|
|
234
326
|
return i;
|
|
235
327
|
if (t.start >= offset)
|
|
236
328
|
return i;
|
|
@@ -322,16 +414,45 @@ function qualifiedSourceColumns(scopes, ast, offset, qualParts, dialect, schema)
|
|
|
322
414
|
/** The member-position twin of `fromRelationColumns` (#38): when the scope is empty (the dangling
|
|
323
415
|
* dot broke the FROM parse), read `FROM/JOIN name(.name)* [AS] [alias]` off the token stream and
|
|
324
416
|
* answer the columns of the ONE relation the qualifier matches — the alias when present, else the
|
|
325
|
-
* name's own trailing parts.
|
|
326
|
-
|
|
417
|
+
* name's own trailing parts. A templated source ({{ ref() }} c) is resolved through the provider
|
|
418
|
+
* (relationOf), matching the qualifier to its alias — the same seam `fromRelationColumns` uses. No
|
|
419
|
+
* match (or several) answers [] — never a fabricated union. */
|
|
420
|
+
function qualifiedFallbackColumns(walkTokens, cfg, qualParts, schema, dialect, tags, text) {
|
|
327
421
|
if (cfg.relationKeywordTokens.size === 0)
|
|
328
422
|
return [];
|
|
329
423
|
const b = resolveBehavior(dialect);
|
|
330
424
|
const toks = walkTokens.filter((t) => t.channel === Token.DEFAULT_CHANNEL);
|
|
331
425
|
const hits = [];
|
|
426
|
+
const colHits = (cols) => {
|
|
427
|
+
if (cols)
|
|
428
|
+
hits.push(cols.map((c) => ({ label: c.name, kind: "column", detail: c.type })));
|
|
429
|
+
};
|
|
332
430
|
for (let i = 0; i + 1 < toks.length; i++) {
|
|
333
431
|
if (!cfg.relationKeywordTokens.has(toks[i].type))
|
|
334
432
|
continue;
|
|
433
|
+
// A templated source ({{ ref('customers') }} c) blanks to a channel-2 tag the filter drops, so
|
|
434
|
+
// the next SQL token is the ALIAS, not a relation name. Resolve the relation through the provider
|
|
435
|
+
// (relationOf) — the same seam fromRelationColumns uses — and match the qualifier to that alias;
|
|
436
|
+
// without this, the alias got read AS the relation name and columnsFor answered nothing.
|
|
437
|
+
const kw = toks[i];
|
|
438
|
+
const next = toks[i + 1];
|
|
439
|
+
const tag = tags?.find((t) => t.kind === "call" && t.tagSpan.start >= kw.start && t.tagSpan.start < next.start);
|
|
440
|
+
if (tag) {
|
|
441
|
+
if (schema instanceof DefaultTemplateProvider) {
|
|
442
|
+
let a = i + 1;
|
|
443
|
+
if (toks[a] && b.fold(toks[a].text) === "as")
|
|
444
|
+
a++;
|
|
445
|
+
const alias = toks[a];
|
|
446
|
+
if (alias &&
|
|
447
|
+
cfg.nameTokens.has(alias.type) &&
|
|
448
|
+
qualParts.length === 1 &&
|
|
449
|
+
b.fold(qualParts[0]) === b.fold(alias.text)) {
|
|
450
|
+
const rel = schema.relationOf(callOf(tag, text));
|
|
451
|
+
colHits(rel ? (rel.columns ?? schema.columnsFor(rel.nameParts, dialect)) : undefined);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
continue; // templated source handled (or unresolvable) — never treat the alias as a relation name
|
|
455
|
+
}
|
|
335
456
|
let j = i + 1;
|
|
336
457
|
if (!toks[j] || !cfg.nameTokens.has(toks[j].type))
|
|
337
458
|
continue;
|
|
@@ -352,9 +473,7 @@ function qualifiedFallbackColumns(walkTokens, cfg, qualParts, schema, dialect) {
|
|
|
352
473
|
qualParts.every((p, k) => b.fold(p, "table") === b.fold(parts[parts.length - qualParts.length + k], "table"));
|
|
353
474
|
if (!matches)
|
|
354
475
|
continue;
|
|
355
|
-
|
|
356
|
-
if (cols)
|
|
357
|
-
hits.push(cols.map((c) => ({ label: c.name, kind: "column", detail: c.type })));
|
|
476
|
+
colHits(schema.columnsFor(parts, dialect));
|
|
358
477
|
}
|
|
359
478
|
return hits.length === 1 ? hits[0] : [];
|
|
360
479
|
}
|
|
@@ -84,7 +84,10 @@ const BIGQUERY_TABLE_RULES = new Set([GoogleSQLParser.RULE_table_path_expression
|
|
|
84
84
|
const BIGQUERY_COLUMN_RULES = new Set([GoogleSQLParser.RULE_identifier]);
|
|
85
85
|
const BIGQUERY_PREFERRED = new Set([...BIGQUERY_TABLE_RULES, ...BIGQUERY_COLUMN_RULES]);
|
|
86
86
|
const BIGQUERY_RELATION_KEYWORDS = new Set([GoogleSQLLexer.FROM_SYMBOL, GoogleSQLLexer.JOIN_SYMBOL]);
|
|
87
|
-
|
|
87
|
+
// UNCLOSED_ESCAPED_IDENTIFIER is the dedicated recovery token GoogleSQL's lexer emits for a
|
|
88
|
+
// backtick-quoted identifier with no closing backtick yet (`` `a `` mid-typing) — completion's
|
|
89
|
+
// caret-in-a-partial-identifier detection needs it alongside the ordinary closed-form token.
|
|
90
|
+
const BIGQUERY_NAME_TOKENS = new Set([GoogleSQLLexer.IDENTIFIER, GoogleSQLLexer.UNCLOSED_ESCAPED_IDENTIFIER]);
|
|
88
91
|
// ── Redshift (Bytebase/Postgres-derived fork) ───────────────────────────────
|
|
89
92
|
// post-FROM → relation_expr (the relation slot; the leaf `identifier` also surfaces post-FROM
|
|
90
93
|
// but never in column position, so tableRules uses relation_expr).
|
|
@@ -93,7 +96,13 @@ const REDSHIFT_TABLE_RULES = new Set([RedshiftParser.RULE_relation_expr]);
|
|
|
93
96
|
const REDSHIFT_COLUMN_RULES = new Set([RedshiftParser.RULE_a_expr]);
|
|
94
97
|
const REDSHIFT_PREFERRED = new Set([...REDSHIFT_TABLE_RULES, ...REDSHIFT_COLUMN_RULES]);
|
|
95
98
|
const REDSHIFT_RELATION_KEYWORDS = new Set([RedshiftLexer.FROM, RedshiftLexer.JOIN]);
|
|
96
|
-
|
|
99
|
+
// UnterminatedQuotedIdentifier is the dedicated recovery token this Postgres-lineage lexer emits
|
|
100
|
+
// for a `"a` with no closing quote yet (mid-typing) — needed alongside the ordinary closed form.
|
|
101
|
+
const REDSHIFT_NAME_TOKENS = new Set([
|
|
102
|
+
RedshiftLexer.Identifier,
|
|
103
|
+
RedshiftLexer.QuotedIdentifier,
|
|
104
|
+
RedshiftLexer.UnterminatedQuotedIdentifier,
|
|
105
|
+
]);
|
|
97
106
|
// ── Postgres / DuckDB (TVL-lineage forks like Redshift) ─────────────────────
|
|
98
107
|
// The same rule split as Redshift applies (same grammar shapes): post-FROM → relation_expr,
|
|
99
108
|
// SELECT/WHERE → a_expr; relation names are plain/quoted identifiers after FROM/JOIN.
|
|
@@ -101,12 +110,24 @@ const POSTGRES_TABLE_RULES = new Set([PostgresParser.RULE_relation_expr]);
|
|
|
101
110
|
const POSTGRES_COLUMN_RULES = new Set([PostgresParser.RULE_a_expr]);
|
|
102
111
|
const POSTGRES_PREFERRED = new Set([...POSTGRES_TABLE_RULES, ...POSTGRES_COLUMN_RULES]);
|
|
103
112
|
const POSTGRES_RELATION_KEYWORDS = new Set([PostgresLexer.FROM, PostgresLexer.JOIN]);
|
|
104
|
-
|
|
113
|
+
// UnterminatedQuotedIdentifier is the dedicated recovery token this lexer emits for a `"a` with no
|
|
114
|
+
// closing quote yet (mid-typing) — needed alongside the ordinary closed form.
|
|
115
|
+
const POSTGRES_NAME_TOKENS = new Set([
|
|
116
|
+
PostgresLexer.Identifier,
|
|
117
|
+
PostgresLexer.QuotedIdentifier,
|
|
118
|
+
PostgresLexer.UnterminatedQuotedIdentifier,
|
|
119
|
+
]);
|
|
105
120
|
const DUCKDB_TABLE_RULES = new Set([DuckdbParser.RULE_relation_expr]);
|
|
106
121
|
const DUCKDB_COLUMN_RULES = new Set([DuckdbParser.RULE_a_expr]);
|
|
107
122
|
const DUCKDB_PREFERRED = new Set([...DUCKDB_TABLE_RULES, ...DUCKDB_COLUMN_RULES]);
|
|
108
123
|
const DUCKDB_RELATION_KEYWORDS = new Set([DuckdbLexer.FROM, DuckdbLexer.JOIN]);
|
|
109
|
-
|
|
124
|
+
// UnterminatedQuotedIdentifier is the dedicated recovery token this Postgres-lineage lexer emits
|
|
125
|
+
// for a `"a` with no closing quote yet (mid-typing) — needed alongside the ordinary closed form.
|
|
126
|
+
const DUCKDB_NAME_TOKENS = new Set([
|
|
127
|
+
DuckdbLexer.Identifier,
|
|
128
|
+
DuckdbLexer.QuotedIdentifier,
|
|
129
|
+
DuckdbLexer.UnterminatedQuotedIdentifier,
|
|
130
|
+
]);
|
|
110
131
|
// -- Trino (first-party SqlBase.g4 split) ------------------------------------
|
|
111
132
|
// Post-FROM relation names live under relationPrimary/qualifiedName; column slots are
|
|
112
133
|
// primaryExpression. Identifiers: plain / "quoted" / backquoted / digit-led.
|
|
@@ -2,7 +2,7 @@ import { acceptsFor } from "../dialect-behavior/coerce-rules.js";
|
|
|
2
2
|
import { likePatternToRegExp } from "../scope/like-pattern.js";
|
|
3
3
|
import { SIGNATURES } from "../signature/signatures.js";
|
|
4
4
|
import { displayName, fold, foldTableName, matchesSourceKey, DATABRICKS_NAME_CONFIG } from "./fold.js";
|
|
5
|
-
import { databricksLiteral, databricksParseType, DATABRICKS_FUNCTION_RETURNS } from "./infer.js";
|
|
5
|
+
import { databricksLiteral, databricksParseType, databricksSpecial, DATABRICKS_FUNCTION_RETURNS } from "./infer.js";
|
|
6
6
|
export const databricksBehavior = {
|
|
7
7
|
fold,
|
|
8
8
|
displayName,
|
|
@@ -14,6 +14,8 @@ export const databricksBehavior = {
|
|
|
14
14
|
parseType: databricksParseType,
|
|
15
15
|
functions: DATABRICKS_FUNCTION_RETURNS,
|
|
16
16
|
division: "float",
|
|
17
|
+
dateSubtraction: "interval",
|
|
18
|
+
special: databricksSpecial,
|
|
17
19
|
signatures: SIGNATURES.databricks,
|
|
18
20
|
// Databricks implicit coercion: STRING containing a number coerces to numeric (STR_TO_NUM=true), no bool<->num (BOOL_NUM=false).
|
|
19
21
|
accepts: (argType, paramText) => acceptsFor(databricksParseType, true, false, argType, paramText),
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { type FnRule } from "../infer/functions.js";
|
|
2
2
|
import { type Type } from "../infer/types.js";
|
|
3
|
+
import type { Expr } from "../ir/ir.js";
|
|
4
|
+
export declare function databricksSpecial(fn: Extract<Expr, {
|
|
5
|
+
kind: "function";
|
|
6
|
+
}>, typeOf: (e: Expr) => Type): Type | undefined;
|
|
3
7
|
export declare const DATABRICKS_FUNCTION_RETURNS: Record<string, FnRule>;
|
|
4
8
|
/** Databricks/Spark literal forms. */
|
|
5
9
|
export declare function databricksLiteral(text: string): Type;
|