@xdbml/parse 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ast.d.ts +30 -2
- package/dist/constraints.d.ts +81 -0
- package/dist/constraints.js +632 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/keywords.d.ts +2 -1
- package/dist/keywords.js +10 -0
- package/dist/monarch.js +4 -0
- package/dist/name-resolver.d.ts +1 -1
- package/dist/name-resolver.js +6 -0
- package/dist/parser.d.ts +27 -1
- package/dist/parser.js +192 -37
- package/dist/views.d.ts +18 -0
- package/dist/views.js +62 -0
- package/package.json +1 -1
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constraints (spec §10, new in v0.6) and referenced keys (spec §11.17).
|
|
3
|
+
*
|
|
4
|
+
* Three jobs:
|
|
5
|
+
*
|
|
6
|
+
* 1. `bodyConstraints()` / `entityConstraints()` gather every key and
|
|
7
|
+
* check of an entity into one list, whatever form declared it: a line
|
|
8
|
+
* in `constraints { }` or `checks { }`, `[pk]` / `[unique]` on a
|
|
9
|
+
* field, a `pk` entry in `indexes`, a field-level `check:`. This is
|
|
10
|
+
* the Constraint node of spec §28.6, and what the renderer and the
|
|
11
|
+
* playground read, so a consumer finds the primary key in one place.
|
|
12
|
+
*
|
|
13
|
+
* 2. `markPrimaryKeyNotNull()` adds an implied `not null` to each
|
|
14
|
+
* primary key field of a v0.6 document (spec §10.4). The parser calls
|
|
15
|
+
* it once the document is built.
|
|
16
|
+
*
|
|
17
|
+
* 3. `checkConstraints()` reports the conditions of spec §10.10 and the
|
|
18
|
+
* referenced-key rule of spec §11.17. Conditions on constructs that
|
|
19
|
+
* earlier versions and DBML accept are errors in a document declaring
|
|
20
|
+
* 0.6 or later and warnings otherwise, so a document stays valid under
|
|
21
|
+
* the version it declares.
|
|
22
|
+
*/
|
|
23
|
+
import { hasForeignMasterFlag, isForeignMaster, versionAtLeast } from "./relationships.js";
|
|
24
|
+
const hasFlag = (settings, ...names) => settings.some((s) => names.includes(s.name) && s.value === null);
|
|
25
|
+
const stringSetting = (settings, name) => {
|
|
26
|
+
const s = settings.find((x) => x.name === name);
|
|
27
|
+
return s?.value?.kind === 'StringValue' ? s.value.value : undefined;
|
|
28
|
+
};
|
|
29
|
+
const isPk = (settings) => hasFlag(settings, 'pk', 'primary key');
|
|
30
|
+
/** Dotted form of a key or index path; only field segments carry names. */
|
|
31
|
+
export function keyPathString(path) {
|
|
32
|
+
return path.map((seg) => (seg.kind === 'PathField' ? seg.name : '[]')).join('.');
|
|
33
|
+
}
|
|
34
|
+
const checkFromEntry = (e, source) => ({
|
|
35
|
+
kind: 'check',
|
|
36
|
+
expression: e.expression,
|
|
37
|
+
name: stringSetting(e.settings, 'name'),
|
|
38
|
+
note: stringSetting(e.settings, 'note'),
|
|
39
|
+
source,
|
|
40
|
+
span: e.span,
|
|
41
|
+
});
|
|
42
|
+
/**
|
|
43
|
+
* The constraints one body declares itself, TablePartial injections left
|
|
44
|
+
* out. `quotedFieldChecks` reads a quoted field-level `check:` as a check
|
|
45
|
+
* (v0.6 §10.5); in earlier versions a quoted value there is a string.
|
|
46
|
+
*/
|
|
47
|
+
export function bodyConstraints(body, quotedFieldChecks = true) {
|
|
48
|
+
const out = [];
|
|
49
|
+
const inlinePk = [];
|
|
50
|
+
for (const item of body) {
|
|
51
|
+
if (item.kind === 'FieldDeclaration') {
|
|
52
|
+
if (isPk(item.settings))
|
|
53
|
+
inlinePk.push(item);
|
|
54
|
+
if (hasFlag(item.settings, 'unique')) {
|
|
55
|
+
out.push({ kind: 'key', keyKind: 'unique', fields: [item.name], source: 'inline', span: item.span });
|
|
56
|
+
}
|
|
57
|
+
for (const s of item.settings) {
|
|
58
|
+
if (s.name !== 'check' || !s.value)
|
|
59
|
+
continue;
|
|
60
|
+
const expression = checkExpression(s.value, quotedFieldChecks);
|
|
61
|
+
if (expression !== undefined) {
|
|
62
|
+
out.push({ kind: 'check', expression, field: item.name, source: 'field-check', span: s.span });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (item.kind === 'ConstraintsBlock') {
|
|
67
|
+
for (const e of item.entries) {
|
|
68
|
+
if (e.kind === 'CheckEntry') {
|
|
69
|
+
out.push(checkFromEntry(e, 'constraints'));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const pk = isPk(e.settings);
|
|
73
|
+
const unique = hasFlag(e.settings, 'unique');
|
|
74
|
+
if (pk === unique)
|
|
75
|
+
continue; // neither or both: reported by checkConstraints
|
|
76
|
+
out.push({
|
|
77
|
+
kind: 'key',
|
|
78
|
+
keyKind: pk ? 'primary' : 'unique',
|
|
79
|
+
fields: e.fields.map(keyPathString),
|
|
80
|
+
name: stringSetting(e.settings, 'name'),
|
|
81
|
+
note: stringSetting(e.settings, 'note'),
|
|
82
|
+
source: 'constraints',
|
|
83
|
+
span: e.span,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else if (item.kind === 'ChecksBlock') {
|
|
88
|
+
for (const e of item.entries)
|
|
89
|
+
out.push(checkFromEntry(e, 'checks'));
|
|
90
|
+
}
|
|
91
|
+
else if (item.kind === 'IndexesBlock') {
|
|
92
|
+
for (const e of item.entries) {
|
|
93
|
+
if (!isPk(e.settings))
|
|
94
|
+
continue;
|
|
95
|
+
const fields = e.components.flatMap((c) => (c.kind === 'IndexPathComponent' ? [keyPathString(c.path)] : []));
|
|
96
|
+
out.push({
|
|
97
|
+
kind: 'key',
|
|
98
|
+
keyKind: 'primary',
|
|
99
|
+
fields,
|
|
100
|
+
name: stringSetting(e.settings, 'name'),
|
|
101
|
+
note: stringSetting(e.settings, 'note'),
|
|
102
|
+
source: 'indexes',
|
|
103
|
+
span: e.span,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (inlinePk.length > 0) {
|
|
109
|
+
// Several inline [pk] fields form one composite key in field order (§10.3).
|
|
110
|
+
out.unshift({
|
|
111
|
+
kind: 'key',
|
|
112
|
+
keyKind: 'primary',
|
|
113
|
+
fields: inlinePk.map((f) => f.name),
|
|
114
|
+
source: 'inline',
|
|
115
|
+
span: inlinePk[0].span,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
function checkExpression(value, quoted) {
|
|
121
|
+
if (value.kind === 'ExpressionValue')
|
|
122
|
+
return value.expression;
|
|
123
|
+
if (quoted && value.kind === 'StringValue')
|
|
124
|
+
return value.value;
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Every constraint of an entity (or edge, or partial): its own, plus those
|
|
129
|
+
* of the TablePartials it injects. One primary key results: the entity's
|
|
130
|
+
* own when it declares one, otherwise the last injected partial's (§10.4,
|
|
131
|
+
* following the conflict resolution of §17.1).
|
|
132
|
+
*/
|
|
133
|
+
export function entityConstraints(decl, doc) {
|
|
134
|
+
const quoted = versionAtLeast(doc, '0.6');
|
|
135
|
+
const partials = partialIndex(doc);
|
|
136
|
+
const own = bodyConstraints(decl.body, quoted);
|
|
137
|
+
const injected = [];
|
|
138
|
+
let partialPk;
|
|
139
|
+
for (const item of decl.body) {
|
|
140
|
+
if (item.kind !== 'PartialInjection')
|
|
141
|
+
continue;
|
|
142
|
+
const p = partials.get(item.partialName);
|
|
143
|
+
if (!p)
|
|
144
|
+
continue;
|
|
145
|
+
for (const c of bodyConstraints(p.body, quoted)) {
|
|
146
|
+
if (c.kind === 'key' && c.keyKind === 'primary')
|
|
147
|
+
partialPk = c;
|
|
148
|
+
else
|
|
149
|
+
injected.push(c);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const ownPk = own.find((c) => c.kind === 'key' && c.keyKind === 'primary');
|
|
153
|
+
const out = [...own, ...injected];
|
|
154
|
+
if (!ownPk && partialPk)
|
|
155
|
+
out.unshift(partialPk);
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
/** The primary key of an entity, or undefined. */
|
|
159
|
+
export function primaryKey(decl, doc) {
|
|
160
|
+
return entityConstraints(decl, doc).find((c) => c.kind === 'key' && c.keyKind === 'primary');
|
|
161
|
+
}
|
|
162
|
+
function bodies(doc) {
|
|
163
|
+
const out = [];
|
|
164
|
+
for (const s of doc.statements) {
|
|
165
|
+
if (s.kind === 'EntityDeclaration' || s.kind === 'EdgeDeclaration' || s.kind === 'TablePartialDeclaration') {
|
|
166
|
+
out.push({ decl: s });
|
|
167
|
+
}
|
|
168
|
+
else if (s.kind === 'ContainerDeclaration') {
|
|
169
|
+
for (const b of s.body) {
|
|
170
|
+
if (b.kind === 'EntityDeclaration' || b.kind === 'EdgeDeclaration')
|
|
171
|
+
out.push({ decl: b, container: s });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
function partialIndex(doc) {
|
|
178
|
+
const m = new Map();
|
|
179
|
+
for (const s of doc.statements)
|
|
180
|
+
if (s.kind === 'TablePartialDeclaration')
|
|
181
|
+
m.set(s.name, s);
|
|
182
|
+
return m;
|
|
183
|
+
}
|
|
184
|
+
function typeIndex(doc) {
|
|
185
|
+
const m = new Map();
|
|
186
|
+
for (const s of doc.statements)
|
|
187
|
+
if (s.kind === 'TypeDeclaration')
|
|
188
|
+
m.set(s.name, s);
|
|
189
|
+
return m;
|
|
190
|
+
}
|
|
191
|
+
/** Top-level fields of a body, including those of injected partials. */
|
|
192
|
+
function topFields(body, partials, seen = new Set()) {
|
|
193
|
+
const out = [];
|
|
194
|
+
for (const item of body) {
|
|
195
|
+
if (item.kind === 'FieldDeclaration')
|
|
196
|
+
out.push(item);
|
|
197
|
+
else if (item.kind === 'PartialInjection' && !seen.has(item.partialName)) {
|
|
198
|
+
const p = partials.get(item.partialName);
|
|
199
|
+
if (p) {
|
|
200
|
+
seen.add(item.partialName);
|
|
201
|
+
out.push(...topFields(p.body, partials, seen));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
/** The fields of an object-shaped type, or undefined for any other type. */
|
|
208
|
+
function objectFields(t, types) {
|
|
209
|
+
if (t.kind === 'ObjectType')
|
|
210
|
+
return t.fields;
|
|
211
|
+
if (t.kind === 'ScalarType' || t.kind === 'NamedTypeReference') {
|
|
212
|
+
const td = types.get(t.name);
|
|
213
|
+
if (td && !td.scalarBase)
|
|
214
|
+
return td.body;
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
const COLLECTION_KINDS = new Set(['ArrayType', 'TupleType', 'MapType', 'SetType']);
|
|
219
|
+
/**
|
|
220
|
+
* Follow a key path through object fields (§10.2). Reports the first field
|
|
221
|
+
* that is missing, or the first step through an array, tuple or map.
|
|
222
|
+
*/
|
|
223
|
+
function walkKeyPath(path, fields, types) {
|
|
224
|
+
let current = fields;
|
|
225
|
+
const walked = [];
|
|
226
|
+
for (let i = 0; i < path.length; i++) {
|
|
227
|
+
const seg = path[i];
|
|
228
|
+
if (seg.kind !== 'PathField')
|
|
229
|
+
return { kind: 'collection', at: walked.join('.') || '(start)' };
|
|
230
|
+
walked.push(seg.name);
|
|
231
|
+
const f = current.find((x) => x.name === seg.name);
|
|
232
|
+
if (!f)
|
|
233
|
+
return { kind: 'missing', at: walked.join('.') };
|
|
234
|
+
if (i === path.length - 1)
|
|
235
|
+
return undefined;
|
|
236
|
+
if (COLLECTION_KINDS.has(f.type.kind))
|
|
237
|
+
return { kind: 'collection', at: walked.join('.') };
|
|
238
|
+
const next = objectFields(f.type, types);
|
|
239
|
+
if (!next)
|
|
240
|
+
return { kind: 'missing', at: `${walked.join('.')}.${path[i + 1].name ?? ''}` };
|
|
241
|
+
current = next.filter((x) => x.kind === 'FieldDeclaration');
|
|
242
|
+
}
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
/* -------------------------------------------------------------------------
|
|
246
|
+
* Primary key fields are not null (§10.4)
|
|
247
|
+
* ----------------------------------------------------------------------- */
|
|
248
|
+
const NULLABILITY = ['null', 'not null'];
|
|
249
|
+
/**
|
|
250
|
+
* Add an implied `not null` to every field of the primary key of each
|
|
251
|
+
* entity, edge and partial the document declares itself, unless the field
|
|
252
|
+
* states its nullability. `null` on a primary key field is left in place
|
|
253
|
+
* for checkConstraints to report. Keys naming a partial's field from the
|
|
254
|
+
* injecting entity are not marked: the field belongs to the partial.
|
|
255
|
+
*/
|
|
256
|
+
export function markPrimaryKeyNotNull(doc) {
|
|
257
|
+
if (!versionAtLeast(doc, '0.6'))
|
|
258
|
+
return;
|
|
259
|
+
for (const { decl } of bodies(doc)) {
|
|
260
|
+
const pk = bodyConstraints(decl.body).find((c) => c.kind === 'key' && c.keyKind === 'primary');
|
|
261
|
+
if (!pk)
|
|
262
|
+
continue;
|
|
263
|
+
for (const dotted of pk.fields) {
|
|
264
|
+
const field = findField(decl.body, dotted.split('.'));
|
|
265
|
+
if (!field || field.settings.some((s) => NULLABILITY.includes(s.name)))
|
|
266
|
+
continue;
|
|
267
|
+
field.settings.push({ kind: 'Setting', name: 'not null', nameSource: 'not null', value: null, implied: true, span: field.span });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function findField(body, names) {
|
|
272
|
+
let current = body;
|
|
273
|
+
let found;
|
|
274
|
+
for (const n of names) {
|
|
275
|
+
found = current.find((x) => x.kind === 'FieldDeclaration' && x.name === n);
|
|
276
|
+
if (!found)
|
|
277
|
+
return undefined;
|
|
278
|
+
current = found.type.kind === 'ObjectType' ? found.type.fields : [];
|
|
279
|
+
}
|
|
280
|
+
return found;
|
|
281
|
+
}
|
|
282
|
+
/* -------------------------------------------------------------------------
|
|
283
|
+
* Targets (§5.1)
|
|
284
|
+
* ----------------------------------------------------------------------- */
|
|
285
|
+
/** Canonical relational targets and their aliases, lower-cased (§5.1). */
|
|
286
|
+
const RELATIONAL_TARGETS = new Set([
|
|
287
|
+
'oracle',
|
|
288
|
+
'postgresql', 'postgres', 'pg',
|
|
289
|
+
'sql server', 'mssql', 'microsoft sql server', 't-sql',
|
|
290
|
+
'mysql', 'mariadb', 'sqlite',
|
|
291
|
+
'db2', 'ibm db2', 'db2 luw', 'db2 for z/os', 'db2 z/os',
|
|
292
|
+
'teradata', 'snowflake', 'databricks',
|
|
293
|
+
'bigquery', 'google bigquery',
|
|
294
|
+
'redshift', 'amazon redshift',
|
|
295
|
+
'synapse analytics', 'azure synapse',
|
|
296
|
+
'timescaledb',
|
|
297
|
+
]);
|
|
298
|
+
/** True for a target name the §11.17 rule covers. Unlisted names are not relational. */
|
|
299
|
+
export function isRelationalTarget(name) {
|
|
300
|
+
return RELATIONAL_TARGETS.has(name.trim().toLowerCase());
|
|
301
|
+
}
|
|
302
|
+
function settingValues(v) {
|
|
303
|
+
if (!v)
|
|
304
|
+
return [];
|
|
305
|
+
if (v.kind === 'ListValue')
|
|
306
|
+
return v.items.flatMap(settingValues);
|
|
307
|
+
if (v.kind === 'StringValue')
|
|
308
|
+
return [v.value];
|
|
309
|
+
if (v.kind === 'IdentifierValue')
|
|
310
|
+
return [v.value];
|
|
311
|
+
return [];
|
|
312
|
+
}
|
|
313
|
+
/** The document-wide target when the Project declares exactly one. */
|
|
314
|
+
function projectTarget(doc) {
|
|
315
|
+
for (const s of doc.statements) {
|
|
316
|
+
if (s.kind !== 'ProjectDeclaration')
|
|
317
|
+
continue;
|
|
318
|
+
for (const item of s.body) {
|
|
319
|
+
if (item.kind === 'Setting' && (item.name === 'targets' || item.name === 'database_type')) {
|
|
320
|
+
const values = settingValues(item.value).filter((x) => x !== '');
|
|
321
|
+
return values.length === 1 ? values[0] : undefined;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
function effectiveTarget(p, project) {
|
|
328
|
+
const own = p.container?.settings.find((s) => s.name === 'target');
|
|
329
|
+
const values = settingValues(own?.value ?? null).filter((x) => x !== '');
|
|
330
|
+
return values[0] ?? project;
|
|
331
|
+
}
|
|
332
|
+
/* -------------------------------------------------------------------------
|
|
333
|
+
* Checks (§10.10, §11.17)
|
|
334
|
+
* ----------------------------------------------------------------------- */
|
|
335
|
+
export function checkConstraints(doc) {
|
|
336
|
+
const diagnostics = [];
|
|
337
|
+
const v06 = versionAtLeast(doc, '0.6');
|
|
338
|
+
// Conditions on constructs that earlier versions accept (§10.10).
|
|
339
|
+
const olderSeverity = v06 ? 'error' : 'warning';
|
|
340
|
+
const partials = partialIndex(doc);
|
|
341
|
+
const types = typeIndex(doc);
|
|
342
|
+
const versionSpan = (fallback) => (doc.version ? doc.version.span : fallback);
|
|
343
|
+
for (const { decl } of bodies(doc)) {
|
|
344
|
+
const fields = topFields(decl.body, partials);
|
|
345
|
+
const blocks = decl.body.filter((b) => b.kind === 'ConstraintsBlock');
|
|
346
|
+
for (const [i, block] of blocks.entries()) {
|
|
347
|
+
if (block.kind !== 'ConstraintsBlock')
|
|
348
|
+
continue;
|
|
349
|
+
if (!v06) {
|
|
350
|
+
diagnostics.push({
|
|
351
|
+
severity: 'error',
|
|
352
|
+
code: 'construct-requires-version',
|
|
353
|
+
message: "The constraints block requires a document declaring 'xdbml: 0.6' or later.",
|
|
354
|
+
span: versionSpan(block.span),
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
if (i > 0) {
|
|
358
|
+
diagnostics.push({
|
|
359
|
+
severity: 'error',
|
|
360
|
+
code: 'duplicate-constraints-block',
|
|
361
|
+
message: `'${decl.name}' declares a second constraints block; one body holds at most one.`,
|
|
362
|
+
span: block.span,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
for (const e of block.entries) {
|
|
366
|
+
if (e.kind === 'CheckEntry')
|
|
367
|
+
continue;
|
|
368
|
+
const pk = isPk(e.settings);
|
|
369
|
+
const unique = hasFlag(e.settings, 'unique');
|
|
370
|
+
if (pk === unique) {
|
|
371
|
+
diagnostics.push({
|
|
372
|
+
severity: 'error',
|
|
373
|
+
code: 'invalid-key-flags',
|
|
374
|
+
message: pk
|
|
375
|
+
? 'A key line carries both pk and unique; it takes exactly one.'
|
|
376
|
+
: 'A key line needs pk or unique.',
|
|
377
|
+
span: e.span,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
for (const path of e.fields) {
|
|
381
|
+
const problem = walkKeyPath(path, fields, types);
|
|
382
|
+
if (problem?.kind === 'missing') {
|
|
383
|
+
diagnostics.push({
|
|
384
|
+
severity: 'error',
|
|
385
|
+
code: 'unresolved-key-field',
|
|
386
|
+
message: `Key field '${problem.at}' is not declared in '${decl.name}'.`,
|
|
387
|
+
span: e.span,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
else if (problem?.kind === 'collection') {
|
|
391
|
+
diagnostics.push({
|
|
392
|
+
severity: 'error',
|
|
393
|
+
code: 'key-path-crosses-collection',
|
|
394
|
+
message: `Key path '${keyPathString(path)}' crosses an array, tuple or map at '${problem.at}'; a key path steps through object fields only.`,
|
|
395
|
+
span: e.span,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// Quoted checks are v0.6 syntax (§10.5).
|
|
402
|
+
if (!v06) {
|
|
403
|
+
for (const item of decl.body) {
|
|
404
|
+
if (item.kind !== 'ChecksBlock' && item.kind !== 'ConstraintsBlock')
|
|
405
|
+
continue;
|
|
406
|
+
for (const e of item.entries) {
|
|
407
|
+
if (e.kind === 'CheckEntry' && e.delimiter === 'quote') {
|
|
408
|
+
diagnostics.push({
|
|
409
|
+
severity: 'error',
|
|
410
|
+
code: 'construct-requires-version',
|
|
411
|
+
message: "A check expression in quotes requires a document declaring 'xdbml: 0.6' or later; use backticks.",
|
|
412
|
+
span: e.span,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// Index fields must be declared (§9, §10.10). Only the first segment is
|
|
419
|
+
// checked: deeper segments may iterate arrays implicitly (§9.3).
|
|
420
|
+
for (const item of decl.body) {
|
|
421
|
+
if (item.kind !== 'IndexesBlock')
|
|
422
|
+
continue;
|
|
423
|
+
for (const e of item.entries) {
|
|
424
|
+
for (const c of e.components) {
|
|
425
|
+
if (c.kind !== 'IndexPathComponent')
|
|
426
|
+
continue;
|
|
427
|
+
const first = c.path[0];
|
|
428
|
+
if (first?.kind === 'PathField' && !fields.some((f) => f.name === first.name)) {
|
|
429
|
+
diagnostics.push({
|
|
430
|
+
severity: olderSeverity,
|
|
431
|
+
code: 'unresolved-index-field',
|
|
432
|
+
message: `Index field '${first.name}' is not declared in '${decl.name}'.`,
|
|
433
|
+
span: c.span,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// One primary key, declared in one place (§10.4). Inline [pk] fields
|
|
440
|
+
// count as one declaration. When the second declaration restates the
|
|
441
|
+
// same fields, say so: the fix is to delete one, not to look for
|
|
442
|
+
// another key.
|
|
443
|
+
const own = bodyConstraints(decl.body, v06);
|
|
444
|
+
const pks = own.filter((c) => c.kind === 'key' && c.keyKind === 'primary');
|
|
445
|
+
for (const extra of pks.slice(1)) {
|
|
446
|
+
const first = pks[0];
|
|
447
|
+
diagnostics.push({
|
|
448
|
+
severity: olderSeverity,
|
|
449
|
+
code: 'duplicate-primary-key',
|
|
450
|
+
message: sameKeyFields(first.fields, extra.fields)
|
|
451
|
+
? `'${decl.name}' declares its primary key (${first.fields.join(', ')}) twice: ` +
|
|
452
|
+
`${twoForms(first, extra)}. Declare it in one place (spec §10.4).`
|
|
453
|
+
: `'${decl.name}' declares a second primary key; an entity has one, declared in one place.`,
|
|
454
|
+
span: extra.span,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
// Each unique key is declared once (§10.3, v0.6.1): a warning in every
|
|
458
|
+
// version, since earlier documents accept the repetition.
|
|
459
|
+
const uniques = own.filter((c) => c.kind === 'key' && c.keyKind === 'unique');
|
|
460
|
+
for (const [i, u] of uniques.entries()) {
|
|
461
|
+
const first = uniques.slice(0, i).find((prev) => sameKeyFields(prev.fields, u.fields));
|
|
462
|
+
if (!first)
|
|
463
|
+
continue;
|
|
464
|
+
diagnostics.push({
|
|
465
|
+
severity: 'warning',
|
|
466
|
+
code: 'duplicate-unique-key',
|
|
467
|
+
message: `'${decl.name}' declares the unique key (${first.fields.join(', ')}) twice: ` +
|
|
468
|
+
`${twoForms(first, u)}. Declare it in one place (spec §10.3).`,
|
|
469
|
+
span: u.span,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
// `null` on a primary key field (§10.4).
|
|
473
|
+
for (const pk of pks.slice(0, 1)) {
|
|
474
|
+
for (const dotted of pk.fields) {
|
|
475
|
+
const f = findField(decl.body, dotted.split('.'));
|
|
476
|
+
const nullSetting = f?.settings.find((s) => s.name === 'null' && !s.implied);
|
|
477
|
+
if (nullSetting) {
|
|
478
|
+
diagnostics.push({
|
|
479
|
+
severity: olderSeverity,
|
|
480
|
+
code: 'null-in-primary-key',
|
|
481
|
+
message: `'${decl.name}.${dotted}' is part of the primary key and cannot be null.`,
|
|
482
|
+
span: nullSetting.span,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
diagnostics.push(...checkReferencedKeys(doc, olderSeverity));
|
|
489
|
+
return diagnostics;
|
|
490
|
+
}
|
|
491
|
+
/** Two keys on the same fields, in any order, are the same key. */
|
|
492
|
+
function sameKeyFields(a, b) {
|
|
493
|
+
if (a.length !== b.length)
|
|
494
|
+
return false;
|
|
495
|
+
const sa = [...a].sort();
|
|
496
|
+
const sb = [...b].sort();
|
|
497
|
+
return sa.every((f, i) => f === sb[i]);
|
|
498
|
+
}
|
|
499
|
+
/** How a key was written, for a message that names both declarations. */
|
|
500
|
+
function keyForm(k) {
|
|
501
|
+
const flag = k.keyKind === 'primary' ? 'pk' : 'unique';
|
|
502
|
+
if (k.source === 'inline')
|
|
503
|
+
return `[${flag}] on ${k.fields.join(', ')}`;
|
|
504
|
+
if (k.source === 'indexes')
|
|
505
|
+
return `a ${flag} entry in indexes`;
|
|
506
|
+
return `a ${flag} line in constraints`;
|
|
507
|
+
}
|
|
508
|
+
/** Both declarations of a repeated key: "[pk] on id and a pk line in constraints". */
|
|
509
|
+
function twoForms(a, b) {
|
|
510
|
+
const fa = keyForm(a);
|
|
511
|
+
const fb = keyForm(b);
|
|
512
|
+
if (fa !== fb)
|
|
513
|
+
return `${fa} and ${fb}`;
|
|
514
|
+
const flag = a.keyKind === 'primary' ? 'pk' : 'unique';
|
|
515
|
+
return a.source === 'indexes' ? `two ${flag} entries in indexes` : `two ${flag} lines in constraints`;
|
|
516
|
+
}
|
|
517
|
+
function entityIndex(doc) {
|
|
518
|
+
const m = new Map();
|
|
519
|
+
const bare = new Map();
|
|
520
|
+
for (const p of bodies(doc)) {
|
|
521
|
+
if (p.decl.kind !== 'EntityDeclaration')
|
|
522
|
+
continue;
|
|
523
|
+
const e = { placed: p, decl: p.decl };
|
|
524
|
+
const q = p.container ? `${p.container.name}.${p.decl.name}` : p.decl.name;
|
|
525
|
+
m.set(q, e);
|
|
526
|
+
const b = p.decl.name.split('.').pop() ?? p.decl.name;
|
|
527
|
+
bare.set(b, [...(bare.get(b) ?? []), e]);
|
|
528
|
+
}
|
|
529
|
+
for (const [b, list] of bare)
|
|
530
|
+
if (list.length === 1 && !m.has(b))
|
|
531
|
+
m.set(b, list[0]);
|
|
532
|
+
return m;
|
|
533
|
+
}
|
|
534
|
+
/** Longest prefix naming an entity; the rest is a field path. Entity-level ends return undefined fields. */
|
|
535
|
+
function resolveEnd(ep, index) {
|
|
536
|
+
const names = ep.path.map((s) => (s.kind === 'PathField' ? s.name : '[]'));
|
|
537
|
+
for (let i = names.length; i >= 1; i--) {
|
|
538
|
+
const entity = index.get(names.slice(0, i).join('.'));
|
|
539
|
+
if (!entity)
|
|
540
|
+
continue;
|
|
541
|
+
if (ep.compositeFields && ep.compositeFields.length > 0)
|
|
542
|
+
return { entity, fields: [...ep.compositeFields] };
|
|
543
|
+
const rest = names.slice(i).join('.');
|
|
544
|
+
return rest ? { entity, fields: [rest] } : 'entity-level';
|
|
545
|
+
}
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
const sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
|
|
549
|
+
/** Keys a referenced end may match: keys, plus DBML unique indexes (§11.17). */
|
|
550
|
+
function referenceableKeys(decl, doc) {
|
|
551
|
+
const keys = entityConstraints(decl, doc)
|
|
552
|
+
.filter((c) => c.kind === 'key')
|
|
553
|
+
.map((k) => k.fields);
|
|
554
|
+
for (const item of decl.body) {
|
|
555
|
+
if (item.kind !== 'IndexesBlock')
|
|
556
|
+
continue;
|
|
557
|
+
for (const e of item.entries) {
|
|
558
|
+
if (isPk(e.settings) || !hasFlag(e.settings, 'unique'))
|
|
559
|
+
continue;
|
|
560
|
+
keys.push(e.components.flatMap((c) => (c.kind === 'IndexPathComponent' ? [keyPathString(c.path)] : [])));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return keys;
|
|
564
|
+
}
|
|
565
|
+
function checkReferencedKeys(doc, severity) {
|
|
566
|
+
const diagnostics = [];
|
|
567
|
+
const index = entityIndex(doc);
|
|
568
|
+
const project = projectTarget(doc);
|
|
569
|
+
const exemptTarget = (e) => {
|
|
570
|
+
const t = effectiveTarget(e.placed, project);
|
|
571
|
+
return t !== undefined && !isRelationalTarget(t);
|
|
572
|
+
};
|
|
573
|
+
const check = (source, operator, target, span) => {
|
|
574
|
+
if (operator === '<>')
|
|
575
|
+
return;
|
|
576
|
+
const s = resolveEnd(source, index);
|
|
577
|
+
const t = resolveEnd(target, index);
|
|
578
|
+
if (!s || !t || s === 'entity-level' || t === 'entity-level')
|
|
579
|
+
return;
|
|
580
|
+
if (exemptTarget(s.entity) || exemptTarget(t.entity))
|
|
581
|
+
return;
|
|
582
|
+
const sides = operator === '>' ? [t] : operator === '<' ? [s] : [s, t];
|
|
583
|
+
const withKeys = sides
|
|
584
|
+
.map((side) => ({ side, keys: referenceableKeys(side.entity.decl, doc) }))
|
|
585
|
+
.filter((x) => x.keys.length > 0);
|
|
586
|
+
if (withKeys.length === 0)
|
|
587
|
+
return; // no key declared: a tool MAY promote (§11.17)
|
|
588
|
+
if (withKeys.some((x) => x.keys.some((k) => sameSet(k, x.side.fields))))
|
|
589
|
+
return;
|
|
590
|
+
const first = withKeys[0];
|
|
591
|
+
const pk = primaryKey(first.side.entity.decl, doc);
|
|
592
|
+
const where = `${first.side.entity.decl.name}.${first.side.fields.length > 1 ? `(${first.side.fields.join(', ')})` : first.side.fields[0]}`;
|
|
593
|
+
diagnostics.push({
|
|
594
|
+
severity,
|
|
595
|
+
code: 'ref-target-not-key',
|
|
596
|
+
message: `Relationship references '${where}', which is not the primary key or a unique key of '${first.side.entity.decl.name}'` +
|
|
597
|
+
(pk ? ` (primary key: ${pk.fields.join(', ')}).` : '.') +
|
|
598
|
+
' Declare those fields unique, or reference a key.',
|
|
599
|
+
span,
|
|
600
|
+
});
|
|
601
|
+
};
|
|
602
|
+
const visitRef = (ref) => {
|
|
603
|
+
if (isForeignMaster(ref))
|
|
604
|
+
return;
|
|
605
|
+
check(ref.spec.source, ref.spec.operator, ref.spec.target, ref.span);
|
|
606
|
+
};
|
|
607
|
+
for (const s of doc.statements) {
|
|
608
|
+
if (s.kind === 'RefDeclaration')
|
|
609
|
+
visitRef(s);
|
|
610
|
+
}
|
|
611
|
+
// Inline refs on top-level fields.
|
|
612
|
+
for (const p of bodies(doc)) {
|
|
613
|
+
if (p.decl.kind !== 'EntityDeclaration')
|
|
614
|
+
continue;
|
|
615
|
+
const owner = p.container ? `${p.container.name}.${p.decl.name}` : p.decl.name;
|
|
616
|
+
for (const item of p.decl.body) {
|
|
617
|
+
if (item.kind !== 'FieldDeclaration' || hasForeignMasterFlag(item.settings))
|
|
618
|
+
continue;
|
|
619
|
+
for (const st of item.settings) {
|
|
620
|
+
if (st.name !== 'ref' || st.value?.kind !== 'RefValue')
|
|
621
|
+
continue;
|
|
622
|
+
const source = {
|
|
623
|
+
kind: 'RefEndpoint',
|
|
624
|
+
path: [...owner.split('.'), item.name].map((name) => ({ kind: 'PathField', name, span: item.span })),
|
|
625
|
+
span: item.span,
|
|
626
|
+
};
|
|
627
|
+
check(source, st.value.operator, st.value.target, st.span);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return diagnostics;
|
|
632
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -32,6 +32,9 @@ export { flatten } from './module-resolver.ts';
|
|
|
32
32
|
export { classifyModuleSource, isUrlKey, ModuleSourceError, } from './module-resolver.ts';
|
|
33
33
|
export type { ModuleSource } from './module-resolver.ts';
|
|
34
34
|
export { resolveNames, SymbolTable } from './name-resolver.ts';
|
|
35
|
+
export { checkViews, viewSourceQuery } from './views.ts';
|
|
36
|
+
export { bodyConstraints, checkConstraints, entityConstraints, isRelationalTarget, keyPathString, markPrimaryKeyNotNull, primaryKey, } from './constraints.ts';
|
|
37
|
+
export type { CheckConstraint, Constraint, ConstraintSource, KeyConstraint } from './constraints.ts';
|
|
35
38
|
export { FOREIGN_MASTER_FLAG, checkRelationships, hasForeignMasterFlag, isForeignMaster, pathToString, refChildEndpoint, refParentEndpoint, relationshipType, versionAtLeast, CONSTRAINT_TYPES, V04_RELATIONSHIP_SETTINGS, constraintType, entityNames, isEntityLevelEndpoint, isUndirected, } from './relationships.ts';
|
|
36
39
|
export type { RelationshipType, ConstraintType } from './relationships.ts';
|
|
37
40
|
export { SUPERTYPE_GROUP_VALUES, canonicalSupertypeGroupValue, checkSupertypeGroups, resolveSupertypeGroups, subtypeStrategy, supertypeChains, supertypeGroupSettings, } from './supertypes.ts';
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,8 @@ export { parse, Parser, ParseError, SUPPORTED_XDBML_VERSION, compareVersions } f
|
|
|
30
30
|
export { flatten } from "./module-resolver.js";
|
|
31
31
|
export { classifyModuleSource, isUrlKey, ModuleSourceError, } from "./module-resolver.js";
|
|
32
32
|
export { resolveNames, SymbolTable } from "./name-resolver.js";
|
|
33
|
+
export { checkViews, viewSourceQuery } from "./views.js";
|
|
34
|
+
export { bodyConstraints, checkConstraints, entityConstraints, isRelationalTarget, keyPathString, markPrimaryKeyNotNull, primaryKey, } from "./constraints.js";
|
|
33
35
|
export { FOREIGN_MASTER_FLAG, checkRelationships, hasForeignMasterFlag, isForeignMaster, pathToString, refChildEndpoint, refParentEndpoint, relationshipType, versionAtLeast, CONSTRAINT_TYPES, V04_RELATIONSHIP_SETTINGS, constraintType, entityNames, isEntityLevelEndpoint, isUndirected, } from "./relationships.js";
|
|
34
36
|
export { SUPERTYPE_GROUP_VALUES, canonicalSupertypeGroupValue, checkSupertypeGroups, resolveSupertypeGroups, subtypeStrategy, supertypeChains, supertypeGroupSettings, } from "./supertypes.js";
|
|
35
37
|
export { xdbmlLanguageConfig, xdbmlMonarchTokensProvider, } from "./monarch.js";
|
package/dist/keywords.d.ts
CHANGED
|
@@ -38,9 +38,10 @@ export declare const STRUCTURAL_TYPE_KEYWORDS: readonly ["object", "struct", "ar
|
|
|
38
38
|
export declare const POLYMORPHISM_KEYWORDS: readonly ["union", "oneof", "anyof", "allof"];
|
|
39
39
|
export declare const SCALAR_TYPES: readonly ["tinyint", "smallint", "mediumint", "int", "integer", "bigint", "int32", "int64", "float", "double", "decimal", "dec", "numeric", "real", "bit", "bool", "boolean", "char", "varchar", "varchar2", "nvarchar", "nvarchar2", "nchar", "text", "mediumtext", "longtext", "string", "ntext", "binary", "varbinary", "blob", "mediumblob", "longblob", "tinyblob", "tinytext", "json", "jsonb", "variant", "xml", "date", "time", "datetime", "datetime2", "timestamp", "timestamptz", "year", "uuid", "inet6", "money", "smallmoney", "enum"];
|
|
40
40
|
export declare const BSON_TYPES: readonly ["objectid", "decimal128", "bindata", "minkey", "maxkey", "symbol", "regex", "long", "double"];
|
|
41
|
+
export declare const BLOCK_KEYWORDS: readonly ["indexes", "checks", "constraints", "records"];
|
|
41
42
|
export declare const TARGET_NATIVE_TYPES: readonly ["number", "binary_float", "binary_double", "clob", "nclob", "bfile", "raw", "rowid", "urowid", "xmltype", "sdo_geometry", "interval", "serial", "smallserial", "bigserial", "serial2", "serial4", "serial8", "int2", "int4", "int8", "float4", "float8", "bytea", "citext", "inet", "cidr", "macaddr", "macaddr8", "tsvector", "tsquery", "hstore", "jsonpath", "timetz", "varbit", "int4range", "int8range", "numrange", "tsrange", "tstzrange", "daterange", "uniqueidentifier", "datetimeoffset", "smalldatetime", "rowversion", "hierarchyid", "sql_variant", "timestamp_ntz", "timestamp_ltz", "timestamp_tz", "vector", "float64", "bignumeric", "bigdecimal", "bytes", "geometry", "geography", "ascii", "varint", "timeuuid", "uint32", "uint64", "sint32", "sint64", "fixed32", "fixed64", "sfixed32", "sfixed64", "uint8", "uint16", "int16", "float32"];
|
|
42
43
|
export declare const SETTING_FLAGS: readonly ["pk", "primary", "key", "unique", "null", "not", "required", "increment", "inactive", "foreign_master"];
|
|
43
|
-
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", "replication", "location", "default_charset", "cloned_at"];
|
|
44
|
+
export declare const SETTING_KEYS: readonly ["note", "default", "ref", "name", "color", "headercolor", "as", "check", "type", "target", "targets", "database_type", "source", "source_cardinality", "target_cardinality", "min_source", "max_source", "min_target", "max_target", "undirected", "source_role", "target_role", "source_verb", "target_verb", "constraint_type", "discriminator", "supertype", "completeness", "exclusivity", "strategy", "merge", "source_query", "materialized", "refresh_schedule", "refresh_on", "source_database", "storage_options", "pattern", "format", "minlength", "maxlength", "minimum", "maximum", "exclusiveminimum", "exclusivemaximum", "multipleof", "minitems", "maxitems", "uniqueitems", "minproperties", "maxproperties", "synonyms", "business_term", "granularity", "tags", "delete", "update", "indexes", "checks", "constraints", "replication", "location", "default_charset", "cloned_at"];
|
|
44
45
|
export declare const GRANULARITY_VALUES: readonly ["year", "quarter", "month", "week", "day", "hour", "minute", "second", "millisecond", "microsecond", "nanosecond"];
|
|
45
46
|
export declare const DIRECTIVE_KEYWORDS: readonly ["xdbml", "experimental"];
|
|
46
47
|
export declare const MODULE_KEYWORDS: readonly ["use", "reuse", "from", "as"];
|