@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
@@ -37,14 +37,19 @@ var __importStar = (this && this.__importStar) || (function () {
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
38
  exports.importSemanticModelYaml = importSemanticModelYaml;
39
39
  const yaml = __importStar(require("yaml"));
40
+ const semantic_model_1 = require("../layouts/semantic-model");
41
+ const deploy_knowledge_catalog_1 = require("./deploy_knowledge_catalog");
40
42
  const deployment_target_1 = require("./deployment_target");
41
43
  const ir_1 = require("./ir");
42
44
  const loader_1 = require("./loader");
43
45
  const names_1 = require("./names");
46
+ const parameters_1 = require("./parameters");
44
47
  const resolve_profiles_1 = require("./resolve_profiles");
48
+ const source_dialect_1 = require("./source_dialect");
45
49
  const sql_expr_utils_1 = require("./sql_expr_utils");
46
50
  const sql_parser_1 = require("./sql_parser");
47
51
  const transpile_1 = require("./transpile");
52
+ const validate_1 = require("./validate");
48
53
  const yaml_options_1 = require("./yaml_options");
49
54
  const yaml_update_1 = require("./yaml_update");
50
55
  const STANDARD_AI_CONTEXT_KEYS = new Set(['instructions', 'synonyms', 'examples']);
@@ -53,10 +58,14 @@ const LOADER_SOURCE_WARNING = /^(.*?): source '(.*)' is not a valid GCP resource
53
58
  // Why no name to write is given when the name holds a quote.
54
59
  const NO_QUOTE = 'kcmd cannot name a table whose name holds a quote';
55
60
  // Each kind of --source-prefix for the service `prefix` names, or every kind
56
- // when there is no prefix.
61
+ // when there is no prefix. The prefix's own kind comes first, so advice names
62
+ // its form.
57
63
  function prefixKinds(prefix) {
58
64
  const host = prefix?.base.split('/')[0];
59
- return SOURCE_PREFIX_PATTERNS.filter(k => host === undefined || k.form.startsWith(`${host}/`));
65
+ const own = prefix &&
66
+ SOURCE_PREFIX_PATTERNS.find(k => k.re.test(prefix.base))?.form;
67
+ return SOURCE_PREFIX_PATTERNS.filter(k => host === undefined || k.form.startsWith(`${host}/`))
68
+ .sort((a, b) => Number(b.form === own) - Number(a.form === own));
60
69
  }
61
70
  // The warning for `source`, which import leaves as written after a run with
62
71
  // `prefix`, if any. It gives the name to write when the loader names one,
@@ -96,6 +105,19 @@ function sourceWarning(ctx, source, prefix) {
96
105
  '.') +
97
106
  ((0, loader_1.isGoogleCloudForm)(source) ? '' : ` ${loader_1.THIRD_PARTY_SOURCE}`);
98
107
  }
108
+ // Push's error for `source`, read as the loader reads a source in `ctx`, or
109
+ // undefined when push accepts it.
110
+ function sourceError(source, ctx) {
111
+ try {
112
+ (0, loader_1.parseSource)(source, {}, ctx);
113
+ return undefined;
114
+ }
115
+ catch (err) {
116
+ if (!(err instanceof loader_1.SemanticModelError))
117
+ throw err;
118
+ return (0, resolve_profiles_1.readerMessage)(err);
119
+ }
120
+ }
99
121
  const ALLOWED_DIALECT_SET = new Set(ir_1.ALLOWED_DIALECTS);
100
122
  const DIALECT_ALIASES = {
101
123
  GOOGLE_SQL: 'BIGQUERY',
@@ -124,6 +146,10 @@ const SOURCE_PREFIX_PATTERNS = [
124
146
  re: /^biglake\.googleapis\.com\/projects\/([^/]+)\/catalogs\/([^/.]+)\/namespaces\/([^/.]+)$/,
125
147
  form: loader_1.TABLE_FORMS.biglake,
126
148
  },
149
+ {
150
+ re: /^biglake\.googleapis\.com\/projects\/([^/]+)\/catalogs\/([^/.]+)\/databases\/([^/.]+)$/,
151
+ form: loader_1.TABLE_FORMS.biglakeHive,
152
+ },
127
153
  {
128
154
  re: /^spanner\.googleapis\.com\/projects\/([^/]+)\/instances\/([^/]+)\/databases\/([^/]+)$/,
129
155
  form: loader_1.TABLE_FORMS.spanner,
@@ -146,6 +172,10 @@ const SOURCE_PREFIX_PATTERNS = [
146
172
  form: loader_1.TABLE_FORMS.cloudsqlSchema,
147
173
  },
148
174
  ];
175
+ // An entry group a `--source-prefix` names. A system entry group, such as
176
+ // `@bigquery`, is refused, since its entries are Google Cloud tables, which a
177
+ // source names by resource name.
178
+ const ENTRY_GROUP_PREFIX = /^projects\/([^/]+)\/locations\/([^/]+)\/entryGroups\/([^/]+)$/;
149
179
  /** Filters an unknown array down to its plain-object elements. */
150
180
  function asRecords(v) {
151
181
  return Array.isArray(v) ? v.filter(loader_1.isRecord) : [];
@@ -217,20 +247,42 @@ function parseImportDocument(rawYaml, opts) {
217
247
  return err?.message ?? String(err);
218
248
  }
219
249
  }
220
- // 3. Validate `opts.sourcePrefix` against the 8 supported first-party GCP
221
- // container resource name patterns before doing any document work.
250
+ // 3. Validate `opts.sourcePrefix` against the supported first-party GCP
251
+ // container resource name patterns, or as an entry group, before doing any
252
+ // document work.
222
253
  let sourcePrefix;
254
+ let entryGroup;
223
255
  if (opts.sourcePrefix !== undefined) {
224
256
  const base = opts.sourcePrefix.trim();
225
257
  const match = SOURCE_PREFIX_PATTERNS.map(p => ({ m: base.match(p.re), p }))
226
258
  .find(x => x.m !== null);
227
- if (!match?.m) {
259
+ const group = base.match(ENTRY_GROUP_PREFIX);
260
+ if (match?.m) {
261
+ sourcePrefix = { base, known: match.m.slice(1), child: match.p.child };
262
+ }
263
+ else if (group?.[3].startsWith('@')) {
264
+ return `sourcePrefix '${opts.sourcePrefix}' names the system entry ` +
265
+ `group '${group[3]}', whose entries are Google Cloud tables. Pass ` +
266
+ `the GCP resource name of their database instead, such as ` +
267
+ `'bigquery.googleapis.com/projects/<p>/datasets/<d>'.`;
268
+ }
269
+ else if (group) {
270
+ entryGroup = {
271
+ base,
272
+ project: group[1],
273
+ location: group[2],
274
+ group: group[3],
275
+ list: opts.listEntries,
276
+ };
277
+ }
278
+ else {
228
279
  return `Invalid sourcePrefix '${opts.sourcePrefix}'; expected the GCP ` +
229
280
  `resource name of a BigQuery project or dataset, a BigLake ` +
230
- `catalog or namespace, a Spanner database, an AlloyDB database ` +
231
- `or schema, or a Cloud SQL database or schema (with no leading '//').`;
281
+ `catalog, namespace or Hive database, a Spanner database, an ` +
282
+ `AlloyDB database or schema, or a Cloud SQL database or schema ` +
283
+ `(with no leading '//'), or the name of a Knowledge Catalog entry ` +
284
+ `group ('projects/<p>/locations/<l>/entryGroups/<g>').`;
232
285
  }
233
- sourcePrefix = { base, known: match.m.slice(1), child: match.p.child };
234
286
  }
235
287
  // 4. Reject a document written for kcmd when `strict` is false: one with a
236
288
  // `GOOGLE` block, or a Google-flavor model converting to vanilla.
@@ -269,6 +321,7 @@ function parseImportDocument(rawYaml, opts) {
269
321
  inputVersion,
270
322
  targetVersion,
271
323
  sourcePrefix,
324
+ entryGroup,
272
325
  strict,
273
326
  modified: false,
274
327
  warnings,
@@ -308,11 +361,10 @@ function parseGoogleBlocks(exts) {
308
361
  }
309
362
  /**
310
363
  * Validates all model, deployment, profile, entity/dataset, field,
311
- * relationship, and metric names in `doc` (plus any `googleBlocks` and
312
- * `profileFiles`) in a single pass so every invalid or over-length identifier
313
- * is reported at once.
364
+ * relationship, and metric names in `doc` (plus any `googleBlocks`) in a
365
+ * single pass so every invalid or over-length identifier is reported at once.
314
366
  */
315
- function validateObjectNames(doc, googleBlocks, modelScope, profileFiles) {
367
+ function validateObjectNames(doc, googleBlocks, modelScope) {
316
368
  // Validates a single identifier `name` against `kind`'s rules via
317
369
  // `nameProblem` and appends any error to `doc.errors`.
318
370
  const validateName = (kind, name, scope) => {
@@ -327,8 +379,8 @@ function validateObjectNames(doc, googleBlocks, modelScope, profileFiles) {
327
379
  for (const record of records)
328
380
  validateName(kind, record.name, scope);
329
381
  };
330
- // 1. Validate model, deployment, and profile names (inline, `GOOGLE` block,
331
- // and sibling `profileFiles`).
382
+ // 1. Validate model, deployment, and profile names (inline and `GOOGLE`
383
+ // block).
332
384
  validateName('model', doc.model.name);
333
385
  validateRecords('deployment', asRecords(doc.model.deployments), modelScope);
334
386
  validateRecords('profile', asRecords(doc.model.profiles), modelScope);
@@ -336,11 +388,6 @@ function validateObjectNames(doc, googleBlocks, modelScope, profileFiles) {
336
388
  validateRecords('deployment', asRecords(g.deployments), modelScope);
337
389
  validateRecords('profile', asRecords(g.profiles), modelScope);
338
390
  }
339
- if (doc.targetVersion === loader_1.OSSIE_VERSION) {
340
- for (const pf of profileFiles ?? []) {
341
- validateName('profile', parseProfileName(pf.fileName), modelScope);
342
- }
343
- }
344
391
  // 2. Validate dataset/entity, field, relationship, and metric names.
345
392
  for (const ds of doc.datasets) {
346
393
  validateName('entity', ds.name);
@@ -352,15 +399,14 @@ function validateObjectNames(doc, googleBlocks, modelScope, profileFiles) {
352
399
  }
353
400
  /**
354
401
  * Phase 2 (Pre-validation): Validates all identifier names in the input
355
- * document and any attached `profileFiles`. The loader rejects a model's
356
- * legacy `deploymentTargets`.
402
+ * document. The loader rejects a model's legacy `deploymentTargets`.
357
403
  */
358
- function validateInputDocument(doc, profileFiles) {
404
+ function validateInputDocument(doc) {
359
405
  const modelScope = typeof doc.model.name === 'string' ?
360
406
  `model '${doc.model.name}'` :
361
407
  'model';
362
408
  const googleBlocks = parseGoogleBlocks(doc.model.custom_extensions);
363
- validateObjectNames(doc, googleBlocks, modelScope, profileFiles);
409
+ validateObjectNames(doc, googleBlocks, modelScope);
364
410
  }
365
411
  /**
366
412
  * The parts of a bare dot-separated table name `source` as `prefix` reads
@@ -375,7 +421,7 @@ function sourceParts(source, prefix) {
375
421
  (0, loader_1.hasUnquotedWhitespace)(trimmed)) {
376
422
  return undefined;
377
423
  }
378
- const segs = prefixParts(trimmed, prefix.base);
424
+ const segs = prefixParts(trimmed, prefix?.base ?? '');
379
425
  return segs.length && segs.every(s => s.text.length) ? segs : undefined;
380
426
  }
381
427
  /**
@@ -478,6 +524,79 @@ function completes(source, kinds) {
478
524
  qualifyTableSource(source, prefix) !== undefined;
479
525
  });
480
526
  }
527
+ /**
528
+ * Phase 2b (Catalog read): When `doc.entryGroup` is set and a dataset's
529
+ * source is a bare table name, lists the entry group's entries once into
530
+ * `doc.entryTables`, keeping those whose fullyQualifiedName names a table or
531
+ * view. Returns the error when the list fails.
532
+ */
533
+ async function listEntryTables(doc) {
534
+ const group = doc.entryGroup;
535
+ if (!group ||
536
+ !doc.datasets.some(ds => typeof ds.source === 'string' && sourceParts(ds.source))) {
537
+ return undefined;
538
+ }
539
+ const tables = [];
540
+ try {
541
+ for await (const e of group.list(group.project, group.location, group.group)) {
542
+ const fqn = e.fullyQualifiedName ?? '';
543
+ const table = (0, source_dialect_1.tableFqnParts)(fqn);
544
+ if (!table)
545
+ continue;
546
+ tables.push({
547
+ name: `${group.base}${e.name.slice(e.name.indexOf('/entries/'))}`,
548
+ fullyQualifiedName: fqn,
549
+ parts: table.parts,
550
+ });
551
+ }
552
+ }
553
+ catch (err) {
554
+ return `Could not list the entries of entry group '${group.base}' to ` +
555
+ `complete bare table names: ${err?.message ?? err}`;
556
+ }
557
+ doc.entryTables = tables;
558
+ return undefined;
559
+ }
560
+ /**
561
+ * Completes the bare table name `source` of `ds` with the one entry in
562
+ * `doc.entryTables` whose fullyQualifiedName ends in its parts, an unquoted
563
+ * part in any letter case and a quoted part in its own case. Leaves a name no
564
+ * entry or several entries end in as written, and reports it with the
565
+ * candidates.
566
+ */
567
+ function completeFromEntryGroup(ds, source, parts, doc) {
568
+ const dsLabel = `dataset '${ds.name}'`;
569
+ const group = doc.entryGroup.base;
570
+ // Compared from the end, so a name with more parts than an entry's matches
571
+ // none.
572
+ const matches = doc.entryTables.filter(t => parts.every((p, i) => {
573
+ const e = t.parts.at(i - parts.length);
574
+ return p.quoted ? p.text === e : p.text.toLowerCase() === e?.toLowerCase();
575
+ }));
576
+ if (matches.length === 1) {
577
+ // The author asked for this rewrite with --source-prefix, so it is a
578
+ // warning in strict mode too.
579
+ const [m] = matches;
580
+ doc.warnings.push(`${dsLabel}: rewrote bare source '${source}' to '${m.name}', the ` +
581
+ `entry whose fullyQualifiedName is '${m.fullyQualifiedName}'.`);
582
+ ds.source = m.name;
583
+ doc.modified = true;
584
+ return;
585
+ }
586
+ const left = `${dsLabel}: left bare source '${source}' as written, since `;
587
+ if (matches.length) {
588
+ const names = matches.map(m => `'${m.name}' (${m.fullyQualifiedName})`).join(', ');
589
+ doc.warnOrError(`${left}several entries in entry group '${group}' have a ` +
590
+ `fullyQualifiedName that ends in it: ${names}. 'kcmd push' rejects ` +
591
+ `the file until the source names one of them.`);
592
+ }
593
+ else {
594
+ doc.warnOrError(`${left}no entry for a table or view in entry group '${group}' has ` +
595
+ `a fullyQualifiedName that ends in it. 'kcmd push' rejects the file ` +
596
+ `until the source is the table's GCP resource name. ` +
597
+ loader_1.THIRD_PARTY_SOURCE);
598
+ }
599
+ }
481
600
  /**
482
601
  * When the output's flavor rejects `obj.datatype`, writes it in its canonical
483
602
  * casing, such as `Integer` for `integer`, or deletes an `Opaque` in another
@@ -618,19 +737,22 @@ function repairField(f, dsName, doc) {
618
737
  }
619
738
  /**
620
739
  * Repairs a dataset's `source` (removing query-valued sources, qualifying bare
621
- * table names against `doc.sourcePrefix`, and warning on unaccepted sources),
622
- * truncates its `description`, and repairs each of its `fields`.
740
+ * table names against `doc.sourcePrefix` or completing them from
741
+ * `doc.entryTables`, and warning on unaccepted sources), truncates its
742
+ * `description`, and repairs each of its `fields`.
623
743
  */
624
744
  function repairDataset(ds, doc) {
625
745
  // 1. Repair or diagnose `ds.source`: remove query-valued sources (in the
626
- // Google version), qualify bare table names against `doc.sourcePrefix`, or
627
- // warn on unaccepted source formats.
746
+ // Google version), qualify bare table names against `doc.sourcePrefix` or
747
+ // complete them from `doc.entryTables`, or warn on unaccepted source
748
+ // formats.
628
749
  const dsLabel = `dataset '${ds.name}'`;
629
750
  if (typeof ds.source === 'string') {
630
751
  const trimmed = ds.source.trim();
631
752
  const qualified = doc.sourcePrefix ?
632
753
  qualifyTableSource(trimmed, doc.sourcePrefix) :
633
754
  undefined;
755
+ const parts = doc.entryTables ? sourceParts(trimmed) : undefined;
634
756
  if ((0, loader_1.hasUnquotedWhitespace)(trimmed)) {
635
757
  if (doc.targetVersion === loader_1.OSSIE_VERSION) {
636
758
  doc.errors.push(`${dsLabel}: query-valued source '${trimmed}' is not supported, ` +
@@ -651,8 +773,10 @@ function repairDataset(ds, doc) {
651
773
  ds.source = qualified;
652
774
  doc.modified = true;
653
775
  }
654
- else if (!((0, loader_1.oldSourceForm)(trimmed) === undefined &&
655
- (0, loader_1.isAcceptedSource)(trimmed))) {
776
+ else if (parts) {
777
+ completeFromEntryGroup(ds, trimmed, parts, doc);
778
+ }
779
+ else if (sourceError(trimmed, dsLabel) !== undefined) {
656
780
  // Why a prefix that nearly fits the name cannot complete it.
657
781
  const p = doc.sourcePrefix;
658
782
  const caseOnly = !!p && qualifyTableSource(trimmed, p, true) !== undefined;
@@ -719,12 +843,51 @@ function repairMetric(mt, entityNames, impliedDialect, doc) {
719
843
  truncateDescription(mt, mtLabel, doc);
720
844
  return true;
721
845
  }
846
+ /**
847
+ * Reads, with `opts.readDialects`, the dialect of each Cloud SQL, Spanner and
848
+ * third-party table the repaired model and its profile files name, as push
849
+ * reads them. A read that fails is one warning per table, or per database for
850
+ * Spanner, and the dialect of the tables it reads stays unknown. A lookup that
851
+ * cannot start is one warning for the run, and every such table's dialect
852
+ * stays unknown. An entry that was read and gives no dialect is reported as
853
+ * push's other refusals are.
854
+ */
855
+ async function readSourceDialects(doc, opts) {
856
+ // The repaired sources are in `doc.root`, and JSON is YAML.
857
+ const texts = [
858
+ JSON.stringify(doc.root),
859
+ ...(opts.profileFiles ?? []).map(f => f.content),
860
+ ];
861
+ if (!opts.readDialects || !(0, source_dialect_1.sourcesToLookUp)(texts).length)
862
+ return {};
863
+ let got;
864
+ try {
865
+ got = await opts.readDialects(texts);
866
+ }
867
+ catch (err) {
868
+ // The lookup could not start, for example with no credentials. gcloud
869
+ // prints the details, so the warning keeps the first line.
870
+ const why = String(err?.message ?? err).split('\n')[0];
871
+ doc.warnings.push(`${(0, parameters_1.sentence)(`kcmd could not look up table dialects: ${why}`)} Import ` +
872
+ "skipped the checks that need a table's dialect.");
873
+ return {};
874
+ }
875
+ const skipped = 'Import skipped the checks that need its dialect.';
876
+ for (const e of got.errors) {
877
+ if (got.unread.includes(e))
878
+ doc.warnings.push(`${(0, parameters_1.sentence)(e)} ${skipped}`);
879
+ else
880
+ doc.warnOrError(e);
881
+ }
882
+ return { sourceDialects: got.dialects, sourceDatabases: got.databases };
883
+ }
722
884
  /**
723
885
  * Phase 3 (Repair): Repairs third-party constructs in-place on `doc` (pruning
724
886
  * extra models, repairing datasets and their fields, pruning unanchored
725
- * metrics, and truncating descriptions > 2,000 chars).
887
+ * metrics, and truncating descriptions > 2,000 chars). Returns the source
888
+ * dialects it reads after repairing the datasets.
726
889
  */
727
- function repairDocument(doc) {
890
+ async function repairDocument(doc, opts) {
728
891
  // 1. Prune extra models after `semantic_model[0]` because `kcmd` allows only
729
892
  // a single model per document, and cap the model's `description` at 2,000
730
893
  // characters (`MAX_ENTRY_DESCRIPTION`).
@@ -742,16 +905,24 @@ function repairDocument(doc) {
742
905
  for (const ds of doc.datasets) {
743
906
  repairDataset(ds, doc);
744
907
  }
745
- // 3. Infer the model's source dialect from its repaired datasets via
746
- // `sourcesDialect`, and repair or prune each metric (`repairMetric`).
908
+ // 3. Read the source dialects, infer the model's source dialect from its
909
+ // repaired datasets via `sourcesDialect`, and repair or prune each metric
910
+ // (`repairMetric`). An ANSI_SQL text is not read when no lookup gave its
911
+ // tables' dialect, as push's checks read none.
912
+ const load = await readSourceDialects(doc, opts);
747
913
  const entityNames = doc.datasets.map(ds => ds.name)
748
914
  .filter((n) => typeof n === 'string');
749
915
  const impliedDialect = (0, resolve_profiles_1.sourcesDialect)({
750
- entities: doc.datasets.map(ds => ({
751
- abstract: Boolean(ds.abstract),
752
- authoredSource: typeof ds.source === 'string' ? ds.source : undefined,
753
- dataSource: '',
754
- })),
916
+ entities: doc.datasets.map(ds => {
917
+ const source = typeof ds.source === 'string' ? ds.source : undefined;
918
+ const key = source && (0, loader_1.dialectLookupKey)(source.trim());
919
+ return {
920
+ abstract: Boolean(ds.abstract),
921
+ authoredSource: source,
922
+ dataSource: '',
923
+ sourceDialect: key ? load.sourceDialects?.get(key) : undefined,
924
+ };
925
+ }),
755
926
  });
756
927
  doc.metrics = doc.metrics.filter(mt => repairMetric(mt, entityNames, impliedDialect, doc));
757
928
  if (Array.isArray(doc.model.metrics) && doc.model.metrics.length) {
@@ -766,6 +937,7 @@ function repairDocument(doc) {
766
937
  truncateDescription(b, rLabel, doc);
767
938
  }
768
939
  }
940
+ return load;
769
941
  }
770
942
  /**
771
943
  * Sets `version` to `doc.targetVersion`, and names the model's entity list as
@@ -985,9 +1157,13 @@ function moveConstructs(doc, profileFiles) {
985
1157
  /**
986
1158
  * Loads `doc.root` into the `SemanticModel` IR, runs `transpileModel` to
987
1159
  * translate vendor or double-quoted `ANSI_SQL` expressions to `BIGQUERY`, and
988
- * writes any newly added `BIGQUERY` dialect entries back onto `doc`.
1160
+ * writes any newly added `BIGQUERY` dialect entries back onto `doc`, including
1161
+ * the field bindings of each BigQuery profile (`isBigQueryProfile`) in the
1162
+ * model's `GOOGLE` blocks. Translates the BigQuery profiles among
1163
+ * `profileFiles` the same way and returns every file. A warning about a JSON
1164
+ * profile file names the matching file of `inputFiles`.
989
1165
  */
990
- async function transpileDocument(doc) {
1166
+ async function transpileDocument(doc, profileFiles, inputFiles = profileFiles) {
991
1167
  let preLoaded;
992
1168
  try {
993
1169
  preLoaded = (0, loader_1.fromDocument)(doc.root, { bindingOptional: true, warnUnacceptedSource: true });
@@ -995,24 +1171,37 @@ async function transpileDocument(doc) {
995
1171
  catch (err) {
996
1172
  if (!(err instanceof loader_1.SemanticModelError))
997
1173
  throw err;
998
- return;
1174
+ return profileFiles;
999
1175
  }
1000
- // Writes `irDialects` back onto `rawNode.expression` when `transpileModel`
1001
- // appended a new dialect entry (e.g. `BIGQUERY`).
1002
- const syncNode = (rawNode, irDialects) => {
1003
- const prevCount = typeof rawNode.expression === 'string' ?
1004
- 1 :
1005
- asRecords(rawNode.expression?.dialects).length;
1006
- if (prevCount > 0 && irDialects && irDialects.length > prevCount) {
1007
- rawNode.expression = {
1008
- dialects: irDialects.map(d => ({ dialect: d.dialect, expression: d.expression })),
1009
- };
1176
+ const syncNode = (...args) => {
1177
+ if (syncDialects(...args))
1010
1178
  doc.modified = true;
1011
- }
1012
1179
  };
1180
+ // Each of `profileFiles` as loaded, or undefined for one that does not
1181
+ // load. That one is written as it is when it parses, since push reports it.
1182
+ const loaded = profileFiles.map(pf => {
1183
+ try {
1184
+ return (0, resolve_profiles_1.loadProfileFile)(pf.content, parseProfileName(pf.fileName), { skipActions: true });
1185
+ }
1186
+ catch (err) {
1187
+ if (!(err instanceof loader_1.SemanticModelError))
1188
+ throw err;
1189
+ return undefined;
1190
+ }
1191
+ });
1192
+ let translatedFiles = profileFiles;
1013
1193
  for (const m of preLoaded.models) {
1014
- const transpiled = await (0, transpile_1.transpileModel)(m);
1194
+ const transpiled = await (0, transpile_1.transpileModel)({
1195
+ ...m,
1196
+ profiles: [...(m.profiles ?? []), ...loaded.filter(p => p !== undefined)],
1197
+ }, { translatesProfile: isBigQueryProfile });
1015
1198
  doc.warnings.push(...transpiled.warnings);
1199
+ const profile = (name) => transpiled.model.profiles?.find(p => p.name === name);
1200
+ translatedFiles = profileFiles.map((pf, i) => {
1201
+ const p = loaded[i] && profile(loaded[i].name);
1202
+ const named = { ...pf, fileName: inputFiles[i].fileName };
1203
+ return p ? { ...pf, content: withDialects(named, p, doc) } : pf;
1204
+ });
1016
1205
  for (const ds of doc.datasets) {
1017
1206
  const irEntity = transpiled.model.entities.find(e => e.name === ds.name);
1018
1207
  for (const f of asRecords(ds.fields)) {
@@ -1022,57 +1211,143 @@ async function transpileDocument(doc) {
1022
1211
  for (const mt of doc.metrics) {
1023
1212
  syncNode(mt, transpiled.model.metrics.find(x => x.name === mt.name)?.dialects);
1024
1213
  }
1214
+ // A profile in a GOOGLE block is JSON text, so a block whose profiles
1215
+ // gained an entry is written back as JSON.
1216
+ for (const block of asRecords(doc.model.custom_extensions)) {
1217
+ const data = parseGoogleBlocks([block])[0];
1218
+ let changed = false;
1219
+ for (const p of asRecords(data?.profiles)) {
1220
+ changed = syncProfile(p, profile(p.name)) || changed;
1221
+ }
1222
+ if (changed) {
1223
+ block.data = JSON.stringify(data);
1224
+ doc.modified = true;
1225
+ }
1226
+ }
1227
+ }
1228
+ return translatedFiles;
1229
+ }
1230
+ // Whether `profile` is a BigQuery profile of `model`, by the rule push's import
1231
+ // hint uses: each source push accepts that it states for a concrete entity of
1232
+ // `model` is a BigQuery or BigLake table, and it states at least one.
1233
+ function isBigQueryProfile(profile, model) {
1234
+ const concrete = new Set(model.entities.filter(e => !e.abstract).map(e => e.name));
1235
+ const entities = profile.entities.flatMap(({ name, source }) => concrete.has(name) && source !== undefined &&
1236
+ sourceError(source, '') === undefined ?
1237
+ [{ authoredSource: source, dataSource: source }] :
1238
+ []);
1239
+ return (0, resolve_profiles_1.sourcesDialect)({ entities }) === 'BIGQUERY';
1240
+ }
1241
+ /**
1242
+ * Adds to `rawNode.expression` each dialect entry, such as `BIGQUERY`, that
1243
+ * `transpileModel` appended to `irDialects`, and returns whether it added any.
1244
+ * The entries already in a dialects list stay as written.
1245
+ */
1246
+ function syncDialects(rawNode, irDialects) {
1247
+ const dialects = rawNode.expression?.dialects;
1248
+ const prevCount = typeof rawNode.expression === 'string' ?
1249
+ 1 :
1250
+ asRecords(dialects).length;
1251
+ if (prevCount === 0 || !irDialects || irDialects.length <= prevCount) {
1252
+ return false;
1253
+ }
1254
+ const entries = irDialects.map(d => ({ dialect: d.dialect, expression: d.expression }));
1255
+ if (typeof rawNode.expression === 'string') {
1256
+ rawNode.expression = { dialects: entries };
1257
+ }
1258
+ else {
1259
+ dialects.push(...entries.slice(prevCount));
1260
+ }
1261
+ return true;
1262
+ }
1263
+ /**
1264
+ * Writes onto the raw profile `p` each field expression that gained a dialect
1265
+ * entry in the translated `irProfile`, and returns whether any did.
1266
+ */
1267
+ function syncProfile(p, irProfile) {
1268
+ let changed = false;
1269
+ for (const pe of asRecords(p.entities)) {
1270
+ const irEntity = irProfile?.entities.find(x => x.name === pe.name);
1271
+ for (const f of asRecords(pe.fields)) {
1272
+ const irField = irEntity?.fields?.find(x => x.name === f.name);
1273
+ changed = syncDialects(f, irField?.dialects) || changed;
1274
+ }
1025
1275
  }
1276
+ return changed;
1277
+ }
1278
+ /**
1279
+ * Returns the content of the profile file `file` with each field expression
1280
+ * that gained a dialect entry in the translated `profile`, written as the
1281
+ * model file is. A JSON file that changes is written as YAML, and a warning
1282
+ * on `doc` names it.
1283
+ */
1284
+ function withDialects({ fileName, content }, profile, doc) {
1285
+ const { document, text } = (0, yaml_update_1.parseForUpdate)(content);
1286
+ const root = (0, resolve_profiles_1.plainCopy)(document.toJS());
1287
+ const links = (0, yaml_update_1.linkNodes)(root, document.contents);
1288
+ if (!syncProfile(root, profile))
1289
+ return content;
1290
+ if (text !== content)
1291
+ doc.warnings.push(`Converted '${fileName}' to YAML.`);
1292
+ return writeDocument({ document, links, text }, root);
1026
1293
  }
1027
1294
  /**
1028
- * Validates every deployment target on `model` using `googleDeploymentTargets`
1029
- * and `targetProblem` so invalid targets fail import with `kcmd push`'s exact
1030
- * message.
1295
+ * Validates every deployment target on `model` using `targetProblem` so
1296
+ * invalid targets fail import with `kcmd push`'s exact message.
1031
1297
  */
1032
1298
  function validateDeploymentTargets(model, doc) {
1033
- for (const t of (0, deployment_target_1.googleDeploymentTargets)(model).uris) {
1034
- if ((0, deployment_target_1.targetProblem)('', t)) {
1035
- const d = model.deployments?.find(x => x.target === t);
1036
- doc.errors.push(d ? (0, deployment_target_1.deploymentTargetError)(model, d) :
1037
- (0, deployment_target_1.googleBlockTargetError)(model, t));
1299
+ for (const d of model.deployments ?? []) {
1300
+ if ((0, deployment_target_1.targetProblem)('', d.target)) {
1301
+ doc.errors.push((0, deployment_target_1.deploymentTargetError)(model, d));
1038
1302
  }
1039
1303
  }
1040
1304
  }
1305
+ /**
1306
+ * Records push's error for the profile files `profileFiles` beside a vanilla
1307
+ * input in `modelFile`, which keeps its profiles in its `GOOGLE` block. Push
1308
+ * checks the name of each file first. The message is an error in strict mode
1309
+ * and a warning otherwise.
1310
+ */
1311
+ function checkVanillaProfileFiles(doc, profileFiles, modelFile) {
1312
+ if (doc.inputVersion !== loader_1.OSSIE_VERSION || profileFiles.length === 0)
1313
+ return;
1314
+ const badName = profileFiles
1315
+ .map(pf => (0, semantic_model_1.profileNameError)(pf.fileName, parseProfileName(pf.fileName)))
1316
+ .find(e => e !== undefined);
1317
+ doc.warnOrError(badName ?? (0, semantic_model_1.vanillaSiblingError)(profileFiles[0].fileName, modelFile));
1318
+ }
1041
1319
  /**
1042
1320
  * Loads each sibling `<model>.profile.<name>.yaml` entry in `profileFiles` via
1043
1321
  * `loadProfileFile` and attaches the resulting `ProfileSpec` list to
1044
- * `model.profiles`, recording any reserved or duplicate profile name or
1045
- * profile load error in `doc.errors`.
1322
+ * `model.profiles`. Records in `doc.errors` push's error for the first file
1323
+ * that breaks a file rule beside `modelFile`, or each profile's load error
1324
+ * labeled with its file.
1046
1325
  */
1047
- function attachProfileFiles(model, profileFiles, doc) {
1048
- if (!profileFiles?.length)
1326
+ function attachProfileFiles(model, profileFiles, doc, modelFile) {
1327
+ const files = (profileFiles ?? []).map(pf => ({ ...pf, name: parseProfileName(pf.fileName) }));
1328
+ // Push checks every file's name before it loads any profile, and stops at
1329
+ // the first file that breaks a file rule.
1330
+ const seen = new Map();
1331
+ try {
1332
+ for (const pf of files) {
1333
+ (0, semantic_model_1.checkProfileFileName)(modelFile, pf.fileName, pf.name, pf.content, seen);
1334
+ }
1335
+ }
1336
+ catch (err) {
1337
+ if (!(err instanceof semantic_model_1.ProfileFileError))
1338
+ throw err;
1339
+ doc.errors.push(err.message);
1049
1340
  return;
1341
+ }
1050
1342
  const profiles = [];
1051
- const seen = new Set();
1052
- for (const pf of profileFiles) {
1053
- const profileName = parseProfileName(pf.fileName);
1054
- if ((0, names_1.nameProblem)('profile', profileName))
1055
- continue;
1056
- const lower = profileName.toLowerCase();
1057
- if (lower === resolve_profiles_1.DEFAULT_PROFILE) {
1058
- doc.errors.push(`model '${model.name}': a profile may not be named ` +
1059
- `'${profileName}'; '${resolve_profiles_1.DEFAULT_PROFILE}' names the model's inline ` +
1060
- `bindings.`);
1061
- continue;
1062
- }
1063
- if (seen.has(lower)) {
1064
- doc.errors.push(`model '${model.name}': duplicate profile name '${profileName}'; ` +
1065
- `profile names must be unique ignoring case.`);
1066
- continue;
1067
- }
1068
- seen.add(lower);
1343
+ for (const pf of files) {
1069
1344
  try {
1070
- profiles.push((0, resolve_profiles_1.loadProfileFile)(pf.content, profileName));
1345
+ profiles.push((0, resolve_profiles_1.loadProfileFile)(pf.content, pf.name));
1071
1346
  }
1072
1347
  catch (err) {
1073
1348
  if (!(err instanceof loader_1.SemanticModelError))
1074
1349
  throw err;
1075
- doc.errors.push(err.message);
1350
+ doc.errors.push(`[${pf.fileName}] ${err.message}`);
1076
1351
  }
1077
1352
  }
1078
1353
  if (profiles.length > 0) {
@@ -1152,7 +1427,7 @@ async function generateConvertedFiles(doc, opts) {
1152
1427
  if (!model)
1153
1428
  return [];
1154
1429
  if (doc.targetVersion === loader_1.OSSIE_VERSION) {
1155
- attachProfileFiles(model, opts.profileFiles, doc);
1430
+ attachProfileFiles(model, opts.profileFiles, doc, opts.fileName ?? `${model.name}.yaml`);
1156
1431
  validateOssieConversion(model, doc);
1157
1432
  if (doc.errors.length > 0)
1158
1433
  return [];
@@ -1163,13 +1438,22 @@ async function generateConvertedFiles(doc, opts) {
1163
1438
  * Phase 4 (Adapt): Adapts `doc` in place to its target version
1164
1439
  * (`moveConstructs`, then `adaptToGoogleVersion` or `adaptToOssieVersion`),
1165
1440
  * optionally transpiles expressions via `transpileDocument`, and returns the
1166
- * output files. Every value these steps do not change keeps its text.
1441
+ * output files: the model file, then the profile files. An import that keeps
1442
+ * the flavor writes `profileFiles` under the model's name. Every value these
1443
+ * steps do not change keeps its text.
1167
1444
  */
1168
1445
  async function generateAdaptedFiles(doc, transpile, profileFiles = []) {
1169
1446
  // 1. Adapt `doc` in place to the target output version (`0.2.0.dev0/google`
1170
- // or `0.2.0.dev0`).
1171
- const profiles = doc.inputVersion === doc.targetVersion ?
1172
- [] :
1447
+ // or `0.2.0.dev0`). Each of `profileFiles` goes under the model's name. A
1448
+ // nameless `<file>.profile.yaml` stays nameless, so push reports it.
1449
+ let profiles = doc.inputVersion === doc.targetVersion ?
1450
+ profileFiles.map(pf => ({
1451
+ fileName: /\.profile\.ya?ml$/i.test(pf.fileName) &&
1452
+ parseProfileName(pf.fileName) === '' ?
1453
+ `${doc.model.name}.profile.yaml` :
1454
+ `${doc.model.name}.profile.${parseProfileName(pf.fileName)}.yaml`,
1455
+ content: pf.content,
1456
+ })) :
1173
1457
  moveConstructs(doc, profileFiles);
1174
1458
  if (doc.inputVersion === loader_1.OSSIE_VERSION &&
1175
1459
  doc.targetVersion === loader_1.GOOGLE_VERSION) {
@@ -1179,9 +1463,10 @@ async function generateAdaptedFiles(doc, transpile, profileFiles = []) {
1179
1463
  adaptToOssieVersion(doc);
1180
1464
  }
1181
1465
  // 2. When `transpile` is enabled and no prior errors occurred, translate
1182
- // vendor or double-quoted `ANSI_SQL` expressions to `BIGQUERY`.
1466
+ // vendor or double-quoted `ANSI_SQL` expressions to `BIGQUERY`, in the model
1467
+ // and in each BigQuery profile, in its GOOGLE blocks or among `profiles`.
1183
1468
  if (transpile && doc.errors.length === 0) {
1184
- await transpileDocument(doc);
1469
+ profiles = await transpileDocument(doc, profiles, doc.inputVersion === doc.targetVersion ? profileFiles : profiles);
1185
1470
  }
1186
1471
  // 3. Write `doc.root` into the input's text when modified or preserve the
1187
1472
  // text verbatim when unmodified.
@@ -1211,17 +1496,29 @@ function writeDocument(doc, root) {
1211
1496
  return (0, yaml_update_1.updateText)(doc.text, doc.document, root, doc.links);
1212
1497
  }
1213
1498
  /**
1214
- * Phase 5 (Post-validation): Validates the serialized output YAML with
1215
- * `loadModels` (`warnUnacceptedSource: !doc.strict`) and checks deployment
1216
- * targets.
1499
+ * Phase 5 (Post-validation): Validates the output model file with `loadModels`
1500
+ * (`warnUnacceptedSource: !doc.strict`), checks deployment targets, and runs
1501
+ * push's local checks (`localChecks`) on the model file and each profile it
1502
+ * declares, with the source dialects in `load`. With KC_V2_ASPECTS on, it also
1503
+ * checks that Knowledge Catalog can store each aspect. It also gives push's
1504
+ * warnings from the Knowledge Catalog entry builder, the pruning and each
1505
+ * graph, and leaves out push's notes.
1217
1506
  */
1218
- function validateOutputDocument(content, doc) {
1507
+ async function validateOutputDocument(files, doc, opts, load) {
1508
+ const [{ fileName, content }, ...profileFiles] = files;
1219
1509
  try {
1220
1510
  // 1. Load the serialized output YAML with `loadModels`
1221
1511
  // (`warnUnacceptedSource: !doc.strict`) to run full schema and semantic
1222
1512
  // validation while turning unaccepted table sources into warnings in
1223
- // non-strict mode, deduplicating warnings already recorded.
1224
- const loaded = (0, loader_1.loadModels)(content, { bindingOptional: true, warnUnacceptedSource: !doc.strict });
1513
+ // non-strict mode, deduplicating warnings already recorded. Step 2 gives
1514
+ // push's error for a field its table cannot run, so the load does not
1515
+ // warn about it too.
1516
+ const loaded = (0, loader_1.loadModels)(content, {
1517
+ bindingOptional: true,
1518
+ warnUnacceptedSource: !doc.strict,
1519
+ fieldTextChecked: true,
1520
+ sourceDialects: load.sourceDialects,
1521
+ });
1225
1522
  for (const w of loaded.warnings) {
1226
1523
  // The repair step has reported each source the loader warns about.
1227
1524
  if (LOADER_SOURCE_WARNING.test(w))
@@ -1230,10 +1527,105 @@ function validateOutputDocument(content, doc) {
1230
1527
  doc.warnings.push(w);
1231
1528
  }
1232
1529
  }
1233
- // 2. Validate every deployment target on the loaded model using
1234
- // `validateDeploymentTargets`.
1235
- for (const m of loaded.models) {
1236
- validateDeploymentTargets(m, doc);
1530
+ // 2. Run push's local checks on the model file and each profile it
1531
+ // declares. What push rejects is an error in strict mode and a warning
1532
+ // otherwise.
1533
+ const input = doc.strict ? opts.fileName : undefined;
1534
+ const model = {
1535
+ name: (input ?? fileName).replace(/\.ya?ml$/, ''),
1536
+ text: content,
1537
+ };
1538
+ // Where the input holds profile `name`, which the output writes in
1539
+ // `written`: its profile file, or the model file's GOOGLE block. Strict
1540
+ // mode writes nothing on an error, so its messages name the input.
1541
+ const inputFiles = doc.inputVersion === loader_1.GOOGLE_VERSION ? opts.profileFiles ?? [] : [];
1542
+ const where = (name, written) => !input ? written :
1543
+ inputFiles.find(pf => parseProfileName(pf.fileName) === name)
1544
+ ?.fileName ??
1545
+ model.name;
1546
+ // Push stops at a profile file that breaks a file rule, then at a
1547
+ // deployment target in no accepted form. validateDeploymentTargets reports
1548
+ // such a target as an error in either mode.
1549
+ let read;
1550
+ try {
1551
+ const seen = new Map();
1552
+ read = profileFiles.map(f => {
1553
+ const name = parseProfileName(f.fileName);
1554
+ const label = where(name, f.fileName);
1555
+ (0, semantic_model_1.checkProfileFileName)(input ?? fileName, label, name, f.content, seen);
1556
+ return { name, text: f.content, fileName: label };
1557
+ });
1558
+ }
1559
+ catch (err) {
1560
+ if (!(err instanceof semantic_model_1.ProfileFileError))
1561
+ throw err;
1562
+ doc.warnOrError(err.message);
1563
+ }
1564
+ if (!doc.errors.length) {
1565
+ for (const m of loaded.models)
1566
+ validateDeploymentTargets(m, doc);
1567
+ }
1568
+ const profiles = read;
1569
+ if (!profiles)
1570
+ return;
1571
+ const declaredBy = () => (0, resolve_profiles_1.declaredProfiles)(model, profiles).map(p => 'spec' in p ? { ...p, label: where(p.name, model.name) } : p);
1572
+ // Push stops at a source its loader rejects. Import still reports each
1573
+ // profile's problems, its sources among them.
1574
+ if (loaded.warnings.some(w => LOADER_SOURCE_WARNING.test(w))) {
1575
+ for (const { error } of (0, resolve_profiles_1.namedProfileErrors)([model], new Map([[model.name, loaded.models[0]]]), () => profiles, declaredBy, doc.warnings, load)) {
1576
+ doc.warnOrError(error);
1577
+ }
1578
+ return;
1579
+ }
1580
+ if (doc.errors.length)
1581
+ return;
1582
+ const base = { document: model.name, model: loaded.models[0] };
1583
+ const own = loaded.warnings.map(w => `[${model.name}] ${w}`);
1584
+ // Push prints each warning once, after the load's. Each is a warning in
1585
+ // either mode.
1586
+ const warn = (w) => {
1587
+ if (!own.includes(w) && !doc.warnings.includes(w))
1588
+ doc.warnings.push(w);
1589
+ };
1590
+ const withFiles = (0, validate_1.withProfileFiles)(base, () => profiles);
1591
+ const checked = 'errors' in withFiles ? withFiles : await (0, validate_1.localChecks)({
1592
+ docs: [model],
1593
+ loaded: { models: [base], warnings: own },
1594
+ models: [withFiles],
1595
+ profileDocs: () => profiles,
1596
+ declaredBy,
1597
+ load,
1598
+ }, warn);
1599
+ // Push prints each error once.
1600
+ const errors = 'errors' in checked ? checked.errors : [];
1601
+ for (const error of new Set(errors.map(e => e.error))) {
1602
+ doc.warnOrError(error);
1603
+ }
1604
+ if ('errors' in checked || 'errors' in withFiles)
1605
+ return;
1606
+ // Push next builds the Knowledge Catalog entries and prints the builder's
1607
+ // warnings. With KC_V2_ASPECTS on, it also checks that Knowledge Catalog
1608
+ // can store each aspect. The entry group does not change an aspect's size.
1609
+ const v2Aspects = (0, validate_1.kcV2Aspects)();
1610
+ const catalog = (0, deploy_knowledge_catalog_1.checkCatalogAspects)([withFiles], {
1611
+ project: 'p', location: 'l', entryGroup: 'g',
1612
+ systemTypeProject: process.env.KC_TYPE_PROJECT, v2Aspects,
1613
+ });
1614
+ for (const w of catalog.warnings)
1615
+ warn(w);
1616
+ if (v2Aspects && catalog.error) {
1617
+ doc.warnOrError(catalog.error);
1618
+ return;
1619
+ }
1620
+ // Push then warns about what pruning leaves out of each deployment, and
1621
+ // about what each graph leaves out. Import leaves out push's notes.
1622
+ for (const line of checked.plans.flatMap(validate_1.pruningLines)) {
1623
+ if (line.startsWith('Warning: '))
1624
+ warn(line.slice('Warning: '.length));
1625
+ }
1626
+ for (const { generated } of checked.plans) {
1627
+ for (const w of generated?.warnings ?? [])
1628
+ warn(w);
1237
1629
  }
1238
1630
  }
1239
1631
  catch (err) {
@@ -1248,23 +1640,51 @@ function validateOutputDocument(content, doc) {
1248
1640
  * errors.
1249
1641
  */
1250
1642
  async function importSemanticModelYaml(rawYaml, opts = {}) {
1251
- // 1. Initialize the SQL parser and parse/pre-validate the input document.
1643
+ // 1. Initialize the SQL parser and parse/pre-validate the input document,
1644
+ // and report any profile file beside a vanilla input as push does.
1252
1645
  await (0, sql_parser_1.loadSqlEngine)();
1253
1646
  const doc = parseImportDocument(rawYaml, opts);
1254
1647
  if (typeof doc === 'string') {
1255
1648
  return { ok: false, files: [], warnings: [], errors: [doc] };
1256
1649
  }
1257
- validateInputDocument(doc, opts.profileFiles);
1258
- // 2. Run third-party repairs (which record errors in `strict` mode).
1259
- repairDocument(doc);
1260
- // 3. Generate the converted or adapted output file(s).
1650
+ validateInputDocument(doc);
1651
+ checkVanillaProfileFiles(doc, opts.profileFiles ?? [], opts.fileName ?? `${doc.model.name}.yaml`);
1652
+ // 2. Read the `--source-prefix` entry group, if needed, and run third-party
1653
+ // repairs (which record errors in `strict` mode) and read the source
1654
+ // dialects.
1655
+ const listError = await listEntryTables(doc);
1656
+ if (listError) {
1657
+ return {
1658
+ ok: false,
1659
+ files: [],
1660
+ warnings: doc.warnings,
1661
+ errors: [...doc.errors, listError],
1662
+ };
1663
+ }
1664
+ const load = await repairDocument(doc, opts);
1665
+ // 3. Generate the converted or adapted output file(s). A conversion to
1666
+ // `0.2.0.dev0` writes the sibling profile files inside the model file, and
1667
+ // a Google-flavor import of a Google-flavor model writes them beside it.
1261
1668
  const shouldConvert = doc.inputVersion !== doc.targetVersion && doc.strict;
1669
+ const googleToGoogle = doc.inputVersion === loader_1.GOOGLE_VERSION &&
1670
+ doc.targetVersion === loader_1.GOOGLE_VERSION;
1262
1671
  const files = shouldConvert ?
1263
1672
  await generateConvertedFiles(doc, opts) :
1264
- await generateAdaptedFiles(doc, !!opts.transpile);
1265
- // 4. Validate the primary output document and return the result.
1673
+ await generateAdaptedFiles(doc, !!opts.transpile, googleToGoogle ? opts.profileFiles ?? [] : []);
1674
+ // 4. Check that each profile file the import writes parses, validate the
1675
+ // output files, and return the result.
1676
+ for (const [i, f] of files.slice(1).entries()) {
1677
+ const [problem] = yaml.parseDocument(f.content, yaml_options_1.YAML_OPTIONS).errors;
1678
+ if (!problem)
1679
+ continue;
1680
+ // A file import passes through unchanged is the input's own file.
1681
+ const input = opts.profileFiles?.[i];
1682
+ doc.errors.push(input?.content === f.content ?
1683
+ (0, semantic_model_1.profileParseError)(input.fileName, problem.message) :
1684
+ `the import writes '${f.fileName}', which does not parse: ${problem.message}`);
1685
+ }
1266
1686
  if (files.length > 0 && doc.errors.length === 0) {
1267
- validateOutputDocument(files[0].content, doc);
1687
+ await validateOutputDocument(files, doc, opts, load);
1268
1688
  }
1269
1689
  return {
1270
1690
  ok: doc.errors.length === 0,