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.
@@ -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 unlike `buildCell` nothing is shifted. Mirrors
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 unlike `buildCell` nothing is shifted. Mirrors
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 do NOT re-parse.
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: { tagOf: c.tagOf, nodeOf: c.nodeOf },
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 duplicates stay uncached).
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 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) {
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 (const c of cells) {
931
- const tag = c.correlation?.tagOf(node);
932
- if (tag)
933
- return tag;
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 (const c of cells) {
939
- const node = c.correlation?.nodeOf(tag);
940
- if (node)
941
- return node;
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
  },
@@ -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
  /**
@@ -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, while `tagOf`/`nodeOf` answer with `whole.tags`'s own nodes.
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: {
@@ -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, while `tagOf`/`nodeOf` answer with `whole.tags`'s own nodes.
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
- tagOf: (node) => correlation.byNode.get(node),
671
- nodeOf: (tag) => correlation.byTag.get(tag),
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 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;
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 the plain parse of
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.0",
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",