@xdbml/parse 0.3.1 → 0.4.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/index.d.ts CHANGED
@@ -32,6 +32,8 @@ export { flatten } from './module-resolver.ts';
32
32
  export { classifyModuleSource, isUrlKey, ModuleSourceError, } from './module-resolver.ts';
33
33
  export type { ModuleSource } from './module-resolver.ts';
34
34
  export { resolveNames, SymbolTable } from './name-resolver.ts';
35
+ export { FOREIGN_MASTER_FLAG, checkRelationships, hasForeignMasterFlag, isForeignMaster, pathToString, refChildEndpoint, refParentEndpoint, relationshipType, versionAtLeast, CONSTRAINT_TYPES, V04_RELATIONSHIP_SETTINGS, constraintType, entityNames, isEntityLevelEndpoint, isUndirected, } from './relationships.ts';
36
+ export type { RelationshipType, ConstraintType } from './relationships.ts';
35
37
  export type { Diagnostic, DiagnosticCode, ResolutionResult, SymbolEntry, SymbolKind, } from './name-resolver.ts';
36
38
  export { xdbmlLanguageConfig, xdbmlMonarchTokensProvider, } from './monarch.ts';
37
39
  export type { XDbmlLanguageConfiguration, XDbmlMonarchLanguage, } from './monarch.ts';
package/dist/index.js CHANGED
@@ -30,4 +30,5 @@ export { parse, Parser, ParseError } from "./parser.js";
30
30
  export { flatten } from "./module-resolver.js";
31
31
  export { classifyModuleSource, isUrlKey, ModuleSourceError, } from "./module-resolver.js";
32
32
  export { resolveNames, SymbolTable } from "./name-resolver.js";
33
+ export { FOREIGN_MASTER_FLAG, checkRelationships, hasForeignMasterFlag, isForeignMaster, pathToString, refChildEndpoint, refParentEndpoint, relationshipType, versionAtLeast, CONSTRAINT_TYPES, V04_RELATIONSHIP_SETTINGS, constraintType, entityNames, isEntityLevelEndpoint, isUndirected, } from "./relationships.js";
33
34
  export { xdbmlLanguageConfig, xdbmlMonarchTokensProvider, } from "./monarch.js";
