sqllens 1.4.0 → 1.5.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 (61) hide show
  1. package/README.md +19 -0
  2. package/dist/bigquery/behavior.js +2 -1
  3. package/dist/bigquery/fold.d.ts +6 -0
  4. package/dist/bigquery/fold.js +8 -0
  5. package/dist/bigquery/lower.js +37 -6
  6. package/dist/completion/complete.d.ts +1 -1
  7. package/dist/completion/complete.js +153 -16
  8. package/dist/completion/jinja-slot.js +6 -1
  9. package/dist/databricks/behavior.js +2 -1
  10. package/dist/databricks/fold.d.ts +4 -0
  11. package/dist/databricks/fold.js +6 -0
  12. package/dist/databricks/lower.js +10 -3
  13. package/dist/dialect-behavior/behavior.d.ts +5 -0
  14. package/dist/duckdb/behavior.js +2 -1
  15. package/dist/duckdb/fold.d.ts +4 -0
  16. package/dist/duckdb/fold.js +6 -0
  17. package/dist/duckdb/lower.js +39 -8
  18. package/dist/index.d.ts +1 -1
  19. package/dist/ir/ir.d.ts +16 -6
  20. package/dist/ir/qualified-name.d.ts +43 -0
  21. package/dist/ir/qualified-name.js +74 -0
  22. package/dist/lineage/hops.js +7 -3
  23. package/dist/lineage/lineage.js +2 -1
  24. package/dist/minijinja/apply-tags.js +27 -4
  25. package/dist/mysql/behavior.js +2 -1
  26. package/dist/mysql/fold.d.ts +4 -0
  27. package/dist/mysql/fold.js +6 -0
  28. package/dist/mysql/lower.js +19 -4
  29. package/dist/postgres/behavior.js +2 -1
  30. package/dist/postgres/fold.d.ts +5 -0
  31. package/dist/postgres/fold.js +7 -0
  32. package/dist/postgres/lower.js +21 -5
  33. package/dist/qualify/qualify.d.ts +1 -1
  34. package/dist/qualify/qualify.js +75 -50
  35. package/dist/qualify/schema-provider.d.ts +13 -0
  36. package/dist/qualify/schema.d.ts +18 -1
  37. package/dist/qualify/schema.js +60 -6
  38. package/dist/redshift/behavior.js +2 -1
  39. package/dist/redshift/fold.d.ts +4 -0
  40. package/dist/redshift/fold.js +6 -0
  41. package/dist/redshift/lower.js +20 -4
  42. package/dist/scope/scope.d.ts +24 -2
  43. package/dist/scope/scope.js +65 -13
  44. package/dist/sema/resolve.js +21 -10
  45. package/dist/snowflake/behavior.js +2 -1
  46. package/dist/snowflake/fold.d.ts +4 -0
  47. package/dist/snowflake/fold.js +6 -0
  48. package/dist/snowflake/lower.js +23 -4
  49. package/dist/sqlite/behavior.js +2 -1
  50. package/dist/sqlite/fold.d.ts +4 -0
  51. package/dist/sqlite/fold.js +6 -0
  52. package/dist/sqlite/lower.js +11 -2
  53. package/dist/trino/behavior.js +2 -1
  54. package/dist/trino/fold.d.ts +3 -0
  55. package/dist/trino/fold.js +5 -0
  56. package/dist/trino/lower.js +22 -4
  57. package/dist/tsql/behavior.js +2 -1
  58. package/dist/tsql/fold.d.ts +5 -0
  59. package/dist/tsql/fold.js +7 -0
  60. package/dist/tsql/lower.js +18 -5
  61. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -22,7 +22,7 @@ export { lower as lowerSqlite } from "./sqlite/lower.js";
22
22
  export { lower as lowerMysql } from "./mysql/lower.js";
23
23
  export type { ParseResult } from "./parse-result.js";
24
24
  export { type TemplatedParseResult, type TemplatedParseOptions } from "./template/engine.js";
