@xdbml/parse 0.3.2 → 0.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.
package/dist/parser.js CHANGED
@@ -13,11 +13,37 @@ import { TokenKind, tokenize, } from "./lexer.js";
13
13
  import { resolveImport, classifyModuleSource, ModuleSourceError } from "./module-resolver.js";
14
14
  export class ParseError extends Error {
15
15
  position;
16
- constructor(message, position) {
16
+ /**
17
+ * Optional machine-readable reason. Set for the errors a caller may want
18
+ * to tell apart from a plain syntax error: `unsupported-version` when a
19
+ * document declares a newer version than this parser supports (spec 4.1).
20
+ */
21
+ code;
22
+ constructor(message, position, code) {
17
23
  super(`${message} (line ${position.line}, column ${position.column})`);
18
24
  this.position = position;
25
+ if (code)
26
+ this.code = code;
19
27
  }
20
28
  }
29
+ /**
30
+ * The newest specification version this parser implements. A document
31
+ * declaring a later version is refused (spec 4.1) rather than parsed with
32
+ * semantics it does not have.
33
+ */
34
+ export const SUPPORTED_XDBML_VERSION = '0.5';
35
+ /** Compare dotted version strings numerically: -1, 0 or 1. */
36
+ export function compareVersions(a, b) {
37
+ const pa = a.split('.').map((n) => Number(n) || 0);
38
+ const pb = b.split('.').map((n) => Number(n) || 0);
39
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
40
+ const x = pa[i] ?? 0;
41
+ const y = pb[i] ?? 0;
42
+ if (x !== y)
43
+ return x < y ? -1 : 1;
44
+ }
45
+ return 0;
46
+ }
21
47
  /* -------------------------------------------------------------------------
22
48
  * Keyword recognition.
23
49
  *
@@ -31,13 +57,13 @@ const CONTAINER_KEYWORDS = new Set([
31
57
  const ENTITY_KEYWORDS = new Set(['table', 'entity', 'collection', 'record']);
32
58
  /**
33
59
  * Element-type keywords accepted in module-system import items
34
- * (spec §26.3). Stored lowercased; matching is case-insensitive.
60
+ * (spec §27.3). Stored lowercased; matching is case-insensitive.
35
61
  *
36
62
  * `field` is recognized but explicitly rejected by parseImportItem in P4
37
63
  * (field-level imports have special declaration-vs-placement semantics
38
64
  * that will land in a later batch).
39
65
  *
40
- * `project` is intentionally excluded -- spec §26.1 forbids importing
66
+ * `project` is intentionally excluded -- spec §27.1 forbids importing
41
67
  * Project declarations.
42
68
  */
