@xdbml/parse 0.5.1 → 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/ast.d.ts CHANGED
@@ -68,7 +68,7 @@ export interface EntityDeclaration {
68
68
  span: Span;
69
69
  }
70
70
  export type EntityKeyword = 'Table' | 'Entity' | 'Collection' | 'Record';
71
- export type EntityBodyItem = FieldDeclaration | IndexesBlock | ChecksBlock | NoteBlock | PartialInjection | RecordsBlock;
71
+ export type EntityBodyItem = FieldDeclaration | IndexesBlock | ChecksBlock | ConstraintsBlock | NoteBlock | PartialInjection | RecordsBlock;
72
72
  export interface FieldDeclaration {
73
73
  kind: 'FieldDeclaration';
74
74
  name: string;
@@ -328,12 +328,34 @@ export interface ChecksBlock {
328
328
  }
329
329
  export interface CheckEntry {
330
330
  kind: 'CheckEntry';
331
- /** Source text inside the backticks, no surrounding backticks. */
331
+ /** Source text inside the delimiters, without them. */
332
332
  expression: string;
333
+ /**
334
+ * How the source wrote the expression (spec §10.5, v0.6): in backticks,
335
+ * or in single or triple quotes. Both mean the same check; the raw AST
336
+ * keeps the form. Absent on entries built by older code paths, which
337
+ * only ever read backticks.
338
+ */
339
+ delimiter?: 'backtick' | 'quote';
333
340
  /** Optional settings -- typically `name:` and/or `note:`. */
334
341
  settings: Setting[];
335
342
  span: Span;
336
343
  }
344
+ export interface ConstraintsBlock {
345
+ kind: 'ConstraintsBlock';
346
+ entries: ConstraintEntry[];
347
+ span: Span;
348
+ }
349
+ export type ConstraintEntry = KeyConstraintEntry | CheckEntry;
350
+ export interface KeyConstraintEntry {
351
+ kind: 'KeyConstraintEntry';
352
+ /** One path per key field, in key order. */
353
+ fields: PathSegment[][];
354
+ /** True when the source wrote the fields in parentheses. */
355
+ parenthesized: boolean;
356
+ settings: Setting[];
357
+ span: Span;
358
+ }
337
359
  export interface TablePartialDeclaration {
338
360
  kind: 'TablePartialDeclaration';
339
361
  name: string;
@@ -500,6 +522,12 @@ export interface Setting {
500
522
  nameSource: string;
501
523
  /** Value, if any. A pure flag like `pk` has `value: null`. */
502
524
  value: SettingValue | null;
525
+ /**
526
+ * True on a setting the parser added rather than read: `not null` on a
527
+ * primary key field of a v0.6 document (spec §10.4). Its span is the
528
+ * field's span.
529
+ */
530
+ implied?: true;
503
531
  span: Span;
504
532
  }
505
533
  export type SettingValue = StringValue | NumberValue | BooleanValue | NullValue | IdentifierValue | ExpressionValue | ListValue | RefValue;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Constraints (spec §10, new in v0.6) and referenced keys (spec §11.17).
3
+ *
4
+ * Three jobs:
5
+ *
6
+ * 1. `bodyConstraints()` / `entityConstraints()` gather every key and
7
+ * check of an entity into one list, whatever form declared it: a line
8
+ * in `constraints { }` or `checks { }`, `[pk]` / `[unique]` on a
9
+ * field, a `pk` entry in `indexes`, a field-level `check:`. This is
10
+ * the Constraint node of spec §28.6, and what the renderer and the
11
+ * playground read, so a consumer finds the primary key in one place.
12
+ *
13
+ * 2. `markPrimaryKeyNotNull()` adds an implied `not null` to each
14
+ * primary key field of a v0.6 document (spec §10.4). The parser calls
15
+ * it once the document is built.
16
+ *
17
+ * 3. `checkConstraints()` reports the conditions of spec §10.10 and the
18
+ * referenced-key rule of spec §11.17. Conditions on constructs that
19
+ * earlier versions and DBML accept are errors in a document declaring
20
+ * 0.6 or later and warnings otherwise, so a document stays valid under
21
+ * the version it declares.
22
+ */
23
+ import type { EntityBodyItem, PathSegment, Span, XDbmlDocument } from './ast.ts';
24
+ import type { Diagnostic } from './name-resolver.ts';
25
+ /** Where a constraint was declared. The raw AST keeps the form; this list does not. */
26
+ export type ConstraintSource = 'constraints' | 'checks' | 'inline' | 'indexes' | 'field-check';
27
+ export interface KeyConstraint {
28
+ kind: 'key';
29
+ keyKind: 'primary' | 'unique';
30
+ /** Dotted field paths, in key order. */
31
+ fields: string[];
32
+ name?: string;
33
+ note?: string;
34
+ source: ConstraintSource;
35
+ span: Span;
36
+ }
37
+ export interface CheckConstraint {
38
+ kind: 'check';
39
+ expression: string;
40
+ name?: string;
41
+ note?: string;
42
+ /** For a field-level `check:`, the field it sits on. */
43
+ field?: string;
44
+ source: ConstraintSource;
45
+ span: Span;
46
+ }
47
+ export type Constraint = KeyConstraint | CheckConstraint;
48
+ type Body = ReadonlyArray<EntityBodyItem>;
49
+ /** Dotted form of a key or index path; only field segments carry names. */
50
+ export declare function keyPathString(path: ReadonlyArray<PathSegment>): string;
51
+ /**
52
+ * The constraints one body declares itself, TablePartial injections left
53
+ * out. `quotedFieldChecks` reads a quoted field-level `check:` as a check
54
+ * (v0.6 §10.5); in earlier versions a quoted value there is a string.
55
+ */
56
+ export declare function bodyConstraints(body: Body, quotedFieldChecks?: boolean): Constraint[];
57
+ /**
58
+ * Every constraint of an entity (or edge, or partial): its own, plus those
59
+ * of the TablePartials it injects. One primary key results: the entity's
60
+ * own when it declares one, otherwise the last injected partial's (§10.4,
61
+ * following the conflict resolution of §17.1).
62
+ */
63
+ export declare function entityConstraints(decl: {
64
+ body: Body;
65
+ }, doc: XDbmlDocument): Constraint[];
66
+ /** The primary key of an entity, or undefined. */
67
+ export declare function primaryKey(decl: {
68
+ body: Body;
69
+ }, doc: XDbmlDocument): KeyConstraint | undefined;
70
+ /**
71
+ * Add an implied `not null` to every field of the primary key of each
72
+ * entity, edge and partial the document declares itself, unless the field
73
+ * states its nullability. `null` on a primary key field is left in place
74
+ * for checkConstraints to report. Keys naming a partial's field from the
75
+ * injecting entity are not marked: the field belongs to the partial.
76
+ */
77
+ export declare function markPrimaryKeyNotNull(doc: XDbmlDocument): void;
78
+ /** True for a target name the §11.17 rule covers. Unlisted names are not relational. */
79
+ export declare function isRelationalTarget(name: string): boolean;
80
+ export declare function checkConstraints(doc: XDbmlDocument): Diagnostic[];
81
+ export {};
@@ -0,0 +1,585 @@
1
+ /**
2
+ * Constraints (spec §10, new in v0.6) and referenced keys (spec §11.17).
3
+ *
4
+ * Three jobs:
5
+ *
6
+ * 1. `bodyConstraints()` / `entityConstraints()` gather every key and
7
+ * check of an entity into one list, whatever form declared it: a line
8
+ * in `constraints { }` or `checks { }`, `[pk]` / `[unique]` on a
9
+ * field, a `pk` entry in `indexes`, a field-level `check:`. This is
10
+ * the Constraint node of spec §28.6, and what the renderer and the
11
+ * playground read, so a consumer finds the primary key in one place.
12
+ *
13
+ * 2. `markPrimaryKeyNotNull()` adds an implied `not null` to each
14
+ * primary key field of a v0.6 document (spec §10.4). The parser calls
15
+ * it once the document is built.
16
+ *
17
+ * 3. `checkConstraints()` reports the conditions of spec §10.10 and the
18
+ * referenced-key rule of spec §11.17. Conditions on constructs that
19
+ * earlier versions and DBML accept are errors in a document declaring
20
+ * 0.6 or later and warnings otherwise, so a document stays valid under
21
+ * the version it declares.
22
+ */
23
+ import { hasForeignMasterFlag, isForeignMaster, versionAtLeast } from "./relationships.js";
24
+ const hasFlag = (settings, ...names) => settings.some((s) => names.includes(s.name) && s.value === null);
25
+ const stringSetting = (settings, name) => {
26
+ const s = settings.find((x) => x.name === name);
27
+ return s?.value?.kind === 'StringValue' ? s.value.value : undefined;
28
+ };
29
+ const isPk = (settings) => hasFlag(settings, 'pk', 'primary key');
30
+ /** Dotted form of a key or index path; only field segments carry names. */
31
+ export function keyPathString(path) {
32
+ return path.map((seg) => (seg.kind === 'PathField' ? seg.name : '[]')).join('.');
33
+ }
34
+ const checkFromEntry = (e, source) => ({
35
+ kind: 'check',
36
+ expression: e.expression,
37
+ name: stringSetting(e.settings, 'name'),
38
+ note: stringSetting(e.settings, 'note'),
39
+ source,
40
+ span: e.span,
41
+ });
42
+ /**
43
+ * The constraints one body declares itself, TablePartial injections left
44
+ * out. `quotedFieldChecks` reads a quoted field-level `check:` as a check
45
+ * (v0.6 §10.5); in earlier versions a quoted value there is a string.
46
+ */
47
+ export function bodyConstraints(body, quotedFieldChecks = true) {
48
+ const out = [];
49
+ const inlinePk = [];
50
+ for (const item of body) {
51
+ if (item.kind === 'FieldDeclaration') {
52
+ if (isPk(item.settings))
53
+ inlinePk.push(item);
54
+ if (hasFlag(item.settings, 'unique')) {
55
+ out.push({ kind: 'key', keyKind: 'unique', fields: [item.name], source: 'inline', span: item.span });
56
+ }
57
+ for (const s of item.settings) {
58
+ if (s.name !== 'check' || !s.value)
59
+ continue;
60
+ const expression = checkExpression(s.value, quotedFieldChecks);
61
+ if (expression !== undefined) {
62
+ out.push({ kind: 'check', expression, field: item.name, source: 'field-check', span: s.span });
63
+ }
64
+ }
65
+ }
66
+ else if (item.kind === 'ConstraintsBlock') {
67
+ for (const e of item.entries) {
68
+ if (e.kind === 'CheckEntry') {
69
+ out.push(checkFromEntry(e, 'constraints'));
70
+ continue;
71
+ }
72
+ const pk = isPk(e.settings);
73
+ const unique = hasFlag(e.settings, 'unique');
74
+ if (pk === unique)
75
+ continue; // neither or both: reported by checkConstraints
76
+ out.push({
77
+ kind: 'key',
78
+ keyKind: pk ? 'primary' : 'unique',
79
+ fields: e.fields.map(keyPathString),
80
+ name: stringSetting(e.settings, 'name'),
81
+ note: stringSetting(e.settings, 'note'),
82
+ source: 'constraints',
83
+ span: e.span,
84
+ });
85
+ }
86
+ }
87
+ else if (item.kind === 'ChecksBlock') {
88
+ for (const e of item.entries)
89
+ out.push(checkFromEntry(e, 'checks'));
90
+ }
91
+ else if (item.kind === 'IndexesBlock') {
92
+ for (const e of item.entries) {
93
+ if (!isPk(e.settings))
94
+ continue;
95
+ const fields = e.components.flatMap((c) => (c.kind === 'IndexPathComponent' ? [keyPathString(c.path)] : []));
96
+ out.push({
97
+ kind: 'key',
98
+ keyKind: 'primary',
99
+ fields,
100
+ name: stringSetting(e.settings, 'name'),
101
+ note: stringSetting(e.settings, 'note'),
102
+ source: 'indexes',
103
+ span: e.span,
104
+ });
105
+ }
106
+ }
107
+ }
108
+ if (inlinePk.length > 0) {
109
+ // Several inline [pk] fields form one composite key in field order (§10.3).
110
+ out.unshift({
111
+ kind: 'key',
112
+ keyKind: 'primary',
113
+ fields: inlinePk.map((f) => f.name),
114
+ source: 'inline',
115
+ span: inlinePk[0].span,
116
+ });
117
+ }
118
+ return out;
119
+ }
120
+ function checkExpression(value, quoted) {
121
+ if (value.kind === 'ExpressionValue')
122
+ return value.expression;
123
+ if (quoted && value.kind === 'StringValue')
124
+ return value.value;
125
+ return undefined;
126
+ }
127
+ /**
128
+ * Every constraint of an entity (or edge, or partial): its own, plus those
129
+ * of the TablePartials it injects. One primary key results: the entity's
130
+ * own when it declares one, otherwise the last injected partial's (§10.4,
131
+ * following the conflict resolution of §17.1).
132
+ */
133
+ export function entityConstraints(decl, doc) {
134
+ const quoted = versionAtLeast(doc, '0.6');
135
+ const partials = partialIndex(doc);
136
+ const own = bodyConstraints(decl.body, quoted);
137
+ const injected = [];
138
+ let partialPk;
139
+ for (const item of decl.body) {
140
+ if (item.kind !== 'PartialInjection')
141
+ continue;
142
+ const p = partials.get(item.partialName);
143
+ if (!p)
144
+ continue;
145
+ for (const c of bodyConstraints(p.body, quoted)) {
146
+ if (c.kind === 'key' && c.keyKind === 'primary')
147
+ partialPk = c;
148
+ else
149
+ injected.push(c);
150
+ }
151
+ }
152
+ const ownPk = own.find((c) => c.kind === 'key' && c.keyKind === 'primary');
153
+ const out = [...own, ...injected];
154
+ if (!ownPk && partialPk)
155
+ out.unshift(partialPk);
156
+ return out;
157
+ }
158
+ /** The primary key of an entity, or undefined. */
159
+ export function primaryKey(decl, doc) {
160
+ return entityConstraints(decl, doc).find((c) => c.kind === 'key' && c.keyKind === 'primary');
161
+ }
162
+ function bodies(doc) {
163
+ const out = [];
164
+ for (const s of doc.statements) {
165
+ if (s.kind === 'EntityDeclaration' || s.kind === 'EdgeDeclaration' || s.kind === 'TablePartialDeclaration') {
166
+ out.push({ decl: s });
167
+ }
168
+ else if (s.kind === 'ContainerDeclaration') {
169
+ for (const b of s.body) {
170
+ if (b.kind === 'EntityDeclaration' || b.kind === 'EdgeDeclaration')
171
+ out.push({ decl: b, container: s });
172
+ }
173
+ }
174
+ }
175
+ return out;
176
+ }
177
+ function partialIndex(doc) {
178
+ const m = new Map();
179
+ for (const s of doc.statements)
180
+ if (s.kind === 'TablePartialDeclaration')
181
+ m.set(s.name, s);
182
+ return m;
183
+ }
184
+ function typeIndex(doc) {
185
+ const m = new Map();
186
+ for (const s of doc.statements)
187
+ if (s.kind === 'TypeDeclaration')
188
+ m.set(s.name, s);
189
+ return m;
190
+ }
191
+ /** Top-level fields of a body, including those of injected partials. */
192
+ function topFields(body, partials, seen = new Set()) {
193
+ const out = [];
194
+ for (const item of body) {
195
+ if (item.kind === 'FieldDeclaration')
196
+ out.push(item);
197
+ else if (item.kind === 'PartialInjection' && !seen.has(item.partialName)) {
198
+ const p = partials.get(item.partialName);
199
+ if (p) {
200
+ seen.add(item.partialName);
201
+ out.push(...topFields(p.body, partials, seen));
202
+ }
203
+ }
204
+ }
205
+ return out;
206
+ }
207
+ /** The fields of an object-shaped type, or undefined for any other type. */
208
+ function objectFields(t, types) {
209
+ if (t.kind === 'ObjectType')
210
+ return t.fields;
211
+ if (t.kind === 'ScalarType' || t.kind === 'NamedTypeReference') {
212
+ const td = types.get(t.name);
213
+ if (td && !td.scalarBase)
214
+ return td.body;
215
+ }
216
+ return undefined;
217
+ }
218
+ const COLLECTION_KINDS = new Set(['ArrayType', 'TupleType', 'MapType', 'SetType']);
219
+ /**
220
+ * Follow a key path through object fields (§10.2). Reports the first field
221
+ * that is missing, or the first step through an array, tuple or map.
222
+ */
223
+ function walkKeyPath(path, fields, types) {
224
+ let current = fields;
225
+ const walked = [];
226
+ for (let i = 0; i < path.length; i++) {
227
+ const seg = path[i];
228
+ if (seg.kind !== 'PathField')
229
+ return { kind: 'collection', at: walked.join('.') || '(start)' };
230
+ walked.push(seg.name);
231
+ const f = current.find((x) => x.name === seg.name);
232
+ if (!f)
233
+ return { kind: 'missing', at: walked.join('.') };
234
+ if (i === path.length - 1)
235
+ return undefined;
236
+ if (COLLECTION_KINDS.has(f.type.kind))
237
+ return { kind: 'collection', at: walked.join('.') };
238
+ const next = objectFields(f.type, types);
239
+ if (!next)
240
+ return { kind: 'missing', at: `${walked.join('.')}.${path[i + 1].name ?? ''}` };
241
+ current = next.filter((x) => x.kind === 'FieldDeclaration');
242
+ }
243
+ return undefined;
244
+ }
245
+ /* -------------------------------------------------------------------------
246
+ * Primary key fields are not null (§10.4)
247
+ * ----------------------------------------------------------------------- */
248
+ const NULLABILITY = ['null', 'not null'];
249
+ /**
250
+ * Add an implied `not null` to every field of the primary key of each
251
+ * entity, edge and partial the document declares itself, unless the field
252
+ * states its nullability. `null` on a primary key field is left in place
253
+ * for checkConstraints to report. Keys naming a partial's field from the
254
+ * injecting entity are not marked: the field belongs to the partial.
255
+ */
256
+ export function markPrimaryKeyNotNull(doc) {
257
+ if (!versionAtLeast(doc, '0.6'))
258
+ return;
259
+ for (const { decl } of bodies(doc)) {
260
+ const pk = bodyConstraints(decl.body).find((c) => c.kind === 'key' && c.keyKind === 'primary');
261
+ if (!pk)
262
+ continue;
263
+ for (const dotted of pk.fields) {
264
+ const field = findField(decl.body, dotted.split('.'));
265
+ if (!field || field.settings.some((s) => NULLABILITY.includes(s.name)))
266
+ continue;
267
+ field.settings.push({ kind: 'Setting', name: 'not null', nameSource: 'not null', value: null, implied: true, span: field.span });
268
+ }
269
+ }
270
+ }
271
+ function findField(body, names) {
272
+ let current = body;
273
+ let found;
274
+ for (const n of names) {
275
+ found = current.find((x) => x.kind === 'FieldDeclaration' && x.name === n);
276
+ if (!found)
277
+ return undefined;
278
+ current = found.type.kind === 'ObjectType' ? found.type.fields : [];
279
+ }
280
+ return found;
281
+ }
282
+ /* -------------------------------------------------------------------------
283
+ * Targets (§5.1)
284
+ * ----------------------------------------------------------------------- */
285
+ /** Canonical relational targets and their aliases, lower-cased (§5.1). */
286
+ const RELATIONAL_TARGETS = new Set([
287
+ 'oracle',
288
+ 'postgresql', 'postgres', 'pg',
289
+ 'sql server', 'mssql', 'microsoft sql server', 't-sql',
290
+ 'mysql', 'mariadb', 'sqlite',
291
+ 'db2', 'ibm db2', 'db2 luw', 'db2 for z/os', 'db2 z/os',
292
+ 'teradata', 'snowflake', 'databricks',
293
+ 'bigquery', 'google bigquery',
294
+ 'redshift', 'amazon redshift',
295
+ 'synapse analytics', 'azure synapse',
296
+ 'timescaledb',
297
+ ]);
298
+ /** True for a target name the §11.17 rule covers. Unlisted names are not relational. */
299
+ export function isRelationalTarget(name) {
300
+ return RELATIONAL_TARGETS.has(name.trim().toLowerCase());
301
+ }
302
+ function settingValues(v) {
303
+ if (!v)
304
+ return [];
305
+ if (v.kind === 'ListValue')
306
+ return v.items.flatMap(settingValues);
307
+ if (v.kind === 'StringValue')
308
+ return [v.value];
309
+ if (v.kind === 'IdentifierValue')
310
+ return [v.value];
311
+ return [];
312
+ }
313
+ /** The document-wide target when the Project declares exactly one. */
314
+ function projectTarget(doc) {
315
+ for (const s of doc.statements) {
316
+ if (s.kind !== 'ProjectDeclaration')
317
+ continue;
318
+ for (const item of s.body) {
319
+ if (item.kind === 'Setting' && (item.name === 'targets' || item.name === 'database_type')) {
320
+ const values = settingValues(item.value).filter((x) => x !== '');
321
+ return values.length === 1 ? values[0] : undefined;
322
+ }
323
+ }
324
+ }
325
+ return undefined;
326
+ }
327
+ function effectiveTarget(p, project) {
328
+ const own = p.container?.settings.find((s) => s.name === 'target');
329
+ const values = settingValues(own?.value ?? null).filter((x) => x !== '');
330
+ return values[0] ?? project;
331
+ }
332
+ /* -------------------------------------------------------------------------
333
+ * Checks (§10.10, §11.17)
334
+ * ----------------------------------------------------------------------- */
335
+ export function checkConstraints(doc) {
336
+ const diagnostics = [];
337
+ const v06 = versionAtLeast(doc, '0.6');
338
+ // Conditions on constructs that earlier versions accept (§10.10).
339
+ const olderSeverity = v06 ? 'error' : 'warning';
340
+ const partials = partialIndex(doc);
341
+ const types = typeIndex(doc);
342
+ const versionSpan = (fallback) => (doc.version ? doc.version.span : fallback);
343
+ for (const { decl } of bodies(doc)) {
344
+ const fields = topFields(decl.body, partials);
345
+ const blocks = decl.body.filter((b) => b.kind === 'ConstraintsBlock');
346
+ for (const [i, block] of blocks.entries()) {
347
+ if (block.kind !== 'ConstraintsBlock')
348
+ continue;
349
+ if (!v06) {
350
+ diagnostics.push({
351
+ severity: 'error',
352
+ code: 'construct-requires-version',
353
+ message: "The constraints block requires a document declaring 'xdbml: 0.6' or later.",
354
+ span: versionSpan(block.span),
355
+ });
356
+ }
357
+ if (i > 0) {
358
+ diagnostics.push({
359
+ severity: 'error',
360
+ code: 'duplicate-constraints-block',
361
+ message: `'${decl.name}' declares a second constraints block; one body holds at most one.`,
362
+ span: block.span,
363
+ });
364
+ }
365
+ for (const e of block.entries) {
366
+ if (e.kind === 'CheckEntry')
367
+ continue;
368
+ const pk = isPk(e.settings);
369
+ const unique = hasFlag(e.settings, 'unique');
370
+ if (pk === unique) {
371
+ diagnostics.push({
372
+ severity: 'error',
373
+ code: 'invalid-key-flags',
374
+ message: pk
375
+ ? 'A key line carries both pk and unique; it takes exactly one.'
376
+ : 'A key line needs pk or unique.',
377
+ span: e.span,
378
+ });
379
+ }
380
+ for (const path of e.fields) {
381
+ const problem = walkKeyPath(path, fields, types);
382
+ if (problem?.kind === 'missing') {
383
+ diagnostics.push({
384
+ severity: 'error',
385
+ code: 'unresolved-key-field',
386
+ message: `Key field '${problem.at}' is not declared in '${decl.name}'.`,
387
+ span: e.span,
388
+ });
389
+ }
390
+ else if (problem?.kind === 'collection') {
391
+ diagnostics.push({
392
+ severity: 'error',
393
+ code: 'key-path-crosses-collection',
394
+ message: `Key path '${keyPathString(path)}' crosses an array, tuple or map at '${problem.at}'; a key path steps through object fields only.`,
395
+ span: e.span,
396
+ });
397
+ }
398
+ }
399
+ }
400
+ }
401
+ // Quoted checks are v0.6 syntax (§10.5).
402
+ if (!v06) {
403
+ for (const item of decl.body) {
404
+ if (item.kind !== 'ChecksBlock' && item.kind !== 'ConstraintsBlock')
405
+ continue;
406
+ for (const e of item.entries) {
407
+ if (e.kind === 'CheckEntry' && e.delimiter === 'quote') {
408
+ diagnostics.push({
409
+ severity: 'error',
410
+ code: 'construct-requires-version',
411
+ message: "A check expression in quotes requires a document declaring 'xdbml: 0.6' or later; use backticks.",
412
+ span: e.span,
413
+ });
414
+ }
415
+ }
416
+ }
417
+ }
418
+ // Index fields must be declared (§9, §10.10). Only the first segment is
419
+ // checked: deeper segments may iterate arrays implicitly (§9.3).
420
+ for (const item of decl.body) {
421
+ if (item.kind !== 'IndexesBlock')
422
+ continue;
423
+ for (const e of item.entries) {
424
+ for (const c of e.components) {
425
+ if (c.kind !== 'IndexPathComponent')
426
+ continue;
427
+ const first = c.path[0];
428
+ if (first?.kind === 'PathField' && !fields.some((f) => f.name === first.name)) {
429
+ diagnostics.push({
430
+ severity: olderSeverity,
431
+ code: 'unresolved-index-field',
432
+ message: `Index field '${first.name}' is not declared in '${decl.name}'.`,
433
+ span: c.span,
434
+ });
435
+ }
436
+ }
437
+ }
438
+ }
439
+ // One primary key, declared in one place (§10.4). Inline [pk] fields
440
+ // count as one declaration.
441
+ const own = bodyConstraints(decl.body, v06);
442
+ const pks = own.filter((c) => c.kind === 'key' && c.keyKind === 'primary');
443
+ for (const extra of pks.slice(1)) {
444
+ diagnostics.push({
445
+ severity: olderSeverity,
446
+ code: 'duplicate-primary-key',
447
+ message: `'${decl.name}' declares a second primary key; an entity has one, declared in one place.`,
448
+ span: extra.span,
449
+ });
450
+ }
451
+ // `null` on a primary key field (§10.4).
452
+ for (const pk of pks.slice(0, 1)) {
453
+ for (const dotted of pk.fields) {
454
+ const f = findField(decl.body, dotted.split('.'));
455
+ const nullSetting = f?.settings.find((s) => s.name === 'null' && !s.implied);
456
+ if (nullSetting) {
457
+ diagnostics.push({
458
+ severity: olderSeverity,
459
+ code: 'null-in-primary-key',
460
+ message: `'${decl.name}.${dotted}' is part of the primary key and cannot be null.`,
461
+ span: nullSetting.span,
462
+ });
463
+ }
464
+ }
465
+ }
466
+ }
467
+ diagnostics.push(...checkReferencedKeys(doc, olderSeverity));
468
+ return diagnostics;
469
+ }
470
+ function entityIndex(doc) {
471
+ const m = new Map();
472
+ const bare = new Map();
473
+ for (const p of bodies(doc)) {
474
+ if (p.decl.kind !== 'EntityDeclaration')
475
+ continue;
476
+ const e = { placed: p, decl: p.decl };
477
+ const q = p.container ? `${p.container.name}.${p.decl.name}` : p.decl.name;
478
+ m.set(q, e);
479
+ const b = p.decl.name.split('.').pop() ?? p.decl.name;
480
+ bare.set(b, [...(bare.get(b) ?? []), e]);
481
+ }
482
+ for (const [b, list] of bare)
483
+ if (list.length === 1 && !m.has(b))
484
+ m.set(b, list[0]);
485
+ return m;
486
+ }
487
+ /** Longest prefix naming an entity; the rest is a field path. Entity-level ends return undefined fields. */
488
+ function resolveEnd(ep, index) {
489
+ const names = ep.path.map((s) => (s.kind === 'PathField' ? s.name : '[]'));
490
+ for (let i = names.length; i >= 1; i--) {
491
+ const entity = index.get(names.slice(0, i).join('.'));
492
+ if (!entity)
493
+ continue;
494
+ if (ep.compositeFields && ep.compositeFields.length > 0)
495
+ return { entity, fields: [...ep.compositeFields] };
496
+ const rest = names.slice(i).join('.');
497
+ return rest ? { entity, fields: [rest] } : 'entity-level';
498
+ }
499
+ return undefined;
500
+ }
501
+ const sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
502
+ /** Keys a referenced end may match: keys, plus DBML unique indexes (§11.17). */
503
+ function referenceableKeys(decl, doc) {
504
+ const keys = entityConstraints(decl, doc)
505
+ .filter((c) => c.kind === 'key')
506
+ .map((k) => k.fields);
507
+ for (const item of decl.body) {
508
+ if (item.kind !== 'IndexesBlock')
509
+ continue;
510
+ for (const e of item.entries) {
511
+ if (isPk(e.settings) || !hasFlag(e.settings, 'unique'))
512
+ continue;
513
+ keys.push(e.components.flatMap((c) => (c.kind === 'IndexPathComponent' ? [keyPathString(c.path)] : [])));
514
+ }
515
+ }
516
+ return keys;
517
+ }
518
+ function checkReferencedKeys(doc, severity) {
519
+ const diagnostics = [];
520
+ const index = entityIndex(doc);
521
+ const project = projectTarget(doc);
522
+ const exemptTarget = (e) => {
523
+ const t = effectiveTarget(e.placed, project);
524
+ return t !== undefined && !isRelationalTarget(t);
525
+ };
526
+ const check = (source, operator, target, span) => {
527
+ if (operator === '<>')
528
+ return;
529
+ const s = resolveEnd(source, index);
530
+ const t = resolveEnd(target, index);
531
+ if (!s || !t || s === 'entity-level' || t === 'entity-level')
532
+ return;
533
+ if (exemptTarget(s.entity) || exemptTarget(t.entity))
534
+ return;
535
+ const sides = operator === '>' ? [t] : operator === '<' ? [s] : [s, t];
536
+ const withKeys = sides
537
+ .map((side) => ({ side, keys: referenceableKeys(side.entity.decl, doc) }))
538
+ .filter((x) => x.keys.length > 0);
539
+ if (withKeys.length === 0)
540
+ return; // no key declared: a tool MAY promote (§11.17)
541
+ if (withKeys.some((x) => x.keys.some((k) => sameSet(k, x.side.fields))))
542
+ return;
543
+ const first = withKeys[0];
544
+ const pk = primaryKey(first.side.entity.decl, doc);
545
+ const where = `${first.side.entity.decl.name}.${first.side.fields.length > 1 ? `(${first.side.fields.join(', ')})` : first.side.fields[0]}`;
546
+ diagnostics.push({
547
+ severity,
548
+ code: 'ref-target-not-key',
549
+ message: `Relationship references '${where}', which is not the primary key or a unique key of '${first.side.entity.decl.name}'` +
550
+ (pk ? ` (primary key: ${pk.fields.join(', ')}).` : '.') +
551
+ ' Declare those fields unique, or reference a key.',
552
+ span,
553
+ });
554
+ };
555
+ const visitRef = (ref) => {
556
+ if (isForeignMaster(ref))
557
+ return;
558
+ check(ref.spec.source, ref.spec.operator, ref.spec.target, ref.span);
559
+ };
560
+ for (const s of doc.statements) {
561
+ if (s.kind === 'RefDeclaration')
562
+ visitRef(s);
563
+ }
564
+ // Inline refs on top-level fields.
565
+ for (const p of bodies(doc)) {
566
+ if (p.decl.kind !== 'EntityDeclaration')
567
+ continue;
568
+ const owner = p.container ? `${p.container.name}.${p.decl.name}` : p.decl.name;
569
+ for (const item of p.decl.body) {
570
+ if (item.kind !== 'FieldDeclaration' || hasForeignMasterFlag(item.settings))
571
+ continue;
572
+ for (const st of item.settings) {
573
+ if (st.name !== 'ref' || st.value?.kind !== 'RefValue')
574
+ continue;
575
+ const source = {
576
+ kind: 'RefEndpoint',
577
+ path: [...owner.split('.'), item.name].map((name) => ({ kind: 'PathField', name, span: item.span })),
578
+ span: item.span,
579
+ };
580
+ check(source, st.value.operator, st.value.target, st.span);
581
+ }
582
+ }
583
+ }
584
+ return diagnostics;
585
+ }
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 { bodyConstraints, checkConstraints, entityConstraints, isRelationalTarget, keyPathString, markPrimaryKeyNotNull, primaryKey, } from './constraints.ts';
36
+ export type { CheckConstraint, Constraint, ConstraintSource, KeyConstraint } from './constraints.ts';
35
37
  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
38
  export type { RelationshipType, ConstraintType } from './relationships.ts';
37
39
  export { SUPERTYPE_GROUP_VALUES, canonicalSupertypeGroupValue, checkSupertypeGroups, resolveSupertypeGroups, subtypeStrategy, supertypeChains, supertypeGroupSettings, } from './supertypes.ts';
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ export { parse, Parser, ParseError, SUPPORTED_XDBML_VERSION, compareVersions } f
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 { bodyConstraints, checkConstraints, entityConstraints, isRelationalTarget, keyPathString, markPrimaryKeyNotNull, primaryKey, } from "./constraints.js";
33
34
  export { FOREIGN_MASTER_FLAG, checkRelationships, hasForeignMasterFlag, isForeignMaster, pathToString, refChildEndpoint, refParentEndpoint, relationshipType, versionAtLeast, CONSTRAINT_TYPES, V04_RELATIONSHIP_SETTINGS, constraintType, entityNames, isEntityLevelEndpoint, isUndirected, } from "./relationships.js";
34
35
  export { SUPERTYPE_GROUP_VALUES, canonicalSupertypeGroupValue, checkSupertypeGroups, resolveSupertypeGroups, subtypeStrategy, supertypeChains, supertypeGroupSettings, } from "./supertypes.js";
35
36
  export { xdbmlLanguageConfig, xdbmlMonarchTokensProvider, } from "./monarch.js";
@@ -40,7 +40,7 @@ export declare const SCALAR_TYPES: readonly ["tinyint", "smallint", "mediumint",
40
40
  export declare const BSON_TYPES: readonly ["objectid", "decimal128", "bindata", "minkey", "maxkey", "symbol", "regex", "long", "double"];
41
41
  export declare const TARGET_NATIVE_TYPES: readonly ["number", "binary_float", "binary_double", "clob", "nclob", "bfile", "raw", "rowid", "urowid", "xmltype", "sdo_geometry", "interval", "serial", "smallserial", "bigserial", "serial2", "serial4", "serial8", "int2", "int4", "int8", "float4", "float8", "bytea", "citext", "inet", "cidr", "macaddr", "macaddr8", "tsvector", "tsquery", "hstore", "jsonpath", "timetz", "varbit", "int4range", "int8range", "numrange", "tsrange", "tstzrange", "daterange", "uniqueidentifier", "datetimeoffset", "smalldatetime", "rowversion", "hierarchyid", "sql_variant", "timestamp_ntz", "timestamp_ltz", "timestamp_tz", "vector", "float64", "bignumeric", "bigdecimal", "bytes", "geometry", "geography", "ascii", "varint", "timeuuid", "uint32", "uint64", "sint32", "sint64", "fixed32", "fixed64", "sfixed32", "sfixed64", "uint8", "uint16", "int16", "float32"];
42
42
  export declare const SETTING_FLAGS: readonly ["pk", "primary", "key", "unique", "null", "not", "required", "increment", "inactive", "foreign_master"];
43
- 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", "supertype", "completeness", "exclusivity", "strategy", "merge", "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
+ 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", "supertype", "completeness", "exclusivity", "strategy", "merge", "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", "constraints", "replication", "location", "default_charset", "cloned_at"];
44
44
  export declare const GRANULARITY_VALUES: readonly ["year", "quarter", "month", "week", "day", "hour", "minute", "second", "millisecond", "microsecond", "nanosecond"];
45
45
  export declare const DIRECTIVE_KEYWORDS: readonly ["xdbml", "experimental"];
46
46
  export declare const MODULE_KEYWORDS: readonly ["use", "reuse", "from", "as"];
package/dist/keywords.js CHANGED
@@ -286,6 +286,7 @@ export const SETTING_KEYS = [
286
286
  // Block keywords (entity-body or top-level)
287
287
  'indexes',
288
288
  'checks', // v0.2 §10: entity-level checks block
289
+ 'constraints', // v0.6 §10: keys and checks block
289
290
  // Container settings
290
291
  'replication',
291
292
  'location',
@@ -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' | 'possible-type-typo' | '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
  *
@@ -228,6 +229,8 @@ export function resolveNames(doc) {
228
229
  diagnostics.push(...checkRelationships(flat));
229
230
  // Pass 4: supertype group rules (spec 12.8).
230
231
  diagnostics.push(...checkSupertypeGroups(flat));
232
+ // Pass 5: constraints and referenced keys (spec §10.10, §11.17).
233
+ diagnostics.push(...checkConstraints(flat));
231
234
  return { diagnostics, symbols };
232
235
  }
233
236
  /* -------------------------------------------------------------------------
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
  }
@@ -1932,17 +1941,71 @@ export class Parser {
1932
1941
  parseCheckEntry() {
1933
1942
  const start = this.peek().start;
1934
1943
  const exprTok = this.peek();
1935
- if (exprTok.kind !== TokenKind.ExpressionLiteral) {
1936
- 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);
1937
1950
  }
1938
1951
  this.advance();
1939
- // The expression is opaque to xDBML per spec §10.3. The value field of
1940
- // 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.
1941
1954
  const expression = exprTok.value ?? '';
1942
1955
  const settings = this.maybeSettingsBlock();
1943
1956
  return {
1944
1957
  kind: 'CheckEntry',
1945
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,
1946
2009
  settings,
1947
2010
  span: this.spanFrom(start),
1948
2011
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xdbml/parse",
3
- "version": "0.5.1",
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",