@xdbml/parse 0.6.0 → 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.
@@ -437,17 +437,38 @@ export function checkConstraints(doc) {
437
437
  }
438
438
  }
439
439
  // One primary key, declared in one place (§10.4). Inline [pk] fields
440
- // count as one declaration.
440
+ // count as one declaration. When the second declaration restates the
441
+ // same fields, say so: the fix is to delete one, not to look for
442
+ // another key.
441
443
  const own = bodyConstraints(decl.body, v06);
442
444
  const pks = own.filter((c) => c.kind === 'key' && c.keyKind === 'primary');
443
445
  for (const extra of pks.slice(1)) {
446
+ const first = pks[0];
444
447
  diagnostics.push({
445
448
  severity: olderSeverity,
446
449
  code: 'duplicate-primary-key',
447
- message: `'${decl.name}' declares a second primary key; an entity has one, declared in one place.`,
450
+ message: sameKeyFields(first.fields, extra.fields)
451
+ ? `'${decl.name}' declares its primary key (${first.fields.join(', ')}) twice: ` +
452
+ `${twoForms(first, extra)}. Declare it in one place (spec §10.4).`
453
+ : `'${decl.name}' declares a second primary key; an entity has one, declared in one place.`,
448
454
  span: extra.span,
449
455
  });
450
456
  }
457
+ // Each unique key is declared once (§10.3, v0.6.1): a warning in every
458
+ // version, since earlier documents accept the repetition.
459
+ const uniques = own.filter((c) => c.kind === 'key' && c.keyKind === 'unique');
460
+ for (const [i, u] of uniques.entries()) {
461
+ const first = uniques.slice(0, i).find((prev) => sameKeyFields(prev.fields, u.fields));
462
+ if (!first)
463
+ continue;
464
+ diagnostics.push({
465
+ severity: 'warning',
466
+ code: 'duplicate-unique-key',
467
+ message: `'${decl.name}' declares the unique key (${first.fields.join(', ')}) twice: ` +
468
+ `${twoForms(first, u)}. Declare it in one place (spec §10.3).`,
469
+ span: u.span,
470
+ });
471
+ }
451
472
  // `null` on a primary key field (§10.4).
