sqllens 1.8.1 → 1.9.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/bigquery/parse.js +1 -0
- package/dist/databricks/parse.js +1 -0
- package/dist/duckdb/parse.js +1 -0
- package/dist/fragment-grammar.d.ts +3 -0
- package/dist/fragment-grammar.js +14 -4
- package/dist/fragment.d.ts +3 -0
- package/dist/fragment.js +26 -9
- package/dist/minijinja/index.d.ts +2 -1
- package/dist/minijinja/index.js +1 -1
- package/dist/minijinja/parse.d.ts +12 -2
- package/dist/minijinja/parse.js +199 -25
- package/dist/minijinja/segment.js +21 -11
- package/dist/mysql/parse.js +1 -0
- package/dist/postgres/parse.js +1 -0
- package/dist/qualify/template-provider.d.ts +8 -3
- package/dist/qualify/template-provider.js +5 -1
- package/dist/redshift/parse.js +1 -0
- package/dist/snowflake/parse.js +1 -0
- package/dist/sqlite/parse.js +1 -0
- package/dist/template/engine.d.ts +38 -1
- package/dist/trino/parse.js +1 -0
- package/dist/tsql/parse.js +1 -0
- package/package.json +1 -1
package/dist/bigquery/parse.js
CHANGED
|
@@ -85,6 +85,7 @@ export const fragmentGrammar = defineFragmentGrammar({
|
|
|
85
85
|
newLexer: (input) => new GoogleSQLLexer(input),
|
|
86
86
|
newParser: (tokens) => new GoogleSQLParser(tokens),
|
|
87
87
|
postParse: postParseDiagnostics,
|
|
88
|
+
separator: GoogleSQLLexer.COMMA_SYMBOL,
|
|
88
89
|
entries: {
|
|
89
90
|
statement: (p) => p.root(),
|
|
90
91
|
expression: (p) => p.expression(),
|
package/dist/databricks/parse.js
CHANGED
|
@@ -79,6 +79,7 @@ function attachErrorCounter(lexer, parser, listener) {
|
|
|
79
79
|
export const fragmentGrammar = defineFragmentGrammar({
|
|
80
80
|
newLexer: (input) => new DatabricksLexer(input),
|
|
81
81
|
newParser: (tokens) => new DatabricksParser(tokens),
|
|
82
|
+
separator: DatabricksLexer.COMMA,
|
|
82
83
|
entries: {
|
|
83
84
|
statement: (p) => p.multiStatement(),
|
|
84
85
|
expression: (p) => p.expression(),
|
package/dist/duckdb/parse.js
CHANGED
|
@@ -70,6 +70,7 @@ function attachErrorCounter(lexer, parser, listener) {
|
|
|
70
70
|
export const fragmentGrammar = defineFragmentGrammar({
|
|
71
71
|
newLexer: (input) => new DuckdbLexer(input),
|
|
72
72
|
newParser: (tokens) => new DuckdbParser(tokens),
|
|
73
|
+
separator: DuckdbLexer.COMMA,
|
|
73
74
|
entries: {
|
|
74
75
|
statement: (p) => p.root(),
|
|
75
76
|
expression: (p) => p.a_expr(),
|
|
@@ -38,6 +38,9 @@ export interface FragmentGrammarSpec<P extends Parser> {
|
|
|
38
38
|
entries: {
|
|
39
39
|
readonly [K in FragmentKind]: FragmentEntry<P>;
|
|
40
40
|
};
|
|
41
|
+
/** The list separator token type (COMMA). A list body (`cteList`/`selectList`) may end with
|
|
42
|
+
* one: a macro's CTE list often ends `),` because the caller appends more CTEs. */
|
|
43
|
+
separator: number;
|
|
41
44
|
/** Tree-walking checks the dialect's statement entry runs after the parse (bigquery). */
|
|
42
45
|
postParse?: (tree: ParserRuleContext) => SyntaxDiagnostic[];
|
|
43
46
|
}
|
package/dist/fragment-grammar.js
CHANGED
|
@@ -19,12 +19,14 @@ export const FRAGMENT_KINDS = [
|
|
|
19
19
|
"cteList",
|
|
20
20
|
"selectList",
|
|
21
21
|
];
|
|
22
|
+
const LIST_KINDS = new Set(["cteList", "selectList"]);
|
|
22
23
|
/** `item (sep item)*` driven from outside the grammar, for dialects without a list rule
|
|
23
24
|
* (a CTE list without its WITH, a select list). The tree is the FIRST item's. */
|
|
24
25
|
export function separatedList(item, separator) {
|
|
25
26
|
return (parser) => {
|
|
26
27
|
const first = item(parser);
|
|
27
|
-
|
|
28
|
+
// A separator followed by EOF is a trailing one (`run` consumes it), not another item.
|
|
29
|
+
while (parser.inputStream.LA(1) === separator && parser.inputStream.LA(2) !== AntlrToken.EOF) {
|
|
28
30
|
parser.inputStream.consume(); // between rules there is no context to attach the separator to
|
|
29
31
|
item(parser);
|
|
30
32
|
}
|
|
@@ -48,7 +50,15 @@ function trailingInput(parser) {
|
|
|
48
50
|
}
|
|
49
51
|
/** Bind a dialect's lexer, parser and fragment entry rules into a `FragmentGrammar`. */
|
|
50
52
|
export function defineFragmentGrammar(spec) {
|
|
51
|
-
const { newLexer, newParser, entries, postParse } = spec;
|
|
53
|
+
const { newLexer, newParser, entries, separator, postParse } = spec;
|
|
54
|
+
/** The entry, then a trailing separator on a list body is consumed rather than left over. */
|
|
55
|
+
const run = (parser, kind) => {
|
|
56
|
+
const tree = entries[kind](parser);
|
|
57
|
+
const input = parser.inputStream;
|
|
58
|
+
if (LIST_KINDS.has(kind) && input.LA(1) === separator && input.LA(2) === AntlrToken.EOF)
|
|
59
|
+
input.consume();
|
|
60
|
+
return tree;
|
|
61
|
+
};
|
|
52
62
|
const lex = spec.lex ??
|
|
53
63
|
((text) => {
|
|
54
64
|
const lexer = newLexer(CharStream.fromString(text));
|
|
@@ -69,7 +79,7 @@ export function defineFragmentGrammar(spec) {
|
|
|
69
79
|
sim.predictionMode = PredictionMode.SLL;
|
|
70
80
|
let tree;
|
|
71
81
|
try {
|
|
72
|
-
tree =
|
|
82
|
+
tree = run(parser, kind);
|
|
73
83
|
}
|
|
74
84
|
catch {
|
|
75
85
|
return undefined;
|
|
@@ -82,7 +92,7 @@ export function defineFragmentGrammar(spec) {
|
|
|
82
92
|
return post.length === 0 ? { tree, diagnostics: [] } : undefined;
|
|
83
93
|
}
|
|
84
94
|
sim.predictionMode = PredictionMode.LL;
|
|
85
|
-
const tree =
|
|
95
|
+
const tree = run(parser, kind);
|
|
86
96
|
const trailing = trailingInput(parser);
|
|
87
97
|
return {
|
|
88
98
|
tree,
|
package/dist/fragment.d.ts
CHANGED
|
@@ -24,6 +24,9 @@ export interface FragmentSession {
|
|
|
24
24
|
* verdict). Never throws: every attempt runs a recovering parser over a fixed kind list.
|
|
25
25
|
*/
|
|
26
26
|
parse(ranges: readonly FragmentRange[], kinds?: readonly FragmentKind[]): FragmentResult | undefined;
|
|
27
|
+
/** The first kind the ranges parse clean as, or undefined (no token, or no clean reading).
|
|
28
|
+
* Probes only (SLL + bail); never a diagnostic. */
|
|
29
|
+
verdict(ranges: readonly FragmentRange[], kinds?: readonly FragmentKind[]): FragmentKind | undefined;
|
|
27
30
|
}
|
|
28
31
|
/** Lex `text` once with `dialect`'s statement-entry token pipeline; parse ranges of it after. */
|
|
29
32
|
export declare function openFragments(text: string, dialect: Dialect): FragmentSession;
|
package/dist/fragment.js
CHANGED
|
@@ -41,19 +41,36 @@ export function openFragments(text, dialect) {
|
|
|
41
41
|
const grammar = FRAGMENT_GRAMMARS[dialect];
|
|
42
42
|
const lexed = grammar.lex(text);
|
|
43
43
|
const inRanges = (offset, ranges) => ranges.some((r) => offset >= r.start && offset < r.end);
|
|
44
|
+
const sliceOf = (ranges) => {
|
|
45
|
+
const slice = lexed.tokens.filter((t) => inRanges(t.start, ranges));
|
|
46
|
+
return slice.some((t) => t.channel === 0) ? slice : undefined;
|
|
47
|
+
};
|
|
48
|
+
// Token-derived diagnostics of a slice (bigquery literal escapes) hold under every reading.
|
|
49
|
+
const lexDiagsOf = (ranges) => lexed.diagnostics.filter((d) => d.offset !== undefined && inRanges(d.offset, ranges));
|
|
50
|
+
const probe = (slice, kinds) => {
|
|
51
|
+
for (const kind of kinds) {
|
|
52
|
+
const clean = grammar.parse(slice, kind, true);
|
|
53
|
+
if (clean)
|
|
54
|
+
return { kind, clean: true, tree: clean.tree, diagnostics: [] };
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
};
|
|
44
58
|
return {
|
|
59
|
+
verdict(ranges, kinds = FRAGMENT_KINDS) {
|
|
60
|
+
const slice = sliceOf(ranges);
|
|
61
|
+
if (!slice || lexDiagsOf(ranges).length > 0)
|
|
62
|
+
return undefined;
|
|
63
|
+
return probe(slice, kinds)?.kind;
|
|
64
|
+
},
|
|
45
65
|
parse(ranges, kinds = FRAGMENT_KINDS) {
|
|
46
|
-
const slice =
|
|
47
|
-
if (!slice
|
|
66
|
+
const slice = sliceOf(ranges);
|
|
67
|
+
if (!slice)
|
|
48
68
|
return undefined;
|
|
49
|
-
|
|
50
|
-
const lexDiags = lexed.diagnostics.filter((d) => d.offset !== undefined && inRanges(d.offset, ranges));
|
|
69
|
+
const lexDiags = lexDiagsOf(ranges);
|
|
51
70
|
if (lexDiags.length === 0) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return { kind, clean: true, tree: clean.tree, diagnostics: [] };
|
|
56
|
-
}
|
|
71
|
+
const clean = probe(slice, kinds);
|
|
72
|
+
if (clean)
|
|
73
|
+
return clean;
|
|
57
74
|
}
|
|
58
75
|
let best;
|
|
59
76
|
let bestAt = -1;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { minijinja } from "./engine.js";
|
|
2
|
-
export { parseTemplated, tokenizeTemplated } from "./parse.js";
|
|
2
|
+
export { parseTemplated, tokenizeTemplated, shapesForCall } from "./parse.js";
|
|
3
|
+
export type { MacroShape } from "../template/engine.js";
|
|
3
4
|
export type { TemplatedParseResult, TemplatedParseOptions } from "../template/engine.js";
|
|
4
5
|
export type { TagNode, MacroCall } from "./parse.js";
|
|
5
6
|
export { templateRegions, templateSymbols } from "./regions.js";
|
package/dist/minijinja/index.js
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
// canonically declared in src/template/engine.ts and re-exported both there
|
|
4
4
|
// and here.
|
|
5
5
|
export { minijinja } from "./engine.js";
|
|
6
|
-
export { parseTemplated, tokenizeTemplated } from "./parse.js";
|
|
6
|
+
export { parseTemplated, tokenizeTemplated, shapesForCall } from "./parse.js";
|
|
7
7
|
export { templateRegions, templateSymbols } from "./regions.js";
|
|
8
8
|
export { templateVariants } from "./variants.js";
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import type { Dialect } from "../dialect.js";
|
|
2
2
|
import type { Token } from "../token/token.js";
|
|
3
|
-
import type { TemplatedParseOptions, TemplatedParseResult } from "../template/engine.js";
|
|
3
|
+
import type { MacroShape, TemplatedParseOptions, TemplatedParseResult } from "../template/engine.js";
|
|
4
|
+
import type { ExpansionShape } from "../qualify/template-provider.js";
|
|
5
|
+
import type { TemplateCall } from "../ir/ir.js";
|
|
4
6
|
export type { TagNode, MacroCall } from "./tag-ast.js";
|
|
5
7
|
export type { TemplateRegion, TemplateArm, TemplateSymbol } from "./regions.js";
|
|
6
8
|
export { templateRegions, templateSymbols } from "./regions.js";
|
|
7
|
-
export type { TemplatedParseOptions, TemplatedParseResult } from "../template/engine.js";
|
|
9
|
+
export type { MacroShape, TemplatedParseOptions, TemplatedParseResult } from "../template/engine.js";
|
|
10
|
+
/**
|
|
11
|
+
* The shapes a specific CALL of a macro takes: `macro.shapes`, with the keyword-parameter hole
|
|
12
|
+
* (if any) resolved from the call's own literal argument (positional or keyword) or the
|
|
13
|
+
* parameter's default. A call whose keyword is not a literal, or names a word that opens no
|
|
14
|
+
* known clause, resolves to nothing for that hole (never-wrong). Pure: definition text + call
|
|
15
|
+
* text, no project knowledge; a host's `shapeOf(call)` is `shapesForCall(index.get(call.name), call)`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function shapesForCall(macro: MacroShape, call: TemplateCall): ExpansionShape[];
|
|
8
18
|
/**
|
|
9
19
|
* Parse raw jinja-SQL: one whole-document jinja lex (segment()), the untouched
|
|
10
20
|
* per-dialect SQL parse over the resulting placeholder, a per-tag jinja parse over
|
package/dist/minijinja/parse.js
CHANGED
|
@@ -202,28 +202,49 @@ function parseSliceTag(slice) {
|
|
|
202
202
|
const tree = parser.tag();
|
|
203
203
|
return { tree, diagnostics: collector.diagnostics };
|
|
204
204
|
}
|
|
205
|
+
/** A pure placeholder-fill run: `j` + up to two base-35 ordinal chars + `j` padding (segment.ts). */
|
|
206
|
+
const FILL_RUN = /^j[0-9a-ik-z]{0,2}j*$/;
|
|
205
207
|
/**
|
|
206
|
-
* Scrub placeholder gibberish out of SQL syntax diagnostics.
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
208
|
+
* Scrub placeholder gibberish out of SQL syntax diagnostics. Two parts:
|
|
209
|
+
* - every message: each placeholder FILL RUN of any tag is rewritten to that tag's original
|
|
210
|
+
* source text. ANTLR's "no viable alternative at input '…'" quotes a whole token RANGE, so a
|
|
211
|
+
* fill can sit inside a message whose offending token is plain SQL far away; fills are
|
|
212
|
+
* ordinal-unique, so plain substring replacement (longest fill first, a shorter fill can be
|
|
213
|
+
* a prefix of a longer one) is exact;
|
|
214
|
+
* - a diagnostic whose offending token starts inside a tag range is really complaining about
|
|
215
|
+
* the TAG: its offset/length (+ line/column) widen to the whole tag.
|
|
216
|
+
* `bySegment` carries the widened diagnostics keyed by the owning tag segment — build() maps
|
|
217
|
+
* that back to a TagNode for `diagnosticsOf`.
|
|
213
218
|
*/
|
|
214
219
|
function scrubPlaceholderDiagnostics(diags, tagRanges, text, placeholder) {
|
|
215
220
|
const bySegment = new Map();
|
|
216
221
|
if (tagRanges.length === 0)
|
|
217
222
|
return { diagnostics: diags, bySegment };
|
|
223
|
+
// fill run → the tag's source text, longest fill first.
|
|
224
|
+
const fills = [];
|
|
225
|
+
for (const tag of tagRanges) {
|
|
226
|
+
const tagText = text.slice(tag.start, tag.end);
|
|
227
|
+
for (const run of placeholder.slice(tag.start, tag.end).split(/\s+/)) {
|
|
228
|
+
if (run.length >= 3 && FILL_RUN.test(run))
|
|
229
|
+
fills.push([run, tagText]);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
fills.sort((a, b) => b[0].length - a[0].length);
|
|
233
|
+
const scrubMessage = (message) => {
|
|
234
|
+
for (const [run, tagText] of fills)
|
|
235
|
+
if (message.includes(run))
|
|
236
|
+
message = message.split(run).join(tagText);
|
|
237
|
+
return message;
|
|
238
|
+
};
|
|
218
239
|
const diagnostics = diags.map((d) => {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
240
|
+
const tag = d.offset === undefined ? undefined : tagRanges.find((s) => d.offset >= s.start && d.offset < s.end);
|
|
241
|
+
if (!tag) {
|
|
242
|
+
const message = scrubMessage(d.message);
|
|
243
|
+
return message === d.message ? d : { ...d, message };
|
|
244
|
+
}
|
|
224
245
|
const seen = placeholder.slice(d.offset, d.offset + d.length);
|
|
225
246
|
const tagText = text.slice(tag.start, tag.end);
|
|
226
|
-
const message = seen.length > 0 ? d.message.split(`'${seen}'`).join(`'${tagText}'`) : d.message;
|
|
247
|
+
const message = scrubMessage(seen.length > 0 ? d.message.split(`'${seen}'`).join(`'${tagText}'`) : d.message);
|
|
227
248
|
const pos = docPosAt(text, tag.start);
|
|
228
249
|
const widened = {
|
|
229
250
|
...d,
|
|
@@ -247,10 +268,12 @@ function scrubPlaceholderDiagnostics(diags, tagRanges, text, placeholder) {
|
|
|
247
268
|
* diagnostics are replaced wholesale: they are recovery noise once a body has derailed it
|
|
248
269
|
* (a body that is a CASE expression leaves "missing 'CASE' at EOF" far outside itself).
|
|
249
270
|
* What replaces them: the text OUTSIDE the macro bodies read as a statement batch (a model
|
|
250
|
-
* file that also defines a macro keeps its real errors)
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
271
|
+
* file that also defines a macro keeps its real errors). Lexer diagnostics (offset-less)
|
|
272
|
+
* are kept as they are. Each region learns what its body is (`body`); a body that reads
|
|
273
|
+
* clean as none of the kinds carries NO diagnostic and no verdict: a body is whatever gets
|
|
274
|
+
* pasted at the call site (a clause tail led by a keyword hole has no reading and never
|
|
275
|
+
* will), so "matches no known shape" is not evidence of invalid SQL (never-wrong). Mutates
|
|
276
|
+
* the regions' `body` field only. Files without a macro region are untouched.
|
|
254
277
|
*/
|
|
255
278
|
function reparseMacroBodies(regions, diagnostics, placeholder, dialect) {
|
|
256
279
|
const macros = [];
|
|
@@ -270,7 +293,6 @@ function reparseMacroBodies(regions, diagnostics, placeholder, dialect) {
|
|
|
270
293
|
return diagnostics;
|
|
271
294
|
const fragments = openFragments(placeholder, dialect);
|
|
272
295
|
const bodies = [];
|
|
273
|
-
const own = [];
|
|
274
296
|
for (const region of macros) {
|
|
275
297
|
const arm = region.arms[0];
|
|
276
298
|
if (!arm)
|
|
@@ -282,12 +304,9 @@ function reparseMacroBodies(regions, diagnostics, placeholder, dialect) {
|
|
|
282
304
|
if (body.end <= body.start)
|
|
283
305
|
continue;
|
|
284
306
|
bodies.push(body);
|
|
285
|
-
const
|
|
286
|
-
if (
|
|
287
|
-
|
|
288
|
-
own.push(...fragment.diagnostics);
|
|
289
|
-
if (fragment.clean)
|
|
290
|
-
region.body = fragment.kind;
|
|
307
|
+
const verdict = fragments.verdict([body]);
|
|
308
|
+
if (verdict)
|
|
309
|
+
region.body = verdict;
|
|
291
310
|
}
|
|
292
311
|
// The remainder: everything between the bodies, as the statement batch it always was.
|
|
293
312
|
const remainder = [];
|
|
@@ -300,7 +319,153 @@ function reparseMacroBodies(regions, diagnostics, placeholder, dialect) {
|
|
|
300
319
|
if (at < placeholder.length)
|
|
301
320
|
remainder.push({ start: at, end: placeholder.length });
|
|
302
321
|
const rest = fragments.parse(remainder, ["statement"]);
|
|
303
|
-
return [...diagnostics.filter((d) => d.offset === undefined), ...(rest?.diagnostics ?? [])
|
|
322
|
+
return [...diagnostics.filter((d) => d.offset === undefined), ...(rest?.diagnostics ?? [])];
|
|
323
|
+
}
|
|
324
|
+
/** Insert a hidden WS-shaped token over every gap in the source-ordered stream whose original text
|
|
325
|
+
* is not pure whitespace (see the call site). Mutates `tokens` in place, keeping it sorted. */
|
|
326
|
+
function fillDeadGaps(tokens, text) {
|
|
327
|
+
const out = [];
|
|
328
|
+
let at = 0;
|
|
329
|
+
const fill = (start, end) => {
|
|
330
|
+
const slice = text.slice(start, end);
|
|
331
|
+
if (slice.trim().length === 0)
|
|
332
|
+
return;
|
|
333
|
+
const pos = docPosAt(text, start);
|
|
334
|
+
const endPos = endPosition(pos.line, pos.column, slice);
|
|
335
|
+
out.push({
|
|
336
|
+
type: 0, // antlr's INVALID_TYPE: no lexer rule produced this token
|
|
337
|
+
name: "WS",
|
|
338
|
+
text: slice,
|
|
339
|
+
start,
|
|
340
|
+
stop: end - 1,
|
|
341
|
+
line: pos.line,
|
|
342
|
+
column: pos.column,
|
|
343
|
+
endLine: endPos.endLine,
|
|
344
|
+
endColumn: endPos.endColumn,
|
|
345
|
+
channel: 1,
|
|
346
|
+
role: "whitespace",
|
|
347
|
+
});
|
|
348
|
+
};
|
|
349
|
+
for (const tok of tokens) {
|
|
350
|
+
if (tok.start > at)
|
|
351
|
+
fill(at, tok.start);
|
|
352
|
+
out.push(tok);
|
|
353
|
+
at = Math.max(at, tok.stop + 1);
|
|
354
|
+
}
|
|
355
|
+
if (at < text.length)
|
|
356
|
+
fill(at, text.length);
|
|
357
|
+
if (out.length !== tokens.length)
|
|
358
|
+
tokens.splice(0, tokens.length, ...out);
|
|
359
|
+
}
|
|
360
|
+
/** Fragment verdict → the provider's shape vocabulary (`MacroShape.shapes`). */
|
|
361
|
+
const VERDICT_SHAPE = {
|
|
362
|
+
statement: "statement",
|
|
363
|
+
expression: "expr",
|
|
364
|
+
tableSource: "relation",
|
|
365
|
+
cteList: "cte-definition",
|
|
366
|
+
selectList: "column-list",
|
|
367
|
+
};
|
|
368
|
+
/** The keyword a leading hole's jinja `default('…')` filter names → the clause shape it opens. */
|
|
369
|
+
const DEFAULT_KEYWORD_SHAPE = {
|
|
370
|
+
where: "where-clause",
|
|
371
|
+
and: "conjunct",
|
|
372
|
+
or: "conjunct",
|
|
373
|
+
};
|
|
374
|
+
/**
|
|
375
|
+
* `MacroShape` for every macro region, read from the text alone. Three sources, all in-text:
|
|
376
|
+
* 1. the body's fragment verdict (`region.body`), mapped 1:1;
|
|
377
|
+
* 2. a body led by a `{{ x|default('where') }}`-style hole: the default literal is the
|
|
378
|
+
* keyword the body opens with (`where` → where-clause, `and`/`or` → conjunct);
|
|
379
|
+
* 3. control flow: when every SQL byte of the body sits under `if` regions that have no
|
|
380
|
+
* `else` arm, the macro can render to nothing → `nothing`, last.
|
|
381
|
+
* Anything else stays out (never-wrong): a hole with no visible default, a return-only body.
|
|
382
|
+
*/
|
|
383
|
+
function macroShapesOf(regions, tags, text, placeholder) {
|
|
384
|
+
const out = [];
|
|
385
|
+
const visit = (list) => {
|
|
386
|
+
for (const region of list) {
|
|
387
|
+
if (region.kind !== "macro") {
|
|
388
|
+
for (const arm of region.arms)
|
|
389
|
+
visit(arm.children);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const arm = region.arms[0];
|
|
393
|
+
const open = tags.find((t) => t.kind === "control" && t.keyword === "macro" && t.tagSpan.start === region.span.start);
|
|
394
|
+
if (!arm || !open?.name || !open.nameSpan)
|
|
395
|
+
continue;
|
|
396
|
+
const shapes = [];
|
|
397
|
+
let keywordParam;
|
|
398
|
+
if (region.body)
|
|
399
|
+
shapes.push(VERDICT_SHAPE[region.body]);
|
|
400
|
+
else {
|
|
401
|
+
// A body opening with a hole: `{{ name }}` / `{{ name|default('kw') }}`, `name` one of
|
|
402
|
+
// the macro's declared parameters (the signature's args are bare identifiers).
|
|
403
|
+
const lead = leadingHole(arm.bodySpan, tags, placeholder);
|
|
404
|
+
const holeText = lead ? text.slice(lead.tagSpan.start, lead.tagSpan.end) : "";
|
|
405
|
+
const bound = /^\{\{-?\s*([A-Za-z_]\w*)\s*(?:\||-?\}\})/.exec(holeText)?.[1];
|
|
406
|
+
const params = (open.calls[0]?.args ?? []).map((a) => text.slice(a.span.start, a.span.end).trim());
|
|
407
|
+
const index = bound === undefined ? -1 : params.indexOf(bound);
|
|
408
|
+
const fallback = /\|\s*default\(\s*['"](\w+)['"]\s*\)/.exec(holeText)?.[1];
|
|
409
|
+
if (bound !== undefined && index >= 0) {
|
|
410
|
+
keywordParam = { name: bound, index, ...(fallback !== undefined ? { default: fallback } : {}) };
|
|
411
|
+
}
|
|
412
|
+
const clause = fallback ? DEFAULT_KEYWORD_SHAPE[fallback.toLowerCase()] : undefined;
|
|
413
|
+
if (clause)
|
|
414
|
+
shapes.push(clause);
|
|
415
|
+
}
|
|
416
|
+
if ((shapes.length > 0 || keywordParam) && rendersToNothing(arm, placeholder))
|
|
417
|
+
shapes.push("nothing");
|
|
418
|
+
out.push({
|
|
419
|
+
name: open.name,
|
|
420
|
+
nameSpan: open.nameSpan,
|
|
421
|
+
span: region.span,
|
|
422
|
+
shapes,
|
|
423
|
+
...(keywordParam ? { keywordParam } : {}),
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
visit(regions);
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* The shapes a specific CALL of a macro takes: `macro.shapes`, with the keyword-parameter hole
|
|
432
|
+
* (if any) resolved from the call's own literal argument (positional or keyword) or the
|
|
433
|
+
* parameter's default. A call whose keyword is not a literal, or names a word that opens no
|
|
434
|
+
* known clause, resolves to nothing for that hole (never-wrong). Pure: definition text + call
|
|
435
|
+
* text, no project knowledge; a host's `shapeOf(call)` is `shapesForCall(index.get(call.name), call)`.
|
|
436
|
+
*/
|
|
437
|
+
export function shapesForCall(macro, call) {
|
|
438
|
+
const kp = macro.keywordParam;
|
|
439
|
+
if (!kp)
|
|
440
|
+
return macro.shapes;
|
|
441
|
+
const kwarg = call.kwargs?.find((k) => k.name === kp.name)?.value;
|
|
442
|
+
const positional = call.args[kp.index];
|
|
443
|
+
const word = (kwarg ?? positional ?? kp.default)?.toLowerCase();
|
|
444
|
+
const clause = word !== undefined ? DEFAULT_KEYWORD_SHAPE[word] : undefined;
|
|
445
|
+
const rest = macro.shapes.filter((s) => s !== "where-clause" && s !== "conjunct");
|
|
446
|
+
return clause ? [clause, ...rest] : rest;
|
|
447
|
+
}
|
|
448
|
+
/** The expression tag at the very start of a body (only whitespace, comments and control tags
|
|
449
|
+
* before it in the placeholder), or undefined when the body opens with SQL. */
|
|
450
|
+
function leadingHole(body, tags, placeholder) {
|
|
451
|
+
const first = placeholder.slice(body.start, body.end).search(/\S/);
|
|
452
|
+
if (first === -1)
|
|
453
|
+
return undefined;
|
|
454
|
+
const at = body.start + first;
|
|
455
|
+
return tags.find((t) => (t.kind === "call" || t.kind === "other") && t.tagSpan.start <= at && at < t.tagSpan.end);
|
|
456
|
+
}
|
|
457
|
+
/** True when every non-whitespace placeholder byte of the arm's body lies inside an `if` child
|
|
458
|
+
* region that has no `else` arm: nothing outside such regions, so the macro can render empty. */
|
|
459
|
+
function rendersToNothing(arm, placeholder) {
|
|
460
|
+
const optional = arm.children.filter((c) => c.kind === "if" && !c.arms.some((a) => a.keyword === "else"));
|
|
461
|
+
if (optional.length === 0)
|
|
462
|
+
return false;
|
|
463
|
+
const chars = placeholder.slice(arm.bodySpan.start, arm.bodySpan.end).split("");
|
|
464
|
+
for (const c of optional) {
|
|
465
|
+
for (let k = c.span.start; k < c.span.end; k++)
|
|
466
|
+
chars[k - arm.bodySpan.start] = " ";
|
|
467
|
+
}
|
|
468
|
+
return chars.join("").trim().length === 0;
|
|
304
469
|
}
|
|
305
470
|
/** The core build — total by construction (every composed piece is total). */
|
|
306
471
|
function build(text, dialect, provider) {
|
|
@@ -365,6 +530,12 @@ function build(text, dialect, provider) {
|
|
|
365
530
|
// disjoint (tag-contained SQL tokens were dropped), so a stable sort by start
|
|
366
531
|
// (stop as tiebreak) tiles the source.
|
|
367
532
|
const tokens = [...sqlTokens, ...jinjaTokens].sort((a, b) => a.start - b.start || a.stop - b.stop);
|
|
533
|
+
// Dead text with no carrier: the statically-dead loop arm above rides as trivia ONLY when an SQL
|
|
534
|
+
// token covers its blanked span. A dialect whose whitespace rule is `-> skip` (tsql) lexes no
|
|
535
|
+
// token over an all-space span, so the arm's true text (`union all`, a trailing `,`) would fall
|
|
536
|
+
// out of the stream. Every uncovered gap holding non-whitespace source text gets a synthesized
|
|
537
|
+
// hidden trivia token carrying that text, the same shape the carrier token takes elsewhere.
|
|
538
|
+
fillDeadGaps(tokens, text);
|
|
368
539
|
// Diagnostics: SQL + jinja, both already in document coordinates, source-ordered
|
|
369
540
|
// so squiggles line up with the merged stream. SQL diagnostics whose offending
|
|
370
541
|
// token is a placeholder fill are scrubbed first — the message quotes the ORIGINAL
|
|
@@ -385,6 +556,7 @@ function build(text, dialect, provider) {
|
|
|
385
556
|
// the statement parse reported inside that body. The IR and tokens stay the whole-file
|
|
386
557
|
// parse's: the fragment verdict rides the region as `body`.
|
|
387
558
|
const sqlDiagnostics = reparseMacroBodies(regions, sqlResult.diagnostics, placeholder, dialect);
|
|
559
|
+
const macros = macroShapesOf(regions, tags, text, placeholder);
|
|
388
560
|
const { diagnostics: scrubbed, bySegment } = scrubPlaceholderDiagnostics(sqlDiagnostics, tagRanges, text, placeholder);
|
|
389
561
|
// Fold the scrubbed SQL diagnostics into the same per-tag map as the jinja ones
|
|
390
562
|
// (Task 10) — a tag's diagnostics are its own jinja parse errors PLUS whatever
|
|
@@ -407,6 +579,7 @@ function build(text, dialect, provider) {
|
|
|
407
579
|
tags,
|
|
408
580
|
regions,
|
|
409
581
|
symbols,
|
|
582
|
+
macros,
|
|
410
583
|
diagnostics,
|
|
411
584
|
placeholder,
|
|
412
585
|
tagOf: (node) => correlation.byNode.get(node),
|
|
@@ -436,6 +609,7 @@ export function parseTemplated(text, dialect, opts) {
|
|
|
436
609
|
tags: [],
|
|
437
610
|
regions: [],
|
|
438
611
|
symbols: [],
|
|
612
|
+
macros: [],
|
|
439
613
|
diagnostics: sql.diagnostics,
|
|
440
614
|
placeholder: text,
|
|
441
615
|
degraded: true,
|
|
@@ -568,7 +568,11 @@ export function segment(text, provider) {
|
|
|
568
568
|
// ONE provider consult per tag — the uniform seam. stmt/comment tags are pure
|
|
569
569
|
// jinja control text and always whitespace-fill (no consult needed).
|
|
570
570
|
const exp = seg.tagKind === "expr" && info.call ? provider.expansion(info.call) : undefined;
|
|
571
|
-
|
|
571
|
+
// Candidate shapes, most specific first. The first one the slot admits fills the tag;
|
|
572
|
+
// `nothing`/`expr` never fail admission, so a list ends with one of them or falls through
|
|
573
|
+
// to the identifier fill like a single unadmitted shape does.
|
|
574
|
+
const candidates = exp?.shapes ?? (exp?.shape !== undefined ? [exp.shape] : []);
|
|
575
|
+
let shape = candidates[0];
|
|
572
576
|
// Fusion boundary (anvil torture-corpus, 2026-07-06): a tag GLUED to a preceding SQL
|
|
573
577
|
// clause/operator keyword (`from{{ ref('x') }}` — dbt compiles it because the rendered
|
|
574
578
|
// relation opens with a quote char) must not fuse with the fill into one token: the old
|
|
@@ -595,19 +599,25 @@ export function segment(text, provider) {
|
|
|
595
599
|
// comma is omitted. Anything else — another CTE name, or "" (ambiguous) — keeps it, which
|
|
596
600
|
// is exactly the already-working "another CTE follows" behavior, unchanged.
|
|
597
601
|
let shaped;
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
+
for (const candidate of candidates) {
|
|
603
|
+
shape = candidate;
|
|
604
|
+
if (candidate === "cte-definition") {
|
|
605
|
+
if (info.isCall && cteDefinitionSlotAdmits(slot)) {
|
|
606
|
+
const needsComma = !QUERY_START_WORDS.has(followingSlot(chars, seg.end));
|
|
607
|
+
shaped = fitWindow(seg, `${PLACEHOLDER_CHAR}${ordinalFill(ordinal)} as (select 1)${needsComma ? "," : ""}`);
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
shaped = undefined;
|
|
611
|
+
}
|
|
612
|
+
if (shaped !== undefined)
|
|
613
|
+
ordinal += 1;
|
|
602
614
|
}
|
|
603
615
|
else {
|
|
604
|
-
shaped =
|
|
616
|
+
shaped = fragmentFill(seg, candidate, info.isCall, slot);
|
|
605
617
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
else {
|
|
610
|
-
shaped = shape !== undefined ? fragmentFill(seg, shape, info.isCall, slot) : undefined;
|
|
618
|
+
// Admitted (a fragment landed), or a shape whose fill is positional and never refused.
|
|
619
|
+
if (shaped !== undefined || candidate === "nothing" || candidate === "expr")
|
|
620
|
+
break;
|
|
611
621
|
}
|
|
612
622
|
if (shaped !== undefined) {
|
|
613
623
|
// Fragment fill: at the fit window's start (`at` — tag start for a one-line tag, the
|
package/dist/mysql/parse.js
CHANGED
|
@@ -76,6 +76,7 @@ function attachErrorCounter(lexer, parser, listener) {
|
|
|
76
76
|
export const fragmentGrammar = defineFragmentGrammar({
|
|
77
77
|
newLexer: (input) => new MysqlLexer(input),
|
|
78
78
|
newParser: (tokens) => new MysqlParser(tokens),
|
|
79
|
+
separator: MysqlLexer.COMMA,
|
|
79
80
|
entries: {
|
|
80
81
|
statement: (p) => p.root(),
|
|
81
82
|
expression: (p) => p.expression(),
|
package/dist/postgres/parse.js
CHANGED
|
@@ -75,6 +75,7 @@ function attachErrorCounter(lexer, parser, listener) {
|
|
|
75
75
|
export const fragmentGrammar = defineFragmentGrammar({
|
|
76
76
|
newLexer: (input) => new PostgresLexer(input),
|
|
77
77
|
newParser: (tokens) => new PostgresParser(tokens),
|
|
78
|
+
separator: PostgresLexer.COMMA,
|
|
78
79
|
entries: {
|
|
79
80
|
statement: (p) => p.root(),
|
|
80
81
|
expression: (p) => p.a_expr(),
|
|
@@ -48,8 +48,13 @@ export interface TemplateCandidate {
|
|
|
48
48
|
/** Everything known about one call's expansion. Every field optional; `undefined` = unknown. */
|
|
49
49
|
export interface ResolvedExpansion {
|
|
50
50
|
/** Parse-time shape. When absent, derived from the strongest present field:
|
|
51
|
-
* relation → "relation", columns → "column-list", value → "expr" (an explicit shape always wins).
|
|
51
|
+
* relation → "relation", columns → "column-list", value → "expr" (an explicit shape always wins).
|
|
52
|
+
* With `shapes` present this is `shapes[0]`. */
|
|
52
53
|
shape?: ExpansionShape;
|
|
54
|
+
/** Every shape the call can take, most specific first (a macro whose body is an `if` arm with
|
|
55
|
+
* no `else` is `["conjunct", "nothing"]`). The segmenter takes the first one the slot admits;
|
|
56
|
+
* `nothing` and `expr` are always admitted, so they belong last. Absent = `shape` alone. */
|
|
57
|
+
shapes?: ExpansionShape[];
|
|
53
58
|
/** A relation-producing call (ref, source, a TVF-like macro). */
|
|
54
59
|
relation?: ResolvedRelation;
|
|
55
60
|
/** A scalar value — `{{ var('x') }}`, `{{ env_var('Y') }}`, a scalar macro. */
|
|
@@ -100,7 +105,7 @@ export declare class DefaultTemplateProvider implements SchemaProvider {
|
|
|
100
105
|
/** The rendered-output shape of a call. Neutral floor: unknown (the engine derives a shape from
|
|
101
106
|
* stronger fields, or falls back to its positional fill). The dbt no-output builtins → "nothing"
|
|
102
107
|
* is `DbtTemplateProvider` knowledge. */
|
|
103
|
-
shapeOf(_call: TemplateCall): ExpansionShape | undefined;
|
|
108
|
+
shapeOf(_call: TemplateCall): ExpansionShape | readonly ExpansionShape[] | undefined;
|
|
104
109
|
/** The columns a column-list-producing macro emits. Default: unknown. */
|
|
105
110
|
columnsOf(_call: TemplateCall): Column[] | undefined;
|
|
106
111
|
/** The items a loop collection holds. Default: unknown (loops analyze one representative pass). */
|
|
@@ -167,7 +172,7 @@ export declare class DbtTemplateProvider extends DefaultTemplateProvider {
|
|
|
167
172
|
valueOf(call: TemplateCall): {
|
|
168
173
|
type: ValueType;
|
|
169
174
|
} | undefined;
|
|
170
|
-
shapeOf(call: TemplateCall): ExpansionShape | undefined;
|
|
175
|
+
shapeOf(call: TemplateCall): ExpansionShape | readonly ExpansionShape[] | undefined;
|
|
171
176
|
}
|
|
172
177
|
/** The provider type the engine consults — the shipped base (or any subclass of it, e.g.
|
|
173
178
|
* `DbtTemplateProvider`). */
|
|
@@ -152,7 +152,10 @@ export class DefaultTemplateProvider {
|
|
|
152
152
|
* undefined when nothing at all is known (the engine's zero-knowledge floor).
|
|
153
153
|
*/
|
|
154
154
|
expansion(call) {
|
|
155
|
-
const
|
|
155
|
+
const answered = this.shapeOf(call);
|
|
156
|
+
// A list answer keeps its order; an empty list is no answer at all.
|
|
157
|
+
const shapes = Array.isArray(answered) ? (answered.length > 0 ? [...answered] : undefined) : undefined;
|
|
158
|
+
const shape = shapes ? shapes[0] : answered;
|
|
156
159
|
const relation = this.relationOf(call);
|
|
157
160
|
const value = this.valueOf(call);
|
|
158
161
|
const columns = this.columnsOf(call);
|
|
@@ -162,6 +165,7 @@ export class DefaultTemplateProvider {
|
|
|
162
165
|
return undefined;
|
|
163
166
|
return {
|
|
164
167
|
...(derived !== undefined ? { shape: derived } : {}),
|
|
168
|
+
...(shapes && shapes.length > 1 ? { shapes } : {}),
|
|
165
169
|
...(relation ? { relation } : {}),
|
|
166
170
|
...(value ? { value } : {}),
|
|
167
171
|
...(columns ? { columns } : {}),
|
package/dist/redshift/parse.js
CHANGED
|
@@ -76,6 +76,7 @@ function attachErrorCounter(lexer, parser, listener) {
|
|
|
76
76
|
export const fragmentGrammar = defineFragmentGrammar({
|
|
77
77
|
newLexer: (input) => new RedshiftLexer(input),
|
|
78
78
|
newParser: (tokens) => new RedshiftParser(tokens),
|
|
79
|
+
separator: RedshiftLexer.COMMA,
|
|
79
80
|
entries: {
|
|
80
81
|
statement: (p) => p.root(),
|
|
81
82
|
expression: (p) => p.a_expr(),
|
package/dist/snowflake/parse.js
CHANGED
|
@@ -76,6 +76,7 @@ function attachErrorCounter(lexer, parser, listener) {
|
|
|
76
76
|
export const fragmentGrammar = defineFragmentGrammar({
|
|
77
77
|
newLexer: (input) => new SnowflakeLexer(input),
|
|
78
78
|
newParser: (tokens) => new SnowflakeParser(tokens),
|
|
79
|
+
separator: SnowflakeLexer.COMMA,
|
|
79
80
|
entries: {
|
|
80
81
|
statement: (p) => p.snowflake_file(),
|
|
81
82
|
expression: (p) => p.expr(),
|
package/dist/sqlite/parse.js
CHANGED
|
@@ -76,6 +76,7 @@ function attachErrorCounter(lexer, parser, listener) {
|
|
|
76
76
|
export const fragmentGrammar = defineFragmentGrammar({
|
|
77
77
|
newLexer: (input) => new SqliteLexer(input),
|
|
78
78
|
newParser: (tokens) => new SqliteParser(tokens),
|
|
79
|
+
separator: SqliteLexer.COMMA,
|
|
79
80
|
entries: {
|
|
80
81
|
statement: (p) => p.parse(),
|
|
81
82
|
expression: (p) => p.expr(),
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Dialect } from "../dialect.js";
|
|
2
|
+
import type { PartSpan } from "../ir/part-span.js";
|
|
2
3
|
import type { ParseResultIR } from "../api.js";
|
|
3
4
|
import type { SyntaxDiagnostic } from "../parse-diagnostics.js";
|
|
4
5
|
import type { Token } from "../token/token.js";
|
|
5
|
-
import type { TemplateProvider } from "../qualify/template-provider.js";
|
|
6
|
+
import type { ExpansionShape, TemplateProvider } from "../qualify/template-provider.js";
|
|
6
7
|
import type { TagNode } from "../minijinja/tag-ast.js";
|
|
7
8
|
import type { TemplateRegion, TemplateSymbol } from "../minijinja/regions.js";
|
|
8
9
|
import type { TemplateVariant } from "../minijinja/variants.js";
|
|
@@ -13,6 +14,39 @@ export type { TemplateVariant } from "../minijinja/variants.js";
|
|
|
13
14
|
export interface TemplatedParseOptions {
|
|
14
15
|
provider?: TemplateProvider;
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* What a `{% macro %}` definition's body can stand in for at a call site, read from the
|
|
19
|
+
* definition text alone (no project, no rendering): the body's fragment verdict mapped onto the
|
|
20
|
+
* provider's shape vocabulary, plus what its control flow adds. A host answers `shapeOf(call)`
|
|
21
|
+
* for a call by looking the macro up by name and returning `shapes` as they are.
|
|
22
|
+
*/
|
|
23
|
+
export interface MacroShape {
|
|
24
|
+
/** The declared macro name (`{% macro name(...) %}`). */
|
|
25
|
+
name: string;
|
|
26
|
+
nameSpan: PartSpan;
|
|
27
|
+
/** The whole block, opening tag through `{% endmacro %}`. */
|
|
28
|
+
span: PartSpan;
|
|
29
|
+
/**
|
|
30
|
+
* Every shape the body can take, most specific first; empty when nothing can be established
|
|
31
|
+
* (never-wrong). `expression` → `expr`, `cteList` → `cte-definition`, `selectList` →
|
|
32
|
+
* `column-list`, `tableSource` → `relation`, `statement` → `statement`; a body led by a hole
|
|
33
|
+
* whose jinja `default('where')` / `default('and')` is visible → `where-clause` / `conjunct`;
|
|
34
|
+
* a body whose SQL all sits under an `if` with no `else` adds `nothing` last.
|
|
35
|
+
*/
|
|
36
|
+
shapes: ExpansionShape[];
|
|
37
|
+
/**
|
|
38
|
+
* Present when the body OPENS with a hole bound to one of the macro's own parameters
|
|
39
|
+
* (`{{ stat|default('where') }} {{ col }} = 0`): the clause keyword the body starts with is
|
|
40
|
+
* whatever the caller passes for that parameter (or `default` when the caller passes
|
|
41
|
+
* nothing). `shapesForCall` resolves it per call; `shapes` above carries only the default's
|
|
42
|
+
* shape (or nothing when there is no default).
|
|
43
|
+
*/
|
|
44
|
+
keywordParam?: {
|
|
45
|
+
name: string;
|
|
46
|
+
index: number;
|
|
47
|
+
default?: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
16
50
|
/** The unified result of parsing raw jinja-SQL: one token stream + the SQL parse + tags. */
|
|
17
51
|
export interface TemplatedParseResult {
|
|
18
52
|
/** ONE source-ordered stream: SQL tokens (channel 0) + jinja tokens (channel 2, role "minijinja"). */
|
|
@@ -25,6 +59,9 @@ export interface TemplatedParseResult {
|
|
|
25
59
|
regions: TemplateRegion[];
|
|
26
60
|
/** R4 go-to-def template symbols (set targets / macro names). */
|
|
27
61
|
symbols: TemplateSymbol[];
|
|
62
|
+
/** Every `{% macro %}` definition in the text with the shapes its body can take (see
|
|
63
|
+
* `MacroShape`). Empty when the text defines no macro. */
|
|
64
|
+
macros: MacroShape[];
|
|
28
65
|
/** SQL diagnostics (+ jinja diagnostics from Task 4), positioned in original coordinates. */
|
|
29
66
|
diagnostics: SyntaxDiagnostic[];
|
|
30
67
|
/**
|
package/dist/trino/parse.js
CHANGED
|
@@ -73,6 +73,7 @@ function attachErrorCounter(lexer, parser, listener) {
|
|
|
73
73
|
export const fragmentGrammar = defineFragmentGrammar({
|
|
74
74
|
newLexer: (input) => new TrinoLexer(input),
|
|
75
75
|
newParser: (tokens) => new TrinoParser(tokens),
|
|
76
|
+
separator: TrinoLexer.COMMA,
|
|
76
77
|
entries: {
|
|
77
78
|
statement: (p) => p.root(),
|
|
78
79
|
expression: (p) => p.expression(),
|
package/dist/tsql/parse.js
CHANGED
|
@@ -80,6 +80,7 @@ function attachErrorCounter(lexer, parser, listener) {
|
|
|
80
80
|
export const fragmentGrammar = defineFragmentGrammar({
|
|
81
81
|
newLexer: (input) => new TSqlLexer(input),
|
|
82
82
|
newParser: (tokens) => new TSqlParser(tokens),
|
|
83
|
+
separator: TSqlLexer.COMMA,
|
|
83
84
|
entries: {
|
|
84
85
|
statement: (p) => p.tsql_file(),
|
|
85
86
|
expression: (p) => p.expression(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sqllens",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.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",
|