@xdbml/parse 0.6.1 → 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
  * ----------------------------------------------------------------------- */
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() {
@@ -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' | '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';
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,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";
45
+ import { checkConstraints, checkTargets } from "./constraints.js";
46
+ import { versionAtLeast } from "./relationships.js";
46
47
  import { checkViews } from "./views.js";
47
48
  /**
48
49
  * Read-only handle on the collected symbol table.
@@ -232,6 +233,7 @@ export function resolveNames(doc) {
232
233
  diagnostics.push(...checkSupertypeGroups(flat));
233
234
  // Pass 5: constraints and referenced keys (spec §10.10, §11.17).
234
235
  diagnostics.push(...checkConstraints(flat));
236
+ diagnostics.push(...checkTargets(flat));
235
237
  // Pass 6: view source queries (spec §14.7).
236
238
  diagnostics.push(...checkViews(flat));
237
239
  return { diagnostics, symbols };
@@ -241,9 +243,18 @@ export function resolveNames(doc) {
241
243
  * ----------------------------------------------------------------------- */
242
244
  function collectDeclarations(doc, entries, diagnostics) {
243
245
  const seen = new Set(); // qualified-name keys to detect duplicates
246
+ const first = diagnostics.length;
244
247
  for (const stmt of doc.statements) {
245
248
  addTopLevelDeclaration(stmt, entries, diagnostics, seen);
246
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
+ }
247
258
  }
248
259
  function addTopLevelDeclaration(stmt, entries, diagnostics, seen) {
249
260
  switch (stmt.kind) {
@@ -313,9 +324,30 @@ function addTopLevelDeclaration(stmt, entries, diagnostics, seen) {
313
324
  return;
314
325
  }
315
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
+ }
316
334
  function addEntry(name, containerName, kind, declaration, span, entries, diagnostics, seen) {
317
335
  const qualifiedName = containerName ? `${containerName}.${name}` : name;
318
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
+ }
319
351
  if (seen.has(key)) {
320
352
  diagnostics.push({
321
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.1";
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 {
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.1';
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);
@@ -1385,6 +1385,17 @@ export class Parser {
1385
1385
  }
1386
1386
  this.expect(TokenKind.RParen, "Expected ')' closing type parameters");
1387
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
+ }
1388
1399
  return {
1389
1400
  kind: 'ScalarType',
1390
1401
  name,
@@ -2376,7 +2387,66 @@ export function parse(source, options = {}) {
2376
2387
  const initialStack = new Set();
2377
2388
  if (options.filePath)
2378
2389
  initialStack.add(options.filePath);
2379
- 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);
2380
2450
  }
2381
2451
  /**
2382
2452
  * Internal `ParseFn` used by the module resolver to recursively parse a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xdbml/parse",
3
- "version": "0.6.1",
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",