@xdbml/parse 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ast.d.ts +40 -3
- package/dist/constraints.d.ts +81 -0
- package/dist/constraints.js +585 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/keywords.d.ts +2 -1
- package/dist/keywords.js +55 -3
- package/dist/module-resolver.js +11 -1
- package/dist/monarch.d.ts +1 -0
- package/dist/monarch.js +5 -1
- package/dist/name-resolver.d.ts +1 -1
- package/dist/name-resolver.js +130 -17
- package/dist/parser.d.ts +3 -1
- package/dist/parser.js +84 -8
- package/package.json +1 -1
package/dist/keywords.js
CHANGED
|
@@ -98,9 +98,15 @@ export const POLYMORPHISM_KEYWORDS = [
|
|
|
98
98
|
/* -------------------------------------------------------------------------
|
|
99
99
|
* Scalar and BSON types
|
|
100
100
|
*
|
|
101
|
-
* The parser accepts any identifier as a scalar type (open vocabulary
|
|
102
|
-
* so these lists
|
|
103
|
-
*
|
|
101
|
+
* The parser accepts any identifier as a scalar type (open vocabulary,
|
|
102
|
+
* spec §1.2 principle 4), so these lists never reject a type name.
|
|
103
|
+
* They serve two purposes: themes color them under `storage.type`
|
|
104
|
+
* (TextMate) or `type` (Monarch), and the name resolver treats them as
|
|
105
|
+
* built-in names, which a Named Type cannot shadow (spec §15.2).
|
|
106
|
+
* Adding a name here therefore changes resolution as well as color: a
|
|
107
|
+
* document declaring `Type <that name>` would stop resolving to its own
|
|
108
|
+
* Type. Target-native names that only need color go in
|
|
109
|
+
* TARGET_NATIVE_TYPES below.
|
|
104
110
|
* ----------------------------------------------------------------------- */
|
|
105
111
|
export const SCALAR_TYPES = [
|
|
106
112
|
// Integers
|
|
@@ -139,6 +145,51 @@ export const BSON_TYPES = [
|
|
|
139
145
|
'long',
|
|
140
146
|
'double',
|
|
141
147
|
];
|
|
148
|
+
/* -------------------------------------------------------------------------
|
|
149
|
+
* Target-native types (highlighting only)
|
|
150
|
+
*
|
|
151
|
+
* Common type names of specific targets, colored like SCALAR_TYPES so
|
|
152
|
+
* that a physical model reads the same in the editor as a generic one.
|
|
153
|
+
* Unlike SCALAR_TYPES, these are not built-in names: the resolver
|
|
154
|
+
* passes them through like any other target-native name, and a Named
|
|
155
|
+
* Type or Enum may be declared under one of them.
|
|
156
|
+
*
|
|
157
|
+
* Coloring is position-independent (Monarch and TextMate both match
|
|
158
|
+
* the bare word), so a field named after a listed type is colored as a
|
|
159
|
+
* type too. Generic words that often name fields -- `point`, `line`,
|
|
160
|
+
* `box`, `path`, `circle`, `image`, `counter`, `duration`, `name` --
|
|
161
|
+
* are left out for that reason. The list is a convenience, not a
|
|
162
|
+
* vocabulary: a name missing from it is still a valid type.
|
|
163
|
+
* ----------------------------------------------------------------------- */
|
|
164
|
+
export const TARGET_NATIVE_TYPES = [
|
|
165
|
+
// Oracle
|
|
166
|
+
'number', 'binary_float', 'binary_double', 'clob', 'nclob', 'bfile',
|
|
167
|
+
'raw', 'rowid', 'urowid', 'xmltype', 'sdo_geometry',
|
|
168
|
+
// Oracle, PostgreSQL, BigQuery, Databricks
|
|
169
|
+
'interval',
|
|
170
|
+
// PostgreSQL
|
|
171
|
+
'serial', 'smallserial', 'bigserial', 'serial2', 'serial4', 'serial8',
|
|
172
|
+
'int2', 'int4', 'int8', 'float4', 'float8', 'bytea', 'citext',
|
|
173
|
+
'inet', 'cidr', 'macaddr', 'macaddr8', 'tsvector', 'tsquery', 'hstore',
|
|
174
|
+
'jsonpath', 'timetz', 'varbit',
|
|
175
|
+
'int4range', 'int8range', 'numrange', 'tsrange', 'tstzrange', 'daterange',
|
|
176
|
+
// SQL Server
|
|
177
|
+
'uniqueidentifier', 'datetimeoffset', 'smalldatetime', 'rowversion',
|
|
178
|
+
'hierarchyid', 'sql_variant',
|
|
179
|
+
// Snowflake (plus `number`, `interval`, `geography`, `geometry`)
|
|
180
|
+
'timestamp_ntz', 'timestamp_ltz', 'timestamp_tz', 'vector',
|
|
181
|
+
// BigQuery, Avro, Protobuf
|
|
182
|
+
'float64', 'bignumeric', 'bigdecimal', 'bytes',
|
|
183
|
+
// Spatial, across several targets
|
|
184
|
+
'geometry', 'geography',
|
|
185
|
+
// Cassandra
|
|
186
|
+
'ascii', 'varint', 'timeuuid',
|
|
187
|
+
// Protobuf
|
|
188
|
+
'uint32', 'uint64', 'sint32', 'sint64',
|
|
189
|
+
'fixed32', 'fixed64', 'sfixed32', 'sfixed64',
|
|
190
|
+
// ClickHouse
|
|
191
|
+
'uint8', 'uint16', 'int16', 'float32',
|
|
192
|
+
];
|
|
142
193
|
/* -------------------------------------------------------------------------
|
|
143
194
|
* Settings vocabulary
|
|
144
195
|
*
|
|
@@ -235,6 +286,7 @@ export const SETTING_KEYS = [
|
|
|
235
286
|
// Block keywords (entity-body or top-level)
|
|
236
287
|
'indexes',
|
|
237
288
|
'checks', // v0.2 §10: entity-level checks block
|
|
289
|
+
'constraints', // v0.6 §10: keys and checks block
|
|
238
290
|
// Container settings
|
|
239
291
|
'replication',
|
|
240
292
|
'location',
|
package/dist/module-resolver.js
CHANGED
|
@@ -390,8 +390,18 @@ function findImportTarget(item, doc) {
|
|
|
390
390
|
}
|
|
391
391
|
return undefined;
|
|
392
392
|
}
|
|
393
|
-
// Enum: top-level OR container-scoped.
|
|
393
|
+
// Enum: top-level OR container-scoped. A top-level Enum declared with
|
|
394
|
+
// a qualified name (`enum core.job_status`, the DBML form) matches its
|
|
395
|
+
// full dotted path and imports under its bare name, like the
|
|
396
|
+
// container-scoped form: the source container does not survive into
|
|
397
|
+
// the importing file (spec §27.16).
|
|
394
398
|
if (item.elementType === 'enum') {
|
|
399
|
+
if (segments.length > 1) {
|
|
400
|
+
const qualifiedTopLevel = doc.statements.find((s) => s.kind === 'EnumDeclaration' && s.name === path);
|
|
401
|
+
if (qualifiedTopLevel && qualifiedTopLevel.kind === 'EnumDeclaration') {
|
|
402
|
+
return { ...qualifiedTopLevel, name: segments[segments.length - 1] };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
395
405
|
if (segments.length === 1) {
|
|
396
406
|
return doc.statements.find((s) => s.kind === 'EnumDeclaration' && s.name === segments[0]);
|
|
397
407
|
}
|
package/dist/monarch.d.ts
CHANGED
package/dist/monarch.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* dependency on consumers that just want to parse. The Monaco types
|
|
19
19
|
* are structurally compatible -- the consumer casts at the boundary.
|
|
20
20
|
*/
|
|
21
|
-
import { CONTAINER_KEYWORDS, ENTITY_KEYWORDS, DECLARATION_KEYWORDS, STRUCTURAL_TYPE_KEYWORDS, POLYMORPHISM_KEYWORDS, SCALAR_TYPES, BSON_TYPES, SETTING_FLAGS, SETTING_KEYS, GRANULARITY_VALUES, } from "./keywords.js";
|
|
21
|
+
import { CONTAINER_KEYWORDS, ENTITY_KEYWORDS, DECLARATION_KEYWORDS, STRUCTURAL_TYPE_KEYWORDS, POLYMORPHISM_KEYWORDS, SCALAR_TYPES, BSON_TYPES, TARGET_NATIVE_TYPES, SETTING_FLAGS, SETTING_KEYS, GRANULARITY_VALUES, } from "./keywords.js";
|
|
22
22
|
/* -------------------------------------------------------------------------
|
|
23
23
|
* Language configuration
|
|
24
24
|
* ----------------------------------------------------------------------- */
|
|
@@ -102,6 +102,9 @@ export const xdbmlMonarchTokensProvider = {
|
|
|
102
102
|
scalarTypes: [...SCALAR_TYPES],
|
|
103
103
|
// BSON / document-store types
|
|
104
104
|
bsonTypes: [...BSON_TYPES],
|
|
105
|
+
// Target-native types (Oracle `number`, PostgreSQL `serial`, ...):
|
|
106
|
+
// colored like scalar types, but not built-ins -- see keywords.ts.
|
|
107
|
+
nativeTypes: [...TARGET_NATIVE_TYPES],
|
|
105
108
|
// Bare-flag settings: `pk`, `unique`, `not null`, etc.
|
|
106
109
|
// `not null` and `primary key` are two words but tokenized one at a time
|
|
107
110
|
// here -- the highlighter colors each as `keyword.setting`.
|
|
@@ -165,6 +168,7 @@ export const xdbmlMonarchTokensProvider = {
|
|
|
165
168
|
'@decls': 'keyword.declaration',
|
|
166
169
|
'@scalarTypes': 'type',
|
|
167
170
|
'@bsonTypes': 'type.bson',
|
|
171
|
+
'@nativeTypes': 'type',
|
|
168
172
|
'@settingFlags': 'keyword.setting',
|
|
169
173
|
'@settingKeys': 'keyword.setting',
|
|
170
174
|
'@granularityValues': 'keyword.value',
|
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' | 'unresolved-entity' | 'unresolved-field' | 'unresolved-partial' | 'unresolved-tablegroup-member' | 'unresolved-records-entity' | 'unresolved-records-column' | 'empty-import' | 'invalid-nested-path' | 'foreign-master-composite' | 'foreign-master-duplicate-child' | 'foreign-master-without-ref' | 'construct-requires-version' | 'ambiguous-ref-endpoint' | 'invalid-constraint-type' | 'constraint-type-on-foreign-master' | 'invalid-undirected' | 'entity-level-many-to-many' | 'missing-supertype' | 'unresolved-supertype-group-member' | 'invalid-supertype-group-value' | 'duplicate-subtype' | 'supertype-is-subtype' | 'subtype-in-multiple-groups' | 'supertype-cycle' | 'supertype-attribute-redeclared' | 'discriminator-on-overlapping-group' | 'merge-without-roll-up' | 'empty-supertype-group';
|
|
70
|
+
export type DiagnosticCode = 'duplicate-declaration' | 'unresolved-type' | 'possible-type-typo' | 'unresolved-key-field' | 'unresolved-index-field' | 'key-path-crosses-collection' | 'invalid-key-flags' | 'duplicate-primary-key' | 'null-in-primary-key' | 'duplicate-constraints-block' | 'ref-target-not-key' | 'unresolved-entity' | 'unresolved-field' | 'unresolved-partial' | 'unresolved-tablegroup-member' | 'unresolved-records-entity' | 'unresolved-records-column' | 'empty-import' | 'invalid-nested-path' | 'foreign-master-composite' | 'foreign-master-duplicate-child' | 'foreign-master-without-ref' | 'construct-requires-version' | 'ambiguous-ref-endpoint' | 'invalid-constraint-type' | 'constraint-type-on-foreign-master' | 'invalid-undirected' | 'entity-level-many-to-many' | 'missing-supertype' | 'unresolved-supertype-group-member' | 'invalid-supertype-group-value' | 'duplicate-subtype' | 'supertype-is-subtype' | 'subtype-in-multiple-groups' | 'supertype-cycle' | 'supertype-attribute-redeclared' | 'discriminator-on-overlapping-group' | 'merge-without-roll-up' | 'empty-supertype-group';
|
|
71
71
|
/**
|
|
72
72
|
* A single resolution diagnostic. Severity is currently always `error`,
|
|
73
73
|
* but the field is included to leave room for future warnings (e.g.,
|
package/dist/name-resolver.js
CHANGED
|
@@ -42,6 +42,7 @@ import { SCALAR_TYPES, BSON_TYPES } from "./keywords.js";
|
|
|
42
42
|
import { flatten } from "./module-resolver.js";
|
|
43
43
|
import { checkRelationships } from "./relationships.js";
|
|
44
44
|
import { checkSupertypeGroups } from "./supertypes.js";
|
|
45
|
+
import { checkConstraints } from "./constraints.js";
|
|
45
46
|
/**
|
|
46
47
|
* Read-only handle on the collected symbol table.
|
|
47
48
|
*
|
|
@@ -96,11 +97,18 @@ export class SymbolTable {
|
|
|
96
97
|
* Built-in type recognition
|
|
97
98
|
*
|
|
98
99
|
* Field type expressions can name a builtin scalar (`int`, `varchar`),
|
|
99
|
-
* a BSON type (`objectId`),
|
|
100
|
-
*
|
|
100
|
+
* a BSON type (`objectId`), a user-defined Named Type (`Email`) or Enum,
|
|
101
|
+
* or any other target-native type (`number`, `clob`, `serial`). The
|
|
102
|
+
* parser doesn't distinguish at parse time -- they all land as
|
|
101
103
|
* ScalarType nodes (or NamedTypeReference in some contexts). The
|
|
102
|
-
* resolver uses these sets
|
|
103
|
-
*
|
|
104
|
+
* resolver uses these sets only to skip the symbol-table lookup for
|
|
105
|
+
* names that can never be a Named Type reference.
|
|
106
|
+
*
|
|
107
|
+
* The sets are not a type vocabulary. Scalar type names pass through
|
|
108
|
+
* as written (spec §1.2, principle 4), so a name that is neither a
|
|
109
|
+
* builtin nor a declared Type or Enum is a target-native type, not an
|
|
110
|
+
* error. See `nearMissTypeName()` for the one diagnostic such a name
|
|
111
|
+
* can raise.
|
|
104
112
|
*
|
|
105
113
|
* Matching is case-insensitive: `Int`, `int`, `INT` all map to the same
|
|
106
114
|
* builtin per spec §3.8.
|
|
@@ -112,6 +120,87 @@ const BUILTIN_TYPES = new Set([
|
|
|
112
120
|
function isBuiltinType(name) {
|
|
113
121
|
return BUILTIN_TYPES.has(name.toLowerCase());
|
|
114
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* The declared Types and Enums a bare type name can refer to. More than
|
|
125
|
+
* one entry means the name is declared in several containers (Enums can
|
|
126
|
+
* be container-scoped); the name still refers to a declaration, so it
|
|
127
|
+
* is neither a target-native type nor a near miss.
|
|
128
|
+
*/
|
|
129
|
+
function typeOrEnumDeclarations(name, symbols) {
|
|
130
|
+
const qualified = symbols.lookup(name);
|
|
131
|
+
if (qualified && (qualified.kind === 'type' || qualified.kind === 'enum'))
|
|
132
|
+
return [qualified];
|
|
133
|
+
return symbols.lookupAllBare(name).filter((e) => e.kind === 'type' || e.kind === 'enum');
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Near-miss detection for target-native type names.
|
|
137
|
+
*
|
|
138
|
+
* A type name that is neither a builtin nor a declared Type or Enum is
|
|
139
|
+
* accepted as a target-native type. The one case worth flagging is a
|
|
140
|
+
* name that differs only slightly from a declared Type or Enum -- most
|
|
141
|
+
* likely a misspelling (`Adress` for `Address`, `email` for `Email`).
|
|
142
|
+
* Returns the declared name to suggest, or undefined when nothing is
|
|
143
|
+
* close enough.
|
|
144
|
+
*
|
|
145
|
+
* Comparison is case-insensitive, so a difference of case alone counts
|
|
146
|
+
* as a near miss (identifiers are case-sensitive, spec §3.8). The edit
|
|
147
|
+
* distance allowed grows with the length of the name: none beyond case
|
|
148
|
+
* for three characters or fewer, one edit up to seven characters, two
|
|
149
|
+
* edits from eight. An adjacent transposition counts as one edit.
|
|
150
|
+
*/
|
|
151
|
+
function nearMissTypeName(name, symbols) {
|
|
152
|
+
const lower = name.toLowerCase();
|
|
153
|
+
const maxDistance = lower.length <= 3 ? 0 : lower.length <= 7 ? 1 : 2;
|
|
154
|
+
// A qualified name (`core.job_stauts`) compares against qualified
|
|
155
|
+
// declarations, a bare name against bare ones.
|
|
156
|
+
const qualified = name.includes('.');
|
|
157
|
+
let best;
|
|
158
|
+
for (const entry of symbols.entries()) {
|
|
159
|
+
if (entry.kind !== 'type' && entry.kind !== 'enum')
|
|
160
|
+
continue;
|
|
161
|
+
const declared = qualified ? entry.qualifiedName : entry.name;
|
|
162
|
+
const candidate = declared.toLowerCase();
|
|
163
|
+
if (Math.abs(candidate.length - lower.length) > maxDistance)
|
|
164
|
+
continue;
|
|
165
|
+
const distance = editDistance(lower, candidate, maxDistance);
|
|
166
|
+
if (distance <= maxDistance && (!best || distance < best.distance)) {
|
|
167
|
+
best = { name: declared, distance };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return best?.name;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Optimal string alignment distance (Levenshtein plus adjacent
|
|
174
|
+
* transposition). Returns `limit + 1` as soon as every alignment of a
|
|
175
|
+
* row exceeds `limit`, since callers only compare against the limit.
|
|
176
|
+
*/
|
|
177
|
+
function editDistance(a, b, limit) {
|
|
178
|
+
const rows = a.length + 1;
|
|
179
|
+
const cols = b.length + 1;
|
|
180
|
+
const d = [];
|
|
181
|
+
for (let i = 0; i < rows; i++) {
|
|
182
|
+
d.push(new Array(cols).fill(0));
|
|
183
|
+
d[i][0] = i;
|
|
184
|
+
}
|
|
185
|
+
for (let j = 0; j < cols; j++)
|
|
186
|
+
d[0][j] = j;
|
|
187
|
+
for (let i = 1; i < rows; i++) {
|
|
188
|
+
let rowMin = Infinity;
|
|
189
|
+
for (let j = 1; j < cols; j++) {
|
|
190
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
191
|
+
let v = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
|
|
192
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
193
|
+
v = Math.min(v, d[i - 2][j - 2] + 1);
|
|
194
|
+
}
|
|
195
|
+
d[i][j] = v;
|
|
196
|
+
if (v < rowMin)
|
|
197
|
+
rowMin = v;
|
|
198
|
+
}
|
|
199
|
+
if (rowMin > limit)
|
|
200
|
+
return limit + 1;
|
|
201
|
+
}
|
|
202
|
+
return d[rows - 1][cols - 1];
|
|
203
|
+
}
|
|
115
204
|
/* -------------------------------------------------------------------------
|
|
116
205
|
* Main entry point
|
|
117
206
|
* ----------------------------------------------------------------------- */
|
|
@@ -140,6 +229,8 @@ export function resolveNames(doc) {
|
|
|
140
229
|
diagnostics.push(...checkRelationships(flat));
|
|
141
230
|
// Pass 4: supertype group rules (spec 12.8).
|
|
142
231
|
diagnostics.push(...checkSupertypeGroups(flat));
|
|
232
|
+
// Pass 5: constraints and referenced keys (spec §10.10, §11.17).
|
|
233
|
+
diagnostics.push(...checkConstraints(flat));
|
|
143
234
|
return { diagnostics, symbols };
|
|
144
235
|
}
|
|
145
236
|
/* -------------------------------------------------------------------------
|
|
@@ -159,9 +250,20 @@ function addTopLevelDeclaration(stmt, entries, diagnostics, seen) {
|
|
|
159
250
|
case 'TypeDeclaration':
|
|
160
251
|
addEntry(stmt.name, undefined, 'type', stmt, stmt.span, entries, diagnostics, seen);
|
|
161
252
|
return;
|
|
162
|
-
case 'EnumDeclaration':
|
|
163
|
-
|
|
253
|
+
case 'EnumDeclaration': {
|
|
254
|
+
// `enum core.job_status { ... }` (the DBML form) files the Enum
|
|
255
|
+
// under container `core`, exactly as `Container core { Enum
|
|
256
|
+
// job_status { ... } }` does: same qualified name, same bare name,
|
|
257
|
+
// and declaring both is a duplicate (spec §16).
|
|
258
|
+
const dot = stmt.name.lastIndexOf('.');
|
|
259
|
+
if (dot > 0) {
|
|
260
|
+
addEntry(stmt.name.slice(dot + 1), stmt.name.slice(0, dot), 'enum', stmt, stmt.span, entries, diagnostics, seen);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
addEntry(stmt.name, undefined, 'enum', stmt, stmt.span, entries, diagnostics, seen);
|
|
264
|
+
}
|
|
164
265
|
return;
|
|
266
|
+
}
|
|
165
267
|
case 'EdgeDeclaration':
|
|
166
268
|
addEntry(stmt.name, undefined, 'edge', stmt, stmt.span, entries, diagnostics, seen);
|
|
167
269
|
return;
|
|
@@ -363,16 +465,18 @@ function resolveFieldDeclaration(field, symbols, diagnostics) {
|
|
|
363
465
|
function resolveTypeExpression(expr, symbols, diagnostics) {
|
|
364
466
|
switch (expr.kind) {
|
|
365
467
|
case 'ScalarType':
|
|
366
|
-
// A ScalarType is
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
468
|
+
// A ScalarType is a builtin, a reference to a declared Type or
|
|
469
|
+
// Enum, or a target-native type passed through as written (spec
|
|
470
|
+
// §1.2, principle 4) -- the parser doesn't distinguish at parse
|
|
471
|
+
// time. A target-native name is valid; the only diagnostic is a
|
|
472
|
+
// warning when it is a near miss of a declared Type or Enum.
|
|
473
|
+
if (!isBuiltinType(expr.name) && typeOrEnumDeclarations(expr.name, symbols).length === 0) {
|
|
474
|
+
const suggestion = nearMissTypeName(expr.name, symbols);
|
|
475
|
+
if (suggestion) {
|
|
372
476
|
diagnostics.push({
|
|
373
|
-
severity: '
|
|
374
|
-
code: '
|
|
375
|
-
message: `Type '${expr.name}' is not
|
|
477
|
+
severity: 'warning',
|
|
478
|
+
code: 'possible-type-typo',
|
|
479
|
+
message: `Type '${expr.name}' is not declared; did you mean '${suggestion}'? Otherwise it is kept as a target-native type.`,
|
|
376
480
|
span: expr.span,
|
|
377
481
|
});
|
|
378
482
|
}
|
|
@@ -824,9 +928,18 @@ function dereferenceNamedType(type, symbols, depth) {
|
|
|
824
928
|
return type;
|
|
825
929
|
}
|
|
826
930
|
const sym = symbols.lookup(name) ?? symbols.lookupBare(name);
|
|
931
|
+
if (type.kind === 'ScalarType' && (!sym || sym.kind !== 'type')) {
|
|
932
|
+
// An Enum or a target-native type: an opaque scalar, so a path that
|
|
933
|
+
// navigates into it gets the walker's shape diagnostic. A name
|
|
934
|
+
// declared as a Type or Enum in several containers stays unresolved.
|
|
935
|
+
if (typeOrEnumDeclarations(name, symbols).length > 1)
|
|
936
|
+
return undefined;
|
|
937
|
+
return type;
|
|
938
|
+
}
|
|
827
939
|
if (!sym || sym.kind !== 'type' || sym.declaration.kind !== 'TypeDeclaration') {
|
|
828
|
-
// Unresolved -- the field-type pass
|
|
829
|
-
// undefined so the walker bails without emitting a
|
|
940
|
+
// Unresolved Named Type reference -- the field-type pass diagnoses
|
|
941
|
+
// this. Return undefined so the walker bails without emitting a
|
|
942
|
+
// duplicate error.
|
|
830
943
|
return undefined;
|
|
831
944
|
}
|
|
832
945
|
const td = sym.declaration;
|
package/dist/parser.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export declare class ParseError extends Error {
|
|
|
26
26
|
* declaring a later version is refused (spec 4.1) rather than parsed with
|
|
27
27
|
* semantics it does not have.
|
|
28
28
|
*/
|
|
29
|
-
export declare const SUPPORTED_XDBML_VERSION = "0.
|
|
29
|
+
export declare const SUPPORTED_XDBML_VERSION = "0.6";
|
|
30
30
|
/** Compare dotted version strings numerically: -1, 0 or 1. */
|
|
31
31
|
export declare function compareVersions(a: string, b: string): number;
|
|
32
32
|
export declare class Parser {
|
|
@@ -326,6 +326,8 @@ export declare class Parser {
|
|
|
326
326
|
private parseIndexComponent;
|
|
327
327
|
private parseChecks;
|
|
328
328
|
private parseCheckEntry;
|
|
329
|
+
private parseConstraints;
|
|
330
|
+
private parseKeyConstraintEntry;
|
|
329
331
|
private maybeSettingsBlock;
|
|
330
332
|
/**
|
|
331
333
|
* A single setting. Forms:
|
package/dist/parser.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* semantic-analysis pass. The grammar test cases the parser passes are
|
|
10
10
|
* the official xDBML example files in /examples.
|
|
11
11
|
*/
|
|
12
|
+
import { markPrimaryKeyNotNull } from "./constraints.js";
|
|
12
13
|
import { TokenKind, tokenize, } from "./lexer.js";
|
|
13
14
|
import { resolveImport, classifyModuleSource, ModuleSourceError } from "./module-resolver.js";
|
|
14
15
|
export class ParseError extends Error {
|
|
@@ -31,7 +32,7 @@ export class ParseError extends Error {
|
|
|
31
32
|
* declaring a later version is refused (spec 4.1) rather than parsed with
|
|
32
33
|
* semantics it does not have.
|
|
33
34
|
*/
|
|
34
|
-
export const SUPPORTED_XDBML_VERSION = '0.
|
|
35
|
+
export const SUPPORTED_XDBML_VERSION = '0.6';
|
|
35
36
|
/** Compare dotted version strings numerically: -1, 0 or 1. */
|
|
36
37
|
export function compareVersions(a, b) {
|
|
37
38
|
const pa = a.split('.').map((n) => Number(n) || 0);
|
|
@@ -191,13 +192,16 @@ export class Parser {
|
|
|
191
192
|
while (!this.check(TokenKind.EOF)) {
|
|
192
193
|
statements.push(this.parseTopLevelStatement());
|
|
193
194
|
}
|
|
194
|
-
|
|
195
|
+
const doc = {
|
|
195
196
|
kind: 'XDbmlDocument',
|
|
196
197
|
version,
|
|
197
198
|
experimental,
|
|
198
199
|
statements,
|
|
199
200
|
span: this.spanFrom(start),
|
|
200
201
|
};
|
|
202
|
+
// v0.6 §10.4: primary key fields are not null in the AST.
|
|
203
|
+
markPrimaryKeyNotNull(doc);
|
|
204
|
+
return doc;
|
|
201
205
|
}
|
|
202
206
|
/* ----- version & experimental ----- */
|
|
203
207
|
parseVersionDeclaration() {
|
|
@@ -487,6 +491,11 @@ export class Parser {
|
|
|
487
491
|
else if (k === 'checks') {
|
|
488
492
|
body.push(this.parseChecks());
|
|
489
493
|
}
|
|
494
|
+
else if (k === 'constraints' && this.peek(1).kind === TokenKind.LBrace) {
|
|
495
|
+
// v0.6 §10.1: a keyword only when `{` follows, so a field may keep
|
|
496
|
+
// the name `constraints`.
|
|
497
|
+
body.push(this.parseConstraints());
|
|
498
|
+
}
|
|
490
499
|
else if (k === 'records') {
|
|
491
500
|
body.push(this.parseRecordsBlock());
|
|
492
501
|
}
|
|
@@ -1289,7 +1298,14 @@ export class Parser {
|
|
|
1289
1298
|
throw new ParseError(`Expected type name, got ${t.kind} ${JSON.stringify(t.text)}`, t.start);
|
|
1290
1299
|
}
|
|
1291
1300
|
this.advance();
|
|
1292
|
-
|
|
1301
|
+
let name = t.kind === TokenKind.QuotedIdentifier ? (t.value ?? '') : t.text;
|
|
1302
|
+
// A qualified type name, `core.job_status`, names an Enum declared in
|
|
1303
|
+
// a container (spec §16), or a target-native type such as
|
|
1304
|
+
// `public.geometry` that passes through as written.
|
|
1305
|
+
while (this.check(TokenKind.Dot)) {
|
|
1306
|
+
this.advance();
|
|
1307
|
+
name += `.${this.parseIdentLikeName('type name after dot')}`;
|
|
1308
|
+
}
|
|
1293
1309
|
let params;
|
|
1294
1310
|
if (this.check(TokenKind.LParen)) {
|
|
1295
1311
|
this.advance();
|
|
@@ -1455,7 +1471,13 @@ export class Parser {
|
|
|
1455
1471
|
parseEnum() {
|
|
1456
1472
|
const start = this.peek().start;
|
|
1457
1473
|
const kwTok = this.advance(); // enum
|
|
1458
|
-
|
|
1474
|
+
// DBML form `enum core.job_status { ... }`: the qualifier names the
|
|
1475
|
+
// container, as for a schema-qualified Table (spec §7.3, §16).
|
|
1476
|
+
let name = this.parseIdentLikeName('enum name');
|
|
1477
|
+
while (this.check(TokenKind.Dot)) {
|
|
1478
|
+
this.advance();
|
|
1479
|
+
name += `.${this.parseIdentLikeName('enum name after dot')}`;
|
|
1480
|
+
}
|
|
1459
1481
|
this.expect(TokenKind.LBrace, "Expected '{' after enum name");
|
|
1460
1482
|
const values = [];
|
|
1461
1483
|
while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
|
|
@@ -1919,17 +1941,71 @@ export class Parser {
|
|
|
1919
1941
|
parseCheckEntry() {
|
|
1920
1942
|
const start = this.peek().start;
|
|
1921
1943
|
const exprTok = this.peek();
|
|
1922
|
-
|
|
1923
|
-
|
|
1944
|
+
// v0.6 §10.5: single (or triple) quotes are the standard form and
|
|
1945
|
+
// backticks an alias. The resolver rejects the quoted form in documents
|
|
1946
|
+
// declaring an earlier version.
|
|
1947
|
+
const quoted = exprTok.kind === TokenKind.StringLiteral || exprTok.kind === TokenKind.MultilineString;
|
|
1948
|
+
if (exprTok.kind !== TokenKind.ExpressionLiteral && !quoted) {
|
|
1949
|
+
throw new ParseError(`Expected a check expression in quotes or backticks, got ${exprTok.kind} ${JSON.stringify(exprTok.text)}`, exprTok.start);
|
|
1924
1950
|
}
|
|
1925
1951
|
this.advance();
|
|
1926
|
-
// The expression is opaque to xDBML
|
|
1927
|
-
//
|
|
1952
|
+
// The expression is opaque to xDBML (spec §10.5). The token value
|
|
1953
|
+
// already has the delimiters stripped.
|
|
1928
1954
|
const expression = exprTok.value ?? '';
|
|
1929
1955
|
const settings = this.maybeSettingsBlock();
|
|
1930
1956
|
return {
|
|
1931
1957
|
kind: 'CheckEntry',
|
|
1932
1958
|
expression,
|
|
1959
|
+
delimiter: quoted ? 'quote' : 'backtick',
|
|
1960
|
+
settings,
|
|
1961
|
+
span: this.spanFrom(start),
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
/* ----- constraints block (v0.6 §10) ----- */
|
|
1965
|
+
parseConstraints() {
|
|
1966
|
+
const start = this.peek().start;
|
|
1967
|
+
this.advance(); // constraints
|
|
1968
|
+
this.expect(TokenKind.LBrace, "Expected '{' after constraints");
|
|
1969
|
+
const entries = [];
|
|
1970
|
+
while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
|
|
1971
|
+
const t = this.peek();
|
|
1972
|
+
if (t.kind === TokenKind.StringLiteral ||
|
|
1973
|
+
t.kind === TokenKind.MultilineString ||
|
|
1974
|
+
t.kind === TokenKind.ExpressionLiteral) {
|
|
1975
|
+
entries.push(this.parseCheckEntry());
|
|
1976
|
+
}
|
|
1977
|
+
else {
|
|
1978
|
+
entries.push(this.parseKeyConstraintEntry());
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
this.expect(TokenKind.RBrace, "Expected '}' closing constraints");
|
|
1982
|
+
return {
|
|
1983
|
+
kind: 'ConstraintsBlock',
|
|
1984
|
+
entries,
|
|
1985
|
+
span: this.spanFrom(start),
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
parseKeyConstraintEntry() {
|
|
1989
|
+
const start = this.peek().start;
|
|
1990
|
+
const fields = [];
|
|
1991
|
+
let parenthesized = false;
|
|
1992
|
+
if (this.check(TokenKind.LParen)) {
|
|
1993
|
+
parenthesized = true;
|
|
1994
|
+
this.advance(); // (
|
|
1995
|
+
fields.push(this.parsePathSegments());
|
|
1996
|
+
while (this.match(TokenKind.Comma)) {
|
|
1997
|
+
fields.push(this.parsePathSegments());
|
|
1998
|
+
}
|
|
1999
|
+
this.expect(TokenKind.RParen, "Expected ')' closing the fields of a key");
|
|
2000
|
+
}
|
|
2001
|
+
else {
|
|
2002
|
+
fields.push(this.parsePathSegments());
|
|
2003
|
+
}
|
|
2004
|
+
const settings = this.maybeSettingsBlock();
|
|
2005
|
+
return {
|
|
2006
|
+
kind: 'KeyConstraintEntry',
|
|
2007
|
+
fields,
|
|
2008
|
+
parenthesized,
|
|
1933
2009
|
settings,
|
|
1934
2010
|
span: this.spanFrom(start),
|
|
1935
2011
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xdbml/parse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Parser for xDBML (eXtended Database Markup Language), a strict superset of DBML 3.13.6: tokenizer, parser, module resolver, and name resolver.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Hackolade",
|