sqllens 1.5.1 → 1.6.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.
Files changed (63) hide show
  1. package/dist/api.d.ts +2 -2
  2. package/dist/api.js +2 -2
  3. package/dist/bigquery/lower.js +28 -2
  4. package/dist/completion/complete.d.ts +28 -7
  5. package/dist/completion/complete.js +101 -10
  6. package/dist/completion/config.js +25 -4
  7. package/dist/databricks/behavior.js +3 -1
  8. package/dist/databricks/infer.d.ts +4 -0
  9. package/dist/databricks/infer.js +257 -38
  10. package/dist/databricks/lower.js +296 -29
  11. package/dist/debug.d.ts +2 -0
  12. package/dist/debug.js +20 -0
  13. package/dist/dialect-behavior/behavior.d.ts +8 -1
  14. package/dist/dialect-symbols.d.ts +14 -0
  15. package/dist/dialect-symbols.js +34 -0
  16. package/dist/document/document.d.ts +26 -20
  17. package/dist/document/document.js +170 -43
  18. package/dist/document/split.js +3 -1
  19. package/dist/duckdb/lower.js +99 -34
  20. package/dist/generated/databricks/DatabricksParser.js +5852 -5822
  21. package/dist/generated/duckdb/DuckdbParser.js +15042 -15030
  22. package/dist/generated/mysql/MysqlLexer.js +5131 -5128
  23. package/dist/generated/mysql/MysqlParser.js +1616 -1604
  24. package/dist/generated/postgres/PostgresParser.js +2305 -2292
  25. package/dist/generated/redshift/RedshiftLexer.js +3478 -3465
  26. package/dist/generated/redshift/RedshiftParser.js +17788 -17463
  27. package/dist/generated/snowflake/SnowflakeLexer.js +5683 -5673
  28. package/dist/generated/snowflake/SnowflakeParser.js +20361 -19877
  29. package/dist/generated/tsql/TSqlLexer.js +6241 -6178
  30. package/dist/generated/tsql/TSqlParser.js +13255 -11071
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/infer/coerce.d.ts +5 -0
  34. package/dist/infer/coerce.js +39 -4
  35. package/dist/infer/infer.js +163 -17
  36. package/dist/infer/nullability.js +3 -3
  37. package/dist/infer/types.d.ts +11 -0
  38. package/dist/infer/types.js +73 -0
  39. package/dist/ir/ir.d.ts +42 -2
  40. package/dist/ir/walk.js +7 -2
  41. package/dist/lineage/hops.js +10 -3
  42. package/dist/lineage/lineage.js +7 -2
  43. package/dist/minijinja/apply-tags.js +3 -1
  44. package/dist/minijinja/parse.js +3 -1
  45. package/dist/minijinja/variants.js +3 -1
  46. package/dist/mysql/lower.js +36 -8
  47. package/dist/postgres/infer.d.ts +0 -1
  48. package/dist/postgres/infer.js +74 -15
  49. package/dist/postgres/lower.js +79 -12
  50. package/dist/qualify/check-calls.js +8 -2
  51. package/dist/qualify/qualify.js +3 -1
  52. package/dist/redshift/lower.js +111 -13
  53. package/dist/references/references.js +46 -1
  54. package/dist/scope/scope.js +6 -2
  55. package/dist/session.d.ts +2 -2
  56. package/dist/signature/signature.js +3 -1
  57. package/dist/snowflake/lower.js +23 -4
  58. package/dist/sqlite/lower.js +25 -2
  59. package/dist/symbols/symbols.d.ts +1 -1
  60. package/dist/symbols/symbols.js +33 -5
  61. package/dist/trino/lower.js +19 -4
  62. package/dist/tsql/lower.js +104 -4
  63. package/package.json +3 -3
package/dist/api.d.ts CHANGED
@@ -116,14 +116,14 @@ export type { Token, TokenRole } from "./token/token.js";
116
116
  export { SqlDocument, type DocumentAnalysis, type StatementCell, type DocumentVariant, type UnionCte, } from "./document/document.js";
117
117
  export { LineIndex } from "./document/line-index.js";
118
118
  export type { StatementCellSpan } from "./document/split.js";
