@google-cloud/kcmd 0.1.0 → 0.2.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/README.md +18 -20
- package/build/ts/kcmd/cli.js +6164 -5631
- package/build/ts/kcmd/gcp/biglake.d.ts +2 -0
- package/build/ts/kcmd/gcp/biglake.js +13 -4
- package/build/ts/kcmd/gcp/spanner.d.ts +10 -0
- package/build/ts/kcmd/gcp/spanner.js +6 -3
- package/build/ts/kcmd/layouts/okf.js +49 -7
- package/build/ts/kcmd/layouts/semantic-model.d.ts +13 -0
- package/build/ts/kcmd/layouts/semantic-model.js +129 -50
- package/build/ts/kcmd/okf/concept.d.ts +1 -1
- package/build/ts/kcmd/okf/concept.js +6 -0
- package/build/ts/kcmd/okf/concept_to_entry.d.ts +2 -0
- package/build/ts/kcmd/okf/concept_to_entry.js +29 -2
- package/build/ts/kcmd/okf/init_okf_workspace.js +7 -9
- package/build/ts/kcmd/okf/okf_metadata.js +17 -0
- package/build/ts/kcmd/semantic/bigquery.js +139 -195
- package/build/ts/kcmd/semantic/binding.js +14 -19
- package/build/ts/kcmd/semantic/deploy_bigquery.js +6 -13
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +10 -35
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +105 -162
- package/build/ts/kcmd/semantic/deploy_spanner.js +7 -14
- package/build/ts/kcmd/semantic/deployment_target.d.ts +1 -4
- package/build/ts/kcmd/semantic/deployment_target.js +6 -62
- package/build/ts/kcmd/semantic/graph_ddl.d.ts +10 -0
- package/build/ts/kcmd/semantic/graph_ddl.js +152 -59
- package/build/ts/kcmd/semantic/import_model.d.ts +26 -4
- package/build/ts/kcmd/semantic/import_model.js +531 -111
- package/build/ts/kcmd/semantic/ir.d.ts +2 -4
- package/build/ts/kcmd/semantic/kc_converter.d.ts +7 -1
- package/build/ts/kcmd/semantic/kc_converter.js +32 -27
- package/build/ts/kcmd/semantic/kc_custom_types.d.ts +5 -12
- package/build/ts/kcmd/semantic/kc_custom_types.js +43 -114
- package/build/ts/kcmd/semantic/kc_ids.d.ts +4 -5
- package/build/ts/kcmd/semantic/kc_ids.js +4 -7
- package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +0 -3
- package/build/ts/kcmd/semantic/knowledge_catalog.js +3 -86
- package/build/ts/kcmd/semantic/loader.d.ts +7 -11
- package/build/ts/kcmd/semantic/loader.js +282 -171
- package/build/ts/kcmd/semantic/osi_converter.js +14 -65
- package/build/ts/kcmd/semantic/parameters.d.ts +1 -0
- package/build/ts/kcmd/semantic/parameters.js +22 -3
- package/build/ts/kcmd/semantic/pull_kc.d.ts +1 -5
- package/build/ts/kcmd/semantic/pull_kc.js +38 -112
- package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +7 -2
- package/build/ts/kcmd/semantic/resolve_inheritance.js +37 -21
- package/build/ts/kcmd/semantic/resolve_profiles.d.ts +51 -10
- package/build/ts/kcmd/semantic/resolve_profiles.js +213 -32
- package/build/ts/kcmd/semantic/runtime/runtime.d.ts +4 -12
- package/build/ts/kcmd/semantic/runtime/runtime.js +4 -21
- package/build/ts/kcmd/semantic/runtime/store.d.ts +14 -19
- package/build/ts/kcmd/semantic/runtime/store.js +21 -37
- package/build/ts/kcmd/semantic/source_dialect.d.ts +9 -2
- package/build/ts/kcmd/semantic/source_dialect.js +83 -33
- package/build/ts/kcmd/semantic/source_entries.d.ts +1 -1
- package/build/ts/kcmd/semantic/source_entries.js +95 -36
- package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +13 -7
- package/build/ts/kcmd/semantic/sql_expr_utils.js +240 -67
- package/build/ts/kcmd/semantic/sql_parser.d.ts +5 -2
- package/build/ts/kcmd/semantic/sql_parser.js +22 -0
- package/build/ts/kcmd/semantic/transpile.d.ts +12 -10
- package/build/ts/kcmd/semantic/transpile.js +79 -61
- package/build/ts/kcmd/semantic/validate.d.ts +37 -2
- package/build/ts/kcmd/semantic/validate.js +467 -126
- package/build/ts/kcmd/snapshot.d.ts +4 -0
- package/build/ts/kcmd/snapshot.js +7 -0
- package/build/ts/kcmd/sync.js +44 -7
- package/package.json +1 -1
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
// 3. Element metadata. The per-element `OPTIONS` clause carrying
|
|
17
17
|
// descriptions and synonyms (BigQuery has one; Spanner has none).
|
|
18
18
|
// 4. SQL text. Which SQL a field binds to: the engine's own dialect entry
|
|
19
|
-
// (`BIGQUERY` or `SPANNER`), falling back to the `ANSI_SQL` one.
|
|
19
|
+
// (`BIGQUERY` or `SPANNER`), falling back to the `ANSI_SQL` one. A field
|
|
20
|
+
// that names a sibling field reads that sibling's SQL in its place (see
|
|
21
|
+
// fieldSql).
|
|
20
22
|
//
|
|
21
23
|
// ./bigquery supplies the BigQuery engine, and ./spanner the Spanner engine.
|
|
22
24
|
//
|
|
@@ -28,6 +30,7 @@ const ir_1 = require("./ir");
|
|
|
28
30
|
const resolve_inheritance_1 = require("./resolve_inheritance");
|
|
29
31
|
const sql_expr_utils_1 = require("./sql_expr_utils");
|
|
30
32
|
const sql_identifiers_1 = require("./sql_identifiers");
|
|
33
|
+
const sql_parser_1 = require("./sql_parser");
|
|
31
34
|
/**
|
|
32
35
|
* Generates the property-graph DDL for a semantic model on one engine.
|
|
33
36
|
*
|
|
@@ -45,8 +48,10 @@ function generateGraphDdl(model, engine) {
|
|
|
45
48
|
// flattens each subclass's inherited fields down and expands `extends` to the
|
|
46
49
|
// full transitive ancestor set, so labels and property signatures can be read
|
|
47
50
|
// straight off the resolved model. Only the graph legs run this; the KC push
|
|
48
|
-
// consumes the raw model, so its output is unaffected.
|
|
49
|
-
|
|
51
|
+
// consumes the raw model, so its output is unaffected. An inherited field
|
|
52
|
+
// that names a field of its ancestor names the subtype's field of that name,
|
|
53
|
+
// and renders from its SQL (see fieldSql).
|
|
54
|
+
const resolved = (0, resolve_inheritance_1.resolveInheritance)(model, undefined, { requalify: true });
|
|
50
55
|
warnings.push(...resolved.warnings);
|
|
51
56
|
// The IR requires entities/relationships/metrics, but be defensive against a
|
|
52
57
|
// hand-built or partially-deserialized model rather than throwing on `.map`.
|
|
@@ -76,8 +81,7 @@ function generateGraphDdl(model, engine) {
|
|
|
76
81
|
// A graph node table requires a non-empty KEY: the entity's primary key, else
|
|
77
82
|
// its first unique key (nodeKey). An entity with neither cannot form a valid
|
|
78
83
|
// node, so skip it (and, below, any edge that references it) rather than emit
|
|
79
|
-
// `KEY()` — invalid DDL.
|
|
80
|
-
// records the resulting structural drop.
|
|
84
|
+
// `KEY()` — invalid DDL.
|
|
81
85
|
const skipped = new Set();
|
|
82
86
|
const validEntities = entities.filter(entity => {
|
|
83
87
|
// An abstract entity is intentionally table-less: skip its node table (it
|
|
@@ -142,7 +146,18 @@ function generateGraphDdl(model, engine) {
|
|
|
142
146
|
// it). The result is keyed by entity name; entries on a skipped entity simply
|
|
143
147
|
// never render.
|
|
144
148
|
const entityByName = new Map(entities.map(e => [e.name, e]));
|
|
145
|
-
|
|
149
|
+
// The SQL `sql` gives, or, when a field on the way has none, what a warning
|
|
150
|
+
// says it reads: each field on `through`, then those `sql` went through.
|
|
151
|
+
const readOf = (entity, sql, through) => 'sql' in sql ? { sql: sql.sql } : {
|
|
152
|
+
reads: missingSibling('through' in sql ? [...through, ...sql.through] : through, entity, engine)
|
|
153
|
+
};
|
|
154
|
+
const measurePropertiesByEntity = engine.placeMetrics(metrics, {
|
|
155
|
+
entityByName,
|
|
156
|
+
skipped,
|
|
157
|
+
ancestorsUsed,
|
|
158
|
+
renderedFieldSql: (entity, field) => readOf(entity, fieldSql(field, entity, engine), [field.name]),
|
|
159
|
+
localSql: (entity, sql) => readOf(entity, siblingsRead(sql, entity, name => fieldSql(entity.fields.find(f => f.name === name), entity, engine)), []),
|
|
160
|
+
}, warnings);
|
|
146
161
|
// A subclass's `LABEL <ancestor>` block lists that ancestor's own (flattened)
|
|
147
162
|
// signature, so the label renderer must reach every LABEL CARRIER by name:
|
|
148
163
|
// node-forming entities plus abstract (table-less) supertypes, which have no
|
|
@@ -160,7 +175,7 @@ function generateGraphDdl(model, engine) {
|
|
|
160
175
|
const leftOutUnder = new Map();
|
|
161
176
|
for (const e of validEntities) {
|
|
162
177
|
for (const f of e.fields) {
|
|
163
|
-
if (!
|
|
178
|
+
if (!isLeftOut(f, e, engine))
|
|
164
179
|
continue;
|
|
165
180
|
for (const a of e.extends ?? []) {
|
|
166
181
|
leftOutUnder.set(a, (leftOutUnder.get(a) ?? new Set()).add(f.name));
|
|
@@ -219,11 +234,11 @@ function renderNodeTable(entity, measureProperties, labelByName, leftOutUnder, i
|
|
|
219
234
|
for (const f of ancestor.fields) {
|
|
220
235
|
// The ancestor's node table leaves out a field this engine has no SQL
|
|
221
236
|
// for, so there is no rendering to defer to.
|
|
222
|
-
if (
|
|
237
|
+
if (!('sql' in fieldSql(f, ancestor, engine)))
|
|
223
238
|
continue;
|
|
224
239
|
if (!inheritedRender.has(f.name)) {
|
|
225
|
-
inheritedRender.set(f.name, renderFieldProperty(f, ancestor
|
|
226
|
-
inheritedCore.set(f.name, renderFieldPropertyCore(f, ancestor
|
|
240
|
+
inheritedRender.set(f.name, renderFieldProperty(f, ancestor, engine));
|
|
241
|
+
inheritedCore.set(f.name, renderFieldPropertyCore(f, ancestor, engine));
|
|
227
242
|
}
|
|
228
243
|
}
|
|
229
244
|
}
|
|
@@ -238,13 +253,13 @@ function renderNodeTable(entity, measureProperties, labelByName, leftOutUnder, i
|
|
|
238
253
|
// subclass loses, rather than mislabeling an added description as a redefined
|
|
239
254
|
// column.
|
|
240
255
|
const renderOwn = (f) => {
|
|
241
|
-
const own = renderFieldProperty(f, entity
|
|
256
|
+
const own = renderFieldProperty(f, entity, engine);
|
|
242
257
|
const canonical = inheritedRender.get(f.name);
|
|
243
258
|
if (canonical === undefined)
|
|
244
259
|
return own; // not inherited: render as-is
|
|
245
260
|
if (canonical === own)
|
|
246
261
|
return canonical; // identical: nothing to warn
|
|
247
|
-
if (renderFieldPropertyCore(f, entity
|
|
262
|
+
if (renderFieldPropertyCore(f, entity, engine) !==
|
|
248
263
|
inheritedCore.get(f.name)) {
|
|
249
264
|
warnings.push(`entity '${entity.name}' remaps inherited property '${f.name}' to a different column/expression than its supertype; ` +
|
|
250
265
|
`${engine.name} allows only one definition per property under a ` +
|
|
@@ -270,19 +285,26 @@ function renderNodeTable(entity, measureProperties, labelByName, leftOutUnder, i
|
|
|
270
285
|
// pruning normally strips it before generation, but a caller that generates
|
|
271
286
|
// DDL straight from a bindingOptional load without pruning could still reach
|
|
272
287
|
// here with one, such as a purely logical field. Skip it too rather than emit
|
|
273
|
-
// `<name>` as a phantom bare column the source table does not have.
|
|
288
|
+
// `<name>` as a phantom bare column the source table does not have. A field
|
|
289
|
+
// that names a sibling the engine has no SQL for has none either, so it is
|
|
290
|
+
// skipped with a warning naming that sibling.
|
|
274
291
|
const boundFields = entity.fields.filter(f => {
|
|
275
|
-
|
|
292
|
+
const sql = fieldSql(f, entity, engine);
|
|
293
|
+
if ('sql' in sql)
|
|
276
294
|
return true;
|
|
277
|
-
|
|
278
|
-
`
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
`
|
|
295
|
+
const omitted = inheritedRender.has(f.name) ?
|
|
296
|
+
`node table's own label, and the supertype's label keeps the ` +
|
|
297
|
+
`supertype's definition` :
|
|
298
|
+
`node table`;
|
|
299
|
+
warnings.push('through' in sql ?
|
|
300
|
+
`entity '${entity.name}': field '${f.name}' reads ${missingSibling(sql.through, entity, engine)}; omitted from ` +
|
|
301
|
+
`the ${omitted}` :
|
|
302
|
+
(0, ir_1.isFieldBound)(f) ?
|
|
303
|
+
`entity '${entity.name}': field '${f.name}' has no '${engine.dialect}' or 'ANSI_SQL' expression; omitted from the ` +
|
|
304
|
+
omitted :
|
|
305
|
+
`entity '${entity.name}': field '${f.name}' has no column under ` +
|
|
306
|
+
`this binding; omitted from the node table (bind it, or ` +
|
|
307
|
+
`govern the logical model in Knowledge Catalog instead)`);
|
|
286
308
|
return false;
|
|
287
309
|
});
|
|
288
310
|
const properties = [...boundFields.map(renderOwn), ...measureProperties];
|
|
@@ -320,7 +342,7 @@ function renderNodeTable(entity, measureProperties, labelByName, leftOutUnder, i
|
|
|
320
342
|
lines.push(line(2, 'DEFAULT LABEL'));
|
|
321
343
|
if (labelOpts)
|
|
322
344
|
lines.push(line(2, labelOpts));
|
|
323
|
-
lines.push(
|
|
345
|
+
lines.push(propertiesClause(properties));
|
|
324
346
|
// Inheritance: declare one LABEL per transitive ancestor (resolveInheritance
|
|
325
347
|
// expanded `extends` to the full ancestor set), each re-listing that
|
|
326
348
|
// ancestor's properties so `MATCH (:Ancestor)` also matches this subclass
|
|
@@ -347,7 +369,6 @@ function renderNodeTable(entity, measureProperties, labelByName, leftOutUnder, i
|
|
|
347
369
|
}
|
|
348
370
|
lines.push(line(2, `LABEL ${(0, sql_identifiers_1.quoteIfReserved)(ancestorName)}`));
|
|
349
371
|
let signature;
|
|
350
|
-
let anyLeftOut = false;
|
|
351
372
|
if (ancestor.abstract) {
|
|
352
373
|
// Render each inherited field from THIS subtype's own binding, keyed by
|
|
353
374
|
// the abstract ancestor's field names. Skip any the subtype leaves
|
|
@@ -356,38 +377,97 @@ function renderNodeTable(entity, measureProperties, labelByName, leftOutUnder, i
|
|
|
356
377
|
// simply does not expose that property under the shared label.
|
|
357
378
|
signature = [];
|
|
358
379
|
for (const af of ancestor.fields) {
|
|
359
|
-
if (leftOutUnder.get(ancestorName)?.has(af.name))
|
|
360
|
-
anyLeftOut = true;
|
|
380
|
+
if (leftOutUnder.get(ancestorName)?.has(af.name))
|
|
361
381
|
continue;
|
|
362
|
-
}
|
|
363
382
|
const child = boundFields.find(cf => cf.name === af.name);
|
|
364
383
|
if (child) {
|
|
365
|
-
signature.push(renderFieldProperty(child, entity
|
|
384
|
+
signature.push(renderFieldProperty(child, entity, engine));
|
|
366
385
|
}
|
|
367
386
|
}
|
|
368
387
|
}
|
|
369
388
|
else {
|
|
370
389
|
// Only the fields the ancestor's own node table lists.
|
|
371
390
|
signature =
|
|
372
|
-
ancestor.fields.filter(f =>
|
|
373
|
-
.map(f => renderFieldProperty(f, ancestor
|
|
374
|
-
anyLeftOut =
|
|
375
|
-
ancestor.fields.some(f => hasOnlyOtherEnginesSql(f, engine));
|
|
391
|
+
ancestor.fields.filter(f => 'sql' in fieldSql(f, ancestor, engine))
|
|
392
|
+
.map(f => renderFieldProperty(f, ancestor, engine));
|
|
376
393
|
}
|
|
377
|
-
lines.push(
|
|
394
|
+
lines.push(propertiesClause(signature));
|
|
378
395
|
}
|
|
379
396
|
return lines.join('\n');
|
|
380
397
|
}
|
|
381
|
-
// The properties clause of a table or
|
|
382
|
-
//
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
return
|
|
398
|
+
// The properties clause of a node table, label or edge. An empty list gives NO
|
|
399
|
+
// PROPERTIES: BigQuery and Spanner read a missing clause as PROPERTIES ARE ALL
|
|
400
|
+
// COLUMNS, which would expose every column of the table, including one a
|
|
401
|
+
// profile leaves out.
|
|
402
|
+
function propertiesClause(properties) {
|
|
403
|
+
return properties.length ? propertiesBlock(properties) :
|
|
404
|
+
line(2, 'NO PROPERTIES');
|
|
387
405
|
}
|
|
388
|
-
// Whether a field has SQL, but
|
|
389
|
-
|
|
390
|
-
|
|
406
|
+
// Whether a field has SQL, but the engine has none to render it from: none of
|
|
407
|
+
// its SQL is the engine's, or it names a sibling that has none.
|
|
408
|
+
function isLeftOut(field, owner, engine) {
|
|
409
|
+
return (0, ir_1.isFieldBound)(field) && !('sql' in fieldSql(field, owner, engine));
|
|
410
|
+
}
|
|
411
|
+
// The SQL `field` of `owner` renders from on `engine`. The text is the
|
|
412
|
+
// engine's (fieldExpression), and a field may name a sibling field in it as
|
|
413
|
+
// `<owner>.<field>`. Each sibling it names is replaced with that sibling's
|
|
414
|
+
// own SQL for the engine, in parentheses unless that SQL is a lone column, and
|
|
415
|
+
// every other column loses the owner's qualifier; the rest of the text stays
|
|
416
|
+
// as written. Both engines' SQL is read as BigQuery's.
|
|
417
|
+
// Throws when the field names itself, directly or through its siblings, since
|
|
418
|
+
// no SQL can render it; push validation rejects such a model first
|
|
419
|
+
// (validateExpressionReferences).
|
|
420
|
+
function fieldSql(field, owner, engine, path = []) {
|
|
421
|
+
const expr = engine.fieldExpression(field);
|
|
422
|
+
if (expr === undefined)
|
|
423
|
+
return { noSql: true };
|
|
424
|
+
const chain = [...path, field.name];
|
|
425
|
+
return siblingsRead(expr, owner, name => {
|
|
426
|
+
if (chain.includes(name)) {
|
|
427
|
+
throw new Error((0, sql_expr_utils_1.selfReferenceMessage)(owner.name, chain.slice(chain.indexOf(name))));
|
|
428
|
+
}
|
|
429
|
+
return fieldSql(owner.fields.find(f => f.name === name), owner, engine, chain);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
// `expr`, SQL on `owner`'s table, local to that table, with each sibling it
|
|
433
|
+
// names as `<owner>.<field>` replaced with what `read` gives for that sibling
|
|
434
|
+
// (see fieldSql). When a sibling has no SQL, `through` runs from it to the
|
|
435
|
+
// sibling with none.
|
|
436
|
+
function siblingsRead(expr, owner, read) {
|
|
437
|
+
const named = new Set(((0, sql_expr_utils_1.qualifiedColumns)(expr, owner.name) ?? [])
|
|
438
|
+
.filter(c => owner.fields.some(f => f.name === c)));
|
|
439
|
+
const replacements = new Map();
|
|
440
|
+
for (const name of named) {
|
|
441
|
+
const sibling = read(name);
|
|
442
|
+
if ('noSql' in sibling)
|
|
443
|
+
return { through: [name] };
|
|
444
|
+
if ('through' in sibling)
|
|
445
|
+
return { through: [name, ...sibling.through] };
|
|
446
|
+
// A sibling bound to a reserved column, such as `order`, reads it quoted.
|
|
447
|
+
// One bound to the column of its own name reads the same either way.
|
|
448
|
+
const sql = (0, sql_identifiers_1.quoteIfReserved)(sibling.sql);
|
|
449
|
+
if (sql !== name)
|
|
450
|
+
replacements.set(name, parenthesized(sql));
|
|
451
|
+
}
|
|
452
|
+
return { sql: (0, sql_expr_utils_1.withSiblingsSpliced)(expr, owner.name, replacements) };
|
|
453
|
+
}
|
|
454
|
+
// `sql` in parentheses, unless it is a lone column or already in them.
|
|
455
|
+
function parenthesized(sql) {
|
|
456
|
+
const tree = (0, sql_parser_1.sqlExpressionTree)(sql, 'BIGQUERY');
|
|
457
|
+
const kind = tree && Object.keys(tree).length === 1 ?
|
|
458
|
+
Object.keys(tree)[0] :
|
|
459
|
+
undefined;
|
|
460
|
+
return kind === 'column' || kind === 'paren' ? sql : `(${sql})`;
|
|
461
|
+
}
|
|
462
|
+
// What a warning says a field reads when it names a sibling the engine has no
|
|
463
|
+
// SQL for: each sibling on `through`, and what the last one lacks.
|
|
464
|
+
function missingSibling(through, owner, engine) {
|
|
465
|
+
const last = owner.fields.find(f => f.name === through[through.length - 1]);
|
|
466
|
+
const lacks = last && (0, ir_1.isFieldBound)(last) ?
|
|
467
|
+
`has no '${engine.dialect}' or 'ANSI_SQL' expression` :
|
|
468
|
+
`has no column under this binding`;
|
|
469
|
+
return `${through.map(n => `field '${n}'`).join(', which reads ')}, ` +
|
|
470
|
+
`which ${lacks}`;
|
|
391
471
|
}
|
|
392
472
|
// Renders a field as a graph property: a bare column when the expression is
|
|
393
473
|
// just the column, else `<expr> AS <name>`. Attaches the engine's metadata
|
|
@@ -395,8 +475,8 @@ function hasOnlyOtherEnginesSql(field, engine) {
|
|
|
395
475
|
// per the `derived_property` rule. A node table leaves out a field with no
|
|
396
476
|
// expression, with a warning. An association edge exposes it as a bare column
|
|
397
477
|
// under its name.
|
|
398
|
-
function renderFieldProperty(field,
|
|
399
|
-
const core = renderFieldPropertyCore(field,
|
|
478
|
+
function renderFieldProperty(field, owner, engine) {
|
|
479
|
+
const core = renderFieldPropertyCore(field, owner, engine);
|
|
400
480
|
const opts = engine.fieldOptions(field);
|
|
401
481
|
return opts ? `${core} ${opts}` : core;
|
|
402
482
|
}
|
|
@@ -409,13 +489,17 @@ function renderFieldProperty(field, entity, engine) {
|
|
|
409
489
|
// renderOwn). This is also the single place a field expression is lowered to
|
|
410
490
|
// its table-local form: the resolve-inheritance pass first rewrites an
|
|
411
491
|
// inherited field's expression into the child's frame (localizeInheritedField),
|
|
412
|
-
// then
|
|
492
|
+
// then fieldSql strips the entity's own qualifier and reads each sibling the
|
|
493
|
+
// expression names through that sibling's SQL -- one normalization, in one
|
|
413
494
|
// place, applied to own and inherited fields alike.
|
|
414
|
-
function renderFieldPropertyCore(field,
|
|
415
|
-
const
|
|
416
|
-
const local =
|
|
495
|
+
function renderFieldPropertyCore(field, owner, engine) {
|
|
496
|
+
const sql = fieldSql(field, owner, engine);
|
|
497
|
+
const local = 'sql' in sql ? sql.sql : field.name;
|
|
417
498
|
const alias = (0, sql_identifiers_1.quoteIfReserved)(field.name);
|
|
418
|
-
|
|
499
|
+
// A field bound to the column of its name, such as `order` bound to
|
|
500
|
+
// `` `order` ``, is that column.
|
|
501
|
+
return local === field.name || local === alias ?
|
|
502
|
+
alias :
|
|
419
503
|
`${(0, sql_identifiers_1.quoteIfReserved)(local)} AS ${alias}`;
|
|
420
504
|
}
|
|
421
505
|
// The key an entity's node is keyed by: its primary key, else its first
|
|
@@ -488,10 +572,14 @@ function renderEdgeTable(rel, entitiesByName, engine, warnings) {
|
|
|
488
572
|
line(2, `DESTINATION KEY(${destFk}) REFERENCES ${(0, sql_identifiers_1.quoteIfReserved)(rel.destination.entity)}(${destRef})`),
|
|
489
573
|
];
|
|
490
574
|
// Edge description and synonyms attach to the DEFAULT LABEL: after the
|
|
491
|
-
// SOURCE/DESTINATION clauses (grammar:
|
|
575
|
+
// SOURCE/DESTINATION clauses, before the properties clause (grammar:
|
|
576
|
+
// element_table_definition).
|
|
492
577
|
const labelOpts = engine.elementOptions(rel.description, rel.aiContext);
|
|
493
578
|
if (labelOpts)
|
|
494
579
|
lines.push(line(2, labelOpts));
|
|
580
|
+
// A foreign-key relationship declares no fields of its own, so its edge
|
|
581
|
+
// lists none.
|
|
582
|
+
lines.push(propertiesClause([]));
|
|
495
583
|
return lines.join('\n');
|
|
496
584
|
}
|
|
497
585
|
// Renders a many-to-many edge backed by an association (junction) table. Unlike
|
|
@@ -531,18 +619,23 @@ function renderAssociationEdge(rel, assoc, entitiesByName, engine, warnings) {
|
|
|
531
619
|
const labelOpts = engine.elementOptions(rel.description, rel.aiContext);
|
|
532
620
|
if (labelOpts)
|
|
533
621
|
lines.push(line(2, labelOpts));
|
|
534
|
-
// The junction's own non-key fields are the edge's properties.
|
|
535
|
-
|
|
622
|
+
// The junction's own non-key fields are the edge's properties. They name
|
|
623
|
+
// each other under the relationship's name.
|
|
624
|
+
const owner = { name: rel.name, fields: assoc.fields ?? [] };
|
|
625
|
+
const properties = owner.fields
|
|
536
626
|
.filter(f => {
|
|
537
|
-
|
|
627
|
+
const sql = fieldSql(f, owner, engine);
|
|
628
|
+
if ('sql' in sql || !(0, ir_1.isFieldBound)(f))
|
|
538
629
|
return true;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
|
|
630
|
+
warnings.push(`relationship '${rel.name}': field '${f.name}' ` +
|
|
631
|
+
('through' in sql ?
|
|
632
|
+
`reads ${missingSibling(sql.through, owner, engine)}` :
|
|
633
|
+
`has no '${engine.dialect}' or 'ANSI_SQL' expression`) +
|
|
634
|
+
`; omitted from the edge table`);
|
|
542
635
|
return false;
|
|
543
636
|
})
|
|
544
|
-
.map(f => renderFieldProperty(f,
|
|
545
|
-
lines.push(
|
|
637
|
+
.map(f => renderFieldProperty(f, owner, engine));
|
|
638
|
+
lines.push(propertiesClause(properties));
|
|
546
639
|
return lines.join('\n');
|
|
547
640
|
}
|
|
548
641
|
// All generated indentation flows through this single mechanism: one nesting
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SerializedFile } from './osi_converter';
|
|
2
|
+
import { SourceDialects } from './source_dialect';
|
|
2
3
|
/** Options controlling how a semantic model YAML document is imported. */
|
|
3
4
|
export interface ImportSemanticModelOptions {
|
|
4
5
|
/**
|
|
@@ -7,9 +8,22 @@ export interface ImportSemanticModelOptions {
|
|
|
7
8
|
flavor?: 'ossie' | 'google';
|
|
8
9
|
/**
|
|
9
10
|
* First-party GCP container resource name used to qualify bare table
|
|
10
|
-
* sources
|
|
11
|
+
* sources, or the name of a Knowledge Catalog entry group
|
|
12
|
+
* (`projects/<p>/locations/<l>/entryGroups/<g>`) whose table entries complete
|
|
13
|
+
* them by their fullyQualifiedName.
|
|
11
14
|
*/
|
|
12
15
|
sourcePrefix?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Lists every entry of an entry group, as `CatalogClient.listEntries` does.
|
|
18
|
+
* Required when `sourcePrefix` names an entry group.
|
|
19
|
+
*/
|
|
20
|
+
listEntries?: EntryLister;
|
|
21
|
+
/**
|
|
22
|
+
* Reads the dialect of each Cloud SQL, Spanner and third-party table the
|
|
23
|
+
* files `texts` name, as `sourceDialects` does for push. Without it, the
|
|
24
|
+
* dialect of each such table is unknown.
|
|
25
|
+
*/
|
|
26
|
+
readDialects?: (texts: string[]) => Promise<SourceDialects>;
|
|
13
27
|
/** When true, translates vendor or double-quoted ANSI SQL to `BIGQUERY`. */
|
|
14
28
|
transpile?: boolean;
|
|
15
29
|
/**
|
|
@@ -18,16 +32,24 @@ export interface ImportSemanticModelOptions {
|
|
|
18
32
|
*/
|
|
19
33
|
strict?: boolean;
|
|
20
34
|
/**
|
|
21
|
-
* Sibling `<model>.profile.<name>.yaml` files located beside the input file
|
|
22
|
-
*
|
|
35
|
+
* Sibling `<model>.profile.<name>.yaml` files located beside the input file.
|
|
36
|
+
* A conversion to vanilla writes them into the model file, and a
|
|
37
|
+
* Google-flavor import of a Google-flavor model writes them beside it.
|
|
23
38
|
*/
|
|
24
39
|
profileFiles?: SerializedFile[];
|
|
25
40
|
/**
|
|
26
41
|
* The input file's name. The warning that a JSON input was written as YAML
|
|
27
|
-
* names it
|
|
42
|
+
* names it, or `<model>.yaml` when it is unset. When set, in strict mode
|
|
43
|
+
* each message about the model's bindings names the input file or profile
|
|
44
|
+
* file that holds them.
|
|
28
45
|
*/
|
|
29
46
|
fileName?: string;
|
|
30
47
|
}
|
|
48
|
+
/** Lists the entries of an entry group. */
|
|
49
|
+
export type EntryLister = (project: string, location: string, entryGroup: string) => AsyncIterable<{
|
|
50
|
+
name: string;
|
|
51
|
+
fullyQualifiedName?: string;
|
|
52
|
+
}>;
|
|
31
53
|
/** Result of importing a semantic model YAML document. */
|
|
32
54
|
export interface ImportSemanticModelResult {
|
|
33
55
|
/** True when `errors` is empty and `files` was produced. */
|