sqllens 1.10.0 → 1.11.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 +5 -1
- package/dist/api.d.ts +1 -0
- package/dist/api.js +3 -0
- package/dist/document/document.d.ts +1 -1
- package/dist/document/document.js +26 -18
- package/dist/document/spans.d.ts +14 -0
- package/dist/document/spans.js +34 -0
- package/dist/document/split.d.ts +13 -2
- package/dist/document/split.js +76 -28
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/minijinja/apply-tags.d.ts +6 -0
- package/dist/minijinja/apply-tags.js +6 -3
- package/dist/minijinja/engine.js +2 -1
- package/dist/minijinja/parse.d.ts +6 -1
- package/dist/minijinja/parse.js +19 -3
- package/dist/template/engine.d.ts +19 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -367,7 +367,11 @@ that run on incomplete, mid-edit text. They never need a clean parse:
|
|
|
367
367
|
- `SqlDocument` is a persistent, immutable, position-addressable per-file model.
|
|
368
368
|
It runs `parse → resolveScopes` once (plus lazy `analyze(schema)`), caches the
|
|
369
369
|
result, and answers `tokenAt` / `nodeAt`. An edit yields a new document; an
|
|
370
|
-
O(log n) `LineIndex` maps positions to offsets.
|
|
370
|
+
O(log n) `LineIndex` maps positions to offsets. Its `statements` are the
|
|
371
|
+
per-statement cells; each cell's `span` carries the separator token that ends
|
|
372
|
+
it (`;`, or the `GO` word), so the statement's own text ends where that starts.
|
|
373
|
+
- `statementSpans(text, dialect, { templating? })`: the same cell spans without
|
|
374
|
+
the per-cell parses, for code lenses and run-at-cursor ranges on every change.
|
|
371
375
|
- `completeAt(doc, offset, schema?)`: scope-aware completion (keywords, columns,
|
|
372
376
|
tables, functions) from an ATN (Augmented Transition Network, the grammar's
|
|
373
377
|
state-machine form) candidate walk over the grammar, our own, with no third-party
|
package/dist/api.d.ts
CHANGED
|
@@ -116,6 +116,7 @@ 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 { statementSpans } from "./document/spans.js";
|
|
119
120
|
export { complete, completeAt, type Completion, type CompletionResult, type ReplaceRange, type CompleteOptions, type CandidateIdentity, type CandidateDecoration, type DecorateCandidate, } from "./completion/complete.js";
|
|
120
121
|
export { jinjaSlotAt, type JinjaSlot } from "./completion/jinja-slot.js";
|
|
121
122
|
export { signatureAt, type SignatureHelpInfo, type SignatureLabel } from "./signature/signature.js";
|
package/dist/api.js
CHANGED
|
@@ -202,6 +202,9 @@ export { tokenize } from "./token/tokenize.js";
|
|
|
202
202
|
// these at call time, never at module-eval time.
|
|
203
203
|
export { SqlDocument, } from "./document/document.js";
|
|
204
204
|
export { LineIndex } from "./document/line-index.js";
|
|
205
|
+
// The cell spans alone (plain or templated), for a consumer that needs statement ranges without
|
|
206
|
+
// the per-cell parses; identical to the spans the document's cells carry.
|
|
207
|
+
export { statementSpans } from "./document/spans.js";
|
|
205
208
|
// Scope-aware completion over a SqlDocument — the broken-input editor feature (keywords + schema
|
|
206
209
|
// tables/columns + function names at the caret). Total: never throws.
|
|
207
210
|
export { complete, completeAt, } from "./completion/complete.js";
|
|
@@ -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
|
|
@@ -168,7 +168,8 @@ export class SqlDocument {
|
|
|
168
168
|
const r = whole.cached.templated;
|
|
169
169
|
const spans = opts.templating.parseCell ? splitStatements(r.placeholder, dialect) : [whole.cell.span];
|
|
170
170
|
if (spans.length === 1) {
|
|
171
|
-
|
|
171
|
+
// The whole-text cell, under the split's own span so it carries its separator.
|
|
172
|
+
cells = [Object.freeze({ ...whole.cell, span: spans[0] })];
|
|
172
173
|
backing = [whole.cached];
|
|
173
174
|
templated = r;
|
|
174
175
|
}
|
|
@@ -182,7 +183,7 @@ export class SqlDocument {
|
|
|
182
183
|
cells.push(built.cell);
|
|
183
184
|
backing.push(built.cached);
|
|
184
185
|
}
|
|
185
|
-
templated = withCellCorrelation(r, backing);
|
|
186
|
+
templated = withCellCorrelation(r, cells, backing);
|
|
186
187
|
}
|
|
187
188
|
}
|
|
188
189
|
else {
|
|
@@ -282,7 +283,7 @@ export class SqlDocument {
|
|
|
282
283
|
* `parse()`. It is the document's one cell when the placeholder splits into one statement (every
|
|
283
284
|
* single-statement model), and the carrier of the engine result (`templated`) that a multi-cell
|
|
284
285
|
* document's slices are built from. `r.tokens`/`r.diagnostics` are ALREADY document coordinates
|
|
285
|
-
* (the cell always starts at 0), so
|
|
286
|
+
* (the cell always starts at 0), so, unlike `buildCell`, nothing is shifted. Mirrors
|
|
286
287
|
* `buildCell`'s CachedCell/StatementCell shapes so every downstream consumer (analyze(), cellAt(),
|
|
287
288
|
* nodeAt()…) sees the same structure whether the document is plain or templated. Cached in the SAME
|
|
288
289
|
* cross-edit `_cellCache` as plain cells, under a prefixed key so a templated cell can never
|
|
@@ -354,15 +355,18 @@ export class SqlDocument {
|
|
|
354
355
|
category: c.sql.ast.statement ?? "other",
|
|
355
356
|
ast: c.sql.ast,
|
|
356
357
|
cst: c.sql.cst,
|
|
357
|
-
// Resolve scopes from the already-lowered (marker-carrying) ast
|
|
358
|
+
// Resolve scopes from the already-lowered (marker-carrying) ast: never re-parse.
|
|
358
359
|
scopes: toScopes(c.sql.ast, { dialect: this.dialect }),
|
|
359
360
|
tokens: c.sql.tokens,
|
|
360
361
|
errors: c.sql.errors,
|
|
361
362
|
diagnostics: c.sql.diagnostics,
|
|
362
363
|
analysis: new WeakMap(),
|
|
363
|
-
correlation: {
|
|
364
|
+
correlation: {
|
|
365
|
+
tagStartOf: new Map(c.links.map((l) => [l.node, l.tagStart])),
|
|
366
|
+
primaryAt: new Map(c.links.filter((l) => l.primary).map((l) => [l.tagStart, l.node])),
|
|
367
|
+
},
|
|
364
368
|
};
|
|
365
|
-
// Cache only the FIRST product for a key (see buildCell
|
|
369
|
+
// Cache only the FIRST product for a key (see buildCell: duplicates stay uncached).
|
|
366
370
|
if (this._cellCache.get(key) === undefined)
|
|
367
371
|
this._cellCache.set(key, cached);
|
|
368
372
|
}
|
|
@@ -920,25 +924,29 @@ function setResolutionKey(r, text) {
|
|
|
920
924
|
return parts.join("\n");
|
|
921
925
|
}
|
|
922
926
|
/** 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
|
-
|
|
927
|
+
* `tagOf`/`nodeOf` answering from the CELLS' own correlations (the per-statement IR consumers reach
|
|
928
|
+
* through `statements`/`cellAt`/`nodeAt`), never from the whole-text parse, whose nodes a multi-cell
|
|
929
|
+
* document does not expose (its `ast` is the compound facade). A cell's join names its tag by
|
|
930
|
+
* cell-relative start, so the answer is always one of THIS parse's TagNodes, whichever parse the
|
|
931
|
+
* cached cell was built under. Everything else is the engine's. */
|
|
932
|
+
function withCellCorrelation(r, cells, backing) {
|
|
933
|
+
const tagAt = new Map(r.tags.map((t) => [t.tagSpan.start, t]));
|
|
927
934
|
return {
|
|
928
935
|
...r,
|
|
929
936
|
tagOf: (node) => {
|
|
930
|
-
for (
|
|
931
|
-
const
|
|
932
|
-
if (
|
|
933
|
-
return
|
|
937
|
+
for (let i = 0; i < backing.length; i++) {
|
|
938
|
+
const rel = backing[i].correlation?.tagStartOf.get(node);
|
|
939
|
+
if (rel !== undefined)
|
|
940
|
+
return tagAt.get(cells[i].span.start + rel);
|
|
934
941
|
}
|
|
935
942
|
return undefined;
|
|
936
943
|
},
|
|
937
944
|
nodeOf: (tag) => {
|
|
938
|
-
for (
|
|
939
|
-
const
|
|
940
|
-
if (
|
|
941
|
-
|
|
945
|
+
for (let i = 0; i < backing.length; i++) {
|
|
946
|
+
const span = cells[i].span;
|
|
947
|
+
if (tag.tagSpan.start < span.start || tag.tagSpan.start >= span.end)
|
|
948
|
+
continue;
|
|
949
|
+
return backing[i].correlation?.primaryAt.get(tag.tagSpan.start - span.start);
|
|
942
950
|
}
|
|
943
951
|
return undefined;
|
|
944
952
|
},
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Dialect } from "../dialect.js";
|
|
2
|
+
import type { TemplateProvider } from "../qualify/template-provider.js";
|
|
3
|
+
import type { TemplateEngine } from "../template/engine.js";
|
|
4
|
+
import { type StatementCellSpan } from "./split.js";
|
|
5
|
+
/**
|
|
6
|
+
* The statement cell spans `SqlDocument.create(text, dialect, opts)` would produce, without the
|
|
7
|
+
* per-cell parses. Plain text splits directly; with `templating` the engine's placeholder is
|
|
8
|
+
* split (its `placeholder` hook when it has one, else its full `parse`), and an engine without
|
|
9
|
+
* `parseCell` answers one whole-text span, exactly as the document door does. Total: never throws.
|
|
10
|
+
*/
|
|
11
|
+
export declare function statementSpans(text: string, dialect: Dialect, opts?: {
|
|
12
|
+
templating?: TemplateEngine;
|
|
13
|
+
provider?: TemplateProvider;
|
|
14
|
+
}): StatementCellSpan[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// statementSpans() — the statement cell spans of a text without building the
|
|
3
|
+
// document. The SqlDocument constructor derives its cells from exactly this
|
|
4
|
+
// split (the plain text, or the engine's placeholder for a templated one), so
|
|
5
|
+
// the spans answered here are the spans `doc.statements[i].span` would carry;
|
|
6
|
+
// a consumer that only needs "where do the statements start and end" (code
|
|
7
|
+
// lenses, run-at-cursor ranges) pays the lex and the split, not the parses.
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
import { debugRethrow } from "../debug.js";
|
|
10
|
+
import { splitStatements } from "./split.js";
|
|
11
|
+
/**
|
|
12
|
+
* The statement cell spans `SqlDocument.create(text, dialect, opts)` would produce, without the
|
|
13
|
+
* per-cell parses. Plain text splits directly; with `templating` the engine's placeholder is
|
|
14
|
+
* split (its `placeholder` hook when it has one, else its full `parse`), and an engine without
|
|
15
|
+
* `parseCell` answers one whole-text span, exactly as the document door does. Total: never throws.
|
|
16
|
+
*/
|
|
17
|
+
export function statementSpans(text, dialect, opts = {}) {
|
|
18
|
+
const engine = opts.templating;
|
|
19
|
+
if (!engine)
|
|
20
|
+
return splitStatements(text, dialect);
|
|
21
|
+
if (!engine.parseCell)
|
|
22
|
+
return [{ start: 0, end: text.length }];
|
|
23
|
+
let placeholder;
|
|
24
|
+
try {
|
|
25
|
+
placeholder = engine.placeholder
|
|
26
|
+
? engine.placeholder(text, dialect, { provider: opts.provider })
|
|
27
|
+
: engine.parse(text, dialect, { provider: opts.provider }).placeholder;
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
debugRethrow(e);
|
|
31
|
+
placeholder = text;
|
|
32
|
+
}
|
|
33
|
+
return splitStatements(placeholder, dialect);
|
|
34
|
+
}
|
package/dist/document/split.d.ts
CHANGED
|
@@ -2,15 +2,26 @@ 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;
|
|
8
|
+
/** The separator token that terminates this cell, as doc offsets (`start` inclusive, `end`
|
|
9
|
+
* exclusive): the `;`, or the `GO` word itself (not its line). Absent when the cell has no
|
|
10
|
+
* separator (an unterminated final statement, a separator-free document, the whole-document
|
|
11
|
+
* fallback). The statement's own text ends where this starts, before any trailing trivia. */
|
|
12
|
+
separator?: {
|
|
13
|
+
start: number;
|
|
14
|
+
end: number;
|
|
15
|
+
};
|
|
7
16
|
}
|
|
8
17
|
/**
|
|
9
18
|
* Split `text` into top-level statement cells using `tokenize(text, dialect)`.
|
|
10
19
|
* Total: never throws. Splits at channel-0 `;` at compound depth 0 (BEGIN/CASE
|
|
11
20
|
* increment, END decrements, floor 0; a T-SQL `BEGIN TRAN`/`TRANSACTION`/
|
|
12
21
|
* `DISTRIBUTED` does not open a depth level) plus, for T-SQL, a `GO` batch
|
|
13
|
-
* separator alone on its line
|
|
22
|
+
* separator alone on its line; text ending inside an open level splits at every
|
|
23
|
+
* separator from the unclosed opener on (see `findSplitEnds`). Each cell carries
|
|
24
|
+
* the separator token that ends it. Returns the whole doc as one cell when
|
|
14
25
|
* splitting is unsafe (the tiling invariant fails) or pointless (no separators).
|
|
15
26
|
*/
|
|
16
27
|
export declare function splitStatements(text: string, dialect: Dialect): StatementCellSpan[];
|
package/dist/document/split.js
CHANGED
|
@@ -26,11 +26,43 @@ 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
|
-
/**
|
|
29
|
+
/** The split point a `;` or a T-SQL `GO` at `channel0[i]` makes, ignoring depth, or undefined when
|
|
30
|
+
* the token is neither. A GO batch separator must sit alone on its line among channel-0 tokens
|
|
31
|
+
* (otherwise it is an identifier/alias use of the word); its cell end is the end of that line. */
|
|
32
|
+
function separatorAt(text, channel0, i, dialect) {
|
|
33
|
+
const t = channel0[i];
|
|
34
|
+
const separator = { start: t.start, end: t.stop + 1 };
|
|
35
|
+
if (t.text === ";")
|
|
36
|
+
return { end: t.stop + 1, separator };
|
|
37
|
+
if (dialect === "tsql" && t.text.toUpperCase() === "GO") {
|
|
38
|
+
const prev = channel0[i - 1];
|
|
39
|
+
const next = channel0[i + 1];
|
|
40
|
+
const alone = (!prev || prev.line !== t.line) && (!next || next.line !== t.line);
|
|
41
|
+
if (!alone)
|
|
42
|
+
return undefined;
|
|
43
|
+
const nl = text.indexOf("\n", t.stop + 1);
|
|
44
|
+
return { end: nl === -1 ? text.length : nl + 1, separator };
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
/** Every top-level split point in `text`, in ascending order, plus `tail`: the separator of a
|
|
49
|
+
* terminated final statement that nothing real follows (only whitespace, comments, the final
|
|
50
|
+
* newline). That separator is not a split end, the trivia after it belongs to the cell it
|
|
51
|
+
* terminates, so a single terminated statement is one cell rather than a statement plus a
|
|
52
|
+
* token-less tail cell; `tail` lets that cell still carry its separator.
|
|
53
|
+
*
|
|
54
|
+
* Depth: `BEGIN`/`CASE` open a level, `END` closes one, and only a separator at depth 0 splits.
|
|
55
|
+
* When the text ends INSIDE an open level (an unclosed CASE mid-typing, jinja arms that leave
|
|
56
|
+
* the placeholder with more openers than closers), the walk cannot know where that block ends,
|
|
57
|
+
* and one cell silently spanning several statements is the worse failure (a "run statement at
|
|
58
|
+
* cursor" would run them all): the split points before the outermost unclosed opener stand, and
|
|
59
|
+
* from that opener on every separator splits regardless of depth. Balanced text is unaffected. */
|
|
30
60
|
function findSplitEnds(text, tokens, dialect) {
|
|
31
61
|
const channel0 = tokens.filter((t) => t.channel === 0);
|
|
32
|
-
|
|
62
|
+
let ends = [];
|
|
33
63
|
let depth = 0;
|
|
64
|
+
/** The outermost currently-open level: where it opened and how many split points preceded it. */
|
|
65
|
+
let opener;
|
|
34
66
|
for (let i = 0; i < channel0.length; i++) {
|
|
35
67
|
const t = channel0[i];
|
|
36
68
|
const upper = t.text.toUpperCase();
|
|
@@ -38,10 +70,15 @@ function findSplitEnds(text, tokens, dialect) {
|
|
|
38
70
|
// `BEGIN TRAN`/`TRANSACTION`/`DISTRIBUTED` (T-SQL) starts a transaction, not a
|
|
39
71
|
// scripting compound — it has no matching END, so it must not open a depth level.
|
|
40
72
|
const next = channel0[i + 1];
|
|
41
|
-
if (!next || !TRAN_WORDS.has(next.text.toUpperCase()))
|
|
73
|
+
if (!next || !TRAN_WORDS.has(next.text.toUpperCase())) {
|
|
74
|
+
if (depth === 0)
|
|
75
|
+
opener = { index: i, endsBefore: ends.length };
|
|
42
76
|
depth++;
|
|
77
|
+
}
|
|
43
78
|
}
|
|
44
79
|
else if (upper === "CASE") {
|
|
80
|
+
if (depth === 0)
|
|
81
|
+
opener = { index: i, endsBefore: ends.length };
|
|
45
82
|
depth++;
|
|
46
83
|
}
|
|
47
84
|
else if (upper === "END") {
|
|
@@ -63,37 +100,46 @@ function findSplitEnds(text, tokens, dialect) {
|
|
|
63
100
|
else {
|
|
64
101
|
depth = Math.max(0, depth - 1);
|
|
65
102
|
}
|
|
66
|
-
}
|
|
67
|
-
else if (t.text === ";") {
|
|
68
103
|
if (depth === 0)
|
|
69
|
-
|
|
104
|
+
opener = undefined;
|
|
70
105
|
}
|
|
71
|
-
else if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const next = channel0[i + 1];
|
|
76
|
-
const alone = (!prev || prev.line !== t.line) && (!next || next.line !== t.line);
|
|
77
|
-
if (alone) {
|
|
78
|
-
const nl = text.indexOf("\n", t.stop + 1);
|
|
79
|
-
ends.push(nl === -1 ? text.length : nl + 1);
|
|
80
|
-
}
|
|
106
|
+
else if (depth === 0) {
|
|
107
|
+
const sep = separatorAt(text, channel0, i, dialect);
|
|
108
|
+
if (sep)
|
|
109
|
+
ends.push(sep);
|
|
81
110
|
}
|
|
82
111
|
}
|
|
83
|
-
|
|
112
|
+
if (depth > 0 && opener) {
|
|
113
|
+
// Unclosed level: keep the split points before it, then split at every separator from it on.
|
|
114
|
+
ends = ends.slice(0, opener.endsBefore);
|
|
115
|
+
for (let i = opener.index; i < channel0.length; i++) {
|
|
116
|
+
const sep = separatorAt(text, channel0, i, dialect);
|
|
117
|
+
if (sep)
|
|
118
|
+
ends.push(sep);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Tokens are in source order, so the last channel-0 token decides whether anything real follows
|
|
122
|
+
// the last separator.
|
|
123
|
+
const lastReal = channel0[channel0.length - 1];
|
|
124
|
+
const last = ends[ends.length - 1];
|
|
125
|
+
if (last && (lastReal === undefined || lastReal.start < last.end)) {
|
|
126
|
+
ends.pop();
|
|
127
|
+
return { ends, tail: last.separator };
|
|
128
|
+
}
|
|
129
|
+
return { ends };
|
|
84
130
|
}
|
|
85
|
-
/** Turn ascending split
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
function buildCells(splitEnds, length) {
|
|
131
|
+
/** Turn ascending split points into contiguous cells tiling `[0, text.length)`. A doc with no
|
|
132
|
+
* split points is one cell; text after the last split point is the final cell, carrying `tail`
|
|
133
|
+
* (its own separator, when it is a terminated statement whose trailing trivia was folded in). */
|
|
134
|
+
function buildCells(splitEnds, tail, length) {
|
|
89
135
|
const spans = [];
|
|
90
136
|
let start = 0;
|
|
91
|
-
for (const
|
|
92
|
-
spans.push({ start, end });
|
|
93
|
-
start = end;
|
|
137
|
+
for (const e of splitEnds) {
|
|
138
|
+
spans.push({ start, end: e.end, separator: e.separator });
|
|
139
|
+
start = e.end;
|
|
94
140
|
}
|
|
95
141
|
if (start < length || spans.length === 0)
|
|
96
|
-
spans.push({ start, end: length });
|
|
142
|
+
spans.push(tail ? { start, end: length, separator: tail } : { start, end: length });
|
|
97
143
|
return spans;
|
|
98
144
|
}
|
|
99
145
|
/** The tiling invariant: contiguous, starts at 0, ends at `length`, in order. */
|
|
@@ -115,14 +161,16 @@ function tiles(spans, length) {
|
|
|
115
161
|
* Total: never throws. Splits at channel-0 `;` at compound depth 0 (BEGIN/CASE
|
|
116
162
|
* increment, END decrements, floor 0; a T-SQL `BEGIN TRAN`/`TRANSACTION`/
|
|
117
163
|
* `DISTRIBUTED` does not open a depth level) plus, for T-SQL, a `GO` batch
|
|
118
|
-
* separator alone on its line
|
|
164
|
+
* separator alone on its line; text ending inside an open level splits at every
|
|
165
|
+
* separator from the unclosed opener on (see `findSplitEnds`). Each cell carries
|
|
166
|
+
* the separator token that ends it. Returns the whole doc as one cell when
|
|
119
167
|
* splitting is unsafe (the tiling invariant fails) or pointless (no separators).
|
|
120
168
|
*/
|
|
121
169
|
export function splitStatements(text, dialect) {
|
|
122
170
|
try {
|
|
123
171
|
const tokens = tokenize(text, dialect);
|
|
124
|
-
const
|
|
125
|
-
const spans = buildCells(
|
|
172
|
+
const { ends, tail } = findSplitEnds(text, tokens, dialect);
|
|
173
|
+
const spans = buildCells(ends, tail, text.length);
|
|
126
174
|
return tiles(spans, text.length) ? spans : wholeDoc(text);
|
|
127
175
|
}
|
|
128
176
|
catch (e) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { parse, analyze, toAst, toScopes, qualify, lineage, deriveSymbols, TypeInfo, type Nullability, Lineage, originsOfExpr, lineageAt, lineageOf, type LineageHop, type ViaStep, tokenize, SqlDocument, LineIndex, complete, completeAt, type Completion, type CompletionResult, type ReplaceRange, type CompleteOptions, type CandidateIdentity, type CandidateDecoration, type DecorateCandidate, jinjaSlotAt, type JinjaSlot, signatureAt, SIGNATURES, lookupSignature, hasSignature, renderSignature, FN_DOCS, lookupFnDoc, referencesAt, type Occurrence, type Occurrences, frameAt, type Frame, clausesOf, type ClauseInfo, type ClauseKind, setOpArmsOf, type SetOpArm, type SetOpArms, dialectSymbols, type DialectSymbols, dialectVocabulary, type DialectVocabulary, reservedKeywords, type KeywordEntry, type KeywordReservation, CallbackSchema, type SchemaProvider, type TableResolver, DERIVED_DIALECTS, resolveDialect, foldIdentifier, displayName, type IdentKind, type SignatureHelpInfo, type SignatureLabel, type FnSignature, type ParamSig, type RenderSignatureOptions, type FnDoc, type Dialect, type DialectOpts, type ParseResultIR, type Analysis, type Token, type TokenRole, type DocumentAnalysis, type StatementCell, type StatementCellSpan, type DocumentVariant, type UnionCte, } from "./api.js";
|
|
1
|
+
export { parse, analyze, toAst, toScopes, qualify, lineage, deriveSymbols, TypeInfo, type Nullability, Lineage, originsOfExpr, lineageAt, lineageOf, type LineageHop, type ViaStep, tokenize, SqlDocument, LineIndex, complete, completeAt, type Completion, type CompletionResult, type ReplaceRange, type CompleteOptions, type CandidateIdentity, type CandidateDecoration, type DecorateCandidate, jinjaSlotAt, type JinjaSlot, signatureAt, SIGNATURES, lookupSignature, hasSignature, renderSignature, FN_DOCS, lookupFnDoc, referencesAt, type Occurrence, type Occurrences, frameAt, type Frame, clausesOf, type ClauseInfo, type ClauseKind, setOpArmsOf, type SetOpArm, type SetOpArms, dialectSymbols, type DialectSymbols, dialectVocabulary, type DialectVocabulary, reservedKeywords, type KeywordEntry, type KeywordReservation, CallbackSchema, type SchemaProvider, type TableResolver, DERIVED_DIALECTS, resolveDialect, foldIdentifier, displayName, type IdentKind, type SignatureHelpInfo, type SignatureLabel, type FnSignature, type ParamSig, type RenderSignatureOptions, type FnDoc, type Dialect, type DialectOpts, type ParseResultIR, type Analysis, type Token, type TokenRole, type DocumentAnalysis, type StatementCell, type StatementCellSpan, statementSpans, type DocumentVariant, type UnionCte, } from "./api.js";
|
|
2
2
|
export { SqlSession, type SessionOptions } from "./session.js";
|
|
3
3
|
export { parseDatabricks } from "./databricks/parse.js";
|
|
4
4
|
export { parseTSql } from "./tsql/parse.js";
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// - Building blocks: the per-dialect parse*/lower and the raw shared passes, for callers who
|
|
11
11
|
// want a specific tier or the raw CST escape hatch.
|
|
12
12
|
// --- Uniform entry, lift helpers, typed wrappers, composable passes (src/api.ts) ---
|
|
13
|
-
export { parse, analyze, toAst, toScopes, qualify, lineage, deriveSymbols, TypeInfo, Lineage, originsOfExpr, lineageAt, lineageOf, tokenize, SqlDocument, LineIndex, complete, completeAt, jinjaSlotAt, signatureAt, SIGNATURES, lookupSignature, hasSignature, renderSignature, FN_DOCS, lookupFnDoc, referencesAt, frameAt, clausesOf, setOpArmsOf, dialectSymbols, dialectVocabulary, reservedKeywords, CallbackSchema, DERIVED_DIALECTS, resolveDialect, foldIdentifier, displayName, } from "./api.js";
|
|
13
|
+
export { parse, analyze, toAst, toScopes, qualify, lineage, deriveSymbols, TypeInfo, Lineage, originsOfExpr, lineageAt, lineageOf, tokenize, SqlDocument, LineIndex, complete, completeAt, jinjaSlotAt, signatureAt, SIGNATURES, lookupSignature, hasSignature, renderSignature, FN_DOCS, lookupFnDoc, referencesAt, frameAt, clausesOf, setOpArmsOf, dialectSymbols, dialectVocabulary, reservedKeywords, CallbackSchema, DERIVED_DIALECTS, resolveDialect, foldIdentifier, displayName, statementSpans, } from "./api.js";
|
|
14
14
|
// SqlSession — the verb-shaped facade over one SqlDocument (offset in, answer out; pure delegation).
|
|
15
15
|
export { SqlSession } from "./session.js";
|
|
16
16
|
// --- Per-dialect building blocks: parse* (CST + errors) and lower* (CST → IR), kept as one
|
|
@@ -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)
|
package/dist/minijinja/engine.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseTemplated, parseTemplatedCell } from "./parse.js";
|
|
1
|
+
import { parseTemplated, parseTemplatedCell, placeholderOf } from "./parse.js";
|
|
2
2
|
import { templateVariants } from "./variants.js";
|
|
3
3
|
/** The minijinja template engine (the Rust engine dbt Fusion uses — the grammar
|
|
4
4
|
* oracle for what we accept). The shipped, and only, TemplateEngine. */
|
|
@@ -7,6 +7,7 @@ export function minijinja() {
|
|
|
7
7
|
name: "minijinja",
|
|
8
8
|
parse: (text, dialect, opts) => parseTemplated(text, dialect, opts),
|
|
9
9
|
variants: (text, dialect) => templateVariants(text, dialect),
|
|
10
|
+
placeholder: (text, _dialect, opts) => placeholderOf(text, opts),
|
|
10
11
|
parseCell: (whole, span, text, dialect, opts) => parseTemplatedCell(whole, span, text, dialect, opts),
|
|
11
12
|
};
|
|
12
13
|
}
|
|
@@ -35,10 +35,15 @@ 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: {
|
|
42
43
|
start: number;
|
|
43
44
|
end: number;
|
|
44
45
|
}, text: string, dialect: Dialect, opts?: TemplatedParseOptions): TemplatedCellResult;
|
|
46
|
+
/** The placeholder `parseTemplated` would parse for `text` (`TemplateEngine.placeholder`): the
|
|
47
|
+
* segmenter alone, no SQL parse. Total: the text itself when segmentation gives up, the same
|
|
48
|
+
* floor `parseTemplated`'s degrade path reports as its placeholder. */
|
|
49
|
+
export declare function placeholderOf(text: string, opts?: TemplatedParseOptions): string;
|
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,10 +668,25 @@ 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
|
}
|
|
678
|
+
/** The placeholder `parseTemplated` would parse for `text` (`TemplateEngine.placeholder`): the
|
|
679
|
+
* segmenter alone, no SQL parse. Total: the text itself when segmentation gives up, the same
|
|
680
|
+
* floor `parseTemplated`'s degrade path reports as its placeholder. */
|
|
681
|
+
export function placeholderOf(text, opts) {
|
|
682
|
+
try {
|
|
683
|
+
return segment(text, opts?.provider ?? OPEN_PROVIDER).placeholder;
|
|
684
|
+
}
|
|
685
|
+
catch (e) {
|
|
686
|
+
debugRethrow(e);
|
|
687
|
+
return text;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
674
690
|
/** The cell start's 0-based line / column / char offset in `text` (`\n` is the line break, the
|
|
675
691
|
* convention every span in the pipeline follows; a `\r` is an ordinary column). */
|
|
676
692
|
function cellBaseOf(text, offset) {
|
|
@@ -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,11 @@ 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
|
|
125
|
+
/** Optional: the placeholder-filled SQL text `parse` would see for `text` (see
|
|
126
|
+
* `TemplatedParseResult.placeholder`), without running the SQL parse. Total: on any internal
|
|
127
|
+
* surprise answer `text` itself. Backs `statementSpans`, which only needs the split. */
|
|
128
|
+
placeholder?(text: string, dialect: Dialect, opts?: TemplatedParseOptions): string;
|
|
129
|
+
/** Optional: the products of ONE statement cell of a templated document, the plain parse of
|
|
117
130
|
* `whole.placeholder`'s slice `[span.start, span.end)` (cell-relative) with `whole`'s tags
|
|
118
131
|
* correlated onto it. `whole` is this engine's own `parse` result for the full `text`, `span`
|
|
119
132
|
* 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.
|
|
3
|
+
"version": "1.11.0",
|
|
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",
|