@@ -38,8 +38,8 @@ export declare const STRUCTURAL_TYPE_KEYWORDS: readonly ["object", "struct", "ar
38
38
  export declare const POLYMORPHISM_KEYWORDS: readonly ["union", "oneof", "anyof", "allof"];
39
39
  export declare const SCALAR_TYPES: readonly ["tinyint", "smallint", "mediumint", "int", "integer", "bigint", "int32", "int64", "float", "double", "decimal", "dec", "numeric", "real", "bit", "bool", "boolean", "char", "varchar", "varchar2", "nvarchar", "nvarchar2", "nchar", "text", "mediumtext", "longtext", "string", "ntext", "binary", "varbinary", "blob", "mediumblob", "longblob", "tinyblob", "tinytext", "json", "jsonb", "variant", "xml", "date", "time", "datetime", "datetime2", "timestamp", "timestamptz", "year", "uuid", "inet6", "money", "smallmoney", "enum"];
40
40
  export declare const BSON_TYPES: readonly ["objectid", "decimal128", "bindata", "minkey", "maxkey", "symbol", "regex", "long", "double"];
41
- export declare const SETTING_FLAGS: readonly ["pk", "primary", "key", "unique", "null", "not", "required", "increment", "inactive"];
42
- export declare const SETTING_KEYS: readonly ["note", "default", "ref", "name", "color", "headercolor", "as", "check", "type", "target", "targets", "database_type", "source", "source_cardinality", "target_cardinality", "min_source", "max_source", "min_target", "max_target", "undirected", "discriminator", "source_query", "materialized", "refresh_schedule", "refresh_on", "source_database", "storage_options", "pattern", "format", "minlength", "maxlength", "minimum", "maximum", "exclusiveminimum", "exclusivemaximum", "multipleof", "minitems", "maxitems", "uniqueitems", "minproperties", "maxproperties", "synonyms", "business_term", "granularity", "tags", "delete", "update", "indexes", "checks", "replication", "location", "default_charset", "cloned_at"];
41
+ export declare const SETTING_FLAGS: readonly ["pk", "primary", "key", "unique", "null", "not", "required", "increment", "inactive", "foreign_master"];
42
+ export declare const SETTING_KEYS: readonly ["note", "default", "ref", "name", "color", "headercolor", "as", "check", "type", "target", "targets", "database_type", "source", "source_cardinality", "target_cardinality", "min_source", "max_source", "min_target", "max_target", "undirected", "source_role", "target_role", "source_verb", "target_verb", "constraint_type", "discriminator", "source_query", "materialized", "refresh_schedule", "refresh_on", "source_database", "storage_options", "pattern", "format", "minlength", "maxlength", "minimum", "maximum", "exclusiveminimum", "exclusivemaximum", "multipleof", "minitems", "maxitems", "uniqueitems", "minproperties", "maxproperties", "synonyms", "business_term", "granularity", "tags", "delete", "update", "indexes", "checks", "replication", "location", "default_charset", "cloned_at"];
43
43
  export declare const GRANULARITY_VALUES: readonly ["year", "quarter", "month", "week", "day", "hour", "minute", "second", "millisecond", "microsecond", "nanosecond"];
44
44
  export declare const DIRECTIVE_KEYWORDS: readonly ["xdbml", "experimental"];
45
45
  export declare const MODULE_KEYWORDS: readonly ["use", "reuse", "from", "as"];
package/dist/keywords.js CHANGED
@@ -164,6 +164,7 @@ export const SETTING_FLAGS = [
164
164
  'required',
165
165
  'increment',
166
166
  'inactive', // v0.2 §11.9: Ref flag for visualization-only deactivation
167
+ 'foreign_master', // v0.4 §11.10: Ref flag for denormalized replication
167
168
  ];
168
169
  export const SETTING_KEYS = [
169
170
  // General
@@ -188,6 +189,12 @@ export const SETTING_KEYS = [
188
189
  'min_target',
189
190
  'max_target',
190
191
  'undirected',
192
+ // v0.4 relationship documentation (spec 11.14, 11.15)
193
+ 'source_role',
194
+ 'target_role',
195
+ 'source_verb',
196
+ 'target_verb',
197
+ 'constraint_type',
191
198
  'discriminator',
192
199
  'source_query',
193
200
  'materialized',
@@ -67,7 +67,7 @@ export interface SymbolEntry {
67
67
  * Stable diagnostic code. Tooling can match on these to filter or style
68
68
  * messages without parsing the human-readable text.
69
69
  */
70
- export type DiagnosticCode = 'duplicate-declaration' | 'unresolved-type' | 'unresolved-entity' | 'unresolved-field' | 'unresolved-partial' | 'unresolved-tablegroup-member' | 'unresolved-records-entity' | 'unresolved-records-column' | 'empty-import' | 'invalid-nested-path';
70
+ export type DiagnosticCode = 'duplicate-declaration' | 'unresolved-type' | 'unresolved-entity' | 'unresolved-field' | 'unresolved-partial' | 'unresolved-tablegroup-member' | 'unresolved-records-entity' | 'unresolved-records-column' | 'empty-import' | 'invalid-nested-path' | 'foreign-master-composite' | 'foreign-master-duplicate-child' | 'foreign-master-without-ref' | 'construct-requires-version' | 'ambiguous-ref-endpoint' | 'invalid-constraint-type' | 'constraint-type-on-foreign-master' | 'invalid-undirected' | 'entity-level-many-to-many';
71
71
  /**
72
72
  * A single resolution diagnostic. Severity is currently always `error`,
73
73
  * but the field is included to leave room for future warnings (e.g.,
@@ -40,6 +40,7 @@
40
40
  */
41
41
  import { SCALAR_TYPES, BSON_TYPES } from "./keywords.js";
42
42
  import { flatten } from "./module-resolver.js";
43
+ import { checkRelationships } from "./relationships.js";
43
44
  /**
44
45
  * Read-only handle on the collected symbol table.
45
46
  *
@@ -134,6 +135,8 @@ export function resolveNames(doc) {
134
135
  const symbols = new SymbolTable(entries);
135
136
  // Pass 2: resolve references.
136
137
  resolveReferences(flat, symbols, diagnostics);
138
+ // Pass 3: relationship rules that the grammar cannot express (spec 11.11).
139
+ diagnostics.push(...checkRelationships(flat));
137
140
  return { diagnostics, symbols };
138
141
  }
139
142
  /* -------------------------------------------------------------------------
@@ -507,8 +510,28 @@ function resolveRefSpec(endpoint, symbols, diagnostics) {
507
510
  else {
508
511
  maxEntityLen = leadingFields.length - 1;
509
512
  }
510
- if (maxEntityLen < 1)
513
+ // An entity-level endpoint (spec 11.16) names an entity and stops there:
514
+ // `Customer`, or `shop.orders` for an entity inside a container. It is
515
+ // checked as a fallback rather than first, so an endpoint that already
516
+ // resolved as entity-plus-attribute keeps that reading and every document
517
+ // that parsed before this means what it meant before.
518
+ const wholePath = leadingFields.join('.');
519
+ const entityLevelMatch = (!hasComposite && !hasNonFieldTail)
520
+ ? resolveEntityRef(wholePath, symbols)
521
+ : undefined;
522
+ if (maxEntityLen < 1) {
523
+ // A single segment cannot be entity-plus-attribute, so it is either an
524
+ // entity-level endpoint or a reference to something undeclared.
525
+ if (!entityLevelMatch) {
526
+ diagnostics.push({
527
+ severity: 'error',
528
+ code: 'unresolved-entity',
529
+ message: `Relationship endpoint references unknown entity '${wholePath}'.`,
530
+ span: endpoint.span,
531
+ });
532
+ }
511
533
  return;
534
+ }
512
535
  // PHASE 1 (cont'd): longest-prefix entity match.
513
536
  let entity;
514
537
  let entityPrefixLen = 0;
@@ -522,15 +545,29 @@ function resolveRefSpec(endpoint, symbols, diagnostics) {
522
545
  }
523
546
  }
524
547
  if (!entity) {
548
+ if (entityLevelMatch)
549
+ return; // entity-level endpoint; nothing further to check
525
550
  const guess = leadingFields.slice(0, maxEntityLen).join('.');
526
551
  diagnostics.push({
527
552
  severity: 'error',
528
553
  code: 'unresolved-entity',
529
- message: `Foreign-key endpoint references unknown entity '${guess}'.`,
554
+ message: `Relationship endpoint references unknown entity '${guess}'.`,
530
555
  span: endpoint.span,
531
556
  });
532
557
  return;
533
558
  }
559
+ if (entityLevelMatch) {
560
+ // Both readings exist: `a.b` names an entity, and `a` names an entity
561
+ // with a field `b`. The attribute reading wins, and the collision is
562
+ // reported so the author can qualify the path differently.
563
+ diagnostics.push({
564
+ severity: 'warning',
565
+ code: 'ambiguous-ref-endpoint',
566
+ message: `Endpoint '${wholePath}' names both an entity and a field of entity '${entity.qualifiedName}'. ` +
567
+ 'Reading it as the field; rename one of them or qualify the path to remove the ambiguity.',
568
+ span: endpoint.span,
569
+ });
570
+ }
534
571
  if (entity.declaration.kind !== 'EntityDeclaration')
535
572
  return;
536
573
  // PHASE 2: compute the post-entity portion of endpoint.path.
package/dist/parser.d.ts CHANGED
@@ -216,6 +216,15 @@ export declare class Parser {
216
216
  * otherwise the first thing is the bare type.
217
217
  */
218
218
  private parseArrayType;
219
+ /** array/set sugar: fold a following comma-list of union-eligible members
220
+ * (`[T1, T2, ...]`) into a UnionType. Applies only when the element is a
221
+ * scalar/named type with no per-member settings; `null` is allowed as a
222
+ * later member. Object shapes use `oneOf`, positional layouts use a tuple. */
223
+ private maybeFoldUnionSugar;
224
+ /** A type usable as a `union` member (and thus foldable from array sugar).
225
+ * `null` is only ever a later member (parsed via parseUnionMember), never
226
+ * the first standalone element type, so it is not listed here. */
227
+ private isUnionMemberType;
219
228
  /** True if the token looks like the start of a TypeExpression. */
220
229
  private tokenStartsType;
221
230
  private parseTupleElements;
package/dist/parser.js CHANGED
@@ -1020,8 +1020,11 @@ export class Parser {
1020
1020
  && this.tokenStartsType(second)) {
1021
1021
  elementName = this.advance().text;
1022
1022
  }
1023
- const elementType = this.parseTypeExpression();
1023
+ const elemStart = this.peek().start;
1024
+ let elementType = this.parseTypeExpression();
1024
1025
  const elementSettings = this.maybeSettingsBlock();
1026
+ // Sugar: `array [T1, T2, ...]` == `array [union [T1, T2, ...]]` (see helper).
1027
+ elementType = this.maybeFoldUnionSugar(elementType, elemStart, elementSettings.length === 0);
1025
1028
  this.expect(TokenKind.RBracket, "Expected ']' closing array");
1026
1029
  return {
1027
1030
  kind: 'ArrayType',
@@ -1032,6 +1035,33 @@ export class Parser {
1032
1035
  span: this.spanFrom(start),
1033
1036
  };
1034
1037
  }
1038
+ /** array/set sugar: fold a following comma-list of union-eligible members
1039
+ * (`[T1, T2, ...]`) into a UnionType. Applies only when the element is a
1040
+ * scalar/named type with no per-member settings; `null` is allowed as a
1041
+ * later member. Object shapes use `oneOf`, positional layouts use a tuple. */
1042
+ maybeFoldUnionSugar(elementType, elemStart, noElementSettings) {
1043
+ if (noElementSettings
1044
+ && this.check(TokenKind.Comma)
1045
+ && this.isUnionMemberType(elementType)) {
1046
+ const members = [elementType];
1047
+ while (this.match(TokenKind.Comma)) {
1048
+ members.push(this.parseUnionMember());
1049
+ }
1050
+ return {
1051
+ kind: 'UnionType',
1052
+ members,
1053
+ span: this.spanFrom(elemStart),
1054
+ };
1055
+ }
1056
+ return elementType;
1057
+ }
1058
+ /** A type usable as a `union` member (and thus foldable from array sugar).
1059
+ * `null` is only ever a later member (parsed via parseUnionMember), never
1060
+ * the first standalone element type, so it is not listed here. */
1061
+ isUnionMemberType(t) {
1062
+ return t.kind === 'ScalarType'
1063
+ || t.kind === 'NamedTypeReference';
1064
+ }
1035
1065
  /** True if the token looks like the start of a TypeExpression. */
1036
1066
  tokenStartsType(t) {
1037
1067
  if (t.kind === TokenKind.LBrace)
@@ -1095,7 +1125,11 @@ export class Parser {
1095
1125
  const start = this.peek().start;
1096
1126
  this.advance(); // set
1097
1127
  this.expect(TokenKind.LBracket, "Expected '[' after set");
1098
- const elementType = this.parseTypeExpression();
1128
+ const elemStart = this.peek().start;
1129
+ let elementType = this.parseTypeExpression();
1130
+ // Sugar: `set [T1, T2, ...]` == `set [union [T1, T2, ...]]` (set has no
1131
+ // per-element settings, so the fold is always eligible).
1132
+ elementType = this.maybeFoldUnionSugar(elementType, elemStart, true);
1099
1133
  this.expect(TokenKind.RBracket, "Expected ']' closing set");
1100
1134
  return {
1101
1135
  kind: 'SetType',
@@ -1460,8 +1494,12 @@ export class Parser {
1460
1494
  settings = this.maybeSettingsBlock();
1461
1495
  }
1462
1496
  else if (this.match(TokenKind.LBrace)) {
1463
- // long form: `Ref name { a > b }`
1497
+ // long form: `Ref name { a > b }`, or, since v0.4, with a settings
1498
+ // block after the relationship expression: `Ref name { a > b [flag] }`.
1499
+ // Accepting settings here puts every setting of spec 11.9 in all three
1500
+ // declaration forms; a long form written without one parses as before.
1464
1501
  spec = this.parseRefSpec();
1502
+ settings = this.maybeSettingsBlock();
1465
1503
  this.expect(TokenKind.RBrace, "Expected '}' closing Ref body");
1466
1504
  }
1467
1505
  else {
@@ -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 };
@@ -0,0 +1,436 @@
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
+ /** The flag name, spelled once. */
24
+ export const FOREIGN_MASTER_FLAG = 'foreign_master';
25
+ /** Accepted values of the `constraint_type` setting (spec 11.15). */
26
+ export const CONSTRAINT_TYPES = ['identifying', 'non_identifying'];
27
+ /**
28
+ * Relationship settings introduced in v0.4 alongside the foreign master
29
+ * flag. Gated on the declared version like the flag itself, so a document
30
+ * declaring an earlier version does not quietly carry v0.4 semantics.
31
+ */
32
+ export const V04_RELATIONSHIP_SETTINGS = [
33
+ 'source_role',
34
+ 'target_role',
35
+ 'source_verb',
36
+ 'target_verb',
37
+ 'constraint_type',
38
+ ];
39
+ /** The `constraint_type` a Ref declares, or undefined when unstated. */
40
+ export function constraintType(ref) {
41
+ const s = ref.settings.find((x) => x.name === 'constraint_type');
42
+ if (!s || !s.value)
43
+ return undefined;
44
+ const raw = settingText(s);
45
+ return CONSTRAINT_TYPES.includes(raw)
46
+ ? raw
47
+ : undefined;
48
+ }
49
+ /** True when the Ref is marked `undirected: true` (spec 11.16.2). */
50
+ export function isUndirected(ref) {
51
+ const s = ref.settings.find((x) => x.name === 'undirected');
52
+ return !!s && !!s.value && settingText(s) === 'true';
53
+ }
54
+ /** The text of a setting value, for the string-ish value kinds. */
55
+ function settingText(s) {
56
+ const v = s.value;
57
+ if (!v)
58
+ return '';
59
+ switch (v.kind) {
60
+ case 'StringValue':
61
+ case 'IdentifierValue':
62
+ case 'NumberValue':
63
+ return String(v.value);
64
+ case 'BooleanValue':
65
+ return String(v.value);
66
+ default:
67
+ return '';
68
+ }
69
+ }
70
+ /**
71
+ * True when a settings array carries the `foreign_master` flag. The flag
72
+ * takes no value, so presence is the whole test; a `foreign_master: true`
73
+ * spelling is accepted as well rather than silently ignored, since a user
74
+ * reaching for the more explicit form means the same thing.
75
+ */
76
+ export function hasForeignMasterFlag(settings) {
77
+ return settings.some((s) => s.name === FOREIGN_MASTER_FLAG);
78
+ }
79
+ /** The relationship type a Ref declares. */
80
+ export function relationshipType(ref) {
81
+ return hasForeignMasterFlag(ref.settings) ? 'foreign_master' : 'referential';
82
+ }
83
+ /** Convenience wrapper over `relationshipType`. */
84
+ export function isForeignMaster(ref) {
85
+ return relationshipType(ref) === 'foreign_master';
86
+ }
87
+ /* -------------------------------------------------------------------------
88
+ * Which end is the child
89
+ * ----------------------------------------------------------------------- */
90
+ /**
91
+ * The child endpoint of a Ref, i.e. the side holding the copy (for a foreign
92
+ * master) or the foreign key (for a referential relationship).
93
+ *
94
+ * a.x > b.y many a to one b -> a.x is the child
95
+ * a.x < b.y one a to many b -> b.y is the child
96
+ * a.x - b.y one to one -> a.x is the child, by convention
97
+ * a.x <> b.y many to many -> no single child; returns undefined
98
+ */
99
+ export function refChildEndpoint(ref) {
100
+ switch (ref.spec.operator) {
101
+ case '>':
102
+ case '-':
103
+ return ref.spec.source;
104
+ case '<':
105
+ return ref.spec.target;
106
+ default:
107
+ return undefined;
108
+ }
109
+ }
110
+ /** The parent endpoint of a Ref, i.e. the opposite side of the child. */
111
+ export function refParentEndpoint(ref) {
112
+ switch (ref.spec.operator) {
113
+ case '>':
114
+ case '-':
115
+ return ref.spec.target;
116
+ case '<':
117
+ return ref.spec.source;
118
+ default:
119
+ return undefined;
120
+ }
121
+ }
122
+ /**
123
+ * Every name an entity answers to in a document: its own name, and its
124
+ * container-qualified name where it sits inside one. Mirrors the index the
125
+ * renderer builds, so both agree on what an entity-level path looks like.
126
+ */
127
+ export function entityNames(doc) {
128
+ const names = new Set();
129
+ const add = (name, container) => {
130
+ names.add(name);
131
+ if (container)
132
+ names.add(`${container}.${name}`);
133
+ };
134
+ for (const stmt of doc.statements) {
135
+ if (stmt.kind === 'EntityDeclaration')
136
+ add(stmt.name);
137
+ else if (stmt.kind === 'ContainerDeclaration') {
138
+ for (const item of stmt.body) {
139
+ if (item.kind === 'EntityDeclaration')
140
+ add(item.name, stmt.name);
141
+ }
142
+ }
143
+ }
144
+ return names;
145
+ }
146
+ /**
147
+ * True when an endpoint names an entity and stops there (spec 11.16), as in
148
+ * `Customer` or `shop.orders`. A structural test is not enough: `b.aid` has
149
+ * the same shape and names a field, so the whole path is checked against the
150
+ * entities the document declares.
151
+ */
152
+ export function isEntityLevelEndpoint(endpoint, names) {
153
+ if (endpoint.compositeFields && endpoint.compositeFields.length > 0)
154
+ return false;
155
+ if (!endpoint.path.every((seg) => seg.kind === 'PathField'))
156
+ return false;
157
+ return names.has(pathToString(endpoint.path));
158
+ }
159
+ /** Render a path as a dotted string, for messages and for identity keys. */
160
+ export function pathToString(path) {
161
+ return path
162
+ .map((seg) => {
163
+ switch (seg.kind) {
164
+ case 'PathField': return seg.name;
165
+ case 'PathArrayIndex': return `[${seg.index}]`;
166
+ case 'PathArrayWildcard': return '[*]';
167
+ case 'PathMapKey': return `[${seg.key}]`;
168
+ default: return '?';
169
+ }
170
+ })
171
+ .join('.');
172
+ }
173
+ /* -------------------------------------------------------------------------
174
+ * Version gating
175
+ * ----------------------------------------------------------------------- */
176
+ /**
177
+ * True when the document declares at least `min`. A document with no version
178
+ * declaration is plain DBML and supports no xDBML construct, so it fails
179
+ * every gate.
180
+ *
181
+ * Kept general rather than special-cased to the foreign master flag: the
182
+ * module-system gates described in the grammar notes are not implemented yet
183
+ * and can adopt this helper when they are.
184
+ */
185
+ export function versionAtLeast(doc, min) {
186
+ if (!doc.version)
187
+ return false;
188
+ const parse = (v) => v.split('.').map((n) => Number(n) || 0);
189
+ const have = parse(doc.version.version);
190
+ const want = parse(min);
191
+ for (let i = 0; i < Math.max(have.length, want.length); i++) {
192
+ const a = have[i] ?? 0;
193
+ const b = want[i] ?? 0;
194
+ if (a !== b)
195
+ return a > b;
196
+ }
197
+ return true;
198
+ }
199
+ /* -------------------------------------------------------------------------
200
+ * Checks
201
+ * ----------------------------------------------------------------------- */
202
+ /**
203
+ * Semantic checks for foreign master relationships (spec 11.11), plus the
204
+ * inline-form rule of 11.10.2 and the version gate of section 4.
205
+ *
206
+ * The composite rule is checked here rather than in the grammar so the error
207
+ * can name the flag that makes the composite invalid, instead of failing on
208
+ * a form that is perfectly legal for a referential relationship.
209
+ */
210
+ export function checkRelationships(doc) {
211
+ const diagnostics = [];
212
+ // Child endpoints already claimed by a foreign master, so a second one
213
+ // into the same attribute can be reported. Keyed by the dotted path.
214
+ const claimedChildren = new Map();
215
+ let sawForeignMaster = false;
216
+ const noteForeignMaster = () => { sawForeignMaster = true; };
217
+ /* ---- top-level and container-level Ref declarations ---- */
218
+ const declaredEntities = entityNames(doc);
219
+ // Any v0.4 relationship setting present anywhere in the document, so the
220
+ // version gate below covers the documentation settings as well as the flag.
221
+ let sawV04Setting = false;
222
+ const checkRelationshipSettings = (ref) => {
223
+ const entityLevel = isEntityLevelEndpoint(ref.spec.source, declaredEntities)
224
+ && isEntityLevelEndpoint(ref.spec.target, declaredEntities);
225
+ for (const setting of ref.settings) {
226
+ if (V04_RELATIONSHIP_SETTINGS.includes(setting.name)) {
227
+ sawV04Setting = true;
228
+ }
229
+ // 11.15: the value vocabulary is closed, so a typo is an error rather
230
+ // than a silently ignored setting.
231
+ if (setting.name === 'constraint_type') {
232
+ const raw = settingText(setting);
233
+ if (!CONSTRAINT_TYPES.includes(raw)) {
234
+ diagnostics.push({
235
+ severity: 'error',
236
+ code: 'invalid-constraint-type',
237
+ message: `'${raw || '(no value)'}' is not a constraint type. ` +
238
+ `Use ${CONSTRAINT_TYPES.map((v) => `'${v}'`).join(' or ')}.`,
239
+ span: setting.span,
240
+ });
241
+ }
242
+ else if (isForeignMaster(ref)) {
243
+ // 11.15: a foreign master carries no key dependency, so there is
244
+ // nothing for identifying or non-identifying to describe.
245
+ diagnostics.push({
246
+ severity: 'error',
247
+ code: 'constraint-type-on-foreign-master',
248
+ message: "'constraint_type' does not apply to a foreign master relationship, which carries no key dependency.",
249
+ span: setting.span,
250
+ });
251
+ }
252
+ }
253
+ if (setting.name === 'undirected') {
254
+ const raw = settingText(setting);
255
+ if (raw !== 'true' && raw !== 'false') {
256
+ diagnostics.push({
257
+ severity: 'error',
258
+ code: 'invalid-undirected',
259
+ message: `'undirected' takes true or false, not '${raw || '(no value)'}'.`,
260
+ span: setting.span,
261
+ });
262
+ }
263
+ }
264
+ }
265
+ // 11.16.2: many-to-many states a cardinality, and an entity-level
266
+ // relationship states none. A many-to-many that carries its own
267
+ // attributes is an Edge.
268
+ if (entityLevel && ref.spec.operator === '<>') {
269
+ diagnostics.push({
270
+ severity: 'error',
271
+ code: 'entity-level-many-to-many',
272
+ message: "'<>' is not available on a relationship between entities: many-to-many states a cardinality, " +
273
+ 'which an entity-level relationship leaves unstated. Declare an Edge if the relationship carries its own attributes.',
274
+ span: ref.spec.span ?? ref.span,
275
+ });
276
+ }
277
+ };
278
+ const checkRefDeclaration = (ref) => {
279
+ checkRelationshipSettings(ref);
280
+ if (!isForeignMaster(ref))
281
+ return;
282
+ noteForeignMaster();
283
+ // 11.11: neither endpoint may be composite.
284
+ for (const [label, endpoint] of [
285
+ ['source', ref.spec.source],
286
+ ['target', ref.spec.target],
287
+ ]) {
288
+ if (endpoint.compositeFields && endpoint.compositeFields.length > 0) {
289
+ diagnostics.push({
290
+ severity: 'error',
291
+ code: 'foreign-master-composite',
292
+ message: `A foreign master relationship takes a single attribute on each side; the ${label} endpoint is composite. ` +
293
+ 'Write one foreign master relationship per duplicated attribute.',
294
+ span: endpoint.span,
295
+ });
296
+ }
297
+ }
298
+ // 11.11: at most one master per child attribute.
299
+ const child = refChildEndpoint(ref);
300
+ if (child) {
301
+ const key = pathToString(child.path);
302
+ const previous = claimedChildren.get(key);
303
+ if (previous) {
304
+ diagnostics.push({
305
+ severity: 'error',
306
+ code: 'foreign-master-duplicate-child',
307
+ message: `'${key}' already has a foreign master. A copied attribute has one master, ` +
308
+ 'so a child attribute is the child of at most one foreign master relationship.',
309
+ span: child.span,
310
+ });
311
+ }
312
+ else {
313
+ claimedChildren.set(key, child.span);
314
+ }
315
+ }
316
+ };
317
+ /* ---- inline refs on fields ---- */
318
+ const checkField = (field, ownerPath) => {
319
+ const flag = field.settings.find((s) => s.name === FOREIGN_MASTER_FLAG);
320
+ if (!flag)
321
+ return;
322
+ noteForeignMaster();
323
+ const inlineRef = field.settings.find((s) => s.value && s.value.kind === 'RefValue');
324
+ // 11.10.2: the flag qualifies an inline ref: and is an error without one.
325
+ if (!inlineRef) {
326
+ diagnostics.push({
327
+ severity: 'error',
328
+ code: 'foreign-master-without-ref',
329
+ message: "The 'foreign_master' flag qualifies an inline 'ref:' in the same settings block; " +
330
+ 'this field declares no inline ref.',
331
+ span: flag.span,
332
+ });
333
+ return;
334
+ }
335
+ // 11.11: at most one master per child attribute. For an inline ref the
336
+ // child is always the field carrying the setting, whatever the operator.
337
+ const key = `${ownerPath}.${field.name}`;
338
+ const previous = claimedChildren.get(key);
339
+ if (previous) {
340
+ diagnostics.push({
341
+ severity: 'error',
342
+ code: 'foreign-master-duplicate-child',
343
+ message: `'${key}' already has a foreign master. A copied attribute has one master, ` +
344
+ 'so a child attribute is the child of at most one foreign master relationship.',
345
+ span: field.span,
346
+ });
347
+ }
348
+ else {
349
+ claimedChildren.set(key, field.span);
350
+ }
351
+ // 11.11: an inline foreign master cannot be composite, because the
352
+ // grammar gives an inline ref a single target; nothing to check here.
353
+ };
354
+ /* ---- walk ---- */
355
+ const walkFields = (items, ownerPath) => {
356
+ for (const item of items) {
357
+ if (item.kind !== 'FieldDeclaration')
358
+ continue;
359
+ const field = item;
360
+ checkField(field, ownerPath);
361
+ // Descend into nested object/array bodies so a flag on a nested field
362
+ // is checked too. Nested field containers vary by type expression;
363
+ // `nestedFields` below normalizes the shapes the AST uses.
364
+ for (const nested of nestedFields(field)) {
365
+ walkFields([nested], `${ownerPath}.${field.name}`);
366
+ }
367
+ }
368
+ };
369
+ const walkEntity = (entity, containerName) => {
370
+ const ownerPath = containerName ? `${containerName}.${entity.name}` : entity.name;
371
+ walkFields(entity.body, ownerPath);
372
+ };
373
+ for (const stmt of doc.statements) {
374
+ switch (stmt.kind) {
375
+ case 'RefDeclaration':
376
+ checkRefDeclaration(stmt);
377
+ break;
378
+ case 'EntityDeclaration':
379
+ walkEntity(stmt);
380
+ break;
381
+ case 'ContainerDeclaration': {
382
+ // Ref declarations are top-level only (spec 11.2), so a container
383
+ // body holds entities, views, edges, enums and notes but no refs.
384
+ const container = stmt;
385
+ for (const item of container.body) {
386
+ if (item.kind === 'EntityDeclaration')
387
+ walkEntity(item, container.name);
388
+ }
389
+ break;
390
+ }
391
+ default:
392
+ break;
393
+ }
394
+ }
395
+ // Section 4: the construct requires a document declaring 0.4 or later.
396
+ if ((sawForeignMaster || sawV04Setting) && !versionAtLeast(doc, '0.4')) {
397
+ const what = sawForeignMaster
398
+ ? "The 'foreign_master' relationship flag"
399
+ : 'Relationship roles, verbs and constraint type';
400
+ diagnostics.push({
401
+ severity: 'error',
402
+ code: 'construct-requires-version',
403
+ message: `${what} requires a document declaring 'xdbml: 0.4' or later.`,
404
+ span: doc.version ? doc.version.span : doc.span,
405
+ });
406
+ }
407
+ return diagnostics;
408
+ }
409
+ /* -------------------------------------------------------------------------
410
+ * Helpers
411
+ * ----------------------------------------------------------------------- */
412
+ /**
413
+ * The field declarations nested inside a field's type expression, for the
414
+ * object and array shapes the AST produces. Returns an empty array for
415
+ * scalar fields and for type expressions with no inner field list.
416
+ */
417
+ function nestedFields(field) {
418
+ const out = [];
419
+ const visit = (node) => {
420
+ if (!node || typeof node !== 'object')
421
+ return;
422
+ const rec = node;
423
+ if (rec.kind === 'FieldDeclaration') {
424
+ out.push(rec);
425
+ return; // walkFields recurses into this one itself
426
+ }
427
+ for (const value of Object.values(rec)) {
428
+ if (Array.isArray(value))
429
+ value.forEach(visit);
430
+ else if (value && typeof value === 'object')
431
+ visit(value);
432
+ }
433
+ };
434
+ visit(field.type);
435
+ return out;
436
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xdbml/parse",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Parser for xDBML (eXtended Database Markup Language), a strict superset of DBML 3.13.6: tokenizer, parser, module resolver, and name resolver.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hackolade",
@@ -42,6 +42,7 @@
42
42
  "node": ">=22"
43
43
  },
44
44
  "devDependencies": {
45
+ "@types/node": "^26.1.1",
45
46
  "typescript": "^5.7.3"
46
47
  },
47
48
  "publishConfig": {