@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.
Files changed (67) hide show
  1. package/README.md +18 -20
  2. package/build/ts/kcmd/cli.js +6164 -5631
  3. package/build/ts/kcmd/gcp/biglake.d.ts +2 -0
  4. package/build/ts/kcmd/gcp/biglake.js +13 -4
  5. package/build/ts/kcmd/gcp/spanner.d.ts +10 -0
  6. package/build/ts/kcmd/gcp/spanner.js +6 -3
  7. package/build/ts/kcmd/layouts/okf.js +49 -7
  8. package/build/ts/kcmd/layouts/semantic-model.d.ts +13 -0
  9. package/build/ts/kcmd/layouts/semantic-model.js +129 -50
  10. package/build/ts/kcmd/okf/concept.d.ts +1 -1
  11. package/build/ts/kcmd/okf/concept.js +6 -0
  12. package/build/ts/kcmd/okf/concept_to_entry.d.ts +2 -0
  13. package/build/ts/kcmd/okf/concept_to_entry.js +29 -2
  14. package/build/ts/kcmd/okf/init_okf_workspace.js +7 -9
  15. package/build/ts/kcmd/okf/okf_metadata.js +17 -0
  16. package/build/ts/kcmd/semantic/bigquery.js +139 -195
  17. package/build/ts/kcmd/semantic/binding.js +14 -19
  18. package/build/ts/kcmd/semantic/deploy_bigquery.js +6 -13
  19. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +10 -35
  20. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +105 -162
  21. package/build/ts/kcmd/semantic/deploy_spanner.js +7 -14
  22. package/build/ts/kcmd/semantic/deployment_target.d.ts +1 -4
  23. package/build/ts/kcmd/semantic/deployment_target.js +6 -62
  24. package/build/ts/kcmd/semantic/graph_ddl.d.ts +10 -0
  25. package/build/ts/kcmd/semantic/graph_ddl.js +152 -59
  26. package/build/ts/kcmd/semantic/import_model.d.ts +26 -4
  27. package/build/ts/kcmd/semantic/import_model.js +531 -111
  28. package/build/ts/kcmd/semantic/ir.d.ts +2 -4
  29. package/build/ts/kcmd/semantic/kc_converter.d.ts +7 -1
  30. package/build/ts/kcmd/semantic/kc_converter.js +32 -27
  31. package/build/ts/kcmd/semantic/kc_custom_types.d.ts +5 -12
  32. package/build/ts/kcmd/semantic/kc_custom_types.js +43 -114
  33. package/build/ts/kcmd/semantic/kc_ids.d.ts +4 -5
  34. package/build/ts/kcmd/semantic/kc_ids.js +4 -7
  35. package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +0 -3
  36. package/build/ts/kcmd/semantic/knowledge_catalog.js +3 -86
  37. package/build/ts/kcmd/semantic/loader.d.ts +7 -11
  38. package/build/ts/kcmd/semantic/loader.js +282 -171
  39. package/build/ts/kcmd/semantic/osi_converter.js +14 -65
  40. package/build/ts/kcmd/semantic/parameters.d.ts +1 -0
  41. package/build/ts/kcmd/semantic/parameters.js +22 -3
  42. package/build/ts/kcmd/semantic/pull_kc.d.ts +1 -5
  43. package/build/ts/kcmd/semantic/pull_kc.js +38 -112
  44. package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +7 -2
  45. package/build/ts/kcmd/semantic/resolve_inheritance.js +37 -21
  46. package/build/ts/kcmd/semantic/resolve_profiles.d.ts +51 -10
  47. package/build/ts/kcmd/semantic/resolve_profiles.js +213 -32
  48. package/build/ts/kcmd/semantic/runtime/runtime.d.ts +4 -12
  49. package/build/ts/kcmd/semantic/runtime/runtime.js +4 -21
  50. package/build/ts/kcmd/semantic/runtime/store.d.ts +14 -19
  51. package/build/ts/kcmd/semantic/runtime/store.js +21 -37
  52. package/build/ts/kcmd/semantic/source_dialect.d.ts +9 -2
  53. package/build/ts/kcmd/semantic/source_dialect.js +83 -33
  54. package/build/ts/kcmd/semantic/source_entries.d.ts +1 -1
  55. package/build/ts/kcmd/semantic/source_entries.js +95 -36
  56. package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +13 -7
  57. package/build/ts/kcmd/semantic/sql_expr_utils.js +240 -67
  58. package/build/ts/kcmd/semantic/sql_parser.d.ts +5 -2
  59. package/build/ts/kcmd/semantic/sql_parser.js +22 -0
  60. package/build/ts/kcmd/semantic/transpile.d.ts +12 -10
  61. package/build/ts/kcmd/semantic/transpile.js +79 -61
  62. package/build/ts/kcmd/semantic/validate.d.ts +37 -2
  63. package/build/ts/kcmd/semantic/validate.js +467 -126
  64. package/build/ts/kcmd/snapshot.d.ts +4 -0
  65. package/build/ts/kcmd/snapshot.js +7 -0
  66. package/build/ts/kcmd/sync.js +44 -7
  67. 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
