sqllens 1.9.1 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/document/document.d.ts +34 -18
- package/dist/document/document.js +155 -35
- package/dist/minijinja/apply-tags.d.ts +13 -1
- package/dist/minijinja/apply-tags.js +40 -11
- package/dist/minijinja/engine.js +2 -1
- package/dist/minijinja/parse.d.ts +15 -1
- package/dist/minijinja/parse.js +33 -0
- package/dist/template/engine.d.ts +23 -0
- package/package.json +1 -1
|
@@ -158,15 +158,29 @@ export declare class SqlDocument {
|
|
|
158
158
|
* NOT replace the cache entry: the first occurrence keeps its stable cross-edit identity (the
|
|
159
159
|
* common case), and only intra-doc duplicates — rare — pay a re-parse per build. */
|
|
160
160
|
private buildCell;
|
|
161
|
-
/** Build the
|
|
162
|
-
*
|
|
163
|
-
* `
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
161
|
+
/** Build the WHOLE-TEXT cell of a TEMPLATED document: span [0, text.length), its products from a
|
|
162
|
+
* single `engine.parse(text, dialect, { provider })` call rather than the plain per-dialect
|
|
163
|
+
* `parse()`. It is the document's one cell when the placeholder splits into one statement (every
|
|
164
|
+
* single-statement model), and the carrier of the engine result (`templated`) that a multi-cell
|
|
165
|
+
* document's slices are built from. `r.tokens`/`r.diagnostics` are ALREADY document coordinates
|
|
166
|
+
* (the cell always starts at 0), so — unlike `buildCell` — nothing is shifted. Mirrors
|
|
167
|
+
* `buildCell`'s CachedCell/StatementCell shapes so every downstream consumer (analyze(), cellAt(),
|
|
168
|
+
* nodeAt()…) sees the same structure whether the document is plain or templated. Cached in the SAME
|
|
169
|
+
* cross-edit `_cellCache` as plain cells, under a prefixed key so a templated cell can never
|
|
170
|
+
* collide with a plain one for the same text. */
|
|
169
171
|
private buildTemplatedCell;
|
|
172
|
+
/** Build one statement cell of a MULTI-cell templated document for `span` (a `splitStatements`
|
|
173
|
+
* span over the engine's placeholder). The cell is the plain parse of its placeholder slice,
|
|
174
|
+
* cell-relative like a plain cell, with the whole-document tags correlated onto it by
|
|
175
|
+
* `engine.parseCell` (provider-resolved source names + `template` markers, so the fill never
|
|
176
|
+
* reaches scope/qualify). Cached across edits like a plain cell: the key is the placeholder
|
|
177
|
+
* slice (what parsed) plus the raw slice (which tags sit inside it), the engine + provider
|
|
178
|
+
* version (what the tags resolve to) and `setsKey` (the `{% set %}`/`{% macro %}`/`{% for %}`
|
|
179
|
+
* tags anywhere in the text that steer a bare `{{ t }}` binding). The document-level products
|
|
180
|
+
* are projected onto the cell by span rather than re-derived: `tokens` is the unified SQL+jinja
|
|
181
|
+
* stream sliced, `diagnostics` the scrubbed set filtered (the last cell absorbs end-of-text),
|
|
182
|
+
* `errors` that count. `handedOut` dedupes intra-document duplicates exactly as `buildCell` does. */
|
|
183
|
+
private buildTemplatedSlice;
|
|
170
184
|
/** Build a document for `text` in `dialect`. Total: never throws, even on broken / mid-edit input.
|
|
171
185
|
* Starts a FRESH cell cache — cross-edit reuse comes from withText(), not create(). Pass
|
|
172
186
|
* `templating` to parse through an injected TemplateEngine (jinja-SQL etc.) instead of the plain
|
|
@@ -295,8 +309,7 @@ export declare class SqlDocument {
|
|
|
295
309
|
* subset, see `scopeOutputColumns`'s pipe doc comment. Falls through to this document's own
|
|
296
310
|
* (single-arm) answer when there are no variants; there is no pre-existing single-doc
|
|
297
311
|
* equivalent to delegate to, unlike unionSymbols/unionDiagnostics, so the no-variant case is
|
|
298
|
-
* just the one-arm instance of the same algorithm. A MULTI-STATEMENT document (
|
|
299
|
-
* templated door always forces exactly one cell, so the two "multi" shapes never overlap) merges
|
|
312
|
+
* just the one-arm instance of the same algorithm. A MULTI-STATEMENT document (or arm) merges
|
|
300
313
|
* every statement CELL's own CTEs instead, each shifted from cell-relative to DOCUMENT coordinates
|
|
301
314
|
* (the same shift `analyze()` already applies to symbols/diagnostics for a multi-cell document);
|
|
302
315
|
* the compound facade itself carries no CTEs, so cells are the real per-statement source. Memoized
|
|
@@ -315,13 +328,16 @@ export declare class SqlDocument {
|
|
|
315
328
|
span: Span;
|
|
316
329
|
}[];
|
|
317
330
|
private buildUnionOutputColumns;
|
|
318
|
-
/**
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* non-templated document is the trivial one-arm case of the same shape (zero shift, this
|
|
325
|
-
* document's own scopes/qualification). */
|
|
331
|
+
/** The "arms" `unionCtes`/`unionOutputColumns` aggregate over, unified across the two shapes that
|
|
332
|
+
* make a document "multi", VARIANTS FIRST (ruled 2026-09-13): a templated document's real
|
|
333
|
+
* variants are the unit, each arm SqlDocument (already in DOCUMENT coordinates, since a
|
|
334
|
+
* realization is length-preserving) contributing its own statement cells through `ownArms`; a
|
|
335
|
+
* document without variants contributes its own cells the same way. A region can span a cell
|
|
336
|
+
* boundary, so cells-first (arms inside each cell) is not an option. */
|
|
326
337
|
private armsData;
|
|
338
|
+
/** This document's own arms, variants ignored: each statement CELL of a multi-cell document
|
|
339
|
+
* (cell-relative scopes, shifted to document coordinates by the cell's start, mirroring
|
|
340
|
+
* `buildAnalysis`'s per-cell shift), or the single-cell document itself (zero shift, its own
|
|
341
|
+
* scopes/qualification). */
|
|
342
|
+
private ownArms;
|
|
327
343
|
}
|
|
@@ -155,12 +155,35 @@ export class SqlDocument {
|
|
|
155
155
|
this._provider = opts.provider;
|
|
156
156
|
let cells;
|
|
157
157
|
let backing;
|
|
158
|
+
let templated;
|
|
158
159
|
if (opts.templating) {
|
|
159
|
-
// The templated door:
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
// The templated door: the engine runs ONCE over the whole text (buildTemplatedCell), then the
|
|
161
|
+
// placeholder it saw is split into statement cells exactly like a plain document's text. The
|
|
162
|
+
// fill is length- and newline-preserving, so the spans are document coordinates already, and
|
|
163
|
+
// each cell is the plain parse of its placeholder slice with the whole-document tags
|
|
164
|
+
// correlated onto it by the engine (buildTemplatedSlice). One span (every single-statement
|
|
165
|
+
// model, or a tiling failure) keeps the whole-text cell itself: byte-identical products,
|
|
166
|
+
// same cache entry as before cells existed on this door.
|
|
167
|
+
const whole = this.buildTemplatedCell(text, opts.templating, opts.provider);
|
|
168
|
+
const r = whole.cached.templated;
|
|
169
|
+
const spans = opts.templating.parseCell ? splitStatements(r.placeholder, dialect) : [whole.cell.span];
|
|
170
|
+
if (spans.length === 1) {
|
|
171
|
+
cells = [whole.cell];
|
|
172
|
+
backing = [whole.cached];
|
|
173
|
+
templated = r;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
const setsKey = setResolutionKey(r, text);
|
|
177
|
+
const handedOut = new Set();
|
|
178
|
+
cells = [];
|
|
179
|
+
backing = [];
|
|
180
|
+
for (const span of spans) {
|
|
181
|
+
const built = this.buildTemplatedSlice(span, r, opts.templating, opts.provider, setsKey, handedOut);
|
|
182
|
+
cells.push(built.cell);
|
|
183
|
+
backing.push(built.cached);
|
|
184
|
+
}
|
|
185
|
+
templated = withCellCorrelation(r, backing);
|
|
186
|
+
}
|
|
164
187
|
}
|
|
165
188
|
else {
|
|
166
189
|
// Split into per-statement cells and parse each independently, reusing unchanged cells from
|
|
@@ -178,7 +201,7 @@ export class SqlDocument {
|
|
|
178
201
|
}
|
|
179
202
|
this.statements = Object.freeze(cells);
|
|
180
203
|
this._cells = backing;
|
|
181
|
-
this.templated =
|
|
204
|
+
this.templated = templated;
|
|
182
205
|
// Whole-document facade. tokens/diagnostics/errors are the cheap concat/sum across cells.
|
|
183
206
|
this.tokens = cells.flatMap((c) => c.tokens);
|
|
184
207
|
this.diagnostics = cells.flatMap((c) => c.diagnostics);
|
|
@@ -254,14 +277,16 @@ export class SqlDocument {
|
|
|
254
277
|
});
|
|
255
278
|
return { cell, cached };
|
|
256
279
|
}
|
|
257
|
-
/** Build the
|
|
258
|
-
*
|
|
259
|
-
* `
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
280
|
+
/** Build the WHOLE-TEXT cell of a TEMPLATED document: span [0, text.length), its products from a
|
|
281
|
+
* single `engine.parse(text, dialect, { provider })` call rather than the plain per-dialect
|
|
282
|
+
* `parse()`. It is the document's one cell when the placeholder splits into one statement (every
|
|
283
|
+
* single-statement model), and the carrier of the engine result (`templated`) that a multi-cell
|
|
284
|
+
* document's slices are built from. `r.tokens`/`r.diagnostics` are ALREADY document coordinates
|
|
285
|
+
* (the cell always starts at 0), so — unlike `buildCell` — nothing is shifted. Mirrors
|
|
286
|
+
* `buildCell`'s CachedCell/StatementCell shapes so every downstream consumer (analyze(), cellAt(),
|
|
287
|
+
* nodeAt()…) sees the same structure whether the document is plain or templated. Cached in the SAME
|
|
288
|
+
* cross-edit `_cellCache` as plain cells, under a prefixed key so a templated cell can never
|
|
289
|
+
* collide with a plain one for the same text. */
|
|
265
290
|
buildTemplatedCell(text, engine, provider) {
|
|
266
291
|
const span = { start: 0, end: text.length };
|
|
267
292
|
// Collision-proofed against a plain cell's `dialect + " " + text` key by the "templated "
|
|
@@ -301,6 +326,64 @@ export class SqlDocument {
|
|
|
301
326
|
});
|
|
302
327
|
return { cell, cached };
|
|
303
328
|
}
|
|
329
|
+
/** Build one statement cell of a MULTI-cell templated document for `span` (a `splitStatements`
|
|
330
|
+
* span over the engine's placeholder). The cell is the plain parse of its placeholder slice,
|
|
331
|
+
* cell-relative like a plain cell, with the whole-document tags correlated onto it by
|
|
332
|
+
* `engine.parseCell` (provider-resolved source names + `template` markers, so the fill never
|
|
333
|
+
* reaches scope/qualify). Cached across edits like a plain cell: the key is the placeholder
|
|
334
|
+
* slice (what parsed) plus the raw slice (which tags sit inside it), the engine + provider
|
|
335
|
+
* version (what the tags resolve to) and `setsKey` (the `{% set %}`/`{% macro %}`/`{% for %}`
|
|
336
|
+
* tags anywhere in the text that steer a bare `{{ t }}` binding). The document-level products
|
|
337
|
+
* are projected onto the cell by span rather than re-derived: `tokens` is the unified SQL+jinja
|
|
338
|
+
* stream sliced, `diagnostics` the scrubbed set filtered (the last cell absorbs end-of-text),
|
|
339
|
+
* `errors` that count. `handedOut` dedupes intra-document duplicates exactly as `buildCell` does. */
|
|
340
|
+
buildTemplatedSlice(span, r, engine, provider, setsKey, handedOut) {
|
|
341
|
+
const rawText = this.text.slice(span.start, span.end);
|
|
342
|
+
const placeholderText = r.placeholder.slice(span.start, span.end);
|
|
343
|
+
const providerVersion = provider?.version ?? 0;
|
|
344
|
+
// `setsKey` is length-prefixed and the two slices are equal-length (the fill is
|
|
345
|
+
// length-preserving), so the plain concatenation is unambiguous without a separator byte.
|
|
346
|
+
const key = `templated-cell ${engine.name}@${providerVersion} ${this.dialect} ${setsKey.length}:${setsKey}${placeholderText}${rawText}`;
|
|
347
|
+
let cached = this._cellCache.get(key);
|
|
348
|
+
if (cached !== undefined && handedOut.has(cached))
|
|
349
|
+
cached = undefined; // intra-doc duplicate
|
|
350
|
+
if (cached === undefined) {
|
|
351
|
+
const c = engine.parseCell(r, span, this.text, this.dialect, { provider });
|
|
352
|
+
cached = {
|
|
353
|
+
text: rawText,
|
|
354
|
+
category: c.sql.ast.statement ?? "other",
|
|
355
|
+
ast: c.sql.ast,
|
|
356
|
+
cst: c.sql.cst,
|
|
357
|
+
// Resolve scopes from the already-lowered (marker-carrying) ast — do NOT re-parse.
|
|
358
|
+
scopes: toScopes(c.sql.ast, { dialect: this.dialect }),
|
|
359
|
+
tokens: c.sql.tokens,
|
|
360
|
+
errors: c.sql.errors,
|
|
361
|
+
diagnostics: c.sql.diagnostics,
|
|
362
|
+
analysis: new WeakMap(),
|
|
363
|
+
correlation: { tagOf: c.tagOf, nodeOf: c.nodeOf },
|
|
364
|
+
};
|
|
365
|
+
// Cache only the FIRST product for a key (see buildCell — duplicates stay uncached).
|
|
366
|
+
if (this._cellCache.get(key) === undefined)
|
|
367
|
+
this._cellCache.set(key, cached);
|
|
368
|
+
}
|
|
369
|
+
handedOut.add(cached);
|
|
370
|
+
const last = span.end === this.text.length;
|
|
371
|
+
const inCell = (offset) => offset >= span.start && (offset < span.end || last);
|
|
372
|
+
const tokens = r.tokens.filter((t) => inCell(t.start));
|
|
373
|
+
const diagnostics = r.diagnostics.filter((d) => inCell(d.offset ?? this.lines.offsetAt(d.line - 1, d.column)));
|
|
374
|
+
const cell = Object.freeze({
|
|
375
|
+
span,
|
|
376
|
+
text: cached.text,
|
|
377
|
+
category: cached.category,
|
|
378
|
+
ast: cached.ast,
|
|
379
|
+
cst: cached.cst,
|
|
380
|
+
scopes: cached.scopes,
|
|
381
|
+
tokens,
|
|
382
|
+
errors: diagnostics.length,
|
|
383
|
+
diagnostics,
|
|
384
|
+
});
|
|
385
|
+
return { cell, cached };
|
|
386
|
+
}
|
|
304
387
|
/** Build a document for `text` in `dialect`. Total: never throws, even on broken / mid-edit input.
|
|
305
388
|
* Starts a FRESH cell cache — cross-edit reuse comes from withText(), not create(). Pass
|
|
306
389
|
* `templating` to parse through an injected TemplateEngine (jinja-SQL etc.) instead of the plain
|
|
@@ -716,8 +799,7 @@ export class SqlDocument {
|
|
|
716
799
|
* subset, see `scopeOutputColumns`'s pipe doc comment. Falls through to this document's own
|
|
717
800
|
* (single-arm) answer when there are no variants; there is no pre-existing single-doc
|
|
718
801
|
* equivalent to delegate to, unlike unionSymbols/unionDiagnostics, so the no-variant case is
|
|
719
|
-
* just the one-arm instance of the same algorithm. A MULTI-STATEMENT document (
|
|
720
|
-
* templated door always forces exactly one cell, so the two "multi" shapes never overlap) merges
|
|
802
|
+
* just the one-arm instance of the same algorithm. A MULTI-STATEMENT document (or arm) merges
|
|
721
803
|
* every statement CELL's own CTEs instead, each shifted from cell-relative to DOCUMENT coordinates
|
|
722
804
|
* (the same shift `analyze()` already applies to symbols/diagnostics for a multi-cell document);
|
|
723
805
|
* the compound facade itself carries no CTEs, so cells are the real per-statement source. Memoized
|
|
@@ -783,27 +865,23 @@ export class SqlDocument {
|
|
|
783
865
|
}
|
|
784
866
|
return order.map((name) => ({ name, span: byName.get(name) }));
|
|
785
867
|
}
|
|
786
|
-
/**
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
* non-templated document is the trivial one-arm case of the same shape (zero shift, this
|
|
793
|
-
* document's own scopes/qualification). */
|
|
868
|
+
/** The "arms" `unionCtes`/`unionOutputColumns` aggregate over, unified across the two shapes that
|
|
869
|
+
* make a document "multi", VARIANTS FIRST (ruled 2026-09-13): a templated document's real
|
|
870
|
+
* variants are the unit, each arm SqlDocument (already in DOCUMENT coordinates, since a
|
|
871
|
+
* realization is length-preserving) contributing its own statement cells through `ownArms`; a
|
|
872
|
+
* document without variants contributes its own cells the same way. A region can span a cell
|
|
873
|
+
* boundary, so cells-first (arms inside each cell) is not an option. */
|
|
794
874
|
armsData(s) {
|
|
875
|
+
if (this.variants.length > 0)
|
|
876
|
+
return this.variants.flatMap((v) => v.doc().ownArms(s));
|
|
877
|
+
return this.ownArms(s);
|
|
878
|
+
}
|
|
879
|
+
/** This document's own arms, variants ignored: each statement CELL of a multi-cell document
|
|
880
|
+
* (cell-relative scopes, shifted to document coordinates by the cell's start, mirroring
|
|
881
|
+
* `buildAnalysis`'s per-cell shift), or the single-cell document itself (zero shift, its own
|
|
882
|
+
* scopes/qualification). */
|
|
883
|
+
ownArms(s) {
|
|
795
884
|
const ZERO = { line: 0, col: 0, offset: 0 };
|
|
796
|
-
if (this.variants.length > 0) {
|
|
797
|
-
return this.variants.map((v) => {
|
|
798
|
-
const doc = v.doc();
|
|
799
|
-
return {
|
|
800
|
-
scopeRoot: doc.scopes.root,
|
|
801
|
-
qualification: doc.analyze(s).qualification,
|
|
802
|
-
dialect: doc.dialect,
|
|
803
|
-
base: ZERO,
|
|
804
|
-
};
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
885
|
if (this.statements.length > 1) {
|
|
808
886
|
return this.statements.map((cell, i) => {
|
|
809
887
|
const p = this.lines.positionAt(cell.span.start);
|
|
@@ -825,6 +903,48 @@ export class SqlDocument {
|
|
|
825
903
|
];
|
|
826
904
|
}
|
|
827
905
|
}
|
|
906
|
+
/** The cache-key component of a templated cell's tag correlation that the cell's own text does not
|
|
907
|
+
* determine: the raw text of every `{% set %}` / `{% macro %}` / `{% for %}` tag in the document. A
|
|
908
|
+
* bare `{{ t }}` source binds through a literal `{% set t = ref(...) %}` declared ANYWHERE, and an
|
|
909
|
+
* inline macro or a `for` target anywhere disables that binding (apply-tags' `resolveSets`), so a
|
|
910
|
+
* change to any of them must miss every cell. */
|
|
911
|
+
function setResolutionKey(r, text) {
|
|
912
|
+
const parts = [];
|
|
913
|
+
for (const t of r.tags) {
|
|
914
|
+
if (t.kind !== "control")
|
|
915
|
+
continue;
|
|
916
|
+
if (t.keyword === "set" || t.keyword === "macro" || t.keyword === "for")
|
|
917
|
+
parts.push(text.slice(t.tagSpan.start, t.tagSpan.end));
|
|
918
|
+
}
|
|
919
|
+
// A control tag's own text never contains a tag boundary, so a newline join is unambiguous.
|
|
920
|
+
return parts.join("\n");
|
|
921
|
+
}
|
|
922
|
+
/** The `templated` facade of a MULTI-cell templated document: the whole-text engine result with
|
|
923
|
+
* `tagOf`/`nodeOf` answering from the CELLS' own correlations — the per-statement IR consumers reach
|
|
924
|
+
* through `statements`/`cellAt`/`nodeAt` — never from the whole-text parse, whose nodes a multi-cell
|
|
925
|
+
* document does not expose (its `ast` is the compound facade). Everything else is the engine's. */
|
|
926
|
+
function withCellCorrelation(r, cells) {
|
|
927
|
+
return {
|
|
928
|
+
...r,
|
|
929
|
+
tagOf: (node) => {
|
|
930
|
+
for (const c of cells) {
|
|
931
|
+
const tag = c.correlation?.tagOf(node);
|
|
932
|
+
if (tag)
|
|
933
|
+
return tag;
|
|
934
|
+
}
|
|
935
|
+
return undefined;
|
|
936
|
+
},
|
|
937
|
+
nodeOf: (tag) => {
|
|
938
|
+
for (const c of cells) {
|
|
939
|
+
const node = c.correlation?.nodeOf(tag);
|
|
940
|
+
if (node)
|
|
941
|
+
return node;
|
|
942
|
+
}
|
|
943
|
+
return undefined;
|
|
944
|
+
},
|
|
945
|
+
diagnosticsOf: (tag) => r.diagnosticsOf(tag),
|
|
946
|
+
};
|
|
947
|
+
}
|
|
828
948
|
/** Dedup `items` by a string key, keeping the FIRST occurrence of each key — arm/document order, so
|
|
829
949
|
* the "first live arm wins" representative-data rule falls out of plain array order. */
|
|
830
950
|
function dedupBy(items, keyOf) {
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { QueryExpr } from "../ir/ir.js";
|
|
2
2
|
import type { TemplateCall, TemplateProvider } from "../qualify/template-provider.js";
|
|
3
3
|
import type { MacroCall, TagNode } from "./tag-ast.js";
|
|
4
|
+
/** A statement cell's start in the document: 0-based line, 0-based column, char offset (the shape
|
|
5
|
+
* `LineIndex.positionAt` answers). Zero = the whole-text parse. */
|
|
6
|
+
export interface CellBase {
|
|
7
|
+
line: number;
|
|
8
|
+
column: number;
|
|
9
|
+
offset: number;
|
|
10
|
+
}
|
|
4
11
|
/** The rebuilt AST plus the tag↔node correlations collected while building it. */
|
|
5
12
|
export interface TagCorrelation {
|
|
6
13
|
ast: QueryExpr;
|
|
@@ -15,8 +22,13 @@ export interface TagCorrelation {
|
|
|
15
22
|
* containment. Returns the SAME `ast` reference when nothing correlates (structural sharing);
|
|
16
23
|
* returns a re-frozen rebuilt tree otherwise. Total, never throws; the correlation maps are empty
|
|
17
24
|
* (not absent) on the no-op and error paths.
|
|
25
|
+
*
|
|
26
|
+
* `base` is the cell's start position when `ast` is ONE STATEMENT CELL's parse of the placeholder
|
|
27
|
+
* (`parseTemplatedCell`): the node offsets are cell-relative while `tags` and `text` are the whole
|
|
28
|
+
* document's, so containment compares `offset + base.offset`, and every span this transform writes
|
|
29
|
+
* into the IR is rebased to cell coordinates. Zero (the default) is the whole-text parse.
|
|
18
30
|
*/
|
|
19
|
-
export declare function applyTemplateTags(ast: QueryExpr, tags: TagNode[], text: string, provider: TemplateProvider): TagCorrelation;
|
|
31
|
+
export declare function applyTemplateTags(ast: QueryExpr, tags: TagNode[], text: string, provider: TemplateProvider, base?: CellBase): TagCorrelation;
|
|
20
32
|
/**
|
|
21
33
|
* The provider key of a tag-AST MacroCall — name + package + literal args, with kwargs
|
|
22
34
|
* carried separately (the channel-agreed TemplateCall contract: quote-stripped, escapes
|
|
@@ -30,6 +30,7 @@ import { debugRethrow } from "../debug.js";
|
|
|
30
30
|
import { freezeIR } from "../ir/freeze.js";
|
|
31
31
|
import { qualifiedNameOf, synthesizedQualifiedName } from "../ir/qualified-name.js";
|
|
32
32
|
import { resolveBehavior } from "../dialect-behavior/registry.js";
|
|
33
|
+
const ZERO_BASE = { line: 0, column: 0, offset: 0 };
|
|
33
34
|
/** Record a freshly built node's correlation to the tag it came from, then return it unchanged
|
|
34
35
|
* (a passthrough so call sites stay expression-shaped). A scalar-slot tag lowers to BOTH a
|
|
35
36
|
* column Expr (`kind: "column"`) and a parallel ColumnRef record (`kind: "columnref"`, same
|
|
@@ -49,8 +50,13 @@ function attach(ctx, node, tag) {
|
|
|
49
50
|
* containment. Returns the SAME `ast` reference when nothing correlates (structural sharing);
|
|
50
51
|
* returns a re-frozen rebuilt tree otherwise. Total, never throws; the correlation maps are empty
|
|
51
52
|
* (not absent) on the no-op and error paths.
|
|
53
|
+
*
|
|
54
|
+
* `base` is the cell's start position when `ast` is ONE STATEMENT CELL's parse of the placeholder
|
|
55
|
+
* (`parseTemplatedCell`): the node offsets are cell-relative while `tags` and `text` are the whole
|
|
56
|
+
* document's, so containment compares `offset + base.offset`, and every span this transform writes
|
|
57
|
+
* into the IR is rebased to cell coordinates. Zero (the default) is the whole-text parse.
|
|
52
58
|
*/
|
|
53
|
-
export function applyTemplateTags(ast, tags, text, provider) {
|
|
59
|
+
export function applyTemplateTags(ast, tags, text, provider, base = ZERO_BASE) {
|
|
54
60
|
const byNode = new WeakMap();
|
|
55
61
|
const byTag = new Map();
|
|
56
62
|
try {
|
|
@@ -66,6 +72,7 @@ export function applyTemplateTags(ast, tags, text, provider) {
|
|
|
66
72
|
sets: resolveSets(tags, text, provider),
|
|
67
73
|
text,
|
|
68
74
|
provider,
|
|
75
|
+
base,
|
|
69
76
|
byNode,
|
|
70
77
|
byTag,
|
|
71
78
|
...(nameConfig ? { nameConfig } : {}),
|
|
@@ -89,14 +96,14 @@ function exprInfoOf(tag, ctx) {
|
|
|
89
96
|
// A call tag (ref/source/var/env_var/a macro) carries its provider key straight off the call,
|
|
90
97
|
// callOf reads name + literal args from the source, uniform across every callee.
|
|
91
98
|
if (tag.kind === "call")
|
|
92
|
-
return { span: tag.tagSpan, call: callOf(tag, ctx.text) };
|
|
99
|
+
return { span: cellSpan(tag.tagSpan, ctx), call: callOf(tag, ctx.text) };
|
|
93
100
|
// A non-call `other` tag: a bare `{{ t }}` resolving through a single-call `{% set t = … %}`
|
|
94
101
|
// carries that RHS call; anything else is opaque.
|
|
95
102
|
const ident = bareIdentOf(tag, ctx.text);
|
|
96
103
|
const resolved = ident !== undefined ? ctx.sets.get(ident) : undefined;
|
|
97
104
|
if (resolved)
|
|
98
|
-
return { span: tag.tagSpan, call: resolved.call };
|
|
99
|
-
return { span: tag.tagSpan };
|
|
105
|
+
return { span: cellSpan(tag.tagSpan, ctx), call: resolved.call };
|
|
106
|
+
return { span: cellSpan(tag.tagSpan, ctx) };
|
|
100
107
|
}
|
|
101
108
|
/** `{{ var('x') }}` / `{{ env_var('Y', …) }}` — the name + first literal arg, lexically. */
|
|
102
109
|
const VALUE_CALL_TAG = /^\{\{-?\s*(var|env_var)\s*\(\s*(['"])([^'"\\]*)\2\s*(,[\s\S]*?)?\)\s*(?:\|[\s\S]*)?-?\}\}$/;
|
|
@@ -129,7 +136,7 @@ function markTemplateExprs(node, ctx) {
|
|
|
129
136
|
if ((isColumnExpr || isColumnRef) && rec.template === undefined) {
|
|
130
137
|
const start = rec.cst?.start?.start;
|
|
131
138
|
if (start !== undefined) {
|
|
132
|
-
const tag = containingTag(ctx.relTags, start);
|
|
139
|
+
const tag = containingTag(ctx.relTags, start + ctx.base.offset);
|
|
133
140
|
if (tag)
|
|
134
141
|
return attach(ctx, { ...rec, template: exprInfoOf(tag, ctx) }, tag);
|
|
135
142
|
}
|
|
@@ -280,6 +287,22 @@ function containingTag(tags, offset) {
|
|
|
280
287
|
}
|
|
281
288
|
return undefined;
|
|
282
289
|
}
|
|
290
|
+
/** A document-coordinate tag span rebased to the cell's coordinates (the inverse of
|
|
291
|
+
* src/document/shift.ts's `shiftPartSpan`): a span on the cell's first line also loses the cell's
|
|
292
|
+
* start column. The whole-text parse (zero base) keeps the span object itself. */
|
|
293
|
+
function cellSpan(p, ctx) {
|
|
294
|
+
const b = ctx.base;
|
|
295
|
+
if (b.offset === 0 && b.line === 0 && b.column === 0)
|
|
296
|
+
return p;
|
|
297
|
+
return {
|
|
298
|
+
start: p.start - b.offset,
|
|
299
|
+
end: p.end - b.offset,
|
|
300
|
+
line: p.line - b.line,
|
|
301
|
+
column: p.line === b.line + 1 ? p.column - b.column : p.column,
|
|
302
|
+
endLine: p.endLine - b.line,
|
|
303
|
+
endColumn: p.endLine === b.line + 1 ? p.endColumn - b.column : p.endColumn,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
283
306
|
function transformQuery(q, ctx) {
|
|
284
307
|
const ctes = mapShared(q.ctes, (c) => transformCte(c, ctx));
|
|
285
308
|
const body = transformBody(q.body, ctx);
|
|
@@ -401,7 +424,7 @@ function transformTableSource(src, ctx) {
|
|
|
401
424
|
const startTok = src.cst?.start;
|
|
402
425
|
if (!startTok)
|
|
403
426
|
return src;
|
|
404
|
-
const tag = containingTag(ctx.relTags, startTok.start);
|
|
427
|
+
const tag = containingTag(ctx.relTags, startTok.start + ctx.base.offset);
|
|
405
428
|
if (!tag)
|
|
406
429
|
return src;
|
|
407
430
|
// A placeholder-fill alias sits INSIDE the tag span: a multi-line tag fills one
|
|
@@ -414,7 +437,7 @@ function transformTableSource(src, ctx) {
|
|
|
414
437
|
// fill limitation, out of apply-tags' reach); making it `undefined` here is honest,
|
|
415
438
|
// where `jjj…` was a fabrication.
|
|
416
439
|
const aliasTok = src.aliasCst?.start;
|
|
417
|
-
const base = aliasTok != null && inSpan(aliasTok.start, tag.tagSpan) ? withoutAlias(src) : src;
|
|
440
|
+
const base = aliasTok != null && inSpan(aliasTok.start + ctx.base.offset, tag.tagSpan) ? withoutAlias(src) : src;
|
|
418
441
|
// An unresolved source's name is the RAW TAG TEXT — the bytes the user actually wrote. The
|
|
419
442
|
// placeholder fill is scaffolding this library invented so the grammar parses; letting it
|
|
420
443
|
// escape as a relation name (scope sources, lineage dependencies, go-to-def) is fabrication
|
|
@@ -432,7 +455,8 @@ function transformTableSource(src, ctx) {
|
|
|
432
455
|
: qualifiedNameOf(parts, ctx.nameConfig);
|
|
433
456
|
return { ...b, relation };
|
|
434
457
|
};
|
|
435
|
-
// NOTE: `template.span` intentionally aliases `tag.tagSpan` BY
|
|
458
|
+
// NOTE: on the whole-text parse `template.span` intentionally aliases `tag.tagSpan` BY
|
|
459
|
+
// REFERENCE (a statement cell's parse gets a rebased copy, see `cellSpan`). freezeIR
|
|
436
460
|
// therefore also freezes the TagNode.tagSpan object returned in `.tags`, benign
|
|
437
461
|
// since spans are read-only. Every call marker carries its `call`, the provider key
|
|
438
462
|
// the semantic layer resolves the relation and its columns through (relation-columns.ts).
|
|
@@ -444,7 +468,7 @@ function transformTableSource(src, ctx) {
|
|
|
444
468
|
// Either way the `call` keeps it consultable, so an unresolved call is not a dead end: a
|
|
445
469
|
// provider added later resolves it. ref vs source is not stored here, it is call.name.
|
|
446
470
|
const named = rel ? renamed(base, [...rel.nameParts], true) : renamed(base, rawTagName, false);
|
|
447
|
-
const template = { kind: "call", span: tag.tagSpan, call };
|
|
471
|
+
const template = { kind: "call", span: cellSpan(tag.tagSpan, ctx), call };
|
|
448
472
|
return attach(ctx, { ...named, template }, tag);
|
|
449
473
|
}
|
|
450
474
|
// Non-call expression tag (var / env_var / other) in a FROM slot. A bare `{{ t }}` resolving
|
|
@@ -455,9 +479,14 @@ function transformTableSource(src, ctx) {
|
|
|
455
479
|
const resolved = ident !== undefined ? ctx.sets.get(ident) : undefined;
|
|
456
480
|
if (resolved) {
|
|
457
481
|
const named = resolved.name ? renamed(base, [...resolved.name], true) : renamed(base, rawTagName, false);
|
|
458
|
-
const template = {
|
|
482
|
+
const template = {
|
|
483
|
+
kind: "call",
|
|
484
|
+
span: cellSpan(tag.tagSpan, ctx),
|
|
485
|
+
indirect: true,
|
|
486
|
+
call: resolved.call,
|
|
487
|
+
};
|
|
459
488
|
return attach(ctx, { ...named, template }, tag);
|
|
460
489
|
}
|
|
461
|
-
const template = { kind: "expr", span: tag.tagSpan, opaque: true };
|
|
490
|
+
const template = { kind: "expr", span: cellSpan(tag.tagSpan, ctx), opaque: true };
|
|
462
491
|
return attach(ctx, { ...renamed(base, rawTagName, false), template }, tag);
|
|
463
492
|
}
|
package/dist/minijinja/engine.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseTemplated } from "./parse.js";
|
|
1
|
+
import { parseTemplated, parseTemplatedCell } 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,5 +7,6 @@ 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
|
+
parseCell: (whole, span, text, dialect, opts) => parseTemplatedCell(whole, span, text, dialect, opts),
|
|
10
11
|
};
|
|
11
12
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Dialect } from "../dialect.js";
|
|
2
2
|
import type { Token } from "../token/token.js";
|
|
3
|
-
import type { MacroShape, TemplatedParseOptions, TemplatedParseResult } from "../template/engine.js";
|
|
3
|
+
import type { MacroShape, TemplatedCellResult, TemplatedParseOptions, TemplatedParseResult } from "../template/engine.js";
|
|
4
4
|
import type { ExpansionShape } from "../qualify/template-provider.js";
|
|
5
5
|
import type { TemplateCall } from "../ir/ir.js";
|
|
6
6
|
export type { TagNode, MacroCall } from "./tag-ast.js";
|
|
@@ -28,3 +28,17 @@ export declare function parseTemplated(text: string, dialect: Dialect, opts?: Te
|
|
|
28
28
|
* of parseTemplated. Total — never throws.
|
|
29
29
|
*/
|
|
30
30
|
export declare function tokenizeTemplated(text: string, dialect: Dialect, opts?: TemplatedParseOptions): Token[];
|
|
31
|
+
/**
|
|
32
|
+
* One statement cell of a templated document (`TemplateEngine.parseCell`): the plain per-dialect
|
|
33
|
+
* parse of the placeholder slice `[span.start, span.end)`, the same batch-of-one path a plain
|
|
34
|
+
* document's cells take, so IR / CST / tokens / diagnostics are CELL-relative, with `whole`'s tags
|
|
35
|
+
* correlated onto it by DOCUMENT offset (a node's cell offset plus the cell's start). The cell's
|
|
36
|
+
* sources then carry their provider-resolved names and `template` markers exactly as the
|
|
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, while `tagOf`/`nodeOf` answer with `whole.tags`'s own nodes.
|
|
39
|
+
* Total: `applyTemplateTags` leaves the plain parse in place on any internal surprise.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseTemplatedCell(whole: TemplatedParseResult, span: {
|
|
42
|
+
start: number;
|
|
43
|
+
end: number;
|
|
44
|
+
}, text: string, dialect: Dialect, opts?: TemplatedParseOptions): TemplatedCellResult;
|
package/dist/minijinja/parse.js
CHANGED
|
@@ -651,3 +651,36 @@ export function parseTemplated(text, dialect, opts) {
|
|
|
651
651
|
export function tokenizeTemplated(text, dialect, opts) {
|
|
652
652
|
return parseTemplated(text, dialect, opts).tokens;
|
|
653
653
|
}
|
|
654
|
+
/**
|
|
655
|
+
* One statement cell of a templated document (`TemplateEngine.parseCell`): the plain per-dialect
|
|
656
|
+
* parse of the placeholder slice `[span.start, span.end)`, the same batch-of-one path a plain
|
|
657
|
+
* document's cells take, so IR / CST / tokens / diagnostics are CELL-relative, with `whole`'s tags
|
|
658
|
+
* correlated onto it by DOCUMENT offset (a node's cell offset plus the cell's start). The cell's
|
|
659
|
+
* sources then carry their provider-resolved names and `template` markers exactly as the
|
|
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, while `tagOf`/`nodeOf` answer with `whole.tags`'s own nodes.
|
|
662
|
+
* Total: `applyTemplateTags` leaves the plain parse in place on any internal surprise.
|
|
663
|
+
*/
|
|
664
|
+
export function parseTemplatedCell(whole, span, text, dialect, opts) {
|
|
665
|
+
const sql = parse(whole.placeholder.slice(span.start, span.end), dialect);
|
|
666
|
+
const provider = opts?.provider ?? OPEN_PROVIDER;
|
|
667
|
+
const correlation = applyTemplateTags(sql.ast, whole.tags, text, provider, cellBaseOf(text, span.start));
|
|
668
|
+
return {
|
|
669
|
+
sql: { ...sql, ast: correlation.ast },
|
|
670
|
+
tagOf: (node) => correlation.byNode.get(node),
|
|
671
|
+
nodeOf: (tag) => correlation.byTag.get(tag),
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
/** The cell start's 0-based line / column / char offset in `text` (`\n` is the line break, the
|
|
675
|
+
* convention every span in the pipeline follows; a `\r` is an ordinary column). */
|
|
676
|
+
function cellBaseOf(text, offset) {
|
|
677
|
+
let line = 0;
|
|
678
|
+
let lineStart = 0;
|
|
679
|
+
for (let i = 0; i < offset; i++) {
|
|
680
|
+
if (text.charCodeAt(i) === 10) {
|
|
681
|
+
line++;
|
|
682
|
+
lineStart = i + 1;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return { line, column: offset - lineStart, offset };
|
|
686
|
+
}
|
|
@@ -88,6 +88,20 @@ export interface TemplatedParseResult {
|
|
|
88
88
|
* scrubber widened to it. Empty array when none. */
|
|
89
89
|
diagnosticsOf(tag: TagNode): SyntaxDiagnostic[];
|
|
90
90
|
}
|
|
91
|
+
/** One statement CELL of a templated document (`TemplateEngine.parseCell`): the plain per-dialect
|
|
92
|
+
* parse of the cell's placeholder slice, in CELL-relative coordinates like a plain document's
|
|
93
|
+
* cells, with the whole-document tags correlated onto it (provider-resolved source names,
|
|
94
|
+
* `template` markers, and the two-spine join). */
|
|
95
|
+
export interface TemplatedCellResult {
|
|
96
|
+
/** The cell's SQL parse over its placeholder slice (ast / cst / tokens / errors / diagnostics),
|
|
97
|
+
* every span cell-relative. A `template` marker's span is cell-relative too. */
|
|
98
|
+
sql: ParseResultIR;
|
|
99
|
+
/** The whole document's TagNode a template-marked node of THIS cell's IR came from. */
|
|
100
|
+
tagOf(node: object): TagNode | undefined;
|
|
101
|
+
/** The node of THIS cell's IR a whole-document tag became; undefined when the tag sits in
|
|
102
|
+
* another cell or has no IR presence. */
|
|
103
|
+
nodeOf(tag: TagNode): object | undefined;
|
|
104
|
+
}
|
|
91
105
|
/** A template engine: the syntax front end for one templating language over
|
|
92
106
|
* SQL. `parse` must satisfy the engine contract the conformance suite
|
|
93
107
|
* checks — tokens tile the source byte-for-byte, every span in original
|
|
@@ -99,4 +113,13 @@ export interface TemplateEngine {
|
|
|
99
113
|
parse(text: string, dialect: Dialect, opts?: TemplatedParseOptions): TemplatedParseResult;
|
|
100
114
|
/** Optional: coherent per-branch variant enumeration, for engines with control-flow arms. */
|
|
101
115
|
variants?(text: string, dialect: Dialect): TemplateVariant[];
|
|
116
|
+
/** Optional: the products of ONE statement cell of a templated document — the plain parse of
|
|
117
|
+
* `whole.placeholder`'s slice `[span.start, span.end)` (cell-relative) with `whole`'s tags
|
|
118
|
+
* correlated onto it. `whole` is this engine's own `parse` result for the full `text`, `span`
|
|
119
|
+
* a `splitStatements` span over that placeholder. An engine without it keeps the templated
|
|
120
|
+
* `SqlDocument` door at one whole-text cell. */
|
|
121
|
+
parseCell?(whole: TemplatedParseResult, span: {
|
|
122
|
+
start: number;
|
|
123
|
+
end: number;
|
|
124
|
+
}, text: string, dialect: Dialect, opts?: TemplatedParseOptions): TemplatedCellResult;
|
|
102
125
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sqllens",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.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",
|