@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.
- package/dist/constraints.d.ts +9 -0
- package/dist/constraints.js +69 -0
- package/dist/lexer.js +14 -0
- package/dist/name-resolver.d.ts +1 -1
- package/dist/name-resolver.js +33 -1
- package/dist/parser.d.ts +1 -1
- package/dist/parser.js +72 -2
- package/package.json +1 -1
package/dist/constraints.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/constraints.js
CHANGED
|
@@ -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() {
|
package/dist/name-resolver.d.ts
CHANGED
|
@@ -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.,
|
package/dist/name-resolver.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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",
|