- const resolved = (0, resolve_inheritance_1.resolveInheritance)(model);
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. The loader already warns about the missing key; this
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
- const measurePropertiesByEntity = engine.placeMetrics(metrics, { entityByName, skipped, ancestorsUsed }, warnings);
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 (!hasOnlyOtherEnginesSql(f, engine))
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 (engine.fieldExpression(f) === undefined)
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.name, engine));
226
- inheritedCore.set(f.name, renderFieldPropertyCore(f, ancestor.name, engine));
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.name, engine);
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.name, engine) !==
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
- if (engine.fieldExpression(f) !== undefined)
292
+ const sql = fieldSql(f, entity, engine);
293
+ if ('sql' in sql)
276
294
  return true;
277
- warnings.push((0, ir_1.isFieldBound)(f) ?
278
- `entity '${entity.name}': field '${f.name}' has no '${engine.dialect}' or 'ANSI_SQL' expression; omitted from the ` +
279
- (inheritedRender.has(f.name) ?
280
- `node table's own label, and the supertype's label keeps ` +
281
- `the supertype's definition` :
282
- `node table`) :
283
- `entity '${entity.name}': field '${f.name}' has no column under ` +
284
- `this binding; omitted from the node table (bind it, or ` +
285
- `govern the logical model in Knowledge Catalog instead)`);
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(...propertiesClause(properties, entity.fields.some(f => hasOnlyOtherEnginesSql(f, engine))));
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.name, engine));
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 => engine.fieldExpression(f) !== undefined)
373
- .map(f => renderFieldProperty(f, ancestor.name, engine));
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(...propertiesClause(signature, anyLeftOut));
394
+ lines.push(propertiesClause(signature));
378
395
  }
379
396
  return lines.join('\n');
380
397
  }
381
- // The properties clause of a table or label. When `anyLeftOut` is set, an
382
- // empty list gives NO PROPERTIES, since a missing clause exposes every column.
383
- function propertiesClause(properties, anyLeftOut) {
384
- if (properties.length)
385
- return [propertiesBlock(properties)];
386
- return anyLeftOut ? [line(2, 'NO PROPERTIES')] : [];
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 none the engine reads.
389
- function hasOnlyOtherEnginesSql(field, engine) {
390
- return (0, ir_1.isFieldBound)(field) && engine.fieldExpression(field) === undefined;
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, entity, engine) {
399
- const core = renderFieldPropertyCore(field, entity, engine);
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 this strips the entity's own qualifier -- one normalization, in one
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, entity, engine) {
415
- const expr = engine.fieldExpression(field);
416
- const local = expr !== undefined ? (0, sql_expr_utils_1.stripQualifier)(expr, entity) : field.name;
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
- return local === field.name ? alias :
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: element_table_definition).
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
- const properties = (assoc.fields ?? [])
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
- if (engine.fieldExpression(f) !== undefined || !(0, ir_1.isFieldBound)(f)) {
627
+ const sql = fieldSql(f, owner, engine);
628
+ if ('sql' in sql || !(0, ir_1.isFieldBound)(f))
538
629
  return true;
539
- }
540
- warnings.push(`relationship '${rel.name}': field '${f.name}' has no '${engine.dialect}' or 'ANSI_SQL' expression; omitted from ` +
541
- `the edge table`);
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, rel.name, engine));
545
- lines.push(...propertiesClause(properties, (assoc.fields ?? []).some(f => hasOnlyOtherEnginesSql(f, engine))));
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
- * used when converting a Google-flavor model to vanilla (`'ossie'`).
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. Defaults to `<model>.yaml`.
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. */