@google-cloud/kcmd 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -20
- package/build/ts/kcmd/cli.js +6164 -5631
- 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 +7 -9
- 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_bigquery.js +6 -13
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +10 -35
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +105 -162
- package/build/ts/kcmd/semantic/deploy_spanner.js +7 -14
- package/build/ts/kcmd/semantic/deployment_target.d.ts +1 -4
- package/build/ts/kcmd/semantic/deployment_target.js +6 -62
- 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 +531 -111
- package/build/ts/kcmd/semantic/ir.d.ts +2 -4
- package/build/ts/kcmd/semantic/kc_converter.d.ts +7 -1
- package/build/ts/kcmd/semantic/kc_converter.js +32 -27
- package/build/ts/kcmd/semantic/kc_custom_types.d.ts +5 -12
- package/build/ts/kcmd/semantic/kc_custom_types.js +43 -114
- 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 +282 -171
- package/build/ts/kcmd/semantic/osi_converter.js +14 -65
- 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 +4 -12
- package/build/ts/kcmd/semantic/runtime/runtime.js +4 -21
- package/build/ts/kcmd/semantic/runtime/store.d.ts +14 -19
- package/build/ts/kcmd/semantic/runtime/store.js +21 -37
- 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 +467 -126
- 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,13 +50,13 @@ export type ChosenStore = SpannerStore | AlloyDbDatabase | BigQueryStore;
|
|
|
50
50
|
* `named` is set when the model's deployments name a target and still no
|
|
51
51
|
* store results, so a caller that runs against the store fails. `rejected` is
|
|
52
52
|
* set with it when the deployments give no store whatever the model is used
|
|
53
|
-
* for: a target in no accepted form in any deployment,
|
|
54
|
-
*
|
|
55
|
-
*
|
|
53
|
+
* for: a target in no accepted form in any deployment, or several targets for
|
|
54
|
+
* the profile. `named` alone means the model binds a table outside its Spanner
|
|
55
|
+
* or AlloyDB store.
|
|
56
56
|
*
|
|
57
57
|
* Neither is set when the profile names no store, unless a target is in no
|
|
58
|
-
* accepted form
|
|
59
|
-
*
|
|
58
|
+
* accepted form. Neither is set either when a BigQuery model binds a table
|
|
59
|
+
* outside the graph's dataset.
|
|
60
60
|
*/
|
|
61
61
|
export interface StoreError {
|
|
62
62
|
error: string;
|
|
@@ -67,21 +67,18 @@ export interface StoreError {
|
|
|
67
67
|
* The store a model runs against under binding profile `profile`, which
|
|
68
68
|
* defaults to `default`, the model file's own bindings. It is the one the
|
|
69
69
|
* deployments for that profile name: those that name it in `profile`, or that
|
|
70
|
-
* name none or `default` for `default`.
|
|
71
|
-
*
|
|
72
|
-
* With several targets, the store is the single operational one, Spanner or
|
|
73
|
-
* AlloyDB.
|
|
70
|
+
* name none or `default` for `default`. With several targets, the store is the
|
|
71
|
+
* single operational one, Spanner or AlloyDB.
|
|
74
72
|
*
|
|
75
|
-
* Fails
|
|
73
|
+
* Fails five ways:
|
|
76
74
|
*
|
|
77
|
-
* 1. A
|
|
78
|
-
* 2. A target names no supported destination. Push checks the target of every
|
|
75
|
+
* 1. A target names no supported destination. Push checks the target of every
|
|
79
76
|
* deployment, so this fails the store under every profile.
|
|
80
|
-
*
|
|
81
|
-
*
|
|
77
|
+
* 2. No deployment uses the profile.
|
|
78
|
+
* 3. There are several targets and no single operational one, so which store
|
|
82
79
|
* is meant is ambiguous.
|
|
83
|
-
*
|
|
84
|
-
*
|
|
80
|
+
* 4. There is no target, so there is nothing to run against.
|
|
81
|
+
* 5. The model binds a table outside the store. For a Spanner or AlloyDB
|
|
85
82
|
* store, a write would land in the wrong place, and the error is named. A
|
|
86
83
|
* BigQuery graph may read tables in any dataset, so
|
|
87
84
|
* for a BigQuery store the error is not named.
|
|
@@ -89,9 +86,7 @@ export interface StoreError {
|
|
|
89
86
|
export declare function resolveStore(model: SemanticModel, profile?: string): ChosenStore | StoreError;
|
|
90
87
|
/**
|
|
91
88
|
* The targets binding profile `profile` uses, as resolveStore reads them: its
|
|
92
|
-
* deployments' targets
|
|
93
|
-
* the model has no `deployments` list. Empty when no deployment is for the
|
|
94
|
-
* profile. Throws when a GOOGLE block's data is not valid JSON.
|
|
89
|
+
* deployments' targets. Empty when no deployment is for the profile.
|
|
95
90
|
*/
|
|
96
91
|
export declare function profileTargets(model: SemanticModel, profile: string): string[];
|
|
97
92
|
/**
|
|
@@ -101,21 +101,18 @@ function strayBindings(model, store) {
|
|
|
101
101
|
* The store a model runs against under binding profile `profile`, which
|
|
102
102
|
* defaults to `default`, the model file's own bindings. It is the one the
|
|
103
103
|
* deployments for that profile name: those that name it in `profile`, or that
|
|
104
|
-
* name none or `default` for `default`.
|
|
105
|
-
*
|
|
106
|
-
* With several targets, the store is the single operational one, Spanner or
|
|
107
|
-
* AlloyDB.
|
|
104
|
+
* name none or `default` for `default`. With several targets, the store is the
|
|
105
|
+
* single operational one, Spanner or AlloyDB.
|
|
108
106
|
*
|
|
109
|
-
* Fails
|
|
107
|
+
* Fails five ways:
|
|
110
108
|
*
|
|
111
|
-
* 1. A
|
|
112
|
-
* 2. A target names no supported destination. Push checks the target of every
|
|
109
|
+
* 1. A target names no supported destination. Push checks the target of every
|
|
113
110
|
* deployment, so this fails the store under every profile.
|
|
114
|
-
*
|
|
115
|
-
*
|
|
111
|
+
* 2. No deployment uses the profile.
|
|
112
|
+
* 3. There are several targets and no single operational one, so which store
|
|
116
113
|
* is meant is ambiguous.
|
|
117
|
-
*
|
|
118
|
-
*
|
|
114
|
+
* 4. There is no target, so there is nothing to run against.
|
|
115
|
+
* 5. The model binds a table outside the store. For a Spanner or AlloyDB
|
|
119
116
|
* store, a write would land in the wrong place, and the error is named. A
|
|
120
117
|
* BigQuery graph may read tables in any dataset, so
|
|
121
118
|
* for a BigQuery store the error is not named.
|
|
@@ -123,15 +120,7 @@ function strayBindings(model, store) {
|
|
|
123
120
|
function resolveStore(model, profile = resolve_profiles_1.DEFAULT_PROFILE) {
|
|
124
121
|
// Push checks the target of every deployment the model declares, so the
|
|
125
122
|
// targets of every deployment are read before the profile's are picked.
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
all = (0, deployment_target_1.googleDeploymentTargets)(model);
|
|
129
|
-
}
|
|
130
|
-
catch (err) {
|
|
131
|
-
// It throws when a GOOGLE block's data is not valid JSON. Push reports
|
|
132
|
-
// that as an error, so the store fails.
|
|
133
|
-
return { error: `${err?.message || err}`, named: true, rejected: true };
|
|
134
|
-
}
|
|
123
|
+
const all = (0, deployment_target_1.googleDeploymentTargets)(model);
|
|
135
124
|
// The loader reads a vanilla model's `deployments` out of its GOOGLE block,
|
|
136
125
|
// so a message names the block's `deployments`, as push does.
|
|
137
126
|
const vanilla = model.version === loader_1.OSSIE_VERSION;
|
|
@@ -141,17 +130,19 @@ function resolveStore(model, profile = resolve_profiles_1.DEFAULT_PROFILE) {
|
|
|
141
130
|
// A target in no accepted form is reported, alone or beside one that gives
|
|
142
131
|
// a store, whichever profile its deployment is for.
|
|
143
132
|
if (all.malformed.length) {
|
|
144
|
-
const unparsed = model.deployments
|
|
145
|
-
|
|
146
|
-
(0, deployment_target_1.
|
|
147
|
-
|
|
133
|
+
const unparsed = model.deployments.find(d => d.target === all.malformed[0]);
|
|
134
|
+
return {
|
|
135
|
+
error: (0, deployment_target_1.deploymentTargetError)(model, unparsed),
|
|
136
|
+
named: true,
|
|
137
|
+
rejected: true,
|
|
138
|
+
};
|
|
148
139
|
}
|
|
149
140
|
const used = deploymentsFor(model, profile);
|
|
150
141
|
if ('error' in used)
|
|
151
142
|
return used;
|
|
152
143
|
const scoped = used.model;
|
|
153
|
-
// The profile's targets are among those read above
|
|
154
|
-
//
|
|
144
|
+
// The profile's targets are among those read above, so none of them is in no
|
|
145
|
+
// accepted form.
|
|
155
146
|
const targets = (0, deployment_target_1.googleDeploymentTargets)(scoped);
|
|
156
147
|
const { spanner, alloyDb, bigQuery } = targets;
|
|
157
148
|
const declared = spanner.length + alloyDb.length + bigQuery.length;
|
|
@@ -202,9 +193,7 @@ function resolveStore(model, profile = resolve_profiles_1.DEFAULT_PROFILE) {
|
|
|
202
193
|
return store;
|
|
203
194
|
}
|
|
204
195
|
// The model with only the deployments `profile` uses, or why there are none.
|
|
205
|
-
// A model with no `deployments` list
|
|
206
|
-
// for `default`. An empty list names no deployment. Either way, any other
|
|
207
|
-
// profile has none.
|
|
196
|
+
// A model with no or empty `deployments` list names no deployment.
|
|
208
197
|
function deploymentsFor(model, profile) {
|
|
209
198
|
const nothingToRun = `so there is no database to run it against`;
|
|
210
199
|
const namedProfile = `binding profile '${profile}' is a named profile, ` +
|
|
@@ -235,21 +224,16 @@ function deploymentsFor(model, profile) {
|
|
|
235
224
|
}
|
|
236
225
|
/**
|
|
237
226
|
* The targets binding profile `profile` uses, as resolveStore reads them: its
|
|
238
|
-
* deployments' targets
|
|
239
|
-
* the model has no `deployments` list. Empty when no deployment is for the
|
|
240
|
-
* profile. Throws when a GOOGLE block's data is not valid JSON.
|
|
227
|
+
* deployments' targets. Empty when no deployment is for the profile.
|
|
241
228
|
*/
|
|
242
229
|
function profileTargets(model, profile) {
|
|
243
230
|
const used = deploymentsFor(model, profile);
|
|
244
231
|
return 'error' in used ? [] : (0, deployment_target_1.googleDeploymentTargets)(used.model).uris;
|
|
245
232
|
}
|
|
246
|
-
// The deployments whose targets are `uris`, for a message
|
|
247
|
-
// the targets come from a GOOGLE block, which names none. `profile` is the
|
|
233
|
+
// The deployments whose targets are `uris`, for a message. `profile` is the
|
|
248
234
|
// profile they are for.
|
|
249
235
|
function deploymentNames(model, uris, profile) {
|
|
250
|
-
const deployments = model.deployments;
|
|
251
|
-
if (!deployments)
|
|
252
|
-
return '';
|
|
236
|
+
const deployments = model.deployments ?? [];
|
|
253
237
|
const names = uris.map(uri => deployments.find(d => d.target === uri)?.name ?? uri);
|
|
254
238
|
return ` for profile '${profile}' (${names.join(', ')})`;
|
|
255
239
|
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
import type { SpannerClient } from '../gcp/spanner';
|
|
1
2
|
import { EntryLookup } from './catalog_entry';
|
|
2
3
|
export interface SourceDialects {
|
|
3
4
|
dialects: Map<string, string>;
|
|
4
5
|
databases: Map<string, string>;
|
|
5
6
|
errors: string[];
|
|
7
|
+
unread: string[];
|
|
6
8
|
}
|
|
7
|
-
export declare function sourceDialects(texts: Iterable<string>, catalog: EntryLookup, { thirdParty }?: {
|
|
8
|
-
thirdParty?: boolean
|
|
9
|
+
export declare function sourceDialects(texts: Iterable<string>, catalog: EntryLookup, { thirdParty, spanner }?: {
|
|
10
|
+
thirdParty?: boolean;
|
|
11
|
+
spanner?: Pick<SpannerClient, 'getDatabase'>;
|
|
9
12
|
}): Promise<SourceDialects>;
|
|
13
|
+
export declare function tableFqnParts(fqn: string): {
|
|
14
|
+
prefix: string;
|
|
15
|
+
parts: string[];
|
|
16
|
+
} | undefined;
|
|
10
17
|
export declare function sourcesToLookUp(texts: Iterable<string>): string[];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// The dialect of a Cloud SQL table written as its resource name, or of a
|
|
3
3
|
// third-party table written as its entry name, read from the table's
|
|
4
|
-
// Knowledge Catalog entry.
|
|
4
|
+
// Knowledge Catalog entry, and the dialect of a Spanner table's database.
|
|
5
5
|
//
|
|
6
6
|
// The resource name cannot say the dialect: a MySQL table has no schema, but a
|
|
7
7
|
// PostgreSQL table and a SQL Server table have the same shape. Dataplex keeps an
|
|
@@ -16,9 +16,12 @@
|
|
|
16
16
|
// fullyQualifiedName, so its parts say whether the entry is a table's or, for
|
|
17
17
|
// example, a schema's.
|
|
18
18
|
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
19
|
+
// Knowledge Catalog's Spanner entries leave their dialect empty, so a Spanner
|
|
20
|
+
// table's dialect is its database's `databaseDialect`, read by databases.get.
|
|
21
|
+
//
|
|
22
|
+
// This is the only place kcmd looks up a source's dialect. A command looks
|
|
23
|
+
// every such source up once, before it loads the model, and passes the
|
|
24
|
+
// dialects to the load (LoadOptions.sourceDialects).
|
|
22
25
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
23
26
|
if (k2 === undefined) k2 = k;
|
|
24
27
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -54,10 +57,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
54
57
|
})();
|
|
55
58
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
56
59
|
exports.sourceDialects = sourceDialects;
|
|
60
|
+
exports.tableFqnParts = tableFqnParts;
|
|
57
61
|
exports.sourcesToLookUp = sourcesToLookUp;
|
|
58
62
|
const yaml = __importStar(require("yaml"));
|
|
59
63
|
const loader_1 = require("./loader");
|
|
60
64
|
const catalog_entry_1 = require("./catalog_entry");
|
|
65
|
+
const parameters_1 = require("./parameters");
|
|
61
66
|
const yaml_options_1 = require("./yaml_options");
|
|
62
67
|
// The dialect each entrySource.system gives, in lower case.
|
|
63
68
|
const DIALECT_OF_ENTRY_SYSTEM = new Map([...loader_1.DIALECT_OF_FQN_PREFIX, ['postgres', 'POSTGRES']]);
|
|
@@ -70,14 +75,16 @@ const TABLE_FQN_PARTS = new Map([
|
|
|
70
75
|
['postgresql', ['host', 'database', 'schema', 'table']],
|
|
71
76
|
['sqlserver', ['host', 'database', 'schema', 'table']],
|
|
72
77
|
]);
|
|
73
|
-
// Looks up the dialect of every Cloud SQL and
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
// `
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
//
|
|
78
|
+
// Looks up, once each and in parallel, the dialect of every Cloud SQL and
|
|
79
|
+
// third-party table the files `texts` write (see sourcesToLookUp), and of every
|
|
80
|
+
// Spanner database they name. Without `spanner`, a Spanner database gives
|
|
81
|
+
// SPANNER, the dialect kcmd's Spanner store speaks, with no call.
|
|
82
|
+
// `thirdParty: false` skips third-party tables. Never throws: a failed lookup
|
|
83
|
+
// goes to `errors`.
|
|
84
|
+
async function sourceDialects(texts, catalog, { thirdParty = true, spanner } = {}) {
|
|
85
|
+
// Keyed as dialectLookupKey gives, so both spellings of one table, and the
|
|
86
|
+
// tables of one Spanner database, are one lookup. A database's lookup names
|
|
87
|
+
// the first of its tables.
|
|
81
88
|
const lookups = new Map();
|
|
82
89
|
for (const source of sourcesToLookUp(texts)) {
|
|
83
90
|
const table = (0, loader_1.cloudSqlTableOf)(source);
|
|
@@ -87,32 +94,42 @@ async function sourceDialects(texts, catalog, { thirdParty = true } = {}) {
|
|
|
87
94
|
const entry = thirdParty ? (0, loader_1.thirdPartyEntryOf)(source) : undefined;
|
|
88
95
|
if (entry)
|
|
89
96
|
lookups.set(entry.name, () => thirdPartyDialect(catalog, entry));
|
|
97
|
+
const database = (0, loader_1.spannerDatabaseOf)(source);
|
|
98
|
+
if (database && !lookups.has(database)) {
|
|
99
|
+
lookups.set(database, spanner ? () => spannerDialect(spanner, database, source) :
|
|
100
|
+
async () => ({ dialect: 'SPANNER' }));
|
|
101
|
+
}
|
|
90
102
|
}
|
|
91
103
|
const dialects = new Map();
|
|
92
104
|
const databases = new Map();
|
|
93
105
|
const errors = [];
|
|
106
|
+
const unread = [];
|
|
94
107
|
await Promise.all([...lookups].map(async ([key, lookUp]) => {
|
|
95
108
|
const got = await lookUp();
|
|
96
109
|
if ('error' in got)
|
|
97
110
|
errors.push(got.error);
|
|
98
111
|
else
|
|
99
112
|
dialects.set(key, got.dialect);
|
|
113
|
+
if ('unread' in got)
|
|
114
|
+
unread.push(got.error);
|
|
100
115
|
if ('database' in got && got.database)
|
|
101
116
|
databases.set(key, got.database);
|
|
102
117
|
}));
|
|
103
118
|
errors.sort();
|
|
104
|
-
|
|
119
|
+
unread.sort();
|
|
120
|
+
return { dialects, databases, errors, unread };
|
|
105
121
|
}
|
|
106
122
|
// The dialect of a Cloud SQL table, by its entry's type.
|
|
107
123
|
async function cloudSqlDialect(catalog, table) {
|
|
108
124
|
const at = `source '${table.resourceName}'`;
|
|
109
125
|
const res = await (0, catalog_entry_1.lookupSystemEntry)(catalog, 'cloudsql', table.project, table.location, table.resourceName);
|
|
110
126
|
if ('error' in res)
|
|
111
|
-
return { error: `${at}: ${res.error}
|
|
127
|
+
return { error: `${at}: ${res.error}`, unread: true };
|
|
112
128
|
// Dataplex answers 403 both when the entry does not exist and when the
|
|
113
129
|
// caller may not read it, so the message names both causes.
|
|
114
130
|
if ('unreadable' in res) {
|
|
115
131
|
return {
|
|
132
|
+
unread: true,
|
|
116
133
|
error: `${at}: kcmd reads a Cloud SQL table's dialect from its ` +
|
|
117
134
|
`Knowledge Catalog entry, and could not read it. Turn on Dataplex ` +
|
|
118
135
|
`integration for instance '${table.instance}' (gcloud sql ` +
|
|
@@ -131,6 +148,27 @@ async function cloudSqlDialect(catalog, table) {
|
|
|
131
148
|
`not a Cloud SQL table or view type kcmd knows (${Object.keys(loader_1.DIALECT_OF_ENTRY_TYPE).join(', ')}).`,
|
|
132
149
|
};
|
|
133
150
|
}
|
|
151
|
+
// The dialect of the Spanner database `database`, which `source` names a
|
|
152
|
+
// table of, by its databaseDialect.
|
|
153
|
+
async function spannerDialect(spanner, database, source) {
|
|
154
|
+
const [, , project, , instance, , name] = database.split('/');
|
|
155
|
+
const failed = (why) => ({
|
|
156
|
+
unread: true,
|
|
157
|
+
error: `source '${source}': kcmd reads its Spanner database's SQL ` +
|
|
158
|
+
`dialect, and could not read database ${project}/${instance}/${name}: ${why}`,
|
|
159
|
+
});
|
|
160
|
+
try {
|
|
161
|
+
const res = await spanner.getDatabase(project, instance, name);
|
|
162
|
+
if (res.status !== 200) {
|
|
163
|
+
return failed((0, parameters_1.storeMessage)(res.message, res.status));
|
|
164
|
+
}
|
|
165
|
+
const postgres = res.result?.databaseDialect === 'POSTGRESQL';
|
|
166
|
+
return { dialect: postgres ? 'POSTGRES' : 'SPANNER' };
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
return failed(err?.message ?? String(err));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
134
172
|
// The dialect of a third-party table, by its entry's fullyQualifiedName or
|
|
135
173
|
// entrySource.system, and the database the fullyQualifiedName names. An entry
|
|
136
174
|
// whose fullyQualifiedName names no table or view is rejected.
|
|
@@ -138,9 +176,10 @@ async function thirdPartyDialect(catalog, { project, location, name }) {
|
|
|
138
176
|
const at = `source '${name}'`;
|
|
139
177
|
const res = await (0, catalog_entry_1.read)('looking up its Knowledge Catalog entry', () => catalog.lookupEntry(project, location, name));
|
|
140
178
|
if ('error' in res)
|
|
141
|
-
return { error: `${at}: ${res.error}
|
|
179
|
+
return { error: `${at}: ${res.error}`, unread: true };
|
|
142
180
|
if ('unreadable' in res) {
|
|
143
181
|
return {
|
|
182
|
+
unread: true,
|
|
144
183
|
error: `${at} names no table Knowledge Catalog knows about. The entry ` +
|
|
145
184
|
`may not exist, or you may not be allowed to read it. kcmd reads a ` +
|
|
146
185
|
`third-party table's dialect from its entry, so add the table to ` +
|
|
@@ -177,14 +216,12 @@ async function thirdPartyDialect(catalog, { project, location, name }) {
|
|
|
177
216
|
`fullyQualifiedName.`,
|
|
178
217
|
};
|
|
179
218
|
}
|
|
180
|
-
// The
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
// `databricks:view:` name as the same table as the `databricks:` name.
|
|
187
|
-
function databaseOfFqn(fqn) {
|
|
219
|
+
// The prefix of the fullyQualifiedName `fqn` of a third-party table or view,
|
|
220
|
+
// and its unquoted parts, such as `snowflake` and ['acme', 'retail', 'public',
|
|
221
|
+
// 'orders'] for `snowflake:acme.retail.public.orders`. Undefined when `fqn`
|
|
222
|
+
// has another number of parts than TABLE_FQN_PARTS gives, or a subtype other
|
|
223
|
+
// than Databricks' `table:` and `view:`.
|
|
224
|
+
function tableFqnParts(fqn) {
|
|
188
225
|
// The subtype holds no backtick, so a colon inside a quoted host and port
|
|
189
226
|
// stays in the path. A colon in an unquoted database, schema or table
|
|
190
227
|
// name stays in its segment when it comes after the name's first backtick.
|
|
@@ -195,19 +232,31 @@ function databaseOfFqn(fqn) {
|
|
|
195
232
|
(prefix === 'databricks' && (subtype === 'table' || subtype === 'view'));
|
|
196
233
|
if (!parts || !tableSubtype || seg.length !== parts.length)
|
|
197
234
|
return undefined;
|
|
235
|
+
return { prefix, parts: seg };
|
|
236
|
+
}
|
|
237
|
+
// The database the fullyQualifiedName `fqn` of a third-party table or view
|
|
238
|
+
// names: a Snowflake account, a MySQL or SQL Server host, a PostgreSQL host
|
|
239
|
+
// and database, or a Databricks metastore. Each account and host is in lower
|
|
240
|
+
// case. Undefined when `fqn` names no table or view.
|
|
241
|
+
function databaseOfFqn(fqn) {
|
|
242
|
+
const table = tableFqnParts(fqn);
|
|
243
|
+
if (!table)
|
|
244
|
+
return undefined;
|
|
245
|
+
const { prefix, parts } = table;
|
|
198
246
|
const n = prefix === 'postgresql' ? 2 : 1;
|
|
199
247
|
// A Snowflake account and a host name ignore letter case.
|
|
200
248
|
if (prefix !== 'databricks')
|
|
201
|
-
|
|
202
|
-
return [prefix, ...
|
|
249
|
+
parts[0] = parts[0].toLowerCase();
|
|
250
|
+
return [prefix, ...parts.slice(0, n)].join('/');
|
|
203
251
|
}
|
|
204
|
-
// Every Cloud SQL table and third-party entry the model and
|
|
205
|
-
// `texts` write. A Cloud SQL table counts as a resource name or
|
|
206
|
-
// anywhere, JSON strings included.
|
|
207
|
-
// the YAML or in a GOOGLE block's JSON
|
|
208
|
-
// an action parameter's default or
|
|
209
|
-
// from the parsed YAML, so a name in a
|
|
210
|
-
// does not parse collects nothing; the
|
|
252
|
+
// Every Cloud SQL table, Spanner table and third-party entry the model and
|
|
253
|
+
// profile files `texts` write. A Cloud SQL table counts as a resource name or
|
|
254
|
+
// resource URI anywhere, JSON strings included. A Spanner table or an entry
|
|
255
|
+
// name counts as a `source` value in the YAML or in a GOOGLE block's JSON
|
|
256
|
+
// `data`, unless it sits in ai_context, an action parameter's default or
|
|
257
|
+
// another vendor's block. Values are read from the parsed YAML, so a name in a
|
|
258
|
+
// comment is not collected. A file that does not parse collects nothing; the
|
|
259
|
+
// load reports it.
|
|
211
260
|
function sourcesToLookUp(texts) {
|
|
212
261
|
const found = new Set();
|
|
213
262
|
// `key` names the member that holds `value`. `opaque` marks a value whose
|
|
@@ -217,7 +266,8 @@ function sourcesToLookUp(texts) {
|
|
|
217
266
|
const visit = (value, key, opaque = false) => {
|
|
218
267
|
if (typeof value === 'string') {
|
|
219
268
|
if ((0, loader_1.cloudSqlTableOf)(value) ||
|
|
220
|
-
(key === 'source' && !opaque &&
|
|
269
|
+
(key === 'source' && !opaque &&
|
|
270
|
+
((0, loader_1.thirdPartyEntryOf)(value) || (0, loader_1.spannerDatabaseOf)(value)))) {
|
|
221
271
|
found.add(value.trim());
|
|
222
272
|
}
|
|
223
273
|
else if (/^\s*[[{]/.test(value)) {
|
|
@@ -6,7 +6,7 @@ import type { SemanticModel } from './ir';
|
|
|
6
6
|
export interface SourceReaders {
|
|
7
7
|
catalog: EntryLookup;
|
|
8
8
|
bigquery: Pick<BigQueryClient, 'getTable'>;
|
|
9
|
-
biglake: Pick<BigLakeClient, 'getCatalog'>;
|
|
9
|
+
biglake: Pick<BigLakeClient, 'getCatalog' | 'getHiveCatalog'>;
|
|
10
10
|
spanner: Pick<SpannerClient, 'getInstance' | 'getInstanceConfig'>;
|
|
11
11
|
}
|
|
12
12
|
export declare function sourceEntryErrors(model: Pick<SemanticModel, 'name' | 'entities' | 'profiles'>, readers: SourceReaders): Promise<string[]>;
|