@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 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 `~`. */
@@ -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
  * ----------------------------------------------------------------------- */
@@ -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";
@@ -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() {
@@ -78,7 +78,18 @@ function flattenTopLevel(stmt, out) {
78
78
  return;
79
79
  }
80
80
  if (stmt.kind === 'ContainerDeclaration') {
81
- out.push(flattenContainer(stmt));
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
- // View / DiagramView: top-level OR container-scoped.
380
- if (item.elementType === 'view' || item.elementType === 'diagramview') {
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.
@@ -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.,
@@ -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.1";
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.1';
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
- return new Parser(tokens, options, initialStack, 0).parseDocument();
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.1",
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",