43
69
  const IMPORT_ELEMENT_TYPES = new Set([
@@ -45,7 +71,7 @@ const IMPORT_ELEMENT_TYPES = new Set([
45
71
  'enum', 'tablepartial', 'note',
46
72
  'schema', 'container', 'tablegroup',
47
73
  'type', 'edge', 'view', 'diagramview',
48
- 'field',
74
+ 'field', 'supertypegroup',
49
75
  ]);
50
76
  const STRUCTURAL_TYPE_KEYWORDS = new Set([
51
77
  'object', 'struct', 'record', 'array', 'list', 'map', 'dict', 'dictionary',
@@ -98,7 +124,7 @@ export class Parser {
98
124
  * The set of file paths currently being parsed in the resolution chain.
99
125
  * Used for cycle detection: when resolving a directive whose `from` path
100
126
  * is already in this set, the parser produces an empty clone for that
101
- * directive rather than recursing (matching spec §26.15: cycles are
127
+ * directive rather than recursing (matching spec §27.15: cycles are
102
128
  * allowed; name resolution handles them). The set is passed by reference
103
129
  * across recursive parse() calls so all transitive levels see it.
104
130
  *
@@ -179,6 +205,11 @@ export class Parser {
179
205
  this.advance(); // xdbml
180
206
  this.expect(TokenKind.Colon, "Expected ':' after 'xdbml'");
181
207
  const numTok = this.expect(TokenKind.NumberLiteral, 'Expected version number');
208
+ if (compareVersions(numTok.text, SUPPORTED_XDBML_VERSION) > 0) {
209
+ throw new ParseError(`This document declares 'xdbml: ${numTok.text}', which is newer than the ` +
210
+ `latest version this parser supports (${SUPPORTED_XDBML_VERSION}). ` +
211
+ 'Use a newer parser, or declare a supported version.', numTok.start, 'unsupported-version');
212
+ }
182
213
  return {
183
214
  kind: 'VersionDeclaration',
184
215
  version: numTok.text,
@@ -231,6 +262,8 @@ export class Parser {
231
262
  return this.parseTablePartial();
232
263
  if (k === 'tablegroup')
233
264
  return this.parseTableGroup();
265
+ if (k === 'supertypegroup')
266
+ return this.parseSupertypeGroup();
234
267
  if (k === 'note')
235
268
  return this.parseNoteDeclaration();
236
269
  if (k === 'records')
@@ -477,7 +510,7 @@ export class Parser {
477
510
  };
478
511
  }
479
512
  /**
480
- * Parse a `records { ... }` block inside an entity body (§25.1, implicit
513
+ * Parse a `records { ... }` block inside an entity body (§26.1, implicit
481
514
  * column list). Values are stored as SettingValue cells; row boundaries
482
515
  * are determined by source line (see `parseRecordRow`).
483
516
  */
@@ -497,7 +530,7 @@ export class Parser {
497
530
  };
498
531
  }
499
532
  /**
500
- * Top-level records declaration (§25.2, new in v0.2):
533
+ * Top-level records declaration (§26.2, new in v0.2):
501
534
  *
502
535
  * records users (id, name, email) { ... }
503
536
  * records core.users (id, name, email) { ... }
@@ -587,7 +620,7 @@ export class Parser {
587
620
  span: this.spanFrom(start),
588
621
  };
589
622
  }
590
- /* ----- Module-system directives (spec §26, new in v0.2) ----- */
623
+ /* ----- Module-system directives (spec §27, new in v0.2) ----- */
591
624
  /**
592
625
  * Parse a `use` or `reuse` directive. Called from both the top-level
593
626
  * dispatcher and the Container body dispatcher; the caller indicates
@@ -701,7 +734,7 @@ export class Parser {
701
734
  resolvedPath = result.resolvedPath;
702
735
  break;
703
736
  case 'cycle':
704
- // Per spec §26.15, cycles are allowed; the parser produces a
737
+ // Per spec §27.15, cycles are allowed; the parser produces a
705
738
  // directive with no clone, and name resolution (P6+) is
706
739
  // expected to bridge the cycle. We leave clone undefined.
707
740
  resolvedPath = result.resolvedPath;
@@ -759,12 +792,12 @@ export class Parser {
759
792
  `Expected one of: ${Array.from(IMPORT_ELEMENT_TYPES).join(', ')}.`, elemTok.start);
760
793
  }
761
794
  if (elementType === 'field' && context !== 'file-scope') {
762
- // Spec §26.8: field imports must appear at file scope. Inside a
795
+ // Spec §27.8: field imports must appear at file scope. Inside a
763
796
  // Container body, the field's eventual placement (as a Named Type)
764
797
  // would have no meaningful container scope -- field imports are
765
798
  // always lifted to file scope by flatten(), regardless of where
766
799
  // the directive sits.
767
- throw new ParseError(`Field-level imports must appear at file scope, not inside a Container body (spec §26.8).`, elemTok.start);
800
+ throw new ParseError(`Field-level imports must appear at file scope, not inside a Container body (spec §27.8).`, elemTok.start);
768
801
  }
769
802
  this.advance(); // consume element type keyword
770
803
  // Dotted source path.
@@ -795,12 +828,12 @@ export class Parser {
795
828
  * that match the import items by name and element type (matching is
796
829
  * downstream-consumer's job; the parser is permissive).
797
830
  *
798
- * Per spec §26.6, clone content uses the importing file's vocabulary
831
+ * Per spec §27.6, clone content uses the importing file's vocabulary
799
832
  * (aliases already applied) and is parsed under the importing file's
800
833
  * xdbml version directive.
801
834
  *
802
835
  * Most clone-block content uses TopLevelStatement shapes (Entity, Type,
803
- * Container, etc.). The exception is field imports (§26.8): when the
836
+ * Container, etc.). The exception is field imports (§27.8): when the
804
837
  * directive imports one or more fields via `field <path>` items, the
805
838
  * clone block holds each field as a bare FieldDeclaration with no entity
806
839
  * wrapper. The dispatch below checks whether the next token starts a
@@ -816,7 +849,7 @@ export class Parser {
816
849
  statements.push(this.parseTopLevelStatement());
817
850
  }
818
851
  else {
819
- // Bare field declaration -- the field-import case. Per spec §26.6
852
+ // Bare field declaration -- the field-import case. Per spec §27.6
820
853
  // the field appears without an entity wrapper.
821
854
  statements.push(this.parseFieldDeclaration());
822
855
  }
@@ -1284,7 +1317,7 @@ export class Parser {
1284
1317
  }
1285
1318
  throw new ParseError(`Expected type parameter, got ${t.kind}`, t.start);
1286
1319
  }
1287
- /* ----- Type declaration (§13) ----- */
1320
+ /* ----- Type declaration (§15) ----- */
1288
1321
  parseTypeDecl() {
1289
1322
  const start = this.peek().start;
1290
1323
  this.advance(); // Type
@@ -1293,7 +1326,7 @@ export class Parser {
1293
1326
  //
1294
1327
  // { ... } v0.1 object form, no pre-body settings
1295
1328
  // [ settings ] { ... } v0.1 object form, pre-body settings (permissive)
1296
- // typeExpression v0.2 scalar form (spec §14.7)
1329
+ // typeExpression v0.2 scalar form (spec §15.7)
1297
1330
  // typeExpression [ settings ] v0.2 scalar form with field-level settings
1298
1331
  //
1299
1332
  // Note that LBrace and LBracket are distinct from any start-of-type-expression
@@ -1494,8 +1527,12 @@ export class Parser {
1494
1527
  settings = this.maybeSettingsBlock();
1495
1528
  }
1496
1529
  else if (this.match(TokenKind.LBrace)) {
1497
- // long form: `Ref name { a > b }`
1530
+ // long form: `Ref name { a > b }`, or, since v0.4, with a settings
1531
+ // block after the relationship expression: `Ref name { a > b [flag] }`.
1532
+ // Accepting settings here puts every setting of spec 11.9 in all three
1533
+ // declaration forms; a long form written without one parses as before.
1498
1534
  spec = this.parseRefSpec();
1535
+ settings = this.maybeSettingsBlock();
1499
1536
  this.expect(TokenKind.RBrace, "Expected '}' closing Ref body");
1500
1537
  }
1501
1538
  else {
@@ -1568,7 +1605,7 @@ export class Parser {
1568
1605
  };
1569
1606
  }
1570
1607
  /**
1571
- * Parse a dotted path with the §18 segment vocabulary:
1608
+ * Parse a dotted path with the §20 segment vocabulary:
1572
1609
  *
1573
1610
  * IDENTIFIER -- a field segment
1574
1611
  * .IDENTIFIER -- field
@@ -1742,6 +1779,67 @@ export class Parser {
1742
1779
  span: this.spanFrom(start),
1743
1780
  };
1744
1781
  }
1782
+ /* ----- SupertypeGroup (spec §12, new in v0.5) ----- */
1783
+ /**
1784
+ * `SupertypeGroup <name> [supertype: X, ...] { Sub1 Sub2 [strategy: y] }`.
1785
+ * Members are separated like TableGroup members: newline, comma or
1786
+ * semicolon (spec §3.9). A name is required (§12.1); a writer exporting a
1787
+ * group that has none emits `undefinedGroup1`, `undefinedGroup2`, ... so
1788
+ * the parser never supplies one. Values are validated after parsing, by
1789
+ * `checkSupertypeGroups()`, so an unknown value gets a located diagnostic
1790
+ * rather than stopping the parse.
1791
+ */
1792
+ parseSupertypeGroup() {
1793
+ const start = this.peek().start;
1794
+ this.advance(); // SupertypeGroup
1795
+ const nameTok = this.peek();
1796
+ if (nameTok.kind !== TokenKind.Identifier && nameTok.kind !== TokenKind.QuotedIdentifier) {
1797
+ throw new ParseError('Expected a name after SupertypeGroup. Every group has a name (spec §12.1); ' +
1798
+ 'a group exported without one is written undefinedGroup1, undefinedGroup2, ...', nameTok.start);
1799
+ }
1800
+ const name = this.parseIdentLikeName('SupertypeGroup name');
1801
+ const settings = this.maybeSettingsBlock();
1802
+ this.expect(TokenKind.LBrace, "Expected '{' after SupertypeGroup name and settings");
1803
+ const members = [];
1804
+ while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
1805
+ const t = this.peek();
1806
+ if (t.kind === TokenKind.Identifier || t.kind === TokenKind.QuotedIdentifier) {
1807
+ const memberStart = t.start;
1808
+ this.advance();
1809
+ let n = t.kind === TokenKind.QuotedIdentifier ? (t.value ?? '') : t.text;
1810
+ while (this.check(TokenKind.Dot)) {
1811
+ this.advance();
1812
+ const next = this.peek();
1813
+ if (next.kind !== TokenKind.Identifier && next.kind !== TokenKind.QuotedIdentifier) {
1814
+ throw new ParseError('Expected identifier after dot in subtype path', next.start);
1815
+ }
1816
+ this.advance();
1817
+ n += `.${next.kind === TokenKind.QuotedIdentifier ? (next.value ?? '') : next.text}`;
1818
+ }
1819
+ const memberSettings = this.maybeSettingsBlock();
1820
+ members.push({
1821
+ kind: 'SupertypeGroupMember',
1822
+ name: n,
1823
+ settings: memberSettings,
1824
+ span: this.spanFrom(memberStart),
1825
+ });
1826
+ }
1827
+ else if (t.kind === TokenKind.Semicolon || t.kind === TokenKind.Comma) {
1828
+ this.advance();
1829
+ }
1830
+ else {
1831
+ throw new ParseError(`Unexpected ${t.kind} in SupertypeGroup body; expected a subtype entity name`, t.start);
1832
+ }
1833
+ }
1834
+ this.expect(TokenKind.RBrace, "Expected '}' closing SupertypeGroup");
1835
+ return {
1836
+ kind: 'SupertypeGroupDeclaration',
1837
+ name,
1838
+ settings,
1839
+ members,
1840
+ span: this.spanFrom(start),
1841
+ };
1842
+ }
1745
1843
  /* ----- Indexes ----- */
1746
1844
  parseIndexes() {
1747
1845
  const start = this.peek().start;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Relationship helpers and checks (spec 11.10 through 11.12, new in v0.4).
3
+ *
4
+ * A `Ref` carries its relationship type in its settings block. The
5
+ * `foreign_master` flag marks denormalized replication: the parent endpoint
6
+ * holds the master value for a copy held at the child endpoint. Absence of
7
+ * the flag marks a referential relationship, the foreign key.
8
+ *
9
+ * This module holds three things:
10
+ *
11
+ * 1. `relationshipType()` and `isForeignMaster()` -- one definition of the
12
+ * type test, so the renderer, the MCP server, and any generator agree
13
+ * rather than each re-reading the settings array.
14
+ *
15
+ * 2. `refChildEndpoint()` / `refParentEndpoint()` -- which side of a Ref is
16
+ * the child. The cardinality operator decides: `>` and `-` put the child
17
+ * on the left, `<` puts it on the right. `<>` has no single child.
18
+ *
19
+ * 3. `checkRelationships()` -- the semantic rules of spec 11.11 that the
20
+ * grammar cannot express. Called from `resolveNames()`, so every
21
+ * consumer of the resolver's diagnostics gets them without opting in.
22
+ */
23
+ import type { Diagnostic } from './name-resolver.ts';
24
+ import type { PathSegment, RefDeclaration, RefEndpoint, RefValue, Setting, XDbmlDocument } from './ast.ts';
25
+ export type RelationshipType = 'referential' | 'foreign_master';
26
+ /** The flag name, spelled once. */
27
+ export declare const FOREIGN_MASTER_FLAG = "foreign_master";
28
+ /** Accepted values of the `constraint_type` setting (spec 11.15). */
29
+ export declare const CONSTRAINT_TYPES: readonly ["identifying", "non_identifying"];
30
+ export type ConstraintType = (typeof CONSTRAINT_TYPES)[number];
31
+ /**
32
+ * Relationship settings introduced in v0.4 alongside the foreign master
33
+ * flag. Gated on the declared version like the flag itself, so a document
34
+ * declaring an earlier version does not quietly carry v0.4 semantics.
35
+ */
36
+ export declare const V04_RELATIONSHIP_SETTINGS: readonly ["source_role", "target_role", "source_verb", "target_verb", "constraint_type"];
37
+ /** The `constraint_type` a Ref declares, or undefined when unstated. */
38
+ export declare function constraintType(ref: RefDeclaration): ConstraintType | undefined;
39
+ /** True when the Ref is marked `undirected: true` (spec 11.16.2). */
40
+ export declare function isUndirected(ref: RefDeclaration): boolean;
41
+ /**
42
+ * True when a settings array carries the `foreign_master` flag. The flag
43
+ * takes no value, so presence is the whole test; a `foreign_master: true`
44
+ * spelling is accepted as well rather than silently ignored, since a user
45
+ * reaching for the more explicit form means the same thing.
46
+ */
47
+ export declare function hasForeignMasterFlag(settings: ReadonlyArray<Setting>): boolean;
48
+ /** The relationship type a Ref declares. */
49
+ export declare function relationshipType(ref: RefDeclaration): RelationshipType;
50
+ /** Convenience wrapper over `relationshipType`. */
51
+ export declare function isForeignMaster(ref: RefDeclaration): boolean;
52
+ /**
53
+ * The child endpoint of a Ref, i.e. the side holding the copy (for a foreign
54
+ * master) or the foreign key (for a referential relationship).
55
+ *
56
+ * a.x > b.y many a to one b -> a.x is the child
57
+ * a.x < b.y one a to many b -> b.y is the child
58
+ * a.x - b.y one to one -> a.x is the child, by convention
59
+ * a.x <> b.y many to many -> no single child; returns undefined
60
+ */
61
+ export declare function refChildEndpoint(ref: RefDeclaration): RefEndpoint | undefined;
62
+ /** The parent endpoint of a Ref, i.e. the opposite side of the child. */
63
+ export declare function refParentEndpoint(ref: RefDeclaration): RefEndpoint | undefined;
64
+ /**
65
+ * Every name an entity answers to in a document: its own name, and its
66
+ * container-qualified name where it sits inside one. Mirrors the index the
67
+ * renderer builds, so both agree on what an entity-level path looks like.
68
+ */
69
+ export declare function entityNames(doc: XDbmlDocument): Set<string>;
70
+ /**
71
+ * True when an endpoint names an entity and stops there (spec 11.16), as in
72
+ * `Customer` or `shop.orders`. A structural test is not enough: `b.aid` has
73
+ * the same shape and names a field, so the whole path is checked against the
74
+ * entities the document declares.
75
+ */
76
+ export declare function isEntityLevelEndpoint(endpoint: RefEndpoint, names: Set<string>): boolean;
77
+ /** Render a path as a dotted string, for messages and for identity keys. */
78
+ export declare function pathToString(path: ReadonlyArray<PathSegment>): string;
79
+ /**
80
+ * True when the document declares at least `min`. A document with no version
81
+ * declaration is plain DBML and supports no xDBML construct, so it fails
82
+ * every gate.
83
+ *
84
+ * Kept general rather than special-cased to the foreign master flag: the
85
+ * module-system gates described in the grammar notes are not implemented yet
86
+ * and can adopt this helper when they are.
87
+ */
88
+ export declare function versionAtLeast(doc: XDbmlDocument, min: string): boolean;
89
+ /**
90
+ * Semantic checks for foreign master relationships (spec 11.11), plus the
91
+ * inline-form rule of 11.10.2 and the version gate of section 4.
92
+ *
93
+ * The composite rule is checked here rather than in the grammar so the error
94
+ * can name the flag that makes the composite invalid, instead of failing on
95
+ * a form that is perfectly legal for a referential relationship.
96
+ */
97
+ export declare function checkRelationships(doc: XDbmlDocument): Diagnostic[];
98
+ /** Re-exported for consumers that only need the inline ref value shape. */
99
+ export type { RefValue };