@xdbml/parse 0.6.1 → 0.6.3
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/README.md +9 -0
- package/dist/ast.d.ts +31 -1
- package/dist/constraints.d.ts +9 -0
- package/dist/constraints.js +69 -0
- package/dist/diagram-views.d.ts +48 -0
- package/dist/diagram-views.js +411 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/keywords.d.ts +1 -0
- package/dist/keywords.js +16 -0
- package/dist/lexer.js +14 -0
- package/dist/module-resolver.js +21 -3
- package/dist/monarch.js +4 -1
- package/dist/name-resolver.d.ts +1 -1
- package/dist/name-resolver.js +36 -1
- package/dist/parser.d.ts +16 -1
- package/dist/parser.js +210 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,6 +48,15 @@ try {
|
|
|
48
48
|
|
|
49
49
|
The package also exports `tokenize`, `LexError`, `Parser`, `SymbolTable`, and the AST types.
|
|
50
50
|
|
|
51
|
+
For diagram views (spec §18), `diagramViews(doc)` lists the `DiagramView` declarations, and `diagramViewMembers(doc, name)` returns the entities, database views and sticky notes a diagram view contains, with the Containers, TableGroups and supertype groups a renderer frames or draws around them.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { parse, diagramViewMembers } from '@xdbml/parse';
|
|
55
|
+
|
|
56
|
+
const members = diagramViewMembers(parse(source), 'order_flow');
|
|
57
|
+
console.log(members?.entities); // ['sales.orders', 'billing.invoices', ...]
|
|
58
|
+
```
|
|
59
|
+
|
|
51
60
|
## Learn more
|
|
52
61
|
|
|
53
62
|
- [xdbml.org](https://xdbml.org), and the language [in 5 minutes](https://xdbml.org/learn/)
|
package/dist/ast.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface ExperimentalDeclaration {
|
|
|
38
38
|
features: string[];
|
|
39
39
|
span: Span;
|
|
40
40
|
}
|
|
41
|
-
export type TopLevelStatement = ProjectDeclaration | ContainerDeclaration | EntityDeclaration | TypeDeclaration | EdgeDeclaration | ViewDeclaration | EnumDeclaration | RefDeclaration | TablePartialDeclaration | TableGroupDeclaration | SupertypeGroupDeclaration | NoteDeclaration | TopLevelRecordsDeclaration | ModuleImportDirective;
|
|
41
|
+
export type TopLevelStatement = ProjectDeclaration | ContainerDeclaration | EntityDeclaration | TypeDeclaration | EdgeDeclaration | ViewDeclaration | EnumDeclaration | RefDeclaration | TablePartialDeclaration | TableGroupDeclaration | SupertypeGroupDeclaration | DiagramViewDeclaration | NoteDeclaration | TopLevelRecordsDeclaration | ModuleImportDirective;
|
|
42
42
|
export interface ProjectDeclaration {
|
|
43
43
|
kind: 'ProjectDeclaration';
|
|
44
44
|
name: string;
|
|
@@ -385,6 +385,36 @@ export interface SupertypeGroupMember {
|
|
|
385
385
|
settings: Setting[];
|
|
386
386
|
span: Span;
|
|
387
387
|
}
|
|
388
|
+
export type DiagramViewCategoryName = 'Tables' | 'Views' | 'Containers' | 'TableGroups' | 'SupertypeGroups' | 'Notes';
|
|
389
|
+
export interface DiagramViewDeclaration {
|
|
390
|
+
kind: 'DiagramViewDeclaration';
|
|
391
|
+
name: string;
|
|
392
|
+
/** Bracket settings: `note` and custom `x_` properties (spec §18.1). */
|
|
393
|
+
settings: Setting[];
|
|
394
|
+
/** `Note:` / `Note { }` elements of the body. `diagramViewNote()` gives the note. */
|
|
395
|
+
notes: NoteBlock[];
|
|
396
|
+
/** True when the whole body is `{ * }`: every element of every category. */
|
|
397
|
+
wildcardBody: boolean;
|
|
398
|
+
categories: DiagramViewCategory[];
|
|
399
|
+
span: Span;
|
|
400
|
+
}
|
|
401
|
+
export interface DiagramViewCategory {
|
|
402
|
+
kind: 'DiagramViewCategory';
|
|
403
|
+
/** Canonical category: `Schemas` reads as `Containers`. */
|
|
404
|
+
category: DiagramViewCategoryName;
|
|
405
|
+
/** The keyword as written, for round trips and the DBML-form check of §18.5. */
|
|
406
|
+
keyword: string;
|
|
407
|
+
/** True when the list holds `*`; names written beside it have no effect. */
|
|
408
|
+
wildcard: boolean;
|
|
409
|
+
items: DiagramViewItem[];
|
|
410
|
+
span: Span;
|
|
411
|
+
}
|
|
412
|
+
export interface DiagramViewItem {
|
|
413
|
+
kind: 'DiagramViewItem';
|
|
414
|
+
/** Name as written: `orders` or `sales.orders`. */
|
|
415
|
+
name: string;
|
|
416
|
+
span: Span;
|
|
417
|
+
}
|
|
388
418
|
export interface PartialInjection {
|
|
389
419
|
kind: 'PartialInjection';
|
|
390
420
|
/** Identifier after the `~`. */
|
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
|
* ----------------------------------------------------------------------- */
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagram views (spec §18, defined in v0.6.3).
|
|
3
|
+
*
|
|
4
|
+
* A diagram view is a named subset of the model's diagram. Its body lists
|
|
5
|
+
* categories of elements; `diagramViewMembers()` turns those lists into the
|
|
6
|
+
* members of §18.2, and `checkDiagramViews()` reports the conditions of
|
|
7
|
+
* §18.6. Both take a document and flatten it, so elements imported from
|
|
8
|
+
* other files count like the ones declared in place (§18.3).
|
|
9
|
+
*
|
|
10
|
+
* Element ids follow the renderer and the supertype group resolver: an
|
|
11
|
+
* entity or database view inside a Container is `container.name`, one at
|
|
12
|
+
* the top level is its name as declared, which for the DBML form
|
|
13
|
+
* `Table core.users` is `core.users` in the implicit Container `core`.
|
|
14
|
+
*/
|
|
15
|
+
import type { DiagramViewDeclaration, XDbmlDocument } from './ast.ts';
|
|
16
|
+
import type { Diagnostic } from './name-resolver.ts';
|
|
17
|
+
/** The members of a diagram view (spec §18.2) and what a renderer draws with them (§18.4). */
|
|
18
|
+
export interface DiagramViewMembers {
|
|
19
|
+
/** The diagram view these members belong to. */
|
|
20
|
+
declaration: DiagramViewDeclaration;
|
|
21
|
+
/** Ids of the member entities, in declaration order. */
|
|
22
|
+
entities: string[];
|
|
23
|
+
/** Ids of the member database views, in declaration order. */
|
|
24
|
+
views: string[];
|
|
25
|
+
/** Names of the member sticky notes, in declaration order. */
|
|
26
|
+
notes: string[];
|
|
27
|
+
/** Containers holding at least one member: their frames surround those members only. */
|
|
28
|
+
containers: string[];
|
|
29
|
+
/** TableGroups with at least one member entity, named in the diagram view or not. */
|
|
30
|
+
tableGroups: string[];
|
|
31
|
+
/** Supertype groups whose supertype and at least one subtype are members (§12.9). */
|
|
32
|
+
supertypeGroups: string[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The note of a diagram view (spec §18.1): its first `Note` in the body,
|
|
36
|
+
* otherwise the `note` setting in its brackets, otherwise undefined.
|
|
37
|
+
*/
|
|
38
|
+
export declare function diagramViewNote(view: DiagramViewDeclaration): string | undefined;
|
|
39
|
+
/** Every DiagramView of a document, in declaration order, after module resolution. */
|
|
40
|
+
export declare function diagramViews(doc: XDbmlDocument): DiagramViewDeclaration[];
|
|
41
|
+
/**
|
|
42
|
+
* The members of the diagram view named `name`, or undefined when the
|
|
43
|
+
* document declares none of that name. With two of one name (an error),
|
|
44
|
+
* the first one counts.
|
|
45
|
+
*/
|
|
46
|
+
export declare function diagramViewMembers(doc: XDbmlDocument, name: string): DiagramViewMembers | undefined;
|
|
47
|
+
/** The conditions of spec §18.6 that a parse leaves to name resolution. */
|
|
48
|
+
export declare function checkDiagramViews(doc: XDbmlDocument): Diagnostic[];
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagram views (spec §18, defined in v0.6.3).
|
|
3
|
+
*
|
|
4
|
+
* A diagram view is a named subset of the model's diagram. Its body lists
|
|
5
|
+
* categories of elements; `diagramViewMembers()` turns those lists into the
|
|
6
|
+
* members of §18.2, and `checkDiagramViews()` reports the conditions of
|
|
7
|
+
* §18.6. Both take a document and flatten it, so elements imported from
|
|
8
|
+
* other files count like the ones declared in place (§18.3).
|
|
9
|
+
*
|
|
10
|
+
* Element ids follow the renderer and the supertype group resolver: an
|
|
11
|
+
* entity or database view inside a Container is `container.name`, one at
|
|
12
|
+
* the top level is its name as declared, which for the DBML form
|
|
13
|
+
* `Table core.users` is `core.users` in the implicit Container `core`.
|
|
14
|
+
*/
|
|
15
|
+
import { flatten } from "./module-resolver.js";
|
|
16
|
+
import { resolveSupertypeGroups } from "./supertypes.js";
|
|
17
|
+
/**
|
|
18
|
+
* The note of a diagram view (spec §18.1): its first `Note` in the body,
|
|
19
|
+
* otherwise the `note` setting in its brackets, otherwise undefined.
|
|
20
|
+
*/
|
|
21
|
+
export function diagramViewNote(view) {
|
|
22
|
+
if (view.notes.length > 0)
|
|
23
|
+
return view.notes[0].body;
|
|
24
|
+
const s = view.settings.find((x) => x.name === 'note');
|
|
25
|
+
const v = s?.value;
|
|
26
|
+
return v && v.kind === 'StringValue' ? v.value : undefined;
|
|
27
|
+
}
|
|
28
|
+
/** Every DiagramView of a document, in declaration order, after module resolution. */
|
|
29
|
+
export function diagramViews(doc) {
|
|
30
|
+
return flatten(doc).statements.filter((s) => s.kind === 'DiagramViewDeclaration');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The members of the diagram view named `name`, or undefined when the
|
|
34
|
+
* document declares none of that name. With two of one name (an error),
|
|
35
|
+
* the first one counts.
|
|
36
|
+
*/
|
|
37
|
+
export function diagramViewMembers(doc, name) {
|
|
38
|
+
const flat = flatten(doc);
|
|
39
|
+
const decl = flat.statements.find((s) => s.kind === 'DiagramViewDeclaration' && s.name === name);
|
|
40
|
+
if (!decl)
|
|
41
|
+
return undefined;
|
|
42
|
+
return computeMembers(decl, buildIndex(flat), flat);
|
|
43
|
+
}
|
|
44
|
+
const key = (kind, id) => `${kind}:${id}`;
|
|
45
|
+
function buildIndex(doc) {
|
|
46
|
+
const elements = [];
|
|
47
|
+
const byKindId = new Map();
|
|
48
|
+
const entitiesOf = new Map();
|
|
49
|
+
const viewsOf = new Map();
|
|
50
|
+
const tableGroupMembers = new Map();
|
|
51
|
+
const aliasCandidates = new Map();
|
|
52
|
+
const takenNames = new Set();
|
|
53
|
+
const add = (kind, id, name, container, topLevel) => {
|
|
54
|
+
if (byKindId.has(key(kind, id)))
|
|
55
|
+
return;
|
|
56
|
+
const el = { kind, id, name, container, topLevel };
|
|
57
|
+
elements.push(el);
|
|
58
|
+
byKindId.set(key(kind, id), el);
|
|
59
|
+
if (container && (kind === 'entity' || kind === 'view')) {
|
|
60
|
+
const map = kind === 'entity' ? entitiesOf : viewsOf;
|
|
61
|
+
const list = map.get(container) ?? [];
|
|
62
|
+
list.push(id);
|
|
63
|
+
map.set(container, list);
|
|
64
|
+
if (!byKindId.has(key('container', container))) {
|
|
65
|
+
add('container', container, container, undefined, true);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const addBoxed = (kind, decl, container) => {
|
|
70
|
+
let id;
|
|
71
|
+
let bare;
|
|
72
|
+
let owner = container;
|
|
73
|
+
if (container) {
|
|
74
|
+
id = `${container}.${decl.name}`;
|
|
75
|
+
bare = decl.name;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
id = decl.name;
|
|
79
|
+
const dot = decl.name.lastIndexOf('.');
|
|
80
|
+
bare = dot > 0 ? decl.name.slice(dot + 1) : decl.name;
|
|
81
|
+
owner = dot > 0 ? decl.name.slice(0, dot) : undefined;
|
|
82
|
+
}
|
|
83
|
+
add(kind, id, bare, owner, owner === undefined);
|
|
84
|
+
takenNames.add(bare);
|
|
85
|
+
if (kind === 'entity') {
|
|
86
|
+
const alias = decl.alias;
|
|
87
|
+
if (alias)
|
|
88
|
+
aliasCandidates.set(alias, aliasCandidates.has(alias) ? null : id);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
for (const stmt of doc.statements) {
|
|
92
|
+
switch (stmt.kind) {
|
|
93
|
+
case 'ContainerDeclaration': {
|
|
94
|
+
const c = stmt;
|
|
95
|
+
add('container', c.name, c.name, undefined, true);
|
|
96
|
+
takenNames.add(c.name);
|
|
97
|
+
for (const item of c.body) {
|
|
98
|
+
if (item.kind === 'EntityDeclaration')
|
|
99
|
+
addBoxed('entity', item, c.name);
|
|
100
|
+
else if (item.kind === 'ViewDeclaration')
|
|
101
|
+
addBoxed('view', item, c.name);
|
|
102
|
+
else if (item.kind === 'EdgeDeclaration')
|
|
103
|
+
add('edge', `${c.name}.${item.name}`, item.name, c.name, false);
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case 'EntityDeclaration':
|
|
108
|
+
addBoxed('entity', stmt);
|
|
109
|
+
break;
|
|
110
|
+
case 'ViewDeclaration':
|
|
111
|
+
addBoxed('view', stmt);
|
|
112
|
+
break;
|
|
113
|
+
case 'EdgeDeclaration':
|
|
114
|
+
add('edge', stmt.name, stmt.name, undefined, true);
|
|
115
|
+
break;
|
|
116
|
+
case 'TableGroupDeclaration':
|
|
117
|
+
add('tablegroup', stmt.name, stmt.name, undefined, true);
|
|
118
|
+
if (!tableGroupMembers.has(stmt.name))
|
|
119
|
+
tableGroupMembers.set(stmt.name, stmt.members);
|
|
120
|
+
break;
|
|
121
|
+
case 'SupertypeGroupDeclaration':
|
|
122
|
+
add('supertypegroup', stmt.name, stmt.name, undefined, true);
|
|
123
|
+
break;
|
|
124
|
+
case 'NoteDeclaration':
|
|
125
|
+
if (stmt.name)
|
|
126
|
+
add('note', stmt.name, stmt.name, undefined, true);
|
|
127
|
+
break;
|
|
128
|
+
default: break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// An alias that repeats the name of an entity or a Container, or that two
|
|
132
|
+
// entities declare, names nothing (§7.4).
|
|
133
|
+
const aliases = new Map();
|
|
134
|
+
for (const [alias, id] of aliasCandidates) {
|
|
135
|
+
if (id !== null && !takenNames.has(alias))
|
|
136
|
+
aliases.set(alias, id);
|
|
137
|
+
}
|
|
138
|
+
return { elements, byKindId, entitiesOf, viewsOf, aliases, tableGroupMembers };
|
|
139
|
+
}
|
|
140
|
+
/* --------------------------------------------------------- resolution */
|
|
141
|
+
const CATEGORY_KIND = {
|
|
142
|
+
Tables: 'entity',
|
|
143
|
+
Views: 'view',
|
|
144
|
+
Containers: 'container',
|
|
145
|
+
TableGroups: 'tablegroup',
|
|
146
|
+
SupertypeGroups: 'supertypegroup',
|
|
147
|
+
Notes: 'note',
|
|
148
|
+
};
|
|
149
|
+
const KIND_CATEGORY = {
|
|
150
|
+
entity: 'Tables',
|
|
151
|
+
view: 'Views',
|
|
152
|
+
container: 'Containers',
|
|
153
|
+
tablegroup: 'TableGroups',
|
|
154
|
+
supertypegroup: 'SupertypeGroups',
|
|
155
|
+
note: 'Notes',
|
|
156
|
+
};
|
|
157
|
+
const KIND_LABEL = {
|
|
158
|
+
entity: 'an entity',
|
|
159
|
+
view: 'a database view',
|
|
160
|
+
container: 'a Container',
|
|
161
|
+
tablegroup: 'a TableGroup',
|
|
162
|
+
supertypegroup: 'a supertype group',
|
|
163
|
+
note: 'a sticky note',
|
|
164
|
+
edge: 'an Edge',
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Resolve a name of one kind (spec §18.3). A qualified name names the
|
|
168
|
+
* element with that id. An unqualified name names the element declared
|
|
169
|
+
* outside any Container under that name, if any, and otherwise the one
|
|
170
|
+
* element of that name inside a Container. Under Tables, an alias names
|
|
171
|
+
* its entity.
|
|
172
|
+
*/
|
|
173
|
+
function resolveName(name, kind, index) {
|
|
174
|
+
const direct = index.byKindId.get(key(kind, name));
|
|
175
|
+
if (direct)
|
|
176
|
+
return { ok: true, id: direct.id };
|
|
177
|
+
if (!name.includes('.') && (kind === 'entity' || kind === 'view')) {
|
|
178
|
+
const top = index.elements.find((e) => e.kind === kind && e.topLevel && e.name === name);
|
|
179
|
+
if (top)
|
|
180
|
+
return { ok: true, id: top.id };
|
|
181
|
+
const inside = index.elements.filter((e) => e.kind === kind && e.name === name);
|
|
182
|
+
if (inside.length === 1)
|
|
183
|
+
return { ok: true, id: inside[0].id };
|
|
184
|
+
if (inside.length > 1)
|
|
185
|
+
return { ok: false, reason: 'ambiguous', candidates: inside.map((e) => e.id) };
|
|
186
|
+
}
|
|
187
|
+
if (kind === 'entity') {
|
|
188
|
+
const aliased = index.aliases.get(name);
|
|
189
|
+
if (aliased)
|
|
190
|
+
return { ok: true, id: aliased };
|
|
191
|
+
}
|
|
192
|
+
for (const other of ['entity', 'view', 'container', 'tablegroup', 'supertypegroup', 'note', 'edge']) {
|
|
193
|
+
if (other === kind)
|
|
194
|
+
continue;
|
|
195
|
+
if (resolveSimple(name, other, index))
|
|
196
|
+
return { ok: false, reason: 'other-kind', kind: other };
|
|
197
|
+
}
|
|
198
|
+
return { ok: false, reason: 'unresolved' };
|
|
199
|
+
}
|
|
200
|
+
function resolveSimple(name, kind, index) {
|
|
201
|
+
if (index.byKindId.has(key(kind, name)))
|
|
202
|
+
return true;
|
|
203
|
+
if (!name.includes('.'))
|
|
204
|
+
return index.elements.some((e) => e.kind === kind && e.name === name);
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
/** The categories of a diagram view, with a body-level `*` read as `*` in every category (§28.8). */
|
|
208
|
+
function effectiveCategories(decl) {
|
|
209
|
+
const out = new Map();
|
|
210
|
+
if (decl.wildcardBody) {
|
|
211
|
+
for (const name of Object.keys(CATEGORY_KIND)) {
|
|
212
|
+
out.set(name, { wildcard: true, items: [] });
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
// A category written twice is reported by checkDiagramViews(); its lists combine.
|
|
217
|
+
for (const c of decl.categories) {
|
|
218
|
+
const prev = out.get(c.category);
|
|
219
|
+
out.set(c.category, {
|
|
220
|
+
wildcard: (prev?.wildcard ?? false) || c.wildcard,
|
|
221
|
+
items: [...(prev?.items ?? []), ...c.items],
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
function computeMembers(decl, index, doc) {
|
|
227
|
+
const cats = effectiveCategories(decl);
|
|
228
|
+
const all = (kind) => index.elements.filter((e) => e.kind === kind).map((e) => e.id);
|
|
229
|
+
const named = (category) => {
|
|
230
|
+
const c = cats.get(category);
|
|
231
|
+
if (!c)
|
|
232
|
+
return new Set();
|
|
233
|
+
if (c.wildcard)
|
|
234
|
+
return new Set(all(CATEGORY_KIND[category]));
|
|
235
|
+
const ids = new Set();
|
|
236
|
+
for (const item of c.items) {
|
|
237
|
+
const r = resolveName(item.name, CATEGORY_KIND[category], index);
|
|
238
|
+
if (r.ok)
|
|
239
|
+
ids.add(r.id);
|
|
240
|
+
}
|
|
241
|
+
return ids;
|
|
242
|
+
};
|
|
243
|
+
const entities = named('Tables');
|
|
244
|
+
const views = named('Views');
|
|
245
|
+
// Rule 2: a listed Container contributes its entities and its database
|
|
246
|
+
// views, each list narrowed separately to the names under Tables or Views
|
|
247
|
+
// when they name at least one of the Container's own.
|
|
248
|
+
for (const container of named('Containers')) {
|
|
249
|
+
const ownEntities = index.entitiesOf.get(container) ?? [];
|
|
250
|
+
if (!ownEntities.some((id) => entities.has(id)))
|
|
251
|
+
ownEntities.forEach((id) => entities.add(id));
|
|
252
|
+
const ownViews = index.viewsOf.get(container) ?? [];
|
|
253
|
+
if (!ownViews.some((id) => views.has(id)))
|
|
254
|
+
ownViews.forEach((id) => views.add(id));
|
|
255
|
+
}
|
|
256
|
+
// Rule 3: each named TableGroup contributes all of its members.
|
|
257
|
+
for (const group of named('TableGroups')) {
|
|
258
|
+
for (const member of index.tableGroupMembers.get(group) ?? []) {
|
|
259
|
+
const r = resolveName(member, 'entity', index);
|
|
260
|
+
if (r.ok)
|
|
261
|
+
entities.add(r.id);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Rule 4: each named supertype group contributes its supertype and subtypes.
|
|
265
|
+
const groups = resolveSupertypeGroups(doc);
|
|
266
|
+
const namedGroups = named('SupertypeGroups');
|
|
267
|
+
for (const g of groups) {
|
|
268
|
+
if (!namedGroups.has(g.declaration.name))
|
|
269
|
+
continue;
|
|
270
|
+
if (g.supertype)
|
|
271
|
+
entities.add(g.supertype);
|
|
272
|
+
for (const s of g.subtypes)
|
|
273
|
+
if (s.entity)
|
|
274
|
+
entities.add(s.entity);
|
|
275
|
+
}
|
|
276
|
+
const notes = named('Notes');
|
|
277
|
+
const ordered = (kind, set) => all(kind).filter((id) => set.has(id));
|
|
278
|
+
const memberEntities = ordered('entity', entities);
|
|
279
|
+
const memberViews = ordered('view', views);
|
|
280
|
+
const boxed = new Set([...memberEntities, ...memberViews]);
|
|
281
|
+
const containers = all('container').filter((c) => [...(index.entitiesOf.get(c) ?? []), ...(index.viewsOf.get(c) ?? [])].some((id) => boxed.has(id)));
|
|
282
|
+
const tableGroups = all('tablegroup').filter((g) => (index.tableGroupMembers.get(g) ?? []).some((m) => {
|
|
283
|
+
const r = resolveName(m, 'entity', index);
|
|
284
|
+
return r.ok && entities.has(r.id);
|
|
285
|
+
}));
|
|
286
|
+
const supertypeGroups = groups
|
|
287
|
+
.filter((g) => g.supertype && entities.has(g.supertype) && g.subtypes.some((s) => s.entity && entities.has(s.entity)))
|
|
288
|
+
.map((g) => g.declaration.name);
|
|
289
|
+
return {
|
|
290
|
+
declaration: decl,
|
|
291
|
+
entities: memberEntities,
|
|
292
|
+
views: memberViews,
|
|
293
|
+
notes: ordered('note', notes),
|
|
294
|
+
containers,
|
|
295
|
+
tableGroups,
|
|
296
|
+
supertypeGroups: [...new Set(supertypeGroups)],
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/* --------------------------------------------------------- diagnostics */
|
|
300
|
+
/** The conditions of spec §18.6 that a parse leaves to name resolution. */
|
|
301
|
+
export function checkDiagramViews(doc) {
|
|
302
|
+
const views = doc.statements.filter((s) => s.kind === 'DiagramViewDeclaration');
|
|
303
|
+
if (views.length === 0)
|
|
304
|
+
return [];
|
|
305
|
+
const index = buildIndex(doc);
|
|
306
|
+
const diagnostics = [];
|
|
307
|
+
const declared = !!doc.version;
|
|
308
|
+
const seen = new Set();
|
|
309
|
+
for (const view of views) {
|
|
310
|
+
if (seen.has(view.name)) {
|
|
311
|
+
diagnostics.push({
|
|
312
|
+
severity: 'error',
|
|
313
|
+
code: 'duplicate-diagram-view',
|
|
314
|
+
message: `A diagram view named '${view.name}' is already declared. Diagram view names are unique (spec §18.1).`,
|
|
315
|
+
span: view.span,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
seen.add(view.name);
|
|
319
|
+
// §18.1: the brackets take `note` and custom properties only.
|
|
320
|
+
for (const setting of view.settings) {
|
|
321
|
+
if (setting.name === 'note' || setting.name.startsWith('x_'))
|
|
322
|
+
continue;
|
|
323
|
+
diagnostics.push({
|
|
324
|
+
severity: 'error',
|
|
325
|
+
code: 'unknown-diagram-view-setting',
|
|
326
|
+
message: `Diagram view '${view.name}' writes the setting '${setting.nameSource}'. A diagram view takes ` +
|
|
327
|
+
'a note and custom x_ properties in its brackets, and no other setting (spec §18.1).',
|
|
328
|
+
span: setting.span,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
// §18.5: a note and the brackets are xDBML extensions.
|
|
332
|
+
if (!declared && (view.settings.length > 0 || view.notes.length > 0)) {
|
|
333
|
+
diagnostics.push({
|
|
334
|
+
severity: 'error',
|
|
335
|
+
code: 'construct-requires-version',
|
|
336
|
+
message: `Diagram view '${view.name}' carries a note or settings, an xDBML extension that needs a ` +
|
|
337
|
+
'version declaration such as xdbml: 0.6 at the top of the document (spec §18.5).',
|
|
338
|
+
span: (view.settings[0] ?? view.notes[0]).span,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
const written = new Map();
|
|
342
|
+
for (const c of view.categories) {
|
|
343
|
+
const first = written.get(c.category);
|
|
344
|
+
if (first) {
|
|
345
|
+
const same = first.keyword.toLowerCase() === c.keyword.toLowerCase();
|
|
346
|
+
diagnostics.push({
|
|
347
|
+
severity: declared ? 'error' : 'warning',
|
|
348
|
+
code: 'duplicate-diagram-view-category',
|
|
349
|
+
message: same
|
|
350
|
+
? `Diagram view '${view.name}' writes ${c.keyword} twice. Write each category once (spec §18.1).`
|
|
351
|
+
: `Diagram view '${view.name}' writes both ${first.keyword} and ${c.keyword}, which are one ` +
|
|
352
|
+
'category: Schemas is the DBML name of Containers (spec §18.1).',
|
|
353
|
+
span: c.span,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
written.set(c.category, c);
|
|
358
|
+
}
|
|
359
|
+
// §18.5: Containers, Views and SupertypeGroups are xDBML extensions.
|
|
360
|
+
if (!declared && c.keyword.toLowerCase() !== 'schemas'
|
|
361
|
+
&& (c.category === 'Containers' || c.category === 'Views' || c.category === 'SupertypeGroups')) {
|
|
362
|
+
diagnostics.push({
|
|
363
|
+
severity: 'error',
|
|
364
|
+
code: 'construct-requires-version',
|
|
365
|
+
message: `The ${c.keyword} category of a diagram view is an xDBML extension and needs a version ` +
|
|
366
|
+
'declaration such as xdbml: 0.6 at the top of the document' +
|
|
367
|
+
(c.category === 'Containers' ? ', or the DBML name Schemas' : '') + ' (spec §18.5).',
|
|
368
|
+
span: c.span,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
if (c.wildcard)
|
|
372
|
+
continue;
|
|
373
|
+
for (const item of c.items) {
|
|
374
|
+
const r = resolveName(item.name, CATEGORY_KIND[c.category], index);
|
|
375
|
+
if (r.ok)
|
|
376
|
+
continue;
|
|
377
|
+
if (r.reason === 'ambiguous') {
|
|
378
|
+
diagnostics.push({
|
|
379
|
+
severity: 'error',
|
|
380
|
+
code: 'ambiguous-diagram-view-name',
|
|
381
|
+
message: `'${item.name}' under ${c.keyword} matches ${r.candidates.map((x) => `'${x}'`).join(' and ')}, ` +
|
|
382
|
+
'and none is declared outside a Container: write the qualified name (spec §18.3).',
|
|
383
|
+
span: item.span,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
else if (r.reason === 'other-kind') {
|
|
387
|
+
const right = KIND_CATEGORY[r.kind];
|
|
388
|
+
diagnostics.push({
|
|
389
|
+
severity: 'error',
|
|
390
|
+
code: 'diagram-view-wrong-category',
|
|
391
|
+
message: `'${item.name}' under ${c.keyword} is ${KIND_LABEL[r.kind]}` +
|
|
392
|
+
(right
|
|
393
|
+
? `: list it under ${right} (spec §18.3).`
|
|
394
|
+
: ', which appears in a diagram view when both of its ends do and is never listed (spec §18.1, §18.4).'),
|
|
395
|
+
span: item.span,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
diagnostics.push({
|
|
400
|
+
severity: 'error',
|
|
401
|
+
code: 'unresolved-diagram-view-name',
|
|
402
|
+
message: `'${item.name}' under ${c.keyword} matches no ${KIND_LABEL[CATEGORY_KIND[c.category]].replace(/^an? /, '')} ` +
|
|
403
|
+
'in the project (spec §18.3).',
|
|
404
|
+
span: item.span,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return diagnostics;
|
|
411
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export { classifyModuleSource, isUrlKey, ModuleSourceError, } from './module-res
|
|
|
33
33
|
export type { ModuleSource } from './module-resolver.ts';
|
|
34
34
|
export { resolveNames, SymbolTable } from './name-resolver.ts';
|
|
35
35
|
export { checkViews, viewSourceQuery } from './views.ts';
|
|
36
|
+
export { checkDiagramViews, diagramViewMembers, diagramViewNote, diagramViews } from './diagram-views.ts';
|
|
37
|
+
export type { DiagramViewMembers } from './diagram-views.ts';
|
|
36
38
|
export { bodyConstraints, checkConstraints, entityConstraints, isRelationalTarget, keyPathString, markPrimaryKeyNotNull, primaryKey, } from './constraints.ts';
|
|
37
39
|
export type { CheckConstraint, Constraint, ConstraintSource, KeyConstraint } from './constraints.ts';
|
|
38
40
|
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
|
@@ -31,6 +31,7 @@ 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
33
|
export { checkViews, viewSourceQuery } from "./views.js";
|
|
34
|
+
export { checkDiagramViews, diagramViewMembers, diagramViewNote, diagramViews } from "./diagram-views.js";
|
|
34
35
|
export { bodyConstraints, checkConstraints, entityConstraints, isRelationalTarget, keyPathString, markPrimaryKeyNotNull, primaryKey, } from "./constraints.js";
|
|
35
36
|
export { FOREIGN_MASTER_FLAG, checkRelationships, hasForeignMasterFlag, isForeignMaster, pathToString, refChildEndpoint, refParentEndpoint, relationshipType, versionAtLeast, CONSTRAINT_TYPES, V04_RELATIONSHIP_SETTINGS, constraintType, entityNames, isEntityLevelEndpoint, isUndirected, } from "./relationships.js";
|
|
36
37
|
export { SUPERTYPE_GROUP_VALUES, canonicalSupertypeGroupValue, checkSupertypeGroups, resolveSupertypeGroups, subtypeStrategy, supertypeChains, supertypeGroupSettings, } from "./supertypes.js";
|
package/dist/keywords.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ export declare const POLYMORPHISM_KEYWORDS: readonly ["union", "oneof", "anyof",
|
|
|
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
41
|
export declare const BLOCK_KEYWORDS: readonly ["indexes", "checks", "constraints", "records"];
|
|
42
|
+
export declare const DIAGRAM_VIEW_CATEGORIES: readonly ["tables", "views", "containers", "schemas", "tablegroups", "supertypegroups", "notes"];
|
|
42
43
|
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"];
|
|
43
44
|
export declare const SETTING_FLAGS: readonly ["pk", "primary", "key", "unique", "null", "not", "required", "increment", "inactive", "foreign_master"];
|
|
44
45
|
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
|
@@ -170,6 +170,22 @@ export const BSON_TYPES = [
|
|
|
170
170
|
* and only when `{` follows, since a field may carry one of these names.
|
|
171
171
|
* ----------------------------------------------------------------------- */
|
|
172
172
|
export const BLOCK_KEYWORDS = ['indexes', 'checks', 'constraints', 'records'];
|
|
173
|
+
/* -------------------------------------------------------------------------
|
|
174
|
+
* DiagramView categories (spec §18.1, v0.6.3)
|
|
175
|
+
*
|
|
176
|
+
* The words that open a category inside a DiagramView body. `schemas` is
|
|
177
|
+
* the DBML name of `containers`. Both highlighters color them only when `{`
|
|
178
|
+
* follows, since an entity or a field may carry one of these names.
|
|
179
|
+
* ----------------------------------------------------------------------- */
|
|
180
|
+
export const DIAGRAM_VIEW_CATEGORIES = [
|
|
181
|
+
'tables',
|
|
182
|
+
'views',
|
|
183
|
+
'containers',
|
|
184
|
+
'schemas',
|
|
185
|
+
'tablegroups',
|
|
186
|
+
'supertypegroups',
|
|
187
|
+
'notes',
|
|
188
|
+
];
|
|
173
189
|
export const TARGET_NATIVE_TYPES = [
|
|
174
190
|
// Oracle
|
|
175
191
|
'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/module-resolver.js
CHANGED
|
@@ -78,7 +78,18 @@ function flattenTopLevel(stmt, out) {
|
|
|
78
78
|
return;
|
|
79
79
|
}
|
|
80
80
|
if (stmt.kind === 'ContainerDeclaration') {
|
|
81
|
-
|
|
81
|
+
// A `reuse *` inside a Container body can clone a DiagramView into it.
|
|
82
|
+
// A DiagramView is top-level only (spec §18.1), so it moves out to the
|
|
83
|
+
// top level, after its Container.
|
|
84
|
+
const flat = flattenContainer(stmt);
|
|
85
|
+
const hoisted = flat.body.filter((b) => b.kind === 'DiagramViewDeclaration');
|
|
86
|
+
if (hoisted.length === 0) {
|
|
87
|
+
out.push(flat);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
out.push({ ...flat, body: flat.body.filter((b) => !hoisted.includes(b)) });
|
|
91
|
+
for (const view of hoisted)
|
|
92
|
+
out.push(view);
|
|
82
93
|
return;
|
|
83
94
|
}
|
|
84
95
|
// All other top-level statement kinds pass through unchanged.
|
|
@@ -376,8 +387,13 @@ function findImportTarget(item, doc) {
|
|
|
376
387
|
}
|
|
377
388
|
return undefined;
|
|
378
389
|
}
|
|
379
|
-
//
|
|
380
|
-
|
|
390
|
+
// DiagramView (spec §18): top-level only, bare name. Its names resolve
|
|
391
|
+
// in the importing document, like the members of a TableGroup.
|
|
392
|
+
if (item.elementType === 'diagramview') {
|
|
393
|
+
return doc.statements.find((s) => s.kind === 'DiagramViewDeclaration' && s.name === path);
|
|
394
|
+
}
|
|
395
|
+
// View: top-level OR container-scoped.
|
|
396
|
+
if (item.elementType === 'view') {
|
|
381
397
|
if (segments.length === 1) {
|
|
382
398
|
return doc.statements.find((s) => s.kind === 'ViewDeclaration' && s.name === segments[0]);
|
|
383
399
|
}
|
|
@@ -614,6 +630,8 @@ function applyAlias(stmt, item) {
|
|
|
614
630
|
return { ...stmt, name: item.alias };
|
|
615
631
|
case 'SupertypeGroupDeclaration':
|
|
616
632
|
return { ...stmt, name: item.alias };
|
|
633
|
+
case 'DiagramViewDeclaration':
|
|
634
|
+
return { ...stmt, name: item.alias };
|
|
617
635
|
case 'TablePartialDeclaration':
|
|
618
636
|
return { ...stmt, name: item.alias };
|
|
619
637
|
case 'NoteDeclaration':
|
package/dist/monarch.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* dependency on consumers that just want to parse. The Monaco types
|
|
19
19
|
* are structurally compatible -- the consumer casts at the boundary.
|
|
20
20
|
*/
|
|
21
|
-
import { CONTAINER_KEYWORDS, ENTITY_KEYWORDS, DECLARATION_KEYWORDS, STRUCTURAL_TYPE_KEYWORDS, POLYMORPHISM_KEYWORDS, SCALAR_TYPES, BSON_TYPES, TARGET_NATIVE_TYPES, SETTING_FLAGS, SETTING_KEYS, GRANULARITY_VALUES, } from "./keywords.js";
|
|
21
|
+
import { CONTAINER_KEYWORDS, ENTITY_KEYWORDS, DECLARATION_KEYWORDS, STRUCTURAL_TYPE_KEYWORDS, POLYMORPHISM_KEYWORDS, SCALAR_TYPES, BSON_TYPES, TARGET_NATIVE_TYPES, SETTING_FLAGS, SETTING_KEYS, GRANULARITY_VALUES, DIAGRAM_VIEW_CATEGORIES, } from "./keywords.js";
|
|
22
22
|
/* -------------------------------------------------------------------------
|
|
23
23
|
* Language configuration
|
|
24
24
|
* ----------------------------------------------------------------------- */
|
|
@@ -160,6 +160,9 @@ export const xdbmlMonarchTokensProvider = {
|
|
|
160
160
|
// or at the top level before a name and '{'. Anywhere else it names a
|
|
161
161
|
// field, so it takes the identifier color like any other field name.
|
|
162
162
|
[/note(?![\w$])(?!\s*[:{])(?!\s+(?:[A-Za-z_][\w$]*|"[^"]*")\s*\{)/, 'identifier'],
|
|
163
|
+
// Spec §18.1 (v0.6.3): a DiagramView category keyword opens its list
|
|
164
|
+
// only before '{'. Elsewhere the same word names an entity or a field.
|
|
165
|
+
[new RegExp(`(?:${DIAGRAM_VIEW_CATEGORIES.join('|')})(?![\\w$])(?=\\s*\\{)`), 'keyword.declaration'],
|
|
163
166
|
// Identifiers and keyword recognition.
|
|
164
167
|
// The parser is the authority on keyword vs identifier disambiguation;
|
|
165
168
|
// Monarch does coarse highlighting based on lowercase comparison.
|
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' | 'duplicate-diagram-view' | 'duplicate-diagram-view-category' | 'unresolved-diagram-view-name' | 'ambiguous-diagram-view-name' | 'diagram-view-wrong-category' | 'unknown-diagram-view-setting';
|
|
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,8 +42,10 @@ 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";
|
|
48
|
+
import { checkDiagramViews } from "./diagram-views.js";
|
|
47
49
|
/**
|
|
48
50
|
* Read-only handle on the collected symbol table.
|
|
49
51
|
*
|
|
@@ -232,8 +234,11 @@ export function resolveNames(doc) {
|
|
|
232
234
|
diagnostics.push(...checkSupertypeGroups(flat));
|
|
233
235
|
// Pass 5: constraints and referenced keys (spec §10.10, §11.17).
|
|
234
236
|
diagnostics.push(...checkConstraints(flat));
|
|
237
|
+
diagnostics.push(...checkTargets(flat));
|
|
235
238
|
// Pass 6: view source queries (spec §14.7).
|
|
236
239
|
diagnostics.push(...checkViews(flat));
|
|
240
|
+
// Pass 7: diagram views (spec §18.6).
|
|
241
|
+
diagnostics.push(...checkDiagramViews(flat));
|
|
237
242
|
return { diagnostics, symbols };
|
|
238
243
|
}
|
|
239
244
|
/* -------------------------------------------------------------------------
|
|
@@ -241,9 +246,18 @@ export function resolveNames(doc) {
|
|
|
241
246
|
* ----------------------------------------------------------------------- */
|
|
242
247
|
function collectDeclarations(doc, entries, diagnostics) {
|
|
243
248
|
const seen = new Set(); // qualified-name keys to detect duplicates
|
|
249
|
+
const first = diagnostics.length;
|
|
244
250
|
for (const stmt of doc.statements) {
|
|
245
251
|
addTopLevelDeclaration(stmt, entries, diagnostics, seen);
|
|
246
252
|
}
|
|
253
|
+
// Before v0.6 the parser accepted a collision, so a document declaring an
|
|
254
|
+
// earlier version, or none (DBML), draws a warning and stays valid.
|
|
255
|
+
if (!versionAtLeast(doc, '0.6')) {
|
|
256
|
+
for (const d of diagnostics.slice(first)) {
|
|
257
|
+
if (d.code === 'name-collision')
|
|
258
|
+
d.severity = 'warning';
|
|
259
|
+
}
|
|
260
|
+
}
|
|
247
261
|
}
|
|
248
262
|
function addTopLevelDeclaration(stmt, entries, diagnostics, seen) {
|
|
249
263
|
switch (stmt.kind) {
|
|
@@ -313,9 +327,30 @@ function addTopLevelDeclaration(stmt, entries, diagnostics, seen) {
|
|
|
313
327
|
return;
|
|
314
328
|
}
|
|
315
329
|
}
|
|
330
|
+
const SHARED_NAMESPACE = new Set(['entity', 'view', 'edge', 'type', 'enum', 'tablepartial']);
|
|
331
|
+
const KIND_NAMES = {
|
|
332
|
+
entity: 'an Entity', view: 'a View', edge: 'an Edge', type: 'a Type', enum: 'an Enum', tablepartial: 'a TablePartial',
|
|
333
|
+
};
|
|
334
|
+
function article(kind) {
|
|
335
|
+
return KIND_NAMES[kind] ?? kind;
|
|
336
|
+
}
|
|
316
337
|
function addEntry(name, containerName, kind, declaration, span, entries, diagnostics, seen) {
|
|
317
338
|
const qualifiedName = containerName ? `${containerName}.${name}` : name;
|
|
318
339
|
const key = `${kind}:${qualifiedName}`;
|
|
340
|
+
// Spec §15.5: Entities, Views, Edges, Types, Enums and TablePartials
|
|
341
|
+
// share one namespace. collectDeclarations() sets the severity.
|
|
342
|
+
if (SHARED_NAMESPACE.has(kind) && !seen.has(key)) {
|
|
343
|
+
const other = entries.find((e) => e.qualifiedName === qualifiedName && e.kind !== kind && SHARED_NAMESPACE.has(e.kind));
|
|
344
|
+
if (other) {
|
|
345
|
+
diagnostics.push({
|
|
346
|
+
severity: 'error',
|
|
347
|
+
code: 'name-collision',
|
|
348
|
+
message: `'${qualifiedName}' names both ${article(other.kind)} and ${article(kind)}. ` +
|
|
349
|
+
`Entities, Views, Edges, Types, Enums and TablePartials share one namespace (spec §15.5): rename one of them.`,
|
|
350
|
+
span,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
319
354
|
if (seen.has(key)) {
|
|
320
355
|
diagnostics.push({
|
|
321
356
|
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.3";
|
|
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 {
|
|
@@ -345,6 +345,21 @@ export declare class Parser {
|
|
|
345
345
|
* rather than stopping the parse.
|
|
346
346
|
*/
|
|
347
347
|
private parseSupertypeGroup;
|
|
348
|
+
/**
|
|
349
|
+
* `DiagramView <name> { <Category> { <names> | * } ... }`, or
|
|
350
|
+
* `DiagramView <name> { * }` for every element of every category.
|
|
351
|
+
* Category keywords are case-insensitive; `Schemas` reads as `Containers`.
|
|
352
|
+
* Lists follow spec §3.9: whitespace between names, with commas and
|
|
353
|
+
* semicolons allowed and meaningless. Names stay unresolved here; the
|
|
354
|
+
* checks of §18.6 run after parsing, in `checkDiagramViews()`, so a bad
|
|
355
|
+
* name gets a located diagnostic and the rest of the diagram still draws.
|
|
356
|
+
* Two conditions stop the parse because they leave no category to read:
|
|
357
|
+
* an unknown category, `Edges` included, and a body-level `*` with a
|
|
358
|
+
* category beside it. A note may sit in the brackets or in the body
|
|
359
|
+
* (spec §18.1, v0.6.3).
|
|
360
|
+
*/
|
|
361
|
+
private parseDiagramView;
|
|
362
|
+
private parseDiagramViewCategory;
|
|
348
363
|
private parseIndexes;
|
|
349
364
|
private parseIndexEntry;
|
|
350
365
|
private parseIndexComponent;
|
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.3';
|
|
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);
|
|
@@ -74,6 +74,19 @@ const IMPORT_ELEMENT_TYPES = new Set([
|
|
|
74
74
|
'type', 'edge', 'view', 'diagramview',
|
|
75
75
|
'field', 'supertypegroup',
|
|
76
76
|
]);
|
|
77
|
+
/**
|
|
78
|
+
* DiagramView categories (spec §18.1), by lowercased keyword. `Schemas`, the
|
|
79
|
+
* DBML name, reads as `Containers`, after the `Schema` keyword (§6.2).
|
|
80
|
+
*/
|
|
81
|
+
const DIAGRAM_VIEW_CATEGORIES = {
|
|
82
|
+
tables: 'Tables',
|
|
83
|
+
views: 'Views',
|
|
84
|
+
containers: 'Containers',
|
|
85
|
+
schemas: 'Containers',
|
|
86
|
+
tablegroups: 'TableGroups',
|
|
87
|
+
supertypegroups: 'SupertypeGroups',
|
|
88
|
+
notes: 'Notes',
|
|
89
|
+
};
|
|
77
90
|
const STRUCTURAL_TYPE_KEYWORDS = new Set([
|
|
78
91
|
'object', 'struct', 'record', 'array', 'list', 'map', 'dict', 'dictionary',
|
|
79
92
|
'set', 'union', 'oneof', 'anyof', 'allof', 'json', 'jsonb', 'variant',
|
|
@@ -320,6 +333,8 @@ export class Parser {
|
|
|
320
333
|
return this.parseTableGroup();
|
|
321
334
|
if (k === 'supertypegroup')
|
|
322
335
|
return this.parseSupertypeGroup();
|
|
336
|
+
if (k === 'diagramview')
|
|
337
|
+
return this.parseDiagramView();
|
|
323
338
|
if (k === 'note')
|
|
324
339
|
return this.parseNoteDeclaration();
|
|
325
340
|
if (k === 'records')
|
|
@@ -471,6 +486,10 @@ export class Parser {
|
|
|
471
486
|
else if (k === 'use' || k === 'reuse') {
|
|
472
487
|
body.push(this.parseModuleDirective('container-body'));
|
|
473
488
|
}
|
|
489
|
+
else if (k === 'diagramview') {
|
|
490
|
+
throw new ParseError(`A DiagramView is a top-level declaration and never appears in a Container (spec §18.1). ` +
|
|
491
|
+
`Move it out of Container '${name}'.`, t.start);
|
|
492
|
+
}
|
|
474
493
|
else {
|
|
475
494
|
// Unknown line; tolerate as no-op rather than fail the whole parse.
|
|
476
495
|
throw new ParseError(`Unexpected token in Container body: ${t.kind} ${JSON.stringify(t.text)}`, t.start);
|
|
@@ -860,6 +879,11 @@ export class Parser {
|
|
|
860
879
|
throw new ParseError(`Unknown import element type '${elemTok.text}'. ` +
|
|
861
880
|
`Expected one of: ${Array.from(IMPORT_ELEMENT_TYPES).join(', ')}.`, elemTok.start);
|
|
862
881
|
}
|
|
882
|
+
if (elementType === 'diagramview' && context !== 'file-scope') {
|
|
883
|
+
// Spec §18.1, §27.5: a DiagramView is a top-level declaration, so its
|
|
884
|
+
// import goes at file scope, where the declaration itself would.
|
|
885
|
+
throw new ParseError('A DiagramView is a top-level declaration: import it at file scope, not inside a Container body (spec §18.1, §27.5).', elemTok.start);
|
|
886
|
+
}
|
|
863
887
|
if (elementType === 'field' && context !== 'file-scope') {
|
|
864
888
|
// Spec §27.8: field imports must appear at file scope. Inside a
|
|
865
889
|
// Container body, the field's eventual placement (as a Named Type)
|
|
@@ -1385,6 +1409,17 @@ export class Parser {
|
|
|
1385
1409
|
}
|
|
1386
1410
|
this.expect(TokenKind.RParen, "Expected ')' closing type parameters");
|
|
1387
1411
|
}
|
|
1412
|
+
// DBML's array type of a target, `text[]` or `varchar(20)[]`, keeps its
|
|
1413
|
+
// brackets in the type name (spec §8.1). Only empty brackets written
|
|
1414
|
+
// right after the type, with no space, belong to it: `int [pk]` and
|
|
1415
|
+
// `int []` remain settings blocks.
|
|
1416
|
+
while (this.check(TokenKind.LBracket) &&
|
|
1417
|
+
this.peek(1).kind === TokenKind.RBracket &&
|
|
1418
|
+
this.peek().start.offset === this.tokens[this.idx - 1].end.offset) {
|
|
1419
|
+
this.advance();
|
|
1420
|
+
this.advance();
|
|
1421
|
+
name += '[]';
|
|
1422
|
+
}
|
|
1388
1423
|
return {
|
|
1389
1424
|
kind: 'ScalarType',
|
|
1390
1425
|
name,
|
|
@@ -1940,6 +1975,120 @@ export class Parser {
|
|
|
1940
1975
|
span: this.spanFrom(start),
|
|
1941
1976
|
};
|
|
1942
1977
|
}
|
|
1978
|
+
/* ----- DiagramView (spec §18, defined in v0.6.3) ----- */
|
|
1979
|
+
/**
|
|
1980
|
+
* `DiagramView <name> { <Category> { <names> | * } ... }`, or
|
|
1981
|
+
* `DiagramView <name> { * }` for every element of every category.
|
|
1982
|
+
* Category keywords are case-insensitive; `Schemas` reads as `Containers`.
|
|
1983
|
+
* Lists follow spec §3.9: whitespace between names, with commas and
|
|
1984
|
+
* semicolons allowed and meaningless. Names stay unresolved here; the
|
|
1985
|
+
* checks of §18.6 run after parsing, in `checkDiagramViews()`, so a bad
|
|
1986
|
+
* name gets a located diagnostic and the rest of the diagram still draws.
|
|
1987
|
+
* Two conditions stop the parse because they leave no category to read:
|
|
1988
|
+
* an unknown category, `Edges` included, and a body-level `*` with a
|
|
1989
|
+
* category beside it. A note may sit in the brackets or in the body
|
|
1990
|
+
* (spec §18.1, v0.6.3).
|
|
1991
|
+
*/
|
|
1992
|
+
parseDiagramView() {
|
|
1993
|
+
const start = this.peek().start;
|
|
1994
|
+
this.advance(); // DiagramView
|
|
1995
|
+
const nameTok = this.peek();
|
|
1996
|
+
if (nameTok.kind !== TokenKind.Identifier && nameTok.kind !== TokenKind.QuotedIdentifier) {
|
|
1997
|
+
throw new ParseError('Expected a name after DiagramView (spec §18.1)', nameTok.start);
|
|
1998
|
+
}
|
|
1999
|
+
const name = this.parseIdentLikeName('DiagramView name');
|
|
2000
|
+
// Spec §18.1 (v0.6.3): `note` and custom properties in the brackets.
|
|
2001
|
+
// Other settings are reported by checkDiagramViews(), not here.
|
|
2002
|
+
const settings = this.maybeSettingsBlock();
|
|
2003
|
+
this.expect(TokenKind.LBrace, `Expected '{' after DiagramView ${name}`);
|
|
2004
|
+
const categories = [];
|
|
2005
|
+
const notes = [];
|
|
2006
|
+
let wildcardBody = false;
|
|
2007
|
+
for (;;) {
|
|
2008
|
+
this.skipListSeparators();
|
|
2009
|
+
const t = this.peek();
|
|
2010
|
+
if (t.kind === TokenKind.RBrace || t.kind === TokenKind.EOF)
|
|
2011
|
+
break;
|
|
2012
|
+
if (this.atNoteElement()) {
|
|
2013
|
+
notes.push(this.parseNoteBlockOrSetting());
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
if (t.kind === TokenKind.Star) {
|
|
2017
|
+
if (categories.length > 0 || wildcardBody) {
|
|
2018
|
+
throw new ParseError(`A '*' body lists every element of every category, so no category goes beside it in DiagramView ${name} (spec §18.1)`, t.start);
|
|
2019
|
+
}
|
|
2020
|
+
this.advance();
|
|
2021
|
+
wildcardBody = true;
|
|
2022
|
+
continue;
|
|
2023
|
+
}
|
|
2024
|
+
const k = kw(t);
|
|
2025
|
+
const category = k ? DIAGRAM_VIEW_CATEGORIES[k] : undefined;
|
|
2026
|
+
if (!category) {
|
|
2027
|
+
if (k === 'edges') {
|
|
2028
|
+
throw new ParseError('Edges is not a DiagramView category: a Ref or an Edge appears in a diagram view when both ' +
|
|
2029
|
+
'of its ends do, so there is nothing to list (spec §18.1, §18.4)', t.start);
|
|
2030
|
+
}
|
|
2031
|
+
throw new ParseError(`Unknown DiagramView category ${JSON.stringify(t.text)}. Expected Tables, Views, Containers ` +
|
|
2032
|
+
'(or Schemas), TableGroups, SupertypeGroups or Notes, each followed by { names }, or a Note (spec §18.1)', t.start);
|
|
2033
|
+
}
|
|
2034
|
+
if (wildcardBody) {
|
|
2035
|
+
throw new ParseError(`A '*' body lists every element of every category, so no category goes beside it in DiagramView ${name} (spec §18.1)`, t.start);
|
|
2036
|
+
}
|
|
2037
|
+
categories.push(this.parseDiagramViewCategory(category));
|
|
2038
|
+
}
|
|
2039
|
+
this.expect(TokenKind.RBrace, "Expected '}' closing DiagramView");
|
|
2040
|
+
return {
|
|
2041
|
+
kind: 'DiagramViewDeclaration',
|
|
2042
|
+
name,
|
|
2043
|
+
settings,
|
|
2044
|
+
notes,
|
|
2045
|
+
wildcardBody,
|
|
2046
|
+
categories,
|
|
2047
|
+
span: this.spanFrom(start),
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
parseDiagramViewCategory(category) {
|
|
2051
|
+
const start = this.peek().start;
|
|
2052
|
+
const keywordTok = this.advance();
|
|
2053
|
+
this.expect(TokenKind.LBrace, `Expected '{' after ${keywordTok.text} in a DiagramView`);
|
|
2054
|
+
const items = [];
|
|
2055
|
+
let wildcard = false;
|
|
2056
|
+
for (;;) {
|
|
2057
|
+
this.skipListSeparators();
|
|
2058
|
+
const t = this.peek();
|
|
2059
|
+
if (t.kind === TokenKind.RBrace || t.kind === TokenKind.EOF)
|
|
2060
|
+
break;
|
|
2061
|
+
if (t.kind === TokenKind.Star) {
|
|
2062
|
+
this.advance();
|
|
2063
|
+
wildcard = true;
|
|
2064
|
+
continue;
|
|
2065
|
+
}
|
|
2066
|
+
if (t.kind !== TokenKind.Identifier && t.kind !== TokenKind.QuotedIdentifier) {
|
|
2067
|
+
throw new ParseError(`Expected a name or '*' in ${keywordTok.text} { }, got ${t.kind} ${JSON.stringify(t.text)}`, t.start);
|
|
2068
|
+
}
|
|
2069
|
+
this.advance();
|
|
2070
|
+
let n = t.kind === TokenKind.QuotedIdentifier ? (t.value ?? '') : t.text;
|
|
2071
|
+
while (this.check(TokenKind.Dot)) {
|
|
2072
|
+
this.advance();
|
|
2073
|
+
const next = this.peek();
|
|
2074
|
+
if (next.kind !== TokenKind.Identifier && next.kind !== TokenKind.QuotedIdentifier) {
|
|
2075
|
+
throw new ParseError(`Expected identifier after '.' in ${keywordTok.text} { }`, next.start);
|
|
2076
|
+
}
|
|
2077
|
+
this.advance();
|
|
2078
|
+
n += `.${next.kind === TokenKind.QuotedIdentifier ? (next.value ?? '') : next.text}`;
|
|
2079
|
+
}
|
|
2080
|
+
items.push({ kind: 'DiagramViewItem', name: n, span: this.spanFrom(t.start) });
|
|
2081
|
+
}
|
|
2082
|
+
this.expect(TokenKind.RBrace, `Expected '}' closing ${keywordTok.text}`);
|
|
2083
|
+
return {
|
|
2084
|
+
kind: 'DiagramViewCategory',
|
|
2085
|
+
category,
|
|
2086
|
+
keyword: keywordTok.text,
|
|
2087
|
+
wildcard,
|
|
2088
|
+
items,
|
|
2089
|
+
span: this.spanFrom(start),
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
1943
2092
|
/* ----- Indexes ----- */
|
|
1944
2093
|
parseIndexes() {
|
|
1945
2094
|
const start = this.peek().start;
|
|
@@ -2376,7 +2525,66 @@ export function parse(source, options = {}) {
|
|
|
2376
2525
|
const initialStack = new Set();
|
|
2377
2526
|
if (options.filePath)
|
|
2378
2527
|
initialStack.add(options.filePath);
|
|
2379
|
-
|
|
2528
|
+
const doc = new Parser(tokens, options, initialStack, 0).parseDocument();
|
|
2529
|
+
applyEntityAliases(doc);
|
|
2530
|
+
return doc;
|
|
2531
|
+
}
|
|
2532
|
+
/**
|
|
2533
|
+
* Spec §7.4: `Table very_long_name as V { ... }` lets a relationship name
|
|
2534
|
+
* the entity by its alias, `Ref: V.id < posts.user_id`, as in DBML. This
|
|
2535
|
+
* pass rewrites the first segment of every relationship endpoint that is
|
|
2536
|
+
* an alias into the entity's name, so the resolver, the key checks and the
|
|
2537
|
+
* renderer see the entity itself. The entity keeps its `alias`. An alias
|
|
2538
|
+
* that repeats the name of an entity or a container, or that two entities
|
|
2539
|
+
* declare, is left alone: the name it would shadow applies.
|
|
2540
|
+
*/
|
|
2541
|
+
function applyEntityAliases(doc) {
|
|
2542
|
+
const aliases = new Map();
|
|
2543
|
+
const taken = new Set();
|
|
2544
|
+
const visit = (decl, container) => {
|
|
2545
|
+
if (decl.kind !== 'EntityDeclaration')
|
|
2546
|
+
return;
|
|
2547
|
+
const segments = [...(container ? [container] : []), ...decl.name.split('.')];
|
|
2548
|
+
taken.add(segments[segments.length - 1]);
|
|
2549
|
+
if (!decl.alias)
|
|
2550
|
+
return;
|
|
2551
|
+
aliases.set(decl.alias, aliases.has(decl.alias) ? null : segments);
|
|
2552
|
+
};
|
|
2553
|
+
for (const stmt of doc.statements) {
|
|
2554
|
+
if (stmt.kind === 'ContainerDeclaration') {
|
|
2555
|
+
taken.add(stmt.name);
|
|
2556
|
+
for (const body of stmt.body)
|
|
2557
|
+
visit(body, stmt.name);
|
|
2558
|
+
}
|
|
2559
|
+
else {
|
|
2560
|
+
visit(stmt);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
for (const name of taken)
|
|
2564
|
+
aliases.delete(name);
|
|
2565
|
+
if (![...aliases.values()].some((v) => v !== null))
|
|
2566
|
+
return;
|
|
2567
|
+
const walk = (node) => {
|
|
2568
|
+
if (Array.isArray(node)) {
|
|
2569
|
+
node.forEach(walk);
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
if (!node || typeof node !== 'object')
|
|
2573
|
+
return;
|
|
2574
|
+
const obj = node;
|
|
2575
|
+
if (obj.kind === 'RefEndpoint' && Array.isArray(obj.path)) {
|
|
2576
|
+
const first = obj.path[0];
|
|
2577
|
+
const segments = first?.kind === 'PathField' && first.name ? aliases.get(first.name) : undefined;
|
|
2578
|
+
if (segments) {
|
|
2579
|
+
obj.path = [...segments.map((name) => ({ ...first, name })), ...obj.path.slice(1)];
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2583
|
+
if (key !== 'span' && value && typeof value === 'object')
|
|
2584
|
+
walk(value);
|
|
2585
|
+
}
|
|
2586
|
+
};
|
|
2587
|
+
walk(doc.statements);
|
|
2380
2588
|
}
|
|
2381
2589
|
/**
|
|
2382
2590
|
* 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.3",
|
|
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",
|