@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.
Files changed (60) hide show
  1. package/README.md +18 -20
  2. package/build/ts/kcmd/cli.js +5842 -5168
  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 +8 -10
  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_knowledge_catalog.d.ts +10 -35
  19. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +92 -121
  20. package/build/ts/kcmd/semantic/graph_ddl.d.ts +10 -0
  21. package/build/ts/kcmd/semantic/graph_ddl.js +152 -59
  22. package/build/ts/kcmd/semantic/import_model.d.ts +26 -4
  23. package/build/ts/kcmd/semantic/import_model.js +526 -103
  24. package/build/ts/kcmd/semantic/kc_converter.d.ts +7 -1
  25. package/build/ts/kcmd/semantic/kc_converter.js +20 -5
  26. package/build/ts/kcmd/semantic/kc_custom_types.d.ts +5 -12
  27. package/build/ts/kcmd/semantic/kc_custom_types.js +16 -93
  28. package/build/ts/kcmd/semantic/kc_ids.d.ts +4 -5
  29. package/build/ts/kcmd/semantic/kc_ids.js +4 -7
  30. package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +0 -3
  31. package/build/ts/kcmd/semantic/knowledge_catalog.js +3 -86
  32. package/build/ts/kcmd/semantic/loader.d.ts +7 -11
  33. package/build/ts/kcmd/semantic/loader.js +284 -148
  34. package/build/ts/kcmd/semantic/osi_converter.js +1 -2
  35. package/build/ts/kcmd/semantic/parameters.d.ts +1 -0
  36. package/build/ts/kcmd/semantic/parameters.js +22 -3
  37. package/build/ts/kcmd/semantic/pull_kc.d.ts +1 -5
  38. package/build/ts/kcmd/semantic/pull_kc.js +38 -112
  39. package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +7 -2
  40. package/build/ts/kcmd/semantic/resolve_inheritance.js +37 -21
  41. package/build/ts/kcmd/semantic/resolve_profiles.d.ts +51 -10
  42. package/build/ts/kcmd/semantic/resolve_profiles.js +213 -32
  43. package/build/ts/kcmd/semantic/runtime/runtime.d.ts +0 -6
  44. package/build/ts/kcmd/semantic/runtime/runtime.js +4 -21
  45. package/build/ts/kcmd/semantic/source_dialect.d.ts +9 -2
  46. package/build/ts/kcmd/semantic/source_dialect.js +83 -33
  47. package/build/ts/kcmd/semantic/source_entries.d.ts +1 -1
  48. package/build/ts/kcmd/semantic/source_entries.js +95 -36
  49. package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +13 -7
  50. package/build/ts/kcmd/semantic/sql_expr_utils.js +240 -67
  51. package/build/ts/kcmd/semantic/sql_parser.d.ts +5 -2
  52. package/build/ts/kcmd/semantic/sql_parser.js +22 -0
  53. package/build/ts/kcmd/semantic/transpile.d.ts +12 -10
  54. package/build/ts/kcmd/semantic/transpile.js +79 -61
  55. package/build/ts/kcmd/semantic/validate.d.ts +37 -2
  56. package/build/ts/kcmd/semantic/validate.js +465 -98
  57. package/build/ts/kcmd/snapshot.d.ts +4 -0
  58. package/build/ts/kcmd/snapshot.js +7 -0
  59. package/build/ts/kcmd/sync.js +44 -7
  60. package/package.json +1 -1
@@ -50,6 +50,7 @@ exports.undefinedEndpointProblem = undefinedEndpointProblem;
50
50
  exports.isFormatVersion = isFormatVersion;
51
51
  exports.noSqlReason = noSqlReason;
52
52
  exports.listed = listed;
53
+ exports.plural = plural;
53
54
  exports.expressionDocOf = expressionDocOf;
54
55
  exports.noSqlProblemOf = noSqlProblemOf;
55
56
  exports.inheritedNoSqlProblems = inheritedNoSqlProblems;
@@ -85,9 +86,9 @@ exports.sourceToDataSource = sourceToDataSource;
85
86
  exports.thirdPartyEntryOf = thirdPartyEntryOf;
86
87
  exports.bindsThirdPartyTable = bindsThirdPartyTable;
87
88
  exports.dialectLookupKey = dialectLookupKey;
89
+ exports.spannerDatabaseOf = spannerDatabaseOf;
88
90
  exports.isGoogleCloudForm = isGoogleCloudForm;
89
91
  exports.parseSource = parseSource;
90
- exports.unquote = unquote;
91
92
  exports.bareNameSegments = bareNameSegments;
92
93
  exports.bareNameParts = bareNameParts;
93
94
  exports.loadSemanticModels = loadSemanticModels;
@@ -131,9 +132,6 @@ function sourceRequiredMessage(dataset) {
131
132
  return `dataset '${dataset}': a non-abstract dataset requires a ` +
132
133
  `source; set 'source', or mark it 'abstract: true' if it has no table`;
133
134
  }
134
- function noPrimaryKeyWarning(dataset) {
135
- return `dataset '${dataset}': no primary_key; the entity's KEY will be empty (invalid for graph generation)`;
136
- }
137
135
  function ansiFallbackNote(preferred) {
138
136
  return `note: no '${preferred}' dialect for one or more expressions; using the portable ` +
139
137
  `'${FALLBACK_DIALECT}' dialect verbatim ('${preferred}' accepts the ANSI core subset — ` +
@@ -261,7 +259,7 @@ function noSqlReason(text, dialect, entity) {
261
259
  return 'is only a comment';
262
260
  if (entity === undefined)
263
261
  return undefined;
264
- const onlyQualifier = (d) => (0, sql_expr_utils_1.stripQualifier)((0, sql_parser_1.withoutComments)(text, d), entity).trim() === '';
262
+ const onlyQualifier = (d) => (0, sql_expr_utils_1.withoutQualifier)((0, sql_parser_1.withoutComments)(text, d), entity, d).trim() === '';
265
263
  return readers.some(onlyQualifier) ? `is only the qualifier '${entity}.'` :
266
264
  undefined;
267
265
  }
@@ -280,10 +278,15 @@ function noSqlProblem(raw, entity) {
280
278
  }));
281
279
  }
282
280
  // `problems` as one list, "a, b and c", or undefined when there are none.
