@xdbml/parse 0.5.1 → 0.6.0
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 +585 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/keywords.d.ts +1 -1
- package/dist/keywords.js +1 -0
- package/dist/name-resolver.d.ts +1 -1
- package/dist/name-resolver.js +3 -0
- package/dist/parser.d.ts +3 -1
- package/dist/parser.js +69 -6
- package/package.json +1 -1
package/dist/ast.d.ts
CHANGED
|
@@ -68,7 +68,7 @@ export interface EntityDeclaration {
|
|
|
68
68
|
span: Span;
|
|
69
69
|
}
|
|
70
70
|
export type EntityKeyword = 'Table' | 'Entity' | 'Collection' | 'Record';
|
|
71
|
-
export type EntityBodyItem = FieldDeclaration | IndexesBlock | ChecksBlock | NoteBlock | PartialInjection | RecordsBlock;
|
|
71
|
+
export type EntityBodyItem = FieldDeclaration | IndexesBlock | ChecksBlock | ConstraintsBlock | NoteBlock | PartialInjection | RecordsBlock;
|
|
72
72
|
export interface FieldDeclaration {
|
|
73
73
|
kind: 'FieldDeclaration';
|
|
74
74
|
name: string;
|
|
@@ -328,12 +328,34 @@ export interface ChecksBlock {
|
|
|
328
328
|
}
|
|
329
329
|
export interface CheckEntry {
|
|
330
330
|
kind: 'CheckEntry';
|
|
331
|
-
/** Source text inside the
|
|
331
|
+
/** Source text inside the delimiters, without them. */
|
|
332
332
|
expression: string;
|
|
333
|
+
/**
|
|
334
|
+
* How the source wrote the expression (spec §10.5, v0.6): in backticks,
|
|
335
|
+
* or in single or triple quotes. Both mean the same check; the raw AST
|
|
336
|
+
* keeps the form. Absent on entries built by older code paths, which
|
|
337
|
+
* only ever read backticks.
|
|
338
|
+
*/
|
|
339
|
+
delimiter?: 'backtick' | 'quote';
|
|
333
340
|
/** Optional settings -- typically `name:` and/or `note:`. */
|
|
334
341
|
settings: Setting[];
|
|
335
342
|
span: Span;
|
|
336
343
|
}
|
|
344
|
+
export interface ConstraintsBlock {
|
|
345
|
+
kind: 'ConstraintsBlock';
|
|
346
|
+
entries: ConstraintEntry[];
|
|
347
|
+
span: Span;
|
|
348
|
+
}
|
|
349
|
+
export type ConstraintEntry = KeyConstraintEntry | CheckEntry;
|
|
350
|
+
export interface KeyConstraintEntry {
|
|
351
|
+
kind: 'KeyConstraintEntry';
|
|
352
|
+
/** One path per key field, in key order. */
|
|
353
|
+
fields: PathSegment[][];
|
|
354
|
+
/** True when the source wrote the fields in parentheses. */
|
|
355
|
+
parenthesized: boolean;
|
|
356
|
+
settings: Setting[];
|
|
357
|
+
span: Span;
|
|
358
|
+
}
|
|
337
359
|
export interface TablePartialDeclaration {
|
|
338
360
|
kind: 'TablePartialDeclaration';
|
|
339
361
|
name: string;
|
|
@@ -500,6 +522,12 @@ export interface Setting {
|
|
|
500
522
|
nameSource: string;
|
|
501
523
|
/** Value, if any. A pure flag like `pk` has `value: null`. */
|
|
502
524
|
value: SettingValue | null;
|
|
525
|
+
/**
|
|
526
|
+
* True on a setting the parser added rather than read: `not null` on a
|
|
527
|
+
* primary key field of a v0.6 document (spec §10.4). Its span is the
|
|
528
|
+
* field's span.
|
|
529
|
+
*/
|
|
530
|
+
implied?: true;
|
|
503
531
|
span: Span;
|
|
504
532
|
}
|
|
505
533
|
export type SettingValue = StringValue | NumberValue | BooleanValue | NullValue | IdentifierValue | ExpressionValue | ListValue | RefValue;
|
|
@@ -0,0 +1,81 @@
|
|
|
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 type { EntityBodyItem, PathSegment, Span, XDbmlDocument } from './ast.ts';
|
|
24
|
+
import type { Diagnostic } from './name-resolver.ts';
|
|
25
|
+
/** Where a constraint was declared. The raw AST keeps the form; this list does not. */
|
|
26
|
+
export type ConstraintSource = 'constraints' | 'checks' | 'inline' | 'indexes' | 'field-check';
|
|
27
|
+
export interface KeyConstraint {
|
|
28
|
+
kind: 'key';
|
|
29
|
+
keyKind: 'primary' | 'unique';
|
|
30
|
+
/** Dotted field paths, in key order. */
|
|
31
|
+
fields: string[];
|
|
32
|
+
name?: string;
|
|
33
|
+
note?: string;
|
|
34
|
+
source: ConstraintSource;
|
|
35
|
+
span: Span;
|
|
36
|
+
}
|
|
37
|
+
export interface CheckConstraint {
|
|
38
|
+
kind: 'check';
|
|
39
|
+
expression: string;
|
|
40
|
+
name?: string;
|
|
41
|
+
note?: string;
|
|
42
|
+
/** For a field-level `check:`, the field it sits on. */
|
|
43
|
+
field?: string;
|
|
44
|
+
source: ConstraintSource;
|
|
45
|
+
span: Span;
|
|
46
|
+
}
|
|
47
|
+
export type Constraint = KeyConstraint | CheckConstraint;
|
|
48
|
+
type Body = ReadonlyArray<EntityBodyItem>;
|
|
49
|
+
/** Dotted form of a key or index path; only field segments carry names. */
|
|
50
|
+
export declare function keyPathString(path: ReadonlyArray<PathSegment>): string;
|
|
51
|
+
/**
|
|
52
|
+
* The constraints one body declares itself, TablePartial injections left
|
|
53
|
+
* out. `quotedFieldChecks` reads a quoted field-level `check:` as a check
|
|
54
|
+
* (v0.6 §10.5); in earlier versions a quoted value there is a string.
|
|
55
|
+
*/
|
|
56
|
+
export declare function bodyConstraints(body: Body, quotedFieldChecks?: boolean): Constraint[];
|
|
57
|
+
/**
|
|
58
|
+
* Every constraint of an entity (or edge, or partial): its own, plus those
|
|
59
|
+
* of the TablePartials it injects. One primary key results: the entity's
|
|
60
|
+
* own when it declares one, otherwise the last injected partial's (§10.4,
|
|
61
|
+
* following the conflict resolution of §17.1).
|
|
62
|
+
*/
|
|
63
|
+
export declare function entityConstraints(decl: {
|
|
64
|
+
body: Body;
|
|
65
|
+
}, doc: XDbmlDocument): Constraint[];
|
|
66
|
+
/** The primary key of an entity, or undefined. */
|
|
67
|
+
export declare function primaryKey(decl: {
|
|
68
|
+
body: Body;
|
|
69
|
+
}, doc: XDbmlDocument): KeyConstraint | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* Add an implied `not null` to every field of the primary key of each
|
|
72
|
+
* entity, edge and partial the document declares itself, unless the field
|
|
73
|
+
* states its nullability. `null` on a primary key field is left in place
|
|
74
|
+
* for checkConstraints to report. Keys naming a partial's field from the
|
|
75
|
+
* injecting entity are not marked: the field belongs to the partial.
|
|
76
|
+
*/
|
|
77
|
+
export declare function markPrimaryKeyNotNull(doc: XDbmlDocument): void;
|
|
78
|
+
/** True for a target name the §11.17 rule covers. Unlisted names are not relational. */
|
|
79
|
+
export declare function isRelationalTarget(name: string): boolean;
|
|
80
|
+
export declare function checkConstraints(doc: XDbmlDocument): Diagnostic[];
|
|
81
|
+
export {};
|
|
@@ -0,0 +1,585 @@
|
|
|
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.
|
|
441
|
+
const own = bodyConstraints(decl.body, v06);
|
|
442
|
+
const pks = own.filter((c) => c.kind === 'key' && c.keyKind === 'primary');
|
|
443
|
+
for (const extra of pks.slice(1)) {
|
|
444
|
+
diagnostics.push({
|
|
445
|
+
severity: olderSeverity,
|
|
446
|
+
code: 'duplicate-primary-key',
|
|
447
|
+
message: `'${decl.name}' declares a second primary key; an entity has one, declared in one place.`,
|
|
448
|
+
span: extra.span,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
// `null` on a primary key field (§10.4).
|
|
452
|
+
for (const pk of pks.slice(0, 1)) {
|
|
453
|
+
for (const dotted of pk.fields) {
|
|
454
|
+
const f = findField(decl.body, dotted.split('.'));
|
|
455
|
+
const nullSetting = f?.settings.find((s) => s.name === 'null' && !s.implied);
|
|
456
|
+
if (nullSetting) {
|
|
457
|
+
diagnostics.push({
|
|
458
|
+
severity: olderSeverity,
|
|
459
|
+
code: 'null-in-primary-key',
|
|
460
|
+
message: `'${decl.name}.${dotted}' is part of the primary key and cannot be null.`,
|
|
461
|
+
span: nullSetting.span,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
diagnostics.push(...checkReferencedKeys(doc, olderSeverity));
|
|
468
|
+
return diagnostics;
|
|
469
|
+
}
|
|
470
|
+
function entityIndex(doc) {
|
|
471
|
+
const m = new Map();
|
|
472
|
+
const bare = new Map();
|
|
473
|
+
for (const p of bodies(doc)) {
|
|
474
|
+
if (p.decl.kind !== 'EntityDeclaration')
|
|
475
|
+
continue;
|
|
476
|
+
const e = { placed: p, decl: p.decl };
|
|
477
|
+
const q = p.container ? `${p.container.name}.${p.decl.name}` : p.decl.name;
|
|
478
|
+
m.set(q, e);
|
|
479
|
+
const b = p.decl.name.split('.').pop() ?? p.decl.name;
|
|
480
|
+
bare.set(b, [...(bare.get(b) ?? []), e]);
|
|
481
|
+
}
|
|
482
|
+
for (const [b, list] of bare)
|
|
483
|
+
if (list.length === 1 && !m.has(b))
|
|
484
|
+
m.set(b, list[0]);
|
|
485
|
+
return m;
|
|
486
|
+
}
|
|
487
|
+
/** Longest prefix naming an entity; the rest is a field path. Entity-level ends return undefined fields. */
|
|
488
|
+
function resolveEnd(ep, index) {
|
|
489
|
+
const names = ep.path.map((s) => (s.kind === 'PathField' ? s.name : '[]'));
|
|
490
|
+
for (let i = names.length; i >= 1; i--) {
|
|
491
|
+
const entity = index.get(names.slice(0, i).join('.'));
|
|
492
|
+
if (!entity)
|
|
493
|
+
continue;
|
|
494
|
+
if (ep.compositeFields && ep.compositeFields.length > 0)
|
|
495
|
+
return { entity, fields: [...ep.compositeFields] };
|
|
496
|
+
const rest = names.slice(i).join('.');
|
|
497
|
+
return rest ? { entity, fields: [rest] } : 'entity-level';
|
|
498
|
+
}
|
|
499
|
+
return undefined;
|
|
500
|
+
}
|
|
501
|
+
const sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
|
|
502
|
+
/** Keys a referenced end may match: keys, plus DBML unique indexes (§11.17). */
|
|
503
|
+
function referenceableKeys(decl, doc) {
|
|
504
|
+
const keys = entityConstraints(decl, doc)
|
|
505
|
+
.filter((c) => c.kind === 'key')
|
|
506
|
+
.map((k) => k.fields);
|
|
507
|
+
for (const item of decl.body) {
|
|
508
|
+
if (item.kind !== 'IndexesBlock')
|
|
509
|
+
continue;
|
|
510
|
+
for (const e of item.entries) {
|
|
511
|
+
if (isPk(e.settings) || !hasFlag(e.settings, 'unique'))
|
|
512
|
+
continue;
|
|
513
|
+
keys.push(e.components.flatMap((c) => (c.kind === 'IndexPathComponent' ? [keyPathString(c.path)] : [])));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return keys;
|
|
517
|
+
}
|
|
518
|
+
function checkReferencedKeys(doc, severity) {
|
|
519
|
+
const diagnostics = [];
|
|
520
|
+
const index = entityIndex(doc);
|
|
521
|
+
const project = projectTarget(doc);
|
|
522
|
+
const exemptTarget = (e) => {
|
|
523
|
+
const t = effectiveTarget(e.placed, project);
|
|
524
|
+
return t !== undefined && !isRelationalTarget(t);
|
|
525
|
+
};
|
|
526
|
+
const check = (source, operator, target, span) => {
|
|
527
|
+
if (operator === '<>')
|
|
528
|
+
return;
|
|
529
|
+
const s = resolveEnd(source, index);
|
|
530
|
+
const t = resolveEnd(target, index);
|
|
531
|
+
if (!s || !t || s === 'entity-level' || t === 'entity-level')
|
|
532
|
+
return;
|
|
533
|
+
if (exemptTarget(s.entity) || exemptTarget(t.entity))
|
|
534
|
+
return;
|
|
535
|
+
const sides = operator === '>' ? [t] : operator === '<' ? [s] : [s, t];
|
|
536
|
+
const withKeys = sides
|
|
537
|
+
.map((side) => ({ side, keys: referenceableKeys(side.entity.decl, doc) }))
|
|
538
|
+
.filter((x) => x.keys.length > 0);
|
|
539
|
+
if (withKeys.length === 0)
|
|
540
|
+
return; // no key declared: a tool MAY promote (§11.17)
|
|
541
|
+
if (withKeys.some((x) => x.keys.some((k) => sameSet(k, x.side.fields))))
|
|
542
|
+
return;
|
|
543
|
+
const first = withKeys[0];
|
|
544
|
+
const pk = primaryKey(first.side.entity.decl, doc);
|
|
545
|
+
const where = `${first.side.entity.decl.name}.${first.side.fields.length > 1 ? `(${first.side.fields.join(', ')})` : first.side.fields[0]}`;
|
|
546
|
+
diagnostics.push({
|
|
547
|
+
severity,
|
|
548
|
+
code: 'ref-target-not-key',
|
|
549
|
+
message: `Relationship references '${where}', which is not the primary key or a unique key of '${first.side.entity.decl.name}'` +
|
|
550
|
+
(pk ? ` (primary key: ${pk.fields.join(', ')}).` : '.') +
|
|
551
|
+
' Declare those fields unique, or reference a key.',
|
|
552
|
+
span,
|
|
553
|
+
});
|
|
554
|
+
};
|
|
555
|
+
const visitRef = (ref) => {
|
|
556
|
+
if (isForeignMaster(ref))
|
|
557
|
+
return;
|
|
558
|
+
check(ref.spec.source, ref.spec.operator, ref.spec.target, ref.span);
|
|
559
|
+
};
|
|
560
|
+
for (const s of doc.statements) {
|
|
561
|
+
if (s.kind === 'RefDeclaration')
|
|
562
|
+
visitRef(s);
|
|
563
|
+
}
|
|
564
|
+
// Inline refs on top-level fields.
|
|
565
|
+
for (const p of bodies(doc)) {
|
|
566
|
+
if (p.decl.kind !== 'EntityDeclaration')
|
|
567
|
+
continue;
|
|
568
|
+
const owner = p.container ? `${p.container.name}.${p.decl.name}` : p.decl.name;
|
|
569
|
+
for (const item of p.decl.body) {
|
|
570
|
+
if (item.kind !== 'FieldDeclaration' || hasForeignMasterFlag(item.settings))
|
|
571
|
+
continue;
|
|
572
|
+
for (const st of item.settings) {
|
|
573
|
+
if (st.name !== 'ref' || st.value?.kind !== 'RefValue')
|
|
574
|
+
continue;
|
|
575
|
+
const source = {
|
|
576
|
+
kind: 'RefEndpoint',
|
|
577
|
+
path: [...owner.split('.'), item.name].map((name) => ({ kind: 'PathField', name, span: item.span })),
|
|
578
|
+
span: item.span,
|
|
579
|
+
};
|
|
580
|
+
check(source, st.value.operator, st.value.target, st.span);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return diagnostics;
|
|
585
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ 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 { bodyConstraints, checkConstraints, entityConstraints, isRelationalTarget, keyPathString, markPrimaryKeyNotNull, primaryKey, } from './constraints.ts';
|
|
36
|
+
export type { CheckConstraint, Constraint, ConstraintSource, KeyConstraint } from './constraints.ts';
|
|
35
37
|
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
38
|
export type { RelationshipType, ConstraintType } from './relationships.ts';
|
|
37
39
|
export { SUPERTYPE_GROUP_VALUES, canonicalSupertypeGroupValue, checkSupertypeGroups, resolveSupertypeGroups, subtypeStrategy, supertypeChains, supertypeGroupSettings, } from './supertypes.ts';
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,7 @@ 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 { bodyConstraints, checkConstraints, entityConstraints, isRelationalTarget, keyPathString, markPrimaryKeyNotNull, primaryKey, } from "./constraints.js";
|
|
33
34
|
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
35
|
export { SUPERTYPE_GROUP_VALUES, canonicalSupertypeGroupValue, checkSupertypeGroups, resolveSupertypeGroups, subtypeStrategy, supertypeChains, supertypeGroupSettings, } from "./supertypes.js";
|
|
35
36
|
export { xdbmlLanguageConfig, xdbmlMonarchTokensProvider, } from "./monarch.js";
|
package/dist/keywords.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export declare const SCALAR_TYPES: readonly ["tinyint", "smallint", "mediumint",
|
|
|
40
40
|
export declare const BSON_TYPES: readonly ["objectid", "decimal128", "bindata", "minkey", "maxkey", "symbol", "regex", "long", "double"];
|
|
41
41
|
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
42
|
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"];
|
|
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", "constraints", "replication", "location", "default_charset", "cloned_at"];
|
|
44
44
|
export declare const GRANULARITY_VALUES: readonly ["year", "quarter", "month", "week", "day", "hour", "minute", "second", "millisecond", "microsecond", "nanosecond"];
|
|
45
45
|
export declare const DIRECTIVE_KEYWORDS: readonly ["xdbml", "experimental"];
|
|
46
46
|
export declare const MODULE_KEYWORDS: readonly ["use", "reuse", "from", "as"];
|
package/dist/keywords.js
CHANGED
|
@@ -286,6 +286,7 @@ export const SETTING_KEYS = [
|
|
|
286
286
|
// Block keywords (entity-body or top-level)
|
|
287
287
|
'indexes',
|
|
288
288
|
'checks', // v0.2 §10: entity-level checks block
|
|
289
|
+
'constraints', // v0.6 §10: keys and checks block
|
|
289
290
|
// Container settings
|
|
290
291
|
'replication',
|
|
291
292
|
'location',
|
package/dist/name-resolver.d.ts
CHANGED
|
@@ -67,7 +67,7 @@ export interface SymbolEntry {
|
|
|
67
67
|
* Stable diagnostic code. Tooling can match on these to filter or style
|
|
68
68
|
* messages without parsing the human-readable text.
|
|
69
69
|
*/
|
|
70
|
-
export type DiagnosticCode = 'duplicate-declaration' | 'unresolved-type' | 'possible-type-typo' | 'unresolved-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';
|
|
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' | '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';
|
|
71
71
|
/**
|
|
72
72
|
* A single resolution diagnostic. Severity is currently always `error`,
|
|
73
73
|
* but the field is included to leave room for future warnings (e.g.,
|
package/dist/name-resolver.js
CHANGED
|
@@ -42,6 +42,7 @@ 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
46
|
/**
|
|
46
47
|
* Read-only handle on the collected symbol table.
|
|
47
48
|
*
|
|
@@ -228,6 +229,8 @@ export function resolveNames(doc) {
|
|
|
228
229
|
diagnostics.push(...checkRelationships(flat));
|
|
229
230
|
// Pass 4: supertype group rules (spec 12.8).
|
|
230
231
|
diagnostics.push(...checkSupertypeGroups(flat));
|
|
232
|
+
// Pass 5: constraints and referenced keys (spec §10.10, §11.17).
|
|
233
|
+
diagnostics.push(...checkConstraints(flat));
|
|
231
234
|
return { diagnostics, symbols };
|
|
232
235
|
}
|
|
233
236
|
/* -------------------------------------------------------------------------
|
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.
|
|
29
|
+
export declare const SUPPORTED_XDBML_VERSION = "0.6";
|
|
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 {
|
|
@@ -326,6 +326,8 @@ export declare class Parser {
|
|
|
326
326
|
private parseIndexComponent;
|
|
327
327
|
private parseChecks;
|
|
328
328
|
private parseCheckEntry;
|
|
329
|
+
private parseConstraints;
|
|
330
|
+
private parseKeyConstraintEntry;
|
|
329
331
|
private maybeSettingsBlock;
|
|
330
332
|
/**
|
|
331
333
|
* A single setting. Forms:
|
package/dist/parser.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* semantic-analysis pass. The grammar test cases the parser passes are
|
|
10
10
|
* the official xDBML example files in /examples.
|
|
11
11
|
*/
|
|
12
|
+
import { markPrimaryKeyNotNull } from "./constraints.js";
|
|
12
13
|
import { TokenKind, tokenize, } from "./lexer.js";
|
|
13
14
|
import { resolveImport, classifyModuleSource, ModuleSourceError } from "./module-resolver.js";
|
|
14
15
|
export class ParseError extends Error {
|
|
@@ -31,7 +32,7 @@ export class ParseError extends Error {
|
|
|
31
32
|
* declaring a later version is refused (spec 4.1) rather than parsed with
|
|
32
33
|
* semantics it does not have.
|
|
33
34
|
*/
|
|
34
|
-
export const SUPPORTED_XDBML_VERSION = '0.
|
|
35
|
+
export const SUPPORTED_XDBML_VERSION = '0.6';
|
|
35
36
|
/** Compare dotted version strings numerically: -1, 0 or 1. */
|
|
36
37
|
export function compareVersions(a, b) {
|
|
37
38
|
const pa = a.split('.').map((n) => Number(n) || 0);
|
|
@@ -191,13 +192,16 @@ export class Parser {
|
|
|
191
192
|
while (!this.check(TokenKind.EOF)) {
|
|
192
193
|
statements.push(this.parseTopLevelStatement());
|
|
193
194
|
}
|
|
194
|
-
|
|
195
|
+
const doc = {
|
|
195
196
|
kind: 'XDbmlDocument',
|
|
196
197
|
version,
|
|
197
198
|
experimental,
|
|
198
199
|
statements,
|
|
199
200
|
span: this.spanFrom(start),
|
|
200
201
|
};
|
|
202
|
+
// v0.6 §10.4: primary key fields are not null in the AST.
|
|
203
|
+
markPrimaryKeyNotNull(doc);
|
|
204
|
+
return doc;
|
|
201
205
|
}
|
|
202
206
|
/* ----- version & experimental ----- */
|
|
203
207
|
parseVersionDeclaration() {
|
|
@@ -487,6 +491,11 @@ export class Parser {
|
|
|
487
491
|
else if (k === 'checks') {
|
|
488
492
|
body.push(this.parseChecks());
|
|
489
493
|
}
|
|
494
|
+
else if (k === 'constraints' && this.peek(1).kind === TokenKind.LBrace) {
|
|
495
|
+
// v0.6 §10.1: a keyword only when `{` follows, so a field may keep
|
|
496
|
+
// the name `constraints`.
|
|
497
|
+
body.push(this.parseConstraints());
|
|
498
|
+
}
|
|
490
499
|
else if (k === 'records') {
|
|
491
500
|
body.push(this.parseRecordsBlock());
|
|
492
501
|
}
|
|
@@ -1932,17 +1941,71 @@ export class Parser {
|
|
|
1932
1941
|
parseCheckEntry() {
|
|
1933
1942
|
const start = this.peek().start;
|
|
1934
1943
|
const exprTok = this.peek();
|
|
1935
|
-
|
|
1936
|
-
|
|
1944
|
+
// v0.6 §10.5: single (or triple) quotes are the standard form and
|
|
1945
|
+
// backticks an alias. The resolver rejects the quoted form in documents
|
|
1946
|
+
// declaring an earlier version.
|
|
1947
|
+
const quoted = exprTok.kind === TokenKind.StringLiteral || exprTok.kind === TokenKind.MultilineString;
|
|
1948
|
+
if (exprTok.kind !== TokenKind.ExpressionLiteral && !quoted) {
|
|
1949
|
+
throw new ParseError(`Expected a check expression in quotes or backticks, got ${exprTok.kind} ${JSON.stringify(exprTok.text)}`, exprTok.start);
|
|
1937
1950
|
}
|
|
1938
1951
|
this.advance();
|
|
1939
|
-
// The expression is opaque to xDBML
|
|
1940
|
-
//
|
|
1952
|
+
// The expression is opaque to xDBML (spec §10.5). The token value
|
|
1953
|
+
// already has the delimiters stripped.
|
|
1941
1954
|
const expression = exprTok.value ?? '';
|
|
1942
1955
|
const settings = this.maybeSettingsBlock();
|
|
1943
1956
|
return {
|
|
1944
1957
|
kind: 'CheckEntry',
|
|
1945
1958
|
expression,
|
|
1959
|
+
delimiter: quoted ? 'quote' : 'backtick',
|
|
1960
|
+
settings,
|
|
1961
|
+
span: this.spanFrom(start),
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
/* ----- constraints block (v0.6 §10) ----- */
|
|
1965
|
+
parseConstraints() {
|
|
1966
|
+
const start = this.peek().start;
|
|
1967
|
+
this.advance(); // constraints
|
|
1968
|
+
this.expect(TokenKind.LBrace, "Expected '{' after constraints");
|
|
1969
|
+
const entries = [];
|
|
1970
|
+
while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
|
|
1971
|
+
const t = this.peek();
|
|
1972
|
+
if (t.kind === TokenKind.StringLiteral ||
|
|
1973
|
+
t.kind === TokenKind.MultilineString ||
|
|
1974
|
+
t.kind === TokenKind.ExpressionLiteral) {
|
|
1975
|
+
entries.push(this.parseCheckEntry());
|
|
1976
|
+
}
|
|
1977
|
+
else {
|
|
1978
|
+
entries.push(this.parseKeyConstraintEntry());
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
this.expect(TokenKind.RBrace, "Expected '}' closing constraints");
|
|
1982
|
+
return {
|
|
1983
|
+
kind: 'ConstraintsBlock',
|
|
1984
|
+
entries,
|
|
1985
|
+
span: this.spanFrom(start),
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
parseKeyConstraintEntry() {
|
|
1989
|
+
const start = this.peek().start;
|
|
1990
|
+
const fields = [];
|
|
1991
|
+
let parenthesized = false;
|
|
1992
|
+
if (this.check(TokenKind.LParen)) {
|
|
1993
|
+
parenthesized = true;
|
|
1994
|
+
this.advance(); // (
|
|
1995
|
+
fields.push(this.parsePathSegments());
|
|
1996
|
+
while (this.match(TokenKind.Comma)) {
|
|
1997
|
+
fields.push(this.parsePathSegments());
|
|
1998
|
+
}
|
|
1999
|
+
this.expect(TokenKind.RParen, "Expected ')' closing the fields of a key");
|
|
2000
|
+
}
|
|
2001
|
+
else {
|
|
2002
|
+
fields.push(this.parsePathSegments());
|
|
2003
|
+
}
|
|
2004
|
+
const settings = this.maybeSettingsBlock();
|
|
2005
|
+
return {
|
|
2006
|
+
kind: 'KeyConstraintEntry',
|
|
2007
|
+
fields,
|
|
2008
|
+
parenthesized,
|
|
1946
2009
|
settings,
|
|
1947
2010
|
span: this.spanFrom(start),
|
|
1948
2011
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xdbml/parse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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",
|