@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
@@ -19,12 +19,11 @@
19
19
  //
20
20
  // Publish sequence (split into read-only `preflightKnowledgeCatalog` and write
21
21
  // `applyKnowledgeCatalog`):
22
- // * Preflight validates that exactly one model is deployed per entry group
23
- // and that each file copy fits in one aspect, snapshots the destination
24
- // entry group to guard against foreign models (unless `--force-remove` is
25
- // set), and checks that any required custom entry/aspect types exist in
26
- // the target project. With the second-generation aspects, it also checks
27
- // every aspect's size.
22
+ // * Preflight validates that exactly one model is deployed per entry group,
23
+ // snapshots the destination entry group to guard against foreign models
24
+ // (unless `--force-remove` is set), and checks that any required custom
25
+ // entry/aspect types exist in the target project. With the
26
+ // second-generation aspects, it also checks every aspect's size.
28
27
  // * Apply creates or updates each model's entries in dependency order: the
29
28
  // `semantic-model` anchor first, then independent child entries
30
29
  // concurrently, and finally dependent child entries (`semantic-metric` and
@@ -44,6 +43,8 @@
44
43
  // * Reconcile deletions: after writing, delete any child entry this push owns
45
44
  // (by `ownedPrefixes`) that is present in the pre-write listing but was not
46
45
  // re-emitted.
46
+ // * The anchor's write labels it `kcmd-push-state: writing`, and the push's
47
+ // last write, after the deletions, relabels it `finished`.
47
48
  //
48
49
  // This is a library module: it emits no console output. Warnings and the
49
50
  // dry-run plan are returned in `KcDeployResult` for the CLI (commands.ts) to
@@ -88,9 +89,7 @@ exports.applyKnowledgeCatalog = applyKnowledgeCatalog;
88
89
  exports.deployKnowledgeCatalog = deployKnowledgeCatalog;
89
90
  exports.deployEmittedModels = deployEmittedModels;
90
91
  exports.oneModelPerEntryGroupMessage = oneModelPerEntryGroupMessage;
91
- exports.checkFileCopies = checkFileCopies;
92
92
  exports.checkCatalogAspects = checkCatalogAspects;
93
- exports.checkCustomTypes = checkCustomTypes;
94
93
  const crm = __importStar(require("../gcp/crm"));
95
94
  const dataplex_1 = require("../gcp/dataplex");
96
95
  const kc_custom_types_1 = require("./kc_custom_types");
