@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
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
// Runs once over the shared, already-parsed models (see loadSemanticModels)
|
|
5
5
|
// before any destination leg, so a real `kcmd push` AND a `--validate-only` dry
|
|
6
6
|
// run enforce the same requirements. Returns one message per violation (an
|
|
7
|
-
// empty array means valid);
|
|
8
|
-
//
|
|
9
|
-
// schema -- because these are deployment requirements, not schema
|
|
10
|
-
//
|
|
7
|
+
// empty array means valid); push prints them and aborts, and `kcmd import`
|
|
8
|
+
// reports them. Kept separate from the loader -- which validates a document
|
|
9
|
+
// against the schema -- because these are deployment requirements, not schema
|
|
10
|
+
// rules.
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.validatePushRequirements = validatePushRequirements;
|
|
13
13
|
exports.validateDeployments = validateDeployments;
|
|
@@ -18,12 +18,18 @@ exports.validateActions = validateActions;
|
|
|
18
18
|
exports.validateInheritance = validateInheritance;
|
|
19
19
|
exports.validateExpressionReferences = validateExpressionReferences;
|
|
20
20
|
exports.deploymentTableErrors = deploymentTableErrors;
|
|
21
|
+
exports.spannerMetricsErrors = spannerMetricsErrors;
|
|
22
|
+
exports.kcV2Aspects = kcV2Aspects;
|
|
23
|
+
exports.withProfileFiles = withProfileFiles;
|
|
24
|
+
exports.localChecks = localChecks;
|
|
25
|
+
exports.pruningLines = pruningLines;
|
|
21
26
|
exports.validateBigQueryDataSources = validateBigQueryDataSources;
|
|
22
|
-
exports.storeMessage = storeMessage;
|
|
23
27
|
exports.validateSpannerActionStatements = validateSpannerActionStatements;
|
|
24
28
|
exports.validateBigQueryActionStatements = validateBigQueryActionStatements;
|
|
29
|
+
const context_1 = require("../gcp/context");
|
|
25
30
|
const bigquery_1 = require("./bigquery");
|
|
26
31
|
const deploy_bigquery_1 = require("./deploy_bigquery");
|
|
32
|
+
const deploy_spanner_1 = require("./deploy_spanner");
|
|
27
33
|
const deployment_target_1 = require("./deployment_target");
|
|
28
34
|
const ir_1 = require("./ir");
|
|
29
35
|
const kc_ids_1 = require("./kc_ids");
|
|
@@ -34,7 +40,6 @@ const resolve_inheritance_1 = require("./resolve_inheritance");
|
|
|
34
40
|
const resolve_profiles_1 = require("./resolve_profiles");
|
|
35
41
|
const sql_expr_utils_1 = require("./sql_expr_utils");
|
|
36
42
|
const sql_identifiers_1 = require("./sql_identifiers");
|
|
37
|
-
const sql_parser_1 = require("./sql_parser");
|
|
38
43
|
// Checks every model against the push requirements and returns the collected
|
|
39
44
|
// error messages (empty when all models pass), each tagged with the model's
|
|
40
45
|
// source document so the author can find it.
|
|
@@ -49,23 +54,12 @@ const sql_parser_1 = require("./sql_parser");
|
|
|
49
54
|
// caller to print.
|
|
50
55
|
//
|
|
51
56
|
// `v2Aspects` says push writes the second-generation Knowledge Catalog types
|
|
52
|
-
// (
|
|
57
|
+
// (the CLI sets it unless KC_V2_ASPECTS=0).
|
|
53
58
|
function validatePushRequirements(models, opts = {}) {
|
|
54
59
|
const errors = [];
|
|
55
60
|
for (const { document, model } of models) {
|
|
56
61
|
errors.push(...nameAndDescriptionErrors(model, document, !!opts.v2Aspects));
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
// One pass over the model's GOOGLE extension(s): both checks below read
|
|
60
|
-
// the same parse rather than re-parsing the JSON per reader.
|
|
61
|
-
deployInfo = (0, deploy_bigquery_1.googleDeploymentTargets)(model);
|
|
62
|
-
}
|
|
63
|
-
catch (err) {
|
|
64
|
-
// Malformed GOOGLE extension JSON: surface it as a validation error here
|
|
65
|
-
// rather than letting it throw out of a later leg as an uncaught stack.
|
|
66
|
-
errors.push(`${err.message || err} (${document})`);
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
62
|
+
const deployInfo = (0, deploy_bigquery_1.googleDeploymentTargets)(model);
|
|
69
63
|
// A model that targets a graph (BigQuery OR Spanner) must have every
|
|
70
64
|
// relationship's join columns bound. The loader accepts a column-less
|
|
71
65
|
// relationship so a purely logical model (an OWL import) loads and pushes
|
|
@@ -94,7 +88,7 @@ function validatePushRequirements(models, opts = {}) {
|
|
|
94
88
|
const flaggedMetrics = new Set();
|
|
95
89
|
errors.push(...validateExpressionReferences([{ document, model }], !!opts.fieldsPruned, opts.warnings, flaggedMetrics));
|
|
96
90
|
if (deployInfo.bigQuery.length > 0) {
|
|
97
|
-
errors.push(...
|
|
91
|
+
errors.push(...measurePlacementErrors(model, document, flaggedMetrics));
|
|
98
92
|
}
|
|
99
93
|
// An action reaches Knowledge Catalog only, so its checks are
|
|
100
94
|
// target-independent: each parameter's type must resolve to something in
|
|
@@ -171,7 +165,7 @@ function nameAndDescriptionErrors(model, document, v2Aspects) {
|
|
|
171
165
|
// Checks each deployment of `model` against the rules of its engine, under the
|
|
172
166
|
// profile it names in `model.profiles` or the model file's own bindings, and
|
|
173
167
|
// returns one message per problem, naming every deployment it applies to.
|
|
174
|
-
function validateDeployments(model, document) {
|
|
168
|
+
function validateDeployments(model, document, load = {}) {
|
|
175
169
|
// The deployments each problem applies to, keyed by the text that follows
|
|
176
170
|
// their names in the message.
|
|
177
171
|
const problems = new Map();
|
|
@@ -193,7 +187,7 @@ function validateDeployments(model, document) {
|
|
|
193
187
|
// The model the deployment's bindings give, less what they leave
|
|
194
188
|
// unavailable. The completeness check reports a named profile that does
|
|
195
189
|
// not apply, and the fields its metrics read.
|
|
196
|
-
const bound = named ? profiled(model, named) : (0, resolve_profiles_1.pruneUnavailable)(model, resolve_profiles_1.DEFAULT_PROFILE, alloyDb ? 'ALLOYDB' : (0, resolve_profiles_1.sourcesDialect)(model));
|
|
190
|
+
const bound = named ? profiled(model, named, load) : (0, resolve_profiles_1.pruneUnavailable)(model, resolve_profiles_1.DEFAULT_PROFILE, alloyDb ? 'ALLOYDB' : (0, resolve_profiles_1.sourcesDialect)(model));
|
|
197
191
|
if (!bound)
|
|
198
192
|
continue;
|
|
199
193
|
const available = bound.model.metrics ?? [];
|
|
@@ -251,9 +245,9 @@ function validateDeployments(model, document) {
|
|
|
251
245
|
}
|
|
252
246
|
// `model` with `profile` applied and what it leaves unavailable pruned, or
|
|
253
247
|
// undefined when the profile does not apply.
|
|
254
|
-
function profiled(model, profile) {
|
|
248
|
+
function profiled(model, profile, load) {
|
|
255
249
|
try {
|
|
256
|
-
return (0, resolve_profiles_1.pruneUnavailable)((0, resolve_profiles_1.applyProfile)(model, profile), profile.name);
|
|
250
|
+
return (0, resolve_profiles_1.pruneUnavailable)((0, resolve_profiles_1.applyProfile)(model, profile, { load }), profile.name);
|
|
257
251
|
}
|
|
258
252
|
catch (err) {
|
|
259
253
|
if (!(err instanceof loader_1.SemanticModelError))
|
|
@@ -262,7 +256,7 @@ function profiled(model, profile) {
|
|
|
262
256
|
}
|
|
263
257
|
}
|
|
264
258
|
// Why `metric` cannot be a BigQuery measure, or undefined when it can. Leaves
|
|
265
|
-
// out a metric that
|
|
259
|
+
// out a metric that measurePlacementErrors or the inheritance rules report:
|
|
266
260
|
// one that reads several entities or none, or one on an abstract or extended
|
|
267
261
|
// entity.
|
|
268
262
|
function measureProblem(model, metric) {
|
|
@@ -273,15 +267,10 @@ function measureProblem(model, metric) {
|
|
|
273
267
|
if (!columns)
|
|
274
268
|
return undefined;
|
|
275
269
|
const entities = model.entities ?? [];
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
// BigQuery graph runs.
|
|
279
|
-
const matched = (0, sql_expr_utils_1.referencedEntityNames)((0, sql_parser_1.withoutComments)(text, 'BIGQUERY'), names);
|
|
280
|
-
if (matched.length > 1 || !matched.length && !metric.entity)
|
|
270
|
+
const placed = (0, sql_expr_utils_1.measureEntities)(text, entities.map(e => e.name), metric.entity);
|
|
271
|
+
if (placed.length !== 1)
|
|
281
272
|
return undefined;
|
|
282
|
-
const
|
|
283
|
-
const name = reached.length > 1 ? undefined : reached[0] ?? metric.entity;
|
|
284
|
-
const entity = entities.find(e => e.name === name);
|
|
273
|
+
const entity = entities.find(e => e.name === placed[0]);
|
|
285
274
|
if (!entity || entity.abstract ||
|
|
286
275
|
entities.some(e => e.extends?.includes(entity.name))) {
|
|
287
276
|
return undefined;
|
|
@@ -295,17 +284,73 @@ function measureProblem(model, metric) {
|
|
|
295
284
|
}
|
|
296
285
|
return undefined;
|
|
297
286
|
}
|
|
298
|
-
//
|
|
299
|
-
//
|
|
287
|
+
// The BigQuery graph's rules for placing each metric of `model` as a measure,
|
|
288
|
+
// one message per metric that breaks them, skipping each one in
|
|
289
|
+
// `alreadyFlagged`, which validateExpressionReferences has reported. The
|
|
290
|
+
// generator (`bigquery.ts`) places a measure on the one entity whose
|
|
291
|
+
// qualifier its BigQuery SQL writes before a column, or on its anchor when the
|
|
292
|
+
// SQL writes none (see measureEntities), and skips a metric that has no
|
|
293
|
+
// BigQuery or ANSI_SQL text. validateExpressionReferences reads every dialect
|
|
294
|
+
// entry together and lets an authored anchor settle SQL that reads two
|
|
295
|
+
// entities, so it does not catch each metric the generator would skip.
|
|
296
|
+
function measurePlacementErrors(model, document, alreadyFlagged) {
|
|
297
|
+
const errors = [];
|
|
298
|
+
const entityNames = (model.entities ?? []).map(e => e.name);
|
|
299
|
+
for (const metric of model.metrics ?? []) {
|
|
300
|
+
if (alreadyFlagged.has(metric.name))
|
|
301
|
+
continue;
|
|
302
|
+
const prefix = `metric '${metric.name}' in model '${model.name}' (${document})`;
|
|
303
|
+
// The text BigQuery runs: its `BIGQUERY` entry, else its `ANSI_SQL` one,
|
|
304
|
+
// else `expression` for a metric held without a dialect list.
|
|
305
|
+
const bqText = (0, ir_1.expressionForDialect)(metric, 'BIGQUERY');
|
|
306
|
+
if (bqText === undefined && metric.importedExpression !== undefined) {
|
|
307
|
+
errors.push(`${prefix} targets a BigQuery graph with only a '${metric.importedDialect ?? 'vendor'}' expression; supply a ` +
|
|
308
|
+
`'BIGQUERY' or 'ANSI_SQL' expression${(0, resolve_profiles_1.importTranspiles)(metric, 'BIGQUERY', model) ?
|
|
309
|
+
`, or add one with '${(0, resolve_profiles_1.importCommand)('--transpile', model.version)}'` :
|
|
310
|
+
''}.`);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const placed = (0, sql_expr_utils_1.measureEntities)(bqText ?? '', entityNames, metric.entity);
|
|
314
|
+
if (placed.length > 1) {
|
|
315
|
+
errors.push(`${prefix} targets a BigQuery graph and its BigQuery SQL reads ` +
|
|
316
|
+
`several entities (${placed.join(', ')}); a BigQuery measure ` +
|
|
317
|
+
`belongs to one entity's node table, so scope its expression to ` +
|
|
318
|
+
`one entity.`);
|
|
319
|
+
}
|
|
320
|
+
else if (placed.length === 0) {
|
|
321
|
+
const anchor = model.version === loader_1.OSSIE_VERSION ?
|
|
322
|
+
`'entity' in a GOOGLE custom_extensions block on the metric` :
|
|
323
|
+
`top-level 'entity'`;
|
|
324
|
+
errors.push(`${prefix} targets a BigQuery graph but does not resolve to a ` +
|
|
325
|
+
`single entity; the BigQuery generator places a measure on the ` +
|
|
326
|
+
`entity its BigQuery SQL reads, or on ${anchor}.`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return errors;
|
|
330
|
+
}
|
|
331
|
+
// Checks that the model file's Spanner tables are all in one database, so they
|
|
332
|
+
// share one dialect, and that each bound field has a text for the dialect of
|
|
333
|
+
// its entity's source. Returns one message per problem.
|
|
300
334
|
function validateInlineDialects(model, document) {
|
|
301
335
|
const errors = [];
|
|
336
|
+
const databases = new Map();
|
|
337
|
+
for (const e of model.entities ?? []) {
|
|
338
|
+
const db = (0, loader_1.databaseOf)(e.authoredSource ?? '');
|
|
339
|
+
if (db?.startsWith('spanner/') && !databases.has(db)) {
|
|
340
|
+
databases.set(db, e.name);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (databases.size > 1) {
|
|
344
|
+
const named = [...databases.values()].map(n => `'${n}'`).join(', ');
|
|
345
|
+
errors.push(`model '${model.name}' (${document}) binds entities in more than one ` +
|
|
346
|
+
`Spanner database (${named}); bind the model file's entities to ` +
|
|
347
|
+
`tables in one Spanner database.`);
|
|
348
|
+
}
|
|
302
349
|
for (const { entity: e, field: f, table: { name: dialect } } of (0, resolve_profiles_1.fieldsOnTables)(model)) {
|
|
303
350
|
if (!(0, resolve_profiles_1.noTextFor)(f, [dialect]))
|
|
304
351
|
continue;
|
|
305
|
-
const hint = (0, resolve_profiles_1.importTranspiles)(f, dialect) ?
|
|
306
|
-
`; give it one, or add one with '${resolve_profiles_1.IMPORT_TRANSPILE_COMMAND}'` : '';
|
|
307
352
|
errors.push(`field '${e.name}.${f.name}' in model '${model.name}' (${document}) has no expression for ${dialect} or ANSI_SQL, and ` +
|
|
308
|
-
`the model file binds entity '${e.name}' to ${/^[AEIOU]/.test(dialect) ? 'an' : 'a'} ${dialect} source${
|
|
353
|
+
`the model file binds entity '${e.name}' to ${/^[AEIOU]/.test(dialect) ? 'an' : 'a'} ${dialect} source${(0, resolve_profiles_1.importHint)(f, dialect, model)}.`);
|
|
309
354
|
}
|
|
310
355
|
return errors;
|
|
311
356
|
}
|
|
@@ -1292,6 +1337,94 @@ function fieldNoSqlErrors(model, document) {
|
|
|
1292
1337
|
}
|
|
1293
1338
|
return errors;
|
|
1294
1339
|
}
|
|
1340
|
+
// The errors for the fields of `entity` that name themselves, directly or
|
|
1341
|
+
// through other fields of `entity`. The graph renders a field that names a
|
|
1342
|
+
// sibling as `<entity>.<field>` from that sibling's own SQL, so a field that
|
|
1343
|
+
// reaches itself that way has no SQL to render. A field expression that writes
|
|
1344
|
+
// its own column as `<entity>.<field>` names itself too: write the bare column
|
|
1345
|
+
// instead. Each dialect is read on its own, from the text an engine of that
|
|
1346
|
+
// dialect renders (its entry, else the ANSI_SQL one), so two texts that no one
|
|
1347
|
+
// engine reads together do not form a cycle.
|
|
1348
|
+
//
|
|
1349
|
+
// `resolved` is `entity` with the fields it inherits, which name its fields
|
|
1350
|
+
// as the graph renders them (see resolve_inheritance.localizeInheritedField),
|
|
1351
|
+
// so a field can reach itself through one it inherits. A cycle is reported on
|
|
1352
|
+
// an entity only when a field on it is bound there, so one an ancestor forms on
|
|
1353
|
+
// its own is reported on that ancestor alone.
|
|
1354
|
+
function selfReferenceErrors(entity, resolved, model, document, impliedDialect) {
|
|
1355
|
+
const own = entity.fields ?? [];
|
|
1356
|
+
const inherited = resolved.fields ?? [];
|
|
1357
|
+
// A field a profile excludes is left off `resolved`, but still checked.
|
|
1358
|
+
const fields = [
|
|
1359
|
+
...inherited,
|
|
1360
|
+
...own.filter(f => !inherited.some(r => r.name === f.name)),
|
|
1361
|
+
];
|
|
1362
|
+
const bound = new Set(own.filter(f => (0, ir_1.isFieldBound)(f)).map(f => f.name));
|
|
1363
|
+
const names = new Set(fields.map(f => f.name));
|
|
1364
|
+
const dialects = new Set(['ANSI_SQL']);
|
|
1365
|
+
for (const f of fields) {
|
|
1366
|
+
for (const d of f.dialects ?? [])
|
|
1367
|
+
dialects.add(d.dialect);
|
|
1368
|
+
if (f.importedDialect)
|
|
1369
|
+
dialects.add(f.importedDialect);
|
|
1370
|
+
}
|
|
1371
|
+
const errors = new Set();
|
|
1372
|
+
for (const dialect of dialects) {
|
|
1373
|
+
// The fields of `entity` each field names, in this dialect, read as the
|
|
1374
|
+
// graph generators read them (see graph_ddl.siblingsRead), so a qualifier
|
|
1375
|
+
// the parser reserves, such as `Date` in `Date.days`, names them too. An
|
|
1376
|
+
// ANSI_SQL text is read in the dialect the sources imply, as
|
|
1377
|
+
// columnsForSource reads it, and not at all when their dialect is null.
|
|
1378
|
+
if (dialect === 'ANSI_SQL' && impliedDialect === null)
|
|
1379
|
+
continue;
|
|
1380
|
+
const readIn = dialect === 'ANSI_SQL' && impliedDialect ? impliedDialect : dialect;
|
|
1381
|
+
const reads = new Map();
|
|
1382
|
+
for (const f of fields) {
|
|
1383
|
+
const text = (0, ir_1.expressionForDialect)(f, dialect);
|
|
1384
|
+
if (text === undefined ||
|
|
1385
|
+
(0, loader_1.noSqlReason)(text, dialect, entity.name) !== undefined) {
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
reads.set(f.name, [...new Set((0, sql_expr_utils_1.qualifiedColumns)(text, entity.name, readIn) ?? [])]
|
|
1389
|
+
.filter(n => names.has(n)));
|
|
1390
|
+
}
|
|
1391
|
+
for (const f of fields) {
|
|
1392
|
+
const through = pathBack(f.name, reads);
|
|
1393
|
+
if (!through || ![f.name, ...through].some(n => bound.has(n)))
|
|
1394
|
+
continue;
|
|
1395
|
+
errors.add((0, sql_expr_utils_1.selfReferenceMessage)(entity.name, [f.name, ...through], ` in model '${model.name}' (${document})`) +
|
|
1396
|
+
'.');
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
return [...errors];
|
|
1400
|
+
}
|
|
1401
|
+
// The shortest run of other fields through which `start` reaches itself in
|
|
1402
|
+
// `reads`, empty when it names itself directly, or undefined when it does not
|
|
1403
|
+
// reach itself.
|
|
1404
|
+
function pathBack(start, reads) {
|
|
1405
|
+
const via = new Map();
|
|
1406
|
+
const queue = [];
|
|
1407
|
+
for (const next of reads.get(start) ?? []) {
|
|
1408
|
+
if (next === start)
|
|
1409
|
+
return [];
|
|
1410
|
+
if (!via.has(next)) {
|
|
1411
|
+
via.set(next, [next]);
|
|
1412
|
+
queue.push(next);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
while (queue.length) {
|
|
1416
|
+
const name = queue.shift();
|
|
1417
|
+
for (const next of reads.get(name) ?? []) {
|
|
1418
|
+
if (next === start)
|
|
1419
|
+
return via.get(name);
|
|
1420
|
+
if (!via.has(next)) {
|
|
1421
|
+
via.set(next, [...via.get(name), next]);
|
|
1422
|
+
queue.push(next);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
return undefined;
|
|
1427
|
+
}
|
|
1295
1428
|
// The error for a metric with no SQL, or undefined. A metric is rejected when
|
|
1296
1429
|
// none of its texts holds SQL, with or without an anchor, since the graph
|
|
1297
1430
|
// would drop it. It is also rejected when any one of its texts holds none, as
|
|
@@ -1332,10 +1465,13 @@ function metricNoSqlError(metric, model, document) {
|
|
|
1332
1465
|
// differs only in case from a declared entity.
|
|
1333
1466
|
// 2. For references to E itself, every field named must be declared or
|
|
1334
1467
|
// inherited on E (gated on `!fieldsPruned`, and run before pruning).
|
|
1468
|
+
// 3. Reject a field that names itself, directly or through other fields of
|
|
1469
|
+
// E, declared or inherited, since the graph renders a field that names
|
|
1470
|
+
// a sibling from that sibling's SQL (see selfReferenceErrors).
|
|
1335
1471
|
//
|
|
1336
|
-
//
|
|
1337
|
-
//
|
|
1338
|
-
//
|
|
1472
|
+
// deploymentPlan also runs it on the unpruned model before `pruneUnavailable`
|
|
1473
|
+
// drops unbound fields and metrics on a graph push. When given,
|
|
1474
|
+
// `flaggedMetrics` collects the name of each metric it reports.
|
|
1339
1475
|
function validateExpressionReferences(models, fieldsPruned = false, warnings = [], flaggedMetrics) {
|
|
1340
1476
|
const errors = [];
|
|
1341
1477
|
for (const { document, model } of models) {
|
|
@@ -1352,8 +1488,18 @@ function validateExpressionReferences(models, fieldsPruned = false, warnings = [
|
|
|
1352
1488
|
continue;
|
|
1353
1489
|
}
|
|
1354
1490
|
const entityNames = [...fieldsByEntity.keys()];
|
|
1491
|
+
// Each entity's fields as the graph renders them (see selfReferenceErrors).
|
|
1492
|
+
const resolvedByName = new Map((ifResolvable(() => (0, resolve_inheritance_1.resolveInheritance)(model, undefined, { requalify: true })
|
|
1493
|
+
.model.entities) ??
|
|
1494
|
+
model.entities ?? [])
|
|
1495
|
+
.map(e => [e.name, e]));
|
|
1355
1496
|
const impliedDialect = (0, resolve_profiles_1.sourcesDialect)(model);
|
|
1356
|
-
const addUnreadableWarning = (what, total,
|
|
1497
|
+
const addUnreadableWarning = (what, total, unread) => {
|
|
1498
|
+
// A caller that looked up no table's dialect reads no ANSI_SQL
|
|
1499
|
+
// text when every table needs the lookup, so it warns about none.
|
|
1500
|
+
const unreadDialects = impliedDialect === null ?
|
|
1501
|
+
unread.filter(d => d !== 'ANSI_SQL') :
|
|
1502
|
+
unread;
|
|
1357
1503
|
if (!unreadDialects.length)
|
|
1358
1504
|
return;
|
|
1359
1505
|
if (unreadDialects.length === total) {
|
|
@@ -1406,6 +1552,7 @@ function validateExpressionReferences(models, fieldsPruned = false, warnings = [
|
|
|
1406
1552
|
}
|
|
1407
1553
|
}
|
|
1408
1554
|
}
|
|
1555
|
+
errors.push(...selfReferenceErrors(entity, resolvedByName.get(entity.name) ?? entity, model, document, impliedDialect));
|
|
1409
1556
|
}
|
|
1410
1557
|
for (const metric of model.metrics ?? []) {
|
|
1411
1558
|
const before = errors.length;
|
|
@@ -1496,53 +1643,6 @@ function validateExpressionReferences(models, fieldsPruned = false, warnings = [
|
|
|
1496
1643
|
}
|
|
1497
1644
|
return errors;
|
|
1498
1645
|
}
|
|
1499
|
-
// Temporary BigQuery-only metric check, until the BigQuery generator places
|
|
1500
|
-
// measures with the SQL parser. Rejects a metric that the generator
|
|
1501
|
-
// (`bigquery.ts`) would skip: one with only another engine's SQL, which the
|
|
1502
|
-
// generator does not emit, or one it drops because its regex
|
|
1503
|
-
// (`referencedEntityNames`) disagrees with the parser or because
|
|
1504
|
-
// `metric.entity` is unset. Reports only metrics that
|
|
1505
|
-
// `validateExpressionReferences` has not already flagged.
|
|
1506
|
-
function validateBigQueryMetrics(model, document, alreadyFlagged) {
|
|
1507
|
-
const errors = [];
|
|
1508
|
-
const entityNames = (model.entities ?? []).map(e => e.name);
|
|
1509
|
-
for (const metric of model.metrics ?? []) {
|
|
1510
|
-
if (alreadyFlagged.has(metric.name))
|
|
1511
|
-
continue;
|
|
1512
|
-
const prefix = `metric '${metric.name}' in model '${model.name}' (${document})`;
|
|
1513
|
-
// The text BigQuery runs: its `BIGQUERY` entry, else its `ANSI_SQL` one,
|
|
1514
|
-
// else `expression` for a metric held without a dialect list.
|
|
1515
|
-
const bqText = (0, ir_1.expressionForDialect)(metric, 'BIGQUERY');
|
|
1516
|
-
if (bqText === undefined && metric.importedExpression !== undefined) {
|
|
1517
|
-
errors.push(`${prefix} targets a BigQuery graph with only a '${metric.importedDialect ?? 'vendor'}' expression; supply a ` +
|
|
1518
|
-
`'BIGQUERY' or 'ANSI_SQL' expression, or add one with '${resolve_profiles_1.IMPORT_TRANSPILE_COMMAND}'.`);
|
|
1519
|
-
continue;
|
|
1520
|
-
}
|
|
1521
|
-
// The generator reads the resolved BigQuery expression (`bqText`) with its
|
|
1522
|
-
// comments removed.
|
|
1523
|
-
const expr = (0, sql_parser_1.withoutComments)(bqText ?? metric.importedExpression ?? '', 'BIGQUERY');
|
|
1524
|
-
let referenced = (0, sql_expr_utils_1.referencedEntityNames)(expr, entityNames);
|
|
1525
|
-
if (referenced.length === 0 && metric.entity) {
|
|
1526
|
-
referenced = [metric.entity];
|
|
1527
|
-
}
|
|
1528
|
-
if (referenced.length > 1) {
|
|
1529
|
-
errors.push(`${prefix} targets a BigQuery graph and references multiple ` +
|
|
1530
|
-
`entities (${referenced.join(', ')}); the BigQuery generator ` +
|
|
1531
|
-
`reads every '<entity>.' outside comments as a qualifier, ` +
|
|
1532
|
-
`including a function namespace such as 'NET.', so remove the ` +
|
|
1533
|
-
`extra '<entity>.' match or scope its expression to one entity.`);
|
|
1534
|
-
}
|
|
1535
|
-
else if (referenced.length === 0) {
|
|
1536
|
-
const anchor = model.version === loader_1.OSSIE_VERSION ?
|
|
1537
|
-
`'entity' in a GOOGLE custom_extensions block on the metric` :
|
|
1538
|
-
`top-level 'entity'`;
|
|
1539
|
-
errors.push(`${prefix} targets a BigQuery graph but does not resolve to a ` +
|
|
1540
|
-
`single entity; the BigQuery generator reads '<entity>.' ` +
|
|
1541
|
-
`qualifiers in the expression or ${anchor}.`);
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
return errors;
|
|
1545
|
-
}
|
|
1546
1646
|
// Every field each entity has, inherited and profile-excluded ones included so
|
|
1547
1647
|
// an unpruned check before `pruneUnavailable` does not report a profile-excluded
|
|
1548
1648
|
// inherited field as undeclared. Inheritance is resolved through the same pass
|
|
@@ -1608,6 +1708,20 @@ function deploymentTableErrors(document, model, deployment) {
|
|
|
1608
1708
|
}
|
|
1609
1709
|
return errors;
|
|
1610
1710
|
}
|
|
1711
|
+
// The error for a Spanner `deployment` on the model file's own bindings when
|
|
1712
|
+
// `model` declares metrics, which a Spanner graph cannot hold.
|
|
1713
|
+
function spannerMetricsErrors(document, model, deployment) {
|
|
1714
|
+
if (deployment.engine !== 'spanner' ||
|
|
1715
|
+
deployment.profile !== resolve_profiles_1.DEFAULT_PROFILE || !model.metrics?.length) {
|
|
1716
|
+
return [];
|
|
1717
|
+
}
|
|
1718
|
+
return [
|
|
1719
|
+
`[${document}] deployment '${deployment.name}' targets a Spanner graph ` +
|
|
1720
|
+
`and uses the model file's own bindings, but the model declares ` +
|
|
1721
|
+
`metrics, which a Spanner graph cannot hold; name a profile with ` +
|
|
1722
|
+
`metrics_exclude: "*".`,
|
|
1723
|
+
];
|
|
1724
|
+
}
|
|
1611
1725
|
// The database the tables of a deployment target must be in, as databaseOf
|
|
1612
1726
|
// gives it, and how an error names the target and a table outside it.
|
|
1613
1727
|
function targetDatabase({ engine, coordinates: c }) {
|
|
@@ -1621,6 +1735,265 @@ function targetDatabase({ engine, coordinates: c }) {
|
|
|
1621
1735
|
return { database: `${engine}/${name}`, target: `${system} database '${name}'`,
|
|
1622
1736
|
outside: 'which is not a table in that database' };
|
|
1623
1737
|
}
|
|
1738
|
+
// Whether Knowledge Catalog push and pull use the second-generation built-in
|
|
1739
|
+
// aspect fields and entry-id layout. They do unless `KC_V2_ASPECTS=0`; every
|
|
1740
|
+
// other value, including unset and `''`, is on. `KC_V2_ASPECTS=0` is a
|
|
1741
|
+
// temporary escape hatch to the first-generation path, removed with that path
|
|
1742
|
+
// (b/575695037).
|
|
1743
|
+
function kcV2Aspects() {
|
|
1744
|
+
return process.env.KC_V2_ASPECTS !== '0';
|
|
1745
|
+
}
|
|
1746
|
+
// Adds the profile files of the model in `base` to `model.profiles`, without
|
|
1747
|
+
// their actions. Returns the error of the first profile file that breaks a
|
|
1748
|
+
// file rule. A profile file that does not load is left out, for
|
|
1749
|
+
// namedProfileErrors to report.
|
|
1750
|
+
function withProfileFiles(base, profileDocs) {
|
|
1751
|
+
const { document, model } = base;
|
|
1752
|
+
const profiles = [...(model.profiles ?? [])];
|
|
1753
|
+
let files;
|
|
1754
|
+
try {
|
|
1755
|
+
files = profileDocs(document);
|
|
1756
|
+
}
|
|
1757
|
+
catch (err) {
|
|
1758
|
+
return { errors: [{ error: err.message || String(err), document }] };
|
|
1759
|
+
}
|
|
1760
|
+
const reserved = (0, resolve_profiles_1.reservedProfileNameError)(document, files);
|
|
1761
|
+
if (reserved)
|
|
1762
|
+
return { errors: [{ error: reserved, document }] };
|
|
1763
|
+
for (const p of files) {
|
|
1764
|
+
try {
|
|
1765
|
+
profiles.push((0, resolve_profiles_1.loadProfileFile)(p.text, p.name, { skipActions: true }));
|
|
1766
|
+
}
|
|
1767
|
+
catch (err) {
|
|
1768
|
+
if (!(err instanceof loader_1.SemanticModelError))
|
|
1769
|
+
throw err;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
return { document, model: profiles.length ? { ...model, profiles } : model };
|
|
1773
|
+
}
|
|
1774
|
+
// Runs push's offline checks that follow withProfileFiles and the UTF-8 and
|
|
1775
|
+
// sidecar checks, through the graph generation. Returns the errors of the
|
|
1776
|
+
// first stage that finds any, or else a plan for each deployment `selected`
|
|
1777
|
+
// names, or for every deployment when it is unset.
|
|
1778
|
+
async function localChecks(scope, warn, selected) {
|
|
1779
|
+
const { docs, loaded, models, profileDocs, declaredBy, load } = scope;
|
|
1780
|
+
const stop = (errors) => ({ errors });
|
|
1781
|
+
const ofModel = (document, ...errors) => errors.map(error => ({ error, document }));
|
|
1782
|
+
const ofProfile = (document, profile, ...errors) => errors.map(error => ({ error, document, profile }));
|
|
1783
|
+
const declaredNames = (document) => declaredBy(docs.find(d => d.name === document)).map(p => p.name);
|
|
1784
|
+
// Every deployment's target must be in an accepted form, and its profile one
|
|
1785
|
+
// the model declares, whether or not this push selects the deployment.
|
|
1786
|
+
const selectedByDoc = new Map();
|
|
1787
|
+
const everyByDoc = new Map();
|
|
1788
|
+
for (const { document, model } of models) {
|
|
1789
|
+
const every = (0, deployment_target_1.resolveModelDeployments)(model);
|
|
1790
|
+
if ('error' in every)
|
|
1791
|
+
return stop(ofModel(document, every.error));
|
|
1792
|
+
const resolved = (0, deployment_target_1.resolveModelDeployments)(model, selected);
|
|
1793
|
+
if ('error' in resolved)
|
|
1794
|
+
return stop(ofModel(document, resolved.error));
|
|
1795
|
+
const undeclared = (0, resolve_profiles_1.undeclaredProfileError)(document, model, declaredNames(document));
|
|
1796
|
+
if (undeclared)
|
|
1797
|
+
return stop(ofModel(document, undeclared));
|
|
1798
|
+
everyByDoc.set(document, every.deployments);
|
|
1799
|
+
selectedByDoc.set(document, resolved.deployments);
|
|
1800
|
+
}
|
|
1801
|
+
// Every named profile is checked on its own against the model file, and so
|
|
1802
|
+
// are the model file's own bindings and each deployment. The catalog's model
|
|
1803
|
+
// is checked as a model that deploys nowhere. A selected deployment's own
|
|
1804
|
+
// checks run only after these pass, so these give every error and every
|
|
1805
|
+
// warning about SQL kcmd cannot read.
|
|
1806
|
+
const profileWarnings = [];
|
|
1807
|
+
const profileErrors = (0, resolve_profiles_1.namedProfileErrors)(docs, new Map(loaded.models.map(m => [m.document, m.model])), profileDocs, declaredBy, profileWarnings, load);
|
|
1808
|
+
for (const { document, model } of models) {
|
|
1809
|
+
profileErrors.push(...ofProfile(document, resolve_profiles_1.DEFAULT_PROFILE, ...validateInlineDialects(model, document), ...validateInheritedColumns(model, document)),
|
|
1810
|
+
// One call per profile, so each error carries its deployments' profile.
|
|
1811
|
+
...[...new Set(everyByDoc.get(document).map(d => d.profile))].flatMap(profile => ofProfile(document, profile, ...validateDeployments({
|
|
1812
|
+
...model,
|
|
1813
|
+
deployments: model.deployments.filter(d => (d.profile ?? resolve_profiles_1.DEFAULT_PROFILE) === profile),
|
|
1814
|
+
}, document, load))));
|
|
1815
|
+
const doc = docs.find(d => d.name === document);
|
|
1816
|
+
const declared = declaredBy(doc);
|
|
1817
|
+
const named = declared.some(p => p.name.toLowerCase() !== resolve_profiles_1.DEFAULT_PROFILE);
|
|
1818
|
+
profileWarnings.push(...inlineBindingWarnings(model, document, named));
|
|
1819
|
+
// A profile that no selected deployment applies still goes to Knowledge
|
|
1820
|
+
// Catalog, so its load warnings print here, less those the model file's
|
|
1821
|
+
// load gave, unless a check above recorded an error for that profile. A
|
|
1822
|
+
// selected deployment prints its own profile's warnings.
|
|
1823
|
+
const applied = selectedByDoc.get(document).map(d => d.profile);
|
|
1824
|
+
for (const { name } of declared) {
|
|
1825
|
+
if (applied.includes(name))
|
|
1826
|
+
continue;
|
|
1827
|
+
if (profileErrors.some(e => e.document === document && e.profile === name)) {
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
profileWarnings.push(...(0, resolve_profiles_1.appliedProfileWarnings)(doc, declared, name, loaded.warnings, load));
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
const kcWarnings = [];
|
|
1834
|
+
const kcErrors = models.flatMap(m => ofModel(m.document, ...validatePushRequirements([{ ...m, model: { ...m.model, deployments: [] } }], {
|
|
1835
|
+
warnings: kcWarnings,
|
|
1836
|
+
v2Aspects: kcV2Aspects(),
|
|
1837
|
+
})));
|
|
1838
|
+
for (const w of [...profileWarnings, ...kcWarnings])
|
|
1839
|
+
warn(w);
|
|
1840
|
+
if (profileErrors.length || kcErrors.length) {
|
|
1841
|
+
return stop([...profileErrors, ...kcErrors]);
|
|
1842
|
+
}
|
|
1843
|
+
// Each deployment's tables must be in the database it deploys to, whether
|
|
1844
|
+
// or not this push selects the deployment.
|
|
1845
|
+
const tableErrors = models.flatMap(({ document, model }) => (everyByDoc.get(document) ?? []).flatMap(d => ofProfile(document, d.profile, ...deploymentTableErrors(document, model, d))));
|
|
1846
|
+
if (tableErrors.length)
|
|
1847
|
+
return stop(tableErrors);
|
|
1848
|
+
// Spanner creates no property graph in a PostgreSQL-dialect database, so a
|
|
1849
|
+
// selected Spanner deployment there is refused by the dialect already read.
|
|
1850
|
+
const dialectErrors = models.flatMap(({ document }) => (selectedByDoc.get(document) ?? []).flatMap(d => {
|
|
1851
|
+
if (d.engine !== 'spanner')
|
|
1852
|
+
return [];
|
|
1853
|
+
const { project, instance, database } = d.coordinates;
|
|
1854
|
+
const key = `spanner.googleapis.com/projects/${project}/instances/${instance}/databases/${database}`;
|
|
1855
|
+
if (load.sourceDialects?.get(key) !== 'POSTGRES')
|
|
1856
|
+
return [];
|
|
1857
|
+
return ofProfile(document, d.profile, `[${document}] deployment '${d.name}': database ${project}/${instance}/${database} has the PostgreSQL dialect, and ` +
|
|
1858
|
+
`Spanner creates no property graph in a PostgreSQL-dialect ` +
|
|
1859
|
+
`database. Remove the deployment; push still publishes the ` +
|
|
1860
|
+
`model to Knowledge Catalog.`);
|
|
1861
|
+
}));
|
|
1862
|
+
if (dialectErrors.length)
|
|
1863
|
+
return stop(dialectErrors);
|
|
1864
|
+
const plans = [];
|
|
1865
|
+
for (const base of models) {
|
|
1866
|
+
for (const deployment of selectedByDoc.get(base.document) ?? []) {
|
|
1867
|
+
const plan = await deploymentPlan(base, profileDocs(base.document), deployment, load, warn);
|
|
1868
|
+
if ('errors' in plan) {
|
|
1869
|
+
return stop(ofProfile(base.document, deployment.profile, ...plan.errors));
|
|
1870
|
+
}
|
|
1871
|
+
plans.push(plan);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
return { plans };
|
|
1875
|
+
}
|
|
1876
|
+
// Builds and checks the model `deployment` deploys from `base`, reading its
|
|
1877
|
+
// profile file in `files` again with its actions, and generates its DDL
|
|
1878
|
+
// offline. The profile applies with the source dialects in `load`. Returns
|
|
1879
|
+
// the plan, or the errors that stop the push.
|
|
1880
|
+
async function deploymentPlan(base, files, deployment, load, warn) {
|
|
1881
|
+
const { document, model } = base;
|
|
1882
|
+
const at = `[${document}] deployment '${deployment.name}'`;
|
|
1883
|
+
const fail = (...errors) => ({ errors });
|
|
1884
|
+
const spanner = spannerMetricsErrors(document, model, deployment);
|
|
1885
|
+
if (spanner.length)
|
|
1886
|
+
return fail(...spanner);
|
|
1887
|
+
let applied = model;
|
|
1888
|
+
if (deployment.profile !== resolve_profiles_1.DEFAULT_PROFILE) {
|
|
1889
|
+
// localChecks has checked that the model declares the profile and that
|
|
1890
|
+
// its file loads, so `model.profiles` holds it.
|
|
1891
|
+
let spec = model.profiles.find(p => p.name === deployment.profile);
|
|
1892
|
+
const file = files.find(p => p.name === deployment.profile);
|
|
1893
|
+
const warnings = [];
|
|
1894
|
+
try {
|
|
1895
|
+
// A profile in a GOOGLE block has no file and applies as loaded.
|
|
1896
|
+
if (file)
|
|
1897
|
+
spec = (0, resolve_profiles_1.loadProfileFile)(file.text, file.name);
|
|
1898
|
+
}
|
|
1899
|
+
catch (err) {
|
|
1900
|
+
if (!(err instanceof loader_1.SemanticModelError))
|
|
1901
|
+
throw err;
|
|
1902
|
+
return fail(`[${file.fileName}] ${err.message}`);
|
|
1903
|
+
}
|
|
1904
|
+
try {
|
|
1905
|
+
applied = (0, resolve_profiles_1.applyProfile)(model, spec, { load, warnings });
|
|
1906
|
+
}
|
|
1907
|
+
catch (err) {
|
|
1908
|
+
if (!(err instanceof loader_1.SemanticModelError))
|
|
1909
|
+
throw err;
|
|
1910
|
+
return fail(`${at}: ${err.message}`);
|
|
1911
|
+
}
|
|
1912
|
+
for (const w of warnings) {
|
|
1913
|
+
warn(`profile '${deployment.profile}': [${document}] ${w}`);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
// Messages name the deployment.
|
|
1917
|
+
const label = `${document}, deployment '${deployment.name}'`;
|
|
1918
|
+
const checked = { document: label, model: applied };
|
|
1919
|
+
// Pruning can remove the field these rules are about, so they run first.
|
|
1920
|
+
// localChecks has already warned about the SQL they cannot read.
|
|
1921
|
+
const unprunedErrors = [
|
|
1922
|
+
...validateInheritance([checked]),
|
|
1923
|
+
...validateExpressionReferences([checked]),
|
|
1924
|
+
];
|
|
1925
|
+
const { model: pruned, report } = (0, resolve_profiles_1.pruneUnavailable)(checked.model, deployment.profile);
|
|
1926
|
+
// Keeps only this deployment, so every reader sees its target alone.
|
|
1927
|
+
const narrowed = {
|
|
1928
|
+
document: checked.document,
|
|
1929
|
+
model: {
|
|
1930
|
+
...pruned,
|
|
1931
|
+
deployments: (model.deployments ?? []).filter(d => d.name === deployment.name),
|
|
1932
|
+
},
|
|
1933
|
+
};
|
|
1934
|
+
const errors = [
|
|
1935
|
+
...unprunedErrors,
|
|
1936
|
+
...validatePushRequirements([narrowed], {
|
|
1937
|
+
fieldsPruned: true,
|
|
1938
|
+
v2Aspects: kcV2Aspects(),
|
|
1939
|
+
}),
|
|
1940
|
+
];
|
|
1941
|
+
if (errors.length)
|
|
1942
|
+
return { errors };
|
|
1943
|
+
// An AlloyDB database holds no graph.
|
|
1944
|
+
if (deployment.engine === 'alloydb') {
|
|
1945
|
+
return { deployment, at, model: narrowed, report };
|
|
1946
|
+
}
|
|
1947
|
+
const generated = await generateOffline(deployment.engine === 'bigquery' ? deploy_bigquery_1.deployBigQuery : deploy_spanner_1.deploySpanner, [narrowed]);
|
|
1948
|
+
if (typeof generated === 'string')
|
|
1949
|
+
return fail(`${at}: ${generated}`);
|
|
1950
|
+
return { deployment, at, model: narrowed, report, generated };
|
|
1951
|
+
}
|
|
1952
|
+
// The warnings and the note push prints on what pruning leaves out of the
|
|
1953
|
+
// model `plan` deploys. Each line names the model, and the document and the
|
|
1954
|
+
// deployment.
|
|
1955
|
+
function pruningLines({ deployment, model, report: r }) {
|
|
1956
|
+
const where = `model '${model.model.name}' (${model.document})`;
|
|
1957
|
+
// What becomes of a field left out. An AlloyDB database holds no graph.
|
|
1958
|
+
const outcome = deployment.engine === 'alloydb' ? 'it is unavailable there' :
|
|
1959
|
+
'the graph omits it';
|
|
1960
|
+
const excluded = new Set(r.excludedFields);
|
|
1961
|
+
const omits = (field, why) => `Warning: field '${field}' in ${where} ${why}, so ${outcome}.`;
|
|
1962
|
+
// Such as "metric 'a'" or "metrics 'a' and 'b'", or '' for no names.
|
|
1963
|
+
const named = (kind, names) => names.length ?
|
|
1964
|
+
`${(0, loader_1.plural)(names.length, kind, `${kind}s`)} ${(0, loader_1.listed)(names.map(n => `'${n}'`))}` :
|
|
1965
|
+
'';
|
|
1966
|
+
const unavailable = [
|
|
1967
|
+
named('metric', r.droppedMetrics.map(m => m.name)),
|
|
1968
|
+
named('action', r.droppedActions.map(a => a.name)),
|
|
1969
|
+
].filter(n => n).join(r.droppedMetrics.length > 1 ? ', and ' : ' and ');
|
|
1970
|
+
const note = [
|
|
1971
|
+
excluded.size ? `excludes ${named('field', [...excluded])}.` : '',
|
|
1972
|
+
unavailable && `leaves ${unavailable} unavailable.`,
|
|
1973
|
+
].filter(p => p).join(' It ');
|
|
1974
|
+
return [
|
|
1975
|
+
...r.unboundFields.filter(f => !excluded.has(f))
|
|
1976
|
+
.map(f => omits(f, 'has no expression')),
|
|
1977
|
+
...r.droppedFields.map(d => omits(d.name, d.reason)),
|
|
1978
|
+
...note ? [`Note: ${where} ${note}`] : [],
|
|
1979
|
+
];
|
|
1980
|
+
}
|
|
1981
|
+
// A validate-only deploy builds the DDL and calls no service, so it needs no
|
|
1982
|
+
// project or token.
|
|
1983
|
+
const NO_SERVICE = new context_1.ApiContext('', '', '');
|
|
1984
|
+
// Generates a graph leg's DDL without calling a service, or gives why it
|
|
1985
|
+
// cannot be generated.
|
|
1986
|
+
async function generateOffline(deployLeg, models) {
|
|
1987
|
+
try {
|
|
1988
|
+
const result = await deployLeg(models, NO_SERVICE, { validateOnly: true });
|
|
1989
|
+
if (result.success)
|
|
1990
|
+
return result;
|
|
1991
|
+
return result.details ?? 'the graph could not be generated';
|
|
1992
|
+
}
|
|
1993
|
+
catch (err) {
|
|
1994
|
+
return err.message || String(err);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1624
1997
|
// Live pre-flight over the BigQuery-targeting models: confirms every entity's
|
|
1625
1998
|
// BigQuery source table is reachable BEFORE any destination leg runs, so a push
|
|
1626
1999
|
// fails fast when the model could not deploy, rather than surfacing a missing
|
|
@@ -1704,16 +2077,8 @@ async function validateBigQueryDataSources(models, bq, defaultProject, graph) {
|
|
|
1704
2077
|
// bills to: the project of the model's first BigQuery Graph deployment target
|
|
1705
2078
|
// (where the CREATE PROPERTY GRAPH runs), falling back to the scope's default
|
|
1706
2079
|
// project when the model declares no parseable BigQuery Graph target.
|
|
1707
|
-
// googleDeploymentTargets is safe here: validatePushRequirements ran first and
|
|
1708
|
-
// already rejected a malformed GOOGLE extension.
|
|
1709
2080
|
function billingProject(model, defaultProject) {
|
|
1710
|
-
|
|
1711
|
-
return (0, deploy_bigquery_1.googleDeploymentTargets)(model).bigQuery[0]?.project ??
|
|
1712
|
-
defaultProject;
|
|
1713
|
-
}
|
|
1714
|
-
catch {
|
|
1715
|
-
return defaultProject;
|
|
1716
|
-
}
|
|
2081
|
+
return (0, deploy_bigquery_1.googleDeploymentTargets)(model).bigQuery[0]?.project ?? defaultProject;
|
|
1717
2082
|
}
|
|
1718
2083
|
// A source that can be probed as a BigQuery table: the canonical `dataSource`,
|
|
1719
2084
|
// trimmed, or null when it is empty or a non-BigQuery resource URI. Unlike a
|
|
@@ -1781,24 +2146,6 @@ function rejectedStatement(pending, store, detail) {
|
|
|
1781
2146
|
return `action '${pending.action.name}' in model '${pending.model}' (${pending.document}): statement ${pending.index + 1} of its sql executor was rejected by ${store}: ${detail} Statements reach the store exactly as written, so every ` +
|
|
1782
2147
|
`table and column has to be the name the database uses.`;
|
|
1783
2148
|
}
|
|
1784
|
-
// The first line of a multi-line diagnosis. The lines after it echo the
|
|
1785
|
-
// statement and point a caret at the offending token, which is worth nothing
|
|
1786
|
-
// here: the author has the statement open in the profile, the position in
|
|
1787
|
-
// `[at 1:54]` survives, and a caret counted from column 1 lands in the wrong
|
|
1788
|
-
// place once the message is embedded in a longer sentence.
|
|
1789
|
-
//
|
|
1790
|
-
// `\n` is matched in both its forms because Spanner's top-level `error.message`
|
|
1791
|
-
// arrives with its newlines ESCAPED -- a literal backslash and an n survive
|
|
1792
|
-
// JSON.parse -- while every other message uses real ones.
|
|
1793
|
-
function firstLine(message) {
|
|
1794
|
-
return message.replace(/\\n/g, '\n').split('\n')[0].trim();
|
|
1795
|
-
}
|
|
1796
|
-
// An API error's message reduced to its first line as a sentence, such as
|
|
1797
|
-
// "Unrecognized name: accountId; Did you mean account_id? [at 1:54]."
|
|
1798
|
-
function storeMessage(message, status) {
|
|
1799
|
-
const text = (message ?? '').trim();
|
|
1800
|
-
return text ? (0, parameters_1.sentence)(firstLine(text)) : `HTTP ${status}.`;
|
|
1801
|
-
}
|
|
1802
2149
|
// The Spanner arm. Each distinct database gets one session, reused across every
|
|
1803
2150
|
// statement bound to it and deleted at the end -- which also discards the
|
|
1804
2151
|
// read-write transactions PLAN had to begin and never committed. Returns one
|
|
@@ -1814,13 +2161,7 @@ async function validateSpannerActionStatements(models, spanner) {
|
|
|
1814
2161
|
// The database an action writes to is the one this profile deploys its
|
|
1815
2162
|
// graph to: on Spanner the published graph and the operational rows live in
|
|
1816
2163
|
// the same database. A model with no Spanner target contributes nothing.
|
|
1817
|
-
|
|
1818
|
-
try {
|
|
1819
|
-
target = (0, deployment_target_1.spannerGraphTargets)(loaded.model).targets[0];
|
|
1820
|
-
}
|
|
1821
|
-
catch {
|
|
1822
|
-
continue;
|
|
1823
|
-
}
|
|
2164
|
+
const target = (0, deployment_target_1.spannerGraphTargets)(loaded.model).targets[0];
|
|
1824
2165
|
if (!target)
|
|
1825
2166
|
continue;
|
|
1826
2167
|
const key = `${target.project}/${target.instance}/${target.database}`;
|
|
@@ -1838,7 +2179,7 @@ async function validateSpannerActionStatements(models, spanner) {
|
|
|
1838
2179
|
// is reported rather than skipped: a push that could not verify its
|
|
1839
2180
|
// statements must not proceed as though it had.
|
|
1840
2181
|
errors.push(`could not open a session on ${store} to verify the sql executor ` +
|
|
1841
|
-
`statements in model '${statements[0].model}' (${statements[0].document}): ${storeMessage(opened.message, opened.status)}`);
|
|
2182
|
+
`statements in model '${statements[0].model}' (${statements[0].document}): ${(0, parameters_1.storeMessage)(opened.message, opened.status)}`);
|
|
1842
2183
|
continue;
|
|
1843
2184
|
}
|
|
1844
2185
|
try {
|
|
@@ -1853,7 +2194,7 @@ async function validateSpannerActionStatements(models, spanner) {
|
|
|
1853
2194
|
const res = await spanner.planDml(session, pending.sql, params, paramTypes);
|
|
1854
2195
|
if (res.status === 200)
|
|
1855
2196
|
continue;
|
|
1856
|
-
errors.push(rejectedStatement(pending, store, storeMessage(res.message, res.status)));
|
|
2197
|
+
errors.push(rejectedStatement(pending, store, (0, parameters_1.storeMessage)(res.message, res.status)));
|
|
1857
2198
|
}
|
|
1858
2199
|
}
|
|
1859
2200
|
finally {
|
|
@@ -1892,7 +2233,7 @@ async function validateBigQueryActionStatements(models, bq, defaultProject) {
|
|
|
1892
2233
|
const res = await bq.query(project, pending.sql, undefined, true, statementParameterTypes(pending.action, pending.sql));
|
|
1893
2234
|
if (res.status === 200)
|
|
1894
2235
|
continue;
|
|
1895
|
-
errors.push(rejectedStatement(pending, store, storeMessage(res.message, res.status)));
|
|
2236
|
+
errors.push(rejectedStatement(pending, store, (0, parameters_1.storeMessage)(res.message, res.status)));
|
|
1896
2237
|
}
|
|
1897
2238
|
}
|
|
1898
2239
|
return errors;
|