@xdbml/parse 0.6.0 → 0.6.2
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.d.ts +9 -0
- package/dist/constraints.js +118 -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/lexer.js +14 -0
- package/dist/monarch.js +4 -0
- package/dist/name-resolver.d.ts +1 -1
- package/dist/name-resolver.js +36 -1
- package/dist/parser.d.ts +25 -1
- package/dist/parser.js +199 -37
- package/dist/views.d.ts +18 -0
- package/dist/views.js +62 -0
- package/package.json +1 -1
package/dist/constraints.d.ts
CHANGED
|
@@ -77,5 +77,14 @@ export declare function primaryKey(decl: {
|
|
|
77
77
|
export declare function markPrimaryKeyNotNull(doc: XDbmlDocument): void;
|
|
78
78
|
/** True for a target name the §11.17 rule covers. Unlisted names are not relational. */
|
|
79
79
|
export declare function isRelationalTarget(name: string): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Spec §5.2, rules 3 and 4: in a Project with several targets, every
|
|
82
|
+
* Container declares its own; and a Container's target is one of the
|
|
83
|
+
* Project's. Both apply to Containers written as such, only when the
|
|
84
|
+
* Project declares `targets:`. An implicit container (`Table core.users`)
|
|
85
|
+
* has no place for a target and is not checked. Before v0.6 the parser
|
|
86
|
+
* accepted both, so an earlier version draws warnings.
|
|
87
|
+
*/
|
|
88
|
+
export declare function checkTargets(doc: XDbmlDocument): Diagnostic[];
|
|
80
89
|
export declare function checkConstraints(doc: XDbmlDocument): Diagnostic[];
|
|
81
90
|
export {};
|
package/dist/constraints.js
CHANGED
|
@@ -329,6 +329,75 @@ function effectiveTarget(p, project) {
|
|
|
329
329
|
const values = settingValues(own?.value ?? null).filter((x) => x !== '');
|
|
330
330
|
return values[0] ?? project;
|
|
331
331
|
}
|
|
332
|
+
/** Aliases of §5.1, lower-cased, mapped to their canonical name. */
|
|
333
|
+
const TARGET_ALIASES = {
|
|
334
|
+
postgres: 'postgresql', pg: 'postgresql',
|
|
335
|
+
mssql: 'sql server', 'microsoft sql server': 'sql server', 't-sql': 'sql server',
|
|
336
|
+
'ibm db2': 'db2', 'db2 luw': 'db2', 'db2 z/os': 'db2 for z/os',
|
|
337
|
+
'google bigquery': 'bigquery', 'amazon redshift': 'redshift', 'azure synapse': 'synapse analytics',
|
|
338
|
+
mongo: 'mongodb', 'aws documentdb': 'documentdb', cosmos: 'cosmos db', 'azure cosmos db': 'cosmos db',
|
|
339
|
+
'apache cassandra': 'cassandra', 'amazon neptune': 'neptune',
|
|
340
|
+
'apache avro': 'avro', 'apache parquet': 'parquet',
|
|
341
|
+
'protocol buffers': 'protobuf', proto: 'protobuf', swagger: 'openapi',
|
|
342
|
+
};
|
|
343
|
+
function canonicalTarget(name) {
|
|
344
|
+
const key = name.trim().toLowerCase();
|
|
345
|
+
return TARGET_ALIASES[key] ?? key;
|
|
346
|
+
}
|
|
347
|
+
/** Every target the Project declares, or undefined when it declares none. */
|
|
348
|
+
function projectTargets(doc) {
|
|
349
|
+
for (const s of doc.statements) {
|
|
350
|
+
if (s.kind !== 'ProjectDeclaration')
|
|
351
|
+
continue;
|
|
352
|
+
for (const item of s.body) {
|
|
353
|
+
if (item.kind === 'Setting' && (item.name === 'targets' || item.name === 'database_type')) {
|
|
354
|
+
const values = settingValues(item.value).filter((x) => x !== '');
|
|
355
|
+
return values.length > 0 ? values : undefined;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Spec §5.2, rules 3 and 4: in a Project with several targets, every
|
|
363
|
+
* Container declares its own; and a Container's target is one of the
|
|
364
|
+
* Project's. Both apply to Containers written as such, only when the
|
|
365
|
+
* Project declares `targets:`. An implicit container (`Table core.users`)
|
|
366
|
+
* has no place for a target and is not checked. Before v0.6 the parser
|
|
367
|
+
* accepted both, so an earlier version draws warnings.
|
|
368
|
+
*/
|
|
369
|
+
export function checkTargets(doc) {
|
|
370
|
+
const diagnostics = [];
|
|
371
|
+
const declared = projectTargets(doc);
|
|
372
|
+
if (!declared)
|
|
373
|
+
return diagnostics;
|
|
374
|
+
const severity = versionAtLeast(doc, '0.6') ? 'error' : 'warning';
|
|
375
|
+
const canon = new Set(declared.map(canonicalTarget));
|
|
376
|
+
for (const s of doc.statements) {
|
|
377
|
+
if (s.kind !== 'ContainerDeclaration')
|
|
378
|
+
continue;
|
|
379
|
+
const own = settingValues(s.settings.find((x) => x.name === 'target')?.value ?? null).filter((x) => x !== '');
|
|
380
|
+
if (own.length === 0 && declared.length > 1) {
|
|
381
|
+
diagnostics.push({
|
|
382
|
+
severity,
|
|
383
|
+
code: 'container-target-missing',
|
|
384
|
+
message: `Container '${s.name}' declares no target, and the Project declares several (${declared.join(', ')}). ` +
|
|
385
|
+
`Write the target after its name: ${s.keyword} ${s.name} [target: ${declared[0]}] { ... } (spec §5.2).`,
|
|
386
|
+
span: s.span,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
else if (own.length > 0 && !canon.has(canonicalTarget(own[0]))) {
|
|
390
|
+
diagnostics.push({
|
|
391
|
+
severity,
|
|
392
|
+
code: 'container-target-not-in-project',
|
|
393
|
+
message: `Container '${s.name}' targets ${own[0]}, which the Project's targets do not list (${declared.join(', ')}). ` +
|
|
394
|
+
`Add it to the Project's targets, or change the Container's target (spec §5.2).`,
|
|
395
|
+
span: s.span,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return diagnostics;
|
|
400
|
+
}
|
|
332
401
|
/* -------------------------------------------------------------------------
|
|
333
402
|
* Checks (§10.10, §11.17)
|
|
334
403
|
* ----------------------------------------------------------------------- */
|
|
@@ -437,17 +506,38 @@ export function checkConstraints(doc) {
|
|
|
437
506
|
}
|
|
438
507
|
}
|
|
439
508
|
// One primary key, declared in one place (§10.4). Inline [pk] fields
|
|
440
|
-
// count as one declaration.
|
|
509
|
+
// count as one declaration. When the second declaration restates the
|
|
510
|
+
// same fields, say so: the fix is to delete one, not to look for
|
|
511
|
+
// another key.
|
|
441
512
|
const own = bodyConstraints(decl.body, v06);
|
|
442
513
|
const pks = own.filter((c) => c.kind === 'key' && c.keyKind === 'primary');
|
|
443
514
|
for (const extra of pks.slice(1)) {
|
|
515
|
+
const first = pks[0];
|
|
444
516
|
diagnostics.push({
|
|
445
517
|
severity: olderSeverity,
|
|
446
518
|
code: 'duplicate-primary-key',
|
|
447
|
-
message:
|
|
519
|
+
message: sameKeyFields(first.fields, extra.fields)
|
|
520
|
+
? `'${decl.name}' declares its primary key (${first.fields.join(', ')}) twice: ` +
|
|
521
|
+
`${twoForms(first, extra)}. Declare it in one place (spec §10.4).`
|
|
522
|
+
: `'${decl.name}' declares a second primary key; an entity has one, declared in one place.`,
|
|
448
523
|
span: extra.span,
|
|
449
524
|
});
|
|
450
525
|
}
|
|
526
|
+
// Each unique key is declared once (§10.3, v0.6.1): a warning in every
|
|
527
|
+
// version, since earlier documents accept the repetition.
|
|
528
|
+
const uniques = own.filter((c) => c.kind === 'key' && c.keyKind === 'unique');
|
|
529
|
+
for (const [i, u] of uniques.entries()) {
|
|
530
|
+
const first = uniques.slice(0, i).find((prev) => sameKeyFields(prev.fields, u.fields));
|
|
531
|
+
if (!first)
|
|
532
|
+
continue;
|
|
533
|
+
diagnostics.push({
|
|
534
|
+
severity: 'warning',
|
|
535
|
+
code: 'duplicate-unique-key',
|
|
536
|
+
message: `'${decl.name}' declares the unique key (${first.fields.join(', ')}) twice: ` +
|
|
537
|
+
`${twoForms(first, u)}. Declare it in one place (spec §10.3).`,
|
|
538
|
+
span: u.span,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
451
541
|
// `null` on a primary key field (§10.4).
|
|
452
542
|
for (const pk of pks.slice(0, 1)) {
|
|
453
543
|
for (const dotted of pk.fields) {
|
|
@@ -467,6 +557,32 @@ export function checkConstraints(doc) {
|
|
|
467
557
|
diagnostics.push(...checkReferencedKeys(doc, olderSeverity));
|
|
468
558
|
return diagnostics;
|
|
469
559
|
}
|
|
560
|
+
/** Two keys on the same fields, in any order, are the same key. */
|
|
561
|
+
function sameKeyFields(a, b) {
|
|
562
|
+
if (a.length !== b.length)
|
|
563
|
+
return false;
|
|
564
|
+
const sa = [...a].sort();
|
|
565
|
+
const sb = [...b].sort();
|
|
566
|
+
return sa.every((f, i) => f === sb[i]);
|
|
567
|
+
}
|
|
568
|
+
/** How a key was written, for a message that names both declarations. */
|
|
569
|
+
function keyForm(k) {
|
|
570
|
+
const flag = k.keyKind === 'primary' ? 'pk' : 'unique';
|
|
571
|
+
if (k.source === 'inline')
|
|
572
|
+
return `[${flag}] on ${k.fields.join(', ')}`;
|
|
573
|
+
if (k.source === 'indexes')
|
|
574
|
+
return `a ${flag} entry in indexes`;
|
|
575
|
+
return `a ${flag} line in constraints`;
|
|
576
|
+
}
|
|
577
|
+
/** Both declarations of a repeated key: "[pk] on id and a pk line in constraints". */
|
|
578
|
+
function twoForms(a, b) {
|
|
579
|
+
const fa = keyForm(a);
|
|
580
|
+
const fb = keyForm(b);
|
|
581
|
+
if (fa !== fb)
|
|
582
|
+
return `${fa} and ${fb}`;
|
|
583
|
+
const flag = a.keyKind === 'primary' ? 'pk' : 'unique';
|
|
584
|
+
return a.source === 'indexes' ? `two ${flag} entries in indexes` : `two ${flag} lines in constraints`;
|
|
585
|
+
}
|
|
470
586
|
function entityIndex(doc) {
|
|
471
587
|
const m = new Map();
|
|
472
588
|
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/lexer.js
CHANGED
|
@@ -500,6 +500,20 @@ export class Lexer {
|
|
|
500
500
|
if (this.isIdentStart(c)) {
|
|
501
501
|
return this.lexIdentifier();
|
|
502
502
|
}
|
|
503
|
+
if (c === '#') {
|
|
504
|
+
// DBML writes a color unquoted, `headercolor: #3498DB` (spec §3.3):
|
|
505
|
+
// `#` and three or six hex digits read as the string '#3498DB'.
|
|
506
|
+
let n = 1;
|
|
507
|
+
while (/[0-9A-Fa-f]/.test(this.peek(n)))
|
|
508
|
+
n++;
|
|
509
|
+
const digits = n - 1;
|
|
510
|
+
if ((digits === 3 || digits === 6) && !/[A-Za-z0-9_]/.test(this.peek(n))) {
|
|
511
|
+
for (let i = 0; i < n; i++)
|
|
512
|
+
this.advance();
|
|
513
|
+
const text = this.text.slice(start.offset, this.offset);
|
|
514
|
+
return { kind: TokenKind.StringLiteral, text, value: text, start, end: this.pos() };
|
|
515
|
+
}
|
|
516
|
+
}
|
|
503
517
|
throw new LexError(`Unexpected character: ${JSON.stringify(c)}`, start);
|
|
504
518
|
}
|
|
505
519
|
tokenize() {
|
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' | 'name-collision' | 'container-target-missing' | 'container-target-not-in-project' | 'unresolved-type' | 'possible-type-typo' | 'unresolved-key-field' | 'unresolved-index-field' | 'key-path-crosses-collection' | 'invalid-key-flags' | 'duplicate-primary-key' | 'duplicate-unique-key' | 'null-in-primary-key' | 'duplicate-constraints-block' | 'ref-target-not-key' | 'unresolved-entity' | 'unresolved-field' | 'unresolved-partial' | 'unresolved-tablegroup-member' | 'unresolved-records-entity' | 'unresolved-records-column' | 'empty-import' | 'invalid-nested-path' | 'foreign-master-composite' | 'foreign-master-duplicate-child' | 'foreign-master-without-ref' | 'construct-requires-version' | 'ambiguous-ref-endpoint' | 'invalid-constraint-type' | 'constraint-type-on-foreign-master' | 'invalid-undirected' | 'entity-level-many-to-many' | 'missing-supertype' | 'unresolved-supertype-group-member' | 'invalid-supertype-group-value' | 'duplicate-subtype' | 'supertype-is-subtype' | 'subtype-in-multiple-groups' | 'supertype-cycle' | 'supertype-attribute-redeclared' | 'discriminator-on-overlapping-group' | 'merge-without-roll-up' | 'empty-supertype-group' | 'source-query-in-settings' | 'duplicate-source-query';
|
|
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,7 +42,9 @@ import { SCALAR_TYPES, BSON_TYPES } from "./keywords.js";
|
|
|
42
42
|
import { flatten } from "./module-resolver.js";
|
|
43
43
|
import { checkRelationships } from "./relationships.js";
|
|
44
44
|
import { checkSupertypeGroups } from "./supertypes.js";
|
|
45
|
-
import { checkConstraints } from "./constraints.js";
|
|
45
|
+
import { checkConstraints, checkTargets } from "./constraints.js";
|
|
46
|
+
import { versionAtLeast } from "./relationships.js";
|
|
47
|
+
import { checkViews } from "./views.js";
|
|
46
48
|
/**
|
|
47
49
|
* Read-only handle on the collected symbol table.
|
|
48
50
|
*
|
|
@@ -231,6 +233,9 @@ export function resolveNames(doc) {
|
|
|
231
233
|
diagnostics.push(...checkSupertypeGroups(flat));
|
|
232
234
|
// Pass 5: constraints and referenced keys (spec §10.10, §11.17).
|
|
233
235
|
diagnostics.push(...checkConstraints(flat));
|
|
236
|
+
diagnostics.push(...checkTargets(flat));
|
|
237
|
+
// Pass 6: view source queries (spec §14.7).
|
|
238
|
+
diagnostics.push(...checkViews(flat));
|
|
234
239
|
return { diagnostics, symbols };
|
|
235
240
|
}
|
|
236
241
|
/* -------------------------------------------------------------------------
|
|
@@ -238,9 +243,18 @@ export function resolveNames(doc) {
|
|
|
238
243
|
* ----------------------------------------------------------------------- */
|
|
239
244
|
function collectDeclarations(doc, entries, diagnostics) {
|
|
240
245
|
const seen = new Set(); // qualified-name keys to detect duplicates
|
|
246
|
+
const first = diagnostics.length;
|
|
241
247
|
for (const stmt of doc.statements) {
|
|
242
248
|
addTopLevelDeclaration(stmt, entries, diagnostics, seen);
|
|
243
249
|
}
|
|
250
|
+
// Before v0.6 the parser accepted a collision, so a document declaring an
|
|
251
|
+
// earlier version, or none (DBML), draws a warning and stays valid.
|
|
252
|
+
if (!versionAtLeast(doc, '0.6')) {
|
|
253
|
+
for (const d of diagnostics.slice(first)) {
|
|
254
|
+
if (d.code === 'name-collision')
|
|
255
|
+
d.severity = 'warning';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
244
258
|
}
|
|
245
259
|
function addTopLevelDeclaration(stmt, entries, diagnostics, seen) {
|
|
246
260
|
switch (stmt.kind) {
|
|
@@ -310,9 +324,30 @@ function addTopLevelDeclaration(stmt, entries, diagnostics, seen) {
|
|
|
310
324
|
return;
|
|
311
325
|
}
|
|
312
326
|
}
|
|
327
|
+
const SHARED_NAMESPACE = new Set(['entity', 'view', 'edge', 'type', 'enum', 'tablepartial']);
|
|
328
|
+
const KIND_NAMES = {
|
|
329
|
+
entity: 'an Entity', view: 'a View', edge: 'an Edge', type: 'a Type', enum: 'an Enum', tablepartial: 'a TablePartial',
|
|
330
|
+
};
|
|
331
|
+
function article(kind) {
|
|
332
|
+
return KIND_NAMES[kind] ?? kind;
|
|
333
|
+
}
|
|
313
334
|
function addEntry(name, containerName, kind, declaration, span, entries, diagnostics, seen) {
|
|
314
335
|
const qualifiedName = containerName ? `${containerName}.${name}` : name;
|
|
315
336
|
const key = `${kind}:${qualifiedName}`;
|
|
337
|
+
// Spec §15.5: Entities, Views, Edges, Types, Enums and TablePartials
|
|
338
|
+
// share one namespace. collectDeclarations() sets the severity.
|
|
339
|
+
if (SHARED_NAMESPACE.has(kind) && !seen.has(key)) {
|
|
340
|
+
const other = entries.find((e) => e.qualifiedName === qualifiedName && e.kind !== kind && SHARED_NAMESPACE.has(e.kind));
|
|
341
|
+
if (other) {
|
|
342
|
+
diagnostics.push({
|
|
343
|
+
severity: 'error',
|
|
344
|
+
code: 'name-collision',
|
|
345
|
+
message: `'${qualifiedName}' names both ${article(other.kind)} and ${article(kind)}. ` +
|
|
346
|
+
`Entities, Views, Edges, Types, Enums and TablePartials share one namespace (spec §15.5): rename one of them.`,
|
|
347
|
+
span,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
316
351
|
if (seen.has(key)) {
|
|
317
352
|
diagnostics.push({
|
|
318
353
|
severity: 'error',
|
package/dist/parser.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export declare class ParseError extends Error {
|
|
|
26
26
|
* declaring a later version is refused (spec 4.1) rather than parsed with
|
|
27
27
|
* semantics it does not have.
|
|
28
28
|
*/
|
|
29
|
-
export declare const SUPPORTED_XDBML_VERSION = "0.6";
|
|
29
|
+
export declare const SUPPORTED_XDBML_VERSION = "0.6.2";
|
|
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.2';
|
|
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) {
|
|
@@ -1318,6 +1385,17 @@ export class Parser {
|
|
|
1318
1385
|
}
|
|
1319
1386
|
this.expect(TokenKind.RParen, "Expected ')' closing type parameters");
|
|
1320
1387
|
}
|
|
1388
|
+
// DBML's array type of a target, `text[]` or `varchar(20)[]`, keeps its
|
|
1389
|
+
// brackets in the type name (spec §8.1). Only empty brackets written
|
|
1390
|
+
// right after the type, with no space, belong to it: `int [pk]` and
|
|
1391
|
+
// `int []` remain settings blocks.
|
|
1392
|
+
while (this.check(TokenKind.LBracket) &&
|
|
1393
|
+
this.peek(1).kind === TokenKind.RBracket &&
|
|
1394
|
+
this.peek().start.offset === this.tokens[this.idx - 1].end.offset) {
|
|
1395
|
+
this.advance();
|
|
1396
|
+
this.advance();
|
|
1397
|
+
name += '[]';
|
|
1398
|
+
}
|
|
1321
1399
|
return {
|
|
1322
1400
|
kind: 'ScalarType',
|
|
1323
1401
|
name,
|
|
@@ -1383,10 +1461,12 @@ export class Parser {
|
|
|
1383
1461
|
finishObjectTypeDecl(start, name, settings) {
|
|
1384
1462
|
this.expect(TokenKind.LBrace, "Expected '{' after Type name");
|
|
1385
1463
|
const body = [];
|
|
1386
|
-
|
|
1464
|
+
for (;;) {
|
|
1465
|
+
this.skipListSeparators();
|
|
1466
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
1467
|
+
break;
|
|
1387
1468
|
const t = this.peek();
|
|
1388
|
-
|
|
1389
|
-
if (k === 'note') {
|
|
1469
|
+
if (this.atNoteElement()) {
|
|
1390
1470
|
body.push(this.parseNoteBlockOrSetting());
|
|
1391
1471
|
}
|
|
1392
1472
|
else if (t.kind === TokenKind.Tilde) {
|
|
@@ -1412,7 +1492,7 @@ export class Parser {
|
|
|
1412
1492
|
const name = this.parseIdentLikeName('edge name');
|
|
1413
1493
|
const settings = this.maybeSettingsBlock();
|
|
1414
1494
|
this.expect(TokenKind.LBrace, "Expected '{' after Edge settings");
|
|
1415
|
-
const body = this.parseEntityBody();
|
|
1495
|
+
const body = this.parseEntityBody('Edge', name);
|
|
1416
1496
|
this.expect(TokenKind.RBrace, "Expected '}' closing Edge");
|
|
1417
1497
|
return {
|
|
1418
1498
|
kind: 'EdgeDeclaration',
|
|
@@ -1430,16 +1510,22 @@ export class Parser {
|
|
|
1430
1510
|
const settings = this.maybeSettingsBlock();
|
|
1431
1511
|
this.expect(TokenKind.LBrace, "Expected '{' after View name");
|
|
1432
1512
|
const body = [];
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1513
|
+
for (;;) {
|
|
1514
|
+
this.skipListSeparators();
|
|
1515
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
1516
|
+
break;
|
|
1517
|
+
// Spec §3.10: `Note` and `source_query` start their elements only
|
|
1518
|
+
// before `:` (or `{` for a note); otherwise they name a field.
|
|
1519
|
+
if (this.atNoteElement()) {
|
|
1437
1520
|
body.push(this.parseNoteBlockOrSetting());
|
|
1438
1521
|
}
|
|
1439
|
-
else if (
|
|
1522
|
+
else if (isKw(this.peek(), 'source_query') && this.peek(1).kind === TokenKind.Colon) {
|
|
1440
1523
|
body.push(this.parseSourceQueryItem());
|
|
1441
1524
|
}
|
|
1442
1525
|
else {
|
|
1526
|
+
// Spec §14.2: `materialized: true` and the other settings go in
|
|
1527
|
+
// the brackets, never in the body.
|
|
1528
|
+
this.rejectSettingInBody('View', name);
|
|
1443
1529
|
body.push(this.parseFieldDeclaration());
|
|
1444
1530
|
}
|
|
1445
1531
|
}
|
|
@@ -1480,7 +1566,10 @@ export class Parser {
|
|
|
1480
1566
|
}
|
|
1481
1567
|
this.expect(TokenKind.LBrace, "Expected '{' after enum name");
|
|
1482
1568
|
const values = [];
|
|
1483
|
-
|
|
1569
|
+
for (;;) {
|
|
1570
|
+
this.skipListSeparators();
|
|
1571
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
1572
|
+
break;
|
|
1484
1573
|
const vStart = this.peek().start;
|
|
1485
1574
|
const vt = this.peek();
|
|
1486
1575
|
let vname;
|
|
@@ -1754,7 +1843,7 @@ export class Parser {
|
|
|
1754
1843
|
const name = this.parseIdentLikeName('TablePartial name');
|
|
1755
1844
|
const settings = this.maybeSettingsBlock();
|
|
1756
1845
|
this.expect(TokenKind.LBrace, "Expected '{' after TablePartial name");
|
|
1757
|
-
const body = this.parseEntityBody();
|
|
1846
|
+
const body = this.parseEntityBody('TablePartial', name);
|
|
1758
1847
|
this.expect(TokenKind.RBrace, "Expected '}' closing TablePartial");
|
|
1759
1848
|
return {
|
|
1760
1849
|
kind: 'TablePartialDeclaration',
|
|
@@ -1869,6 +1958,14 @@ export class Parser {
|
|
|
1869
1958
|
this.expect(TokenKind.LBrace, "Expected '{' after indexes");
|
|
1870
1959
|
const entries = [];
|
|
1871
1960
|
while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
|
|
1961
|
+
// Spec §3.9: `indexes` is not a list body. `email, name` may be meant
|
|
1962
|
+
// as the composite index `(email, name)`, so a separator between
|
|
1963
|
+
// entries stays an error rather than declaring two indexes.
|
|
1964
|
+
const t = this.peek();
|
|
1965
|
+
if (t.kind === TokenKind.Comma || t.kind === TokenKind.Semicolon) {
|
|
1966
|
+
throw new ParseError(`Unexpected '${t.text}' in indexes: write one index per line, and a composite ` +
|
|
1967
|
+
'index in parentheses, as in (email, name) (spec §3.9).', t.start);
|
|
1968
|
+
}
|
|
1872
1969
|
entries.push(this.parseIndexEntry());
|
|
1873
1970
|
}
|
|
1874
1971
|
this.expect(TokenKind.RBrace, "Expected '}' closing indexes");
|
|
@@ -1928,7 +2025,10 @@ export class Parser {
|
|
|
1928
2025
|
this.advance(); // checks
|
|
1929
2026
|
this.expect(TokenKind.LBrace, "Expected '{' after checks");
|
|
1930
2027
|
const entries = [];
|
|
1931
|
-
|
|
2028
|
+
for (;;) {
|
|
2029
|
+
this.skipListSeparators();
|
|
2030
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
2031
|
+
break;
|
|
1932
2032
|
entries.push(this.parseCheckEntry());
|
|
1933
2033
|
}
|
|
1934
2034
|
this.expect(TokenKind.RBrace, "Expected '}' closing checks");
|
|
@@ -1967,7 +2067,10 @@ export class Parser {
|
|
|
1967
2067
|
this.advance(); // constraints
|
|
1968
2068
|
this.expect(TokenKind.LBrace, "Expected '{' after constraints");
|
|
1969
2069
|
const entries = [];
|
|
1970
|
-
|
|
2070
|
+
for (;;) {
|
|
2071
|
+
this.skipListSeparators();
|
|
2072
|
+
if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
|
|
2073
|
+
break;
|
|
1971
2074
|
const t = this.peek();
|
|
1972
2075
|
if (t.kind === TokenKind.StringLiteral ||
|
|
1973
2076
|
t.kind === TokenKind.MultilineString ||
|
|
@@ -2284,7 +2387,66 @@ export function parse(source, options = {}) {
|
|
|
2284
2387
|
const initialStack = new Set();
|
|
2285
2388
|
if (options.filePath)
|
|
2286
2389
|
initialStack.add(options.filePath);
|
|
2287
|
-
|
|
2390
|
+
const doc = new Parser(tokens, options, initialStack, 0).parseDocument();
|
|
2391
|
+
applyEntityAliases(doc);
|
|
2392
|
+
return doc;
|
|
2393
|
+
}
|
|
2394
|
+
/**
|
|
2395
|
+
* Spec §7.4: `Table very_long_name as V { ... }` lets a relationship name
|
|
2396
|
+
* the entity by its alias, `Ref: V.id < posts.user_id`, as in DBML. This
|
|
2397
|
+
* pass rewrites the first segment of every relationship endpoint that is
|
|
2398
|
+
* an alias into the entity's name, so the resolver, the key checks and the
|
|
2399
|
+
* renderer see the entity itself. The entity keeps its `alias`. An alias
|
|
2400
|
+
* that repeats the name of an entity or a container, or that two entities
|
|
2401
|
+
* declare, is left alone: the name it would shadow applies.
|
|
2402
|
+
*/
|
|
2403
|
+
function applyEntityAliases(doc) {
|
|
2404
|
+
const aliases = new Map();
|
|
2405
|
+
const taken = new Set();
|
|
2406
|
+
const visit = (decl, container) => {
|
|
2407
|
+
if (decl.kind !== 'EntityDeclaration')
|
|
2408
|
+
return;
|
|
2409
|
+
const segments = [...(container ? [container] : []), ...decl.name.split('.')];
|
|
2410
|
+
taken.add(segments[segments.length - 1]);
|
|
2411
|
+
if (!decl.alias)
|
|
2412
|
+
return;
|
|
2413
|
+
aliases.set(decl.alias, aliases.has(decl.alias) ? null : segments);
|
|
2414
|
+
};
|
|
2415
|
+
for (const stmt of doc.statements) {
|
|
2416
|
+
if (stmt.kind === 'ContainerDeclaration') {
|
|
2417
|
+
taken.add(stmt.name);
|
|
2418
|
+
for (const body of stmt.body)
|
|
2419
|
+
visit(body, stmt.name);
|
|
2420
|
+
}
|
|
2421
|
+
else {
|
|
2422
|
+
visit(stmt);
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
for (const name of taken)
|
|
2426
|
+
aliases.delete(name);
|
|
2427
|
+
if (![...aliases.values()].some((v) => v !== null))
|
|
2428
|
+
return;
|
|
2429
|
+
const walk = (node) => {
|
|
2430
|
+
if (Array.isArray(node)) {
|
|
2431
|
+
node.forEach(walk);
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
if (!node || typeof node !== 'object')
|
|
2435
|
+
return;
|
|
2436
|
+
const obj = node;
|
|
2437
|
+
if (obj.kind === 'RefEndpoint' && Array.isArray(obj.path)) {
|
|
2438
|
+
const first = obj.path[0];
|
|
2439
|
+
const segments = first?.kind === 'PathField' && first.name ? aliases.get(first.name) : undefined;
|
|
2440
|
+
if (segments) {
|
|
2441
|
+
obj.path = [...segments.map((name) => ({ ...first, name })), ...obj.path.slice(1)];
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2445
|
+
if (key !== 'span' && value && typeof value === 'object')
|
|
2446
|
+
walk(value);
|
|
2447
|
+
}
|
|
2448
|
+
};
|
|
2449
|
+
walk(doc.statements);
|
|
2288
2450
|
}
|
|
2289
2451
|
/**
|
|
2290
2452
|
* Internal `ParseFn` used by the module resolver to recursively parse a
|
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.2",
|
|
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",
|