@@ -109,35 +108,19 @@ function sleep(ms) {
109
108
  *
110
109
  * The method performs the following actions:
111
110
  * 1. Translates the IR models into catalog resources.
112
- * 2. Verifies that exactly one model is being deployed and that each file
113
- * copy fits in one aspect, builds the dry-run plan, and returns early
114
- * when `opts.validateOnly` is set.
115
- * 3. Lists the destination entry group and fails if an unrecognized model's
116
- * anchor entry is present without `--force-remove`.
111
+ * 2. Verifies that exactly one model is being deployed, builds the dry-run
112
+ * plan, and returns early when `opts.validateOnly` is set.
113
+ * 3. Fails when the destination entry group does not exist. Lists it and
114
+ * fails if an unrecognized model's anchor entry is present without
115
+ * `--force-remove`.
117
116
  * 4. Verifies that every custom entry type and aspect type required by the
118
- * model exists in the target project. Skips the file-copy type when
119
- * `opts.fileCopyTypeChecked` is set.
117
+ * model exists in the target project.
120
118
  * 5. Returns the emitted resources, entry-group listing, warnings, and plan
121
119
  * as `prepared` for `applyKnowledgeCatalog`.
122
120
  * With `opts.v2Aspects` set, step 2 also checks every aspect's size.
123
121
  */
124
122
  async function preflightKnowledgeCatalog(models, ctx, opts) {
125
123
  const emit = emitModels(models, opts);
126
- if (emit.error) {
127
- return {
128
- result: {
129
- success: false,
130
- details: emit.error,
131
- warnings: emit.warnings,
132
- created: 0,
133
- updated: 0,
134
- deleted: 0,
135
- linked: 0,
136
- unlinked: 0,
137
- plan: [],
138
- },
139
- };
140
- }
141
124
  return preflightEmittedModels(emit.emitted, emit.warnings, ctx, opts);
142
125
  }
143
126
  /**
@@ -157,20 +140,10 @@ async function applyKnowledgeCatalog(prepared, ctx, opts) {
157
140
  const { emitted, existing } = prepared;
158
141
  // Resource Manager is asked for the project number at most once per push.
159
142
  const isScopeProject = scopeProjectMatcher(ctx, opts.project);
160
- // Some of this model's writes may have landed before a failure in them.
161
- // The catalog may then lack some or all of this push's file copies, and
162
- // pull refuses that until a push succeeds.
163
- const storesCopies = emitted.some(m => m.resources.entries.some(e => (0, kc_entries_1.entryTypeId)(e) === kc_custom_types_1.FILE_COPY_TYPE_ID));
164
- const failed = (details) => result(false, storesCopies ?
165
- `${details.replace(/\.$/, '')}. The catalog may now lack some or ` +
166
- `all of this push's file copies, so push again before you pull.` :
167
- details);
168
143
  if (opts.forceRemove) {
169
144
  const foreignAnchorIds = findForeignAnchorIds(emitted[0], existing);
170
145
  if (foreignAnchorIds.length) {
171
- // This runs before any of this model's writes. A failure here leaves
172
- // none of this push's file copies, so it gets the note too, and so
173
- // does a thrown error.
146
+ // A thrown error is reported like a failed delete.
174
147
  const removed = await removeForeignModels(cat, opts, existing, foreignAnchorIds, isScopeProject)
175
148
  .catch((err) => ({
176
149
  deleted: 0,
@@ -180,7 +153,7 @@ async function applyKnowledgeCatalog(prepared, ctx, opts) {
180
153
  counts.deleted += removed.deleted;
181
154
  counts.unlinked += removed.unlinked;
182
155
  if (removed.error)
183
- return failed(removed.error);
156
+ return result(false, removed.error);
184
157
  }
185
158
  }
186
159
  // Write each emitted model's entries and relationship links and delete any
@@ -190,7 +163,7 @@ async function applyKnowledgeCatalog(prepared, ctx, opts) {
190
163
  const written = await writeEmittedModels(cat, opts, emitted, existing, counts, isScopeProject)
191
164
  .catch((err) => ({ error: String(err?.message || err).trim() }));
192
165
  if (written.error)
193
- return failed(written.error);
166
+ return result(false, written.error);
194
167
  return result(true);
195
168
  }
196
169
  /**
@@ -223,15 +196,15 @@ async function deployEmittedModels(emitted, emitWarnings, ctx, opts) {
223
196
  * Runs read-only preflight checks on already-emitted models.
224
197
  *
225
198
  * The method performs the following checks:
226
- * 1. Validates locally that `emitted` contains a single model, that each
227
- * file copy fits in one aspect and, with `opts.v2Aspects` set, that every
228
- * aspect size and instance count is within its limit. Builds the
229
- * dry-run plan and returns early when `opts.validateOnly` is set.
230
- * 2. Lists the destination entry group and validates that it contains no
231
- * foreign models unless `--force-remove` is set.
199
+ * 1. Validates locally that `emitted` contains a single model and, with
200
+ * `opts.v2Aspects` set, that every aspect size and instance count is
201
+ * within its limit. Builds the dry-run plan and returns early when
202
+ * `opts.validateOnly` is set.
203
+ * 2. Validates that the destination entry group exists. Lists it and
204
+ * validates that it contains no foreign models unless `--force-remove`
205
+ * is set.
232
206
  * 3. Validates that every custom type used by the model exists in the
233
- * target project. Skips the file-copy type when
234
- * `opts.fileCopyTypeChecked` is set.
207
+ * target project.
235
208
  */
236
209
  async function preflightEmittedModels(emitted, emitWarnings, ctx, opts) {
237
210
  const warnings = [...emitWarnings];
@@ -246,9 +219,6 @@ async function preflightEmittedModels(emitted, emitWarnings, ctx, opts) {
246
219
  // Once `validateEmittedModels` passes and `emitted` is non-empty, `emitted`
247
220
  // is guaranteed to contain exactly one model (`emitted[0]`).
248
221
  const model = emitted[0];
249
- const sizeValidation = validateFileCopySizes(model, result);
250
- if (!sizeValidation.success)
251
- return { result: sizeValidation };
252
222
  const aspectsError = opts.v2Aspects ? emittedAspectsError(model) : undefined;
253
223
  if (aspectsError)
254
224
  return { result: result(false, aspectsError) };
@@ -260,6 +230,14 @@ async function preflightEmittedModels(emitted, emitWarnings, ctx, opts) {
260
230
  // reconciliation. A re-emitted entry is never a deletion candidate, so a
261
231
  // pre-write snapshot is correct for all three.
262
232
  const cat = new dataplex_1.CatalogClient(ctx);
233
+ // The listing reads a missing group as empty, so read the group itself.
234
+ const group = await cat.getEntryGroup(opts.project, opts.location, opts.entryGroup)
235
+ .catch((err) => `reading entry group '${opts.entryGroup}': ${err?.message || String(err)}`);
236
+ if (typeof group === 'string')
237
+ return { result: result(false, group) };
238
+ if (isNotFoundStatus(group)) {
239
+ return { result: result(false, missingEntryGroupMessage(opts)) };
240
+ }
263
241
  const listing = await listEntryGroup(cat, opts);
264
242
  if (listing.error)
265
243
  return { result: result(false, listing.error) };
@@ -315,6 +293,14 @@ function oneModelPerEntryGroupMessage(entryGroup, models) {
315
293
  `only one model per entry group is supported; split them into ` +
316
294
  `separate entry groups.`;
317
295
  }
296
+ /** The error for a push to an entry group that does not exist. */
297
+ function missingEntryGroupMessage(opts) {
298
+ const { project, location, entryGroup } = opts;
299
+ return `entry group '${entryGroup}' does not exist in location ` +
300
+ `'${location}' of project '${project}'; run 'kcmd init ` +
301
+ `--semantic-model ${project}.${location}.${entryGroup}' to create it, ` +
302
+ `or correct the scope in catalog.yaml.`;
303
+ }
318
304
  /**
319
305
  * Validates that `existing` (the destination entry-group snapshot) contains no
320
306
  * foreign `semantic-model` anchors unless `--force-remove` is set.
@@ -339,66 +325,13 @@ function validateEntryGroup(model, existing, opts, result) {
339
325
  exports.MAX_ASPECT_DATA_BYTES = 120 * 1024;
340
326
  const MAX_SCHEMA_ASPECT_DATA_BYTES = 2 * 1024 * 1024;
341
327
  /**
342
- * Validates that every file copy in `model` fits in one aspect.
343
- *
344
- * A file too large to store then fails before any write, naming the file,
345
- * rather than partway through the writes. Push runs the same check on its
346
- * copies, with `checkFileCopies`, before any graph deploys.
347
- */
348
- function validateFileCopySizes(model, result) {
349
- for (const entry of model.resources.entries) {
350
- if ((0, kc_entries_1.entryTypeId)(entry) !== kc_custom_types_1.FILE_COPY_TYPE_ID)
351
- continue;
352
- for (const aspect of Object.values(entry.aspects ?? {})) {
353
- const error = fileCopySizeError(aspect.data ?? {});
354
- if (error)
355
- return result(false, error);
356
- }
357
- }
358
- return result(true);
359
- }
360
- /**
361
- * Checks a push's file copies before anything is written. Returns the error
362
- * to report, or undefined when every copy fits in one aspect and no two file
363
- * names map to one entry id.
364
- *
365
- * Push calls it before any graph deploys. Every copy of a model shares the
366
- * model's part of the entry id, so two file names collide when their slugs
367
- * do.
368
- */
369
- function checkFileCopies(files) {
370
- const bySlug = new Map();
371
- for (const file of files) {
372
- const tooLarge = fileCopySizeError(kcEmit.fileCopyData(file));
373
- if (tooLarge)
374
- return tooLarge;
375
- const earlier = bySlug.get((0, kc_ids_1.slug)(file.fileName));
376
- if (earlier !== undefined) {
377
- return `files '${earlier}' and '${file.fileName}' both map to one ` +
378
- `entry id; rename one of them`;
379
- }
380
- bySlug.set((0, kc_ids_1.slug)(file.fileName), file.fileName);
381
- }
382
- return undefined;
383
- }
384
- // Why the aspect data of one file copy is too large to store, or undefined
385
- // when it fits.
386
- function fileCopySizeError(data) {
387
- const bytes = new TextEncoder().encode(JSON.stringify(data)).length;
388
- if (bytes <= exports.MAX_ASPECT_DATA_BYTES)
389
- return undefined;
390
- return `file '${data.fileName}' is too large to store in Knowledge ` +
391
- `Catalog: its copy takes ${bytes} bytes, and one aspect holds at most ` +
392
- `${exports.MAX_ASPECT_DATA_BYTES}.`;
393
- }
394
- /**
395
- * The emitter's warnings for `models` with v2Aspects on, and why push's
396
- * catalog leg cannot write them: an emit error or an aspect too large to
397
- * store. Calls no service.
328
+ * The emitter's warnings for `models`, and the first aspect too large to
329
+ * store. With v2Aspects on, that error is why push's catalog leg cannot write
330
+ * them. Calls no service.
398
331
  */
399
332
  function checkCatalogAspects(models, opts) {
400
333
  const emit = emitModels(models, opts);
401
- const error = emit.error ?? emit.emitted.map(emittedAspectsError).find(e => e);
334
+ const error = emit.emitted.map(emittedAspectsError).find(e => e);
402
335
  return { error, warnings: emit.warnings };
403
336
  }
404
337
  // Why push cannot write what `model` emits, or undefined when it can.
@@ -446,8 +379,7 @@ function aspectTypeIdOfKey(key) {
446
379
  }
447
380
  /**
448
381
  * Validates that every kcmd-owned custom type (`semantic-action`,
449
- * `semantic-constraint`, `semantic-model-file`) used by `model` exists in the
450
- * target project.
382
+ * `semantic-constraint`) used by `model` exists in the target project.
451
383
  *
452
384
  * Unlike built-in system types in `dataplex-types`, custom entry and aspect
453
385
  * types are provisioned in the destination project by `kcmd init`;
@@ -455,22 +387,15 @@ function aspectTypeIdOfKey(key) {
455
387
  * before any catalog entries or entry links are written, rather than failing
456
388
  * mid-push when creating an action or constraint entry.
457
389
  *
458
- * Skips the file-copy type when `opts.fileCopyTypeChecked` is set, because
459
- * the caller has already looked it up. Makes no network calls when no type
460
- * is left to check.
390
+ * Makes no network calls when the model uses no custom type.
461
391
  */
462
392
  async function validateCustomTypes(cat, model, opts, result) {
463
- const typeIds = (0, kc_custom_types_1.collectCustomTypes)(model.resources.entries).filter(id => !(opts.fileCopyTypeChecked && id === kc_custom_types_1.FILE_COPY_TYPE_ID));
464
- const error = await checkCustomTypes(cat, opts, typeIds);
393
+ const error = await checkCustomTypes(cat, opts, (0, kc_custom_types_1.collectCustomTypes)(model.resources.entries));
465
394
  return error ? result(false, error) : result(true);
466
395
  }
467
396
  /**
468
397
  * Checks that each custom type in `typeIds` exists in the destination
469
398
  * project. Returns the error to report, or undefined when every one exists.
470
- *
471
- * Push also calls it before any graph deploys, for the file-copy type every
472
- * push with v2Aspects off needs. Push then sets `fileCopyTypeChecked`, so the
473
- * catalog preflight looks up only the other custom types.
474
399
  */
475
400
  async function checkCustomTypes(cat, dest, typeIds) {
476
401
  const home = (0, kc_custom_types_1.customTypeHome)(dest);
@@ -498,36 +423,22 @@ async function checkCustomTypes(cat, dest, typeIds) {
498
423
  * Turns every authored model into its catalog resources.
499
424
  *
500
425
  * Pure (no network I/O), so the dry-run plan and any generation warnings are
501
- * produced even when a later write fails. A malformed GOOGLE
502
- * `custom_extension` (reached via the `semantic-model` aspect) throws; report
503
- * it against its document, as the BigQuery leg does, rather than letting it
504
- * escape as an uncaught stack trace.
426
+ * produced even when a later write fails.
505
427
  */
506
428
  function emitModels(models, opts) {
507
429
  const emitted = [];
508
430
  const warnings = [];
509
- for (const { document, model } of models) {
510
- let resources;
511
- try {
512
- resources = kcEmit.generateCatalogResources(model, {
513
- project: opts.project,
514
- location: opts.location,
515
- entryGroup: opts.entryGroup,
516
- systemTypeProject: opts.systemTypeProject,
517
- systemTypeLocation: opts.systemTypeLocation,
518
- emitExpressions: opts.emitExpressions,
519
- v2Aspects: opts.v2Aspects,
520
- sourceFiles: opts.sourceFiles,
521
- overviews: opts.overviews,
522
- });
523
- }
524
- catch (err) {
525
- return {
526
- emitted,
527
- warnings,
528
- error: `Model '${model.name}' (${document}): ${err.message || err}`,
529
- };
530
- }
431
+ for (const { model } of models) {
432
+ const resources = kcEmit.generateCatalogResources(model, {
433
+ project: opts.project,
434
+ location: opts.location,
435
+ entryGroup: opts.entryGroup,
436
+ systemTypeProject: opts.systemTypeProject,
437
+ systemTypeLocation: opts.systemTypeLocation,
438
+ emitExpressions: opts.emitExpressions,
439
+ v2Aspects: opts.v2Aspects,
440
+ overviews: opts.overviews,
441
+ });
531
442
  for (const w of resources.warnings)
532
443
  warnings.push(`[${model.name}] ${w}`);
533
444
  emitted.push({ model: model.name, resources });
@@ -571,7 +482,8 @@ function planSummary(model, resources, opts) {
571
482
  * resources in model order.
572
483
  *
573
484
  * For each model, the method performs the following actions:
574
- * 1. Creates or updates the model's entries (anchor first).
485
+ * 1. Creates or updates the model's entries (anchor first, labeled as
486
+ * writing).
575
487
  * 2. Looks up the model's existing relationship links when the pre-write
576
488
  * entry group contains entities owned by the model.
577
489
  * 3. Deletes any orphaned relationship links (no longer emitted) and stale
@@ -581,6 +493,7 @@ function planSummary(model, resources, opts) {
581
493
  * 4. Creates or updates the model's relationship links.
582
494
  * 5. Deletes any existing entries owned by the model that are no longer
583
495
  * emitted.
496
+ * 6. Labels the anchor finished.
584
497
  *
585
498
  * Progress accumulates into `counts`, so a mid-way failure still reports what
586
499
  * had been written.
@@ -684,9 +597,40 @@ async function writeEmittedModels(cat, opts, emitted, existing, counts, isScopeP
684
597
  error: `Deleting orphaned entry '${entryId}' from model '${model}' failed: ${message}`,
685
598
  };
686
599
  }
600
+ if (resources.entries.length) {
601
+ const anchor = resources.entries[0];
602
+ const error = await finishPush(cat, opts, anchor)
603
+ .catch((err) => String(err?.message || err).trim() || 'unknown error');
604
+ if (error) {
605
+ return {
606
+ error: `Model '${model}': setting the ${kc_custom_types_1.PUSH_STATE_LABEL} label of ` +
607
+ `entry '${(0, kc_entries_1.entryId)(anchor)}' to ${kc_custom_types_1.PUSH_FINISHED} failed: ` +
608
+ `${error.replace(/\.$/, '')}. Push again before you pull.`,
609
+ };
610
+ }
611
+ }
687
612
  }
688
613
  return {};
689
614
  }
615
+ /**
616
+ * Labels the model entry `anchor` finished, and returns the error when that
617
+ * fails. It reads the entry first and sends back the entrySource the entry
618
+ * holds then, so what another writer set during the push stays. The update
619
+ * names only the entrySource.
620
+ */
621
+ async function finishPush(cat, opts, anchor) {
622
+ const read = await cat.getEntry(opts.project, opts.location, opts.entryGroup, (0, kc_entries_1.entryId)(anchor), undefined, { keepSourceResource: true });
623
+ if (!isOkStatus(read))
624
+ return errText(read);
625
+ const { name, entryType } = anchor;
626
+ const entrySource = withPushState(read.result?.entrySource, kc_custom_types_1.PUSH_FINISHED);
627
+ const res = await cat.updateEntry({ name, entryType, entrySource }, ['entry_source']);
628
+ return isOkStatus(res) ? undefined : errText(res);
629
+ }
630
+ // `source` with its kcmd-push-state label set to `state`.
631
+ function withPushState(source, state) {
632
+ return { ...source, labels: { ...source?.labels, [kc_custom_types_1.PUSH_STATE_LABEL]: state } };
633
+ }
690
634
  /**
691
635
  * Returns the push-again hint for an error, or '' when no link is missing.
692
636
  *
@@ -1032,8 +976,8 @@ async function removeForeignModels(cat, opts, existing, foreignAnchorIds, isScop
1032
976
  * Creates or updates a model's entries in dependency order.
1033
977
  *
1034
978
  * The method writes entries in three stages:
1035
- * 1. Writes the model anchor (`entries[0]`) first, since it is the parent of
1036
- * every child entry.
979
+ * 1. Writes the model anchor (`entries[0]`) first, labeled as writing, since
980
+ * it is the parent of every child entry.
1037
981
  * 2. Writes independent child entries (such as entities, relationships,
1038
982
  * actions, and constraints) concurrently.
1039
983
  * 3. Writes dependent child entries (`semantic-metric` via
@@ -1051,7 +995,8 @@ async function writeEntries(cat, opts, entries, existing = []) {
1051
995
  const sources = new Map(existing.map(e => [(0, kc_entries_1.entryId)(e), e.entrySource]));
1052
996
  const write = (e) => writeEntry(cat, opts, e, sources);
1053
997
  const [anchor, ...children] = entries;
1054
- const anchorRes = await write(anchor);
998
+ const entrySource = withPushState(anchor.entrySource, kc_custom_types_1.PUSH_WRITING);
999
+ const anchorRes = await write({ ...anchor, entrySource });
1055
1000
  if (anchorRes.error)
1056
1001
  return { created: 0, updated: 0, error: anchorRes.error };
1057
1002
  let created = anchorRes.updated ? 0 : 1;
@@ -1314,18 +1259,16 @@ const KEPT_SOURCE_FIELDS = ['resource', 'system', 'platform', 'createTime', 'upd
1314
1259
  *
1315
1260
  * The update mask replaces the whole entrySource, so the update carries over
1316
1261
  * fields from `source`, the entrySource the entry had before this push. Of
1317
- * those fields kcmd owns the display name, the description and the
1318
- * `kcmd-push-id` label. The update keeps another writer's `resource`,
1319
- * `system`, `platform`, `createTime` and `updateTime`, and every label but
1320
- * `kcmd-push-id`. It sets that label when `entry` carries it and drops it
1321
- * when not. Dataplex refuses an update that leaves out an `updateTime` the
1322
- * entry has, or moves it back. A kept field another writer changes after
1323
- * `source` was read is written back with the old value. For `updateTime` the
1324
- * update fails instead.
1262
+ * those fields kcmd owns the display name and the description. The update
1263
+ * keeps another writer's `resource`, `system`, `platform`, `createTime` and
1264
+ * `updateTime`, and every label. A label `entry` carries, such as the model
1265
+ * entry's `kcmd-push-state`, replaces the stored one. Dataplex refuses an
1266
+ * update that leaves out an `updateTime` the entry has, or moves it back. A
1267
+ * kept field another writer changes after `source` was read is written back
1268
+ * with the old value. For `updateTime` the update fails instead.
1325
1269
  */
1326
1270
  async function updateEntry(cat, opts, entry, source) {
1327
- const { [kc_custom_types_1.PUSH_ID_LABEL]: _own, ...others } = source?.labels ?? {};
1328
- const labels = { ...others, ...entry.entrySource?.labels };
1271
+ const labels = { ...source?.labels, ...entry.entrySource?.labels };
1329
1272
  const kept = Object.fromEntries(KEPT_SOURCE_FIELDS.filter(k => source?.[k] !== undefined)
1330
1273
  .map(k => [k, source[k]]));
1331
1274
  const merged = {
@@ -3,10 +3,10 @@
3
3
  //
4
4
  // This is the Spanner leg of `kcmd push` for the semantic-model scope, a
5
5
  // sibling to deploy_bigquery: it consumes models already parsed into the
6
- // semantic IR (see loadSemanticModels, shared across legs so a multi-destination
7
- // push parses each document once), lowers each to `CREATE OR REPLACE PROPERTY
8
- // GRAPH` DDL (generator: ./spanner), and applies that DDL to the Spanner
9
- // database named by the model's GOOGLE deployment target.
6
+ // semantic IR (see loadSemanticModels, shared across legs so a
7
+ // multi-destination push parses each document once), lowers each to `CREATE OR
8
+ // REPLACE PROPERTY GRAPH` DDL (generator: ./spanner), and applies that DDL to
9
+ // the Spanner database named by the model's deployment target.
10
10
  //
11
11
  // Spanner DDL is asynchronous: updateDatabaseDdl returns a long-running
12
12
  // operation which this leg polls to completion, unlike the BigQuery leg's
@@ -86,23 +86,16 @@ async function deploySpanner(models, ctx, options = {}) {
86
86
  const backoffMs = options.pollBackoffMs ?? POLL_BACKOFF_MS;
87
87
  for (const { document, model } of models) {
88
88
  modelsSeen++;
89
- let targets;
90
- let malformed;
91
- try {
92
- ({ targets, malformed } = (0, deployment_target_1.spannerGraphTargets)(model));
93
- }
94
- catch (err) {
95
- return fail(`Model '${model.name}' (${document}): ${err.message || err}`);
96
- }
89
+ const { targets, malformed } = (0, deployment_target_1.spannerGraphTargets)(model);
97
90
  if (!targets.length) {
98
91
  // A malformed-but-present target must not be reported as "none declared".
99
92
  if (malformed.length) {
100
93
  return fail(`Model '${model.name}' (${document}) declares Spanner Graph ` +
101
- `deploymentTarget(s) that could not be parsed: ` +
94
+ `deployment target(s) that could not be parsed: ` +
102
95
  `${malformed.join(', ')}. Expected ${deployment_target_1.SPANNER_GRAPH_FORM}.`);
103
96
  }
104
97
  return fail(`Model '${model.name}' (${document}) declares no Spanner Graph ` +
105
- `deploymentTarget in a GOOGLE custom_extension; nothing to deploy.`);
98
+ `deployment target; nothing to deploy.`);
106
99
  }
107
100
  for (const bad of malformed) {
108
101
  warnings.push(`[${model.name}] ignoring unparseable Spanner Graph target: ${bad}`);
@@ -1,4 +1,4 @@
1
- import { CustomExtension, DeploymentSpec, SemanticModel } from './ir';
1
+ import { DeploymentSpec, SemanticModel } from './ir';
2
2
  export declare const BQ_GRAPH_FORM = "bigquery.googleapis.com/projects/<p>/datasets/<d>/propertyGraphs/<g>";
3
3
  export declare const SPANNER_GRAPH_FORM = "spanner.googleapis.com/projects/<p>/instances/<i>/databases/<db>/graphs/<g>";
4
4
  export declare const ALLOYDB_FORM = "alloydb.googleapis.com/projects/<p>/locations/<l>/clusters/<c>/databases/<db>";
@@ -45,8 +45,6 @@ export type ResolvedDeployment = ParsedDeploymentTarget & {
45
45
  };
46
46
  /** Why a deployment's target is not accepted in a `deployments:` list. */
47
47
  export declare function deploymentTargetError(model: SemanticModel, d: DeploymentSpec): string;
48
- /** Why a target in a GOOGLE block's `deploymentTargets` is not accepted. */
49
- export declare function googleBlockTargetError(model: SemanticModel, target: string): string;
50
48
  /**
51
49
  * Why a push would reject `target`, or undefined when it accepts it. `at` says
52
50
  * where it is written.
@@ -73,7 +71,6 @@ export declare function resolveModelDeployments(model: SemanticModel, selected?:
73
71
  } | {
74
72
  error: string;
75
73
  };
76
- export declare function googleBlockTargets(exts: readonly CustomExtension[] | undefined, modelName: string): string[];
77
74
  export declare function unnamedDeployments(targets: readonly string[]): Array<{
78
75
  name: string;
79
76
  target: string;
@@ -2,10 +2,8 @@
2
2
  // Reads where a model deploys and classifies each declared target.
3
3
  //
4
4
  // A semantic model names where it deploys with its `deployments:` list. Each
5
- // entry has a name, a target and optionally the binding profile it uses. The
6
- // loader also folds every target into `deploymentTargets` in a GOOGLE
7
- // `custom_extensions` block, which is also where a vanilla document's GOOGLE
8
- // block carries them. Three destination types are recognized here, each a
5
+ // entry has a name, a target and optionally the binding profile it uses. Three
6
+ // destination types are recognized here, each a
9
7
  // GCP resource name, written with no leading `//`:
10
8
  //
11
9
  // BigQuery Graph:
@@ -36,11 +34,9 @@
36
34
  Object.defineProperty(exports, "__esModule", { value: true });
37
35
  exports.ALLOYDB_FORM = exports.SPANNER_GRAPH_FORM = exports.BQ_GRAPH_FORM = void 0;
38
36
  exports.deploymentTargetError = deploymentTargetError;
39
- exports.googleBlockTargetError = googleBlockTargetError;
40
37
  exports.targetProblem = targetProblem;
41
38
  exports.parseDeploymentSelection = parseDeploymentSelection;
42
39
  exports.resolveModelDeployments = resolveModelDeployments;
43
- exports.googleBlockTargets = googleBlockTargets;
44
40
  exports.unnamedDeployments = unnamedDeployments;
45
41
  exports.googleDeploymentTargets = googleDeploymentTargets;
46
42
  exports.bigQueryGraphTargets = bigQueryGraphTargets;
@@ -48,8 +44,6 @@ exports.spannerGraphTargets = spannerGraphTargets;
48
44
  exports.alloyDbTargets = alloyDbTargets;
49
45
  const loader_1 = require("./loader");
50
46
  const resolve_profiles_1 = require("./resolve_profiles");
51
- // Our own vendor tag in the Ossie `custom_extensions` list.
52
- const GOOGLE_VENDOR = 'GOOGLE';
53
47
  // A BigQuery Graph deployment target.
54
48
  const BQ_GRAPH_TARGET = /^\/\/bigquery\.googleapis\.com\/projects\/([A-Za-z0-9_-]+)\/datasets\/([A-Za-z0-9_-]+)\/propertyGraphs\/([A-Za-z0-9_-]+)$/;
55
49
  // A Spanner Graph deployment target, named as Dataplex names the graph's
@@ -140,11 +134,6 @@ function clusterUri(legacy) {
140
134
  function deploymentTargetError(model, d) {
141
135
  return targetError(`Model '${model.name}': deployment '${d.name}' targets '${d.target}'`, d.target);
142
136
  }
143
- /** Why a target in a GOOGLE block's `deploymentTargets` is not accepted. */
144
- function googleBlockTargetError(model, target) {
145
- return targetError(`Model '${model.name}': the GOOGLE custom_extension's ` +
146
- `'deploymentTargets' lists '${target}'`, target);
147
- }
148
137
  // The Dataplex name of a Spanner graph `target` names with `propertyGraphs`,
149
138
  // or undefined for any other target.
150
139
  function spannerGraphName(target) {
@@ -251,53 +240,13 @@ function resolveModelDeployments(model, selected) {
251
240
  }
252
241
  // Memoizes the parse per model object so the several callers within one push
253
242
  // (validation, source-project lookup, per-leg routing, each deploy leg) share a
254
- // single parse rather than re-running JSON.parse + the regexes once per reader.
243
+ // single parse rather than re-running the regexes once per reader.
255
244
  // Keyed by model identity: the IR object is stable through a push (the loader
256
245
  // and inheritance passes each hand on a fixed set of objects) and
257
- // neither its deployments nor its customExtensions are mutated after loading.
246
+ // its deployments are not mutated after loading.
258
247
  // A caller that narrows `deployments` builds a new model object. The returned
259
248
  // value is read-only by convention -- no caller mutates its arrays.
260
249
  const targetsCache = new WeakMap();
261
- // The targets in a model's GOOGLE custom_extension(s). The extension `data` is
262
- // an opaque, vendor-serialized JSON string (the loader keeps it verbatim); we
263
- // own its `deploymentTargets` shape. Throws on malformed extension JSON.
264
- function extensionTargets(model) {
265
- return googleBlockTargets(model.customExtensions, model.name);
266
- }
267
- // The targets listed in the GOOGLE blocks among `exts`, in order. A string
268
- // target is trimmed, and a blank one is dropped. A padded target is the same
269
- // target, so trimming here keeps the duplicate check and the stored list in
270
- // step. An entry that is not a string is kept as its JSON text, which parses
271
- // as no supported destination. Throws when a GOOGLE block's data is not valid
272
- // JSON.
273
- function googleBlockTargets(exts, modelName) {
274
- const uris = [];
275
- for (const ext of exts ?? []) {
276
- if (ext.vendorName !== GOOGLE_VENDOR) {
277
- continue;
278
- }
279
- let data;
280
- try {
281
- data = JSON.parse(ext.data);
282
- }
283
- catch {
284
- throw new Error(`Model '${modelName}': GOOGLE custom_extension 'data' is not valid JSON.`);
285
- }
286
- const list = data?.deploymentTargets;
287
- if (!Array.isArray(list)) {
288
- continue;
289
- }
290
- for (const uri of list) {
291
- if (typeof uri !== 'string') {
292
- uris.push(JSON.stringify(uri));
293
- }
294
- else if (uri.trim() !== '') {
295
- uris.push(uri.trim());
296
- }
297
- }
298
- }
299
- return uris;
300
- }
301
250
  // Deployments for targets whose names are not known: a pull reads only the
302
251
  // targets back from the catalog. One target is `default`; several are
303
252
  // `deployment_1`, `deployment_2`, and so on. Targets are trimmed, and blank
@@ -317,17 +266,12 @@ function unnamedDeployments(targets) {
317
266
  }));
318
267
  }
319
268
  // Reads a model's deployment targets in a single pass and returns the facts
320
- // every caller derives from them. The targets come from `model.deployments`
321
- // when the model has it, and otherwise from its GOOGLE custom_extension(s).
322
- // Never both: in the Google flavor, the loader's GOOGLE block holds a copy of
323
- // the list. Throws on malformed extension JSON.
269
+ // every caller derives from them. The targets come from `model.deployments`.
324
270
  function googleDeploymentTargets(model) {
325
271
  const cached = targetsCache.get(model);
326
272
  if (cached)
327
273
  return cached;
328
- const uris = model.deployments !== undefined ?
329
- model.deployments.map(d => d.target) :
330
- extensionTargets(model);
274
+ const uris = (model.deployments ?? []).map(d => d.target);
331
275
  const bigQuery = [];
332
276
  const spanner = [];
333
277
  const alloyDb = [];
@@ -7,6 +7,16 @@ export interface MetricPlacementInput {
7
7
  entityByName: ReadonlyMap<string, Entity>;
8
8
  skipped: ReadonlySet<string>;
9
9
  ancestorsUsed: ReadonlySet<string>;
10
+ renderedFieldSql(entity: Entity, field: Field): {
11
+ sql: string;
12
+ } | {
13
+ reads: string;
14
+ };
15
+ localSql(entity: Entity, sql: string): {
16
+ sql: string;
17
+ } | {
18
+ reads: string;
19
+ };
10
20
  }
11
21
  export interface GraphEngine {
12
22
  name: string;