@google-cloud/kcmd 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +18 -20
  2. package/build/ts/kcmd/cli.js +6164 -5631
  3. package/build/ts/kcmd/gcp/biglake.d.ts +2 -0
  4. package/build/ts/kcmd/gcp/biglake.js +13 -4
  5. package/build/ts/kcmd/gcp/spanner.d.ts +10 -0
  6. package/build/ts/kcmd/gcp/spanner.js +6 -3
  7. package/build/ts/kcmd/layouts/okf.js +49 -7
  8. package/build/ts/kcmd/layouts/semantic-model.d.ts +13 -0
  9. package/build/ts/kcmd/layouts/semantic-model.js +129 -50
  10. package/build/ts/kcmd/okf/concept.d.ts +1 -1
  11. package/build/ts/kcmd/okf/concept.js +6 -0
  12. package/build/ts/kcmd/okf/concept_to_entry.d.ts +2 -0
  13. package/build/ts/kcmd/okf/concept_to_entry.js +29 -2
  14. package/build/ts/kcmd/okf/init_okf_workspace.js +7 -9
  15. package/build/ts/kcmd/okf/okf_metadata.js +17 -0
  16. package/build/ts/kcmd/semantic/bigquery.js +139 -195
  17. package/build/ts/kcmd/semantic/binding.js +14 -19
  18. package/build/ts/kcmd/semantic/deploy_bigquery.js +6 -13
  19. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +10 -35
  20. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +105 -162
  21. package/build/ts/kcmd/semantic/deploy_spanner.js +7 -14
  22. package/build/ts/kcmd/semantic/deployment_target.d.ts +1 -4
  23. package/build/ts/kcmd/semantic/deployment_target.js +6 -62
  24. package/build/ts/kcmd/semantic/graph_ddl.d.ts +10 -0
  25. package/build/ts/kcmd/semantic/graph_ddl.js +152 -59
  26. package/build/ts/kcmd/semantic/import_model.d.ts +26 -4
  27. package/build/ts/kcmd/semantic/import_model.js +531 -111
  28. package/build/ts/kcmd/semantic/ir.d.ts +2 -4
  29. package/build/ts/kcmd/semantic/kc_converter.d.ts +7 -1
  30. package/build/ts/kcmd/semantic/kc_converter.js +32 -27
  31. package/build/ts/kcmd/semantic/kc_custom_types.d.ts +5 -12
  32. package/build/ts/kcmd/semantic/kc_custom_types.js +43 -114
  33. package/build/ts/kcmd/semantic/kc_ids.d.ts +4 -5
  34. package/build/ts/kcmd/semantic/kc_ids.js +4 -7
  35. package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +0 -3
  36. package/build/ts/kcmd/semantic/knowledge_catalog.js +3 -86
  37. package/build/ts/kcmd/semantic/loader.d.ts +7 -11
  38. package/build/ts/kcmd/semantic/loader.js +282 -171
  39. package/build/ts/kcmd/semantic/osi_converter.js +14 -65
  40. package/build/ts/kcmd/semantic/parameters.d.ts +1 -0
  41. package/build/ts/kcmd/semantic/parameters.js +22 -3
  42. package/build/ts/kcmd/semantic/pull_kc.d.ts +1 -5
  43. package/build/ts/kcmd/semantic/pull_kc.js +38 -112
  44. package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +7 -2
  45. package/build/ts/kcmd/semantic/resolve_inheritance.js +37 -21
  46. package/build/ts/kcmd/semantic/resolve_profiles.d.ts +51 -10
  47. package/build/ts/kcmd/semantic/resolve_profiles.js +213 -32
  48. package/build/ts/kcmd/semantic/runtime/runtime.d.ts +4 -12
  49. package/build/ts/kcmd/semantic/runtime/runtime.js +4 -21
  50. package/build/ts/kcmd/semantic/runtime/store.d.ts +14 -19
  51. package/build/ts/kcmd/semantic/runtime/store.js +21 -37
  52. package/build/ts/kcmd/semantic/source_dialect.d.ts +9 -2
  53. package/build/ts/kcmd/semantic/source_dialect.js +83 -33
  54. package/build/ts/kcmd/semantic/source_entries.d.ts +1 -1
  55. package/build/ts/kcmd/semantic/source_entries.js +95 -36
  56. package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +13 -7
  57. package/build/ts/kcmd/semantic/sql_expr_utils.js +240 -67
  58. package/build/ts/kcmd/semantic/sql_parser.d.ts +5 -2
  59. package/build/ts/kcmd/semantic/sql_parser.js +22 -0
  60. package/build/ts/kcmd/semantic/transpile.d.ts +12 -10
  61. package/build/ts/kcmd/semantic/transpile.js +79 -61
  62. package/build/ts/kcmd/semantic/validate.d.ts +37 -2
  63. package/build/ts/kcmd/semantic/validate.js +467 -126
  64. package/build/ts/kcmd/snapshot.d.ts +4 -0
  65. package/build/ts/kcmd/snapshot.js +7 -0
  66. package/build/ts/kcmd/sync.js +44 -7
  67. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { Entity, Field, ProfileSpec, SemanticModel } from './ir';