25
- export type { Clause, ColumnRef, CteDef, Expr, GraphElement, GraphTableSource, Join, JoinKind, LateralViewSource, LimitInfo, PartSpan, PipeBranch, PipeExpr, PipeSetItem, PipeStage, PivotInfo, Projection, QueryBody, QueryExpr, SelectExpr, SetOpExpr, Source, SubquerySource, TableSource, TemplateExprInfo, TemplateSourceInfo, UnpivotInfo, UnsupportedFlag, WindowSpec, } from "./ir/ir.js";
25
+ export type { Clause, ColumnRef, CteDef, Expr, GraphElement, GraphTableSource, Join, JoinKind, LateralViewSource, LimitInfo, PartSpan, PipeBranch, PipeExpr, PipeSetItem, PipeStage, PivotInfo, Projection, QualifiedName, QueryBody, QueryExpr, SelectExpr, SetOpExpr, Source, SubquerySource, TableSource, TemplateExprInfo, TemplateSourceInfo, UnpivotInfo, UnsupportedFlag, WindowSpec, } from "./ir/ir.js";
26
26
  export { partSpanOf, partSpansOf } from "./ir/part-span.js";
27
27
  export { endPosition } from "./ir/span.js";
28
28
  export { childExprs, walkExprs, allQueryExprs } from "./ir/walk.js";
package/dist/ir/ir.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import type { ParserRuleContext } from "antlr4ng";
2
2
  import type { PartSpan } from "./part-span.js";
3
+ import type { QualifiedName } from "./qualified-name.js";
3
4
  import type { StatementCategory } from "./statement.js";
4
5
  export type { PartSpan } from "./part-span.js";
6
+ export type { QualifiedName, QualifiedNameConfig, NameRole } from "./qualified-name.js";
5
7
  /**
6
8
  * The identity of one template call, the `expansion()` key. Args are LITERAL
7
9
  * string values: quote-stripped, escapes NOT resolved — an argument whose
@@ -647,12 +649,16 @@ export interface TemplateSourceInfo {
647
649
  }
648
650
  export interface TableSource {
649
651
  kind: "table";
650
- /** Multipart name parts as written, e.g. ["catalog","schema","t"]. */
651
- name: string[];
652
- /** Per-part spans PARALLEL to `name`, one per multipart segment — same all-or-nothing convention
653
- * as `ColumnRef.partSpans` (absent when any part lacks a real token). Lets a consumer hit-test a
654
- * cursor on `catalog` vs `schema` vs `t` in `catalog.schema.t`, and recover each part's raw
655
- * (delimiter-included) source text via span + document text without re-scanning the token stream. */
652
+ /** The STRUCTURED name (issue #38): the object's own `name`, `parts` exactly as written, roles
653
+ * right-aligned per the dialect's namespace, the folded identity `key`, and a display-ready
654
+ * `fqn`. Built by the dialect's lower() (the one layer that knows the namespace shape);
655
+ * consumers read structure here and never re-derive it from a parts array. */
656
+ relation: QualifiedName;
657
+ /** Per-part spans PARALLEL to `relation.parts`, one per multipart segment — same all-or-nothing
658
+ * convention as `ColumnRef.partSpans` (absent when any part lacks a real token). Lets a
659
+ * consumer hit-test a cursor on `catalog` vs `schema` vs `t` in `catalog.schema.t`, and recover
660
+ * each part's raw (delimiter-included) source text via span + document text without re-scanning
661
+ * the token stream. */
656
662
  namePartSpans?: PartSpan[];
657
663
  alias?: string;
658
664
  /** The alias identifier's own CST node (for its precise span), when aliased. */
@@ -670,6 +676,10 @@ export interface TableSource {
670
676
  /** Present when this source was written as a minijinja template tag in a FROM/JOIN slot; attached
671
677
  * post-lower by the jinja front end (src/minijinja/apply-tags.ts). See TemplateSourceInfo. */
672
678
  template?: TemplateSourceInfo;
679
+ /** True when the LIBRARY built this node rather than lowering it from user syntax (a graph
680
+ * element variable exposed as a relation). Catalog existence semantics don't apply: a closed
681
+ * world's unknown-table never fires for a synthesized source. */
682
+ synthesized?: true;
673
683
  cst: ParserRuleContext;
674
684
  }
