@xdbml/parse 0.5.0 → 0.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.
package/dist/keywords.js CHANGED
@@ -98,9 +98,15 @@ export const POLYMORPHISM_KEYWORDS = [
98
98
  /* -------------------------------------------------------------------------
99
99
  * Scalar and BSON types
100
100
  *
101
- * The parser accepts any identifier as a scalar type (open vocabulary),
102
- * so these lists drive color, not validation. Themes color them under
103
- * `storage.type` (TextMate) or `type` (Monarch).
101
+ * The parser accepts any identifier as a scalar type (open vocabulary,
102
+ * spec §1.2 principle 4), so these lists never reject a type name.
103
+ * They serve two purposes: themes color them under `storage.type`
104
+ * (TextMate) or `type` (Monarch), and the name resolver treats them as
105
+ * built-in names, which a Named Type cannot shadow (spec §15.2).
106
+ * Adding a name here therefore changes resolution as well as color: a
107
+ * document declaring `Type <that name>` would stop resolving to its own
108
+ * Type. Target-native names that only need color go in
109
+ * TARGET_NATIVE_TYPES below.
104
110
  * ----------------------------------------------------------------------- */
105
111
  export const SCALAR_TYPES = [
106
112
  // Integers
@@ -139,6 +145,51 @@ export const BSON_TYPES = [
139
145
  'long',
140
146
  'double',
141
147
  ];
148
+ /* -------------------------------------------------------------------------
149
+ * Target-native types (highlighting only)
150
+ *
151
+ * Common type names of specific targets, colored like SCALAR_TYPES so
152
+ * that a physical model reads the same in the editor as a generic one.
153
+ * Unlike SCALAR_TYPES, these are not built-in names: the resolver
154
+ * passes them through like any other target-native name, and a Named
155
+ * Type or Enum may be declared under one of them.
156
+ *
157
+ * Coloring is position-independent (Monarch and TextMate both match
158
+ * the bare word), so a field named after a listed type is colored as a
159
+ * type too. Generic words that often name fields -- `point`, `line`,
160
+ * `box`, `path`, `circle`, `image`, `counter`, `duration`, `name` --
161
+ * are left out for that reason. The list is a convenience, not a
162
+ * vocabulary: a name missing from it is still a valid type.
163
+ * ----------------------------------------------------------------------- */
164
+ export const TARGET_NATIVE_TYPES = [
165
+ // Oracle
166
+ 'number', 'binary_float', 'binary_double', 'clob', 'nclob', 'bfile',
167
+ 'raw', 'rowid', 'urowid', 'xmltype', 'sdo_geometry',
168
+ // Oracle, PostgreSQL, BigQuery, Databricks
169
+ 'interval',
170
+ // PostgreSQL
171
+ 'serial', 'smallserial', 'bigserial', 'serial2', 'serial4', 'serial8',
172
+ 'int2', 'int4', 'int8', 'float4', 'float8', 'bytea', 'citext',
173
+ 'inet', 'cidr', 'macaddr', 'macaddr8', 'tsvector', 'tsquery', 'hstore',
174
+ 'jsonpath', 'timetz', 'varbit',
175
+ 'int4range', 'int8range', 'numrange', 'tsrange', 'tstzrange', 'daterange',
176
+ // SQL Server
177
+ 'uniqueidentifier', 'datetimeoffset', 'smalldatetime', 'rowversion',
178
+ 'hierarchyid', 'sql_variant',
179
+ // Snowflake (plus `number`, `interval`, `geography`, `geometry`)
180
+ 'timestamp_ntz', 'timestamp_ltz', 'timestamp_tz', 'vector',
181
+ // BigQuery, Avro, Protobuf
182
+ 'float64', 'bignumeric', 'bigdecimal', 'bytes',
183
+ // Spatial, across several targets
184
+ 'geometry', 'geography',
185
+ // Cassandra
186
+ 'ascii', 'varint', 'timeuuid',
187
+ // Protobuf
188
+ 'uint32', 'uint64', 'sint32', 'sint64',
189
+ 'fixed32', 'fixed64', 'sfixed32', 'sfixed64',
190
+ // ClickHouse
191
+ 'uint8', 'uint16', 'int16', 'float32',
192
+ ];
142
193
  /* -------------------------------------------------------------------------
143
194
  * Settings vocabulary
144
195
  *
@@ -235,6 +286,7 @@ export const SETTING_KEYS = [
235
286
  // Block keywords (entity-body or top-level)
236
287
  'indexes',
237
288
  'checks', // v0.2 §10: entity-level checks block
289
+ 'constraints', // v0.6 §10: keys and checks block
238
290
  // Container settings
239
291
  'replication',
240
292
  'location',
@@ -390,8 +390,18 @@ function findImportTarget(item, doc) {
390
390
  }
391
391
  return undefined;
392
392
  }
393
- // Enum: top-level OR container-scoped.
393
+ // Enum: top-level OR container-scoped. A top-level Enum declared with
394
+ // a qualified name (`enum core.job_status`, the DBML form) matches its
395
+ // full dotted path and imports under its bare name, like the
396
+ // container-scoped form: the source container does not survive into
397
+ // the importing file (spec §27.16).
394
398
  if (item.elementType === 'enum') {
399
+ if (segments.length > 1) {
400
+ const qualifiedTopLevel = doc.statements.find((s) => s.kind === 'EnumDeclaration' && s.name === path);
401
+ if (qualifiedTopLevel && qualifiedTopLevel.kind === 'EnumDeclaration') {
402
+ return { ...qualifiedTopLevel, name: segments[segments.length - 1] };
403
+ }
404
+ }
395
405
  if (segments.length === 1) {
396
406
  return doc.statements.find((s) => s.kind === 'EnumDeclaration' && s.name === segments[0]);
397
407
  }
package/dist/monarch.d.ts CHANGED
@@ -53,6 +53,7 @@ export interface XDbmlMonarchLanguage {
53
53
  polymorphismKeywords: string[];
54
54
  scalarTypes: string[];
55
55
  bsonTypes: string[];
56
+ nativeTypes: string[];
56
57
  settingFlags: string[];
57
58
  settingKeys: string[];
58
59
  granularityValues: string[];
package/dist/monarch.js CHANGED
@@ -18,7 +18,7 @@
18
18
  * dependency on consumers that just want to parse. The Monaco types
19
19
  * are structurally compatible -- the consumer casts at the boundary.
20
20
  */
21
- import { CONTAINER_KEYWORDS, ENTITY_KEYWORDS, DECLARATION_KEYWORDS, STRUCTURAL_TYPE_KEYWORDS, POLYMORPHISM_KEYWORDS, SCALAR_TYPES, BSON_TYPES, SETTING_FLAGS, SETTING_KEYS, GRANULARITY_VALUES, } from "./keywords.js";
21
+ import { CONTAINER_KEYWORDS, ENTITY_KEYWORDS, DECLARATION_KEYWORDS, STRUCTURAL_TYPE_KEYWORDS, POLYMORPHISM_KEYWORDS, SCALAR_TYPES, BSON_TYPES, TARGET_NATIVE_TYPES, SETTING_FLAGS, SETTING_KEYS, GRANULARITY_VALUES, } from "./keywords.js";
22
22
  /* -------------------------------------------------------------------------
23
23
  * Language configuration
24
24
  * ----------------------------------------------------------------------- */
@@ -102,6 +102,9 @@ export const xdbmlMonarchTokensProvider = {
102
102
  scalarTypes: [...SCALAR_TYPES],
103
103
  // BSON / document-store types
104
104
  bsonTypes: [...BSON_TYPES],
105
+ // Target-native types (Oracle `number`, PostgreSQL `serial`, ...):
106
+ // colored like scalar types, but not built-ins -- see keywords.ts.
107
+ nativeTypes: [...TARGET_NATIVE_TYPES],
105
108
  // Bare-flag settings: `pk`, `unique`, `not null`, etc.
106
109
  // `not null` and `primary key` are two words but tokenized one at a time
107
110
  // here -- the highlighter colors each as `keyword.setting`.
@@ -165,6 +168,7 @@ export const xdbmlMonarchTokensProvider = {
165
168
  '@decls': 'keyword.declaration',
166
169
  '@scalarTypes': 'type',
167
170
  '@bsonTypes': 'type.bson',
171
+ '@nativeTypes': 'type',
168
172
  '@settingFlags': 'keyword.setting',
169
173
  '@settingKeys': 'keyword.setting',
170
174
  '@granularityValues': 'keyword.value',
@@ -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' | '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' | 'missing-supertype' | 'unresolved-supertype-group-member' | 'invalid-supertype-group-value' | 'duplicate-subtype' | 'supertype-is-subtype' | 'subtype-in-multiple-groups' | 'supertype-cycle' | 'supertype-attribute-redeclared' | 'discriminator-on-overlapping-group' | 'merge-without-roll-up' | 'empty-supertype-group';
70
+ export type DiagnosticCode = 'duplicate-declaration' | 'unresolved-type' | 'possible-type-typo' | 'unresolved-key-field' | 'unresolved-index-field' | 'key-path-crosses-collection' | 'invalid-key-flags' | 'duplicate-primary-key' | 'null-in-primary-key' | 'duplicate-constraints-block' | 'ref-target-not-key' | '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' | 'missing-supertype' | 'unresolved-supertype-group-member' | 'invalid-supertype-group-value' | 'duplicate-subtype' | 'supertype-is-subtype' | 'subtype-in-multiple-groups' | 'supertype-cycle' | 'supertype-attribute-redeclared' | 'discriminator-on-overlapping-group' | 'merge-without-roll-up' | 'empty-supertype-group';
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.,
@@ -42,6 +42,7 @@ import { SCALAR_TYPES, BSON_TYPES } from "./keywords.js";
42
42
  import { flatten } from "./module-resolver.js";
43
43
  import { checkRelationships } from "./relationships.js";
44
44
  import { checkSupertypeGroups } from "./supertypes.js";
45
+ import { checkConstraints } from "./constraints.js";
45
46
  /**
46
47
  * Read-only handle on the collected symbol table.
47
48
  *
@@ -96,11 +97,18 @@ export class SymbolTable {
96
97
  * Built-in type recognition
97
98
  *
98
99
  * Field type expressions can name a builtin scalar (`int`, `varchar`),
99
- * a BSON type (`objectId`), or a user-defined Named Type (`Email`).
100
- * The parser doesn't distinguish at parse time -- they all land as
100
+ * a BSON type (`objectId`), a user-defined Named Type (`Email`) or Enum,
101
+ * or any other target-native type (`number`, `clob`, `serial`). The
102
+ * parser doesn't distinguish at parse time -- they all land as
101
103
  * ScalarType nodes (or NamedTypeReference in some contexts). The
102
- * resolver uses these sets to decide whether to look up a name in the
103
- * symbol table.
104
+ * resolver uses these sets only to skip the symbol-table lookup for
105
+ * names that can never be a Named Type reference.
106
+ *
107
+ * The sets are not a type vocabulary. Scalar type names pass through
108
+ * as written (spec §1.2, principle 4), so a name that is neither a
109
+ * builtin nor a declared Type or Enum is a target-native type, not an
110
+ * error. See `nearMissTypeName()` for the one diagnostic such a name
111
+ * can raise.
104
112
  *
105
113
  * Matching is case-insensitive: `Int`, `int`, `INT` all map to the same
106
114
  * builtin per spec §3.8.
@@ -112,6 +120,87 @@ const BUILTIN_TYPES = new Set([
112
120
  function isBuiltinType(name) {
113
121
  return BUILTIN_TYPES.has(name.toLowerCase());
114
122
  }
123
+ /**
124
+ * The declared Types and Enums a bare type name can refer to. More than
125
+ * one entry means the name is declared in several containers (Enums can
126
+ * be container-scoped); the name still refers to a declaration, so it
127
+ * is neither a target-native type nor a near miss.
128
+ */
129
+ function typeOrEnumDeclarations(name, symbols) {
130
+ const qualified = symbols.lookup(name);
131
+ if (qualified && (qualified.kind === 'type' || qualified.kind === 'enum'))
132
+ return [qualified];
133
+ return symbols.lookupAllBare(name).filter((e) => e.kind === 'type' || e.kind === 'enum');
134
+ }
135
+ /**
136
+ * Near-miss detection for target-native type names.
137
+ *
138
+ * A type name that is neither a builtin nor a declared Type or Enum is
139
+ * accepted as a target-native type. The one case worth flagging is a
140
+ * name that differs only slightly from a declared Type or Enum -- most
141
+ * likely a misspelling (`Adress` for `Address`, `email` for `Email`).
142
+ * Returns the declared name to suggest, or undefined when nothing is
143
+ * close enough.
144
+ *
145
+ * Comparison is case-insensitive, so a difference of case alone counts
146
+ * as a near miss (identifiers are case-sensitive, spec §3.8). The edit
147
+ * distance allowed grows with the length of the name: none beyond case
148
+ * for three characters or fewer, one edit up to seven characters, two
149
+ * edits from eight. An adjacent transposition counts as one edit.
150
+ */
151
+ function nearMissTypeName(name, symbols) {
152
+ const lower = name.toLowerCase();
153
+ const maxDistance = lower.length <= 3 ? 0 : lower.length <= 7 ? 1 : 2;
154
+ // A qualified name (`core.job_stauts`) compares against qualified
155
+ // declarations, a bare name against bare ones.
156
+ const qualified = name.includes('.');
157
+ let best;
158
+ for (const entry of symbols.entries()) {
159
+ if (entry.kind !== 'type' && entry.kind !== 'enum')
160
+ continue;
161
+ const declared = qualified ? entry.qualifiedName : entry.name;
162
+ const candidate = declared.toLowerCase();
163
+ if (Math.abs(candidate.length - lower.length) > maxDistance)
164
+ continue;
165
+ const distance = editDistance(lower, candidate, maxDistance);
166
+ if (distance <= maxDistance && (!best || distance < best.distance)) {
167
+ best = { name: declared, distance };
168
+ }
169
+ }
170
+ return best?.name;
171
+ }
172
+ /**
173
+ * Optimal string alignment distance (Levenshtein plus adjacent
174
+ * transposition). Returns `limit + 1` as soon as every alignment of a
175
+ * row exceeds `limit`, since callers only compare against the limit.
176
+ */
177
+ function editDistance(a, b, limit) {
178
+ const rows = a.length + 1;
179
+ const cols = b.length + 1;
180
+ const d = [];
181
+ for (let i = 0; i < rows; i++) {
182
+ d.push(new Array(cols).fill(0));
183
+ d[i][0] = i;
184
+ }
185
+ for (let j = 0; j < cols; j++)
186
+ d[0][j] = j;
187
+ for (let i = 1; i < rows; i++) {
188
+ let rowMin = Infinity;
189
+ for (let j = 1; j < cols; j++) {
190
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
191
+ let v = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
192
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
193
+ v = Math.min(v, d[i - 2][j - 2] + 1);
194
+ }
195
+ d[i][j] = v;
196
+ if (v < rowMin)
197
+ rowMin = v;
198
+ }
199
+ if (rowMin > limit)
200
+ return limit + 1;
201
+ }
202
+ return d[rows - 1][cols - 1];
203
+ }
115
204
  /* -------------------------------------------------------------------------
116
205
  * Main entry point
117
206
  * ----------------------------------------------------------------------- */
@@ -140,6 +229,8 @@ export function resolveNames(doc) {
140
229
  diagnostics.push(...checkRelationships(flat));
141
230
  // Pass 4: supertype group rules (spec 12.8).
142
231
  diagnostics.push(...checkSupertypeGroups(flat));
232
+ // Pass 5: constraints and referenced keys (spec §10.10, §11.17).
233
+ diagnostics.push(...checkConstraints(flat));
143
234
  return { diagnostics, symbols };
144
235
  }
145
236
  /* -------------------------------------------------------------------------
@@ -159,9 +250,20 @@ function addTopLevelDeclaration(stmt, entries, diagnostics, seen) {
159
250
  case 'TypeDeclaration':
160
251
  addEntry(stmt.name, undefined, 'type', stmt, stmt.span, entries, diagnostics, seen);
161
252
  return;
162
- case 'EnumDeclaration':
163
- addEntry(stmt.name, undefined, 'enum', stmt, stmt.span, entries, diagnostics, seen);
253
+ case 'EnumDeclaration': {
254
+ // `enum core.job_status { ... }` (the DBML form) files the Enum
255
+ // under container `core`, exactly as `Container core { Enum
256
+ // job_status { ... } }` does: same qualified name, same bare name,
257
+ // and declaring both is a duplicate (spec §16).
258
+ const dot = stmt.name.lastIndexOf('.');
259
+ if (dot > 0) {
260
+ addEntry(stmt.name.slice(dot + 1), stmt.name.slice(0, dot), 'enum', stmt, stmt.span, entries, diagnostics, seen);
261
+ }
262
+ else {
263
+ addEntry(stmt.name, undefined, 'enum', stmt, stmt.span, entries, diagnostics, seen);
264
+ }
164
265
  return;
266
+ }
165
267
  case 'EdgeDeclaration':
166
268
  addEntry(stmt.name, undefined, 'edge', stmt, stmt.span, entries, diagnostics, seen);
167
269
  return;
@@ -363,16 +465,18 @@ function resolveFieldDeclaration(field, symbols, diagnostics) {
363
465
  function resolveTypeExpression(expr, symbols, diagnostics) {
364
466
  switch (expr.kind) {
365
467
  case 'ScalarType':
366
- // A ScalarType is either a builtin or a reference to a Named Type
367
- // (the parser doesn't distinguish at parse time). Look up only
368
- // when the name isn't a builtin.
369
- if (!isBuiltinType(expr.name)) {
370
- const found = symbols.lookup(expr.name) ?? symbols.lookupBare(expr.name);
371
- if (!found || found.kind !== 'type') {
468
+ // A ScalarType is a builtin, a reference to a declared Type or
469
+ // Enum, or a target-native type passed through as written (spec
470
+ // §1.2, principle 4) -- the parser doesn't distinguish at parse
471
+ // time. A target-native name is valid; the only diagnostic is a
472
+ // warning when it is a near miss of a declared Type or Enum.
473
+ if (!isBuiltinType(expr.name) && typeOrEnumDeclarations(expr.name, symbols).length === 0) {
474
+ const suggestion = nearMissTypeName(expr.name, symbols);
475
+ if (suggestion) {
372
476
  diagnostics.push({
373
- severity: 'error',
374
- code: 'unresolved-type',
375
- message: `Type '${expr.name}' is not a built-in type or declared Named Type.`,
477
+ severity: 'warning',
478
+ code: 'possible-type-typo',
479
+ message: `Type '${expr.name}' is not declared; did you mean '${suggestion}'? Otherwise it is kept as a target-native type.`,
376
480
  span: expr.span,
377
481
  });
378
482
  }
@@ -824,9 +928,18 @@ function dereferenceNamedType(type, symbols, depth) {
824
928
  return type;
825
929
  }
826
930
  const sym = symbols.lookup(name) ?? symbols.lookupBare(name);
931
+ if (type.kind === 'ScalarType' && (!sym || sym.kind !== 'type')) {
932
+ // An Enum or a target-native type: an opaque scalar, so a path that
933
+ // navigates into it gets the walker's shape diagnostic. A name
934
+ // declared as a Type or Enum in several containers stays unresolved.
935
+ if (typeOrEnumDeclarations(name, symbols).length > 1)
936
+ return undefined;
937
+ return type;
938
+ }
827
939
  if (!sym || sym.kind !== 'type' || sym.declaration.kind !== 'TypeDeclaration') {
828
- // Unresolved -- the field-type pass will diagnose this. Return
829
- // undefined so the walker bails without emitting a duplicate error.
940
+ // Unresolved Named Type reference -- the field-type pass diagnoses
941
+ // this. Return undefined so the walker bails without emitting a
942
+ // duplicate error.
830
943
  return undefined;
831
944
  }
832
945
  const td = sym.declaration;
package/dist/parser.d.ts CHANGED
@@ -26,7 +26,7 @@ export declare class ParseError extends Error {
26
26
  * declaring a later version is refused (spec 4.1) rather than parsed with
27
27
  * semantics it does not have.
28
28
  */
29
- export declare const SUPPORTED_XDBML_VERSION = "0.5";
29
+ export declare const SUPPORTED_XDBML_VERSION = "0.6";
30
30
  /** Compare dotted version strings numerically: -1, 0 or 1. */
31
31
  export declare function compareVersions(a: string, b: string): number;
32
32
  export declare class Parser {
@@ -326,6 +326,8 @@ export declare class Parser {
326
326
  private parseIndexComponent;
327
327
  private parseChecks;
328
328
  private parseCheckEntry;
329
+ private parseConstraints;
330
+ private parseKeyConstraintEntry;
329
331
  private maybeSettingsBlock;
330
332
  /**
331
333
  * A single setting. Forms:
package/dist/parser.js CHANGED
@@ -9,6 +9,7 @@
9
9
  * semantic-analysis pass. The grammar test cases the parser passes are
10
10
  * the official xDBML example files in /examples.
11
11
  */
12
+ import { markPrimaryKeyNotNull } from "./constraints.js";
12
13
  import { TokenKind, tokenize, } from "./lexer.js";
13
14
  import { resolveImport, classifyModuleSource, ModuleSourceError } from "./module-resolver.js";
14
15
  export class ParseError extends Error {
@@ -31,7 +32,7 @@ export class ParseError extends Error {
31
32
  * declaring a later version is refused (spec 4.1) rather than parsed with
32
33
  * semantics it does not have.
33
34
  */
34
- export const SUPPORTED_XDBML_VERSION = '0.5';
35
+ export const SUPPORTED_XDBML_VERSION = '0.6';
35
36
  /** Compare dotted version strings numerically: -1, 0 or 1. */
36
37
  export function compareVersions(a, b) {
37
38
  const pa = a.split('.').map((n) => Number(n) || 0);
@@ -191,13 +192,16 @@ export class Parser {
191
192
  while (!this.check(TokenKind.EOF)) {
192
193
  statements.push(this.parseTopLevelStatement());
193
194
  }
194
- return {
195
+ const doc = {
195
196
  kind: 'XDbmlDocument',
196
197
  version,
197
198
  experimental,
198
199
  statements,
199
200
  span: this.spanFrom(start),
200
201
  };
202
+ // v0.6 §10.4: primary key fields are not null in the AST.
203
+ markPrimaryKeyNotNull(doc);
204
+ return doc;
201
205
  }
202
206
  /* ----- version & experimental ----- */
203
207
  parseVersionDeclaration() {
@@ -487,6 +491,11 @@ export class Parser {
487
491
  else if (k === 'checks') {
488
492
  body.push(this.parseChecks());
489
493
  }
494
+ else if (k === 'constraints' && this.peek(1).kind === TokenKind.LBrace) {
495
+ // v0.6 §10.1: a keyword only when `{` follows, so a field may keep
496
+ // the name `constraints`.
497
+ body.push(this.parseConstraints());
498
+ }
490
499
  else if (k === 'records') {
491
500
  body.push(this.parseRecordsBlock());
492
501
  }
@@ -1289,7 +1298,14 @@ export class Parser {
1289
1298
  throw new ParseError(`Expected type name, got ${t.kind} ${JSON.stringify(t.text)}`, t.start);
1290
1299
  }
1291
1300
  this.advance();
1292
- const name = t.kind === TokenKind.QuotedIdentifier ? (t.value ?? '') : t.text;
1301
+ let name = t.kind === TokenKind.QuotedIdentifier ? (t.value ?? '') : t.text;
1302
+ // A qualified type name, `core.job_status`, names an Enum declared in
1303
+ // a container (spec §16), or a target-native type such as
1304
+ // `public.geometry` that passes through as written.
1305
+ while (this.check(TokenKind.Dot)) {
1306
+ this.advance();
1307
+ name += `.${this.parseIdentLikeName('type name after dot')}`;
1308
+ }
1293
1309
  let params;
1294
1310
  if (this.check(TokenKind.LParen)) {
1295
1311
  this.advance();
@@ -1455,7 +1471,13 @@ export class Parser {
1455
1471
  parseEnum() {
1456
1472
  const start = this.peek().start;
1457
1473
  const kwTok = this.advance(); // enum
1458
- const name = this.parseIdentLikeName('enum name');
1474
+ // DBML form `enum core.job_status { ... }`: the qualifier names the
1475
+ // container, as for a schema-qualified Table (spec §7.3, §16).
1476
+ let name = this.parseIdentLikeName('enum name');
1477
+ while (this.check(TokenKind.Dot)) {
1478
+ this.advance();
1479
+ name += `.${this.parseIdentLikeName('enum name after dot')}`;
1480
+ }
1459
1481
  this.expect(TokenKind.LBrace, "Expected '{' after enum name");
1460
1482
  const values = [];
1461
1483
  while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
@@ -1919,17 +1941,71 @@ export class Parser {
1919
1941
  parseCheckEntry() {
1920
1942
  const start = this.peek().start;
1921
1943
  const exprTok = this.peek();
1922
- if (exprTok.kind !== TokenKind.ExpressionLiteral) {
1923
- throw new ParseError(`Expected backtick-wrapped check expression, got ${exprTok.kind} ${JSON.stringify(exprTok.text)}`, exprTok.start);
1944
+ // v0.6 §10.5: single (or triple) quotes are the standard form and
1945
+ // backticks an alias. The resolver rejects the quoted form in documents
1946
+ // declaring an earlier version.
1947
+ const quoted = exprTok.kind === TokenKind.StringLiteral || exprTok.kind === TokenKind.MultilineString;
1948
+ if (exprTok.kind !== TokenKind.ExpressionLiteral && !quoted) {
1949
+ throw new ParseError(`Expected a check expression in quotes or backticks, got ${exprTok.kind} ${JSON.stringify(exprTok.text)}`, exprTok.start);
1924
1950
  }
1925
1951
  this.advance();
1926
- // The expression is opaque to xDBML per spec §10.3. The value field of
1927
- // an ExpressionLiteral token already has the surrounding backticks stripped.
1952
+ // The expression is opaque to xDBML (spec §10.5). The token value
1953
+ // already has the delimiters stripped.
1928
1954
  const expression = exprTok.value ?? '';
1929
1955
  const settings = this.maybeSettingsBlock();
1930
1956
  return {
1931
1957
  kind: 'CheckEntry',
1932
1958
  expression,
1959
+ delimiter: quoted ? 'quote' : 'backtick',
1960
+ settings,
1961
+ span: this.spanFrom(start),
1962
+ };
1963
+ }
1964
+ /* ----- constraints block (v0.6 §10) ----- */
1965
+ parseConstraints() {
1966
+ const start = this.peek().start;
1967
+ this.advance(); // constraints
1968
+ this.expect(TokenKind.LBrace, "Expected '{' after constraints");
1969
+ const entries = [];
1970
+ while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
1971
+ const t = this.peek();
1972
+ if (t.kind === TokenKind.StringLiteral ||
1973
+ t.kind === TokenKind.MultilineString ||
1974
+ t.kind === TokenKind.ExpressionLiteral) {
1975
+ entries.push(this.parseCheckEntry());
1976
+ }
1977
+ else {
1978
+ entries.push(this.parseKeyConstraintEntry());
1979
+ }
1980
+ }
1981
+ this.expect(TokenKind.RBrace, "Expected '}' closing constraints");
1982
+ return {
1983
+ kind: 'ConstraintsBlock',
1984
+ entries,
1985
+ span: this.spanFrom(start),
1986
+ };
1987
+ }
1988
+ parseKeyConstraintEntry() {
1989
+ const start = this.peek().start;
1990
+ const fields = [];
1991
+ let parenthesized = false;
1992
+ if (this.check(TokenKind.LParen)) {
1993
+ parenthesized = true;
1994
+ this.advance(); // (
1995
+ fields.push(this.parsePathSegments());
1996
+ while (this.match(TokenKind.Comma)) {
1997
+ fields.push(this.parsePathSegments());
1998
+ }
1999
+ this.expect(TokenKind.RParen, "Expected ')' closing the fields of a key");
2000
+ }
2001
+ else {
2002
+ fields.push(this.parsePathSegments());
2003
+ }
2004
+ const settings = this.maybeSettingsBlock();
2005
+ return {
2006
+ kind: 'KeyConstraintEntry',
2007
+ fields,
2008
+ parenthesized,
1933
2009
  settings,
1934
2010
  span: this.spanFrom(start),
1935
2011
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xdbml/parse",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",