@xdbml/parse 0.6.0 → 0.6.2

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.
@@ -77,5 +77,14 @@ export declare function primaryKey(decl: {
77
77
  export declare function markPrimaryKeyNotNull(doc: XDbmlDocument): void;
78
78
  /** True for a target name the §11.17 rule covers. Unlisted names are not relational. */
79
79
  export declare function isRelationalTarget(name: string): boolean;
80
+ /**
81
+ * Spec §5.2, rules 3 and 4: in a Project with several targets, every
82
+ * Container declares its own; and a Container's target is one of the
83
+ * Project's. Both apply to Containers written as such, only when the
84
+ * Project declares `targets:`. An implicit container (`Table core.users`)
85
+ * has no place for a target and is not checked. Before v0.6 the parser
86
+ * accepted both, so an earlier version draws warnings.
87
+ */
88
+ export declare function checkTargets(doc: XDbmlDocument): Diagnostic[];
80
89
  export declare function checkConstraints(doc: XDbmlDocument): Diagnostic[];
81
90
  export {};
@@ -329,6 +329,75 @@ function effectiveTarget(p, project) {
329
329
  const values = settingValues(own?.value ?? null).filter((x) => x !== '');
330
330
  return values[0] ?? project;
331
331
  }
332
+ /** Aliases of §5.1, lower-cased, mapped to their canonical name. */
333
+ const TARGET_ALIASES = {
334
+ postgres: 'postgresql', pg: 'postgresql',
335
+ mssql: 'sql server', 'microsoft sql server': 'sql server', 't-sql': 'sql server',
336
+ 'ibm db2': 'db2', 'db2 luw': 'db2', 'db2 z/os': 'db2 for z/os',
337
+ 'google bigquery': 'bigquery', 'amazon redshift': 'redshift', 'azure synapse': 'synapse analytics',
338
+ mongo: 'mongodb', 'aws documentdb': 'documentdb', cosmos: 'cosmos db', 'azure cosmos db': 'cosmos db',
339
+ 'apache cassandra': 'cassandra', 'amazon neptune': 'neptune',
340
+ 'apache avro': 'avro', 'apache parquet': 'parquet',
341
+ 'protocol buffers': 'protobuf', proto: 'protobuf', swagger: 'openapi',
342
+ };
343
+ function canonicalTarget(name) {
344
+ const key = name.trim().toLowerCase();
345
+ return TARGET_ALIASES[key] ?? key;
346
+ }
347
+ /** Every target the Project declares, or undefined when it declares none. */
348
+ function projectTargets(doc) {
349
+ for (const s of doc.statements) {
350
+ if (s.kind !== 'ProjectDeclaration')
351
+ continue;
352
+ for (const item of s.body) {
353
+ if (item.kind === 'Setting' && (item.name === 'targets' || item.name === 'database_type')) {
354
+ const values = settingValues(item.value).filter((x) => x !== '');
355
+ return values.length > 0 ? values : undefined;
356
+ }
357
+ }
358
+ }
359
+ return undefined;
360
+ }
361
+ /**
362
+ * Spec §5.2, rules 3 and 4: in a Project with several targets, every
363
+ * Container declares its own; and a Container's target is one of the
364
+ * Project's. Both apply to Containers written as such, only when the
365
+ * Project declares `targets:`. An implicit container (`Table core.users`)
366
+ * has no place for a target and is not checked. Before v0.6 the parser
367
+ * accepted both, so an earlier version draws warnings.
368
+ */
369
+ export function checkTargets(doc) {
370
+ const diagnostics = [];
371
+ const declared = projectTargets(doc);
372
+ if (!declared)
373
+ return diagnostics;
374
+ const severity = versionAtLeast(doc, '0.6') ? 'error' : 'warning';
375
+ const canon = new Set(declared.map(canonicalTarget));
376
+ for (const s of doc.statements) {
377
+ if (s.kind !== 'ContainerDeclaration')
378
+ continue;
379
+ const own = settingValues(s.settings.find((x) => x.name === 'target')?.value ?? null).filter((x) => x !== '');
380
+ if (own.length === 0 && declared.length > 1) {
381
+ diagnostics.push({
382
+ severity,
383
+ code: 'container-target-missing',
384
+ message: `Container '${s.name}' declares no target, and the Project declares several (${declared.join(', ')}). ` +
385
+ `Write the target after its name: ${s.keyword} ${s.name} [target: ${declared[0]}] { ... } (spec §5.2).`,
386
+ span: s.span,
387
+ });
388
+ }
389
+ else if (own.length > 0 && !canon.has(canonicalTarget(own[0]))) {
390
+ diagnostics.push({
391
+ severity,
392
+ code: 'container-target-not-in-project',
393
+ message: `Container '${s.name}' targets ${own[0]}, which the Project's targets do not list (${declared.join(', ')}). ` +
394
+ `Add it to the Project's targets, or change the Container's target (spec §5.2).`,
395
+ span: s.span,
396
+ });
397
+ }
398
+ }
399
+ return diagnostics;
400
+ }
332
401
  /* -------------------------------------------------------------------------
333
402
  * Checks (§10.10, §11.17)
334
403
  * ----------------------------------------------------------------------- */
@@ -437,17 +506,38 @@ export function checkConstraints(doc) {
437
506
  }
438
507
  }
439
508
  // One primary key, declared in one place (§10.4). Inline [pk] fields
440
- // count as one declaration.
509
+ // count as one declaration. When the second declaration restates the
510
+ // same fields, say so: the fix is to delete one, not to look for
511
+ // another key.
441
512
  const own = bodyConstraints(decl.body, v06);
442
513
  const pks = own.filter((c) => c.kind === 'key' && c.keyKind === 'primary');
443
514
  for (const extra of pks.slice(1)) {
515
+ const first = pks[0];
444
516
  diagnostics.push({
445
517
  severity: olderSeverity,
446
518
  code: 'duplicate-primary-key',
447
- message: `'${decl.name}' declares a second primary key; an entity has one, declared in one place.`,
519
+ message: sameKeyFields(first.fields, extra.fields)
520
+ ? `'${decl.name}' declares its primary key (${first.fields.join(', ')}) twice: ` +
521
+ `${twoForms(first, extra)}. Declare it in one place (spec §10.4).`
522
+ : `'${decl.name}' declares a second primary key; an entity has one, declared in one place.`,
448
523
  span: extra.span,
449
524
  });
450
525
  }
526
+ // Each unique key is declared once (§10.3, v0.6.1): a warning in every
527
+ // version, since earlier documents accept the repetition.
528
+ const uniques = own.filter((c) => c.kind === 'key' && c.keyKind === 'unique');
529
+ for (const [i, u] of uniques.entries()) {
530
+ const first = uniques.slice(0, i).find((prev) => sameKeyFields(prev.fields, u.fields));
531
+ if (!first)
532
+ continue;
533
+ diagnostics.push({
534
+ severity: 'warning',
535
+ code: 'duplicate-unique-key',
536
+ message: `'${decl.name}' declares the unique key (${first.fields.join(', ')}) twice: ` +
537
+ `${twoForms(first, u)}. Declare it in one place (spec §10.3).`,
538
+ span: u.span,
539
+ });
540
+ }
451
541
  // `null` on a primary key field (§10.4).
452
542
  for (const pk of pks.slice(0, 1)) {
453
543
  for (const dotted of pk.fields) {
@@ -467,6 +557,32 @@ export function checkConstraints(doc) {
467
557
  diagnostics.push(...checkReferencedKeys(doc, olderSeverity));
468
558
  return diagnostics;
469
559
  }
560
+ /** Two keys on the same fields, in any order, are the same key. */
561
+ function sameKeyFields(a, b) {
562
+ if (a.length !== b.length)
563
+ return false;
564
+ const sa = [...a].sort();
565
+ const sb = [...b].sort();
566
+ return sa.every((f, i) => f === sb[i]);
567
+ }
568
+ /** How a key was written, for a message that names both declarations. */
569
+ function keyForm(k) {
570
+ const flag = k.keyKind === 'primary' ? 'pk' : 'unique';
571
+ if (k.source === 'inline')
572
+ return `[${flag}] on ${k.fields.join(', ')}`;
573
+ if (k.source === 'indexes')
574
+ return `a ${flag} entry in indexes`;
575
+ return `a ${flag} line in constraints`;
576
+ }
577
+ /** Both declarations of a repeated key: "[pk] on id and a pk line in constraints". */
578
+ function twoForms(a, b) {
579
+ const fa = keyForm(a);
580
+ const fb = keyForm(b);
581
+ if (fa !== fb)
582
+ return `${fa} and ${fb}`;
583
+ const flag = a.keyKind === 'primary' ? 'pk' : 'unique';
584
+ return a.source === 'indexes' ? `two ${flag} entries in indexes` : `two ${flag} lines in constraints`;
585
+ }
470
586
  function entityIndex(doc) {
471
587
  const m = new Map();
472
588
  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/lexer.js CHANGED
@@ -500,6 +500,20 @@ export class Lexer {
500
500
  if (this.isIdentStart(c)) {
501
501
  return this.lexIdentifier();
502
502
  }
503
+ if (c === '#') {
504
+ // DBML writes a color unquoted, `headercolor: #3498DB` (spec §3.3):
505
+ // `#` and three or six hex digits read as the string '#3498DB'.
506
+ let n = 1;
507
+ while (/[0-9A-Fa-f]/.test(this.peek(n)))
508
+ n++;
509
+ const digits = n - 1;
510
+ if ((digits === 3 || digits === 6) && !/[A-Za-z0-9_]/.test(this.peek(n))) {
511
+ for (let i = 0; i < n; i++)
512
+ this.advance();
513
+ const text = this.text.slice(start.offset, this.offset);
514
+ return { kind: TokenKind.StringLiteral, text, value: text, start, end: this.pos() };
515
+ }
516
+ }
503
517
  throw new LexError(`Unexpected character: ${JSON.stringify(c)}`, start);
504
518
  }
505
519
  tokenize() {
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' | 'name-collision' | 'container-target-missing' | 'container-target-not-in-project' | '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,7 +42,9 @@ 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
+ import { checkConstraints, checkTargets } from "./constraints.js";
46
+ import { versionAtLeast } from "./relationships.js";
47
+ import { checkViews } from "./views.js";
46
48
  /**
47
49
  * Read-only handle on the collected symbol table.
48
50
  *
@@ -231,6 +233,9 @@ export function resolveNames(doc) {
231
233
  diagnostics.push(...checkSupertypeGroups(flat));
232
234
  // Pass 5: constraints and referenced keys (spec §10.10, §11.17).
233
235
  diagnostics.push(...checkConstraints(flat));
236
+ diagnostics.push(...checkTargets(flat));
237
+ // Pass 6: view source queries (spec §14.7).
238
+ diagnostics.push(...checkViews(flat));
234
239
  return { diagnostics, symbols };
235
240
  }
236
241
  /* -------------------------------------------------------------------------
@@ -238,9 +243,18 @@ export function resolveNames(doc) {
238
243
  * ----------------------------------------------------------------------- */
239
244
  function collectDeclarations(doc, entries, diagnostics) {
240
245
  const seen = new Set(); // qualified-name keys to detect duplicates
246
+ const first = diagnostics.length;
241
247
  for (const stmt of doc.statements) {
242
248
  addTopLevelDeclaration(stmt, entries, diagnostics, seen);
243
249
  }
250
+ // Before v0.6 the parser accepted a collision, so a document declaring an
251
+ // earlier version, or none (DBML), draws a warning and stays valid.
252
+ if (!versionAtLeast(doc, '0.6')) {
253
+ for (const d of diagnostics.slice(first)) {
254
+ if (d.code === 'name-collision')
255
+ d.severity = 'warning';
256
+ }
257
+ }
244
258
  }
245
259
  function addTopLevelDeclaration(stmt, entries, diagnostics, seen) {
246
260
  switch (stmt.kind) {
@@ -310,9 +324,30 @@ function addTopLevelDeclaration(stmt, entries, diagnostics, seen) {
310
324
  return;
311
325
  }
312
326
  }
327
+ const SHARED_NAMESPACE = new Set(['entity', 'view', 'edge', 'type', 'enum', 'tablepartial']);
328
+ const KIND_NAMES = {
329
+ entity: 'an Entity', view: 'a View', edge: 'an Edge', type: 'a Type', enum: 'an Enum', tablepartial: 'a TablePartial',
330
+ };
331
+ function article(kind) {
332
+ return KIND_NAMES[kind] ?? kind;
333
+ }
313
334
  function addEntry(name, containerName, kind, declaration, span, entries, diagnostics, seen) {
314
335
  const qualifiedName = containerName ? `${containerName}.${name}` : name;
315
336
  const key = `${kind}:${qualifiedName}`;
337
+ // Spec §15.5: Entities, Views, Edges, Types, Enums and TablePartials
338
+ // share one namespace. collectDeclarations() sets the severity.
339
+ if (SHARED_NAMESPACE.has(kind) && !seen.has(key)) {
340
+ const other = entries.find((e) => e.qualifiedName === qualifiedName && e.kind !== kind && SHARED_NAMESPACE.has(e.kind));
341
+ if (other) {
342
+ diagnostics.push({
343
+ severity: 'error',
344
+ code: 'name-collision',
345
+ message: `'${qualifiedName}' names both ${article(other.kind)} and ${article(kind)}. ` +
346
+ `Entities, Views, Edges, Types, Enums and TablePartials share one namespace (spec §15.5): rename one of them.`,
347
+ span,
348
+ });
349
+ }
350
+ }
316
351
  if (seen.has(key)) {
317
352
  diagnostics.push({
318
353
  severity: 'error',
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.2";
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.2';
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) {
@@ -1318,6 +1385,17 @@ export class Parser {
1318
1385
  }
1319
1386
  this.expect(TokenKind.RParen, "Expected ')' closing type parameters");
1320
1387
  }
1388
+ // DBML's array type of a target, `text[]` or `varchar(20)[]`, keeps its
1389
+ // brackets in the type name (spec §8.1). Only empty brackets written
1390
+ // right after the type, with no space, belong to it: `int [pk]` and
1391
+ // `int []` remain settings blocks.
1392
+ while (this.check(TokenKind.LBracket) &&
1393
+ this.peek(1).kind === TokenKind.RBracket &&
1394
+ this.peek().start.offset === this.tokens[this.idx - 1].end.offset) {
1395
+ this.advance();
1396
+ this.advance();
1397
+ name += '[]';
1398
+ }
1321
1399
  return {
1322
1400
  kind: 'ScalarType',
1323
1401
  name,
@@ -1383,10 +1461,12 @@ export class Parser {
1383
1461
  finishObjectTypeDecl(start, name, settings) {
1384
1462
  this.expect(TokenKind.LBrace, "Expected '{' after Type name");
1385
1463
  const body = [];
1386
- while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
1464
+ for (;;) {
1465
+ this.skipListSeparators();
1466
+ if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
1467
+ break;
1387
1468
  const t = this.peek();
1388
- const k = kw(t);
1389
- if (k === 'note') {
1469
+ if (this.atNoteElement()) {
1390
1470
  body.push(this.parseNoteBlockOrSetting());
1391
1471
  }
1392
1472
  else if (t.kind === TokenKind.Tilde) {
@@ -1412,7 +1492,7 @@ export class Parser {
1412
1492
  const name = this.parseIdentLikeName('edge name');
1413
1493
  const settings = this.maybeSettingsBlock();
1414
1494
  this.expect(TokenKind.LBrace, "Expected '{' after Edge settings");
1415
- const body = this.parseEntityBody();
1495
+ const body = this.parseEntityBody('Edge', name);
1416
1496
  this.expect(TokenKind.RBrace, "Expected '}' closing Edge");
1417
1497
  return {
1418
1498
  kind: 'EdgeDeclaration',
@@ -1430,16 +1510,22 @@ export class Parser {
1430
1510
  const settings = this.maybeSettingsBlock();
1431
1511
  this.expect(TokenKind.LBrace, "Expected '{' after View name");
1432
1512
  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') {
1513
+ for (;;) {
1514
+ this.skipListSeparators();
1515
+ if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
1516
+ break;
1517
+ // Spec §3.10: `Note` and `source_query` start their elements only
1518
+ // before `:` (or `{` for a note); otherwise they name a field.
1519
+ if (this.atNoteElement()) {
1437
1520
  body.push(this.parseNoteBlockOrSetting());
1438
1521
  }
1439
- else if (k === 'source_query') {
1522
+ else if (isKw(this.peek(), 'source_query') && this.peek(1).kind === TokenKind.Colon) {
1440
1523
  body.push(this.parseSourceQueryItem());
1441
1524
  }
1442
1525
  else {
1526
+ // Spec §14.2: `materialized: true` and the other settings go in
1527
+ // the brackets, never in the body.
1528
+ this.rejectSettingInBody('View', name);
1443
1529
  body.push(this.parseFieldDeclaration());
1444
1530
  }
1445
1531
  }
@@ -1480,7 +1566,10 @@ export class Parser {
1480
1566
  }
1481
1567
  this.expect(TokenKind.LBrace, "Expected '{' after enum name");
1482
1568
  const values = [];
1483
- while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
1569
+ for (;;) {
1570
+ this.skipListSeparators();
1571
+ if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
1572
+ break;
1484
1573
  const vStart = this.peek().start;
1485
1574
  const vt = this.peek();
1486
1575
  let vname;
@@ -1754,7 +1843,7 @@ export class Parser {
1754
1843
  const name = this.parseIdentLikeName('TablePartial name');
1755
1844
  const settings = this.maybeSettingsBlock();
1756
1845
  this.expect(TokenKind.LBrace, "Expected '{' after TablePartial name");
1757
- const body = this.parseEntityBody();
1846
+ const body = this.parseEntityBody('TablePartial', name);
1758
1847
  this.expect(TokenKind.RBrace, "Expected '}' closing TablePartial");
1759
1848
  return {
1760
1849
  kind: 'TablePartialDeclaration',
@@ -1869,6 +1958,14 @@ export class Parser {
1869
1958
  this.expect(TokenKind.LBrace, "Expected '{' after indexes");
1870
1959
  const entries = [];
1871
1960
  while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
1961
+ // Spec §3.9: `indexes` is not a list body. `email, name` may be meant
1962
+ // as the composite index `(email, name)`, so a separator between
1963
+ // entries stays an error rather than declaring two indexes.
1964
+ const t = this.peek();
1965
+ if (t.kind === TokenKind.Comma || t.kind === TokenKind.Semicolon) {
1966
+ throw new ParseError(`Unexpected '${t.text}' in indexes: write one index per line, and a composite ` +
1967
+ 'index in parentheses, as in (email, name) (spec §3.9).', t.start);
1968
+ }
1872
1969
  entries.push(this.parseIndexEntry());
1873
1970
  }
1874
1971
  this.expect(TokenKind.RBrace, "Expected '}' closing indexes");
@@ -1928,7 +2025,10 @@ export class Parser {
1928
2025
  this.advance(); // checks
1929
2026
  this.expect(TokenKind.LBrace, "Expected '{' after checks");
1930
2027
  const entries = [];
1931
- while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
2028
+ for (;;) {
2029
+ this.skipListSeparators();
2030
+ if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
2031
+ break;
1932
2032
  entries.push(this.parseCheckEntry());
1933
2033
  }
1934
2034
  this.expect(TokenKind.RBrace, "Expected '}' closing checks");
@@ -1967,7 +2067,10 @@ export class Parser {
1967
2067
  this.advance(); // constraints
1968
2068
  this.expect(TokenKind.LBrace, "Expected '{' after constraints");
1969
2069
  const entries = [];
1970
- while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
2070
+ for (;;) {
2071
+ this.skipListSeparators();
2072
+ if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
2073
+ break;
1971
2074
  const t = this.peek();
1972
2075
  if (t.kind === TokenKind.StringLiteral ||
1973
2076
  t.kind === TokenKind.MultilineString ||
@@ -2284,7 +2387,66 @@ export function parse(source, options = {}) {
2284
2387
  const initialStack = new Set();
2285
2388
  if (options.filePath)
2286
2389
  initialStack.add(options.filePath);
2287
- return new Parser(tokens, options, initialStack, 0).parseDocument();
2390
+ const doc = new Parser(tokens, options, initialStack, 0).parseDocument();
2391
+ applyEntityAliases(doc);
2392
+ return doc;
2393
+ }
2394
+ /**
2395
+ * Spec §7.4: `Table very_long_name as V { ... }` lets a relationship name
2396
+ * the entity by its alias, `Ref: V.id < posts.user_id`, as in DBML. This
2397
+ * pass rewrites the first segment of every relationship endpoint that is
2398
+ * an alias into the entity's name, so the resolver, the key checks and the
2399
+ * renderer see the entity itself. The entity keeps its `alias`. An alias
2400
+ * that repeats the name of an entity or a container, or that two entities
2401
+ * declare, is left alone: the name it would shadow applies.
2402
+ */
2403
+ function applyEntityAliases(doc) {
2404
+ const aliases = new Map();
2405
+ const taken = new Set();
2406
+ const visit = (decl, container) => {
2407
+ if (decl.kind !== 'EntityDeclaration')
2408
+ return;
2409
+ const segments = [...(container ? [container] : []), ...decl.name.split('.')];
2410
+ taken.add(segments[segments.length - 1]);
2411
+ if (!decl.alias)
2412
+ return;
2413
+ aliases.set(decl.alias, aliases.has(decl.alias) ? null : segments);
2414
+ };
2415
+ for (const stmt of doc.statements) {
2416
+ if (stmt.kind === 'ContainerDeclaration') {
2417
+ taken.add(stmt.name);
2418
+ for (const body of stmt.body)
2419
+ visit(body, stmt.name);
2420
+ }
2421
+ else {
2422
+ visit(stmt);
2423
+ }
2424
+ }
2425
+ for (const name of taken)
2426
+ aliases.delete(name);
2427
+ if (![...aliases.values()].some((v) => v !== null))
2428
+ return;
2429
+ const walk = (node) => {
2430
+ if (Array.isArray(node)) {
2431
+ node.forEach(walk);
2432
+ return;
2433
+ }
2434
+ if (!node || typeof node !== 'object')
2435
+ return;
2436
+ const obj = node;
2437
+ if (obj.kind === 'RefEndpoint' && Array.isArray(obj.path)) {
2438
+ const first = obj.path[0];
2439
+ const segments = first?.kind === 'PathField' && first.name ? aliases.get(first.name) : undefined;
2440
+ if (segments) {
2441
+ obj.path = [...segments.map((name) => ({ ...first, name })), ...obj.path.slice(1)];
2442
+ }
2443
+ }
2444
+ for (const [key, value] of Object.entries(obj)) {
2445
+ if (key !== 'span' && value && typeof value === 'object')
2446
+ walk(value);
2447
+ }
2448
+ };
2449
+ walk(doc.statements);
2288
2450
  }
2289
2451
  /**
2290
2452
  * Internal `ParseFn` used by the module resolver to recursively parse a
@@ -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.2",
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",