283
- function listed(problems) {
281
+ // `conjunction` replaces the "and".
282
+ function listed(problems, conjunction = 'and') {
284
283
  if (problems.length < 2)
285
284
  return problems[0];
286
- return `${problems.slice(0, -1).join(', ')} and ${problems[problems.length - 1]}`;
285
+ return `${problems.slice(0, -1).join(', ')} ${conjunction} ${problems[problems.length - 1]}`;
286
+ }
287
+ // Selects the singular or plural form based on `n` (English count agreement).
288
+ function plural(n, one, many) {
289
+ return n === 1 ? one : many;
287
290
  }
288
291
  // Whether reading `doc` for a field of `entity` names `text`, or finds every
289
292
  // problem that reading `text` in `dialect` finds. The string form and an
@@ -417,10 +420,7 @@ const googleAiContextSchema = z.union([
417
420
  z.string(),
418
421
  z.object({
419
422
  ...aiContextMembers,
420
- custom: z.record(z.string(), z.unknown(), {
421
- error: `ai_context 'custom' must be a mapping of custom ` +
422
- `member names to values`,
423
- }).optional(),
423
+ custom: z.record(z.string(), z.unknown()).optional(),
424
424
  })
425
425
  .strict()
426
426
  .superRefine((ai, ctx) => {
@@ -707,10 +707,6 @@ const modelBase = z.object({
707
707
  datasets: z.array(datasetBase).min(1),
708
708
  relationships: z.array(relationshipSchema).optional(),
709
709
  metrics: z.array(metricSchema).optional(),
710
- // A native deployment-target key (GOOGLE_VERSION only, under
711
- // allowLegacyBareSource). Folded into a GOOGLE `custom_extensions` block on
712
- // the IR after validation (see convertModel).
713
- deployment_target: z.string().optional(),
714
710
  // Named deployment targets (GOOGLE_VERSION only).
715
711
  deployments: z.array(deploymentSchema).optional(),
716
712
  // Model-level write operations, also GOOGLE_VERSION only (see actionSchema).
@@ -723,6 +719,14 @@ const modelBase = z.object({
723
719
  // separately. Actions and constraints are extended-profile constructs with no
724
720
  // `custom_extensions` encoding, so they do not take it.
725
721
  const customExtensionsKey = { custom_extensions: z.array(customExtensionSchema).optional() };
722
+ // The schemas of keys a document schema accepts only so that the loader can
723
+ // reject them with its own message. A message about an unknown key does not
724
+ // list them among the keys an object takes.
725
+ const UNLISTED_KEYS = new WeakSet();
726
+ function unlisted(schema) {
727
+ UNLISTED_KEYS.add(schema);
728
+ return schema;
729
+ }
726
730
  // Builds the document schema for one (bindingOptional, extended) combination.
727
731
  // Every object is `.strict()`, so an unknown key is a hard error rather than
728
732
  // silently dropped. Two axes shape it:
@@ -770,10 +774,10 @@ function buildDocumentSchema(bindingOptional, extended) {
770
774
  // dataset and the fix, not a bare "unrecognized key".
771
775
  extends: placement.dataset.extends === 'native' ?
772
776
  z.array(z.string()).optional() :
773
- z.unknown().optional(),
777
+ unlisted(z.unknown().optional()),
774
778
  abstract: placement.dataset.abstract === 'native' ?
775
779
  z.boolean().optional() :
776
- z.unknown().optional(),
780
+ unlisted(z.unknown().optional()),
777
781
  })
778
782
  .strict()
779
783
  .superRefine((ds, ctx) => {
@@ -821,7 +825,7 @@ function buildDocumentSchema(bindingOptional, extended) {
821
825
  ctx.addIssue({
822
826
  code: z.ZodIssueCode.custom,
823
827
  path: ['source'],
824
- message: `dataset '${ds.name}': an abstract dataset has no table; ` +
828
+ message: `entity '${ds.name}': an abstract entity has no table; ` +
825
829
  `remove 'source', or drop 'abstract: true' to bind it to that table`,
826
830
  });
827
831
  }
@@ -855,7 +859,10 @@ function buildDocumentSchema(bindingOptional, extended) {
855
859
  to: z.string(),
856
860
  from_columns: z.array(z.string()).min(1).optional(),
857
861
  to_columns: z.array(z.string()).min(1).optional(),
858
- description: z.string().optional(),
862
+ // Vanilla rejects a relationship's description after the schema.
863
+ description: placement.relationship.description === 'native' ?
864
+ z.string().optional() :
865
+ unlisted(z.unknown().optional()),
859
866
  ai_context: aiContext.optional(),
860
867
  ...customExtensionsKey,
861
868
  })
@@ -880,8 +887,9 @@ function buildDocumentSchema(bindingOptional, extended) {
880
887
  // An authored anchor is a native extension: extended profile only.
881
888
  // Vanilla still declares the key so the refinement below can reject it
882
889
  // with a message that names the fix, not a bare "unrecognized key".
883
- entity: placement.metric.entity === 'native' ? z.string().optional() :
884
- z.unknown().optional(),
890
+ entity: placement.metric.entity === 'native' ?
891
+ z.string().optional() :
892
+ unlisted(z.unknown().optional()),
885
893
  })
886
894
  .strict()
887
895
  .superRefine((mt, ctx) => {
@@ -935,7 +943,7 @@ function buildDocumentSchema(bindingOptional, extended) {
935
943
  // rejectExpressionBody, which is what turns it into a useful error.
936
944
  const constraint = z.object({
937
945
  name: z.string(),
938
- expression: z.string().optional(),
946
+ expression: unlisted(z.unknown().optional()),
939
947
  judgment: z.string().optional(),
940
948
  description: z.string().optional(),
941
949
  // What a violation does; required. See
@@ -950,7 +958,6 @@ function buildDocumentSchema(bindingOptional, extended) {
950
958
  .superRefine(rejectExpressionBody);
951
959
  const modelExtensions = {
952
960
  ...(placement.model.deployments === 'native' ? {
953
- deployment_target: z.string().optional(),
954
961
  deployments: z.array(deploymentSchema).optional(),
955
962
  } :
956
963
  {}),
@@ -1069,7 +1076,7 @@ exports.GOOGLE_VENDOR = 'GOOGLE';
1069
1076
  // to `datasets:` and `deployments:` is left in place here and folded into a
1070
1077
  // GOOGLE `custom_extensions` block on the IR after validation (see
1071
1078
  // convertModel). Operates on the parsed document before validation.
1072
- function normalizeDocumentSugars(doc, extended, allowLegacyBareSource = false) {
1079
+ function normalizeDocumentSugars(doc, extended) {
1073
1080
  if (!doc || typeof doc !== 'object')
1074
1081
  return doc;
1075
1082
  const cloned = structuredClone(doc);
@@ -1078,31 +1085,17 @@ function normalizeDocumentSugars(doc, extended, allowLegacyBareSource = false) {
1078
1085
  return cloned;
1079
1086
  for (const m of models) {
1080
1087
  if (m && typeof m === 'object') {
1081
- normalizeModelSugars(m, extended, allowLegacyBareSource);
1088
+ normalizeModelSugars(m, extended);
1082
1089
  }
1083
1090
  }
1084
1091
  return cloned;
1085
1092
  }
1086
- function normalizeModelSugars(m, extended, allowLegacyBareSource = false) {
1093
+ function normalizeModelSugars(m, extended) {
1087
1094
  const label = typeof m.name === 'string' ? `model '${m.name}'` : 'model';
1088
1095
  const placement = exports.FORMAT_VERSION_PLACEMENT[extended ? exports.GOOGLE_VERSION : exports.OSSIE_VERSION];
1089
- // Only a load that still accepts the legacy key can be left holding both.
1090
- // Elsewhere the `deployment_target:` rejection below speaks to it. The open
1091
- // format has neither key, so a vanilla document is told to switch flavor.
1092
- if (allowLegacyBareSource && m.deployment_target !== undefined &&
1093
- m.deployments !== undefined) {
1094
- throw new SemanticModelError(extended ?
1095
- `Semantic model load error: ${label}: set either 'deployments' ` +
1096
- `or 'deployment_target', not both.` :
1097
- `Semantic model load error: ${label}: neither 'deployments' nor ` +
1098
- `'deployment_target' is in the open format ` +
1099
- `('${exports.OSSIE_VERSION}'); set version: '${exports.GOOGLE_VERSION}', ` +
1100
- `keep 'deployments' and remove 'deployment_target'.`);
1101
- }
1102
1096
  // `deployment_target` has been replaced by the named `deployments:` list in
1103
- // the extended profile (and was never in vanilla). Reject it unless
1104
- // `allowLegacyBareSource` is set for legacy fixtures and profile files.
1105
- if (m.deployment_target !== undefined && !allowLegacyBareSource) {
1097
+ // the extended profile (and was never in vanilla).
1098
+ if (m.deployment_target !== undefined) {
1106
1099
  if (!extended) {
1107
1100
  throw new SemanticModelError(`Semantic model load error: ${label}: 'deployment_target' is not ` +
1108
1101
  `in the open format ('${exports.OSSIE_VERSION}'); ` +
@@ -1162,9 +1155,8 @@ function normalizeModelSugars(m, extended, allowLegacyBareSource = false) {
1162
1155
  }
1163
1156
  }
1164
1157
  // Builds the GOOGLE custom-extension block that carries a model's deployment
1165
- // target URIs on the IR (the form the deploy leg reads). Both the native
1166
- // `deployments:` list and (under `allowLegacyBareSource`) the legacy
1167
- // `deployment_target:` key fold into this block after validation (see
1158
+ // target URIs on the IR (the form the deploy leg reads). The native
1159
+ // `deployments:` list folds into this block after validation (see
1168
1160
  // convertModel). The extended profile rejects any authored GOOGLE block (see
1169
1161
  // rejectGoogleBlock), so there is never a pre-existing one to reconcile with.
1170
1162
  function deploymentTargetsExtension(targets) {
@@ -1282,7 +1274,7 @@ function readGoogleBlocks(exts, version, level, ctx) {
1282
1274
  }
1283
1275
  const parsed = GOOGLE_BLOCK_SCHEMAS[key].safeParse(value);
1284
1276
  if (!parsed.success) {
1285
- throw error(`'${key}' in a ${block}: ${issueList(parsed.error)}`);
1277
+ throw error(keyProblems(parsed.error, GOOGLE_BLOCK_SCHEMAS[key], key, value, `the ${block}`).replaceAll('\n', `\n${ctx}: `));
1286
1278
  }
1287
1279
  values[key] = parsed.data;
1288
1280
  }
@@ -1756,11 +1748,14 @@ function fromDocument(doc, opts = {}) {
1756
1748
  if (proto) {
1757
1749
  throw new SemanticModelError(`Semantic model load error: ${proto}`);
1758
1750
  }
1759
- const normalized = normalizeDocumentSugars(doc, extended, opts.allowLegacyBareSource);
1760
- const result = makeDocumentSchema(opts.bindingOptional ?? false, extended)
1761
- .safeParse(normalized);
1751
+ const normalized = normalizeDocumentSugars(doc, extended);
1752
+ const schema = makeDocumentSchema(opts.bindingOptional ?? false, extended);
1753
+ const result = schema.safeParse(normalized);
1762
1754
  if (!result.success) {
1763
- throw new SemanticModelError(`Semantic model load error: ${result.error.message}`);
1755
+ throw new SemanticModelError(`Semantic model load error: ${schemaProblems(result.error.issues, {
1756
+ schema, doc: normalized, authored: doc, extended,
1757
+ subject: 'the document',
1758
+ })}`);
1764
1759
  }
1765
1760
  const warnings = [];
1766
1761
  const parsed = result.data;
@@ -1823,16 +1818,19 @@ function runsDialect(entity, dialect) {
1823
1818
  return (0, resolve_profiles_1.sourcesDialect)({ entities: [entity] }) === dialect.toUpperCase();
1824
1819
  }
1825
1820
  // The warning `item` gives on `table`, the table of `entity`:
1826
- // expressionWarning's when the load knows the table's dialect, or, unless
1827
- // `profiled`, one saying the table has no text to run. On a table of the
1821
+ // expressionWarning's when the load knows the table's dialect, or one saying
1822
+ // the table has no text to run. With `profiled`, the second is given only on a
1823
+ // Cloud SQL, Spanner or third-party table no lookup has read. On a table of the
1828
1824
  // `preferred` dialect, expressionWarning's transpilation warning already says
1829
1825
  // so.
1830
1826
  function tableWarning(item, subject, table, entity, preferred, profiled) {
1831
1827
  const w = table.unread ?
1832
1828
  undefined :
1833
1829
  expressionWarning(item, subject, table.name, preferred);
1834
- if (w || profiled || !(0, resolve_profiles_1.noTextFor)(item, table.dialects))
1830
+ if (w || (profiled && !table.unread && !table.guessed) ||
1831
+ !(0, resolve_profiles_1.noTextFor)(item, table.dialects)) {
1835
1832
  return w;
1833
+ }
1836
1834
  const own = table.dialects.map(d => `'${d}'`).join(', ');
1837
1835
  return `${subject.ctx}: no ${own} or '${FALLBACK_DIALECT}' dialect, so the ${table.name} table of entity '${entity}' cannot run it`;
1838
1836
  }
@@ -1865,19 +1863,13 @@ function metricWarning(metric, reached, entities, preferred, profiled) {
1865
1863
  }
1866
1864
  return undefined;
1867
1865
  }
1868
- // The warning a concrete entity with neither a primary key nor a unique key
1869
- // gives, or undefined. A unique key is enough, as the graph keys the entity's
1870
- // node by the first one.
1871
- function keyWarning(entity) {
1872
- return !(0, resolve_profiles_1.statesKey)(entity.keys, entity.uniqueKeys) && !entity.abstract ?
1873
- noPrimaryKeyWarning(entity.name) :
1874
- undefined;
1875
- }
1876
1866
  // Infers the entities a metric's expressions reach across all dialect entries,
1877
1867
  // and returns the warning the metric gives when an expression could not be
1878
1868
  // parsed or when it states no `entity` and its expressions name none. An
1879
1869
  // authored anchor, which vanilla writes in a GOOGLE block, settles which entity
1880
- // the metric belongs to, so a `COUNT(*)` with one is not warned about.
1870
+ // the metric belongs to, so a `COUNT(*)` with one is not warned about. An
1871
+ // ANSI_SQL text whose `impliedDialect` no lookup read (null) is read as
1872
+ // generic SQL, the closest reading there is.
1881
1873
  function inferMetricEntities(metric, entityNames, preferred = exports.DEFAULT_DIALECT, impliedDialect, version) {
1882
1874
  const ctx = `metric '${metric.name}'`;
1883
1875
  const sources = (metric.dialects ?? [])
@@ -1895,7 +1887,7 @@ function inferMetricEntities(metric, entityNames, preferred = exports.DEFAULT_DI
1895
1887
  for (const { text, dialect } of sources) {
1896
1888
  if (!text)
1897
1889
  continue;
1898
- const cols = (0, sql_expr_utils_1.columnsForSource)(text, dialect, impliedDialect);
1890
+ const cols = (0, sql_expr_utils_1.columnsForSource)(text, dialect, impliedDialect ?? undefined);
1899
1891
  if (cols === undefined) {
1900
1892
  unparsed = true;
1901
1893
  }
@@ -1923,16 +1915,14 @@ function inferMetricEntities(metric, entityNames, preferred = exports.DEFAULT_DI
1923
1915
  return { referenced };
1924
1916
  }
1925
1917
  // Every warning a load gives about `model`'s entities and metrics, in the
1926
- // order a load gives them: each concrete entity's missing key, each field's
1927
- // expression warning, then each metric's expression warning and its warning
1928
- // when it names no entity. A metric a profile excludes gives none. With
1929
- // `profiled`, the profile check reports a field or metric its tables cannot
1930
- // run, so no warning repeats it. A profile changes only these; a load's other
1931
- // warnings, about actions, come after them.
1918
+ // order a load gives them: each field's expression warning, then each metric's
1919
+ // expression warning and its warning when it names no entity. A metric a
1920
+ // profile excludes gives none. With `profiled`, the profile check reports a
1921
+ // field or metric its tables cannot run, so no warning repeats it. A profile
1922
+ // changes only these; a load's other warnings, about actions, come after them.
1932
1923
  function entityAndMetricWarnings(model, preferred = exports.DEFAULT_DIALECT, profiled = false) {
1933
1924
  const entities = model.entities ?? [];
1934
- const out = entities.map(keyWarning).filter((w) => !!w);
1935
- out.push(...fieldWarnings(model, preferred, profiled));
1925
+ const out = fieldWarnings(model, preferred, profiled);
1936
1926
  const entityNames = entities.map(e => e.name);
1937
1927
  const impliedDialect = (0, resolve_profiles_1.sourcesDialect)(model);
1938
1928
  const excluded = new Set(model.excludedMetrics);
@@ -2074,10 +2064,6 @@ function convertModel(m, version, opts, warnings) {
2074
2064
  }
2075
2065
  seenTargets.set(d.target, d.name);
2076
2066
  }
2077
- // A blank `deployment_target` names no target, as push reads it.
2078
- if (!deployments.length && m.deployment_target?.trim()) {
2079
- deployments.push({ name: 'default', target: m.deployment_target.trim() });
2080
- }
2081
2067
  const description = composeDescription(m.description);
2082
2068
  // `version` records the flavor the document was written in, so a consumer
2083
2069
  // (e.g. a pull) can return the model in that same flavor.
@@ -2096,9 +2082,9 @@ function convertModel(m, version, opts, warnings) {
2096
2082
  if (ai)
2097
2083
  model.aiContext = ai;
2098
2084
  // The blocks the model keeps are carried verbatim. Under the extended
2099
- // profile the native `deployments` list (or legacy `deployment_target` key)
2100
- // is folded into a GOOGLE block appended after them; no authored GOOGLE block
2101
- // can collide with it, since that flavor rejects one (see rejectGoogleBlock).
2085
+ // profile the native `deployments` list is folded into a GOOGLE block
2086
+ // appended after them; no authored GOOGLE block can collide with it, since
2087
+ // that flavor rejects one (see rejectGoogleBlock).
2102
2088
  // Vanilla deployments, read from a GOOGLE block, are not folded.
2103
2089
  if (block.kept)
2104
2090
  model.customExtensions = block.kept;
@@ -2118,15 +2104,12 @@ function convertDataset(ds, block, version, opts, warnings, dialect) {
2118
2104
  const ctxLabel = `dataset '${ds.name}'`;
2119
2105
  // An abstract entity has no physical table, so it carries no source (empty
2120
2106
  // dataSource) and no key -- both are meaningless for a class never
2121
- // materialized. Only a concrete entity is parsed/warned for those.
2107
+ // materialized. Only a concrete entity's source is parsed.
2122
2108
  const parsedSource = ds.source !== undefined ?
2123
- parseSource(ds.source, opts, ctxLabel, warnings, /* importHint= */ true) :
2109
+ parseSource(ds.source, opts, ctxLabel, warnings, version) :
2124
2110
  undefined;
2125
2111
  const dataSource = parsedSource ? parsedSource.dataSource : '';
2126
2112
  const keys = ds.primary_key ?? [];
2127
- const kw = keyWarning({ name: ds.name, keys, uniqueKeys: ds.unique_keys, abstract: ds.abstract });
2128
- if (kw)
2129
- warnings.push(kw);
2130
2113
  const fields = (ds.fields ?? []).map(f => convertField(f, ds.name, version, dialect));
2131
2114
  rejectDuplicateNames(fields.map(f => f.name), 'field name', `dataset '${ds.name}'`, true);
2132
2115
  const entity = { name: ds.name, dataSource, keys, fields };
@@ -2231,7 +2214,7 @@ function readFieldExpression(raw, where, dialect, problemOf) {
2231
2214
  z.string().safeParse(raw) :
2232
2215
  expressionObjectSchema.safeParse(raw);
2233
2216
  if (!result.success) {
2234
- throw new SemanticModelError(`${where}: 'expression': ${issueList(result.error)}`);
2217
+ throw new SemanticModelError(keyProblems(result.error, expressionObjectSchema, 'expression', raw, where));
2235
2218
  }
2236
2219
  const problem = problemOf(result.data);
2237
2220
  if (problem) {
@@ -2645,11 +2628,178 @@ function convertParameter(p, actionName, concepts, warnings, inheritanceResolved
2645
2628
  }
2646
2629
  return param;
2647
2630
  }
2648
- // A schema error's problems on one line, each led by its path.
2649
- function issueList(error) {
2650
- return error.issues
2651
- .map(i => (i.path.length ? `${i.path.join('.')}: ` : '') + i.message)
2652
- .join('; ');
2631
+ // One line per problem that `schema` finds in the value of `key` on
2632
+ // `subject`.
2633
+ function keyProblems(error, schema, key, value, subject) {
2634
+ const doc = { [key]: value };
2635
+ return schemaProblems(error.issues.map(i => ({ ...i, path: [key, ...i.path] })), { schema: z.object({ [key]: schema }), doc, authored: doc, extended: true,
2636
+ subject });
2637
+ }
2638
+ // The noun a message gives an entry of each list of named objects in a model
2639
+ // document. An entry of `datasets` is an entity in the Google flavor.
2640
+ const ENTRY_NOUNS = new Map([
2641
+ ['semantic_model', 'model'], ['datasets', 'dataset'], ['fields', 'field'],
2642
+ ['relationships', 'relationship'], ['metrics', 'metric'],
2643
+ ['actions', 'action'], ['parameters', 'parameter'],
2644
+ ['constraints', 'constraint'], ['deployments', 'deployment'],
2645
+ ]);
2646
+ // What a message says a schema expects.
2647
+ const EXPECTED = {
2648
+ string: 'a string',
2649
+ boolean: 'true or false',
2650
+ array: 'a list',
2651
+ object: 'a mapping',
2652
+ record: 'a mapping',
2653
+ };
2654
+ // A value as a message describes it.
2655
+ function described(value) {
2656
+ if (value === undefined)
2657
+ return 'missing';
2658
+ if (value === null)
2659
+ return 'empty';
2660
+ if (Array.isArray(value))
2661
+ return 'a list';
2662
+ if (typeof value === 'object')
2663
+ return 'a mapping';
2664
+ if (typeof value !== 'string')
2665
+ return String(value);
2666
+ return value.includes('\n') ? 'a string' : `'${value}'`;
2667
+ }
2668
+ // The keys the object at `path` in `schema` takes, apart from unlisted keys.
2669
+ function keysAt(schema, path) {
2670
+ // `s` without its optional, nullable and pipe wrappers. Of a union, the
2671
+ // member that is a `kind`.
2672
+ const unwrapped = (s, kind) => {
2673
+ if (s instanceof z.ZodOptional || s instanceof z.ZodNullable) {
2674
+ return unwrapped(s.unwrap(), kind);
2675
+ }
2676
+ if (s instanceof z.ZodPipe)
2677
+ return unwrapped(s.in, kind);
2678
+ if (s instanceof z.ZodUnion) {
2679
+ return s.options.map(o => unwrapped(o, kind))
2680
+ .find(o => o instanceof kind);
2681
+ }
2682
+ return s;
2683
+ };
2684
+ let node = schema;
2685
+ for (const seg of path) {
2686
+ const holder = unwrapped(node, typeof seg === 'number' ? z.ZodArray : z.ZodObject);
2687
+ node = holder instanceof z.ZodArray ? holder.element :
2688
+ holder instanceof z.ZodObject ? holder.shape[String(seg)] :
2689
+ undefined;
2690
+ }
2691
+ const object = unwrapped(node, z.ZodObject);
2692
+ if (!(object instanceof z.ZodObject))
2693
+ return [];
2694
+ return Object.keys(object.shape)
2695
+ .filter(k => !UNLISTED_KEYS.has(object.shape[k]));
2696
+ }
2697
+ // Where `path` points in `input.doc`: the innermost named object, as messages
2698
+ // name it, and the key below it, such as "'data' of entry 1 of
2699
+ // 'custom_extensions'". `spelled` gives a key as the author wrote it: a
2700
+ // Google-flavor model's `datasets` is `entities` unless the author wrote
2701
+ // `datasets`.
2702
+ function schemaLocation(path, input) {
2703
+ let subject = input.subject;
2704
+ let name;
2705
+ let entity;
2706
+ let datasets = 'datasets';
2707
+ let rest = [];
2708
+ let node = input.doc;
2709
+ for (const seg of path) {
2710
+ node = node?.[seg];
2711
+ const list = rest.length === 1 ? String(rest[0]) : '';
2712
+ const noun = list === 'datasets' && input.extended ?
2713
+ 'entity' :
2714
+ ENTRY_NOUNS.get(list);
2715
+ if (typeof seg !== 'number' || !noun) {
2716
+ rest.push(seg);
2717
+ continue;
2718
+ }
2719
+ rest = [];
2720
+ name = typeof node?.name === 'string' ? node.name : undefined;
2721
+ const named = name === undefined ? `${noun} number ${seg + 1}` : `${noun} '${name}'`;
2722
+ if (list === 'semantic_model') {
2723
+ const written = input.authored?.semantic_model?.[seg];
2724
+ datasets = input.extended && written?.datasets === undefined ?
2725
+ 'entities' :
2726
+ 'datasets';
2727
+ }
2728
+ if (list === 'datasets')
2729
+ entity = name;
2730
+ if (list === 'fields' && name !== undefined && entity !== undefined) {
2731
+ name = `${entity}.${name}`;
2732
+ subject = `field '${name}'`;
2733
+ }
2734
+ else {
2735
+ subject = list === 'fields' || list === 'parameters' ?
2736
+ `${named} of ${subject}` :
2737
+ named;
2738
+ }
2739
+ }
2740
+ const spelled = (key) => key === 'datasets' ? datasets : String(key);
2741
+ const place = rest.map(s => typeof s === 'number' ? `entry ${s + 1}` :
2742
+ `'${spelled(s)}'`)
2743
+ .reverse()
2744
+ .join(' of ');
2745
+ return { subject, name, place, value: node, spelled };
2746
+ }
2747
+ // One line per problem of a schema error, naming the object and the key and
2748
+ // saying how to fix it.
2749
+ function schemaProblems(issues, input) {
2750
+ // zod also checks the length of a value of the wrong type, as for
2751
+ // `target: []`. The type is what to fix, so that check gives no line.
2752
+ const mistyped = new Set(issues.filter(i => i.code === 'invalid_type')
2753
+ .map(i => JSON.stringify(i.path)));
2754
+ return issues
2755
+ .filter(i => i.code !== 'too_small' || !mistyped.has(JSON.stringify(i.path)))
2756
+ .map(i => schemaProblem(i, input))
2757
+ .join('\n');
2758
+ }
2759
+ function schemaProblem(issue, input) {
2760
+ if (issue.code === 'invalid_union') {
2761
+ // When only one alternative takes the value's type, report its problems.
2762
+ const typed = issue.errors.filter(b => !b.every(i => i.code === 'invalid_type' && !i.path.length));
2763
+ if (typed.length === 1) {
2764
+ return schemaProblems(typed[0].map(i => ({ ...i, path: [...issue.path, ...i.path] })), input);
2765
+ }
2766
+ }
2767
+ const { subject, name, place, value, spelled } = schemaLocation(issue.path, input);
2768
+ const at = place ? `${subject}: ${place}` : subject;
2769
+ const takes = (what) => `${at} is ${described(value)}; it takes ${what}`;
2770
+ const expected = (type) => EXPECTED[type] ?? type;
2771
+ switch (issue.code) {
2772
+ case 'unrecognized_keys': {
2773
+ const keys = keysAt(input.schema, issue.path).map(k => `'${spelled(k)}'`);
2774
+ return `${subject}: unknown key${issue.keys.length > 1 ? 's' : ''} ${listed(issue.keys.map(k => `'${k}'`))}${place ? ` in ${place}` : ''}; it takes ${listed(keys)}`;
2775
+ }
2776
+ case 'invalid_type':
2777
+ return takes(expected(issue.expected));
2778
+ case 'invalid_union': {
2779
+ // A vanilla document writes an expression only as a mapping.
2780
+ const types = issue.errors.flatMap(b => b.flatMap(i => i.code === 'invalid_type' ? [i.expected] : []));
2781
+ const offered = types.filter(t => input.extended || t !== 'string' ||
2782
+ issue.path.at(-1) !== 'expression');
2783
+ return takes(listed([...new Set(offered.map(expected))], 'or'));
2784
+ }
2785
+ case 'invalid_value':
2786
+ return takes(listed(issue.values.map(v => `'${String(v)}'`), 'or'));
2787
+ case 'too_small':
2788
+ if (issue.origin === 'array') {
2789
+ return `${at} is an empty list; it takes at least one entry`;
2790
+ }
2791
+ if (issue.origin === 'string') {
2792
+ return `${at} is empty; it takes a non-empty string`;
2793
+ }
2794
+ break;
2795
+ case 'custom':
2796
+ // A rule on a named object quotes the object's name in its message.
2797
+ if (name !== undefined && issue.message.includes(`'${name}'`)) {
2798
+ return issue.message;
2799
+ }
2800
+ return `${subject}: ${issue.message}`;
2801
+ }
2802
+ return `${at}: ${issue.message}`;
2653
2803
  }
2654
2804
  // An executor written outside a model document, such as in a profile file,
2655
2805
  // checked against the loader's schema and converted as a load converts one.
@@ -2657,7 +2807,7 @@ function issueList(error) {
2657
2807
  function parseExecutor(raw, where) {
2658
2808
  const result = executorSchema.safeParse(raw);
2659
2809
  if (!result.success) {
2660
- throw new SemanticModelError(`${where} 'executor': ${issueList(result.error)}`);
2810
+ throw new SemanticModelError(keyProblems(result.error, executorSchema, 'executor', raw, where));
2661
2811
  }
2662
2812
  return convertExecutor(result.data);
2663
2813
  }
@@ -2703,7 +2853,9 @@ function pickDialect(expr, preferred) {
2703
2853
  }
2704
2854
  const MAX_CATALOG_NAME_LENGTH = 4000;
2705
2855
  const BIGQUERY_TABLE_URI = /^\/\/bigquery\.googleapis\.com\/projects\/([^/]+)\/datasets\/([^/.]+)\/tables\/([^/.]+)$/;
2706
- const BIGLAKE_TABLE_URI = /^\/\/biglake\.googleapis\.com\/projects\/([^/]+)\/catalogs\/([^/.]+)\/namespaces\/([^/.]+)\/tables\/([^/.]+)$/;
2856
+ // A BigLake table sits in a namespace of an Iceberg REST catalog, or in a
2857
+ // database of a Hive catalog.
2858
+ const BIGLAKE_TABLE_URI = /^\/\/biglake\.googleapis\.com\/projects\/([^/]+)\/catalogs\/([^/.]+)\/(namespaces|databases)\/([^/.]+)\/tables\/([^/.]+)$/;
2707
2859
  const SPANNER_TABLE_URI = /^\/\/spanner\.googleapis\.com\/projects\/([^/]+)\/instances\/([^/]+)\/databases\/([^/]+)\/tables\/[^/]+$/;
2708
2860
  const ALLOYDB_TABLE_URI = /^\/\/alloydb\.googleapis\.com\/projects\/([^/]+)\/locations\/([^/]+)\/clusters\/([^/]+)\/databases\/([^/]+)\/schemas\/[^/]+\/tables\/[^/]+$/;
2709
2861
  const LEGACY_ALLOYDB_INSTANCE_URI = /^\/\/alloydb\.googleapis\.com\/projects\/([^/]+)\/locations\/([^/]+)\/clusters\/([^/]+)\/instances\/[^/]+\/databases\/([^/]+)\/tables\/([^/]+)$/;
@@ -2742,8 +2894,10 @@ function sourceTableOf(source) {
2742
2894
  };
2743
2895
  }
2744
2896
  m = uri.match(BIGLAKE_TABLE_URI);
2745
- if (m)
2746
- return { system: 'biglake', resourceName, project: m[1], catalog: m[2] };
2897
+ if (m) {
2898
+ const hive = m[3] === 'databases';
2899
+ return { system: 'biglake', resourceName, project: m[1], catalog: m[2], hive };
2900
+ }
2747
2901
  m = uri.match(SPANNER_TABLE_URI);
2748
2902
  if (m)
2749
2903
  return { system: 'spanner', resourceName, project: m[1], instance: m[2] };
@@ -2829,7 +2983,8 @@ function oldSourceForm(source) {
2829
2983
  exports.NO_SPACE = 'kcmd cannot name a table whose name holds a space';
2830
2984
  // The resource name of a table in each Google Cloud service a source can
2831
2985
  // name, with a placeholder for each name in it. A Cloud SQL table has one
2832
- // form without a schema and one with.
2986
+ // form without a schema and one with, and a BigLake table one for each kind
2987
+ // of catalog.
2833
2988
  exports.TABLE_FORMS = {
2834
2989
  bigquery: 'bigquery.googleapis.com/projects/<project>/datasets/<dataset>/tables/<table>',
2835
2990
  spanner: 'spanner.googleapis.com/projects/<project>/instances/<instance>/databases/<database>/tables/<table>',
@@ -2837,11 +2992,14 @@ exports.TABLE_FORMS = {
2837
2992
  cloudsql: 'cloudsql.googleapis.com/projects/<project>/locations/<location>/instances/<instance>/databases/<database>/tables/<table>',
2838
2993
  cloudsqlSchema: 'cloudsql.googleapis.com/projects/<project>/locations/<location>/instances/<instance>/databases/<database>/schemas/<schema>/tables/<table>',
2839
2994
  biglake: 'biglake.googleapis.com/projects/<project>/catalogs/<catalog>/namespaces/<namespace>/tables/<table>',
2995
+ biglakeHive: 'biglake.googleapis.com/projects/<project>/catalogs/<catalog>/databases/<database>/tables/<table>',
2840
2996
  };
2841
- // The databases each Cloud SQL form in TABLE_FORMS is for.
2842
- const CLOUD_SQL_DATABASES = {
2997
+ // What each form in TABLE_FORMS is for, where its service has two.
2998
+ const FORM_USES = {
2843
2999
  cloudsql: 'MySQL',
2844
3000
  cloudsqlSchema: 'PostgreSQL, SQL Server',
3001
+ biglake: 'Iceberg REST catalog',
3002
+ biglakeHive: 'Hive catalog',
2845
3003
  };
2846
3004
  // `form` with its last placeholders replaced by `parts`, in order, or
2847
3005
  // undefined when `form` has fewer placeholders than `parts`.
@@ -3095,9 +3253,8 @@ function isAcceptedSource(source) {
3095
3253
  return source !== undefined && databaseOf(source) !== undefined;
3096
3254
  }
3097
3255
  // The source an entity is written with: its `authoredSource` when that names a
3098
- // table in a form `databaseOf` reads, and otherwise its `dataSource`, which is
3099
- // how the load completes a legacy bare table name, shown as a resource name. A
3100
- // model is serialized, shown and judged by it.
3256
+ // table in a form `databaseOf` reads, and otherwise its `dataSource`, shown as
3257
+ // a resource name. A model is serialized, shown and judged by it.
3101
3258
  function writtenSource(entity) {
3102
3259
  const authored = entity.authoredSource?.trim();
3103
3260
  return authored !== undefined && isAcceptedSource(authored) ?
@@ -3119,8 +3276,8 @@ function sourceToDataSource(source) {
3119
3276
  return `${bqUri[1]}.${bqUri[2]}.${bqUri[3]}`;
3120
3277
  const biglakeUri = value.match(BIGLAKE_TABLE_URI);
3121
3278
  if (biglakeUri) {
3122
- const [, project, catalog, namespace, table] = biglakeUri;
3123
- return `${project}.${catalog}.${namespace}.${table}`;
3279
+ const [, project, catalog, , parent, table] = biglakeUri;
3280
+ return `${project}.${catalog}.${parent}.${table}`;
3124
3281
  }
3125
3282
  return value;
3126
3283
  }
@@ -3160,11 +3317,19 @@ function bindsThirdPartyTable(entity) {
3160
3317
  return thirdPartyEntryOf(entity.authoredSource ?? '') !== undefined;
3161
3318
  }
3162
3319
  // The key LoadOptions.sourceDialects holds a source's dialect under: a Cloud
3163
- // SQL table's resource name, or a third-party table's entry name. Undefined
3164
- // for a source whose own name gives its dialect.
3320
+ // SQL table's resource name, a third-party table's entry name, or a Spanner
3321
+ // table's database. Undefined for a source whose own name gives its dialect.
3165
3322
  function dialectLookupKey(source) {
3166
3323
  return cloudSqlTableOf(source)?.resourceName ??
3167
- thirdPartyEntryOf(source)?.name;
3324
+ thirdPartyEntryOf(source)?.name ?? spannerDatabaseOf(source);
3325
+ }
3326
+ // The resource name of the database the Spanner table `source` is in.
3327
+ // Undefined for any other source and for a name with quotes.
3328
+ function spannerDatabaseOf(source) {
3329
+ const uri = asResourceUri(source.trim());
3330
+ return !QUOTE.test(uri) && SPANNER_TABLE_URI.test(uri) ?
3331
+ uri.slice(2, uri.lastIndexOf('/tables/')) :
3332
+ undefined;
3168
3333
  }
3169
3334
  // Why `source` is not accepted when it is a Knowledge Catalog entry name in a
3170
3335
  // system entry group, one whose id starts with `@`, such as `@bigquery`.
@@ -3234,14 +3399,12 @@ function thirdPartyNameError(source, ctx) {
3234
3399
  // Reads an entity's `source` into the dotted name the generators use and the
3235
3400
  // source as written. Throws on a form the loader does not accept. A profile
3236
3401
  // file's sources are read here too, so they follow the same rules. With
3237
- // `importHint`, for a model file's source, a bare table name's error also
3238
- // names `kcmd import`, which completes a model file's bare names but not a
3239
- // profile file's.
3240
- function parseSource(source, opts, ctx, warnings, importHint = false) {
3402
+ // `importVersion`, the version of the model file the source is in, a bare
3403
+ // table name's error also names `kcmd import`, which completes a model file's
3404
+ // bare names but not a profile file's.
3405
+ function parseSource(source, opts, ctx, warnings, importVersion) {
3241
3406
  const trimmed = source.trim();
3242
- // Under the legacy wrapper, a space inside double quotes also marks a query.
3243
- if (hasUnquotedWhitespace(trimmed) || (opts.allowLegacyBareSource &&
3244
- hasUnquotedWhitespace(trimmed.replaceAll('"', '')))) {
3407
+ if (hasUnquotedWhitespace(trimmed)) {
3245
3408
  throw new SemanticModelError(`Semantic model load error: ${ctx}: source '${trimmed}' looks like a ` +
3246
3409
  `SQL query; a query-valued source is not supported yet. Bind 'source' ` +
3247
3410
  `to a physical table using its GCP resource name (e.g. ` +
@@ -3288,27 +3451,12 @@ function parseSource(source, opts, ctx, warnings, importHint = false) {
3288
3451
  !trimmed.includes(':');
3289
3452
  let dotted;
3290
3453
  if (isBareTableName) {
3291
- // The legacy wrapper splits at every dot outside backticks.
3292
- const rawSegs = catalogNameSegments(trimmed);
3293
- if (opts.allowLegacyBareSource && rawSegs.length > 0) {
3294
- const parts = rawSegs.map(unquote);
3295
- if (parts.length === 1 && opts.defaultDataset) {
3296
- parts.unshift(opts.defaultDataset);
3297
- }
3298
- if (parts.length < 3 && opts.defaultProject) {
3299
- parts.unshift(opts.defaultProject);
3300
- }
3301
- return {
3302
- authoredSource: trimmed,
3303
- dataSource: parts.join('.'),
3304
- };
3305
- }
3306
- // With `importHint`, the example also names the command that completes a
3307
- // model file's bare names.
3308
- const viaImport = importHint ?
3309
- `\nOr run 'kcmd import <file> --flavor <ossie|google> ` +
3310
- `--source-prefix <prefix>' to complete each bare table name ` +
3311
- `from the database that <prefix> names.` :
3454
+ // With `importVersion`, the example also names the command that completes
3455
+ // a model file's bare names.
3456
+ const viaImport = importVersion ?
3457
+ `\nOr run '${(0, resolve_profiles_1.importCommand)('--source-prefix <prefix>', importVersion)}' to ` +
3458
+ `complete each bare table name from the database or Knowledge ` +
3459
+ `Catalog entry group that <prefix> names.` :
3312
3460
  '';
3313
3461
  // A name holding a space gets no name to write. Any other name gets an
3314
3462
  // example built from its parts when the loader accepts the example, and
@@ -3334,11 +3482,11 @@ function parseSource(source, opts, ctx, warnings, importHint = false) {
3334
3482
  }
3335
3483
  }
3336
3484
  // Each table form with its placeholders shortened, such as <p> for
3337
- // <project>, and the databases a Cloud SQL form is for.
3485
+ // <project>, and what it is for where its service has two.
3338
3486
  const forms = Object.entries(exports.TABLE_FORMS).map(([kind, form]) => {
3339
3487
  const short = form.replace(/<(\w+)>/g, (_, name) => `<${name === 'database' ? 'db' : name[0]}>`);
3340
- const databases = CLOUD_SQL_DATABASES[kind];
3341
- return `'${short}'${databases ? ` (${databases})` : ''}`;
3488
+ const use = FORM_USES[kind];
3489
+ return `'${short}'${use ? ` (${use})` : ''}`;
3342
3490
  });
3343
3491
  throw new SemanticModelError(`Semantic model load error: ${ctx}: source '${trimmed}' is not a valid ` +
3344
3492
  `table reference${dotted ? `; ${dotted}` : ''}. Expected a GCP ` +
@@ -3346,9 +3494,6 @@ function parseSource(source, opts, ctx, warnings, importHint = false) {
3346
3494
  `or, for a table outside Google Cloud, the entry name of its ` +
3347
3495
  `Knowledge Catalog entry ('${exports.ENTRY_NAME_FORM}').`);
3348
3496
  }
3349
- function unquote(part) {
3350
- return part.replace(/^[`"]/, '').replace(/[`"]$/, '');
3351
- }
3352
3497
  // The parts of a bare table name, without their quotes. A part in backticks
3353
3498
  // or double quotes is one name, dots included, and a doubled quote character
3354
3499
  // in it, as in "a""b", is one literal quote. The quotes are those
@@ -3401,9 +3546,6 @@ function bareNameParts(name, bigQuery) {
3401
3546
  * thrown, mirroring how the deploy legs previously reported it; loader warnings
3402
3547
  * are prefixed with their document name.
3403
3548
  *
3404
- * A document's `allowLegacyBareSource` sets that load option for that document
3405
- * alone.
3406
- *
3407
3549
  * Each document needs the SQL parser as loadModels does, so call
3408
3550
  * `loadSqlEngine()` first. Without it, this throws the parser's plain Error.
3409
3551
  */
@@ -3412,14 +3554,8 @@ function loadSemanticModels(docs, opts = {}) {
3412
3554
  const warnings = [];
3413
3555
  for (const doc of docs) {
3414
3556
  let loaded;
3415
- const docOpts = {
3416
- ...opts,
3417
- ...(doc.allowLegacyBareSource !== undefined ?
3418
- { allowLegacyBareSource: doc.allowLegacyBareSource } :
3419
- {}),
3420
- };
3421
3557
  try {
3422
- loaded = loadModels(doc.text, docOpts);
3558
+ loaded = loadModels(doc.text, opts);
3423
3559
  }
3424
3560
  catch (err) {
3425
3561
  if (!(err instanceof SemanticModelError))