sqllens 1.10.0 → 1.10.1
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/document/document.d.ts +1 -1
- package/dist/document/document.js +24 -17
- package/dist/document/split.d.ts +2 -1
- package/dist/document/split.js +9 -1
- package/dist/minijinja/apply-tags.d.ts +6 -0
- package/dist/minijinja/apply-tags.js +6 -3
- package/dist/minijinja/parse.d.ts +2 -1
- package/dist/minijinja/parse.js +7 -3
- package/dist/template/engine.d.ts +15 -6
- package/package.json +1 -1
|
@@ -163,7 +163,7 @@ export declare class SqlDocument {
|
|
|
163
163
|
* `parse()`. It is the document's one cell when the placeholder splits into one statement (every
|
|
164
164
|
* single-statement model), and the carrier of the engine result (`templated`) that a multi-cell
|
|
165
165
|
* document's slices are built from. `r.tokens`/`r.diagnostics` are ALREADY document coordinates
|
|
166
|
-
* (the cell always starts at 0), so
|
|
166
|
+
* (the cell always starts at 0), so, unlike `buildCell`, nothing is shifted. Mirrors
|
|
167
167
|
* `buildCell`'s CachedCell/StatementCell shapes so every downstream consumer (analyze(), cellAt(),
|
|
168
168
|
* nodeAt()…) sees the same structure whether the document is plain or templated. Cached in the SAME
|
|
169
169
|
* cross-edit `_cellCache` as plain cells, under a prefixed key so a templated cell can never
|
|
@@ -182,7 +182,7 @@ export class SqlDocument {
|
|
|
182
182
|
cells.push(built.cell);
|
|
183
183
|
backing.push(built.cached);
|
|
184
184
|
}
|
|
185
|
-
templated = withCellCorrelation(r, backing);
|
|
185
|
+
templated = withCellCorrelation(r, cells, backing);
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
else {
|
|
@@ -282,7 +282,7 @@ export class SqlDocument {
|
|
|
282
282
|
* `parse()`. It is the document's one cell when the placeholder splits into one statement (every
|
|
283
283
|
* single-statement model), and the carrier of the engine result (`templated`) that a multi-cell
|
|
284
284
|
* document's slices are built from. `r.tokens`/`r.diagnostics` are ALREADY document coordinates
|
|
285
|
-
* (the cell always starts at 0), so
|
|
285
|
+
* (the cell always starts at 0), so, unlike `buildCell`, nothing is shifted. Mirrors
|
|
286
286
|
* `buildCell`'s CachedCell/StatementCell shapes so every downstream consumer (analyze(), cellAt(),
|
|
287
287
|
* nodeAt()…) sees the same structure whether the document is plain or templated. Cached in the SAME
|
|
288
288
|
* cross-edit `_cellCache` as plain cells, under a prefixed key so a templated cell can never
|
|
@@ -354,15 +354,18 @@ export class SqlDocument {
|
|
|
354
354
|
category: c.sql.ast.statement ?? "other",
|
|
355
355
|
ast: c.sql.ast,
|
|
356
356
|
cst: c.sql.cst,
|
|
357
|
-
// Resolve scopes from the already-lowered (marker-carrying) ast
|
|
357
|
+
// Resolve scopes from the already-lowered (marker-carrying) ast: never re-parse.
|
|
358
358
|
scopes: toScopes(c.sql.ast, { dialect: this.dialect }),
|
|
359
359
|
tokens: c.sql.tokens,
|
|
360
360
|
errors: c.sql.errors,
|
|
361
361
|
diagnostics: c.sql.diagnostics,
|
|
362
362
|
analysis: new WeakMap(),
|
|
363
|
-
correlation: {
|
|
363
|
+
correlation: {
|
|
364
|
+
tagStartOf: new Map(c.links.map((l) => [l.node, l.tagStart])),
|
|
365
|
+
primaryAt: new Map(c.links.filter((l) => l.primary).map((l) => [l.tagStart, l.node])),
|
|
366
|
+
},
|
|
364
367
|
};
|
|
365
|
-
// Cache only the FIRST product for a key (see buildCell
|
|
368
|
+
// Cache only the FIRST product for a key (see buildCell: duplicates stay uncached).
|
|
366
369
|
if (this._cellCache.get(key) === undefined)
|
|
367
370
|
this._cellCache.set(key, cached);
|
|
368
371
|
}
|
|
@@ -920,25 +923,29 @@ function setResolutionKey(r, text) {
|
|
|
920
923
|
return parts.join("\n");
|
|
921
924
|
}
|
|
922
925
|
/** The `templated` facade of a MULTI-cell templated document: the whole-text engine result with
|
|
923
|
-
* `tagOf`/`nodeOf` answering from the CELLS' own correlations
|
|
924
|
-
* through `statements`/`cellAt`/`nodeAt`
|
|
925
|
-
* document does not expose (its `ast` is the compound facade).
|
|
926
|
-
|
|
926
|
+
* `tagOf`/`nodeOf` answering from the CELLS' own correlations (the per-statement IR consumers reach
|
|
927
|
+
* through `statements`/`cellAt`/`nodeAt`), never from the whole-text parse, whose nodes a multi-cell
|
|
928
|
+
* document does not expose (its `ast` is the compound facade). A cell's join names its tag by
|
|
929
|
+
* cell-relative start, so the answer is always one of THIS parse's TagNodes, whichever parse the
|
|
930
|
+
* cached cell was built under. Everything else is the engine's. */
|
|
931
|
+
function withCellCorrelation(r, cells, backing) {
|
|
932
|
+
const tagAt = new Map(r.tags.map((t) => [t.tagSpan.start, t]));
|
|
927
933
|
return {
|
|
928
934
|
...r,
|
|
929
935
|
tagOf: (node) => {
|
|
930
|
-
for (
|
|
931
|
-
const
|
|
932
|
-
if (
|
|
933
|
-
return
|
|
936
|
+
for (let i = 0; i < backing.length; i++) {
|
|
937
|
+
const rel = backing[i].correlation?.tagStartOf.get(node);
|
|
938
|
+
if (rel !== undefined)
|
|
939
|
+
return tagAt.get(cells[i].span.start + rel);
|
|
934
940
|
}
|
|
935
941
|
return undefined;
|
|
936
942
|
},
|
|
937
943
|
nodeOf: (tag) => {
|
|
938
|
-
for (
|
|
939
|
-
const
|
|
940
|
-
if (
|
|
941
|
-
|
|
944
|
+
for (let i = 0; i < backing.length; i++) {
|
|
945
|
+
const span = cells[i].span;
|
|
946
|
+
if (tag.tagSpan.start < span.start || tag.tagSpan.start >= span.end)
|
|
947
|
+
continue;
|
|
948
|
+
return backing[i].correlation?.primaryAt.get(tag.tagSpan.start - span.start);
|
|
942
949
|
}
|
|
943
950
|
return undefined;
|
|
944
951
|
},
|
package/dist/document/split.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ import type { Dialect } from "../dialect.js";
|
|
|
2
2
|
export interface StatementCellSpan {
|
|
3
3
|
/** doc offset, inclusive — cell text includes leading trivia. */
|
|
4
4
|
start: number;
|
|
5
|
-
/** doc offset, exclusive — includes the trailing separator (`;` / GO line)
|
|
5
|
+
/** doc offset, exclusive — includes the trailing separator (`;` / GO line); the document's last
|
|
6
|
+
* cell also includes whatever trivia follows its separator, up to `text.length`. */
|
|
6
7
|
end: number;
|
|
7
8
|
}
|
|
8
9
|
/**
|
package/dist/document/split.js
CHANGED
|
@@ -26,7 +26,10 @@ const NON_OPENER_END_SUFFIXES = new Set(["IF", "WHILE", "FOR", "LOOP", "REPEAT"]
|
|
|
26
26
|
function wholeDoc(text) {
|
|
27
27
|
return [{ start: 0, end: text.length }];
|
|
28
28
|
}
|
|
29
|
-
/** Every offset in `text` where a top-level separator ends (exclusive), in ascending order.
|
|
29
|
+
/** Every offset in `text` where a top-level separator ends (exclusive), in ascending order. A
|
|
30
|
+
* separator that no channel-0 token follows (only whitespace, comments, the final newline) is not
|
|
31
|
+
* a split end: the trivia after it belongs to the cell it terminates, so a single terminated
|
|
32
|
+
* statement is one cell rather than a statement plus a token-less tail cell. */
|
|
30
33
|
function findSplitEnds(text, tokens, dialect) {
|
|
31
34
|
const channel0 = tokens.filter((t) => t.channel === 0);
|
|
32
35
|
const ends = [];
|
|
@@ -80,6 +83,11 @@ function findSplitEnds(text, tokens, dialect) {
|
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
}
|
|
86
|
+
// Tokens are in source order, so the last channel-0 token decides whether anything real follows
|
|
87
|
+
// the last separator.
|
|
88
|
+
const lastReal = channel0[channel0.length - 1];
|
|
89
|
+
if (ends.length > 0 && (lastReal === undefined || lastReal.start < ends[ends.length - 1]))
|
|
90
|
+
ends.pop();
|
|
83
91
|
return ends;
|
|
84
92
|
}
|
|
85
93
|
/** Turn ascending split-end offsets into contiguous cells tiling `[0, text.length)`. A doc
|
|
@@ -15,6 +15,12 @@ export interface TagCorrelation {
|
|
|
15
15
|
byNode: WeakMap<object, TagNode>;
|
|
16
16
|
/** TagNode → the IR node it became (undefined-by-absence for tags with no IR presence). */
|
|
17
17
|
byTag: Map<TagNode, object>;
|
|
18
|
+
/** Every attached (node, tag) pair in attach order: the enumerable form of `byNode`, for a
|
|
19
|
+
* caller that must re-key the join to another parse's TagNodes (a cached statement cell). */
|
|
20
|
+
links: {
|
|
21
|
+
node: object;
|
|
22
|
+
tag: TagNode;
|
|
23
|
+
}[];
|
|
18
24
|
}
|
|
19
25
|
/**
|
|
20
26
|
* Rewrite templated FROM/JOIN sources in `ast` to carry their provider-resolved name (when the
|
|
@@ -39,6 +39,7 @@ const ZERO_BASE = { line: 0, column: 0, offset: 0 };
|
|
|
39
39
|
* of which one this walk visits first. */
|
|
40
40
|
function attach(ctx, node, tag) {
|
|
41
41
|
ctx.byNode.set(node, tag);
|
|
42
|
+
ctx.links.push({ node, tag });
|
|
42
43
|
const existing = ctx.byTag.get(tag);
|
|
43
44
|
if (existing?.kind !== "column")
|
|
44
45
|
ctx.byTag.set(tag, node);
|
|
@@ -59,13 +60,14 @@ function attach(ctx, node, tag) {
|
|
|
59
60
|
export function applyTemplateTags(ast, tags, text, provider, base = ZERO_BASE) {
|
|
60
61
|
const byNode = new WeakMap();
|
|
61
62
|
const byTag = new Map();
|
|
63
|
+
const links = [];
|
|
62
64
|
try {
|
|
63
65
|
// config is a no-output tag (whitespace-filled), so it can never yield a table
|
|
64
66
|
// source and stays out of the correlation set even though ExprTag admits it.
|
|
65
67
|
// An incomplete/mid-typing call (`{{ ref('cu`) is NOT a resolved source, so skip it.
|
|
66
68
|
const relTags = tags.filter((t) => (t.kind === "call" && !t.incomplete) || t.kind === "other");
|
|
67
69
|
if (relTags.length === 0)
|
|
68
|
-
return { ast, byNode, byTag };
|
|
70
|
+
return { ast, byNode, byTag, links };
|
|
69
71
|
const nameConfig = ast.dialect !== undefined ? resolveBehavior(ast.dialect).nameConfig : undefined;
|
|
70
72
|
const ctx = {
|
|
71
73
|
relTags,
|
|
@@ -75,6 +77,7 @@ export function applyTemplateTags(ast, tags, text, provider, base = ZERO_BASE) {
|
|
|
75
77
|
base,
|
|
76
78
|
byNode,
|
|
77
79
|
byTag,
|
|
80
|
+
links,
|
|
78
81
|
...(nameConfig ? { nameConfig } : {}),
|
|
79
82
|
};
|
|
80
83
|
const next = transformQuery(ast, ctx);
|
|
@@ -82,11 +85,11 @@ export function applyTemplateTags(ast, tags, text, provider, base = ZERO_BASE) {
|
|
|
82
85
|
// fill gets a `template` marker (span + provider key), so inference resolves it
|
|
83
86
|
// through the provider and qualify never checks the placeholder as a real column.
|
|
84
87
|
const marked = markTemplateExprs(next, ctx);
|
|
85
|
-
return { ast: marked === ast ? ast : freezeIR(marked), byNode, byTag };
|
|
88
|
+
return { ast: marked === ast ? ast : freezeIR(marked), byNode, byTag, links };
|
|
86
89
|
}
|
|
87
90
|
catch (e) {
|
|
88
91
|
debugRethrow(e);
|
|
89
|
-
return { ast, byNode: new WeakMap(), byTag: new Map() };
|
|
92
|
+
return { ast, byNode: new WeakMap(), byTag: new Map(), links: [] };
|
|
90
93
|
}
|
|
91
94
|
}
|
|
92
95
|
/** The TemplateExprInfo for a scalar-slot tag: its span + (when an identity is extractable)
|
|
@@ -35,7 +35,8 @@ export declare function tokenizeTemplated(text: string, dialect: Dialect, opts?:
|
|
|
35
35
|
* correlated onto it by DOCUMENT offset (a node's cell offset plus the cell's start). The cell's
|
|
36
36
|
* sources then carry their provider-resolved names and `template` markers exactly as the
|
|
37
37
|
* whole-text parse's do, never the fill; a marker's `span` is rebased to cell coordinates like
|
|
38
|
-
* every other span in the cell IR,
|
|
38
|
+
* every other span in the cell IR, and the tag↔node join is returned as cell-relative `links`
|
|
39
|
+
* so a cached cell re-keys it to whatever parse it is later reused under.
|
|
39
40
|
* Total: `applyTemplateTags` leaves the plain parse in place on any internal surprise.
|
|
40
41
|
*/
|
|
41
42
|
export declare function parseTemplatedCell(whole: TemplatedParseResult, span: {
|
package/dist/minijinja/parse.js
CHANGED
|
@@ -658,7 +658,8 @@ export function tokenizeTemplated(text, dialect, opts) {
|
|
|
658
658
|
* correlated onto it by DOCUMENT offset (a node's cell offset plus the cell's start). The cell's
|
|
659
659
|
* sources then carry their provider-resolved names and `template` markers exactly as the
|
|
660
660
|
* whole-text parse's do, never the fill; a marker's `span` is rebased to cell coordinates like
|
|
661
|
-
* every other span in the cell IR,
|
|
661
|
+
* every other span in the cell IR, and the tag↔node join is returned as cell-relative `links`
|
|
662
|
+
* so a cached cell re-keys it to whatever parse it is later reused under.
|
|
662
663
|
* Total: `applyTemplateTags` leaves the plain parse in place on any internal surprise.
|
|
663
664
|
*/
|
|
664
665
|
export function parseTemplatedCell(whole, span, text, dialect, opts) {
|
|
@@ -667,8 +668,11 @@ export function parseTemplatedCell(whole, span, text, dialect, opts) {
|
|
|
667
668
|
const correlation = applyTemplateTags(sql.ast, whole.tags, text, provider, cellBaseOf(text, span.start));
|
|
668
669
|
return {
|
|
669
670
|
sql: { ...sql, ast: correlation.ast },
|
|
670
|
-
|
|
671
|
-
|
|
671
|
+
links: correlation.links.map(({ node, tag }) => ({
|
|
672
|
+
node,
|
|
673
|
+
tagStart: tag.tagSpan.start - span.start,
|
|
674
|
+
primary: correlation.byTag.get(tag) === node,
|
|
675
|
+
})),
|
|
672
676
|
};
|
|
673
677
|
}
|
|
674
678
|
/** The cell start's 0-based line / column / char offset in `text` (`\n` is the line break, the
|
|
@@ -88,6 +88,17 @@ export interface TemplatedParseResult {
|
|
|
88
88
|
* scrubber widened to it. Empty array when none. */
|
|
89
89
|
diagnosticsOf(tag: TagNode): SyntaxDiagnostic[];
|
|
90
90
|
}
|
|
91
|
+
/** One template-marked node of a statement cell's IR and the tag it came from, the tag named by
|
|
92
|
+
* its CELL-relative start offset rather than by object: a cell is cached across edits and reused
|
|
93
|
+
* under a later parse whose TagNodes are fresh objects, so the join is re-keyed to that parse's
|
|
94
|
+
* tag at the same offset. `primary` marks the tag's one answer (a scalar-slot tag marks both a
|
|
95
|
+
* column expression and its column-ref record; the expression is primary). */
|
|
96
|
+
export interface TemplatedCellLink {
|
|
97
|
+
node: object;
|
|
98
|
+
/** `tag.tagSpan.start - cell.span.start`. */
|
|
99
|
+
tagStart: number;
|
|
100
|
+
primary: boolean;
|
|
101
|
+
}
|
|
91
102
|
/** One statement CELL of a templated document (`TemplateEngine.parseCell`): the plain per-dialect
|
|
92
103
|
* parse of the cell's placeholder slice, in CELL-relative coordinates like a plain document's
|
|
93
104
|
* cells, with the whole-document tags correlated onto it (provider-resolved source names,
|
|
@@ -96,11 +107,9 @@ export interface TemplatedCellResult {
|
|
|
96
107
|
/** The cell's SQL parse over its placeholder slice (ast / cst / tokens / errors / diagnostics),
|
|
97
108
|
* every span cell-relative. A `template` marker's span is cell-relative too. */
|
|
98
109
|
sql: ParseResultIR;
|
|
99
|
-
/** The
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
* another cell or has no IR presence. */
|
|
103
|
-
nodeOf(tag: TagNode): object | undefined;
|
|
110
|
+
/** The tag↔node join over THIS cell's IR, position- and identity-independent (see
|
|
111
|
+
* `TemplatedCellLink`). Empty when nothing correlates. */
|
|
112
|
+
links: TemplatedCellLink[];
|
|
104
113
|
}
|
|
105
114
|
/** A template engine: the syntax front end for one templating language over
|
|
106
115
|
* SQL. `parse` must satisfy the engine contract the conformance suite
|
|
@@ -113,7 +122,7 @@ export interface TemplateEngine {
|
|
|
113
122
|
parse(text: string, dialect: Dialect, opts?: TemplatedParseOptions): TemplatedParseResult;
|
|
114
123
|
/** Optional: coherent per-branch variant enumeration, for engines with control-flow arms. */
|
|
115
124
|
variants?(text: string, dialect: Dialect): TemplateVariant[];
|
|
116
|
-
/** Optional: the products of ONE statement cell of a templated document
|
|
125
|
+
/** Optional: the products of ONE statement cell of a templated document, the plain parse of
|
|
117
126
|
* `whole.placeholder`'s slice `[span.start, span.end)` (cell-relative) with `whole`'s tags
|
|
118
127
|
* correlated onto it. `whole` is this engine's own `parse` result for the full `text`, `span`
|
|
119
128
|
* a `splitStatements` span over that placeholder. An engine without it keeps the templated
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sqllens",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.1",
|
|
4
4
|
"description": "A TypeScript SQL parser and static analyzer: parse, resolve names, infer types, and trace column lineage across many SQL dialects (Databricks, T-SQL, Snowflake, BigQuery, Redshift, PostgreSQL, DuckDB, Trino, SQLite, MySQL).",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|