@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
|
@@ -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
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
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
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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,
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* 2.
|
|
231
|
-
* foreign models unless `--force-remove`
|
|
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.
|
|
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
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
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.
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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.
|
|
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 {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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,
|
|
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
|
|
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
|
|
1318
|
-
*
|
|
1319
|
-
* `
|
|
1320
|
-
* `kcmd-push-
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1323
|
-
*
|
|
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
|
|
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
|
|
7
|
-
// push parses each document once), lowers each to `CREATE OR
|
|
8
|
-
// GRAPH` DDL (generator: ./spanner), and applies that DDL to
|
|
9
|
-
// database named by the model's
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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 {
|
|
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.
|
|
6
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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;
|