119
- export { complete, completeAt, type Completion } from "./completion/complete.js";
119
+ export { complete, completeAt, type Completion, type CompletionResult, type ReplaceRange, } from "./completion/complete.js";
120
120
  export { jinjaSlotAt, type JinjaSlot } from "./completion/jinja-slot.js";
121
121
  export { signatureAt, type SignatureHelpInfo, type SignatureLabel } from "./signature/signature.js";
122
122
  export { SIGNATURES, lookupSignature, hasSignature, type FnSignature, type ParamSig } from "./signature/signatures.js";
123
123
  export { renderSignature, type RenderSignatureOptions } from "./signature/render.js";
124
124
  export { FN_DOCS, lookupFnDoc, type FnDoc } from "./signature/docs.js";
125
125
  export { referencesAt, type Occurrence, type Occurrences } from "./references/references.js";
126
- export { dialectSymbols, type DialectSymbols } from "./dialect-symbols.js";
126
+ export { dialectSymbols, dialectVocabulary, type DialectSymbols, type DialectVocabulary } from "./dialect-symbols.js";
127
127
  export { CallbackSchema, type SchemaProvider, type TableResolver } from "./qualify/schema-provider.js";
128
128
  export { DERIVED_DIALECTS, resolveDialect } from "./derived-dialects.js";
129
129
  export { foldIdentifier, displayName } from "./dialect-behavior/public-fold.js";
package/dist/api.js CHANGED
@@ -204,7 +204,7 @@ export { SqlDocument, } from "./document/document.js";
204
204
  export { LineIndex } from "./document/line-index.js";
205
205
  // Scope-aware completion over a SqlDocument — the broken-input editor feature (keywords + schema
206
206
  // tables/columns + function names at the caret). Total: never throws.
207
- export { complete, completeAt } from "./completion/complete.js";
207
+ export { complete, completeAt, } from "./completion/complete.js";
208
208
  // The NEUTRAL half of jinja completion: which call + arg slot the caret sits in inside a jinja tag
209
209
  // (`{{ ref('cu│` → { callee: "ref", argIndex: 0, prefix: "cu" }). Carries no dbt vocabulary; a
210
210
  // consumer maps callee + argIndex to a role and supplies candidates. Reuses the parsed tags.
@@ -225,7 +225,7 @@ export { FN_DOCS, lookupFnDoc } from "./signature/docs.js";
225
225
  export { referencesAt } from "./references/references.js";
226
226
  // Per-dialect function/keyword/type-name membership sets — for lint-style "is this identifier a
227
227
  // known X for this dialect?" checks. Computed once per dialect and cached.
228
- export { dialectSymbols } from "./dialect-symbols.js";
228
+ export { dialectSymbols, dialectVocabulary } from "./dialect-symbols.js";
229
229
  // The catalog interface the whole pipeline resolves against, plus its resolve-on-demand
230
230
  // implementation. `Schema` (a full upfront mapping) and `CallbackSchema` (a host-driven
231
231
  // lazy resolver whose prime() bumps a version to invalidate SqlDocument.analyze's memo) both
