@google-cloud/kcmd 0.1.0 → 0.2.0
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 +5842 -5168
- 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 +8 -10
- 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_knowledge_catalog.d.ts +10 -35
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +92 -121
- 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 +526 -103
- package/build/ts/kcmd/semantic/kc_converter.d.ts +7 -1
- package/build/ts/kcmd/semantic/kc_converter.js +20 -5
- package/build/ts/kcmd/semantic/kc_custom_types.d.ts +5 -12
- package/build/ts/kcmd/semantic/kc_custom_types.js +16 -93
- 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 +284 -148
- package/build/ts/kcmd/semantic/osi_converter.js +1 -2
- 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 +0 -6
- package/build/ts/kcmd/semantic/runtime/runtime.js +4 -21
- 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 +465 -98
- 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
|
@@ -61,7 +61,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
61
61
|
};
|
|
62
62
|
})();
|
|
63
63
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
64
|
-
exports.
|
|
64
|
+
exports.DEFAULT_PROFILE = void 0;
|
|
65
65
|
exports.plainCopy = plainCopy;
|
|
66
66
|
exports.pruneUnavailable = pruneUnavailable;
|
|
67
67
|
exports.withoutExclusions = withoutExclusions;
|
|
@@ -72,18 +72,24 @@ exports.applyProfile = applyProfile;
|
|
|
72
72
|
exports.applyProfileFile = applyProfileFile;
|
|
73
73
|
exports.declaredProfiles = declaredProfiles;
|
|
74
74
|
exports.profileForDocument = profileForDocument;
|
|
75
|
+
exports.appliedProfileWarnings = appliedProfileWarnings;
|
|
75
76
|
exports.profileErrorMessage = profileErrorMessage;
|
|
77
|
+
exports.undeclaredProfileError = undeclaredProfileError;
|
|
76
78
|
exports.unknownProfileMessage = unknownProfileMessage;
|
|
77
79
|
exports.reservedProfileNameError = reservedProfileNameError;
|
|
80
|
+
exports.readerMessage = readerMessage;
|
|
78
81
|
exports.profileSpecOf = profileSpecOf;
|
|
79
82
|
exports.expressionSources = expressionSources;
|
|
80
83
|
exports.noTextFor = noTextFor;
|
|
81
84
|
exports.tableDialects = tableDialects;
|
|
82
85
|
exports.fieldsOnTables = fieldsOnTables;
|
|
86
|
+
exports.importCommand = importCommand;
|
|
83
87
|
exports.importTranspiles = importTranspiles;
|
|
88
|
+
exports.importHint = importHint;
|
|
84
89
|
exports.sourcesDialect = sourcesDialect;
|
|
85
90
|
exports.validateProfileCompleteness = validateProfileCompleteness;
|
|
86
91
|
exports.validateProfileConsistency = validateProfileConsistency;
|
|
92
|
+
exports.namedProfileErrors = namedProfileErrors;
|
|
87
93
|
exports.inheritanceErrorOf = inheritanceErrorOf;
|
|
88
94
|
exports.columnBinding = columnBinding;
|
|
89
95
|
exports.statesKey = statesKey;
|
|
@@ -135,7 +141,8 @@ function notAllowed(where, key) {
|
|
|
135
141
|
* it. What a field or metric reads comes from the text `dialect` evaluates
|
|
136
142
|
* (see textsFor). `dialect` defaults to the dialect the model's sources
|
|
137
143
|
* imply. When the sources are in systems of different dialects, or one names
|
|
138
|
-
* no system, there is no dialect and every text counts.
|
|
144
|
+
* no system, there is no dialect and every text counts. When no lookup read
|
|
145
|
+
* the dialect, it is null and no text counts.
|
|
139
146
|
* The input is never mutated. The report names each dropped block
|
|
140
147
|
* and the unbound field that stops it, so a caller can state the withheld
|
|
141
148
|
* coverage. A model with nothing unbound resolves unchanged.
|
|
@@ -579,6 +586,19 @@ function profileForDocument(doc, profiles, profileName, load = {}) {
|
|
|
579
586
|
applyProfileFileToDocument(doc, chosen.text, profileName, load);
|
|
580
587
|
return 'error' in res ? { kind: 'error', ...res } : { kind: 'file', ...res };
|
|
581
588
|
}
|
|
589
|
+
/**
|
|
590
|
+
* The warnings that applying the profile `profileName` among `profiles` to the
|
|
591
|
+
* model document `doc` gives, each labeled with the profile. The warnings in
|
|
592
|
+
* `own`, which the document's own load gives, are left out. Empty unless
|
|
593
|
+
* profileForDocument applies the profile as a profile file.
|
|
594
|
+
*/
|
|
595
|
+
function appliedProfileWarnings(doc, profiles, profileName, own, load = {}) {
|
|
596
|
+
const res = profileForDocument(doc, profiles, profileName, load);
|
|
597
|
+
if (res.kind !== 'file')
|
|
598
|
+
return [];
|
|
599
|
+
return res.warnings.filter(w => !own.includes(w))
|
|
600
|
+
.map(w => `profile '${profileName}': ${w}`);
|
|
601
|
+
}
|
|
582
602
|
/**
|
|
583
603
|
* `error`, from loading a model document on its own before a profile file
|
|
584
604
|
* applies to it, with a note that says so. A model file that a profile used to
|
|
@@ -605,6 +625,22 @@ function profileErrorMessage(docName, res, profileName, profileFiles) {
|
|
|
605
625
|
const file = profileFiles.find(p => p.name === profileName);
|
|
606
626
|
return `[${file?.fileName ?? docName}] ${res.error}`;
|
|
607
627
|
}
|
|
628
|
+
/**
|
|
629
|
+
* Why `model` cannot be used because a deployment names a binding profile
|
|
630
|
+
* `declared` does not hold, or undefined when each names `default` or a
|
|
631
|
+
* declared profile. `document` is the model's document, for the message.
|
|
632
|
+
*/
|
|
633
|
+
function undeclaredProfileError(document, model, declared) {
|
|
634
|
+
for (const d of model.deployments ?? []) {
|
|
635
|
+
const profile = d.profile ?? exports.DEFAULT_PROFILE;
|
|
636
|
+
if (profile === exports.DEFAULT_PROFILE || declared.includes(profile))
|
|
637
|
+
continue;
|
|
638
|
+
return `[${document}] model '${model.name}': deployment '${d.name}' names unknown binding profile '${profile}'; ` +
|
|
639
|
+
(declared.length ? `defined profiles: ${declared.join(', ')}.` :
|
|
640
|
+
`no profiles are defined for this model.`);
|
|
641
|
+
}
|
|
642
|
+
return undefined;
|
|
643
|
+
}
|
|
608
644
|
/** The error for a profile name a model document does not declare. */
|
|
609
645
|
function unknownProfileMessage(profileName, model, declared) {
|
|
610
646
|
return `unknown binding profile '${profileName}' for model '${model}'; ` +
|
|
@@ -1147,8 +1183,11 @@ function expressionSources(f) {
|
|
|
1147
1183
|
// database runs an ANSI_SQL text as its own SQL. A field held without a
|
|
1148
1184
|
// dialect list offers its imported text when that is in the profile's dialect,
|
|
1149
1185
|
// and otherwise the text the loader picked. With no known dialect, every text
|
|
1150
|
-
// counts, each read in the dialect it is written in.
|
|
1186
|
+
// counts, each read in the dialect it is written in. With a null dialect, which
|
|
1187
|
+
// no lookup read, the text the database runs is not known, and none counts.
|
|
1151
1188
|
function textsFor(f, dialect) {
|
|
1189
|
+
if (dialect === null)
|
|
1190
|
+
return [];
|
|
1152
1191
|
if (dialect === undefined)
|
|
1153
1192
|
return expressionSources(f);
|
|
1154
1193
|
// The text a database of this dialect runs: its own entry, or the
|
|
@@ -1162,8 +1201,9 @@ function noTextFor(f, dialects) {
|
|
|
1162
1201
|
return (0, ir_1.isFieldBound)(f) && dialects.every(d => !textsFor(f, d).length);
|
|
1163
1202
|
}
|
|
1164
1203
|
// The table of `entity`, or undefined when the load does not know its
|
|
1165
|
-
// dialect. With `withUnread`, a Cloud SQL or third-party table no
|
|
1166
|
-
// read is given too, and a Cloud SQL name with no schema gives
|
|
1204
|
+
// dialect. With `withUnread`, a Cloud SQL, Spanner or third-party table no
|
|
1205
|
+
// lookup has read is given too, and a Cloud SQL name with no schema gives
|
|
1206
|
+
// MySQL.
|
|
1167
1207
|
function tableDialects(entity, withUnread) {
|
|
1168
1208
|
const dialect = sourcesDialect({ entities: [entity] });
|
|
1169
1209
|
if (dialect)
|
|
@@ -1176,7 +1216,7 @@ function tableDialects(entity, withUnread) {
|
|
|
1176
1216
|
// Only a MySQL table's name has no schema.
|
|
1177
1217
|
const mysql = DIALECT_OF_SYSTEM.cloudsql_mysql;
|
|
1178
1218
|
if (!table.inSchema)
|
|
1179
|
-
return { name: mysql, dialects: [mysql] };
|
|
1219
|
+
return { name: mysql, dialects: [mysql], guessed: true };
|
|
1180
1220
|
const dialects = [...new Set(Object.values(loader_1.DIALECT_OF_ENTRY_TYPE))].filter(d => d !== mysql);
|
|
1181
1221
|
return { name: 'Cloud SQL', dialects, unread: true };
|
|
1182
1222
|
}
|
|
@@ -1184,6 +1224,9 @@ function tableDialects(entity, withUnread) {
|
|
|
1184
1224
|
const dialects = [...loader_1.DIALECT_OF_FQN_PREFIX.values()];
|
|
1185
1225
|
return { name: 'third-party', dialects, unread: true };
|
|
1186
1226
|
}
|
|
1227
|
+
if ((0, loader_1.spannerDatabaseOf)(source)) {
|
|
1228
|
+
return { name: 'Spanner', dialects: ['SPANNER', 'POSTGRES'], unread: true };
|
|
1229
|
+
}
|
|
1187
1230
|
return undefined;
|
|
1188
1231
|
}
|
|
1189
1232
|
// Each field of each entity whose table `tableDialects(entity, withUnread)`
|
|
@@ -1197,21 +1240,34 @@ function fieldsOnTables(model, withUnread = false) {
|
|
|
1197
1240
|
return (entity.fields ?? []).map(field => ({ entity, field, table }));
|
|
1198
1241
|
});
|
|
1199
1242
|
}
|
|
1200
|
-
// The import command
|
|
1201
|
-
//
|
|
1202
|
-
|
|
1243
|
+
// The import command, ending in `flags`, that keeps a model file of `version`
|
|
1244
|
+
// in its own flavor: `--flavor google` for a Google model, `--flavor ossie` for
|
|
1245
|
+
// a vanilla one, and both flavors when no model is known. Push reads only files
|
|
1246
|
+
// written for kcmd, so `--input first-party` fits each of them.
|
|
1247
|
+
function importCommand(flags, version) {
|
|
1248
|
+
const flavor = version === loader_1.GOOGLE_VERSION ? 'google' :
|
|
1249
|
+
version === loader_1.OSSIE_VERSION ? 'ossie' :
|
|
1250
|
+
'<ossie|google>';
|
|
1251
|
+
return `kcmd import <file> --input first-party --flavor ${flavor} ${flags}`;
|
|
1252
|
+
}
|
|
1203
1253
|
// Whether `kcmd import --transpile` gives BigQuery SQL to a field or metric
|
|
1204
|
-
// with no text for `dialect`: true when `dialect` is BIGQUERY and
|
|
1205
|
-
//
|
|
1206
|
-
function importTranspiles(f, dialect) {
|
|
1254
|
+
// of `model` with no text for `dialect`: true when `dialect` is BIGQUERY and
|
|
1255
|
+
// import's translator turns its imported text into BigQuery SQL.
|
|
1256
|
+
function importTranspiles(f, dialect, model) {
|
|
1207
1257
|
return dialect === 'BIGQUERY' && !!f.importedExpression &&
|
|
1208
|
-
(0, transpile_1.
|
|
1258
|
+
(0, transpile_1.translatesToBigQuery)(f, (model.entities ?? []).map(e => e.name));
|
|
1259
|
+
}
|
|
1260
|
+
// The hint that import can add a text in `dialect` to `f`, a field of
|
|
1261
|
+
// `model`, or ''.
|
|
1262
|
+
function importHint(f, dialect, model) {
|
|
1263
|
+
if (!importTranspiles(f, dialect, model))
|
|
1264
|
+
return '';
|
|
1265
|
+
return `; give it one, or add one with '${importCommand('--transpile', model.version)}'`;
|
|
1209
1266
|
}
|
|
1210
1267
|
// The dialect a model's sources imply: that of the one database every concrete
|
|
1211
1268
|
// entity reads from, or undefined when they name several, or a source names
|
|
1212
|
-
// none.
|
|
1213
|
-
//
|
|
1214
|
-
// was written with.
|
|
1269
|
+
// none. It is null when every source is a table whose dialect no lookup read
|
|
1270
|
+
// (see dialectOfSources).
|
|
1215
1271
|
function sourcesDialect(model) {
|
|
1216
1272
|
return dialectOfSources((model.entities ?? [])
|
|
1217
1273
|
.filter(e => !e.abstract)
|
|
@@ -1222,14 +1278,20 @@ function sourcesDialect(model) {
|
|
|
1222
1278
|
}
|
|
1223
1279
|
// The dialect every one of `sources` implies, or undefined when one names no
|
|
1224
1280
|
// database in an accepted form or two imply different dialects. Two databases
|
|
1225
|
-
// of one system, such as two
|
|
1226
|
-
// A three-part dotted name
|
|
1227
|
-
// name, names BigQuery.
|
|
1281
|
+
// of one system, such as two AlloyDB databases, imply that system's dialect.
|
|
1282
|
+
// A three-part dotted name names BigQuery.
|
|
1228
1283
|
//
|
|
1229
|
-
// A Cloud SQL table written as its resource name,
|
|
1230
|
-
// imply the dialect
|
|
1231
|
-
//
|
|
1284
|
+
// A Cloud SQL table written as its resource name, a third-party table and a
|
|
1285
|
+
// Spanner table imply the dialect a lookup gave in `lookedUp`
|
|
1286
|
+
// (LoadOptions.sourceDialects): the one their catalog entry or Spanner
|
|
1287
|
+
// database gives. When every source is such a table and none was looked up,
|
|
1288
|
+
// as when `kcmd import` cannot read them, the dialect is null: how their
|
|
1289
|
+
// database reads an ANSI_SQL text is not known, so a check reads no such text.
|
|
1232
1290
|
function dialectOfSources(sources) {
|
|
1291
|
+
if (sources.length &&
|
|
1292
|
+
sources.every(s => (0, loader_1.dialectLookupKey)(s.source.trim()) && !s.lookedUp)) {
|
|
1293
|
+
return null;
|
|
1294
|
+
}
|
|
1233
1295
|
const dialects = new Set(sources.map(({ source, lookedUp }) => {
|
|
1234
1296
|
const s = source.trim();
|
|
1235
1297
|
if ((0, loader_1.dialectLookupKey)(s))
|
|
@@ -1304,6 +1366,13 @@ function validateProfileCompleteness(baseModel, profile, warnings = [], opts = {
|
|
|
1304
1366
|
// is the one its own sources imply, so a source the model file states for an
|
|
1305
1367
|
// entity the profile leaves out does not decide it.
|
|
1306
1368
|
const databases = new Map();
|
|
1369
|
+
// Each table whose entry no lookup read, with the keys of the databases it
|
|
1370
|
+
// may be in: its PostgreSQL database or its SQL Server instance for a Cloud
|
|
1371
|
+
// SQL table in a schema, and any third-party database for a third-party
|
|
1372
|
+
// table. `key` is the one it takes when it counts as a database of its own.
|
|
1373
|
+
const unreadTables = [];
|
|
1374
|
+
const isThirdParty = (k) => k.startsWith('thirdparty/') ||
|
|
1375
|
+
[...loader_1.DIALECT_OF_FQN_PREFIX.keys()].some(p => k.startsWith(`${p}/`));
|
|
1307
1376
|
const stated = [];
|
|
1308
1377
|
const profiledByName = new Map((profiled.entities ?? []).map(e => [e.name, e]));
|
|
1309
1378
|
// The entities whose source the loader's reader rejects.
|
|
@@ -1329,10 +1398,24 @@ function validateProfileCompleteness(baseModel, profile, warnings = [], opts = {
|
|
|
1329
1398
|
}
|
|
1330
1399
|
const lookedUp = profiledEntity.sourceDialect;
|
|
1331
1400
|
stated.push({ source: pe.source, lookedUp });
|
|
1332
|
-
// One query reaches every database of a SQL Server instance
|
|
1333
|
-
// table is
|
|
1334
|
-
|
|
1335
|
-
|
|
1401
|
+
// One query reaches every database of a SQL Server instance, so a SQL
|
|
1402
|
+
// Server table is keyed by its instance. A third-party table is in the
|
|
1403
|
+
// database its entry's fullyQualifiedName names. A Cloud SQL table in a
|
|
1404
|
+
// schema may be either engine's, so for it and for a third-party table
|
|
1405
|
+
// the entry decides which database of its system it is in. A table whose
|
|
1406
|
+
// entry no lookup read is compared after the loop.
|
|
1407
|
+
const table = (0, loader_1.cloudSqlTableOf)(pe.source);
|
|
1408
|
+
const instance = table?.resourceName.split('/databases/')[0];
|
|
1409
|
+
if (lookedUp === undefined && table?.inSchema) {
|
|
1410
|
+
const fits = (k) => k === db || k === instance;
|
|
1411
|
+
unreadTables.push({ entity: e.name, key: instance, fits });
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
if (lookedUp === undefined && db === 'thirdparty') {
|
|
1415
|
+
unreadTables.push({ entity: e.name, key: 'thirdparty/', fits: isThirdParty });
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
const key = table && lookedUp === 'SQLSERVER' ? instance :
|
|
1336
1419
|
db !== 'thirdparty' ? db :
|
|
1337
1420
|
opts.sourceDatabases?.get(pe.source.trim()) ?? `thirdparty/${lookedUp}`;
|
|
1338
1421
|
if (!databases.has(key))
|
|
@@ -1345,6 +1428,14 @@ function validateProfileCompleteness(baseModel, profile, warnings = [], opts = {
|
|
|
1345
1428
|
databases.delete(`thirdparty/${systemDialect}`);
|
|
1346
1429
|
}
|
|
1347
1430
|
}
|
|
1431
|
+
// A table whose entry no lookup read counts as another database only when
|
|
1432
|
+
// none of the databases it may be in holds another table. Tables of two
|
|
1433
|
+
// systems are never in one database, so it always counts beside a table of
|
|
1434
|
+
// another system.
|
|
1435
|
+
for (const u of unreadTables) {
|
|
1436
|
+
if (![...databases.keys()].some(u.fits))
|
|
1437
|
+
databases.set(u.key, u.entity);
|
|
1438
|
+
}
|
|
1348
1439
|
if (databases.size > 1) {
|
|
1349
1440
|
const named = [...databases.values()].map(n => `'${n}'`).join(', ');
|
|
1350
1441
|
errors.push(`${at} binds entities in more than one database (${named}); a ` +
|
|
@@ -1365,11 +1456,13 @@ function validateProfileCompleteness(baseModel, profile, warnings = [], opts = {
|
|
|
1365
1456
|
// These rules check every text the profile wrote, since each must be a
|
|
1366
1457
|
// valid field expression wherever it runs; the dependency rules below read
|
|
1367
1458
|
// only the text the profile's dialect evaluates. An ANSI_SQL text is read
|
|
1368
|
-
// as the profile's own SQL
|
|
1459
|
+
// as the profile's own SQL, and not at all when no lookup read its
|
|
1460
|
+
// dialect.
|
|
1369
1461
|
const others = entities.map(x => x.name).filter(n => n !== pe.name);
|
|
1370
1462
|
for (const pf of pe.fields ?? []) {
|
|
1371
1463
|
// A text with no SQL is reported above, and has nothing to read.
|
|
1372
|
-
const readable = expressionSources(pf).filter(s => (0, loader_1.noSqlReason)(s.text, s.dialect, pe.name) === undefined
|
|
1464
|
+
const readable = expressionSources(pf).filter(s => (0, loader_1.noSqlReason)(s.text, s.dialect, pe.name) === undefined &&
|
|
1465
|
+
!(s.dialect === 'ANSI_SQL' && dialect === null));
|
|
1373
1466
|
for (const { text, dialect: written } of readable) {
|
|
1374
1467
|
const columns = (0, sql_expr_utils_1.columnReferences)(text, written === 'ANSI_SQL' && dialect ? dialect : written);
|
|
1375
1468
|
if (!columns) {
|
|
@@ -1386,6 +1479,9 @@ function validateProfileCompleteness(baseModel, profile, warnings = [], opts = {
|
|
|
1386
1479
|
if (!known.has(ref) && !unknownAncestry.has(pe.name)) {
|
|
1387
1480
|
errors.push(`${at}: field '${pe.name}.${pf.name}' reads '${pe.name}.${ref}', which is not a field of '${pe.name}'`);
|
|
1388
1481
|
}
|
|
1482
|
+
else if (ref === pf.name) {
|
|
1483
|
+
errors.push(`${at}: ${(0, sql_expr_utils_1.selfReferenceMessage)(pe.name, [pf.name])}`);
|
|
1484
|
+
}
|
|
1389
1485
|
}
|
|
1390
1486
|
}
|
|
1391
1487
|
}
|
|
@@ -1519,7 +1615,7 @@ function validateProfileCompleteness(baseModel, profile, warnings = [], opts = {
|
|
|
1519
1615
|
JSON.stringify(expressionSources(asInModelFile)) &&
|
|
1520
1616
|
sourcesDialect({ entities: [e] }) === dialect;
|
|
1521
1617
|
if (noTextFor(f, [dialect]) && !reported) {
|
|
1522
|
-
errors.push(`${at}: field '${e.name}.${f.name}' has no expression for ${dialect} or ANSI_SQL, and this profile's sources are ${dialect}`);
|
|
1618
|
+
errors.push(`${at}: field '${e.name}.${f.name}' has no expression for ${dialect} or ANSI_SQL, and this profile's sources are ${dialect}${importHint(f, dialect, baseModel)}`);
|
|
1523
1619
|
}
|
|
1524
1620
|
}
|
|
1525
1621
|
}
|
|
@@ -1527,8 +1623,9 @@ function validateProfileCompleteness(baseModel, profile, warnings = [], opts = {
|
|
|
1527
1623
|
const excludedMetrics = new Set(profile.metricsExclude ?? []);
|
|
1528
1624
|
for (const m of baseModel.metrics ?? []) {
|
|
1529
1625
|
if (!excludedMetrics.has(m.name) && noTextFor(m, [dialect])) {
|
|
1530
|
-
const viaImport = importTranspiles(m, dialect) ?
|
|
1531
|
-
` add one with '${
|
|
1626
|
+
const viaImport = importTranspiles(m, dialect, baseModel) ?
|
|
1627
|
+
` add one with '${importCommand('--transpile', baseModel.version)}',` :
|
|
1628
|
+
'';
|
|
1532
1629
|
errors.push(`${at}: metric '${m.name}' has no expression for ${dialect} or ` +
|
|
1533
1630
|
`ANSI_SQL, and this profile's sources are ${dialect}; give it ` +
|
|
1534
1631
|
`one,${viaImport} or add it to 'metrics_exclude'`);
|
|
@@ -1661,6 +1758,90 @@ function validateProfileConsistency(baseModel, profiles, applied) {
|
|
|
1661
1758
|
}
|
|
1662
1759
|
return errors;
|
|
1663
1760
|
}
|
|
1761
|
+
// The errors the named profiles of each model in `docs` give: each profile
|
|
1762
|
+
// file, and each profile a vanilla model carries in its GOOGLE block, checked
|
|
1763
|
+
// on its own for completeness. An error about a profile file names that file,
|
|
1764
|
+
// and one about a block profile names its label or the model document.
|
|
1765
|
+
// A model's profiles are checked against its model file as loaded on its own,
|
|
1766
|
+
// which `baseByDoc` holds. A file under the reserved name 'default', which no
|
|
1767
|
+
// push deploys, is not read here. Push also checks the
|
|
1768
|
+
// keys and join columns of the model file and of each named profile. The
|
|
1769
|
+
// warnings go to `warnings`. `profileDocs` gives a model's profile files, and
|
|
1770
|
+
// `declared` the profiles a document declares, as push reads them.
|
|
1771
|
+
function namedProfileErrors(docs, baseByDoc, profileDocs, declared, warnings, load) {
|
|
1772
|
+
const errors = [];
|
|
1773
|
+
for (const d of docs) {
|
|
1774
|
+
// Each named profile, with the label its errors carry and a function that
|
|
1775
|
+
// reads it. declared() lists the profile files too, so only its block
|
|
1776
|
+
// profiles are taken from it. A block profile is already read. Its actions
|
|
1777
|
+
// are dropped, as loadProfileFile skips a profile file's.
|
|
1778
|
+
let named;
|
|
1779
|
+
try {
|
|
1780
|
+
named = [
|
|
1781
|
+
...profileDocs(d.name).map(p => ({
|
|
1782
|
+
name: p.name,
|
|
1783
|
+
label: p.fileName,
|
|
1784
|
+
read: () => loadProfileFile(p.text, p.name, { skipActions: true }),
|
|
1785
|
+
})),
|
|
1786
|
+
...declared(d).flatMap(p => 'spec' in p ? [{
|
|
1787
|
+
name: p.name,
|
|
1788
|
+
label: p.label ?? d.name,
|
|
1789
|
+
read: () => {
|
|
1790
|
+
const { actions: _, ...spec } = p.spec;
|
|
1791
|
+
return spec;
|
|
1792
|
+
},
|
|
1793
|
+
}] : []),
|
|
1794
|
+
].filter(p => p.name.toLowerCase() !== exports.DEFAULT_PROFILE);
|
|
1795
|
+
}
|
|
1796
|
+
catch (err) {
|
|
1797
|
+
// A profile file that breaks one of the layout's file rules, or block
|
|
1798
|
+
// profiles a load rejects.
|
|
1799
|
+
errors.push({ error: `${err.message || err}`, document: d.name });
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
// A document that holds no model has nothing to check its profiles
|
|
1803
|
+
// against.
|
|
1804
|
+
const base = baseByDoc.get(d.name);
|
|
1805
|
+
if (!base)
|
|
1806
|
+
continue;
|
|
1807
|
+
// Actions are out of scope for the preview. Push checks a profile's
|
|
1808
|
+
// actions only when it applies the profile, so these checks leave them
|
|
1809
|
+
// out.
|
|
1810
|
+
const logical = { ...base, actions: [] };
|
|
1811
|
+
// Push warns about the SQL it cannot read in the model file, so these
|
|
1812
|
+
// checks leave out a warning about model file SQL that push reads in the
|
|
1813
|
+
// same dialect. Push stops reading what the SQL reads when the model's
|
|
1814
|
+
// hierarchy does not resolve, and these checks then give every warning.
|
|
1815
|
+
const ownSqlOnly = !inheritanceErrorOf(base) ?
|
|
1816
|
+
{ dialect: sourcesDialect(base) } :
|
|
1817
|
+
undefined;
|
|
1818
|
+
const specs = [];
|
|
1819
|
+
for (const p of named) {
|
|
1820
|
+
const at = (m) => `[${p.label}] ${m}`;
|
|
1821
|
+
const tag = (m) => ({ error: at(m), document: d.name, profile: p.name });
|
|
1822
|
+
let spec;
|
|
1823
|
+
try {
|
|
1824
|
+
spec = p.read();
|
|
1825
|
+
}
|
|
1826
|
+
catch (err) {
|
|
1827
|
+
if (!(err instanceof loader_1.SemanticModelError))
|
|
1828
|
+
throw err;
|
|
1829
|
+
errors.push(tag(err.message));
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1832
|
+
specs.push(spec);
|
|
1833
|
+
const profileWarnings = [];
|
|
1834
|
+
errors.push(...validateProfileCompleteness(logical, spec, profileWarnings, {
|
|
1835
|
+
ownSqlOnly,
|
|
1836
|
+
...load,
|
|
1837
|
+
}).map(tag));
|
|
1838
|
+
warnings.push(...profileWarnings.map(at));
|
|
1839
|
+
}
|
|
1840
|
+
errors.push(...validateProfileConsistency(logical, specs, exports.DEFAULT_PROFILE)
|
|
1841
|
+
.map(e => ({ error: `[${d.name}] ${e}`, document: d.name })));
|
|
1842
|
+
}
|
|
1843
|
+
return errors;
|
|
1844
|
+
}
|
|
1664
1845
|
// Each entity's fields, declared plus inherited, by name. Resolution throws on a
|
|
1665
1846
|
// broken hierarchy, which push reports elsewhere. Each entity is then resolved
|
|
1666
1847
|
// with only its own ancestors, and one whose own ancestors do not resolve
|
|
@@ -1767,7 +1948,7 @@ function boundDeclaration(model, entityName, field) {
|
|
|
1767
1948
|
// The first column `entityName`'s binding of `field` reads, as written, and
|
|
1768
1949
|
// the entity that declares it (see boundDeclaration), or undefined when that
|
|
1769
1950
|
// entity is abstract. A column is a reference no entity name qualifies, and an
|
|
1770
|
-
// ANSI_SQL text is read in `dialect` if set.
|
|
1951
|
+
// ANSI_SQL text is read in `dialect` if set, and not at all if it is null.
|
|
1771
1952
|
function columnBinding(model, entityName, field, dialect) {
|
|
1772
1953
|
const byName = new Map((model.entities ?? []).map(e => [e.name, e]));
|
|
1773
1954
|
const bound = boundDeclaration(model, entityName, field);
|
|
@@ -86,9 +86,3 @@ export interface CreateRuntimeOptions {
|
|
|
86
86
|
export declare function createSemanticRuntimes(options?: CreateRuntimeOptions): Promise<SemanticRuntime[] | {
|
|
87
87
|
error: string;
|
|
88
88
|
}>;
|
|
89
|
-
/**
|
|
90
|
-
* Why `model` cannot be used because a deployment names a binding profile
|
|
91
|
-
* `declared` does not hold, or undefined when each names `default` or a
|
|
92
|
-
* declared profile. `document` is the model's document, for the message.
|
|
93
|
-
*/
|
|
94
|
-
export declare function undeclaredProfileError(document: string, model: SemanticModel, declared: string[]): string | undefined;
|
|
@@ -50,7 +50,6 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
50
50
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
51
|
exports.runtimeStoreError = runtimeStoreError;
|
|
52
52
|
exports.createSemanticRuntimes = createSemanticRuntimes;
|
|
53
|
-
exports.undeclaredProfileError = undeclaredProfileError;
|
|
54
53
|
const alloydb_1 = require("../../gcp/alloydb");
|
|
55
54
|
const context = __importStar(require("../../gcp/context"));
|
|
56
55
|
const dataplex_1 = require("../../gcp/dataplex");
|
|
@@ -143,7 +142,8 @@ async function createSemanticRuntimes(options = {}) {
|
|
|
143
142
|
// A Cloud SQL table written as its resource name takes its dialect from its
|
|
144
143
|
// Knowledge Catalog entry, looked up once for the scope before anything
|
|
145
144
|
// loads. A table whose entry cannot be read stops the scope, as push does.
|
|
146
|
-
// A third-party table is not looked up, since no runtime reads one
|
|
145
|
+
// A third-party table is not looked up, since no runtime reads one, and a
|
|
146
|
+
// Spanner table is SPANNER, the dialect the runtime's Spanner store speaks.
|
|
147
147
|
const lookup = await (0, source_dialect_1.sourceDialects)([...docs.map(d => d.text),
|
|
148
148
|
...[...filesByDoc.values()].flat().map(f => f.text)], new dataplex_1.CatalogClient(ctx), { thirdParty: false });
|
|
149
149
|
if (lookup.errors.length)
|
|
@@ -156,7 +156,6 @@ async function createSemanticRuntimes(options = {}) {
|
|
|
156
156
|
const appliedByDoc = new Map();
|
|
157
157
|
// The warnings applying each profile gave.
|
|
158
158
|
const fileWarnings = new Map();
|
|
159
|
-
const defaultProject = source.project ?? ctx.project;
|
|
160
159
|
for (const doc of docs) {
|
|
161
160
|
if (profile === resolve_profiles_1.DEFAULT_PROFILE) {
|
|
162
161
|
inline.push({ name: doc.name, text: doc.text });
|
|
@@ -174,7 +173,7 @@ async function createSemanticRuntimes(options = {}) {
|
|
|
174
173
|
fileWarnings.set(doc.name, res.warnings);
|
|
175
174
|
appliedByDoc.set(doc.name, res.model);
|
|
176
175
|
}
|
|
177
|
-
const loaded = (0, loader_1.loadSemanticModels)(inline, {
|
|
176
|
+
const loaded = (0, loader_1.loadSemanticModels)(inline, { bindingOptional: true, ...load });
|
|
178
177
|
if (loaded.error)
|
|
179
178
|
return { error: loaded.error };
|
|
180
179
|
// In the order of the documents, as one load of them all gives. The load
|
|
@@ -194,7 +193,7 @@ async function createSemanticRuntimes(options = {}) {
|
|
|
194
193
|
// used by no profile. Push refuses it too.
|
|
195
194
|
for (const { document, model } of models) {
|
|
196
195
|
const declared = (profilesByDoc.get(document) ?? []).map(p => p.name);
|
|
197
|
-
const error = undeclaredProfileError(document, model, declared);
|
|
196
|
+
const error = (0, resolve_profiles_1.undeclaredProfileError)(document, model, declared);
|
|
198
197
|
if (error)
|
|
199
198
|
return { error };
|
|
200
199
|
}
|
|
@@ -300,19 +299,3 @@ async function createSemanticRuntimes(options = {}) {
|
|
|
300
299
|
}
|
|
301
300
|
return runtimes;
|
|
302
301
|
}
|
|
303
|
-
/**
|
|
304
|
-
* Why `model` cannot be used because a deployment names a binding profile
|
|
305
|
-
* `declared` does not hold, or undefined when each names `default` or a
|
|
306
|
-
* declared profile. `document` is the model's document, for the message.
|
|
307
|
-
*/
|
|
308
|
-
function undeclaredProfileError(document, model, declared) {
|
|
309
|
-
for (const d of model.deployments ?? []) {
|
|
310
|
-
const profile = d.profile ?? resolve_profiles_1.DEFAULT_PROFILE;
|
|
311
|
-
if (profile === resolve_profiles_1.DEFAULT_PROFILE || declared.includes(profile))
|
|
312
|
-
continue;
|
|
313
|
-
return `[${document}] model '${model.name}': deployment '${d.name}' names unknown binding profile '${profile}'; ` +
|
|
314
|
-
(declared.length ? `defined profiles: ${declared.join(', ')}.` :
|
|
315
|
-
`no profiles are defined for this model.`);
|
|
316
|
-
}
|
|
317
|
-
return undefined;
|
|
318
|
-
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
import type { SpannerClient } from '../gcp/spanner';
|
|
1
2
|
import { EntryLookup } from './catalog_entry';
|
|
2
3
|
export interface SourceDialects {
|
|
3
4
|
dialects: Map<string, string>;
|
|
4
5
|
databases: Map<string, string>;
|
|
5
6
|
errors: string[];
|
|
7
|
+
unread: string[];
|
|
6
8
|
}
|
|
7
|
-
export declare function sourceDialects(texts: Iterable<string>, catalog: EntryLookup, { thirdParty }?: {
|
|
8
|
-
thirdParty?: boolean
|
|
9
|
+
export declare function sourceDialects(texts: Iterable<string>, catalog: EntryLookup, { thirdParty, spanner }?: {
|
|
10
|
+
thirdParty?: boolean;
|
|
11
|
+
spanner?: Pick<SpannerClient, 'getDatabase'>;
|
|
9
12
|
}): Promise<SourceDialects>;
|
|
13
|
+
export declare function tableFqnParts(fqn: string): {
|
|
14
|
+
prefix: string;
|
|
15
|
+
parts: string[];
|
|
16
|
+
} | undefined;
|
|
10
17
|
export declare function sourcesToLookUp(texts: Iterable<string>): string[];
|