675
685
  export interface SubquerySource {
@@ -0,0 +1,43 @@
1
+ import { type FoldRule } from "../ident/fold.js";
2
+ /** Qualifier roles, normalized across dialects (vendor vocabulary noted per dialect at its
3
+ * config): "catalog" covers database/project, "schema" covers dataset/attached-database,
4
+ * "server" is tsql's linked-server level. The object's own name is never a role — it is the
5
+ * final part, always. */
6
+ export type NameRole = "server" | "catalog" | "schema";
7
+ /** A dialect's namespace shape: its qualifier roles OUTER→INNER, and the fold/delimiter rule
8
+ * its identifiers obey. Declared once per dialect in src/<dialect>/fold.ts, doc-cited there. */
9
+ export interface QualifiedNameConfig {
10
+ roles: readonly NameRole[];
11
+ rule: FoldRule;
12
+ }
13
+ /** A structured, dialect-resolved name. Plain frozen data like every IR node. */
14
+ export interface QualifiedName {
15
+ /** The object's OWN name (the last part), delimiters stripped, as written. */
16
+ name: string;
17
+ /** Every part as written, outermost first, delimiters stripped. An elided middle part is "". */
18
+ parts: string[];
19
+ /** tsql linked-server level. Absent = not written. */
20
+ server?: string;
21
+ /** Catalog level (vendor: catalog / database / project). Absent = not written. */
22
+ catalog?: string;
23
+ /** Schema level (vendor: schema / dataset / attached database). Absent = not written;
24
+ * "" = written-and-elided (`db..table`). */
25
+ schema?: string;
26
+ /** Folded identity parts (the dialect's own quoting-aware fold). COMPARE with these — never
27
+ * display them, never re-fold. */
28
+ key: string[];
29
+ /** The qualified name as ONE display-ready string. Source-derived: the raw parts joined as
30
+ * written (quoting kept). Synthesized: parts joined with dialect quoting where needed. */
31
+ fqn: string;
32
+ }
33
+ /**
34
+ * Build a QualifiedName from SOURCE-derived raw parts (as they appear in the text, delimiters
35
+ * intact where the user wrote them). `fqn` preserves the user's spelling and quoting exactly.
36
+ */
37
+ export declare function qualifiedNameOf(rawParts: string[], config: QualifiedNameConfig): QualifiedName;
38
+ /**
39
+ * Build a QualifiedName from SYNTHESIZED plain parts (a provider's resolved relation — no source
40
+ * text exists). `fqn` renders each part with the dialect's quoting exactly where the part needs
41
+ * delimiters to round-trip; plain identifier parts stay bare.
42
+ */
43
+ export declare function synthesizedQualifiedName(parts: string[], config: QualifiedNameConfig): QualifiedName;
@@ -0,0 +1,74 @@
1
+ // ---------------------------------------------------------------------------
2
+ // QualifiedName — the structured relation/column name the IR carries (issue #38).
3
+ //
4
+ // Root cause this type exists to fix: LOSS OF INFORMATION AT LOWERING. A dotted name's
5
+ // structure (which part is the catalog, which the schema; what was quoted; what identity each
6
+ // part folds to) was flattened to string[] at lower time, and every downstream layer re-derived
7
+ // it by heuristic — collapsing, in practice, to last-part matching. The two standing principles
8
+ // applied to names:
9
+ //
10
+ // lossless — `parts` is exactly what was written (partial qualification represented, never
11
+ // defaulted; a tsql elided middle part `db..table` is an EXPLICITLY EMPTY part);
12
+ // `fqn` joins the raw parts as written, quoting included. Nothing re-cased,
13
+ // nothing invented.
14
+ // never-wrong — roles are assigned only to parts that exist, right-aligned against the
15
+ // dialect's namespace; a part beyond the dialect's depth gets NO role. `key` is
16
+ // the folded identity under the dialect's own quoting semantics; consumers
17
+ // compare keys and never fold or parse dotted strings themselves.
18
+ //
19
+ // The dialect's lower() builds these (it is the one layer that knows both the CST shape and the
20
+ // dialect's namespace); each dialect's config lives next to its fold rule in
21
+ // src/<dialect>/fold.ts. This module is dialect-agnostic mechanics only.
22
+ // ---------------------------------------------------------------------------
23
+ import { displayWith, foldWith } from "../ident/fold.js";
24
+ /** Right-align the dialect's roles onto the qualifier parts (everything before the last part).
25
+ * Only parts that exist get a role; qualifier parts beyond the dialect's depth get none. */
26
+ function assignRoles(q, stripped, roles) {
27
+ const qualifiers = stripped.slice(0, -1);
28
+ const n = Math.min(qualifiers.length, roles.length);
29
+ for (let j = 0; j < n; j++) {
30
+ const role = roles[roles.length - 1 - j];
31
+ q[role] = qualifiers[qualifiers.length - 1 - j];
32
+ }
33
+ }
34
+ /**
35
+ * Build a QualifiedName from SOURCE-derived raw parts (as they appear in the text, delimiters
36
+ * intact where the user wrote them). `fqn` preserves the user's spelling and quoting exactly.
37
+ */
38
+ export function qualifiedNameOf(rawParts, config) {
39
+ const parts = rawParts.map((p) => displayWith(config.rule, p));
40
+ const q = {
41
+ name: parts[parts.length - 1] ?? "",
42
+ parts,
43
+ key: rawParts.map((p) => foldWith(config.rule, p, "table")),
44
+ fqn: rawParts.join("."),
45
+ };
46
+ assignRoles(q, parts, config.roles);
47
+ return q;
48
+ }
49
+ /**
50
+ * Build a QualifiedName from SYNTHESIZED plain parts (a provider's resolved relation — no source
51
+ * text exists). `fqn` renders each part with the dialect's quoting exactly where the part needs
52
+ * delimiters to round-trip; plain identifier parts stay bare.
53
+ */
54
+ export function synthesizedQualifiedName(parts, config) {
55
+ const q = {
56
+ name: parts[parts.length - 1] ?? "",
57
+ parts: [...parts],
58
+ key: parts.map((p) => foldWith(config.rule, p, "table")),
59
+ fqn: parts.map((p) => renderPart(p, config.rule)).join("."),
60
+ };
61
+ assignRoles(q, parts, config.roles);
62
+ return q;
63
+ }
64
+ /** True when `part` round-trips as a bare identifier (letters/digits/underscore, non-digit lead).
65
+ * Anything else needs the dialect's delimiters. Reserved words are a documented boundary: this
66
+ * renderer does not consult keyword lists. */
67
+ const PLAIN_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
68
+ function renderPart(part, rule) {
69
+ if (PLAIN_IDENT.test(part))
70
+ return part;
71
+ const [open, close] = rule.delimiters[0] ?? ['"', '"'];
72
+ const body = rule.escapeStyle === "backslash" ? part.split(close).join(`\\${close}`) : part.split(close).join(close + close);
73
+ return `${open}${body}${close}`;
74
+ }
@@ -129,10 +129,14 @@ function followColumn(scope, parts, walk, trail = []) {
129
129
  return [{ kind: "unresolved", via }];
130
130
  const { source, column } = binding;
131
131
  if (source.kind === "table") {
132
- // A recursive CTE's self-reference is a plain table here (see Walk.activeCtes) — cycle-guard it.
133
- if (source.name.length === 1 && walk.activeCtes.has(behaviorOf(scope).fold(source.name[0])))
132
+ // A recursive CTE's self-reference is a plain table here (see Walk.activeCtes) — cycle-guard
133
+ // it. activeCtes keys are the DEFAULT ("other") fold of the declared raw name, so fold the
134
+ // self-reference's raw text the same way (single-part ⇒ relation.fqn IS the raw text).
135
+ if (source.name.length === 1 && walk.activeCtes.has(behaviorOf(scope).fold(source.source.relation.fqn))) {
134
136
  return [{ kind: "unresolved", via }];
135
- return [{ kind: "origin", origin: { table: source.name, column }, via }];
137
+ }
138
+ // Origins are DISPLAY-facing: the as-written parts, never the folded key.
139
+ return [{ kind: "origin", origin: { table: source.source.relation.parts, column }, via }];
136
140
  }
137
141
  const child = childScopeOf(source);
138
142
  if (!child) {
@@ -172,8 +172,9 @@ function columnRefOrigins(parts, scope, schema, seen) {
172
172
  * lineage walk (src/lineage/hops.ts) can defer pipe/lateral/graphtable/pivot sources — the ones
173
173
  * it has no hop model for — to this exact shared origin walk, so the two walks can't drift. */
174
174
  export function columnOrigins(src, column, schema, seen) {
175
+ // Origins are DISPLAY-facing: the as-written parts (relation.parts), never the folded key.
175
176
  if (src.kind === "table")
176
- return [{ table: src.name, column }];
177
+ return [{ table: src.source.relation.parts, column }];
177
178
  if (src.kind === "cte")
178
179
  return derivedOrigins(src.ref.scope, column, src.ref.def.columnAliases, schema, seen);
179
180
  if (src.kind === "subquery")
@@ -27,6 +27,8 @@
27
27
  // returns the input `ast` unchanged.
28
28
  // ---------------------------------------------------------------------------
29
29
  import { freezeIR } from "../ir/freeze.js";
30
+ import { qualifiedNameOf, synthesizedQualifiedName } from "../ir/qualified-name.js";
31
+ import { resolveBehavior } from "../dialect-behavior/registry.js";
30
32
  /** Record a freshly built node's correlation to the tag it came from, then return it unchanged
31
33
  * (a passthrough so call sites stay expression-shaped). A scalar-slot tag lowers to BOTH a
32
34
  * column Expr (`kind: "column"`) and a parallel ColumnRef record (`kind: "columnref"`, same
@@ -57,7 +59,16 @@ export function applyTemplateTags(ast, tags, text, provider) {
57
59
  const relTags = tags.filter((t) => (t.kind === "call" && !t.incomplete) || t.kind === "other");
58
60
  if (relTags.length === 0)
59
61
  return { ast, byNode, byTag };
60
- const ctx = { relTags, sets: resolveSets(tags, text, provider), text, provider, byNode, byTag };
62
+ const nameConfig = ast.dialect !== undefined ? resolveBehavior(ast.dialect).nameConfig : undefined;
63
+ const ctx = {
64
+ relTags,
65
+ sets: resolveSets(tags, text, provider),
66
+ text,
67
+ provider,
68
+ byNode,
69
+ byTag,
70
+ ...(nameConfig ? { nameConfig } : {}),
71
+ };
61
72
  const next = transformQuery(ast, ctx);
62
73
  // Scalar-slot marking: every column-shaped node whose token is a tag's placeholder
63
74
  // fill gets a `template` marker (span + provider key), so inference resolves it
@@ -407,6 +418,18 @@ function transformTableSource(src, ctx) {
407
418
  // escape as a relation name (scope sources, lineage dependencies, go-to-def) is fabrication
408
419
  // under the never-wrong rule (issue #35, reported by anvil).
409
420
  const rawTagName = [ctx.text.slice(tag.tagSpan.start, tag.tagSpan.end)];
421
+ // Renaming a source rebuilds `relation` (#38): a provider-resolved name is SYNTHESIZED (plain
422
+ // logical parts, quoted where rendering needs it); the raw-tag-text fallback is source text.
423
+ // Without a dialect tag on the ast (never the parseTemplated path) the source keeps its
424
+ // original relation — the documented degrade.
425
+ const renamed = (b, parts, synthesized) => {
426
+ if (!ctx.nameConfig)
427
+ return b;
428
+ const relation = synthesized
429
+ ? synthesizedQualifiedName(parts, ctx.nameConfig)
430
+ : qualifiedNameOf(parts, ctx.nameConfig);
431
+ return { ...b, relation };
432
+ };
410
433
  // NOTE: `template.span` intentionally aliases `tag.tagSpan` BY REFERENCE. freezeIR
411
434
  // therefore also freezes the TagNode.tagSpan object returned in `.tags`, benign
412
435
  // since spans are read-only. Every call marker carries its `call`, the provider key
@@ -418,7 +441,7 @@ function transformTableSource(src, ctx) {
418
441
  // relation, carry the resolved name; otherwise the raw tag text (never the fill).
419
442
  // Either way the `call` keeps it consultable, so an unresolved call is not a dead end: a
420
443
  // provider added later resolves it. ref vs source is not stored here, it is call.name.
421
- const named = { ...base, name: rel ? [...rel.nameParts] : rawTagName };
444
+ const named = rel ? renamed(base, [...rel.nameParts], true) : renamed(base, rawTagName, false);
422
445
  const template = { kind: "call", span: tag.tagSpan, call };
423
446
  return attach(ctx, { ...named, template }, tag);
424
447
  }
@@ -429,10 +452,10 @@ function transformTableSource(src, ctx) {
429
452
  const ident = tag.kind === "other" ? bareIdentOf(tag, ctx.text) : undefined;
430
453
  const resolved = ident !== undefined ? ctx.sets.get(ident) : undefined;
431
454
  if (resolved) {
432
- const named = { ...base, name: resolved.name ? [...resolved.name] : rawTagName };
455
+ const named = resolved.name ? renamed(base, [...resolved.name], true) : renamed(base, rawTagName, false);
433
456
  const template = { kind: "call", span: tag.tagSpan, indirect: true, call: resolved.call };
434
457
  return attach(ctx, { ...named, template }, tag);
435
458
  }
436
459
  const template = { kind: "expr", span: tag.tagSpan, opaque: true };
437
- return attach(ctx, { ...base, name: rawTagName, template }, tag);
460
+ return attach(ctx, { ...renamed(base, rawTagName, false), template }, tag);
438
461
  }
@@ -1,7 +1,7 @@
1
1
  // The mysql DialectBehavior: everything the semantic layer needs for mysql, assembled from
2
2
  // this folder's own pieces. The registry wires it; nothing here reaches a central per-dialect table.
3
3
  import { mysqlLiteral, mysqlParseType, MYSQL_FUNCTION_RETURNS } from "./infer.js";
4
- import { displayName, fold, foldTableName, matchesSourceKey } from "./fold.js";
4
+ import { displayName, fold, foldTableName, matchesSourceKey, MYSQL_NAME_CONFIG } from "./fold.js";
5
5
  import { likePatternToRegExp } from "../scope/like-pattern.js";
6
6
  import { SIGNATURES } from "../signature/signatures.js";
7
7
  import { acceptsFor } from "../dialect-behavior/coerce-rules.js";
@@ -10,6 +10,7 @@ export const mysqlBehavior = {
10
10
  displayName,
11
11
  foldTableName,
12
12
  matchesSourceKey,
13
+ nameConfig: MYSQL_NAME_CONFIG,
13
14
  likeMatch: (pattern, value) => likePatternToRegExp(pattern).test(value),
14
15
  literal: mysqlLiteral,
15
16
  parseType: mysqlParseType,
@@ -1,5 +1,9 @@
1
1
  import { type FoldRule, type IdentKind } from "../ident/fold.js";
2
+ import type { QualifiedNameConfig } from "../ir/qualified-name.js";
2
3
  export declare const MYSQL_FOLD_RULE: FoldRule;
4
+ /** schema.table, two levels — MySQL's database IS its schema
5
+ * (dev.mysql.com/doc/refman/8.4/en/identifier-qualifiers.html). */
6
+ export declare const MYSQL_NAME_CONFIG: QualifiedNameConfig;
3
7
  /** Fold an identifier to its MySQL identity key. */
4
8
  export declare function fold(raw: string, kind?: IdentKind): string;
5
9
  /** Presentation twin: strip delimiters, no case change. */
@@ -33,6 +33,12 @@ export const MYSQL_FOLD_RULE = {
33
33
  unquoted: "lower",
34
34
  quoted: "lower",
35
35
  };
36
+ /** schema.table, two levels — MySQL's database IS its schema
37
+ * (dev.mysql.com/doc/refman/8.4/en/identifier-qualifiers.html). */
38
+ export const MYSQL_NAME_CONFIG = {
39
+ roles: ["schema"],
40
+ rule: MYSQL_FOLD_RULE,
41
+ };
36
42
  /** Fold an identifier to its MySQL identity key. */
37
43
  export function fold(raw, kind = "other") {
38
44
  return foldWith(MYSQL_FOLD_RULE, raw, kind);
@@ -3,6 +3,13 @@ import { MysqlParser as P } from "../generated/mysql/MysqlParser.js";
3
3
  import { keywordCategory, swallowedCategories, swallowedStatements } from "../ir/statement.js";
4
4
  import { collapsePartSpans, dotIdPartSpanOf, partSpanOf } from "../ir/part-span.js";
5
5
  import { freezeIR } from "../ir/freeze.js";
6
+ import { qualifiedNameOf } from "../ir/qualified-name.js";
7
+ import { MYSQL_NAME_CONFIG } from "./fold.js";
8
+ /** The structured name for a table source's raw parts (issue #38) — role assignment + identity
9
+ * key + fqn happen HERE, at lowering, where the dialect's namespace shape is known. */
10
+ function relationOf(rawParts) {
11
+ return qualifiedNameOf(rawParts, MYSQL_NAME_CONFIG);
12
+ }
6
13
  // ---------------------------------------------------------------------------
7
14
  // Lowering — MySQL (grammars-v4 sql/mysql/Positive-Technologies fork, Ivan
8
15
  // Kochurkin's split MySqlLexer/MySqlParser) CST -> the shared, dialect-neutral
@@ -636,7 +643,7 @@ function buildFrom(fromClause) {
636
643
  // tableSource: tableSourceItem joinPart* | '(' tableSourceItem joinPart* ')' | jsonTable.
637
644
  const jt = directChildrenOfRule(ts, P.RULE_jsonTable)[0];
638
645
  if (jt) {
639
- from.push({ kind: "table", name: [jt.getText()], cst: jt });
646
+ from.push({ kind: "table", relation: relationOf([jt.getText()]), cst: jt });
640
647
  continue;
641
648
  }
642
649
  const base = directChildrenOfRule(ts, P.RULE_tableSourceItem)[0];
@@ -709,16 +716,24 @@ function buildSourceItem(item, fromSubqueries) {
709
716
  const seq = directChildrenOfRule(item, P.RULE_sequenceFunctionName)[0];
710
717
  if (seq) {
711
718
  const alias = directChildrenOfRule(item, P.RULE_uid)[0];
712
- return [{ kind: "table", name: [seq.getText()], alias: alias?.getText(), aliasCst: alias, cst: item }];
719
+ return [
720
+ {
721
+ kind: "table",
722
+ relation: relationOf([seq.getText()]),
723
+ alias: alias?.getText(),
724
+ aliasCst: alias,
725
+ cst: item,
726
+ },
727
+ ];
713
728
  }
714
- return [{ kind: "table", name: [item.getText()], cst: item }];
729
+ return [{ kind: "table", relation: relationOf([item.getText()]), cst: item }];
715
730
  }
716
731
  function tableSourceFromName(tn, alias, cst) {
717
732
  const fullId = directChildrenOfRule(tn, P.RULE_fullId)[0] ?? tn;
718
733
  const { parts, spans } = dottedParts(fullId);
719
734
  return {
720
735
  kind: "table",
721
- name: parts.length ? parts : [tn.getText()],
736
+ relation: relationOf(parts.length ? parts : [tn.getText()]),
722
737
  namePartSpans: collapsePartSpans(spans),
723
738
  alias: alias?.getText(),
724
739
  aliasCst: alias,
@@ -1,7 +1,7 @@
1
1
  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
- import { displayName, fold, foldTableName, matchesSourceKey } from "./fold.js";
4
+ import { displayName, fold, foldTableName, matchesSourceKey, POSTGRES_NAME_CONFIG } from "./fold.js";
5
5
  import { postgresLiteral, postgresParseType, POSTGRES_FUNCTION_RETURNS } from "./infer.js";
6
6
  // PostgreSQL implicit coercion: str->num=true, bool<->num=false.
7
7
  export const postgresBehavior = {
@@ -9,6 +9,7 @@ export const postgresBehavior = {
9
9
  displayName,
10
10
  foldTableName,
11
11
  matchesSourceKey,
12
+ nameConfig: POSTGRES_NAME_CONFIG,
12
13
  likeMatch: (pattern, value) => likePatternToRegExp(pattern).test(value),
13
14
  literal: postgresLiteral,
14
15
  parseType: postgresParseType,
@@ -1,5 +1,10 @@
1
1
  import { type FoldRule, type IdentKind } from "../ident/fold.js";
2
+ import type { QualifiedNameConfig } from "../ir/qualified-name.js";
2
3
  export declare const POSTGRES_FOLD_RULE: FoldRule;
4
+ /** database.schema.table (postgresql.org/docs/18/sql-syntax-lexical + ddl-schemas — cross-
5
+ * database references are rejected at runtime but the three-part form parses). Normalized
6
+ * vocabulary: catalog = database. */
7
+ export declare const POSTGRES_NAME_CONFIG: QualifiedNameConfig;
3
8
  /** Fold an identifier to its PostgreSQL identity key. */
4
9
  export declare function fold(raw: string, kind?: IdentKind): string;
5
10
  /** Presentation twin: strip delimiters, no case change. */
@@ -20,6 +20,13 @@ export const POSTGRES_FOLD_RULE = {
20
20
  unquoted: "ascii-lower",
21
21
  quoted: "preserve",
22
22
  };
23
+ /** database.schema.table (postgresql.org/docs/18/sql-syntax-lexical + ddl-schemas — cross-
24
+ * database references are rejected at runtime but the three-part form parses). Normalized
25
+ * vocabulary: catalog = database. */
26
+ export const POSTGRES_NAME_CONFIG = {
27
+ roles: ["catalog", "schema"],
28
+ rule: POSTGRES_FOLD_RULE,
29
+ };
23
30
  /** Fold an identifier to its PostgreSQL identity key. */
24
31
  export function fold(raw, kind = "other") {
25
32
  return foldWith(POSTGRES_FOLD_RULE, raw, kind);
@@ -3,7 +3,13 @@ import { PostgresParser as P } from "../generated/postgres/PostgresParser.js";
3
3
  import { keywordCategory, swallowedCategories, swallowedStatements } from "../ir/statement.js";
4
4
  import { partSpansOf } from "../ir/part-span.js";
5
5
  import { freezeIR } from "../ir/freeze.js";
6
- import { displayName } from "./fold.js";
6
+ import { qualifiedNameOf } from "../ir/qualified-name.js";
7
+ import { displayName, POSTGRES_NAME_CONFIG } from "./fold.js";
8
+ /** The structured name for a table source's raw parts (issue #38) — role assignment + identity
9
+ * key + fqn happen HERE, at lowering, where the dialect's namespace shape is known. */
10
+ function relationOf(rawParts) {
11
+ return qualifiedNameOf(rawParts, POSTGRES_NAME_CONFIG);
12
+ }
7
13
  // ---------------------------------------------------------------------------
8
14
  // Lowering — PostgreSQL (bytebase/parser postgresql/ fork, TVL-lineage grammar)
9
15
  // CST -> the shared dialect-neutral IR (src/ir/ir.ts). The semantic layer runs
@@ -559,7 +565,7 @@ function buildPrimarySource(tr, unsupported) {
559
565
  if (jsonTable) {
560
566
  return {
561
567
  kind: "table",
562
- name: ["json_table"],
568
+ relation: relationOf(["json_table"]),
563
569
  alias,
564
570
  aliasCst,
565
571
  columnAliases: columnAliases ?? jsonTableColumns(jsonTable),
@@ -571,9 +577,10 @@ function buildPrimarySource(tr, unsupported) {
571
577
  if (funcTable) {
572
578
  const fname = firstShallow(funcTable, P.RULE_func_name);
573
579
  const funcAlias = directChildrenOfRule(tr, P.RULE_func_alias_clause)[0];
580
+ const funcTableName = [fname ? lastName(fname) : funcTable.getText()];
574
581
  return {
575
582
  kind: "table",
576
- name: [fname ? lastName(fname) : funcTable.getText()],
583
+ relation: relationOf(funcTableName),
577
584
  alias: alias ?? (funcAlias ? funcAliasName(funcAlias) : undefined),
578
585
  aliasCst,
579
586
  cst: tr,
@@ -592,7 +599,8 @@ function buildPrimarySource(tr, unsupported) {
592
599
  if (innerFrom.length)
593
600
  return innerFrom[0];
594
601
  }
595
- return { kind: "table", name: [textOrEmpty(tr)], alias, aliasCst, columnAliases, cst: tr };
602
+ const tableName = [textOrEmpty(tr)];
603
+ return { kind: "table", relation: relationOf(tableName), alias, aliasCst, columnAliases, cst: tr };
596
604
  }
597
605
  /** The output column names of a JSON_TABLE: every named column definition, NESTED levels included
598
606
  * (they flatten into the same row type). */
@@ -618,7 +626,15 @@ function buildTableFromRelation(rel, alias, aliasCst, columnAliases) {
618
626
  const qn = directChildrenOfRule(rel, P.RULE_qualified_name)[0];
619
627
  const parts = qn ? nameParts(qn) : [textOrEmpty(rel)];
620
628
  const namePartSpans = qn ? columnPartSpans(qn) : undefined;
621
- return { kind: "table", name: parts, namePartSpans, alias, aliasCst, columnAliases, cst: rel };
629
+ return {
630
+ kind: "table",
631
+ relation: relationOf(parts),
632
+ namePartSpans,
633
+ alias,
634
+ aliasCst,
635
+ columnAliases,
636
+ cst: rel,
637
+ };
622
638
  }
623
639
  function aliasName(aliasClause) {
624
640
  const ta = firstShallow(aliasClause, P.RULE_table_alias);
@@ -4,7 +4,7 @@ import type { Column } from "./schema.js";
4
4
  import { type SchemaProvider } from "./schema-provider.js";
5
5
  import { type ResolvedColumn } from "../sema/resolve.js";
6
6
  export interface Diagnostic {
7
- kind: "unknown-table" | "unknown-column" | "ambiguous-column" | "unknown-field" | "wrong-arity" | "wrong-argument-type";
7
+ kind: "unknown-table" | "ambiguous-table" | "unknown-column" | "ambiguous-column" | "unknown-field" | "wrong-arity" | "wrong-argument-type";
8
8
  message: string;
9
9
  /** Start of the offending node: 1-based line, 0-based column. */
10
10
  line: number;