@@ -1482,9 +1482,11 @@ function lowerLeaf(node) {
1482
1482
  case P.RULE_floating_point_literal:
1483
1483
  case P.RULE_date_or_time_literal:
1484
1484
  case P.RULE_range_literal:
1485
+ return { kind: "literal", text: node.getText(), cst: node };
1485
1486
  case P.RULE_parameter_expression:
1487
+ return lowerParameterExpression(node);
1486
1488
  case P.RULE_system_variable_expression:
1487
- return { kind: "literal", text: node.getText(), cst: node };
1489
+ return lowerSystemVariableExpression(node);
1488
1490
  case P.RULE_identifier:
1489
1491
  return { kind: "column", parts: [identText(node)], partSpans: partSpansOf([node]), cst: node };
1490
1492
  case P.RULE_path_expression:
@@ -1844,7 +1846,12 @@ function columnsOf(expr, acc, clause) {
1844
1846
  break;
1845
1847
  case "subscript":
1846
1848
  columnsOf(expr.base, acc, clause);
1847
- columnsOf(expr.index, acc, clause);
1849
+ if (expr.index)
1850
+ columnsOf(expr.index, acc, clause);
1851
+ if (expr.end)
1852
+ columnsOf(expr.end, acc, clause);
1853
+ if (expr.step)
1854
+ columnsOf(expr.step, acc, clause);
1848
1855
  break;
1849
1856
  case "lambda":
1850
1857
  columnsOf(expr.body, acc, clause);
@@ -1949,6 +1956,25 @@ function extractExpressionSubqueries(select, fromQueries) {
1949
1956
  return out;
1950
1957
  }
1951
1958
  // --- name helpers ----------------------------------------------------------------
1959
+ /** parameter_expression: named_parameter_expression | QUESTION_SYMBOL
1960
+ * (cloud.google.com/bigquery/docs/parameterized-queries). Bare `?` carries no name/ordinal —
1961
+ * BigQuery's positional form has no explicit index, so `ordinal` stays a consumer derivation,
1962
+ * never fabricated here. `@name` resolves through named_parameter_expression's single
1963
+ * dot_identifier; reusing pathParts is safe since that rule holds exactly one dot_identifier
1964
+ * and no identifier head, so it degenerates to a one-element (dot-split, backtick-stripped) path. */
1965
+ function lowerParameterExpression(node) {
1966
+ const named = firstOfRule(node, P.RULE_named_parameter_expression);
1967
+ if (!named)
1968
+ return { kind: "parameter", text: node.getText(), cst: node };
1969
+ return { kind: "parameter", text: node.getText(), name: pathParts(named).join("."), cst: node };
1970
+ }
1971
+ /** system_variable_expression: ATAT_SYMBOL dot_identifier (DOT_SYMBOL dot_identifier)* — a script-
1972
+ * level system variable (cloud.google.com/bigquery/docs/reference/system-variables), e.g.
1973
+ * `@@dataset_id` or the dotted `@@a.b`. `name` is the dotted path with the `@@` sigil stripped;
1974
+ * pathParts already handles this shape (no identifier head, one-or-more dot_identifier tail). */
1975
+ function lowerSystemVariableExpression(node) {
1976
+ return { kind: "variable", text: node.getText(), name: pathParts(node).join("."), system: true, cst: node };
1977
+ }
1952
1978
  /** path_expression: identifier (DOT dot_identifier)* — the dotted parts. The head is an identifier;
1953
1979
  * later parts are dot_identifier (which may be a reserved keyword after the dot). A single
1954
1980
  * backtick-quoted identifier may itself hold a dotted path (`proj.ds.t`), so split each part on `.`. */
@@ -1,19 +1,40 @@
1
1
  import type { SqlDocument } from "../document/document.js";
2
2
  import type { SchemaProvider } from "../qualify/schema-provider.js";
3
- /** One completion candidate. The editor filters this list by the typed prefix and applies the
4
- * chosen label at the caret; we only produce the labels, anchored at the caret offset. The
5
- * `"template"` kind is a host candidate for a jinja call slot (a dbt model for a ref's arg). */
3
+ /** One completion candidate, already pruned to the typed prefix (2026-07-12 ruling) and applied at
4
+ * the caret / `CompletionResult.replaceRange`. The `"template"` kind is a host candidate for a
5
+ * jinja call slot (a dbt model for a ref's arg) — its own, separately-decided contract (the
6
+ * consumer still filters those by the typed prefix; see complete.jinja-candidates.test.ts). */
6
7
  export interface Completion {
7
8
  label: string;
8
9
  kind: "keyword" | "column" | "table" | "cte" | "namespace" | "function" | "template";
9
10
  /** Extra display info, e.g. a column's type when the schema knows it. */
10
11
  detail?: string;
11
12
  }
13
+ /** The caret-anchored span of the partial identifier/keyword the candidates were pruned against —
14
+ * `text.slice(start, end)` is what's already typed. `start` includes an opening delimiter when the
15
+ * caret sits inside a quoted/bracketed/backtick-quoted identifier (`"my_t`, `` `my_t ``, `[my_t`),
16
+ * so an editor that replaces this span never leaves a stray leading quote. `end` never extends past
17
+ * the caret (`offset`) — even where a dialect's lexer greedily swallows an unterminated quoted
18
+ * identifier past the caret, only the already-typed portion is ever reported or matched. */
19
+ export interface ReplaceRange {
20
+ start: number;
21
+ end: number;
22
+ }
23
+ /** completeAt()'s result: an ordinary `Completion[]` (`.map`/`.filter`/iteration/`.length` all work
24
+ * exactly as before — every existing consumer sees no change) carrying one optional extra
25
+ * property, the same "array with named extras" shape TypeScript's own `RegExpMatchArray` uses for
26
+ * `String.prototype.match`. `replaceRange` is present only when the caret sits inside a partially
27
+ * typed word; an empty-prefix caret (a token boundary — nothing typed yet) returns a plain array
28
+ * with no `replaceRange`, byte-identical to the pre-pruning contract. */
29
+ export interface CompletionResult extends Array<Completion> {
30
+ replaceRange?: ReplaceRange;
31
+ }
12
32
  /**
13
- * Completion candidates for the caret at `offset` in `doc`. Schema-aware when a `Schema` is given
14
- * (table names + column types). NEVER throws: on broken / mid-edit input it still returns the
15
- * keyword candidates the walk can reach.
33
+ * Completion candidates for the caret at `offset` in `doc`, pruned to the identifier/keyword
34
+ * fragment already typed there (case-insensitive, dialect-fold-aware; plain prefix match never
35
+ * fuzzy). Schema-aware when a `Schema` is given (table names + column types). NEVER throws: on
36
+ * broken / mid-edit input it still returns the keyword candidates the walk can reach.
16
37
  */
17
- export declare function completeAt(doc: SqlDocument, offset: number, schema?: SchemaProvider): Completion[];
38
+ export declare function completeAt(doc: SqlDocument, offset: number, schema?: SchemaProvider): CompletionResult;
18
39
  /** @deprecated Use completeAt — same function, uniform cursor-verb naming. */
19
40
  export declare const complete: typeof completeAt;
@@ -16,6 +16,7 @@
16
16
  // throws — broken input still yields at least the keyword candidates.
17
17
  // ---------------------------------------------------------------------------
18
18
  import { Token } from "antlr4ng";
19
+ import { debugRethrow } from "../debug.js";
19
20
  import { nodeAt } from "../document/node-at.js";
20
21
  import { resolveBehavior } from "../dialect-behavior/registry.js";
21
22
  import { DefaultTemplateProvider } from "../qualify/template-provider.js";
@@ -26,9 +27,10 @@ import { jinjaSlotAt } from "./jinja-slot.js";
26
27
  import { COMPLETION_CONFIG } from "./config.js";
27
28
  import { completionMeta } from "./parser-factory.js";
28
29
  /**
29
- * Completion candidates for the caret at `offset` in `doc`. Schema-aware when a `Schema` is given
30
- * (table names + column types). NEVER throws: on broken / mid-edit input it still returns the
31
- * keyword candidates the walk can reach.
30
+ * Completion candidates for the caret at `offset` in `doc`, pruned to the identifier/keyword
31
+ * fragment already typed there (case-insensitive, dialect-fold-aware; plain prefix match never
32
+ * fuzzy). Schema-aware when a `Schema` is given (table names + column types). NEVER throws: on
33
+ * broken / mid-edit input it still returns the keyword candidates the walk can reach.
32
34
  */
33
35
  export function completeAt(doc, offset, schema) {
34
36
  try {
@@ -36,8 +38,7 @@ export function completeAt(doc, offset, schema) {
36
38
  }
37
39
  catch (e) {
38
40
  // Total by contract: a walk/parse hiccup must not surface to the editor.
39
- if (process.env.SQLLENS_DEBUG_COMPLETE)
40
- throw e;
41
+ debugRethrow(e);
41
42
  return [];
42
43
  }
43
44
  }
@@ -84,7 +85,7 @@ function collect(doc, offset, schema) {
84
85
  ...(cell ? cell.tokens : doc.tokens),
85
86
  { type: Token.EOF, channel: Token.DEFAULT_CHANNEL, start: end, text: "" },
86
87
  ];
87
- const caretIdx = caretTokenIndex(walkTokens, offset);
88
+ const caretIdx = caretTokenIndex(walkTokens, offset, cfg);
88
89
  const cand = collectCandidates(meta.atn, meta.entryRuleIndex, walkTokens, caretIdx, cfg.preferredRules, cfg.ignoredTokens);
89
90
  const out = [];
90
91
  const seen = new Set(); // dedup by `${kind}\0${label}`
@@ -159,7 +160,93 @@ function collect(doc, offset, schema) {
159
160
  add({ label: fn, kind: "function" });
160
161
  }
161
162
  }
162
- return out;
163
+ return pruneByPrefix(out, walkTokens, offset, cfg, doc.text, dialect);
164
+ }
165
+ /** SQL identifier-quoting delimiters recognized across all ten dialects
166
+ * (docs/identifier-delimiter-contract.md) — used only to recognize a PARTIAL, possibly
167
+ * unterminated quoted identifier under the caret; a dialect's own quoting/case rules stay put in
168
+ * src/<dialect>/fold.ts. */
169
+ const CLOSE_FOR_OPEN = { '"': '"', "`": "`", "[": "]" };
170
+ /** The identifier/keyword fragment under (or immediately preceding) the caret, read off the
171
+ * document's OWN token stream — never re-lexed. Two shapes recognized:
172
+ * - a bare word (keyword or identifier) the caret sits inside/at the end of (`SEL|`, `ifn|`) —
173
+ * word-START only (`[A-Za-z_]`), so a numeric literal (`… WHERE x > 10|`) is never mistaken
174
+ * for an identifier prefix;
175
+ * - a delimited identifier: either ONE token spanning both delimiters (`cfg.nameTokens` — a
176
+ * complete `"a"`/`` `a` ``/`[a]`, or an unterminated one some dialects lex greedily to EOF), or
177
+ * a lone opening-delimiter character immediately followed by a bare identifier token (the
178
+ * lexer error-recovery split other dialects produce for an unterminated `` `a `` / `"a` — the
179
+ * delimiter alone fails to match any token and is skipped, the identifier body then lexes
180
+ * cleanly on its own).
181
+ * Returns undefined at a token boundary — nothing partially typed (the empty-prefix case, left
182
+ * completely unpruned). The returned span never extends past `offset`: even where a dialect's
183
+ * lexer greedily swallows an unterminated quoted identifier to EOF, only the already-typed portion
184
+ * is ever reported. */
185
+ function identifierPrefixAt(toks, offset, cfg, text) {
186
+ for (let i = 0; i < toks.length; i++) {
187
+ const t = toks[i];
188
+ if (!t || t.channel !== Token.DEFAULT_CHANNEL)
189
+ continue;
190
+ const nameLike = /^[A-Za-z_]/.test(t.text) || cfg.nameTokens.has(t.type);
191
+ if (nameLike && t.start < offset && offset <= t.start + t.text.length) {
192
+ let start = t.start;
193
+ // A lone opening-delimiter token immediately before this one (the split-recovery case) —
194
+ // fold it into the same partial identifier so the replace range covers it too.
195
+ for (let j = i - 1; j >= 0; j--) {
196
+ const p = toks[j];
197
+ if (!p || p.channel !== Token.DEFAULT_CHANNEL)
198
+ continue;
199
+ if (p.start + p.text.length === start && Object.hasOwn(CLOSE_FOR_OPEN, p.text))
200
+ start = p.start;
201
+ break;
202
+ }
203
+ return { start, raw: text.slice(start, offset) };
204
+ }
205
+ if (t.start >= offset)
206
+ return undefined; // moved past the caret — nothing partially typed here
207
+ }
208
+ return undefined;
209
+ }
210
+ /** Fold a (possibly partial/unterminated) identifier fragment to the same identity space
211
+ * `behavior.fold` gives a COMPLETE identifier. A partial `"a`/`` `a `` /`[a` has no closing
212
+ * delimiter yet — `fold()`'s own unwrap requires one to recognize it as quoted — so this
213
+ * synthesizes the matching close for the fold call only; the raw text / replace range reported
214
+ * elsewhere are untouched. Folding an already-complete candidate label through this is a no-op. */
215
+ function foldForPrefixMatch(behavior, raw, kind) {
216
+ const open = raw[0];
217
+ const close = open ? CLOSE_FOR_OPEN[open] : undefined;
218
+ const closed = close && !raw.endsWith(close) ? raw + close : raw;
219
+ return behavior.fold(closed, kind);
220
+ }
221
+ /** Post-filters the assembled SQL-slot candidates by the identifier/keyword fragment under the
222
+ * caret (2026-07-12 ruling: "completeAt must return only candidates that validly complete at the
223
+ * caret right now, pruned by the word already typed" — the library holds the token stream, CST,
224
+ * and dialect fold rules, so it prunes here instead of handing the consumer the whole per-slot
225
+ * set). An empty-prefix caret (a token boundary — nothing partially typed) returns `out`
226
+ * completely unchanged: byte-identical to the pre-pruning contract, no `replaceRange`. Keywords
227
+ * fold plain ASCII-case-insensitive (every dialect's keywords are case-insensitive, unlike
228
+ * identifiers); identifier-kind candidates (column/table/cte/namespace/function) fold through the
229
+ * dialect's own identity-key rule, `"table"` kind for `table` candidates (only BigQuery's table
230
+ * case rule differs from the rest). Plain-prefix `startsWith` only — never fuzzy (never-wrong: no
231
+ * guessed matches). Jinja template/call-slot candidates (returned earlier in `collect`, from
232
+ * `templateCompletions`) are OUT of this — that path has its own, separately-decided "editor
233
+ * filters" contract (tests/completion/complete.jinja-candidates.test.ts). */
234
+ function pruneByPrefix(out, toks, offset, cfg, text, dialect) {
235
+ const prefix = identifierPrefixAt(toks, offset, cfg, text);
236
+ if (!prefix)
237
+ return out;
238
+ const behavior = resolveBehavior(dialect);
239
+ const keywordPrefix = prefix.raw.toLowerCase();
240
+ const otherPrefix = foldForPrefixMatch(behavior, prefix.raw, "other");
241
+ const tablePrefix = foldForPrefixMatch(behavior, prefix.raw, "table");
242
+ const pruned = out.filter((c) => {
243
+ if (c.kind === "keyword")
244
+ return c.label.toLowerCase().startsWith(keywordPrefix);
245
+ const kind = c.kind === "table" ? "table" : "other";
246
+ return foldForPrefixMatch(behavior, c.label, kind).startsWith(kind === "table" ? tablePrefix : otherPrefix);
247
+ });
248
+ pruned.replaceRange = { start: prefix.start, end: offset };
249
+ return pruned;
163
250
  }
164
251
  /** The host's candidates for a jinja call slot, as completions. The template provider carries them,
165
252
  * so this reads the `schema` when it is one (a DbtTemplateProvider IS a SchemaProvider, and the host
@@ -220,18 +307,22 @@ function visibleCteNames(scopes, ast, offset) {
220
307
  * convention):
221
308
  * 1. the token being TYPED — a word-like token whose span CONTAINS the caret (start < offset <=
222
309
  * end). A caret at the end of `ifn` completes `ifn`; it does not mean the slot is filled.
223
- * Word-like only: punctuation is never partially typed, so `abs(|` keeps rule 2.
310
+ * Word-like OR a delimited identifier token (cfg.nameTokens: `` `a` ``/`"a"`/`[a]`) counts too —
311
+ * without it, a caret inside a quoted identifier fell through to rule 2 and reported the
312
+ * position of whatever token happened to follow it. Punctuation is never partially typed,
313
+ * so `abs(|` keeps rule 2.
224
314
  * 2. between tokens — the first default-channel token whose `.start >= offset`; for an
225
315
  * end-of-input caret that is the EOF sentinel's index (last entry).
226
316
  * `toks` is the document's own token stream (doc coordinates) with the EOF sentinel appended.
227
317
  * Source order makes one pass sufficient: a containing token starts before any `.start >= offset`
228
318
  * token, so rule 1 fires first whenever it applies. */
229
- function caretTokenIndex(toks, offset) {
319
+ function caretTokenIndex(toks, offset, cfg) {
230
320
  for (let i = 0; i < toks.length; i++) {
231
321
  const t = toks[i];
232
322
  if (!t || t.channel !== Token.DEFAULT_CHANNEL)
233
323
  continue;
234
- if (/^\w/.test(t.text) && t.start < offset && offset <= t.start + t.text.length)
324
+ const wordLike = /^\w/.test(t.text) || cfg.nameTokens.has(t.type);
325
+ if (wordLike && t.start < offset && offset <= t.start + t.text.length)
235
326
  return i;
236
327
  if (t.start >= offset)
237
328
  return i;
@@ -84,7 +84,10 @@ const BIGQUERY_TABLE_RULES = new Set([GoogleSQLParser.RULE_table_path_expression
84
84
  const BIGQUERY_COLUMN_RULES = new Set([GoogleSQLParser.RULE_identifier]);
85
85
  const BIGQUERY_PREFERRED = new Set([...BIGQUERY_TABLE_RULES, ...BIGQUERY_COLUMN_RULES]);
86
86
  const BIGQUERY_RELATION_KEYWORDS = new Set([GoogleSQLLexer.FROM_SYMBOL, GoogleSQLLexer.JOIN_SYMBOL]);
87
- const BIGQUERY_NAME_TOKENS = new Set([GoogleSQLLexer.IDENTIFIER]);
87
+ // UNCLOSED_ESCAPED_IDENTIFIER is the dedicated recovery token GoogleSQL's lexer emits for a
88
+ // backtick-quoted identifier with no closing backtick yet (`` `a `` mid-typing) — completion's
89
+ // caret-in-a-partial-identifier detection needs it alongside the ordinary closed-form token.
90
+ const BIGQUERY_NAME_TOKENS = new Set([GoogleSQLLexer.IDENTIFIER, GoogleSQLLexer.UNCLOSED_ESCAPED_IDENTIFIER]);
88
91
  // ── Redshift (Bytebase/Postgres-derived fork) ───────────────────────────────
89
92
  // post-FROM → relation_expr (the relation slot; the leaf `identifier` also surfaces post-FROM
90
93
  // but never in column position, so tableRules uses relation_expr).
@@ -93,7 +96,13 @@ const REDSHIFT_TABLE_RULES = new Set([RedshiftParser.RULE_relation_expr]);
93
96
  const REDSHIFT_COLUMN_RULES = new Set([RedshiftParser.RULE_a_expr]);
94
97
  const REDSHIFT_PREFERRED = new Set([...REDSHIFT_TABLE_RULES, ...REDSHIFT_COLUMN_RULES]);
95
98
  const REDSHIFT_RELATION_KEYWORDS = new Set([RedshiftLexer.FROM, RedshiftLexer.JOIN]);
96
- const REDSHIFT_NAME_TOKENS = new Set([RedshiftLexer.Identifier, RedshiftLexer.QuotedIdentifier]);
99
+ // UnterminatedQuotedIdentifier is the dedicated recovery token this Postgres-lineage lexer emits
100
+ // for a `"a` with no closing quote yet (mid-typing) — needed alongside the ordinary closed form.
101
+ const REDSHIFT_NAME_TOKENS = new Set([
102
+ RedshiftLexer.Identifier,
103
+ RedshiftLexer.QuotedIdentifier,
104
+ RedshiftLexer.UnterminatedQuotedIdentifier,
105
+ ]);
97
106
  // ── Postgres / DuckDB (TVL-lineage forks like Redshift) ─────────────────────
98
107
  // The same rule split as Redshift applies (same grammar shapes): post-FROM → relation_expr,
99
108
  // SELECT/WHERE → a_expr; relation names are plain/quoted identifiers after FROM/JOIN.
@@ -101,12 +110,24 @@ const POSTGRES_TABLE_RULES = new Set([PostgresParser.RULE_relation_expr]);
101
110
  const POSTGRES_COLUMN_RULES = new Set([PostgresParser.RULE_a_expr]);
102
111
  const POSTGRES_PREFERRED = new Set([...POSTGRES_TABLE_RULES, ...POSTGRES_COLUMN_RULES]);
103
112
  const POSTGRES_RELATION_KEYWORDS = new Set([PostgresLexer.FROM, PostgresLexer.JOIN]);
104
- const POSTGRES_NAME_TOKENS = new Set([PostgresLexer.Identifier, PostgresLexer.QuotedIdentifier]);
113
+ // UnterminatedQuotedIdentifier is the dedicated recovery token this lexer emits for a `"a` with no
114
+ // closing quote yet (mid-typing) — needed alongside the ordinary closed form.
115
+ const POSTGRES_NAME_TOKENS = new Set([
116
+ PostgresLexer.Identifier,
117
+ PostgresLexer.QuotedIdentifier,
118
+ PostgresLexer.UnterminatedQuotedIdentifier,
119
+ ]);
105
120
  const DUCKDB_TABLE_RULES = new Set([DuckdbParser.RULE_relation_expr]);
106
121
  const DUCKDB_COLUMN_RULES = new Set([DuckdbParser.RULE_a_expr]);
107
122
  const DUCKDB_PREFERRED = new Set([...DUCKDB_TABLE_RULES, ...DUCKDB_COLUMN_RULES]);
108
123
  const DUCKDB_RELATION_KEYWORDS = new Set([DuckdbLexer.FROM, DuckdbLexer.JOIN]);
109
- const DUCKDB_NAME_TOKENS = new Set([DuckdbLexer.Identifier, DuckdbLexer.QuotedIdentifier]);
124
+ // UnterminatedQuotedIdentifier is the dedicated recovery token this Postgres-lineage lexer emits
125
+ // for a `"a` with no closing quote yet (mid-typing) — needed alongside the ordinary closed form.
126
+ const DUCKDB_NAME_TOKENS = new Set([
127
+ DuckdbLexer.Identifier,
128
+ DuckdbLexer.QuotedIdentifier,
129
+ DuckdbLexer.UnterminatedQuotedIdentifier,
130
+ ]);
110
131
  // -- Trino (first-party SqlBase.g4 split) ------------------------------------
111
132
  // Post-FROM relation names live under relationPrimary/qualifiedName; column slots are
112
133
  // primaryExpression. Identifiers: plain / "quoted" / backquoted / digit-led.
@@ -2,7 +2,7 @@ import { acceptsFor } from "../dialect-behavior/coerce-rules.js";
2
2
  import { likePatternToRegExp } from "../scope/like-pattern.js";
3
3
  import { SIGNATURES } from "../signature/signatures.js";
4
4
  import { displayName, fold, foldTableName, matchesSourceKey, DATABRICKS_NAME_CONFIG } from "./fold.js";
5
- import { databricksLiteral, databricksParseType, DATABRICKS_FUNCTION_RETURNS } from "./infer.js";
5
+ import { databricksLiteral, databricksParseType, databricksSpecial, DATABRICKS_FUNCTION_RETURNS } from "./infer.js";
6
6
  export const databricksBehavior = {
7
7
  fold,
8
8
  displayName,
@@ -14,6 +14,8 @@ export const databricksBehavior = {
14
14
  parseType: databricksParseType,
15
15
  functions: DATABRICKS_FUNCTION_RETURNS,
16
16
  division: "float",
17
+ dateSubtraction: "interval",
18
+ special: databricksSpecial,
17
19
  signatures: SIGNATURES.databricks,
18
20
  // Databricks implicit coercion: STRING containing a number coerces to numeric (STR_TO_NUM=true), no bool<->num (BOOL_NUM=false).
19
21
  accepts: (argType, paramText) => acceptsFor(databricksParseType, true, false, argType, paramText),
@@ -1,5 +1,9 @@
1
1
  import { type FnRule } from "../infer/functions.js";
2
2
  import { type Type } from "../infer/types.js";
3
+ import type { Expr } from "../ir/ir.js";
4
+ export declare function databricksSpecial(fn: Extract<Expr, {
5
+ kind: "function";
6
+ }>, typeOf: (e: Expr) => Type): Type | undefined;
3
7
  export declare const DATABRICKS_FUNCTION_RETURNS: Record<string, FnRule>;
4
8
  /** Databricks/Spark literal forms. */
5
9
  export declare function databricksLiteral(text: string): Type;