@google-cloud/kcmd 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -20
- package/build/ts/kcmd/cli.js +5842 -5168
- package/build/ts/kcmd/gcp/biglake.d.ts +2 -0
- package/build/ts/kcmd/gcp/biglake.js +13 -4
- package/build/ts/kcmd/gcp/spanner.d.ts +10 -0
- package/build/ts/kcmd/gcp/spanner.js +6 -3
- package/build/ts/kcmd/layouts/okf.js +49 -7
- package/build/ts/kcmd/layouts/semantic-model.d.ts +13 -0
- package/build/ts/kcmd/layouts/semantic-model.js +129 -50
- package/build/ts/kcmd/okf/concept.d.ts +1 -1
- package/build/ts/kcmd/okf/concept.js +6 -0
- package/build/ts/kcmd/okf/concept_to_entry.d.ts +2 -0
- package/build/ts/kcmd/okf/concept_to_entry.js +29 -2
- package/build/ts/kcmd/okf/init_okf_workspace.js +8 -10
- package/build/ts/kcmd/okf/okf_metadata.js +17 -0
- package/build/ts/kcmd/semantic/bigquery.js +139 -195
- package/build/ts/kcmd/semantic/binding.js +14 -19
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +10 -35
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +92 -121
- package/build/ts/kcmd/semantic/graph_ddl.d.ts +10 -0
- package/build/ts/kcmd/semantic/graph_ddl.js +152 -59
- package/build/ts/kcmd/semantic/import_model.d.ts +26 -4
- package/build/ts/kcmd/semantic/import_model.js +526 -103
- package/build/ts/kcmd/semantic/kc_converter.d.ts +7 -1
- package/build/ts/kcmd/semantic/kc_converter.js +20 -5
- package/build/ts/kcmd/semantic/kc_custom_types.d.ts +5 -12
- package/build/ts/kcmd/semantic/kc_custom_types.js +16 -93
- package/build/ts/kcmd/semantic/kc_ids.d.ts +4 -5
- package/build/ts/kcmd/semantic/kc_ids.js +4 -7
- package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +0 -3
- package/build/ts/kcmd/semantic/knowledge_catalog.js +3 -86
- package/build/ts/kcmd/semantic/loader.d.ts +7 -11
- package/build/ts/kcmd/semantic/loader.js +284 -148
- package/build/ts/kcmd/semantic/osi_converter.js +1 -2
- package/build/ts/kcmd/semantic/parameters.d.ts +1 -0
- package/build/ts/kcmd/semantic/parameters.js +22 -3
- package/build/ts/kcmd/semantic/pull_kc.d.ts +1 -5
- package/build/ts/kcmd/semantic/pull_kc.js +38 -112
- package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +7 -2
- package/build/ts/kcmd/semantic/resolve_inheritance.js +37 -21
- package/build/ts/kcmd/semantic/resolve_profiles.d.ts +51 -10
- package/build/ts/kcmd/semantic/resolve_profiles.js +213 -32
- package/build/ts/kcmd/semantic/runtime/runtime.d.ts +0 -6
- package/build/ts/kcmd/semantic/runtime/runtime.js +4 -21
- package/build/ts/kcmd/semantic/source_dialect.d.ts +9 -2
- package/build/ts/kcmd/semantic/source_dialect.js +83 -33
- package/build/ts/kcmd/semantic/source_entries.d.ts +1 -1
- package/build/ts/kcmd/semantic/source_entries.js +95 -36
- package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +13 -7
- package/build/ts/kcmd/semantic/sql_expr_utils.js +240 -67
- package/build/ts/kcmd/semantic/sql_parser.d.ts +5 -2
- package/build/ts/kcmd/semantic/sql_parser.js +22 -0
- package/build/ts/kcmd/semantic/transpile.d.ts +12 -10
- package/build/ts/kcmd/semantic/transpile.js +79 -61
- package/build/ts/kcmd/semantic/validate.d.ts +37 -2
- package/build/ts/kcmd/semantic/validate.js +465 -98
- package/build/ts/kcmd/snapshot.d.ts +4 -0
- package/build/ts/kcmd/snapshot.js +7 -0
- package/build/ts/kcmd/sync.js +44 -7
- package/package.json +1 -1
|
@@ -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.
|
|
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
|
-
|
|
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(', ')}
|
|
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: `
|
|
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
|
|
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' ?
|
|
884
|
-
z.
|
|
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.
|
|
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
|
|
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
|
|
1088
|
+
normalizeModelSugars(m, extended);
|
|
1082
1089
|
}
|
|
1083
1090
|
}
|
|
1084
1091
|
return cloned;
|
|
1085
1092
|
}
|
|
1086
|
-
function normalizeModelSugars(m, extended
|
|
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).
|
|
1104
|
-
|
|
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).
|
|
1166
|
-
// `deployments:` list
|
|
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(
|
|
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
|
|
1760
|
-
const
|
|
1761
|
-
|
|
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.
|
|
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
|
|
1827
|
-
//
|
|
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
|
|
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
|
|
1927
|
-
// expression warning
|
|
1928
|
-
//
|
|
1929
|
-
//
|
|
1930
|
-
//
|
|
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 =
|
|
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
|
|
2100
|
-
//
|
|
2101
|
-
//
|
|
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
|
|
2107
|
+
// materialized. Only a concrete entity's source is parsed.
|
|
2122
2108
|
const parsedSource = ds.source !== undefined ?
|
|
2123
|
-
parseSource(ds.source, opts, ctxLabel, warnings,
|
|
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(
|
|
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
|
-
//
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
2842
|
-
const
|
|
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`,
|
|
3099
|
-
//
|
|
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,
|
|
3123
|
-
return `${project}.${catalog}.${
|
|
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,
|
|
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
|
-
// `
|
|
3238
|
-
// names `kcmd import`, which completes a model file's
|
|
3239
|
-
// profile file's.
|
|
3240
|
-
function parseSource(source, opts, ctx, warnings,
|
|
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
|
-
|
|
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
|
-
//
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
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
|
|
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
|
|
3341
|
-
return `'${short}'${
|
|
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,
|
|
3558
|
+
loaded = loadModels(doc.text, opts);
|
|
3423
3559
|
}
|
|
3424
3560
|
catch (err) {
|
|
3425
3561
|
if (!(err instanceof SemanticModelError))
|