@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
package/dist/keywords.js
CHANGED
|
@@ -161,6 +161,15 @@ export const BSON_TYPES = [
|
|
|
161
161
|
* are left out for that reason. The list is a convenience, not a
|
|
162
162
|
* vocabulary: a name missing from it is still a valid type.
|
|
163
163
|
* ----------------------------------------------------------------------- */
|
|
164
|
+
/* -------------------------------------------------------------------------
|
|
165
|
+
* Entity-body block keywords (TextMate only)
|
|
166
|
+
*
|
|
167
|
+
* The words that open a block inside an entity body. They also appear in
|
|
168
|
+
* SETTING_KEYS above, which colors them in Monaco; the TextMate grammar
|
|
169
|
+
* colors setting keys only inside `[ ]`, so it matches these separately,
|
|
170
|
+
* and only when `{` follows, since a field may carry one of these names.
|
|
171
|
+
* ----------------------------------------------------------------------- */
|
|
172
|
+
export const BLOCK_KEYWORDS = ['indexes', 'checks', 'constraints', 'records'];
|
|
164
173
|
export const TARGET_NATIVE_TYPES = [
|
|
165
174
|
// Oracle
|
|
166
175
|
'number', 'binary_float', 'binary_double', 'clob', 'nclob', 'bfile',
|
|
@@ -286,6 +295,7 @@ export const SETTING_KEYS = [
|
|
|
286
295
|
// Block keywords (entity-body or top-level)
|
|
287
296
|
'indexes',
|
|
288
297
|
'checks', // v0.2 §10: entity-level checks block
|
|
298
|
+
'constraints', // v0.6 §10: keys and checks block
|
|
289
299
|
// Container settings
|
|
290
300
|
'replication',
|
|
291
301
|
'location',
|
package/dist/monarch.js
CHANGED
|
@@ -156,6 +156,10 @@ export const xdbmlMonarchTokensProvider = {
|
|
|
156
156
|
[/#[0-9A-Fa-f]{3,8}\b/, 'number.hex'],
|
|
157
157
|
// x_ custom property identifiers
|
|
158
158
|
[/\bx_[a-zA-Z0-9_]+/, 'identifier.custom-property'],
|
|
159
|
+
// Spec §3.10 (v0.6.1): `Note` starts a note only before ':' or '{',
|
|
160
|
+
// or at the top level before a name and '{'. Anywhere else it names a
|
|
161
|
+
// field, so it takes the identifier color like any other field name.
|
|
162
|
+
[/note(?![\w$])(?!\s*[:{])(?!\s+(?:[A-Za-z_][\w$]*|"[^"]*")\s*\{)/, 'identifier'],
|
|
159
163
|
// Identifiers and keyword recognition.
|
|
160
164
|
// The parser is the authority on keyword vs identifier disambiguation;
|
|
161
165
|
// Monarch does coarse highlighting based on lowercase comparison.
|
package/dist/name-resolver.d.ts
CHANGED
|
@@ -67,7 +67,7 @@ export interface SymbolEntry {
|
|
|
67
67
|
* Stable diagnostic code. Tooling can match on these to filter or style
|
|
68
68
|
* messages without parsing the human-readable text.
|
|
69
69
|
*/
|
|
70
|
-
export type DiagnosticCode = 'duplicate-declaration' | 'unresolved-type' | 'possible-type-typo' | 'unresolved-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' | 'duplicate-unique-key' | 'null-in-primary-key' | 'duplicate-constraints-block' | 'ref-target-not-key' | 'unresolved-entity' | 'unresolved-field' | 'unresolved-partial' | 'unresolved-tablegroup-member' | 'unresolved-records-entity' | 'unresolved-records-column' | 'empty-import' | 'invalid-nested-path' | 'foreign-master-composite' | 'foreign-master-duplicate-child' | 'foreign-master-without-ref' | 'construct-requires-version' | 'ambiguous-ref-endpoint' | 'invalid-constraint-type' | 'constraint-type-on-foreign-master' | 'invalid-undirected' | 'entity-level-many-to-many' | 'missing-supertype' | 'unresolved-supertype-group-member' | 'invalid-supertype-group-value' | 'duplicate-subtype' | 'supertype-is-subtype' | 'subtype-in-multiple-groups' | 'supertype-cycle' | 'supertype-attribute-redeclared' | 'discriminator-on-overlapping-group' | 'merge-without-roll-up' | 'empty-supertype-group' | 'source-query-in-settings' | 'duplicate-source-query';
|
|
71
71
|
/**
|
|
72
72
|
* A single resolution diagnostic. Severity is currently always `error`,
|
|
73
73
|
* but the field is included to leave room for future warnings (e.g.,
|
package/dist/name-resolver.js
CHANGED
|
@@ -42,6 +42,8 @@ import { SCALAR_TYPES, BSON_TYPES } from "./keywords.js";
|
|
|
42
42
|
import { flatten } from "./module-resolver.js";
|
|
43
43
|
import { checkRelationships } from "./relationships.js";
|
|
44
44
|
import { checkSupertypeGroups } from "./supertypes.js";
|
|
45
|
+
import { checkConstraints } from "./constraints.js";
|
|
46
|
+
import { checkViews } from "./views.js";
|
|
45
47
|
/**
|
|
46
48
|
* Read-only handle on the collected symbol table.
|
|
47
49
|
*
|
|
@@ -228,6 +230,10 @@ export function resolveNames(doc) {
|
|
|
228
230
|
diagnostics.push(...checkRelationships(flat));
|
|
229
231
|
// Pass 4: supertype group rules (spec 12.8).
|
|
230
232
|
diagnostics.push(...checkSupertypeGroups(flat));
|
|
233
|
+
// Pass 5: constraints and referenced keys (spec §10.10, §11.17).
|
|
234
|
+
diagnostics.push(...checkConstraints(flat));
|
|
235
|
+
// Pass 6: view source queries (spec §14.7).
|
|
236
|
+
diagnostics.push(...checkViews(flat));
|
|
231
237
|
return { diagnostics, symbols };
|
|
232
238
|
}
|
|
233
239
|
/* -------------------------------------------------------------------------
|
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.1";
|
|
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 {
|
|
@@ -64,6 +64,30 @@ export declare class Parser {
|
|
|
64
64
|
private check;
|
|
65
65
|
private match;
|
|
66
66
|
private expect;
|
|
67
|
+
/**
|
|
68
|
+
* Skip the commas and semicolons of a list body. In a list body a comma
|
|
69
|
+
* or a semicolon may stand between items, before the first and after the
|
|
70
|
+
* last, any number of times, with no meaning of its own (spec §3.9).
|
|
71
|
+
*/
|
|
72
|
+
private skipListSeparators;
|
|
73
|
+
/**
|
|
74
|
+
* Whether the current token starts a note element: `Note` followed by
|
|
75
|
+
* `:` or `{`. Anywhere else `note` names a field (spec §3.10), as in
|
|
76
|
+
* DBML; a field declaration never has `:` or `{` right after its name.
|
|
77
|
+
*/
|
|
78
|
+
private atNoteElement;
|
|
79
|
+
/**
|
|
80
|
+
* Whether the current token is the body keyword `word` opening its block,
|
|
81
|
+
* that is, followed by `{` (spec §3.10). Before a type, the same word
|
|
82
|
+
* names a field.
|
|
83
|
+
*/
|
|
84
|
+
private atBlockKeyword;
|
|
85
|
+
/**
|
|
86
|
+
* A line such as `materialized: true` in a body is a setting written in
|
|
87
|
+
* the wrong place: a field declaration never has `:` after its name.
|
|
88
|
+
* Name the brackets in the error rather than reporting a missing type.
|
|
89
|
+
*/
|
|
90
|
+
private rejectSettingInBody;
|
|
67
91
|
private spanFrom;
|
|
68
92
|
parseDocument(): XDbmlDocument;
|
|
69
93
|
private parseVersionDeclaration;
|
|
@@ -326,6 +350,8 @@ export declare class Parser {
|
|
|
326
350
|
private parseIndexComponent;
|
|
327
351
|
private parseChecks;
|
|
328
352
|
private parseCheckEntry;
|
|
353
|
+
private parseConstraints;
|
|
354
|
+
private parseKeyConstraintEntry;
|
|
329
355
|
private maybeSettingsBlock;
|
|
330
356
|
/**
|
|
331
357
|
* 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.1';
|
|
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);
|
|
@@ -168,6 +169,50 @@ export class Parser {
|
|
|
168
169
|
const t = this.peek();
|
|
169
170
|
throw new ParseError(`${msg} (got ${t.kind} ${JSON.stringify(t.text)})`, t.start);
|
|
170
171
|
}
|
|
172
|
+
/* ----- list bodies and body keywords (spec §3.9, §3.10, v0.6.1) ----- */
|
|
173
|
+
/**
|
|
174
|
+
* Skip the commas and semicolons of a list body. In a list body a comma
|
|
175
|
+
* or a semicolon may stand between items, before the first and after the
|
|
176
|
+
* last, any number of times, with no meaning of its own (spec §3.9).
|
|
177
|
+
*/
|
|
178
|
+
skipListSeparators() {
|
|
179
|
+
while (this.check(TokenKind.Comma) || this.check(TokenKind.Semicolon))
|
|
180
|
+
this.advance();
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Whether the current token starts a note element: `Note` followed by
|
|
184
|
+
* `:` or `{`. Anywhere else `note` names a field (spec §3.10), as in
|
|
185
|
+
* DBML; a field declaration never has `:` or `{` right after its name.
|
|
186
|
+
*/
|
|
187
|
+
atNoteElement() {
|
|
188
|
+
if (!isKw(this.peek(), 'note'))
|
|
189
|
+
return false;
|
|
190
|
+
const next = this.peek(1).kind;
|
|
191
|
+
return next === TokenKind.Colon || next === TokenKind.LBrace;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Whether the current token is the body keyword `word` opening its block,
|
|
195
|
+
* that is, followed by `{` (spec §3.10). Before a type, the same word
|
|
196
|
+
* names a field.
|
|
197
|
+
*/
|
|
198
|
+
atBlockKeyword(word) {
|
|
199
|
+
return isKw(this.peek(), word) && this.peek(1).kind === TokenKind.LBrace;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* A line such as `materialized: true` in a body is a setting written in
|
|
203
|
+
* the wrong place: a field declaration never has `:` after its name.
|
|
204
|
+
* Name the brackets in the error rather than reporting a missing type.
|
|
205
|
+
*/
|
|
206
|
+
rejectSettingInBody(construct, name) {
|
|
207
|
+
const t = this.peek();
|
|
208
|
+
if (t.kind !== TokenKind.Identifier || this.peek(1).kind !== TokenKind.Colon)
|
|
209
|
+
return;
|
|
210
|
+
const where = construct === 'View'
|
|
211
|
+
? 'The body of a View holds source_query:, fields and Note: (spec §14.1, §14.2).'
|
|
212
|
+
: `The body of ${construct === 'Entity' ? 'an' : 'a'} ${construct} holds fields, blocks and Note:.`;
|
|
213
|
+
throw new ParseError(`'${t.text}:' is a setting: write it in the brackets after the ${construct} name, ` +
|
|
214
|
+
`as in ${construct} ${name} [${t.text}: ...] { ... }. ${where}`, t.start);
|
|
215
|
+
}
|
|
171
216
|
spanFrom(start) {
|
|
172
217
|
// span end = end-position of the previously consumed token if any
|
|
173
218
|
const prev = this.idx > 0 ? this.tokens[this.idx - 1] : this.tokens[0];
|
|
@@ -191,13 +236,16 @@ export class Parser {
|
|
|
191
236
|
while (!this.check(TokenKind.EOF)) {
|
|
192
237
|
statements.push(this.parseTopLevelStatement());
|
|
193
238
|
}
|
|
194
|
-
|
|
239
|
+
const doc = {
|
|
195
240
|
kind: 'XDbmlDocument',
|
|
196
241
|
version,
|
|
197
242
|
experimental,
|
|
198
243
|
statements,
|
|
199
244
|
span: this.spanFrom(start),
|
|
200
245
|
};
|
|
246
|
+
// v0.6 §10.4: primary key fields are not null in the AST.
|
|
247
|
+
markPrimaryKeyNotNull(doc);
|
|
248
|
+
return doc;
|
|
201
249
|
}
|
|
202
250
|
/* ----- version & experimental ----- */
|
|
203
251
|
parseVersionDeclaration() {
|
|
@@ -205,14 +253,22 @@ export class Parser {
|
|
|
205
253
|
this.advance(); // xdbml
|
|
206
254
|
this.expect(TokenKind.Colon, "Expected ':' after 'xdbml'");
|
|
207
255
|
const numTok = this.expect(TokenKind.NumberLiteral, 'Expected version number');
|
|
208
|
-
|
|
209
|
-
|
|
256
|
+
// Spec §4: MAJOR.MINOR[.PATCH]. The lexer reads `0.6` as one number,
|
|
257
|
+
// so a patch number arrives as a dot and a second number. `xdbml: 0.6`
|
|
258
|
+
// compares as 0.6.0, so it stays valid under every 0.6.x parser.
|
|
259
|
+
let version = numTok.text;
|
|
260
|
+
if (this.check(TokenKind.Dot) && this.peek(1).kind === TokenKind.NumberLiteral) {
|
|
261
|
+
this.advance(); // .
|
|
262
|
+
version += `.${this.advance().text}`;
|
|
263
|
+
}
|
|
264
|
+
if (compareVersions(version, SUPPORTED_XDBML_VERSION) > 0) {
|
|
265
|
+
throw new ParseError(`This document declares 'xdbml: ${version}', which is newer than the ` +
|
|
210
266
|
`latest version this parser supports (${SUPPORTED_XDBML_VERSION}). ` +
|
|
211
267
|
'Use a newer parser, or declare a supported version.', numTok.start, 'unsupported-version');
|
|
212
268
|
}
|
|
213
269
|
return {
|
|
214
270
|
kind: 'VersionDeclaration',
|
|
215
|
-
version
|
|
271
|
+
version,
|
|
216
272
|
span: this.spanFrom(start),
|
|
217
273
|
};
|
|
218
274
|
}
|
|
@@ -279,7 +335,10 @@ export class Parser {
|
|
|
279
335
|
const name = this.parseIdentLikeName('project name');
|
|
280
336
|
this.expect(TokenKind.LBrace, "Expected '{' after Project name");
|
|
281
337
|
const body = [];
|
|
282
|
-
|
|
338
|
+
for (;;) {
|
|
339
|
+
this.skipListSeparators();
|
|
340
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
341
|
+
break;
|
|
283
342
|
// Project body is either an inline Note block or a setting line.
|
|
284
343
|
if (isKw(this.peek(), 'note')) {
|
|
285
344
|
body.push(this.parseNoteBlockOrSetting());
|
|
@@ -439,7 +498,7 @@ export class Parser {
|
|
|
439
498
|
}
|
|
440
499
|
const settings = this.maybeSettingsBlock();
|
|
441
500
|
this.expect(TokenKind.LBrace, "Expected '{' after entity name");
|
|
442
|
-
const body = this.parseEntityBody();
|
|
501
|
+
const body = this.parseEntityBody(canonEntityKw(kwTok.text), name);
|
|
443
502
|
this.expect(TokenKind.RBrace, "Expected '}' closing entity");
|
|
444
503
|
return {
|
|
445
504
|
kind: 'EntityDeclaration',
|
|
@@ -473,27 +532,37 @@ export class Parser {
|
|
|
473
532
|
}
|
|
474
533
|
return name;
|
|
475
534
|
}
|
|
476
|
-
parseEntityBody() {
|
|
535
|
+
parseEntityBody(construct, name) {
|
|
477
536
|
const body = [];
|
|
478
|
-
|
|
537
|
+
for (;;) {
|
|
538
|
+
this.skipListSeparators();
|
|
539
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
540
|
+
break;
|
|
479
541
|
const t = this.peek();
|
|
480
|
-
|
|
481
|
-
|
|
542
|
+
// Spec §3.10: each body keyword starts its element only when the
|
|
543
|
+
// token the element needs follows it; otherwise it names a field,
|
|
544
|
+
// as in DBML. (v0.6 §10.1 did this for `constraints`; v0.6.1 does it
|
|
545
|
+
// for `Note`, `indexes`, `checks` and `records`.)
|
|
546
|
+
if (this.atNoteElement()) {
|
|
482
547
|
body.push(this.parseNoteBlockOrSetting());
|
|
483
548
|
}
|
|
484
|
-
else if (
|
|
549
|
+
else if (this.atBlockKeyword('indexes')) {
|
|
485
550
|
body.push(this.parseIndexes());
|
|
486
551
|
}
|
|
487
|
-
else if (
|
|
552
|
+
else if (this.atBlockKeyword('checks')) {
|
|
488
553
|
body.push(this.parseChecks());
|
|
489
554
|
}
|
|
490
|
-
else if (
|
|
555
|
+
else if (this.atBlockKeyword('constraints')) {
|
|
556
|
+
body.push(this.parseConstraints());
|
|
557
|
+
}
|
|
558
|
+
else if (this.atBlockKeyword('records')) {
|
|
491
559
|
body.push(this.parseRecordsBlock());
|
|
492
560
|
}
|
|
493
561
|
else if (t.kind === TokenKind.Tilde) {
|
|
494
562
|
body.push(this.parsePartialInjection());
|
|
495
563
|
}
|
|
496
564
|
else {
|
|
565
|
+
this.rejectSettingInBody(construct, name);
|
|
497
566
|
body.push(this.parseFieldDeclaration());
|
|
498
567
|
}
|
|
499
568
|
}
|
|
@@ -983,10 +1052,12 @@ export class Parser {
|
|
|
983
1052
|
const kwTok = this.advance();
|
|
984
1053
|
this.expect(TokenKind.LBrace, "Expected '{' after object keyword");
|
|
985
1054
|
const fields = [];
|
|
986
|
-
|
|
1055
|
+
for (;;) {
|
|
1056
|
+
this.skipListSeparators();
|
|
1057
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
1058
|
+
break;
|
|
987
1059
|
const t = this.peek();
|
|
988
|
-
|
|
989
|
-
if (k === 'note') {
|
|
1060
|
+
if (this.atNoteElement()) {
|
|
990
1061
|
fields.push(this.parseNoteBlockOrSetting());
|
|
991
1062
|
}
|
|
992
1063
|
else if (t.kind === TokenKind.Tilde) {
|
|
@@ -1207,7 +1278,10 @@ export class Parser {
|
|
|
1207
1278
|
this.advance(); // oneOf | anyOf | allOf
|
|
1208
1279
|
this.expect(TokenKind.LBrace, `Expected '{' after ${flavor}`);
|
|
1209
1280
|
const alternatives = [];
|
|
1210
|
-
|
|
1281
|
+
for (;;) {
|
|
1282
|
+
this.skipListSeparators();
|
|
1283
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
1284
|
+
break;
|
|
1211
1285
|
alternatives.push(this.parsePolymorphicAlternative());
|
|
1212
1286
|
}
|
|
1213
1287
|
this.expect(TokenKind.RBrace, `Expected '}' closing ${flavor}`);
|
|
@@ -1240,10 +1314,12 @@ export class Parser {
|
|
|
1240
1314
|
if (this.check(TokenKind.LBrace)) {
|
|
1241
1315
|
this.advance();
|
|
1242
1316
|
fields = [];
|
|
1243
|
-
|
|
1317
|
+
for (;;) {
|
|
1318
|
+
this.skipListSeparators();
|
|
1319
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
1320
|
+
break;
|
|
1244
1321
|
const t = this.peek();
|
|
1245
|
-
|
|
1246
|
-
if (k === 'note') {
|
|
1322
|
+
if (this.atNoteElement()) {
|
|
1247
1323
|
fields.push(this.parseNoteBlockOrSetting());
|
|
1248
1324
|
}
|
|
1249
1325
|
else if (t.kind === TokenKind.Tilde) {
|
|
@@ -1374,10 +1450,12 @@ export class Parser {
|
|
|
1374
1450
|
finishObjectTypeDecl(start, name, settings) {
|
|
1375
1451
|
this.expect(TokenKind.LBrace, "Expected '{' after Type name");
|
|
1376
1452
|
const body = [];
|
|
1377
|
-
|
|
1453
|
+
for (;;) {
|
|
1454
|
+
this.skipListSeparators();
|
|
1455
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
1456
|
+
break;
|
|
1378
1457
|
const t = this.peek();
|
|
1379
|
-
|
|
1380
|
-
if (k === 'note') {
|
|
1458
|
+
if (this.atNoteElement()) {
|
|
1381
1459
|
body.push(this.parseNoteBlockOrSetting());
|
|
1382
1460
|
}
|
|
1383
1461
|
else if (t.kind === TokenKind.Tilde) {
|
|
@@ -1403,7 +1481,7 @@ export class Parser {
|
|
|
1403
1481
|
const name = this.parseIdentLikeName('edge name');
|
|
1404
1482
|
const settings = this.maybeSettingsBlock();
|
|
1405
1483
|
this.expect(TokenKind.LBrace, "Expected '{' after Edge settings");
|
|
1406
|
-
const body = this.parseEntityBody();
|
|
1484
|
+
const body = this.parseEntityBody('Edge', name);
|
|
1407
1485
|
this.expect(TokenKind.RBrace, "Expected '}' closing Edge");
|
|
1408
1486
|
return {
|
|
1409
1487
|
kind: 'EdgeDeclaration',
|
|
@@ -1421,16 +1499,22 @@ export class Parser {
|
|
|
1421
1499
|
const settings = this.maybeSettingsBlock();
|
|
1422
1500
|
this.expect(TokenKind.LBrace, "Expected '{' after View name");
|
|
1423
1501
|
const body = [];
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1502
|
+
for (;;) {
|
|
1503
|
+
this.skipListSeparators();
|
|
1504
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
1505
|
+
break;
|
|
1506
|
+
// Spec §3.10: `Note` and `source_query` start their elements only
|
|
1507
|
+
// before `:` (or `{` for a note); otherwise they name a field.
|
|
1508
|
+
if (this.atNoteElement()) {
|
|
1428
1509
|
body.push(this.parseNoteBlockOrSetting());
|
|
1429
1510
|
}
|
|
1430
|
-
else if (
|
|
1511
|
+
else if (isKw(this.peek(), 'source_query') && this.peek(1).kind === TokenKind.Colon) {
|
|
1431
1512
|
body.push(this.parseSourceQueryItem());
|
|
1432
1513
|
}
|
|
1433
1514
|
else {
|
|
1515
|
+
// Spec §14.2: `materialized: true` and the other settings go in
|
|
1516
|
+
// the brackets, never in the body.
|
|
1517
|
+
this.rejectSettingInBody('View', name);
|
|
1434
1518
|
body.push(this.parseFieldDeclaration());
|
|
1435
1519
|
}
|
|
1436
1520
|
}
|
|
@@ -1471,7 +1555,10 @@ export class Parser {
|
|
|
1471
1555
|
}
|
|
1472
1556
|
this.expect(TokenKind.LBrace, "Expected '{' after enum name");
|
|
1473
1557
|
const values = [];
|
|
1474
|
-
|
|
1558
|
+
for (;;) {
|
|
1559
|
+
this.skipListSeparators();
|
|
1560
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
1561
|
+
break;
|
|
1475
1562
|
const vStart = this.peek().start;
|
|
1476
1563
|
const vt = this.peek();
|
|
1477
1564
|
let vname;
|
|
@@ -1745,7 +1832,7 @@ export class Parser {
|
|
|
1745
1832
|
const name = this.parseIdentLikeName('TablePartial name');
|
|
1746
1833
|
const settings = this.maybeSettingsBlock();
|
|
1747
1834
|
this.expect(TokenKind.LBrace, "Expected '{' after TablePartial name");
|
|
1748
|
-
const body = this.parseEntityBody();
|
|
1835
|
+
const body = this.parseEntityBody('TablePartial', name);
|
|
1749
1836
|
this.expect(TokenKind.RBrace, "Expected '}' closing TablePartial");
|
|
1750
1837
|
return {
|
|
1751
1838
|
kind: 'TablePartialDeclaration',
|
|
@@ -1860,6 +1947,14 @@ export class Parser {
|
|
|
1860
1947
|
this.expect(TokenKind.LBrace, "Expected '{' after indexes");
|
|
1861
1948
|
const entries = [];
|
|
1862
1949
|
while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
|
|
1950
|
+
// Spec §3.9: `indexes` is not a list body. `email, name` may be meant
|
|
1951
|
+
// as the composite index `(email, name)`, so a separator between
|
|
1952
|
+
// entries stays an error rather than declaring two indexes.
|
|
1953
|
+
const t = this.peek();
|
|
1954
|
+
if (t.kind === TokenKind.Comma || t.kind === TokenKind.Semicolon) {
|
|
1955
|
+
throw new ParseError(`Unexpected '${t.text}' in indexes: write one index per line, and a composite ` +
|
|
1956
|
+
'index in parentheses, as in (email, name) (spec §3.9).', t.start);
|
|
1957
|
+
}
|
|
1863
1958
|
entries.push(this.parseIndexEntry());
|
|
1864
1959
|
}
|
|
1865
1960
|
this.expect(TokenKind.RBrace, "Expected '}' closing indexes");
|
|
@@ -1919,7 +2014,10 @@ export class Parser {
|
|
|
1919
2014
|
this.advance(); // checks
|
|
1920
2015
|
this.expect(TokenKind.LBrace, "Expected '{' after checks");
|
|
1921
2016
|
const entries = [];
|
|
1922
|
-
|
|
2017
|
+
for (;;) {
|
|
2018
|
+
this.skipListSeparators();
|
|
2019
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
2020
|
+
break;
|
|
1923
2021
|
entries.push(this.parseCheckEntry());
|
|
1924
2022
|
}
|
|
1925
2023
|
this.expect(TokenKind.RBrace, "Expected '}' closing checks");
|
|
@@ -1932,17 +2030,74 @@ export class Parser {
|
|
|
1932
2030
|
parseCheckEntry() {
|
|
1933
2031
|
const start = this.peek().start;
|
|
1934
2032
|
const exprTok = this.peek();
|
|
1935
|
-
|
|
1936
|
-
|
|
2033
|
+
// v0.6 §10.5: single (or triple) quotes are the standard form and
|
|
2034
|
+
// backticks an alias. The resolver rejects the quoted form in documents
|
|
2035
|
+
// declaring an earlier version.
|
|
2036
|
+
const quoted = exprTok.kind === TokenKind.StringLiteral || exprTok.kind === TokenKind.MultilineString;
|
|
2037
|
+
if (exprTok.kind !== TokenKind.ExpressionLiteral && !quoted) {
|
|
2038
|
+
throw new ParseError(`Expected a check expression in quotes or backticks, got ${exprTok.kind} ${JSON.stringify(exprTok.text)}`, exprTok.start);
|
|
1937
2039
|
}
|
|
1938
2040
|
this.advance();
|
|
1939
|
-
// The expression is opaque to xDBML
|
|
1940
|
-
//
|
|
2041
|
+
// The expression is opaque to xDBML (spec §10.5). The token value
|
|
2042
|
+
// already has the delimiters stripped.
|
|
1941
2043
|
const expression = exprTok.value ?? '';
|
|
1942
2044
|
const settings = this.maybeSettingsBlock();
|
|
1943
2045
|
return {
|
|
1944
2046
|
kind: 'CheckEntry',
|
|
1945
2047
|
expression,
|
|
2048
|
+
delimiter: quoted ? 'quote' : 'backtick',
|
|
2049
|
+
settings,
|
|
2050
|
+
span: this.spanFrom(start),
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
/* ----- constraints block (v0.6 §10) ----- */
|
|
2054
|
+
parseConstraints() {
|
|
2055
|
+
const start = this.peek().start;
|
|
2056
|
+
this.advance(); // constraints
|
|
2057
|
+
this.expect(TokenKind.LBrace, "Expected '{' after constraints");
|
|
2058
|
+
const entries = [];
|
|
2059
|
+
for (;;) {
|
|
2060
|
+
this.skipListSeparators();
|
|
2061
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
2062
|
+
break;
|
|
2063
|
+
const t = this.peek();
|
|
2064
|
+
if (t.kind === TokenKind.StringLiteral ||
|
|
2065
|
+
t.kind === TokenKind.MultilineString ||
|
|
2066
|
+
t.kind === TokenKind.ExpressionLiteral) {
|
|
2067
|
+
entries.push(this.parseCheckEntry());
|
|
2068
|
+
}
|
|
2069
|
+
else {
|
|
2070
|
+
entries.push(this.parseKeyConstraintEntry());
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
this.expect(TokenKind.RBrace, "Expected '}' closing constraints");
|
|
2074
|
+
return {
|
|
2075
|
+
kind: 'ConstraintsBlock',
|
|
2076
|
+
entries,
|
|
2077
|
+
span: this.spanFrom(start),
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
parseKeyConstraintEntry() {
|
|
2081
|
+
const start = this.peek().start;
|
|
2082
|
+
const fields = [];
|
|
2083
|
+
let parenthesized = false;
|
|
2084
|
+
if (this.check(TokenKind.LParen)) {
|
|
2085
|
+
parenthesized = true;
|
|
2086
|
+
this.advance(); // (
|
|
2087
|
+
fields.push(this.parsePathSegments());
|
|
2088
|
+
while (this.match(TokenKind.Comma)) {
|
|
2089
|
+
fields.push(this.parsePathSegments());
|
|
2090
|
+
}
|
|
2091
|
+
this.expect(TokenKind.RParen, "Expected ')' closing the fields of a key");
|
|
2092
|
+
}
|
|
2093
|
+
else {
|
|
2094
|
+
fields.push(this.parsePathSegments());
|
|
2095
|
+
}
|
|
2096
|
+
const settings = this.maybeSettingsBlock();
|
|
2097
|
+
return {
|
|
2098
|
+
kind: 'KeyConstraintEntry',
|
|
2099
|
+
fields,
|
|
2100
|
+
parenthesized,
|
|
1946
2101
|
settings,
|
|
1947
2102
|
span: this.spanFrom(start),
|
|
1948
2103
|
};
|
package/dist/views.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Views (spec §14): the source query and the checks of §14.7.
|
|
3
|
+
*
|
|
4
|
+
* A View's source query is a body element, `source_query: '...'`, at most
|
|
5
|
+
* once (§14.3). The grammar of v0.6.0 also accepted `source_query` in the
|
|
6
|
+
* settings brackets; since v0.6.1 that form is not the view's source query
|
|
7
|
+
* and draws a warning, so a document valid under v0.6.0 stays valid.
|
|
8
|
+
*
|
|
9
|
+
* `viewSourceQuery()` is the normalized AST of §28.7: the one source query
|
|
10
|
+
* of a View, the first `source_query:` element of its body. Consumers such
|
|
11
|
+
* as the playground inspector read it rather than walking the body.
|
|
12
|
+
*/
|
|
13
|
+
import type { SourceQueryItem, ViewDeclaration, XDbmlDocument } from './ast.ts';
|
|
14
|
+
import type { Diagnostic } from './name-resolver.ts';
|
|
15
|
+
/** The source query of a View (spec §14.3, §28.7), or undefined when it declares none. */
|
|
16
|
+
export declare function viewSourceQuery(view: ViewDeclaration): SourceQueryItem | undefined;
|
|
17
|
+
/** The warnings of spec §14.7. Both concern forms v0.6.0 accepted. */
|
|
18
|
+
export declare function checkViews(doc: XDbmlDocument): Diagnostic[];
|
package/dist/views.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Views (spec §14): the source query and the checks of §14.7.
|
|
3
|
+
*
|
|
4
|
+
* A View's source query is a body element, `source_query: '...'`, at most
|
|
5
|
+
* once (§14.3). The grammar of v0.6.0 also accepted `source_query` in the
|
|
6
|
+
* settings brackets; since v0.6.1 that form is not the view's source query
|
|
7
|
+
* and draws a warning, so a document valid under v0.6.0 stays valid.
|
|
8
|
+
*
|
|
9
|
+
* `viewSourceQuery()` is the normalized AST of §28.7: the one source query
|
|
10
|
+
* of a View, the first `source_query:` element of its body. Consumers such
|
|
11
|
+
* as the playground inspector read it rather than walking the body.
|
|
12
|
+
*/
|
|
13
|
+
/** The source query of a View (spec §14.3, §28.7), or undefined when it declares none. */
|
|
14
|
+
export function viewSourceQuery(view) {
|
|
15
|
+
for (const item of view.body) {
|
|
16
|
+
if (item.kind === 'SourceQueryItem')
|
|
17
|
+
return item;
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
function views(doc) {
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const stmt of doc.statements) {
|
|
24
|
+
if (stmt.kind === 'ViewDeclaration')
|
|
25
|
+
out.push(stmt);
|
|
26
|
+
else if (stmt.kind === 'ContainerDeclaration') {
|
|
27
|
+
for (const item of stmt.body) {
|
|
28
|
+
if (item.kind === 'ViewDeclaration')
|
|
29
|
+
out.push(item);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
/** The warnings of spec §14.7. Both concern forms v0.6.0 accepted. */
|
|
36
|
+
export function checkViews(doc) {
|
|
37
|
+
const diagnostics = [];
|
|
38
|
+
for (const view of views(doc)) {
|
|
39
|
+
for (const s of view.settings) {
|
|
40
|
+
if (s.name !== 'source_query')
|
|
41
|
+
continue;
|
|
42
|
+
diagnostics.push({
|
|
43
|
+
severity: 'warning',
|
|
44
|
+
code: 'source-query-in-settings',
|
|
45
|
+
message: `View '${view.name}' writes source_query in its settings, where it is not the view's ` +
|
|
46
|
+
"source query. Move it into the body: View name { source_query: '...' ... } (spec §14.3).",
|
|
47
|
+
span: s.span,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const queries = view.body.filter((b) => b.kind === 'SourceQueryItem');
|
|
51
|
+
for (const extra of queries.slice(1)) {
|
|
52
|
+
diagnostics.push({
|
|
53
|
+
severity: 'warning',
|
|
54
|
+
code: 'duplicate-source-query',
|
|
55
|
+
message: `View '${view.name}' declares a second source query; the first one is the view's ` +
|
|
56
|
+
'source query (spec §14.3).',
|
|
57
|
+
span: extra.span,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return diagnostics;
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xdbml/parse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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",
|