452
473
  for (const pk of pks.slice(0, 1)) {
453
474
  for (const dotted of pk.fields) {
@@ -467,6 +488,32 @@ export function checkConstraints(doc) {
467
488
  diagnostics.push(...checkReferencedKeys(doc, olderSeverity));
468
489
  return diagnostics;
469
490
  }
491
+ /** Two keys on the same fields, in any order, are the same key. */
492
+ function sameKeyFields(a, b) {
493
+ if (a.length !== b.length)
494
+ return false;
495
+ const sa = [...a].sort();
496
+ const sb = [...b].sort();
497
+ return sa.every((f, i) => f === sb[i]);
498
+ }
499
+ /** How a key was written, for a message that names both declarations. */
500
+ function keyForm(k) {
501
+ const flag = k.keyKind === 'primary' ? 'pk' : 'unique';
502
+ if (k.source === 'inline')
503
+ return `[${flag}] on ${k.fields.join(', ')}`;
504
+ if (k.source === 'indexes')
505
+ return `a ${flag} entry in indexes`;
506
+ return `a ${flag} line in constraints`;
507
+ }
508
+ /** Both declarations of a repeated key: "[pk] on id and a pk line in constraints". */
509
+ function twoForms(a, b) {
510
+ const fa = keyForm(a);
511
+ const fb = keyForm(b);
512
+ if (fa !== fb)
513
+ return `${fa} and ${fb}`;
514
+ const flag = a.keyKind === 'primary' ? 'pk' : 'unique';
515
+ return a.source === 'indexes' ? `two ${flag} entries in indexes` : `two ${flag} lines in constraints`;
516
+ }
470
517
  function entityIndex(doc) {
471
518
  const m = new Map();
472
519
  const bare = new Map();
package/dist/index.d.ts CHANGED
@@ -32,6 +32,7 @@ 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 { checkViews, viewSourceQuery } from './views.ts';
35
36
  export { bodyConstraints, checkConstraints, entityConstraints, isRelationalTarget, keyPathString, markPrimaryKeyNotNull, primaryKey, } from './constraints.ts';
36
37
  export type { CheckConstraint, Constraint, ConstraintSource, KeyConstraint } from './constraints.ts';
37
38
  export { FOREIGN_MASTER_FLAG, checkRelationships, hasForeignMasterFlag, isForeignMaster, pathToString, refChildEndpoint, refParentEndpoint, relationshipType, versionAtLeast, CONSTRAINT_TYPES, V04_RELATIONSHIP_SETTINGS, constraintType, entityNames, isEntityLevelEndpoint, isUndirected, } from './relationships.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 { checkViews, viewSourceQuery } from "./views.js";
33
34
  export { bodyConstraints, checkConstraints, entityConstraints, isRelationalTarget, keyPathString, markPrimaryKeyNotNull, primaryKey, } from "./constraints.js";
34
35
  export { FOREIGN_MASTER_FLAG, checkRelationships, hasForeignMasterFlag, isForeignMaster, pathToString, refChildEndpoint, refParentEndpoint, relationshipType, versionAtLeast, CONSTRAINT_TYPES, V04_RELATIONSHIP_SETTINGS, constraintType, entityNames, isEntityLevelEndpoint, isUndirected, } from "./relationships.js";
35
36
  export { SUPERTYPE_GROUP_VALUES, canonicalSupertypeGroupValue, checkSupertypeGroups, resolveSupertypeGroups, subtypeStrategy, supertypeChains, supertypeGroupSettings, } from "./supertypes.js";
@@ -38,6 +38,7 @@ export declare const STRUCTURAL_TYPE_KEYWORDS: readonly ["object", "struct", "ar
38
38
  export declare const POLYMORPHISM_KEYWORDS: readonly ["union", "oneof", "anyof", "allof"];
39
39
  export declare const SCALAR_TYPES: readonly ["tinyint", "smallint", "mediumint", "int", "integer", "bigint", "int32", "int64", "float", "double", "decimal", "dec", "numeric", "real", "bit", "bool", "boolean", "char", "varchar", "varchar2", "nvarchar", "nvarchar2", "nchar", "text", "mediumtext", "longtext", "string", "ntext", "binary", "varbinary", "blob", "mediumblob", "longblob", "tinyblob", "tinytext", "json", "jsonb", "variant", "xml", "date", "time", "datetime", "datetime2", "timestamp", "timestamptz", "year", "uuid", "inet6", "money", "smallmoney", "enum"];
40
40
  export declare const BSON_TYPES: readonly ["objectid", "decimal128", "bindata", "minkey", "maxkey", "symbol", "regex", "long", "double"];
41
+ export declare const BLOCK_KEYWORDS: readonly ["indexes", "checks", "constraints", "records"];
41
42
  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
43
  export declare const SETTING_FLAGS: readonly ["pk", "primary", "key", "unique", "null", "not", "required", "increment", "inactive", "foreign_master"];
43
44
  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"];
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',
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-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';
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.,
@@ -43,6 +43,7 @@ import { flatten } from "./module-resolver.js";
43
43
  import { checkRelationships } from "./relationships.js";
44
44
  import { checkSupertypeGroups } from "./supertypes.js";
45
45
  import { checkConstraints } from "./constraints.js";
46
+ import { checkViews } from "./views.js";
46
47
  /**
47
48
  * Read-only handle on the collected symbol table.
48
49
  *
@@ -231,6 +232,8 @@ export function resolveNames(doc) {
231
232
  diagnostics.push(...checkSupertypeGroups(flat));
232
233
  // Pass 5: constraints and referenced keys (spec §10.10, §11.17).
233
234
  diagnostics.push(...checkConstraints(flat));
235
+ // Pass 6: view source queries (spec §14.7).
236
+ diagnostics.push(...checkViews(flat));
234
237
  return { diagnostics, symbols };
235
238
  }
236
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.6";
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;
package/dist/parser.js CHANGED
@@ -32,7 +32,7 @@ export class ParseError extends Error {
32
32
  * declaring a later version is refused (spec 4.1) rather than parsed with
33
33
  * semantics it does not have.
34
34
  */
35
- export const SUPPORTED_XDBML_VERSION = '0.6';
35
+ export const SUPPORTED_XDBML_VERSION = '0.6.1';
36
36
  /** Compare dotted version strings numerically: -1, 0 or 1. */
37
37
  export function compareVersions(a, b) {
38
38
  const pa = a.split('.').map((n) => Number(n) || 0);
@@ -169,6 +169,50 @@ export class Parser {
169
169
  const t = this.peek();
170
170
  throw new ParseError(`${msg} (got ${t.kind} ${JSON.stringify(t.text)})`, t.start);
171
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
+ }
172
216
  spanFrom(start) {
173
217
  // span end = end-position of the previously consumed token if any
174
218
  const prev = this.idx > 0 ? this.tokens[this.idx - 1] : this.tokens[0];
@@ -209,14 +253,22 @@ export class Parser {
209
253
  this.advance(); // xdbml
210
254
  this.expect(TokenKind.Colon, "Expected ':' after 'xdbml'");
211
255
  const numTok = this.expect(TokenKind.NumberLiteral, 'Expected version number');
212
- if (compareVersions(numTok.text, SUPPORTED_XDBML_VERSION) > 0) {
213
- 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 ` +
214
266
  `latest version this parser supports (${SUPPORTED_XDBML_VERSION}). ` +
215
267
  'Use a newer parser, or declare a supported version.', numTok.start, 'unsupported-version');
216
268
  }
217
269
  return {
218
270
  kind: 'VersionDeclaration',
219
- version: numTok.text,
271
+ version,
220
272
  span: this.spanFrom(start),
221
273
  };
222
274
  }
@@ -283,7 +335,10 @@ export class Parser {
283
335
  const name = this.parseIdentLikeName('project name');
284
336
  this.expect(TokenKind.LBrace, "Expected '{' after Project name");
285
337
  const body = [];
286
- 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;
287
342
  // Project body is either an inline Note block or a setting line.
288
343
  if (isKw(this.peek(), 'note')) {
289
344
  body.push(this.parseNoteBlockOrSetting());
@@ -443,7 +498,7 @@ export class Parser {
443
498
  }
444
499
  const settings = this.maybeSettingsBlock();
445
500
  this.expect(TokenKind.LBrace, "Expected '{' after entity name");
446
- const body = this.parseEntityBody();
501
+ const body = this.parseEntityBody(canonEntityKw(kwTok.text), name);
447
502
  this.expect(TokenKind.RBrace, "Expected '}' closing entity");
448
503
  return {
449
504
  kind: 'EntityDeclaration',
@@ -477,32 +532,37 @@ export class Parser {
477
532
  }
478
533
  return name;
479
534
  }
480
- parseEntityBody() {
535
+ parseEntityBody(construct, name) {
481
536
  const body = [];
482
- 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;
483
541
  const t = this.peek();
484
- const k = kw(t);
485
- 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()) {
486
547
  body.push(this.parseNoteBlockOrSetting());
487
548
  }
488
- else if (k === 'indexes') {
549
+ else if (this.atBlockKeyword('indexes')) {
489
550
  body.push(this.parseIndexes());
490
551
  }
491
- else if (k === 'checks') {
552
+ else if (this.atBlockKeyword('checks')) {
492
553
  body.push(this.parseChecks());
493
554
  }
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`.
555
+ else if (this.atBlockKeyword('constraints')) {
497
556
  body.push(this.parseConstraints());
498
557
  }
499
- else if (k === 'records') {
558
+ else if (this.atBlockKeyword('records')) {
500
559
  body.push(this.parseRecordsBlock());
501
560
  }
502
561
  else if (t.kind === TokenKind.Tilde) {
503
562
  body.push(this.parsePartialInjection());
504
563
  }
505
564
  else {
565
+ this.rejectSettingInBody(construct, name);
506
566
  body.push(this.parseFieldDeclaration());
507
567
  }
508
568
  }
@@ -992,10 +1052,12 @@ export class Parser {
992
1052
  const kwTok = this.advance();
993
1053
  this.expect(TokenKind.LBrace, "Expected '{' after object keyword");
994
1054
  const fields = [];
995
- 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;
996
1059
  const t = this.peek();
997
- const k = kw(t);
998
- if (k === 'note') {
1060
+ if (this.atNoteElement()) {
999
1061
  fields.push(this.parseNoteBlockOrSetting());
1000
1062
  }
1001
1063
  else if (t.kind === TokenKind.Tilde) {
@@ -1216,7 +1278,10 @@ export class Parser {
1216
1278
  this.advance(); // oneOf | anyOf | allOf
1217
1279
  this.expect(TokenKind.LBrace, `Expected '{' after ${flavor}`);
1218
1280
  const alternatives = [];
1219
- 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;
1220
1285
  alternatives.push(this.parsePolymorphicAlternative());
1221
1286
  }
1222
1287
  this.expect(TokenKind.RBrace, `Expected '}' closing ${flavor}`);
@@ -1249,10 +1314,12 @@ export class Parser {
1249
1314
  if (this.check(TokenKind.LBrace)) {
1250
1315
  this.advance();
1251
1316
  fields = [];
1252
- 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;
1253
1321
  const t = this.peek();
1254
- const k = kw(t);
1255
- if (k === 'note') {
1322
+ if (this.atNoteElement()) {
1256
1323
  fields.push(this.parseNoteBlockOrSetting());
1257
1324
  }
1258
1325
  else if (t.kind === TokenKind.Tilde) {
@@ -1383,10 +1450,12 @@ export class Parser {
1383
1450
  finishObjectTypeDecl(start, name, settings) {
1384
1451
  this.expect(TokenKind.LBrace, "Expected '{' after Type name");
1385
1452
  const body = [];
1386
- 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;
1387
1457
  const t = this.peek();
1388
- const k = kw(t);
1389
- if (k === 'note') {
1458
+ if (this.atNoteElement()) {
1390
1459
  body.push(this.parseNoteBlockOrSetting());
1391
1460
  }
1392
1461
  else if (t.kind === TokenKind.Tilde) {
@@ -1412,7 +1481,7 @@ export class Parser {
1412
1481
  const name = this.parseIdentLikeName('edge name');
1413
1482
  const settings = this.maybeSettingsBlock();
1414
1483
  this.expect(TokenKind.LBrace, "Expected '{' after Edge settings");
1415
- const body = this.parseEntityBody();
1484
+ const body = this.parseEntityBody('Edge', name);
1416
1485
  this.expect(TokenKind.RBrace, "Expected '}' closing Edge");
1417
1486
  return {
1418
1487
  kind: 'EdgeDeclaration',
@@ -1430,16 +1499,22 @@ export class Parser {
1430
1499
  const settings = this.maybeSettingsBlock();
1431
1500
  this.expect(TokenKind.LBrace, "Expected '{' after View name");
1432
1501
  const body = [];
1433
- while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
1434
- const t = this.peek();
1435
- const k = kw(t);
1436
- 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()) {
1437
1509
  body.push(this.parseNoteBlockOrSetting());
1438
1510
  }
1439
- else if (k === 'source_query') {
1511
+ else if (isKw(this.peek(), 'source_query') && this.peek(1).kind === TokenKind.Colon) {
1440
1512
  body.push(this.parseSourceQueryItem());
1441
1513
  }
1442
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);
1443
1518
  body.push(this.parseFieldDeclaration());
1444
1519
  }
1445
1520
  }
@@ -1480,7 +1555,10 @@ export class Parser {
1480
1555
  }
1481
1556
  this.expect(TokenKind.LBrace, "Expected '{' after enum name");
1482
1557
  const values = [];
1483
- 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;
1484
1562
  const vStart = this.peek().start;
1485
1563
  const vt = this.peek();
1486
1564
  let vname;
@@ -1754,7 +1832,7 @@ export class Parser {
1754
1832
  const name = this.parseIdentLikeName('TablePartial name');
1755
1833
  const settings = this.maybeSettingsBlock();
1756
1834
  this.expect(TokenKind.LBrace, "Expected '{' after TablePartial name");
1757
- const body = this.parseEntityBody();
1835
+ const body = this.parseEntityBody('TablePartial', name);
1758
1836
  this.expect(TokenKind.RBrace, "Expected '}' closing TablePartial");
1759
1837
  return {
1760
1838
  kind: 'TablePartialDeclaration',
@@ -1869,6 +1947,14 @@ export class Parser {
1869
1947
  this.expect(TokenKind.LBrace, "Expected '{' after indexes");
1870
1948
  const entries = [];
1871
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
+ }
1872
1958
  entries.push(this.parseIndexEntry());
1873
1959
  }
1874
1960
  this.expect(TokenKind.RBrace, "Expected '}' closing indexes");
@@ -1928,7 +2014,10 @@ export class Parser {
1928
2014
  this.advance(); // checks
1929
2015
  this.expect(TokenKind.LBrace, "Expected '{' after checks");
1930
2016
  const entries = [];
1931
- 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;
1932
2021
  entries.push(this.parseCheckEntry());
1933
2022
  }
1934
2023
  this.expect(TokenKind.RBrace, "Expected '}' closing checks");
@@ -1967,7 +2056,10 @@ export class Parser {
1967
2056
  this.advance(); // constraints
1968
2057
  this.expect(TokenKind.LBrace, "Expected '{' after constraints");
1969
2058
  const entries = [];
1970
- while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
2059
+ for (;;) {
2060
+ this.skipListSeparators();
2061
+ if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
2062
+ break;
1971
2063
  const t = this.peek();
1972
2064
  if (t.kind === TokenKind.StringLiteral ||
1973
2065
  t.kind === TokenKind.MultilineString ||
@@ -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.6.0",
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",