sqllens 1.3.0 → 1.5.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/README.md +19 -0
- package/dist/bigquery/behavior.js +2 -1
- package/dist/bigquery/fold.d.ts +6 -0
- package/dist/bigquery/fold.js +8 -0
- package/dist/bigquery/lower.js +37 -6
- package/dist/completion/complete.d.ts +1 -1
- package/dist/completion/complete.js +166 -20
- package/dist/completion/jinja-slot.d.ts +6 -0
- package/dist/completion/jinja-slot.js +12 -2
- package/dist/databricks/behavior.js +2 -1
- package/dist/databricks/fold.d.ts +4 -0
- package/dist/databricks/fold.js +6 -0
- package/dist/databricks/lower.js +10 -3
- package/dist/dialect-behavior/behavior.d.ts +5 -0
- package/dist/duckdb/behavior.js +2 -1
- package/dist/duckdb/fold.d.ts +4 -0
- package/dist/duckdb/fold.js +6 -0
- package/dist/duckdb/lower.js +39 -8
- package/dist/index.d.ts +1 -1
- package/dist/ir/ir.d.ts +16 -6
- package/dist/ir/qualified-name.d.ts +43 -0
- package/dist/ir/qualified-name.js +74 -0
- package/dist/lineage/hops.js +7 -3
- package/dist/lineage/lineage.js +2 -1
- package/dist/minijinja/apply-tags.js +43 -11
- package/dist/mysql/behavior.js +2 -1
- package/dist/mysql/fold.d.ts +4 -0
- package/dist/mysql/fold.js +6 -0
- package/dist/mysql/lower.js +19 -4
- package/dist/postgres/behavior.js +2 -1
- package/dist/postgres/fold.d.ts +5 -0
- package/dist/postgres/fold.js +7 -0
- package/dist/postgres/lower.js +21 -5
- package/dist/qualify/qualify.d.ts +1 -1
- package/dist/qualify/qualify.js +75 -50
- package/dist/qualify/schema-provider.d.ts +13 -0
- package/dist/qualify/schema.d.ts +18 -1
- package/dist/qualify/schema.js +60 -6
- package/dist/qualify/template-provider.d.ts +5 -2
- package/dist/qualify/template-provider.js +5 -2
- package/dist/redshift/behavior.js +2 -1
- package/dist/redshift/fold.d.ts +4 -0
- package/dist/redshift/fold.js +6 -0
- package/dist/redshift/lower.js +20 -4
- package/dist/scope/scope.d.ts +24 -2
- package/dist/scope/scope.js +65 -13
- package/dist/sema/resolve.js +21 -10
- package/dist/snowflake/behavior.js +2 -1
- package/dist/snowflake/fold.d.ts +4 -0
- package/dist/snowflake/fold.js +6 -0
- package/dist/snowflake/lower.js +23 -4
- package/dist/sqlite/behavior.js +2 -1
- package/dist/sqlite/fold.d.ts +4 -0
- package/dist/sqlite/fold.js +6 -0
- package/dist/sqlite/lower.js +11 -2
- package/dist/trino/behavior.js +2 -1
- package/dist/trino/fold.d.ts +3 -0
- package/dist/trino/fold.js +5 -0
- package/dist/trino/lower.js +22 -4
- package/dist/tsql/behavior.js +2 -1
- package/dist/tsql/fold.d.ts +5 -0
- package/dist/tsql/fold.js +7 -0
- package/dist/tsql/lower.js +18 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,6 +17,25 @@ The front end is error-tolerant and token-first, so the library drives editor
|
|
|
17
17
|
features (completion, hover, diagnostics, go-to-definition) over incomplete,
|
|
18
18
|
mid-edit text. See [Editor / language tooling](#editor--language-tooling).
|
|
19
19
|
|
|
20
|
+
## Guiding principles
|
|
21
|
+
|
|
22
|
+
Two principles govern every layer of this library, and they are duals of each other:
|
|
23
|
+
|
|
24
|
+
- **Never wrong.** A wrong answer is worse than no answer. A name, type, or binding
|
|
25
|
+
that cannot be derived from a documented source stays absent or `unknown`; it is
|
|
26
|
+
never guessed. Where a function's return type depends on an argument's value, the
|
|
27
|
+
answer is `unknown`, not a plausible guess. Where a relation cannot be resolved,
|
|
28
|
+
its name is the text the user wrote, never something the library invented.
|
|
29
|
+
- **Lossless.** Parsing never discards information the input carried. Every token
|
|
30
|
+
survives with its exact source span (even on broken input), identifiers keep their
|
|
31
|
+
as-written spelling and quoting alongside their folded identity, and what the parse
|
|
32
|
+
knew structurally the IR carries, so no downstream layer has to re-derive it by
|
|
33
|
+
heuristic.
|
|
34
|
+
|
|
35
|
+
Never wrong constrains what the library claims; lossless constrains what it keeps.
|
|
36
|
+
Together they are why analysis results can be trusted in an editor: what you are
|
|
37
|
+
shown is derived, and what you wrote is still there.
|
|
38
|
+
|
|
20
39
|
```bash
|
|
21
40
|
npm install sqllens
|
|
22
41
|
```
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
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
|
-
import { displayName, fold, foldTableName, matchesSourceKey } from "./fold.js";
|
|
4
|
+
import { displayName, fold, foldTableName, matchesSourceKey, BIGQUERY_NAME_CONFIG } from "./fold.js";
|
|
5
5
|
import { bigqueryLiteral, bigqueryParseType, bigquerySpecial, BIGQUERY_FUNCTION_RETURNS } from "./infer.js";
|
|
6
6
|
export const bigqueryBehavior = {
|
|
7
7
|
fold,
|
|
8
8
|
displayName,
|
|
9
9
|
foldTableName,
|
|
10
10
|
matchesSourceKey,
|
|
11
|
+
nameConfig: BIGQUERY_NAME_CONFIG,
|
|
11
12
|
likeMatch: (pattern, value) => likePatternToRegExp(pattern).test(value),
|
|
12
13
|
literal: bigqueryLiteral,
|
|
13
14
|
parseType: bigqueryParseType,
|
package/dist/bigquery/fold.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { type FoldRule, type IdentKind } from "../ident/fold.js";
|
|
2
|
+
import type { QualifiedNameConfig } from "../ir/qualified-name.js";
|
|
2
3
|
export declare const BIGQUERY_FOLD_RULE: FoldRule;
|
|
4
|
+
/** project.dataset.table (cloud.google.com/bigquery/docs — "Qualifying table names").
|
|
5
|
+
* Normalized vocabulary: catalog = project, schema = dataset. Relation-path parts keep case in
|
|
6
|
+
* the identity key (this rule's tableCase: "preserve" — dataset and table names are
|
|
7
|
+
* case-sensitive). */
|
|
8
|
+
export declare const BIGQUERY_NAME_CONFIG: QualifiedNameConfig;
|
|
3
9
|
/** Fold an identifier to its BigQuery identity key. */
|
|
4
10
|
export declare function fold(raw: string, kind?: IdentKind): string;
|
|
5
11
|
/** Presentation twin: strip delimiters, no case change. */
|
package/dist/bigquery/fold.js
CHANGED
|
@@ -23,6 +23,14 @@ export const BIGQUERY_FOLD_RULE = {
|
|
|
23
23
|
tableCase: "preserve",
|
|
24
24
|
escapeStyle: "backslash",
|
|
25
25
|
};
|
|
26
|
+
/** project.dataset.table (cloud.google.com/bigquery/docs — "Qualifying table names").
|
|
27
|
+
* Normalized vocabulary: catalog = project, schema = dataset. Relation-path parts keep case in
|
|
28
|
+
* the identity key (this rule's tableCase: "preserve" — dataset and table names are
|
|
29
|
+
* case-sensitive). */
|
|
30
|
+
export const BIGQUERY_NAME_CONFIG = {
|
|
31
|
+
roles: ["catalog", "schema"],
|
|
32
|
+
rule: BIGQUERY_FOLD_RULE,
|
|
33
|
+
};
|
|
26
34
|
/** Fold an identifier to its BigQuery identity key. */
|
|
27
35
|
export function fold(raw, kind = "other") {
|
|
28
36
|
return foldWith(BIGQUERY_FOLD_RULE, raw, kind);
|
package/dist/bigquery/lower.js
CHANGED
|
@@ -3,6 +3,17 @@ import { GoogleSQLParser as P } from "../generated/bigquery/GoogleSQLParser.js";
|
|
|
3
3
|
import { keywordCategory, swallowedCategories, swallowedStatements } from "../ir/statement.js";
|
|
4
4
|
import { partSpanOf, partSpansOf } from "../ir/part-span.js";
|
|
5
5
|
import { freezeIR } from "../ir/freeze.js";
|
|
6
|
+
import { synthesizedQualifiedName } from "../ir/qualified-name.js";
|
|
7
|
+
import { BIGQUERY_NAME_CONFIG } from "./fold.js";
|
|
8
|
+
/** The structured name for a table source's parts (issue #38). BigQuery's lower() strips
|
|
9
|
+
* backtick delimiters from every identifier (the documented exception in
|
|
10
|
+
* docs/identifier-delimiter-contract.md), so parts arrive BARE: the synthesized builder is the
|
|
11
|
+
* correct one (identity is unaffected — quoted and unquoted fold identically in this dialect),
|
|
12
|
+
* and `fqn` re-renders quoting only where a part needs it. The byte-exact written form stays
|
|
13
|
+
* recoverable via namePartSpans, per the contract's own remedy. */
|
|
14
|
+
function relationOf(rawParts) {
|
|
15
|
+
return synthesizedQualifiedName(rawParts, BIGQUERY_NAME_CONFIG);
|
|
16
|
+
}
|
|
6
17
|
// ---------------------------------------------------------------------------
|
|
7
18
|
// Lowering — BigQuery / GoogleSQL (forked bytebase/parser googlesql/) CST ->
|
|
8
19
|
// the shared, dialect-neutral IR (src/ir/ir.ts). The semantic layer runs on
|
|
@@ -224,10 +235,11 @@ function lowerQueryPrimary(primary) {
|
|
|
224
235
|
// `TABLE name` ≡ `SELECT * FROM name`.
|
|
225
236
|
const path = directChildrenOfRule(primary, P.RULE_path_expression)[0];
|
|
226
237
|
if (path) {
|
|
238
|
+
const name = pathParts(path);
|
|
227
239
|
return {
|
|
228
240
|
kind: "select",
|
|
229
241
|
projections: [implicitStar(primary)],
|
|
230
|
-
from: [{ kind: "table",
|
|
242
|
+
from: [{ kind: "table", relation: relationOf(name), namePartSpans: pathPartSpans(path), cst: path }],
|
|
231
243
|
columns: [],
|
|
232
244
|
aggregated: false,
|
|
233
245
|
cst: primary,
|
|
@@ -374,10 +386,20 @@ function lowerPipeSetOperand(operand) {
|
|
|
374
386
|
if (tc) {
|
|
375
387
|
// table_clause: TABLE path_expression | TABLE tvf — `TABLE name` ≡ `SELECT * FROM name`.
|
|
376
388
|
const path = directChildrenOfRule(tc, P.RULE_path_expression)[0];
|
|
389
|
+
const name = path ? pathParts(path) : undefined;
|
|
377
390
|
const body = {
|
|
378
391
|
kind: "select",
|
|
379
392
|
projections: path ? [implicitStar(tc)] : [],
|
|
380
|
-
from: path
|
|
393
|
+
from: path && name
|
|
394
|
+
? [
|
|
395
|
+
{
|
|
396
|
+
kind: "table",
|
|
397
|
+
relation: relationOf(name),
|
|
398
|
+
namePartSpans: pathPartSpans(path),
|
|
399
|
+
cst: path,
|
|
400
|
+
},
|
|
401
|
+
]
|
|
402
|
+
: [],
|
|
381
403
|
columns: [],
|
|
382
404
|
aggregated: false,
|
|
383
405
|
cst: tc,
|
|
@@ -414,7 +436,7 @@ function lowerPipeJoin(join, cst) {
|
|
|
414
436
|
const tp = directChildrenOfRule(join, P.RULE_table_primary)[0];
|
|
415
437
|
if (tp)
|
|
416
438
|
collectTablePrimary(tp, out, unsupported);
|
|
417
|
-
const source = out[0] ?? { kind: "table",
|
|
439
|
+
const source = out[0] ?? { kind: "table", relation: relationOf([]), cst: join };
|
|
418
440
|
const joinConditions = [];
|
|
419
441
|
const columns = [];
|
|
420
442
|
const onUsing = directChildrenOfRule(join, P.RULE_on_or_using_clause)[0];
|
|
@@ -917,16 +939,18 @@ function buildSource(tp, unsupported) {
|
|
|
917
939
|
if (tvf) {
|
|
918
940
|
const path = firstOfRule(tvf, P.RULE_path_expression);
|
|
919
941
|
const aliasInfo = aliasOf(directChildrenOfRule(tvf, P.RULE_pivot_or_unpivot_clause_and_aliases)[0]);
|
|
942
|
+
const name = path ? pathParts(path) : [tp.getText()];
|
|
920
943
|
return {
|
|
921
944
|
kind: "table",
|
|
922
|
-
|
|
945
|
+
relation: relationOf(name),
|
|
923
946
|
namePartSpans: path ? pathPartSpans(path) : undefined,
|
|
924
947
|
alias: aliasInfo?.alias,
|
|
925
948
|
aliasCst: aliasInfo?.cst,
|
|
926
949
|
cst: tp,
|
|
927
950
|
};
|
|
928
951
|
}
|
|
929
|
-
|
|
952
|
+
const name = [stripBackticks(tp.getText())];
|
|
953
|
+
return { kind: "table", relation: relationOf(name), cst: tp };
|
|
930
954
|
}
|
|
931
955
|
// --- graph / GQL -----------------------------------------------------------------
|
|
932
956
|
// GRAPH_TABLE(graph MATCH … COLUMNS(…)) in FROM, and the standalone `GRAPH g … RETURN …` statement.
|
|
@@ -1072,7 +1096,14 @@ function buildPathSource(pathExpr) {
|
|
|
1072
1096
|
const path = base ? firstOfRule(base, P.RULE_path_expression) : undefined;
|
|
1073
1097
|
const name = path ? pathParts(path) : base ? dashedPathParts(base) : [stripBackticks(pathExpr.getText())];
|
|
1074
1098
|
const namePartSpans = path ? pathPartSpans(path) : undefined;
|
|
1075
|
-
return {
|
|
1099
|
+
return {
|
|
1100
|
+
kind: "table",
|
|
1101
|
+
relation: relationOf(name),
|
|
1102
|
+
namePartSpans,
|
|
1103
|
+
alias: aliasInfo?.alias,
|
|
1104
|
+
aliasCst: aliasInfo?.cst,
|
|
1105
|
+
cst: pathExpr,
|
|
1106
|
+
};
|
|
1076
1107
|
}
|
|
1077
1108
|
/** table_path_alias_or_qualify / table_path_pivot_suffix / pivot_or_unpivot_clause_and_aliases → its leading identifier. */
|
|
1078
1109
|
function aliasOf(node) {
|
|
@@ -5,7 +5,7 @@ import type { SchemaProvider } from "../qualify/schema-provider.js";
|
|
|
5
5
|
* `"template"` kind is a host candidate for a jinja call slot (a dbt model for a ref's arg). */
|
|
6
6
|
export interface Completion {
|
|
7
7
|
label: string;
|
|
8
|
-
kind: "keyword" | "column" | "table" | "function" | "template";
|
|
8
|
+
kind: "keyword" | "column" | "table" | "cte" | "namespace" | "function" | "template";
|
|
9
9
|
/** Extra display info, e.g. a column's type when the schema knows it. */
|
|
10
10
|
detail?: string;
|
|
11
11
|
}
|
|
@@ -20,6 +20,7 @@ import { nodeAt } from "../document/node-at.js";
|
|
|
20
20
|
import { resolveBehavior } from "../dialect-behavior/registry.js";
|
|
21
21
|
import { DefaultTemplateProvider } from "../qualify/template-provider.js";
|
|
22
22
|
import { callOf } from "../minijinja/apply-tags.js";
|
|
23
|
+
import { sourcesMatchingQualifier } from "../scope/scope.js";
|
|
23
24
|
import { collectCandidates } from "./atn-walk.js";
|
|
24
25
|
import { jinjaSlotAt } from "./jinja-slot.js";
|
|
25
26
|
import { COMPLETION_CONFIG } from "./config.js";
|
|
@@ -33,8 +34,10 @@ export function completeAt(doc, offset, schema) {
|
|
|
33
34
|
try {
|
|
34
35
|
return collect(doc, offset, schema);
|
|
35
36
|
}
|
|
36
|
-
catch {
|
|
37
|
+
catch (e) {
|
|
37
38
|
// Total by contract: a walk/parse hiccup must not surface to the editor.
|
|
39
|
+
if (process.env.SQLLENS_DEBUG_COMPLETE)
|
|
40
|
+
throw e;
|
|
38
41
|
return [];
|
|
39
42
|
}
|
|
40
43
|
}
|
|
@@ -98,27 +101,62 @@ function collect(doc, offset, schema) {
|
|
|
98
101
|
if (label)
|
|
99
102
|
add({ label, kind: "keyword" });
|
|
100
103
|
}
|
|
104
|
+
// A RELATION PATH position (#38 stage 6): a dotted chain right before the caret whose anchor
|
|
105
|
+
// token is FROM/JOIN-family (cfg.relationKeywordTokens — the same per-dialect set the
|
|
106
|
+
// broken-input fallback uses). Mid-path the ATN reports a generic identifier slot, so the
|
|
107
|
+
// anchor, not the rule set, is the discriminator. The candidates are the typed prefix's NEXT
|
|
108
|
+
// SEGMENTS (segment labels only — a client replaces the caret token, so a full path would
|
|
109
|
+
// double-insert), never CTEs, and never the column/function noise the identifier slot would
|
|
110
|
+
// otherwise pour in.
|
|
111
|
+
const path = dottedPrefixAt(walkTokens, caretIdx);
|
|
112
|
+
const atRelationPath = path.parts.length > 0 && path.anchorIdx >= 0 && cfg.relationKeywordTokens.has(walkTokens[path.anchorIdx].type);
|
|
101
113
|
const atTable = intersects(cand.rules, cfg.tableRules);
|
|
102
114
|
const atColumn = intersects(cand.rules, cfg.columnRules);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
115
|
+
if (atRelationPath) {
|
|
116
|
+
if (schema?.childrenOf) {
|
|
117
|
+
for (const child of schema.childrenOf(path.parts, dialect))
|
|
118
|
+
add({ label: child.name, kind: child.kind });
|
|
119
|
+
}
|
|
107
120
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
121
|
+
else if (path.parts.length > 0) {
|
|
122
|
+
// A qualified MEMBER position (`o.|`, `gold.orders.|`) — anvil item 2: only the columns of
|
|
123
|
+
// the source the qualifier matches (the same validated any-depth primitive binding uses),
|
|
124
|
+
// no function/keyword noise. Deliberately NOT gated on the walk's rules: at a dangling dot
|
|
125
|
+
// mid-edit the walk often reports nothing, but the dot chain itself is the member-position
|
|
126
|
+
// evidence (it is the completion trigger character), and an unmatched qualifier answers [].
|
|
127
|
+
const scoped = qualifiedSourceColumns(cellScopes, cellAst, cellOffset, path.parts, dialect, schema);
|
|
128
|
+
for (const c of scoped)
|
|
113
129
|
add(c);
|
|
114
|
-
|
|
115
|
-
|
|
130
|
+
// Mid-edit the dangling dot often breaks the FROM parse and the scope is EMPTY — the same
|
|
131
|
+
// failure the bare-slot fallback covers. Its member twin: read `FROM/JOIN name [alias]`
|
|
132
|
+
// pairs off the token stream and answer the matching relation's schema columns.
|
|
133
|
+
if (scoped.length === 0 && schema) {
|
|
134
|
+
for (const c of qualifiedFallbackColumns(walkTokens, cfg, path.parts, schema, dialect))
|
|
116
135
|
add(c);
|
|
136
|
+
}
|
|
117
137
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
138
|
+
else {
|
|
139
|
+
if (atTable) {
|
|
140
|
+
// Bare relation slot: in-scope CTE names FIRST (they shadow same-named catalog tables),
|
|
141
|
+
// then the catalog's tables.
|
|
142
|
+
for (const name of visibleCteNames(cellScopes, cellAst, cellOffset))
|
|
143
|
+
add({ label: name, kind: "cte" });
|
|
144
|
+
if (schema)
|
|
145
|
+
for (const t of schema.tables(dialect))
|
|
146
|
+
add({ label: t, kind: "table" });
|
|
147
|
+
}
|
|
148
|
+
// columns — value/column slot: the columns visible from the enclosing scope, plus a
|
|
149
|
+
// broken-input fallback reading FROM/JOIN relation names straight off the token stream.
|
|
150
|
+
if (atColumn) {
|
|
151
|
+
for (const c of visibleColumns(cellScopes, cellAst, dialect, cellOffset, schema))
|
|
152
|
+
add(c);
|
|
153
|
+
if (schema)
|
|
154
|
+
for (const c of fromRelationColumns(walkTokens, cfg, schema, dialect, doc.templated?.tags, doc.text))
|
|
155
|
+
add(c);
|
|
156
|
+
// functions — value/column slot: the dialect's inference-registry function names.
|
|
157
|
+
for (const fn of Object.keys(resolveBehavior(dialect).functions))
|
|
158
|
+
add({ label: fn, kind: "function" });
|
|
159
|
+
}
|
|
122
160
|
}
|
|
123
161
|
return out;
|
|
124
162
|
}
|
|
@@ -130,20 +168,70 @@ function collect(doc, offset, schema) {
|
|
|
130
168
|
function templateCompletions(slot, schema) {
|
|
131
169
|
if (!(schema instanceof DefaultTemplateProvider))
|
|
132
170
|
return [];
|
|
133
|
-
return schema.templateCandidates(slot.
|
|
171
|
+
return schema.templateCandidates(slot.call, slot.argIndex).map((c) => ({
|
|
134
172
|
label: c.label,
|
|
135
173
|
kind: "template",
|
|
136
174
|
...(c.detail !== undefined ? { detail: c.detail } : {}),
|
|
137
175
|
}));
|
|
138
176
|
}
|
|
139
|
-
/** The
|
|
140
|
-
*
|
|
141
|
-
*
|
|
177
|
+
/** The dotted qualifier immediately before the caret (#38): `analytics.` → ["analytics"],
|
|
178
|
+
* `analytics.sales.` → ["analytics","sales"], `analytics.sa|` (typing a segment) → ["analytics"].
|
|
179
|
+
* Reads the walk's own token stream backwards from the caret token: an optional partial segment,
|
|
180
|
+
* then (DOT ident)+ chains. `anchorIdx` is the default-channel token BEFORE the whole chain
|
|
181
|
+
* (-1 at document start) — its type says what the chain qualifies (FROM/JOIN → a relation path).
|
|
182
|
+
* parts: [] when no dot chain precedes the caret. Raw texts, delimiters intact — the schema
|
|
183
|
+
* folds. */
|
|
184
|
+
function dottedPrefixAt(toks, caretIdx) {
|
|
185
|
+
const prev = (i) => {
|
|
186
|
+
for (let j = i - 1; j >= 0; j--)
|
|
187
|
+
if (toks[j].channel === Token.DEFAULT_CHANNEL)
|
|
188
|
+
return j;
|
|
189
|
+
return -1;
|
|
190
|
+
};
|
|
191
|
+
let i = caretIdx;
|
|
192
|
+
// A word-like caret token is the partial segment being typed — the chain sits before it.
|
|
193
|
+
if (toks[i] && /^\w/.test(toks[i].text))
|
|
194
|
+
i = prev(i);
|
|
195
|
+
// `i` is now the DOT itself (partial-segment case) or the caret slot (then look back one).
|
|
196
|
+
let d = toks[i]?.text === "." ? i : prev(i);
|
|
197
|
+
const parts = [];
|
|
198
|
+
let anchorIdx = d;
|
|
199
|
+
while (d >= 0 && toks[d]?.text === ".") {
|
|
200
|
+
const ident = prev(d);
|
|
201
|
+
if (ident < 0 || !/^[\w"`[\]]/.test(toks[ident].text))
|
|
202
|
+
break;
|
|
203
|
+
parts.unshift(toks[ident].text);
|
|
204
|
+
anchorIdx = prev(ident);
|
|
205
|
+
d = anchorIdx;
|
|
206
|
+
}
|
|
207
|
+
return { parts, anchorIdx };
|
|
208
|
+
}
|
|
209
|
+
/** The CTE names visible from the caret's enclosing scope, as declared (display text). */
|
|
210
|
+
function visibleCteNames(scopes, ast, offset) {
|
|
211
|
+
const scope = enclosingScope(scopes, ast, offset);
|
|
212
|
+
const out = [];
|
|
213
|
+
for (let s = scope; s; s = s.parent)
|
|
214
|
+
for (const cte of s.ctes.values())
|
|
215
|
+
out.push(cte.def.name);
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
/** The walk's caret token index. Two rules, in order (anvil 2026-07-15; antlr4-c3's own caret
|
|
219
|
+
* convention):
|
|
220
|
+
* 1. the token being TYPED — a word-like token whose span CONTAINS the caret (start < offset <=
|
|
221
|
+
* end). A caret at the end of `ifn` completes `ifn`; it does not mean the slot is filled.
|
|
222
|
+
* Word-like only: punctuation is never partially typed, so `abs(|` keeps rule 2.
|
|
223
|
+
* 2. between tokens — the first default-channel token whose `.start >= offset`; for an
|
|
224
|
+
* end-of-input caret that is the EOF sentinel's index (last entry).
|
|
225
|
+
* `toks` is the document's own token stream (doc coordinates) with the EOF sentinel appended.
|
|
226
|
+
* Source order makes one pass sufficient: a containing token starts before any `.start >= offset`
|
|
227
|
+
* token, so rule 1 fires first whenever it applies. */
|
|
142
228
|
function caretTokenIndex(toks, offset) {
|
|
143
229
|
for (let i = 0; i < toks.length; i++) {
|
|
144
230
|
const t = toks[i];
|
|
145
231
|
if (!t || t.channel !== Token.DEFAULT_CHANNEL)
|
|
146
232
|
continue;
|
|
233
|
+
if (/^\w/.test(t.text) && t.start < offset && offset <= t.start + t.text.length)
|
|
234
|
+
return i;
|
|
147
235
|
if (t.start >= offset)
|
|
148
236
|
return i;
|
|
149
237
|
}
|
|
@@ -212,6 +300,64 @@ function fromRelationColumns(walkTokens, cfg, schema, dialect, tags, text) {
|
|
|
212
300
|
}
|
|
213
301
|
return out;
|
|
214
302
|
}
|
|
303
|
+
/** The columns of the ONE source a dotted qualifier matches from the caret's scope (anvil item 2):
|
|
304
|
+
* `o.|` answers o's columns only. Matching is sourcesMatchingQualifier — the same validated,
|
|
305
|
+
* any-depth primitive column BINDING uses — walking enclosing scopes nearest-first. Ambiguous or
|
|
306
|
+
* unmatched qualifiers answer nothing (never a fabricated union). */
|
|
307
|
+
function qualifiedSourceColumns(scopes, ast, offset, qualParts, dialect, schema) {
|
|
308
|
+
const scope = enclosingScope(scopes, ast, offset);
|
|
309
|
+
if (!scope)
|
|
310
|
+
return [];
|
|
311
|
+
const behavior = resolveBehavior(dialect);
|
|
312
|
+
for (let s = scope; s; s = s.parent) {
|
|
313
|
+
const matches = sourcesMatchingQualifier(s, qualParts);
|
|
314
|
+
if (matches.length > 1)
|
|
315
|
+
return [];
|
|
316
|
+
if (matches.length === 1) {
|
|
317
|
+
return columnsOf(matches[0], dialect, schema).map((c) => ({ ...c, label: behavior.displayName(c.label) }));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
/** The member-position twin of `fromRelationColumns` (#38): when the scope is empty (the dangling
|
|
323
|
+
* dot broke the FROM parse), read `FROM/JOIN name(.name)* [AS] [alias]` off the token stream and
|
|
324
|
+
* answer the columns of the ONE relation the qualifier matches — the alias when present, else the
|
|
325
|
+
* name's own trailing parts. No match (or several) answers [] — never a fabricated union. */
|
|
326
|
+
function qualifiedFallbackColumns(walkTokens, cfg, qualParts, schema, dialect) {
|
|
327
|
+
if (cfg.relationKeywordTokens.size === 0)
|
|
328
|
+
return [];
|
|
329
|
+
const b = resolveBehavior(dialect);
|
|
330
|
+
const toks = walkTokens.filter((t) => t.channel === Token.DEFAULT_CHANNEL);
|
|
331
|
+
const hits = [];
|
|
332
|
+
for (let i = 0; i + 1 < toks.length; i++) {
|
|
333
|
+
if (!cfg.relationKeywordTokens.has(toks[i].type))
|
|
334
|
+
continue;
|
|
335
|
+
let j = i + 1;
|
|
336
|
+
if (!toks[j] || !cfg.nameTokens.has(toks[j].type))
|
|
337
|
+
continue;
|
|
338
|
+
const parts = [toks[j].text];
|
|
339
|
+
j++;
|
|
340
|
+
while (toks[j]?.text === "." && toks[j + 1] && cfg.nameTokens.has(toks[j + 1].type)) {
|
|
341
|
+
parts.push(toks[j + 1].text);
|
|
342
|
+
j += 2;
|
|
343
|
+
}
|
|
344
|
+
let alias;
|
|
345
|
+
if (toks[j] && b.fold(toks[j].text) === "as" && toks[j + 1] && cfg.nameTokens.has(toks[j + 1].type))
|
|
346
|
+
j++;
|
|
347
|
+
if (toks[j] && cfg.nameTokens.has(toks[j].type))
|
|
348
|
+
alias = toks[j].text;
|
|
349
|
+
const matches = alias
|
|
350
|
+
? qualParts.length === 1 && b.fold(qualParts[0]) === b.fold(alias)
|
|
351
|
+
: qualParts.length <= parts.length &&
|
|
352
|
+
qualParts.every((p, k) => b.fold(p, "table") === b.fold(parts[parts.length - qualParts.length + k], "table"));
|
|
353
|
+
if (!matches)
|
|
354
|
+
continue;
|
|
355
|
+
const cols = schema.columnsFor(parts, dialect);
|
|
356
|
+
if (cols)
|
|
357
|
+
hits.push(cols.map((c) => ({ label: c.name, kind: "column", detail: c.type })));
|
|
358
|
+
}
|
|
359
|
+
return hits.length === 1 ? hits[0] : [];
|
|
360
|
+
}
|
|
215
361
|
/** The columns visible from the scope enclosing `offset` (a CELL-relative offset into `scopes`).
|
|
216
362
|
* Derived sources / CTEs expose their own output column names; base-table sources get their columns
|
|
217
363
|
* (and types) from the schema. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TemplateCall } from "../qualify/template-provider.js";
|
|
1
2
|
import type { TagNode } from "../minijinja/tag-ast.js";
|
|
2
3
|
/** Where the caret sits inside a jinja call tag. NEUTRAL, the callee is a bare string; the dbt
|
|
3
4
|
* meaning of the slot (ref arg0 = a model) is the consumer's to apply. */
|
|
@@ -6,6 +7,11 @@ export interface JinjaSlot {
|
|
|
6
7
|
callee: string;
|
|
7
8
|
/** Dotted package before the callee (`dbt_utils` in `dbt_utils.star(...)`). */
|
|
8
9
|
packageName?: string;
|
|
10
|
+
/** The WHOLE parsed call (issue #37): name, packageParts and every sibling arg's literal value
|
|
11
|
+
* (null where computed) — the same TemplateCall shape every provider method receives. A slot's
|
|
12
|
+
* candidates can depend on the other args (source('raw', '|')'s candidates are the tables OF
|
|
13
|
+
* raw), so the provider callback gets the call, not just the callee name. */
|
|
14
|
+
call: TemplateCall;
|
|
9
15
|
/** 0-based index of the positional arg the caret is in. The callee-name slot (caret still in the
|
|
10
16
|
* callee identifier, `{{ my_mac|`) is `-1`. */
|
|
11
17
|
argIndex: number;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// Reuses the parse: it reads the tags the document already produced, never re-parses.
|
|
19
19
|
// Total: returns undefined off any jinja completion slot; never throws.
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
|
+
import { callOf } from "../minijinja/apply-tags.js";
|
|
21
22
|
/**
|
|
22
23
|
* The jinja completion slot at `offset`, or undefined when the caret is not in a completable jinja
|
|
23
24
|
* position. `tags` is `parseTemplated(...).tags` (or `doc.templated.tags`); `text` is the document
|
|
@@ -82,7 +83,15 @@ function macroHit(c) {
|
|
|
82
83
|
}
|
|
83
84
|
/** The slot for a caret inside a resolved call: the callee name, or the positional argument. */
|
|
84
85
|
function slotFromCall(c, text, offset) {
|
|
85
|
-
|
|
86
|
+
// The whole call rides the slot (#37): callOf reads name + literal args off the source text,
|
|
87
|
+
// the same extraction apply-tags feeds the provider everywhere else.
|
|
88
|
+
const call = callOf({
|
|
89
|
+
name: c.name,
|
|
90
|
+
nameSpan: c.nameSpan,
|
|
91
|
+
args: c.args,
|
|
92
|
+
...(c.packageName !== undefined ? { packageName: c.packageName } : {}),
|
|
93
|
+
}, text);
|
|
94
|
+
const base = { callee: c.name, call, ...(c.packageName !== undefined ? { packageName: c.packageName } : {}) };
|
|
86
95
|
// Callee-name slot: the caret is still within (or right at the end of) the callee identifier,
|
|
87
96
|
// before the open paren, the user is typing the macro name itself.
|
|
88
97
|
if (offset <= c.nameSpan.end) {
|
|
@@ -122,7 +131,8 @@ function bareCalleeSlot(tags, text, offset) {
|
|
|
122
131
|
const m = /^\s*([A-Za-z_]\w*)$/.exec(before);
|
|
123
132
|
if (!m)
|
|
124
133
|
return undefined;
|
|
125
|
-
|
|
134
|
+
// No parsed call exists yet (`{{ re`) — the slot's call is the bare callee with no args.
|
|
135
|
+
return { callee: m[1], call: { name: m[1], args: [] }, argIndex: -1, prefix: m[1], incomplete: true };
|
|
126
136
|
}
|
|
127
137
|
/** Drop a single leading quote from a partial string arg (`'cu` -> `cu`) so the prefix is the value
|
|
128
138
|
* the consumer filters by. Leaves a non-string arg untouched. */
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
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
|
-
import { displayName, fold, foldTableName, matchesSourceKey } from "./fold.js";
|
|
4
|
+
import { displayName, fold, foldTableName, matchesSourceKey, DATABRICKS_NAME_CONFIG } from "./fold.js";
|
|
5
5
|
import { databricksLiteral, databricksParseType, DATABRICKS_FUNCTION_RETURNS } from "./infer.js";
|
|
6
6
|
export const databricksBehavior = {
|
|
7
7
|
fold,
|
|
8
8
|
displayName,
|
|
9
9
|
foldTableName,
|
|
10
10
|
matchesSourceKey,
|
|
11
|
+
nameConfig: DATABRICKS_NAME_CONFIG,
|
|
11
12
|
likeMatch: (pattern, value) => likePatternToRegExp(pattern).test(value),
|
|
12
13
|
literal: databricksLiteral,
|
|
13
14
|
parseType: databricksParseType,
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { type FoldRule, type IdentKind } from "../ident/fold.js";
|
|
2
|
+
import type { QualifiedNameConfig } from "../ir/qualified-name.js";
|
|
2
3
|
export declare const DATABRICKS_FOLD_RULE: FoldRule;
|
|
4
|
+
/** Unity Catalog's three-level namespace: catalog.schema.object
|
|
5
|
+
* (docs.databricks.com/en/data-governance/unity-catalog — "three-level namespace"). */
|
|
6
|
+
export declare const DATABRICKS_NAME_CONFIG: QualifiedNameConfig;
|
|
3
7
|
/** Fold an identifier to its Databricks identity key. */
|
|
4
8
|
export declare function fold(raw: string, kind?: IdentKind): string;
|
|
5
9
|
/** Presentation twin: strip delimiters, no case change. */
|
package/dist/databricks/fold.js
CHANGED
|
@@ -11,6 +11,12 @@ export const DATABRICKS_FOLD_RULE = {
|
|
|
11
11
|
unquoted: "lower",
|
|
12
12
|
quoted: "lower",
|
|
13
13
|
};
|
|
14
|
+
/** Unity Catalog's three-level namespace: catalog.schema.object
|
|
15
|
+
* (docs.databricks.com/en/data-governance/unity-catalog — "three-level namespace"). */
|
|
16
|
+
export const DATABRICKS_NAME_CONFIG = {
|
|
17
|
+
roles: ["catalog", "schema"],
|
|
18
|
+
rule: DATABRICKS_FOLD_RULE,
|
|
19
|
+
};
|
|
14
20
|
/** Fold an identifier to its Databricks identity key. */
|
|
15
21
|
export function fold(raw, kind = "other") {
|
|
16
22
|
return foldWith(DATABRICKS_FOLD_RULE, raw, kind);
|
package/dist/databricks/lower.js
CHANGED
|
@@ -3,6 +3,13 @@ import { ArithmeticBinaryContext, ArithmeticUnaryContext, CastByColonContext, Ca
|
|
|
3
3
|
import { keywordCategory, swallowedCategories, swallowedStatements } from "../ir/statement.js";
|
|
4
4
|
import { partSpansOf } from "../ir/part-span.js";
|
|
5
5
|
import { freezeIR } from "../ir/freeze.js";
|
|
6
|
+
import { qualifiedNameOf } from "../ir/qualified-name.js";
|
|
7
|
+
import { DATABRICKS_NAME_CONFIG } from "./fold.js";
|
|
8
|
+
/** The structured name for a table source's raw parts (issue #38) — role assignment + identity
|
|
9
|
+
* key + fqn happen HERE, at lowering, where the dialect's namespace shape is known. */
|
|
10
|
+
function relationOf(rawParts) {
|
|
11
|
+
return qualifiedNameOf(rawParts, DATABRICKS_NAME_CONFIG);
|
|
12
|
+
}
|
|
6
13
|
// ---------------------------------------------------------------------------
|
|
7
14
|
// CST navigation helpers
|
|
8
15
|
// ---------------------------------------------------------------------------
|
|
@@ -367,7 +374,7 @@ function lowerSparkPipeRhs(rhs) {
|
|
|
367
374
|
const join = directChildrenOfRule(rhs, P.RULE_joinRelation)[0];
|
|
368
375
|
if (join) {
|
|
369
376
|
const rel = directChildrenOfRule(join, P.RULE_relationPrimary)[0];
|
|
370
|
-
const source = rel ? buildSource(rel) : { kind: "table",
|
|
377
|
+
const source = rel ? buildSource(rel) : { kind: "table", relation: relationOf([]), cst: rhs };
|
|
371
378
|
const joinConditions = [];
|
|
372
379
|
const columns = [];
|
|
373
380
|
const crit = directChildrenOfRule(join, P.RULE_joinCriteria)[0];
|
|
@@ -491,7 +498,7 @@ function buildTableShorthand(queryPrimary) {
|
|
|
491
498
|
return {
|
|
492
499
|
kind: "select",
|
|
493
500
|
projections: [{ isStar: true, expr: star, cst: queryPrimary }],
|
|
494
|
-
from: [{ kind: "table", name, namePartSpans, cst: queryPrimary }],
|
|
501
|
+
from: [{ kind: "table", relation: relationOf(name), namePartSpans, cst: queryPrimary }],
|
|
495
502
|
columns: [],
|
|
496
503
|
aggregated: false,
|
|
497
504
|
cst: queryPrimary,
|
|
@@ -1438,7 +1445,7 @@ function buildSource(relationPrimary) {
|
|
|
1438
1445
|
const namePartSpans = partNodes.length ? partSpansOf(partNodes) : multipart ? partSpansOf([multipart]) : undefined;
|
|
1439
1446
|
return {
|
|
1440
1447
|
kind: "table",
|
|
1441
|
-
|
|
1448
|
+
relation: relationOf(parts),
|
|
1442
1449
|
namePartSpans,
|
|
1443
1450
|
alias,
|
|
1444
1451
|
aliasCst,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IdentKind } from "../ident/fold.js";
|
|
2
|
+
import type { QualifiedNameConfig } from "../ir/qualified-name.js";
|
|
2
3
|
import type { FnRule } from "../infer/functions.js";
|
|
3
4
|
import type { Type } from "../infer/types.js";
|
|
4
5
|
import type { Expr } from "../ir/ir.js";
|
|
@@ -8,6 +9,10 @@ export interface DialectBehavior {
|
|
|
8
9
|
displayName(raw: string): string;
|
|
9
10
|
foldTableName(parts: string[]): string[];
|
|
10
11
|
matchesSourceKey(key: string, rawPart: string): boolean;
|
|
12
|
+
/** The dialect's namespace shape + fold rule for building QualifiedNames outside lower()
|
|
13
|
+
* (shared layers synthesizing sources, apply-tags renaming templated ones). Same object the
|
|
14
|
+
* dialect's own lower() uses; declared in src/<dialect>/fold.ts (issue #38). */
|
|
15
|
+
nameConfig: QualifiedNameConfig;
|
|
11
16
|
likeMatch(pattern: string, name: string): boolean;
|
|
12
17
|
literal(text: string): Type;
|
|
13
18
|
parseType(text: string): Type;
|
package/dist/duckdb/behavior.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
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
|
-
import { displayName, fold, foldTableName, matchesSourceKey } from "./fold.js";
|
|
4
|
+
import { displayName, fold, foldTableName, matchesSourceKey, DUCKDB_NAME_CONFIG } from "./fold.js";
|
|
5
5
|
import { duckdbLiteral, duckdbParseType, DUCKDB_FUNCTION_RETURNS } from "./infer.js";
|
|
6
6
|
// DuckDB implicit coercion: a quoted constant is initially UNKNOWN and coerces to whatever the call
|
|
7
7
|
// needs (str->num), no bool<->num.
|
|
@@ -11,6 +11,7 @@ export const duckdbBehavior = {
|
|
|
11
11
|
displayName,
|
|
12
12
|
foldTableName,
|
|
13
13
|
matchesSourceKey,
|
|
14
|
+
nameConfig: DUCKDB_NAME_CONFIG,
|
|
14
15
|
likeMatch: (pattern, value) => likePatternToRegExp(pattern).test(value),
|
|
15
16
|
literal: duckdbLiteral,
|
|
16
17
|
parseType: duckdbParseType,
|
package/dist/duckdb/fold.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { type FoldRule, type IdentKind } from "../ident/fold.js";
|
|
2
|
+
import type { QualifiedNameConfig } from "../ir/qualified-name.js";
|
|
2
3
|
export declare const DUCKDB_FOLD_RULE: FoldRule;
|
|
4
|
+
/** catalog.schema.table — an attached database is a catalog
|
|
5
|
+
* (duckdb.org/docs/current/sql/statements/attach). */
|
|
6
|
+
export declare const DUCKDB_NAME_CONFIG: QualifiedNameConfig;
|
|
3
7
|
/** Fold an identifier to its DuckDB identity key. */
|
|
4
8
|
export declare function fold(raw: string, kind?: IdentKind): string;
|
|
5
9
|
/** Presentation twin: strip delimiters, no case change. */
|
package/dist/duckdb/fold.js
CHANGED
|
@@ -15,6 +15,12 @@ export const DUCKDB_FOLD_RULE = {
|
|
|
15
15
|
unquoted: "ascii-lower",
|
|
16
16
|
quoted: "ascii-lower",
|
|
17
17
|
};
|
|
18
|
+
/** catalog.schema.table — an attached database is a catalog
|
|
19
|
+
* (duckdb.org/docs/current/sql/statements/attach). */
|
|
20
|
+
export const DUCKDB_NAME_CONFIG = {
|
|
21
|
+
roles: ["catalog", "schema"],
|
|
22
|
+
rule: DUCKDB_FOLD_RULE,
|
|
23
|
+
};
|
|
18
24
|
/** Fold an identifier to its DuckDB identity key. */
|
|
19
25
|
export function fold(raw, kind = "other") {
|
|
20
26
|
return foldWith(DUCKDB_FOLD_RULE, raw, kind);
|