2
- import { LoadOptions } from './loader';
2
+ import { LoadOptions, SemanticModelError } from './loader';
3
3
  export declare const DEFAULT_PROFILE = "default";
4
4
  export declare function plainCopy(value: unknown): any;
5
5
  export interface AvailabilityReport {
@@ -38,12 +38,13 @@ export interface AvailabilityReport {
38
38
  * it. What a field or metric reads comes from the text `dialect` evaluates
39
39
  * (see textsFor). `dialect` defaults to the dialect the model's sources
40
40
  * imply. When the sources are in systems of different dialects, or one names
41
- * no system, there is no dialect and every text counts.
41
+ * no system, there is no dialect and every text counts. When no lookup read
42
+ * the dialect, it is null and no text counts.
42
43
  * The input is never mutated. The report names each dropped block
43
44
  * and the unbound field that stops it, so a caller can state the withheld
44
45
  * coverage. A model with nothing unbound resolves unchanged.
45
46
  */
46
- export declare function pruneUnavailable(model: SemanticModel, profileName: string, dialect?: string | undefined): {
47
+ export declare function pruneUnavailable(model: SemanticModel, profileName: string, dialect?: string | null | undefined): {
47
48
  model: SemanticModel;
48
49
  report: AvailabilityReport;
49
50
  };
@@ -116,7 +117,9 @@ export interface AppliedProfile {
116
117
  }
117
118
  /**
118
119
  * A profile a model declares: the text of a profile file, or a profile already
119
- * read, such as one a vanilla model carries in its GOOGLE block.
120
+ * read, such as one a vanilla model carries in its GOOGLE block. A profile
121
+ * already read may carry `label`, the file that holds it, which
122
+ * namedProfileErrors names in place of the model document.
120
123
  */
121
124
  export type DeclaredProfile = {
122
125
  name: string;
@@ -124,7 +127,17 @@ export type DeclaredProfile = {
124
127
  } | {
125
128
  name: string;
126
129
  spec: ProfileSpec;
130
+ label?: string;
127
131
  };
132
+ export type DeclaredProfiles = (doc: {
133
+ name: string;
134
+ text: string;
135
+ }) => DeclaredProfile[];
136
+ export type ProfileFiles = (model: string) => Array<{
137
+ name: string;
138
+ text: string;
139
+ fileName: string;
140
+ }>;
128
141
  /**
129
142
  * The profiles the model document `doc` declares: `files`, its profile files,
130
143
  * then the profiles a vanilla model carries in its GOOGLE block, by name, as
@@ -160,6 +173,16 @@ export declare function profileForDocument(doc: {
160
173
  name: string;
161
174
  text: string;
162
175
  }, profiles: DeclaredProfile[], profileName: string, load?: DialectLoad): DocumentProfile;
176
+ /**
177
+ * The warnings that applying the profile `profileName` among `profiles` to the
178
+ * model document `doc` gives, each labeled with the profile. The warnings in
179
+ * `own`, which the document's own load gives, are left out. Empty unless
180
+ * profileForDocument applies the profile as a profile file.
181
+ */
182
+ export declare function appliedProfileWarnings(doc: {
183
+ name: string;
184
+ text: string;
185
+ }, profiles: DeclaredProfile[], profileName: string, own: string[], load?: DialectLoad): string[];
163
186
  /**
164
187
  * The error a command gives when a profile cannot be applied to the model
165
188
  * document `docName`, whose profile files are `profileFiles`. An error applying
@@ -173,6 +196,12 @@ export declare function profileErrorMessage(docName: string, res: Extract<Docume
173
196
  text: string;
174
197
  fileName: string;
175
198
  }>): string;
199
+ /**
200
+ * Why `model` cannot be used because a deployment names a binding profile
201
+ * `declared` does not hold, or undefined when each names `default` or a
202
+ * declared profile. `document` is the model's document, for the message.
203
+ */
204
+ export declare function undeclaredProfileError(document: string, model: SemanticModel, declared: string[]): string | undefined;
176
205
  /** The error for a profile name a model document does not declare. */
177
206
  export declare function unknownProfileMessage(profileName: string, model: string, declared: string[]): string;
178
207
  /**
@@ -184,12 +213,13 @@ export declare function unknownProfileMessage(profileName: string, model: string
184
213
  export declare function reservedProfileNameError(docName: string, profiles: {
185
214
  name: string;
186
215
  }[]): string | undefined;
216
+ export declare function readerMessage(err: SemanticModelError): string;
187
217
  export declare function profileSpecOf(doc: any, profileName: string): ProfileSpec;
188
218
  export declare function expressionSources(f: Pick<Field, 'expression' | 'dialects' | 'importedExpression' | 'importedDialect'>): Array<{
189
219
  text: string;
190
220
  dialect: string;
191
221
  }>;
192
- declare function textsFor(f: Pick<Field, 'expression' | 'dialects' | 'importedExpression' | 'importedDialect'>, dialect: string | undefined): Array<{
222
+ declare function textsFor(f: Pick<Field, 'expression' | 'dialects' | 'importedExpression' | 'importedDialect'>, dialect: string | null | undefined): Array<{
193
223
  text: string;
194
224
  dialect: string;
195
225
  }>;
@@ -198,6 +228,7 @@ export interface TableDialects {
198
228
  name: string;
199
229
  dialects: string[];
200
230
  unread?: boolean;
231
+ guessed?: boolean;
201
232
  }
202
233
  export declare function tableDialects(entity: Pick<Entity, 'abstract' | 'authoredSource' | 'dataSource' | 'sourceDialect'>, withUnread: boolean): TableDialects | undefined;
203
234
  export declare function fieldsOnTables(model: SemanticModel, withUnread?: boolean): Array<{
@@ -205,15 +236,16 @@ export declare function fieldsOnTables(model: SemanticModel, withUnread?: boolea
205
236
  field: Field;
206
237
  table: TableDialects;
207
238
  }>;
208
- export declare const IMPORT_TRANSPILE_COMMAND = "kcmd import <file> --input first-party --flavor <ossie|google> --transpile";
209
- export declare function importTranspiles(f: Parameters<typeof textsFor>[0], dialect: string | undefined): boolean;
239
+ export declare function importCommand(flags: string, version?: string): string;
240
+ export declare function importTranspiles(f: Parameters<typeof textsFor>[0], dialect: string | undefined, model: Pick<SemanticModel, 'entities'>): boolean;
241
+ export declare function importHint(f: Parameters<typeof textsFor>[0], dialect: string, model: Pick<SemanticModel, 'entities' | 'version'>): string;
210
242
  export declare function sourcesDialect(model: {
211
243
  entities?: ReadonlyArray<Pick<Entity, 'abstract' | 'authoredSource' | 'dataSource' | 'sourceDialect'>>;
212
- }): string | undefined;
244
+ }): string | null | undefined;
213
245
  /** What the caller of validateProfileCompleteness checks itself. */
214
246
  export interface CompletenessOptions extends DialectLoad {
215
247
  ownSqlOnly?: {
216
- dialect?: string;
248
+ dialect?: string | null;
217
249
  };
218
250
  }
219
251
  /**
@@ -241,8 +273,17 @@ export declare function validateProfileCompleteness(baseModel: SemanticModel, pr
241
273
  * profile's join, or the model file's when the profile states none.
242
274
  */
243
275
  export declare function validateProfileConsistency(baseModel: SemanticModel, profiles: ProfileSpec[], applied?: string): string[];
276
+ export interface LocalCheckError {
277
+ error: string;
278
+ document?: string;
279
+ profile?: string;
280
+ }
281
+ export declare function namedProfileErrors(docs: Array<{
282
+ name: string;
283
+ text: string;
284
+ }>, baseByDoc: Map<string, SemanticModel>, profileDocs: ProfileFiles, declared: DeclaredProfiles, warnings: string[], load: DialectLoad): LocalCheckError[];
244
285
  export declare function inheritanceErrorOf(model: SemanticModel): string | undefined;
245
- export declare function columnBinding(model: SemanticModel, entityName: string, field: string, dialect?: string): {
286
+ export declare function columnBinding(model: SemanticModel, entityName: string, field: string, dialect?: string | null): {
246
287
  owner: string;
247
288
  column: string;
248
289
  } | undefined;
@@ -61,7 +61,7 @@ var __importStar = (this && this.__importStar) || (function () {
61
61
  };
62
62
  })();
63
63
  Object.defineProperty(exports, "__esModule", { value: true });
64
- exports.IMPORT_TRANSPILE_COMMAND = exports.DEFAULT_PROFILE = void 0;
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 lookup has
1166
- // read is given too, and a Cloud SQL name with no schema gives MySQL.
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 that adds BigQuery SQL to a model file. Push reads only
1201
- // files written for kcmd, so `--input first-party` fits each of them.
1202
- exports.IMPORT_TRANSPILE_COMMAND = 'kcmd import <file> --input first-party --flavor <ossie|google> --transpile';
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 its only
1205
- // text is an imported one, in a dialect the translator reads.
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.transpilesFrom)(f.importedDialect ?? '');
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. A legacy bare name, which the loader completes into
1213
- // `project.dataset.table` in `dataSource`, names BigQuery however many parts it
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 Spanner databases, imply that system's dialect.
1226
- // A three-part dotted name, which is how the loader completes a legacy bare
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, and a third-party table,
1230
- // imply the dialect their catalog entry gives, which a lookup read into
1231
- // `lookedUp` (LoadOptions.sourceDialects).
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. A third-party
1333
- // table is in the database its entry's fullyQualifiedName names.
1334
- const table = lookedUp === 'SQLSERVER' && (0, loader_1.cloudSqlTableOf)(pe.source);
1335
- const key = table ? table.resourceName.split('/databases/')[0] :
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 '${exports.IMPORT_TRANSPILE_COMMAND}',` : '';
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);
@@ -23,17 +23,15 @@ export interface SemanticRuntime {
23
23
  * profile's several targets may leave the store ambiguous, the model may
24
24
  * bind a table outside its Spanner or AlloyDB store, or finding how to
25
25
  * connect may fail, such as the lookup of an AlloyDB cluster's primary
26
- * instance. Also set when a GOOGLE block's data is not valid JSON, so its
27
- * targets cannot be read. `storeError` says why.
26
+ * instance. `storeError` says why.
28
27
  */
29
28
  storeFailed?: boolean;
30
29
  /**
31
30
  * Set with `storeFailed` when the model's deployments give no store
32
31
  * whatever the model is used for. One such case is a target in
33
- * no accepted form in any deployment. Another is a GOOGLE block that is not
34
- * valid JSON. A third is several targets for the profile with no single
35
- * store. A table bound outside the store and a failed AlloyDB lookup are
36
- * not such cases.
32
+ * no accepted form in any deployment. Another is several targets for the
33
+ * profile with no single store. A table bound outside the store and a failed
34
+ * AlloyDB lookup are not such cases.
37
35
  */
38
36
  deploymentsRejected?: boolean;
39
37
  /** Which binding profile produced this. Provenance, for messages. */
@@ -86,9 +84,3 @@ export interface CreateRuntimeOptions {
86
84
  export declare function createSemanticRuntimes(options?: CreateRuntimeOptions): Promise<SemanticRuntime[] | {
87
85
  error: string;
88
86
  }>;
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, { defaultProject, bindingOptional: true, ...load });
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
- }