@xdbml/parse 0.5.1 → 0.6.1

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
@@ -161,6 +161,15 @@ export const BSON_TYPES = [
161
161
  * are left out for that reason. The list is a convenience, not a
162
162
  * vocabulary: a name missing from it is still a valid type.
163
163
  * ----------------------------------------------------------------------- */
164
+ /* -------------------------------------------------------------------------
165
+ * Entity-body block keywords (TextMate only)
166
+ *
167
+ * The words that open a block inside an entity body. They also appear in
168
+ * SETTING_KEYS above, which colors them in Monaco; the TextMate grammar
169
+ * colors setting keys only inside `[ ]`, so it matches these separately,
170
+ * and only when `{` follows, since a field may carry one of these names.
171
+ * ----------------------------------------------------------------------- */
172
+ export const BLOCK_KEYWORDS = ['indexes', 'checks', 'constraints', 'records'];
164
173
  export const TARGET_NATIVE_TYPES = [
165
174
  // Oracle
166
175
  'number', 'binary_float', 'binary_double', 'clob', 'nclob', 'bfile',
@@ -286,6 +295,7 @@ export const SETTING_KEYS = [
286
295
  // Block keywords (entity-body or top-level)
287
296
  'indexes',
288
297
  'checks', // v0.2 §10: entity-level checks block
298
+ 'constraints', // v0.6 §10: keys and checks block
289
299
  // Container settings
290
300
  'replication',
291
301
  'location',
package/dist/monarch.js CHANGED
@@ -156,6 +156,10 @@ export const xdbmlMonarchTokensProvider = {
156
156
  [/#[0-9A-Fa-f]{3,8}\b/, 'number.hex'],
157
157
  // x_ custom property identifiers
158
158
  [/\bx_[a-zA-Z0-9_]+/, 'identifier.custom-property'],
159
+ // Spec §3.10 (v0.6.1): `Note` starts a note only before ':' or '{',
160
+ // or at the top level before a name and '{'. Anywhere else it names a
161
+ // field, so it takes the identifier color like any other field name.
162
+ [/note(?![\w$])(?!\s*[:{])(?!\s+(?:[A-Za-z_][\w$]*|"[^"]*")\s*\{)/, 'identifier'],
159
163
  // Identifiers and keyword recognition.
160
164
  // The parser is the authority on keyword vs identifier disambiguation;
161
165
  // Monarch does coarse highlighting based on lowercase comparison.
@@ -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' | 'duplicate-unique-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' | 'source-query-in-settings' | 'duplicate-source-query';
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,8 @@ 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";
46
+ import { checkViews } from "./views.js";
45
47
  /**
46
48
  * Read-only handle on the collected symbol table.
47
49
  *
@@ -228,6 +230,10 @@ export function resolveNames(doc) {
228
230
  diagnostics.push(...checkRelationships(flat));
229
231
  // Pass 4: supertype group rules (spec 12.8).
230
232
  diagnostics.push(...checkSupertypeGroups(flat));
233
+ // Pass 5: constraints and referenced keys (spec §10.10, §11.17).
234
+ diagnostics.push(...checkConstraints(flat));
235
+ // Pass 6: view source queries (spec §14.7).
236
+ diagnostics.push(...checkViews(flat));
231
237
  return { diagnostics, symbols };
232
238
  }
233
239
  /* -------------------------------------------------------------------------
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.1";
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 {
@@ -64,6 +64,30 @@ export declare class Parser {
64
64
  private check;
65
65
  private match;
66
66
  private expect;
67
+ /**
68
+ * Skip the commas and semicolons of a list body. In a list body a comma
69
+ * or a semicolon may stand between items, before the first and after the
70
+ * last, any number of times, with no meaning of its own (spec §3.9).
71
+ */
72
+ private skipListSeparators;
73
+ /**
74
+ * Whether the current token starts a note element: `Note` followed by
75
+ * `:` or `{`. Anywhere else `note` names a field (spec §3.10), as in
76
+ * DBML; a field declaration never has `:` or `{` right after its name.
77
+ */
78
+ private atNoteElement;
79
+ /**
80
+ * Whether the current token is the body keyword `word` opening its block,
81
+ * that is, followed by `{` (spec §3.10). Before a type, the same word
82
+ * names a field.
83
+ */
84
+ private atBlockKeyword;
85
+ /**
86
+ * A line such as `materialized: true` in a body is a setting written in
87
+ * the wrong place: a field declaration never has `:` after its name.
88
+ * Name the brackets in the error rather than reporting a missing type.
89
+ */
90
+ private rejectSettingInBody;
67
91
  private spanFrom;
68
92
  parseDocument(): XDbmlDocument;
69
93
  private parseVersionDeclaration;
@@ -326,6 +350,8 @@ export declare class Parser {
326
350
  private parseIndexComponent;
327
351
  private parseChecks;
328
352
  private parseCheckEntry;
353
+ private parseConstraints;
354
+ private parseKeyConstraintEntry;
329
355
  private maybeSettingsBlock;
330
356
  /**
331
357
  * 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.1';
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);
@@ -168,6 +169,50 @@ export class Parser {
168
169
  const t = this.peek();
169
170
  throw new ParseError(`${msg} (got ${t.kind} ${JSON.stringify(t.text)})`, t.start);
170
171
  }
172
+ /* ----- list bodies and body keywords (spec §3.9, §3.10, v0.6.1) ----- */
173
+ /**
174
+ * Skip the commas and semicolons of a list body. In a list body a comma
175
+ * or a semicolon may stand between items, before the first and after the
176
+ * last, any number of times, with no meaning of its own (spec §3.9).
177
+ */
178
+ skipListSeparators() {
179
+ while (this.check(TokenKind.Comma) || this.check(TokenKind.Semicolon))
180
+ this.advance();
181
+ }
182
+ /**
183
+ * Whether the current token starts a note element: `Note` followed by
184
+ * `:` or `{`. Anywhere else `note` names a field (spec §3.10), as in
185
+ * DBML; a field declaration never has `:` or `{` right after its name.
186
+ */
187
+ atNoteElement() {
188
+ if (!isKw(this.peek(), 'note'))
189
+ return false;
190
+ const next = this.peek(1).kind;
191
+ return next === TokenKind.Colon || next === TokenKind.LBrace;
192
+ }
193
+ /**
194
+ * Whether the current token is the body keyword `word` opening its block,
195
+ * that is, followed by `{` (spec §3.10). Before a type, the same word
196
+ * names a field.
197
+ */
198
+ atBlockKeyword(word) {
199
+ return isKw(this.peek(), word) && this.peek(1).kind === TokenKind.LBrace;
200
+ }
201
+ /**
202
+ * A line such as `materialized: true` in a body is a setting written in
203
+ * the wrong place: a field declaration never has `:` after its name.
204
+ * Name the brackets in the error rather than reporting a missing type.
205
+ */
206
+ rejectSettingInBody(construct, name) {
207
+ const t = this.peek();
208
+ if (t.kind !== TokenKind.Identifier || this.peek(1).kind !== TokenKind.Colon)
209
+ return;
210
+ const where = construct === 'View'
211
+ ? 'The body of a View holds source_query:, fields and Note: (spec §14.1, §14.2).'
212
+ : `The body of ${construct === 'Entity' ? 'an' : 'a'} ${construct} holds fields, blocks and Note:.`;
213
+ throw new ParseError(`'${t.text}:' is a setting: write it in the brackets after the ${construct} name, ` +
214
+ `as in ${construct} ${name} [${t.text}: ...] { ... }. ${where}`, t.start);
215
+ }
171
216
  spanFrom(start) {
172
217
  // span end = end-position of the previously consumed token if any
173
218
  const prev = this.idx > 0 ? this.tokens[this.idx - 1] : this.tokens[0];
@@ -191,13 +236,16 @@ export class Parser {
191
236
  while (!this.check(TokenKind.EOF)) {
192
237
  statements.push(this.parseTopLevelStatement());
193
238
  }
194
- return {
239
+ const doc = {
195
240
  kind: 'XDbmlDocument',
196
241
  version,
197
242
  experimental,
198
243
  statements,
199
244
  span: this.spanFrom(start),
200
245
  };
246
+ // v0.6 §10.4: primary key fields are not null in the AST.
247
+ markPrimaryKeyNotNull(doc);
248
+ return doc;
201
249
  }
202
250
  /* ----- version & experimental ----- */
203
251
  parseVersionDeclaration() {
@@ -205,14 +253,22 @@ export class Parser {
205
253
  this.advance(); // xdbml
206
254
  this.expect(TokenKind.Colon, "Expected ':' after 'xdbml'");
207
255
  const numTok = this.expect(TokenKind.NumberLiteral, 'Expected version number');
208
- if (compareVersions(numTok.text, SUPPORTED_XDBML_VERSION) > 0) {
209
- throw new ParseError(`This document declares 'xdbml: ${numTok.text}', which is newer than the ` +
256
+ // Spec §4: MAJOR.MINOR[.PATCH]. The lexer reads `0.6` as one number,
257
+ // so a patch number arrives as a dot and a second number. `xdbml: 0.6`
258
+ // compares as 0.6.0, so it stays valid under every 0.6.x parser.
259
+ let version = numTok.text;
260
+ if (this.check(TokenKind.Dot) && this.peek(1).kind === TokenKind.NumberLiteral) {
261
+ this.advance(); // .
262
+ version += `.${this.advance().text}`;
263
+ }
264
+ if (compareVersions(version, SUPPORTED_XDBML_VERSION) > 0) {
265
+ throw new ParseError(`This document declares 'xdbml: ${version}', which is newer than the ` +
210
266
  `latest version this parser supports (${SUPPORTED_XDBML_VERSION}). ` +
211
267
  'Use a newer parser, or declare a supported version.', numTok.start, 'unsupported-version');
212
268
  }
213
269
  return {
214
270
  kind: 'VersionDeclaration',
215
- version: numTok.text,
271
+ version,
216
272
  span: this.spanFrom(start),
217
273
  };
218
274
  }
@@ -279,7 +335,10 @@ export class Parser {
279
335
  const name = this.parseIdentLikeName('project name');
280
336
  this.expect(TokenKind.LBrace, "Expected '{' after Project name");
281
337
  const body = [];
282
- while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
338
+ for (;;) {
339
+ this.skipListSeparators();
340
+ if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
341
+ break;
283
342
  // Project body is either an inline Note block or a setting line.
284
343
  if (isKw(this.peek(), 'note')) {
285
344
  body.push(this.parseNoteBlockOrSetting());
@@ -439,7 +498,7 @@ export class Parser {
439
498
  }
440
499
  const settings = this.maybeSettingsBlock();
441
500
  this.expect(TokenKind.LBrace, "Expected '{' after entity name");
442
- const body = this.parseEntityBody();
501
+ const body = this.parseEntityBody(canonEntityKw(kwTok.text), name);
443
502
  this.expect(TokenKind.RBrace, "Expected '}' closing entity");
444
503
  return {
445
504
  kind: 'EntityDeclaration',
@@ -473,27 +532,37 @@ export class Parser {
473
532
  }
474
533
  return name;
475
534
  }
476
- parseEntityBody() {
535
+ parseEntityBody(construct, name) {
477
536
  const body = [];
478
- while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
537
+ for (;;) {
538
+ this.skipListSeparators();
539
+ if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
540
+ break;
479
541
  const t = this.peek();
480
- const k = kw(t);
481
- if (k === 'note') {
542
+ // Spec §3.10: each body keyword starts its element only when the
543
+ // token the element needs follows it; otherwise it names a field,
544
+ // as in DBML. (v0.6 §10.1 did this for `constraints`; v0.6.1 does it
545
+ // for `Note`, `indexes`, `checks` and `records`.)
546
+ if (this.atNoteElement()) {
482
547
  body.push(this.parseNoteBlockOrSetting());
483
548
  }
484
- else if (k === 'indexes') {
549
+ else if (this.atBlockKeyword('indexes')) {
485
550
  body.push(this.parseIndexes());
486
551
  }
487
- else if (k === 'checks') {
552
+ else if (this.atBlockKeyword('checks')) {
488
553
  body.push(this.parseChecks());
489
554
  }
490
- else if (k === 'records') {
555
+ else if (this.atBlockKeyword('constraints')) {
556
+ body.push(this.parseConstraints());
557
+ }
558
+ else if (this.atBlockKeyword('records')) {
491
559
  body.push(this.parseRecordsBlock());
492
560
  }
493
561
  else if (t.kind === TokenKind.Tilde) {
494
562
  body.push(this.parsePartialInjection());
495
563
  }
496
564
  else {
565
+ this.rejectSettingInBody(construct, name);
497
566
  body.push(this.parseFieldDeclaration());
498
567
  }
499
568
  }
@@ -983,10 +1052,12 @@ export class Parser {
983
1052
  const kwTok = this.advance();
984
1053
  this.expect(TokenKind.LBrace, "Expected '{' after object keyword");
985
1054
  const fields = [];
986
- while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
1055
+ for (;;) {
1056
+ this.skipListSeparators();
1057
+ if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
1058
+ break;
987
1059
  const t = this.peek();
988
- const k = kw(t);
989
- if (k === 'note') {
1060
+ if (this.atNoteElement()) {
990
1061
  fields.push(this.parseNoteBlockOrSetting());
991
1062
  }
992
1063
  else if (t.kind === TokenKind.Tilde) {
@@ -1207,7 +1278,10 @@ export class Parser {
1207
1278
  this.advance(); // oneOf | anyOf | allOf
1208
1279
  this.expect(TokenKind.LBrace, `Expected '{' after ${flavor}`);
1209
1280
  const alternatives = [];
1210
- while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
1281
+ for (;;) {
1282
+ this.skipListSeparators();
1283
+ if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
1284
+ break;
1211
1285
  alternatives.push(this.parsePolymorphicAlternative());
1212
1286
  }
1213
1287
  this.expect(TokenKind.RBrace, `Expected '}' closing ${flavor}`);
@@ -1240,10 +1314,12 @@ export class Parser {
1240
1314
  if (this.check(TokenKind.LBrace)) {
1241
1315
  this.advance();
1242
1316
  fields = [];
1243
- while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
1317
+ for (;;) {
1318
+ this.skipListSeparators();
1319
+ if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
1320
+ break;
1244
1321
  const t = this.peek();
1245
- const k = kw(t);
1246
- if (k === 'note') {
1322
+ if (this.atNoteElement()) {
1247
1323
  fields.push(this.parseNoteBlockOrSetting());
1248
1324
  }
1249
1325
  else if (t.kind === TokenKind.Tilde) {
@@ -1374,10 +1450,12 @@ export class Parser {
1374
1450
  finishObjectTypeDecl(start, name, settings) {
1375
1451
  this.expect(TokenKind.LBrace, "Expected '{' after Type name");
1376
1452
  const body = [];
1377
- while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
1453
+ for (;;) {
1454
+ this.skipListSeparators();
1455
+ if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
1456
+ break;
1378
1457
  const t = this.peek();
1379
- const k = kw(t);
1380
- if (k === 'note') {
1458
+ if (this.atNoteElement()) {
1381
1459
  body.push(this.parseNoteBlockOrSetting());
1382
1460
  }
1383
1461
  else if (t.kind === TokenKind.Tilde) {
@@ -1403,7 +1481,7 @@ export class Parser {
1403
1481
  const name = this.parseIdentLikeName('edge name');
1404
1482
  const settings = this.maybeSettingsBlock();
1405
1483
  this.expect(TokenKind.LBrace, "Expected '{' after Edge settings");
1406
- const body = this.parseEntityBody();
1484
+ const body = this.parseEntityBody('Edge', name);
1407
1485
  this.expect(TokenKind.RBrace, "Expected '}' closing Edge");
1408
1486
  return {
1409
1487
  kind: 'EdgeDeclaration',
@@ -1421,16 +1499,22 @@ export class Parser {
1421
1499
  const settings = this.maybeSettingsBlock();
1422
1500
  this.expect(TokenKind.LBrace, "Expected '{' after View name");
1423
1501
  const body = [];
1424
- while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
1425
- const t = this.peek();
1426
- const k = kw(t);
1427
- if (k === 'note') {
1502
+ for (;;) {
1503
+ this.skipListSeparators();
1504
+ if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
1505
+ break;
1506
+ // Spec §3.10: `Note` and `source_query` start their elements only
1507
+ // before `:` (or `{` for a note); otherwise they name a field.
1508
+ if (this.atNoteElement()) {
1428
1509
  body.push(this.parseNoteBlockOrSetting());
1429
1510
  }
1430
- else if (k === 'source_query') {
1511
+ else if (isKw(this.peek(), 'source_query') && this.peek(1).kind === TokenKind.Colon) {
1431
1512
  body.push(this.parseSourceQueryItem());
1432
1513
  }
1433
1514
  else {
1515
+ // Spec §14.2: `materialized: true` and the other settings go in
1516
+ // the brackets, never in the body.
1517
+ this.rejectSettingInBody('View', name);
1434
1518
  body.push(this.parseFieldDeclaration());
1435
1519
  }
1436
1520
  }
@@ -1471,7 +1555,10 @@ export class Parser {
1471
1555
  }
1472
1556
  this.expect(TokenKind.LBrace, "Expected '{' after enum name");
1473
1557
  const values = [];
1474
- while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
1558
+ for (;;) {
1559
+ this.skipListSeparators();
1560
+ if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
1561
+ break;
1475
1562
  const vStart = this.peek().start;
1476
1563
  const vt = this.peek();
1477
1564
  let vname;
@@ -1745,7 +1832,7 @@ export class Parser {
1745
1832
  const name = this.parseIdentLikeName('TablePartial name');
1746
1833
  const settings = this.maybeSettingsBlock();
1747
1834
  this.expect(TokenKind.LBrace, "Expected '{' after TablePartial name");
1748
- const body = this.parseEntityBody();
1835
+ const body = this.parseEntityBody('TablePartial', name);
1749
1836
  this.expect(TokenKind.RBrace, "Expected '}' closing TablePartial");
1750
1837
  return {
1751
1838
  kind: 'TablePartialDeclaration',
@@ -1860,6 +1947,14 @@ export class Parser {
1860
1947
  this.expect(TokenKind.LBrace, "Expected '{' after indexes");
1861
1948
  const entries = [];
1862
1949
  while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
1950
+ // Spec §3.9: `indexes` is not a list body. `email, name` may be meant
1951
+ // as the composite index `(email, name)`, so a separator between
1952
+ // entries stays an error rather than declaring two indexes.
1953
+ const t = this.peek();
1954
+ if (t.kind === TokenKind.Comma || t.kind === TokenKind.Semicolon) {
1955
+ throw new ParseError(`Unexpected '${t.text}' in indexes: write one index per line, and a composite ` +
1956
+ 'index in parentheses, as in (email, name) (spec §3.9).', t.start);
1957
+ }
1863
1958
  entries.push(this.parseIndexEntry());
1864
1959
  }
1865
1960
  this.expect(TokenKind.RBrace, "Expected '}' closing indexes");
@@ -1919,7 +2014,10 @@ export class Parser {
1919
2014
  this.advance(); // checks
1920
2015
  this.expect(TokenKind.LBrace, "Expected '{' after checks");
1921
2016
  const entries = [];
1922
- while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
2017
+ for (;;) {
2018
+ this.skipListSeparators();
2019
+ if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
2020
+ break;
1923
2021
  entries.push(this.parseCheckEntry());
1924
2022
  }
1925
2023
  this.expect(TokenKind.RBrace, "Expected '}' closing checks");
@@ -1932,17 +2030,74 @@ export class Parser {
1932
2030
  parseCheckEntry() {
1933
2031
  const start = this.peek().start;
1934
2032
  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);
2033
+ // v0.6 §10.5: single (or triple) quotes are the standard form and
2034
+ // backticks an alias. The resolver rejects the quoted form in documents
2035
+ // declaring an earlier version.
2036
+ const quoted = exprTok.kind === TokenKind.StringLiteral || exprTok.kind === TokenKind.MultilineString;
2037
+ if (exprTok.kind !== TokenKind.ExpressionLiteral && !quoted) {
2038
+ throw new ParseError(`Expected a check expression in quotes or backticks, got ${exprTok.kind} ${JSON.stringify(exprTok.text)}`, exprTok.start);
1937
2039
  }
1938
2040
  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.
2041
+ // The expression is opaque to xDBML (spec §10.5). The token value
2042
+ // already has the delimiters stripped.
1941
2043
  const expression = exprTok.value ?? '';
1942
2044
  const settings = this.maybeSettingsBlock();
1943
2045
  return {
1944
2046
  kind: 'CheckEntry',
1945
2047
  expression,
2048
+ delimiter: quoted ? 'quote' : 'backtick',
2049
+ settings,
2050
+ span: this.spanFrom(start),
2051
+ };
2052
+ }
2053
+ /* ----- constraints block (v0.6 §10) ----- */
2054
+ parseConstraints() {
2055
+ const start = this.peek().start;
2056
+ this.advance(); // constraints
2057
+ this.expect(TokenKind.LBrace, "Expected '{' after constraints");
2058
+ const entries = [];
2059
+ for (;;) {
2060
+ this.skipListSeparators();
2061
+ if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
2062
+ break;
2063
+ const t = this.peek();
2064
+ if (t.kind === TokenKind.StringLiteral ||
2065
+ t.kind === TokenKind.MultilineString ||
2066
+ t.kind === TokenKind.ExpressionLiteral) {
2067
+ entries.push(this.parseCheckEntry());
2068
+ }
2069
+ else {
2070
+ entries.push(this.parseKeyConstraintEntry());
2071
+ }
2072
+ }
2073
+ this.expect(TokenKind.RBrace, "Expected '}' closing constraints");
2074
+ return {
2075
+ kind: 'ConstraintsBlock',
2076
+ entries,
2077
+ span: this.spanFrom(start),
2078
+ };
2079
+ }
2080
+ parseKeyConstraintEntry() {
2081
+ const start = this.peek().start;
2082
+ const fields = [];
2083
+ let parenthesized = false;
2084
+ if (this.check(TokenKind.LParen)) {
2085
+ parenthesized = true;
2086
+ this.advance(); // (
2087
+ fields.push(this.parsePathSegments());
2088
+ while (this.match(TokenKind.Comma)) {
2089
+ fields.push(this.parsePathSegments());
2090
+ }
2091
+ this.expect(TokenKind.RParen, "Expected ')' closing the fields of a key");
2092
+ }
2093
+ else {
2094
+ fields.push(this.parsePathSegments());
2095
+ }
2096
+ const settings = this.maybeSettingsBlock();
2097
+ return {
2098
+ kind: 'KeyConstraintEntry',
2099
+ fields,
2100
+ parenthesized,
1946
2101
  settings,
1947
2102
  span: this.spanFrom(start),
1948
2103
  };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Views (spec §14): the source query and the checks of §14.7.
3
+ *
4
+ * A View's source query is a body element, `source_query: '...'`, at most
5
+ * once (§14.3). The grammar of v0.6.0 also accepted `source_query` in the
6
+ * settings brackets; since v0.6.1 that form is not the view's source query
7
+ * and draws a warning, so a document valid under v0.6.0 stays valid.
8
+ *
9
+ * `viewSourceQuery()` is the normalized AST of §28.7: the one source query
10
+ * of a View, the first `source_query:` element of its body. Consumers such
11
+ * as the playground inspector read it rather than walking the body.
12
+ */
13
+ import type { SourceQueryItem, ViewDeclaration, XDbmlDocument } from './ast.ts';
14
+ import type { Diagnostic } from './name-resolver.ts';
15
+ /** The source query of a View (spec §14.3, §28.7), or undefined when it declares none. */
16
+ export declare function viewSourceQuery(view: ViewDeclaration): SourceQueryItem | undefined;
17
+ /** The warnings of spec §14.7. Both concern forms v0.6.0 accepted. */
18
+ export declare function checkViews(doc: XDbmlDocument): Diagnostic[];
package/dist/views.js ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Views (spec §14): the source query and the checks of §14.7.
3
+ *
4
+ * A View's source query is a body element, `source_query: '...'`, at most
5
+ * once (§14.3). The grammar of v0.6.0 also accepted `source_query` in the
6
+ * settings brackets; since v0.6.1 that form is not the view's source query
7
+ * and draws a warning, so a document valid under v0.6.0 stays valid.
8
+ *
9
+ * `viewSourceQuery()` is the normalized AST of §28.7: the one source query
10
+ * of a View, the first `source_query:` element of its body. Consumers such
11
+ * as the playground inspector read it rather than walking the body.
12
+ */
13
+ /** The source query of a View (spec §14.3, §28.7), or undefined when it declares none. */
14
+ export function viewSourceQuery(view) {
15
+ for (const item of view.body) {
16
+ if (item.kind === 'SourceQueryItem')
17
+ return item;
18
+ }
19
+ return undefined;
20
+ }
21
+ function views(doc) {
22
+ const out = [];
23
+ for (const stmt of doc.statements) {
24
+ if (stmt.kind === 'ViewDeclaration')
25
+ out.push(stmt);
26
+ else if (stmt.kind === 'ContainerDeclaration') {
27
+ for (const item of stmt.body) {
28
+ if (item.kind === 'ViewDeclaration')
29
+ out.push(item);
30
+ }
31
+ }
32
+ }
33
+ return out;
34
+ }
35
+ /** The warnings of spec §14.7. Both concern forms v0.6.0 accepted. */
36
+ export function checkViews(doc) {
37
+ const diagnostics = [];
38
+ for (const view of views(doc)) {
39
+ for (const s of view.settings) {
40
+ if (s.name !== 'source_query')
41
+ continue;
42
+ diagnostics.push({
43
+ severity: 'warning',
44
+ code: 'source-query-in-settings',
45
+ message: `View '${view.name}' writes source_query in its settings, where it is not the view's ` +
46
+ "source query. Move it into the body: View name { source_query: '...' ... } (spec §14.3).",
47
+ span: s.span,
48
+ });
49
+ }
50
+ const queries = view.body.filter((b) => b.kind === 'SourceQueryItem');
51
+ for (const extra of queries.slice(1)) {
52
+ diagnostics.push({
53
+ severity: 'warning',
54
+ code: 'duplicate-source-query',
55
+ message: `View '${view.name}' declares a second source query; the first one is the view's ` +
56
+ 'source query (spec §14.3).',
57
+ span: extra.span,
58
+ });
59
+ }
60
+ }
61
+ return diagnostics;
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xdbml/parse",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
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",