@xdbml/parse 0.6.0 → 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/constraints.js +49 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/keywords.d.ts +1 -0
- package/dist/keywords.js +9 -0
- package/dist/monarch.js +4 -0
- package/dist/name-resolver.d.ts +1 -1
- package/dist/name-resolver.js +3 -0
- package/dist/parser.d.ts +25 -1
- package/dist/parser.js +128 -36
- package/dist/views.d.ts +18 -0
- package/dist/views.js +62 -0
- package/package.json +1 -1
package/dist/constraints.js
CHANGED
|
@@ -437,17 +437,38 @@ export function checkConstraints(doc) {
|
|
|
437
437
|
}
|
|
438
438
|
}
|
|
439
439
|
// One primary key, declared in one place (§10.4). Inline [pk] fields
|
|
440
|
-
// count as one declaration.
|
|
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.
|
|
441
443
|
const own = bodyConstraints(decl.body, v06);
|
|
442
444
|
const pks = own.filter((c) => c.kind === 'key' && c.keyKind === 'primary');
|
|
443
445
|
for (const extra of pks.slice(1)) {
|
|
446
|
+
const first = pks[0];
|
|
444
447
|
diagnostics.push({
|
|
445
448
|
severity: olderSeverity,
|
|
446
449
|
code: 'duplicate-primary-key',
|
|
447
|
-
message:
|
|
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.`,
|
|
448
454
|
span: extra.span,
|
|
449
455
|
});
|
|
450
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
|
+
}
|
|
451
472
|
// `null` on a primary key field (§10.4).
|
|
452
473
|
for (const pk of pks.slice(0, 1)) {
|
|
453
474
|
for (const dotted of pk.fields) {
|
|
@@ -467,6 +488,32 @@ export function checkConstraints(doc) {
|
|
|
467
488
|
diagnostics.push(...checkReferencedKeys(doc, olderSeverity));
|
|
468
489
|
return diagnostics;
|
|
469
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
|
+
}
|
|
470
517
|
function entityIndex(doc) {
|
|
471
518
|
const m = new Map();
|
|
472
519
|
const bare = new Map();
|
package/dist/index.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ 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';
|
|
35
36
|
export { bodyConstraints, checkConstraints, entityConstraints, isRelationalTarget, keyPathString, markPrimaryKeyNotNull, primaryKey, } from './constraints.ts';
|
|
36
37
|
export type { CheckConstraint, Constraint, ConstraintSource, KeyConstraint } from './constraints.ts';
|
|
37
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';
|
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 { checkViews, viewSourceQuery } from "./views.js";
|
|
33
34
|
export { bodyConstraints, checkConstraints, entityConstraints, isRelationalTarget, keyPathString, markPrimaryKeyNotNull, primaryKey, } from "./constraints.js";
|
|
34
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";
|
|
35
36
|
export { SUPERTYPE_GROUP_VALUES, canonicalSupertypeGroupValue, checkSupertypeGroups, resolveSupertypeGroups, subtypeStrategy, supertypeChains, supertypeGroupSettings, } from "./supertypes.js";
|
package/dist/keywords.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ 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
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"];
|
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',
|
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-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';
|
|
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
|
@@ -43,6 +43,7 @@ import { flatten } from "./module-resolver.js";
|
|
|
43
43
|
import { checkRelationships } from "./relationships.js";
|
|
44
44
|
import { checkSupertypeGroups } from "./supertypes.js";
|
|
45
45
|
import { checkConstraints } from "./constraints.js";
|
|
46
|
+
import { checkViews } from "./views.js";
|
|
46
47
|
/**
|
|
47
48
|
* Read-only handle on the collected symbol table.
|
|
48
49
|
*
|
|
@@ -231,6 +232,8 @@ export function resolveNames(doc) {
|
|
|
231
232
|
diagnostics.push(...checkSupertypeGroups(flat));
|
|
232
233
|
// Pass 5: constraints and referenced keys (spec §10.10, §11.17).
|
|
233
234
|
diagnostics.push(...checkConstraints(flat));
|
|
235
|
+
// Pass 6: view source queries (spec §14.7).
|
|
236
|
+
diagnostics.push(...checkViews(flat));
|
|
234
237
|
return { diagnostics, symbols };
|
|
235
238
|
}
|
|
236
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.6";
|
|
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;
|
package/dist/parser.js
CHANGED
|
@@ -32,7 +32,7 @@ export class ParseError extends Error {
|
|
|
32
32
|
* declaring a later version is refused (spec 4.1) rather than parsed with
|
|
33
33
|
* semantics it does not have.
|
|
34
34
|
*/
|
|
35
|
-
export const SUPPORTED_XDBML_VERSION = '0.6';
|
|
35
|
+
export const SUPPORTED_XDBML_VERSION = '0.6.1';
|
|
36
36
|
/** Compare dotted version strings numerically: -1, 0 or 1. */
|
|
37
37
|
export function compareVersions(a, b) {
|
|
38
38
|
const pa = a.split('.').map((n) => Number(n) || 0);
|
|
@@ -169,6 +169,50 @@ export class Parser {
|
|
|
169
169
|
const t = this.peek();
|
|
170
170
|
throw new ParseError(`${msg} (got ${t.kind} ${JSON.stringify(t.text)})`, t.start);
|
|
171
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
|
+
}
|
|
172
216
|
spanFrom(start) {
|
|
173
217
|
// span end = end-position of the previously consumed token if any
|
|
174
218
|
const prev = this.idx > 0 ? this.tokens[this.idx - 1] : this.tokens[0];
|
|
@@ -209,14 +253,22 @@ export class Parser {
|
|
|
209
253
|
this.advance(); // xdbml
|
|
210
254
|
this.expect(TokenKind.Colon, "Expected ':' after 'xdbml'");
|
|
211
255
|
const numTok = this.expect(TokenKind.NumberLiteral, 'Expected version number');
|
|
212
|
-
|
|
213
|
-
|
|
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 ` +
|
|
214
266
|
`latest version this parser supports (${SUPPORTED_XDBML_VERSION}). ` +
|
|
215
267
|
'Use a newer parser, or declare a supported version.', numTok.start, 'unsupported-version');
|
|
216
268
|
}
|
|
217
269
|
return {
|
|
218
270
|
kind: 'VersionDeclaration',
|
|
219
|
-
version
|
|
271
|
+
version,
|
|
220
272
|
span: this.spanFrom(start),
|
|
221
273
|
};
|
|
222
274
|
}
|
|
@@ -283,7 +335,10 @@ export class Parser {
|
|
|
283
335
|
const name = this.parseIdentLikeName('project name');
|
|
284
336
|
this.expect(TokenKind.LBrace, "Expected '{' after Project name");
|
|
285
337
|
const body = [];
|
|
286
|
-
|
|
338
|
+
for (;;) {
|
|
339
|
+
this.skipListSeparators();
|
|
340
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
341
|
+
break;
|
|
287
342
|
// Project body is either an inline Note block or a setting line.
|
|
288
343
|
if (isKw(this.peek(), 'note')) {
|
|
289
344
|
body.push(this.parseNoteBlockOrSetting());
|
|
@@ -443,7 +498,7 @@ export class Parser {
|
|
|
443
498
|
}
|
|
444
499
|
const settings = this.maybeSettingsBlock();
|
|
445
500
|
this.expect(TokenKind.LBrace, "Expected '{' after entity name");
|
|
446
|
-
const body = this.parseEntityBody();
|
|
501
|
+
const body = this.parseEntityBody(canonEntityKw(kwTok.text), name);
|
|
447
502
|
this.expect(TokenKind.RBrace, "Expected '}' closing entity");
|
|
448
503
|
return {
|
|
449
504
|
kind: 'EntityDeclaration',
|
|
@@ -477,32 +532,37 @@ export class Parser {
|
|
|
477
532
|
}
|
|
478
533
|
return name;
|
|
479
534
|
}
|
|
480
|
-
parseEntityBody() {
|
|
535
|
+
parseEntityBody(construct, name) {
|
|
481
536
|
const body = [];
|
|
482
|
-
|
|
537
|
+
for (;;) {
|
|
538
|
+
this.skipListSeparators();
|
|
539
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
540
|
+
break;
|
|
483
541
|
const t = this.peek();
|
|
484
|
-
|
|
485
|
-
|
|
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()) {
|
|
486
547
|
body.push(this.parseNoteBlockOrSetting());
|
|
487
548
|
}
|
|
488
|
-
else if (
|
|
549
|
+
else if (this.atBlockKeyword('indexes')) {
|
|
489
550
|
body.push(this.parseIndexes());
|
|
490
551
|
}
|
|
491
|
-
else if (
|
|
552
|
+
else if (this.atBlockKeyword('checks')) {
|
|
492
553
|
body.push(this.parseChecks());
|
|
493
554
|
}
|
|
494
|
-
else if (
|
|
495
|
-
// v0.6 §10.1: a keyword only when `{` follows, so a field may keep
|
|
496
|
-
// the name `constraints`.
|
|
555
|
+
else if (this.atBlockKeyword('constraints')) {
|
|
497
556
|
body.push(this.parseConstraints());
|
|
498
557
|
}
|
|
499
|
-
else if (
|
|
558
|
+
else if (this.atBlockKeyword('records')) {
|
|
500
559
|
body.push(this.parseRecordsBlock());
|
|
501
560
|
}
|
|
502
561
|
else if (t.kind === TokenKind.Tilde) {
|
|
503
562
|
body.push(this.parsePartialInjection());
|
|
504
563
|
}
|
|
505
564
|
else {
|
|
565
|
+
this.rejectSettingInBody(construct, name);
|
|
506
566
|
body.push(this.parseFieldDeclaration());
|
|
507
567
|
}
|
|
508
568
|
}
|
|
@@ -992,10 +1052,12 @@ export class Parser {
|
|
|
992
1052
|
const kwTok = this.advance();
|
|
993
1053
|
this.expect(TokenKind.LBrace, "Expected '{' after object keyword");
|
|
994
1054
|
const fields = [];
|
|
995
|
-
|
|
1055
|
+
for (;;) {
|
|
1056
|
+
this.skipListSeparators();
|
|
1057
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
1058
|
+
break;
|
|
996
1059
|
const t = this.peek();
|
|
997
|
-
|
|
998
|
-
if (k === 'note') {
|
|
1060
|
+
if (this.atNoteElement()) {
|
|
999
1061
|
fields.push(this.parseNoteBlockOrSetting());
|
|
1000
1062
|
}
|
|
1001
1063
|
else if (t.kind === TokenKind.Tilde) {
|
|
@@ -1216,7 +1278,10 @@ export class Parser {
|
|
|
1216
1278
|
this.advance(); // oneOf | anyOf | allOf
|
|
1217
1279
|
this.expect(TokenKind.LBrace, `Expected '{' after ${flavor}`);
|
|
1218
1280
|
const alternatives = [];
|
|
1219
|
-
|
|
1281
|
+
for (;;) {
|
|
1282
|
+
this.skipListSeparators();
|
|
1283
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
1284
|
+
break;
|
|
1220
1285
|
alternatives.push(this.parsePolymorphicAlternative());
|
|
1221
1286
|
}
|
|
1222
1287
|
this.expect(TokenKind.RBrace, `Expected '}' closing ${flavor}`);
|
|
@@ -1249,10 +1314,12 @@ export class Parser {
|
|
|
1249
1314
|
if (this.check(TokenKind.LBrace)) {
|
|
1250
1315
|
this.advance();
|
|
1251
1316
|
fields = [];
|
|
1252
|
-
|
|
1317
|
+
for (;;) {
|
|
1318
|
+
this.skipListSeparators();
|
|
1319
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
1320
|
+
break;
|
|
1253
1321
|
const t = this.peek();
|
|
1254
|
-
|
|
1255
|
-
if (k === 'note') {
|
|
1322
|
+
if (this.atNoteElement()) {
|
|
1256
1323
|
fields.push(this.parseNoteBlockOrSetting());
|
|
1257
1324
|
}
|
|
1258
1325
|
else if (t.kind === TokenKind.Tilde) {
|
|
@@ -1383,10 +1450,12 @@ export class Parser {
|
|
|
1383
1450
|
finishObjectTypeDecl(start, name, settings) {
|
|
1384
1451
|
this.expect(TokenKind.LBrace, "Expected '{' after Type name");
|
|
1385
1452
|
const body = [];
|
|
1386
|
-
|
|
1453
|
+
for (;;) {
|
|
1454
|
+
this.skipListSeparators();
|
|
1455
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
1456
|
+
break;
|
|
1387
1457
|
const t = this.peek();
|
|
1388
|
-
|
|
1389
|
-
if (k === 'note') {
|
|
1458
|
+
if (this.atNoteElement()) {
|
|
1390
1459
|
body.push(this.parseNoteBlockOrSetting());
|
|
1391
1460
|
}
|
|
1392
1461
|
else if (t.kind === TokenKind.Tilde) {
|
|
@@ -1412,7 +1481,7 @@ export class Parser {
|
|
|
1412
1481
|
const name = this.parseIdentLikeName('edge name');
|
|
1413
1482
|
const settings = this.maybeSettingsBlock();
|
|
1414
1483
|
this.expect(TokenKind.LBrace, "Expected '{' after Edge settings");
|
|
1415
|
-
const body = this.parseEntityBody();
|
|
1484
|
+
const body = this.parseEntityBody('Edge', name);
|
|
1416
1485
|
this.expect(TokenKind.RBrace, "Expected '}' closing Edge");
|
|
1417
1486
|
return {
|
|
1418
1487
|
kind: 'EdgeDeclaration',
|
|
@@ -1430,16 +1499,22 @@ export class Parser {
|
|
|
1430
1499
|
const settings = this.maybeSettingsBlock();
|
|
1431
1500
|
this.expect(TokenKind.LBrace, "Expected '{' after View name");
|
|
1432
1501
|
const body = [];
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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()) {
|
|
1437
1509
|
body.push(this.parseNoteBlockOrSetting());
|
|
1438
1510
|
}
|
|
1439
|
-
else if (
|
|
1511
|
+
else if (isKw(this.peek(), 'source_query') && this.peek(1).kind === TokenKind.Colon) {
|
|
1440
1512
|
body.push(this.parseSourceQueryItem());
|
|
1441
1513
|
}
|
|
1442
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);
|
|
1443
1518
|
body.push(this.parseFieldDeclaration());
|
|
1444
1519
|
}
|
|
1445
1520
|
}
|
|
@@ -1480,7 +1555,10 @@ export class Parser {
|
|
|
1480
1555
|
}
|
|
1481
1556
|
this.expect(TokenKind.LBrace, "Expected '{' after enum name");
|
|
1482
1557
|
const values = [];
|
|
1483
|
-
|
|
1558
|
+
for (;;) {
|
|
1559
|
+
this.skipListSeparators();
|
|
1560
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
1561
|
+
break;
|
|
1484
1562
|
const vStart = this.peek().start;
|
|
1485
1563
|
const vt = this.peek();
|
|
1486
1564
|
let vname;
|
|
@@ -1754,7 +1832,7 @@ export class Parser {
|
|
|
1754
1832
|
const name = this.parseIdentLikeName('TablePartial name');
|
|
1755
1833
|
const settings = this.maybeSettingsBlock();
|
|
1756
1834
|
this.expect(TokenKind.LBrace, "Expected '{' after TablePartial name");
|
|
1757
|
-
const body = this.parseEntityBody();
|
|
1835
|
+
const body = this.parseEntityBody('TablePartial', name);
|
|
1758
1836
|
this.expect(TokenKind.RBrace, "Expected '}' closing TablePartial");
|
|
1759
1837
|
return {
|
|
1760
1838
|
kind: 'TablePartialDeclaration',
|
|
@@ -1869,6 +1947,14 @@ export class Parser {
|
|
|
1869
1947
|
this.expect(TokenKind.LBrace, "Expected '{' after indexes");
|
|
1870
1948
|
const entries = [];
|
|
1871
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
|
+
}
|
|
1872
1958
|
entries.push(this.parseIndexEntry());
|
|
1873
1959
|
}
|
|
1874
1960
|
this.expect(TokenKind.RBrace, "Expected '}' closing indexes");
|
|
@@ -1928,7 +2014,10 @@ export class Parser {
|
|
|
1928
2014
|
this.advance(); // checks
|
|
1929
2015
|
this.expect(TokenKind.LBrace, "Expected '{' after checks");
|
|
1930
2016
|
const entries = [];
|
|
1931
|
-
|
|
2017
|
+
for (;;) {
|
|
2018
|
+
this.skipListSeparators();
|
|
2019
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
2020
|
+
break;
|
|
1932
2021
|
entries.push(this.parseCheckEntry());
|
|
1933
2022
|
}
|
|
1934
2023
|
this.expect(TokenKind.RBrace, "Expected '}' closing checks");
|
|
@@ -1967,7 +2056,10 @@ export class Parser {
|
|
|
1967
2056
|
this.advance(); // constraints
|
|
1968
2057
|
this.expect(TokenKind.LBrace, "Expected '{' after constraints");
|
|
1969
2058
|
const entries = [];
|
|
1970
|
-
|
|
2059
|
+
for (;;) {
|
|
2060
|
+
this.skipListSeparators();
|
|
2061
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
2062
|
+
break;
|
|
1971
2063
|
const t = this.peek();
|
|
1972
2064
|
if (t.kind === TokenKind.StringLiteral ||
|
|
1973
2065
|
t.kind === TokenKind.MultilineString ||
|
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.6.
|
|
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",
|