@google-cloud/kcmd 0.0.0-stage → 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/LICENSE +203 -0
- package/README.md +264 -2
- package/build/ts/kcmd/cli.js +20252 -0
- package/build/ts/kcmd/gcp/alloydb.d.ts +12 -0
- package/build/ts/kcmd/gcp/alloydb.js +70 -0
- package/build/ts/kcmd/gcp/api.d.ts +29 -0
- package/build/ts/kcmd/gcp/api.js +162 -0
- package/build/ts/kcmd/gcp/biglake.d.ts +18 -0
- package/build/ts/kcmd/gcp/biglake.js +59 -0
- package/build/ts/kcmd/gcp/bigquery.d.ts +57 -0
- package/build/ts/kcmd/gcp/bigquery.js +134 -0
- package/build/ts/kcmd/gcp/context.d.ts +10 -0
- package/build/ts/kcmd/gcp/context.js +74 -0
- package/build/ts/kcmd/gcp/crm.d.ts +13 -0
- package/build/ts/kcmd/gcp/crm.js +91 -0
- package/build/ts/kcmd/gcp/dataplex.d.ts +147 -0
- package/build/ts/kcmd/gcp/dataplex.js +406 -0
- package/build/ts/kcmd/gcp/index.d.ts +3 -0
- package/build/ts/kcmd/gcp/index.js +21 -0
- package/build/ts/kcmd/gcp/spanner.d.ts +73 -0
- package/build/ts/kcmd/gcp/spanner.js +126 -0
- package/build/ts/kcmd/index.d.ts +7 -0
- package/build/ts/kcmd/index.js +46 -0
- package/build/ts/kcmd/layout.d.ts +16 -0
- package/build/ts/kcmd/layout.js +31 -0
- package/build/ts/kcmd/layouts/documents.d.ts +24 -0
- package/build/ts/kcmd/layouts/documents.js +264 -0
- package/build/ts/kcmd/layouts/okf.d.ts +31 -0
- package/build/ts/kcmd/layouts/okf.js +386 -0
- package/build/ts/kcmd/layouts/semantic-model.d.ts +74 -0
- package/build/ts/kcmd/layouts/semantic-model.js +621 -0
- package/build/ts/kcmd/layouts/standard.d.ts +13 -0
- package/build/ts/kcmd/layouts/standard.js +108 -0
- package/build/ts/kcmd/manifest.d.ts +34 -0
- package/build/ts/kcmd/manifest.js +262 -0
- package/build/ts/kcmd/metadata.d.ts +25 -0
- package/build/ts/kcmd/metadata.js +8 -0
- package/build/ts/kcmd/okf/catalog_residue.d.ts +14 -0
- package/build/ts/kcmd/okf/catalog_residue.js +124 -0
- package/build/ts/kcmd/okf/concept.d.ts +14 -0
- package/build/ts/kcmd/okf/concept.js +110 -0
- package/build/ts/kcmd/okf/concept_to_entry.d.ts +10 -0
- package/build/ts/kcmd/okf/concept_to_entry.js +281 -0
- package/build/ts/kcmd/okf/init_okf_workspace.d.ts +15 -0
- package/build/ts/kcmd/okf/init_okf_workspace.js +113 -0
- package/build/ts/kcmd/okf/links.d.ts +23 -0
- package/build/ts/kcmd/okf/links.js +468 -0
- package/build/ts/kcmd/okf/okf_metadata.d.ts +12 -0
- package/build/ts/kcmd/okf/okf_metadata.js +99 -0
- package/build/ts/kcmd/semantic/bigquery.d.ts +22 -0
- package/build/ts/kcmd/semantic/bigquery.js +469 -0
- package/build/ts/kcmd/semantic/binding.d.ts +3 -0
- package/build/ts/kcmd/semantic/binding.js +92 -0
- package/build/ts/kcmd/semantic/catalog_entry.d.ts +24 -0
- package/build/ts/kcmd/semantic/catalog_entry.js +35 -0
- package/build/ts/kcmd/semantic/converters/owl/convert.d.ts +26 -0
- package/build/ts/kcmd/semantic/converters/owl/convert.js +41 -0
- package/build/ts/kcmd/semantic/converters/owl/model.d.ts +98 -0
- package/build/ts/kcmd/semantic/converters/owl/model.js +30 -0
- package/build/ts/kcmd/semantic/converters/owl/parse.d.ts +11 -0
- package/build/ts/kcmd/semantic/converters/owl/parse.js +634 -0
- package/build/ts/kcmd/semantic/converters/owl/to_ir.d.ts +19 -0
- package/build/ts/kcmd/semantic/converters/owl/to_ir.js +390 -0
- package/build/ts/kcmd/semantic/deploy_bigquery.d.ts +20 -0
- package/build/ts/kcmd/semantic/deploy_bigquery.js +226 -0
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +104 -0
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +1367 -0
- package/build/ts/kcmd/semantic/deploy_spanner.d.ts +15 -0
- package/build/ts/kcmd/semantic/deploy_spanner.js +150 -0
- package/build/ts/kcmd/semantic/deployment_target.d.ts +93 -0
- package/build/ts/kcmd/semantic/deployment_target.js +379 -0
- package/build/ts/kcmd/semantic/graph_ddl.d.ts +44 -0
- package/build/ts/kcmd/semantic/graph_ddl.js +656 -0
- package/build/ts/kcmd/semantic/import_model.d.ts +72 -0
- package/build/ts/kcmd/semantic/import_model.js +1698 -0
- package/build/ts/kcmd/semantic/ir.d.ts +589 -0
- package/build/ts/kcmd/semantic/ir.js +247 -0
- package/build/ts/kcmd/semantic/kc_actions.d.ts +45 -0
- package/build/ts/kcmd/semantic/kc_actions.js +531 -0
- package/build/ts/kcmd/semantic/kc_constraints.d.ts +44 -0
- package/build/ts/kcmd/semantic/kc_constraints.js +279 -0
- package/build/ts/kcmd/semantic/kc_converter.d.ts +37 -0
- package/build/ts/kcmd/semantic/kc_converter.js +839 -0
- package/build/ts/kcmd/semantic/kc_custom_types.d.ts +63 -0
- package/build/ts/kcmd/semantic/kc_custom_types.js +751 -0
- package/build/ts/kcmd/semantic/kc_entries.d.ts +155 -0
- package/build/ts/kcmd/semantic/kc_entries.js +284 -0
- package/build/ts/kcmd/semantic/kc_ids.d.ts +102 -0
- package/build/ts/kcmd/semantic/kc_ids.js +188 -0
- package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +29 -0
- package/build/ts/kcmd/semantic/knowledge_catalog.js +960 -0
- package/build/ts/kcmd/semantic/loader.d.ts +258 -0
- package/build/ts/kcmd/semantic/loader.js +3576 -0
- package/build/ts/kcmd/semantic/names.d.ts +10 -0
- package/build/ts/kcmd/semantic/names.js +85 -0
- package/build/ts/kcmd/semantic/osi_converter.d.ts +60 -0
- package/build/ts/kcmd/semantic/osi_converter.js +910 -0
- package/build/ts/kcmd/semantic/parameters.d.ts +19 -0
- package/build/ts/kcmd/semantic/parameters.js +171 -0
- package/build/ts/kcmd/semantic/pull_kc.d.ts +10 -0
- package/build/ts/kcmd/semantic/pull_kc.js +272 -0
- package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +59 -0
- package/build/ts/kcmd/semantic/resolve_inheritance.js +346 -0
- package/build/ts/kcmd/semantic/resolve_profiles.d.ts +291 -0
- package/build/ts/kcmd/semantic/resolve_profiles.js +2003 -0
- package/build/ts/kcmd/semantic/runtime/agent_tools.d.ts +93 -0
- package/build/ts/kcmd/semantic/runtime/agent_tools.js +243 -0
- package/build/ts/kcmd/semantic/runtime/dialect.d.ts +13 -0
- package/build/ts/kcmd/semantic/runtime/dialect.js +39 -0
- package/build/ts/kcmd/semantic/runtime/runtime.d.ts +88 -0
- package/build/ts/kcmd/semantic/runtime/runtime.js +301 -0
- package/build/ts/kcmd/semantic/runtime/store.d.ts +112 -0
- package/build/ts/kcmd/semantic/runtime/store.js +344 -0
- package/build/ts/kcmd/semantic/skills.d.ts +110 -0
- package/build/ts/kcmd/semantic/skills.js +697 -0
- package/build/ts/kcmd/semantic/source_dialect.d.ts +17 -0
- package/build/ts/kcmd/semantic/source_dialect.js +311 -0
- package/build/ts/kcmd/semantic/source_entries.d.ts +12 -0
- package/build/ts/kcmd/semantic/source_entries.js +341 -0
- package/build/ts/kcmd/semantic/spanner.d.ts +18 -0
- package/build/ts/kcmd/semantic/spanner.js +60 -0
- package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +23 -0
- package/build/ts/kcmd/semantic/sql_expr_utils.js +394 -0
- package/build/ts/kcmd/semantic/sql_identifiers.d.ts +6 -0
- package/build/ts/kcmd/semantic/sql_identifiers.js +267 -0
- package/build/ts/kcmd/semantic/sql_parser.d.ts +47 -0
- package/build/ts/kcmd/semantic/sql_parser.js +400 -0
- package/build/ts/kcmd/semantic/transpile.d.ts +76 -0
- package/build/ts/kcmd/semantic/transpile.js +581 -0
- package/build/ts/kcmd/semantic/validate.d.ts +70 -0
- package/build/ts/kcmd/semantic/validate.js +2266 -0
- package/build/ts/kcmd/semantic/yaml_options.d.ts +6 -0
- package/build/ts/kcmd/semantic/yaml_options.js +58 -0
- package/build/ts/kcmd/semantic/yaml_update.d.ts +26 -0
- package/build/ts/kcmd/semantic/yaml_update.js +782 -0
- package/build/ts/kcmd/snapshot.d.ts +37 -0
- package/build/ts/kcmd/snapshot.js +386 -0
- package/build/ts/kcmd/source.d.ts +31 -0
- package/build/ts/kcmd/source.js +119 -0
- package/build/ts/kcmd/sources/bq-dataset.d.ts +17 -0
- package/build/ts/kcmd/sources/bq-dataset.js +131 -0
- package/build/ts/kcmd/sources/entrygroup.d.ts +21 -0
- package/build/ts/kcmd/sources/entrygroup.js +83 -0
- package/build/ts/kcmd/sources/kb.d.ts +21 -0
- package/build/ts/kcmd/sources/kb.js +82 -0
- package/build/ts/kcmd/sources/semantic-model.d.ts +18 -0
- package/build/ts/kcmd/sources/semantic-model.js +48 -0
- package/build/ts/kcmd/sync.d.ts +35 -0
- package/build/ts/kcmd/sync.js +734 -0
- package/package.json +62 -4
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SpannerClient } from '../gcp/spanner';
|
|
2
|
+
import { EntryLookup } from './catalog_entry';
|
|
3
|
+
export interface SourceDialects {
|
|
4
|
+
dialects: Map<string, string>;
|
|
5
|
+
databases: Map<string, string>;
|
|
6
|
+
errors: string[];
|
|
7
|
+
unread: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function sourceDialects(texts: Iterable<string>, catalog: EntryLookup, { thirdParty, spanner }?: {
|
|
10
|
+
thirdParty?: boolean;
|
|
11
|
+
spanner?: Pick<SpannerClient, 'getDatabase'>;
|
|
12
|
+
}): Promise<SourceDialects>;
|
|
13
|
+
export declare function tableFqnParts(fqn: string): {
|
|
14
|
+
prefix: string;
|
|
15
|
+
parts: string[];
|
|
16
|
+
} | undefined;
|
|
17
|
+
export declare function sourcesToLookUp(texts: Iterable<string>): string[];
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The dialect of a Cloud SQL table written as its resource name, or of a
|
|
3
|
+
// third-party table written as its entry name, read from the table's
|
|
4
|
+
// Knowledge Catalog entry, and the dialect of a Spanner table's database.
|
|
5
|
+
//
|
|
6
|
+
// The resource name cannot say the dialect: a MySQL table has no schema, but a
|
|
7
|
+
// PostgreSQL table and a SQL Server table have the same shape. Dataplex keeps an
|
|
8
|
+
// entry for each table of an instance with Dataplex integration on, in the
|
|
9
|
+
// system entry group `@cloudsql`, and the entry's id is the table's resource
|
|
10
|
+
// name. Its entry type names the engine.
|
|
11
|
+
//
|
|
12
|
+
// A third-party table's entry names its system twice: in the prefix of its
|
|
13
|
+
// fullyQualifiedName, which Dataplex checks, and in entrySource.system, which
|
|
14
|
+
// is free text. So the prefix decides, and the system only when the entry has
|
|
15
|
+
// no fullyQualifiedName. Dataplex also checks the rest of the
|
|
16
|
+
// fullyQualifiedName, so its parts say whether the entry is a table's or, for
|
|
17
|
+
// example, a schema's.
|
|
18
|
+
//
|
|
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).
|
|
25
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
26
|
+
if (k2 === undefined) k2 = k;
|
|
27
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
28
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
29
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
30
|
+
}
|
|
31
|
+
Object.defineProperty(o, k2, desc);
|
|
32
|
+
}) : (function(o, m, k, k2) {
|
|
33
|
+
if (k2 === undefined) k2 = k;
|
|
34
|
+
o[k2] = m[k];
|
|
35
|
+
}));
|
|
36
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
37
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
38
|
+
}) : function(o, v) {
|
|
39
|
+
o["default"] = v;
|
|
40
|
+
});
|
|
41
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
42
|
+
var ownKeys = function(o) {
|
|
43
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
44
|
+
var ar = [];
|
|
45
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
46
|
+
return ar;
|
|
47
|
+
};
|
|
48
|
+
return ownKeys(o);
|
|
49
|
+
};
|
|
50
|
+
return function (mod) {
|
|
51
|
+
if (mod && mod.__esModule) return mod;
|
|
52
|
+
var result = {};
|
|
53
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
54
|
+
__setModuleDefault(result, mod);
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
57
|
+
})();
|
|
58
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
59
|
+
exports.sourceDialects = sourceDialects;
|
|
60
|
+
exports.tableFqnParts = tableFqnParts;
|
|
61
|
+
exports.sourcesToLookUp = sourcesToLookUp;
|
|
62
|
+
const yaml = __importStar(require("yaml"));
|
|
63
|
+
const loader_1 = require("./loader");
|
|
64
|
+
const catalog_entry_1 = require("./catalog_entry");
|
|
65
|
+
const parameters_1 = require("./parameters");
|
|
66
|
+
const yaml_options_1 = require("./yaml_options");
|
|
67
|
+
// The dialect each entrySource.system gives, in lower case.
|
|
68
|
+
const DIALECT_OF_ENTRY_SYSTEM = new Map([...loader_1.DIALECT_OF_FQN_PREFIX, ['postgres', 'POSTGRES']]);
|
|
69
|
+
// The parts of a table's fullyQualifiedName after each prefix, as Dataplex
|
|
70
|
+
// reads it. A view's name has the same parts as a table's.
|
|
71
|
+
const TABLE_FQN_PARTS = new Map([
|
|
72
|
+
['snowflake', ['account', 'database', 'schema', 'table']],
|
|
73
|
+
['databricks', ['metastore', 'catalog', 'schema', 'table']],
|
|
74
|
+
['mysql', ['host', 'schema', 'table']],
|
|
75
|
+
['postgresql', ['host', 'database', 'schema', 'table']],
|
|
76
|
+
['sqlserver', ['host', 'database', 'schema', 'table']],
|
|
77
|
+
]);
|
|
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.
|
|
88
|
+
const lookups = new Map();
|
|
89
|
+
for (const source of sourcesToLookUp(texts)) {
|
|
90
|
+
const table = (0, loader_1.cloudSqlTableOf)(source);
|
|
91
|
+
if (table) {
|
|
92
|
+
lookups.set(table.resourceName, () => cloudSqlDialect(catalog, table));
|
|
93
|
+
}
|
|
94
|
+
const entry = thirdParty ? (0, loader_1.thirdPartyEntryOf)(source) : undefined;
|
|
95
|
+
if (entry)
|
|
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
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const dialects = new Map();
|
|
104
|
+
const databases = new Map();
|
|
105
|
+
const errors = [];
|
|
106
|
+
const unread = [];
|
|
107
|
+
await Promise.all([...lookups].map(async ([key, lookUp]) => {
|
|
108
|
+
const got = await lookUp();
|
|
109
|
+
if ('error' in got)
|
|
110
|
+
errors.push(got.error);
|
|
111
|
+
else
|
|
112
|
+
dialects.set(key, got.dialect);
|
|
113
|
+
if ('unread' in got)
|
|
114
|
+
unread.push(got.error);
|
|
115
|
+
if ('database' in got && got.database)
|
|
116
|
+
databases.set(key, got.database);
|
|
117
|
+
}));
|
|
118
|
+
errors.sort();
|
|
119
|
+
unread.sort();
|
|
120
|
+
return { dialects, databases, errors, unread };
|
|
121
|
+
}
|
|
122
|
+
// The dialect of a Cloud SQL table, by its entry's type.
|
|
123
|
+
async function cloudSqlDialect(catalog, table) {
|
|
124
|
+
const at = `source '${table.resourceName}'`;
|
|
125
|
+
const res = await (0, catalog_entry_1.lookupSystemEntry)(catalog, 'cloudsql', table.project, table.location, table.resourceName);
|
|
126
|
+
if ('error' in res)
|
|
127
|
+
return { error: `${at}: ${res.error}`, unread: true };
|
|
128
|
+
// Dataplex answers 403 both when the entry does not exist and when the
|
|
129
|
+
// caller may not read it, so the message names both causes.
|
|
130
|
+
if ('unreadable' in res) {
|
|
131
|
+
return {
|
|
132
|
+
unread: true,
|
|
133
|
+
error: `${at}: kcmd reads a Cloud SQL table's dialect from its ` +
|
|
134
|
+
`Knowledge Catalog entry, and could not read it. Turn on Dataplex ` +
|
|
135
|
+
`integration for instance '${table.instance}' (gcloud sql ` +
|
|
136
|
+
`instances patch ${table.instance} --enable-dataplex-integration ` +
|
|
137
|
+
`--project ${table.project}) and wait for the table to appear in ` +
|
|
138
|
+
`the catalog, or check that you can read the table. If integration ` +
|
|
139
|
+
`is on, check the name: the project ID, the instance's region, and ` +
|
|
140
|
+
`a schema only for PostgreSQL and SQL Server.`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// The type comes back as a full name; its last part says the engine.
|
|
144
|
+
const type = (res.result.entryType ?? '').split('/').pop() ?? '';
|
|
145
|
+
const dialect = loader_1.DIALECT_OF_ENTRY_TYPE[type];
|
|
146
|
+
return dialect ? { dialect } : {
|
|
147
|
+
error: `${at}: its Knowledge Catalog entry has type '${type}', which is ` +
|
|
148
|
+
`not a Cloud SQL table or view type kcmd knows (${Object.keys(loader_1.DIALECT_OF_ENTRY_TYPE).join(', ')}).`,
|
|
149
|
+
};
|
|
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
|
+
}
|
|
172
|
+
// The dialect of a third-party table, by its entry's fullyQualifiedName or
|
|
173
|
+
// entrySource.system, and the database the fullyQualifiedName names. An entry
|
|
174
|
+
// whose fullyQualifiedName names no table or view is rejected.
|
|
175
|
+
async function thirdPartyDialect(catalog, { project, location, name }) {
|
|
176
|
+
const at = `source '${name}'`;
|
|
177
|
+
const res = await (0, catalog_entry_1.read)('looking up its Knowledge Catalog entry', () => catalog.lookupEntry(project, location, name));
|
|
178
|
+
if ('error' in res)
|
|
179
|
+
return { error: `${at}: ${res.error}`, unread: true };
|
|
180
|
+
if ('unreadable' in res) {
|
|
181
|
+
return {
|
|
182
|
+
unread: true,
|
|
183
|
+
error: `${at} names no table Knowledge Catalog knows about. The entry ` +
|
|
184
|
+
`may not exist, or you may not be allowed to read it. kcmd reads a ` +
|
|
185
|
+
`third-party table's dialect from its entry, so add the table to ` +
|
|
186
|
+
`Knowledge Catalog, or fix the source.`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const { fullyQualifiedName: fqn, entrySource } = res.result;
|
|
190
|
+
const system = entrySource?.system;
|
|
191
|
+
const prefix = fqn?.match(/^([a-z]+):/)?.[1] ?? '';
|
|
192
|
+
const dialect = fqn ? loader_1.DIALECT_OF_FQN_PREFIX.get(prefix) :
|
|
193
|
+
DIALECT_OF_ENTRY_SYSTEM.get(system?.toLowerCase() ?? '');
|
|
194
|
+
if (dialect && fqn) {
|
|
195
|
+
const database = databaseOfFqn(fqn);
|
|
196
|
+
if (database)
|
|
197
|
+
return { dialect, database };
|
|
198
|
+
const parts = TABLE_FQN_PARTS.get(prefix).map(p => `<${p}>`).join('.');
|
|
199
|
+
return {
|
|
200
|
+
error: `${at}: its Knowledge Catalog entry has fullyQualifiedName ` +
|
|
201
|
+
`'${fqn}', which names no table or view. A table's ` +
|
|
202
|
+
`fullyQualifiedName has the form ${prefix}:${parts}. Write the ` +
|
|
203
|
+
`entry name of the table's own entry.`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (dialect)
|
|
207
|
+
return { dialect };
|
|
208
|
+
const has = fqn ? `fullyQualifiedName '${fqn}'` :
|
|
209
|
+
system ? `entrySource.system '${system}' and no fullyQualifiedName` :
|
|
210
|
+
'neither a fullyQualifiedName nor an entrySource.system';
|
|
211
|
+
return {
|
|
212
|
+
error: `${at}: kcmd cannot tell the table's SQL dialect from its ` +
|
|
213
|
+
`Knowledge Catalog entry, which has ${has}. kcmd reads the prefix of ` +
|
|
214
|
+
`the entry's fullyQualifiedName (snowflake:, databricks:, mysql:, ` +
|
|
215
|
+
`postgresql: or sqlserver:), or entrySource.system when there is no ` +
|
|
216
|
+
`fullyQualifiedName.`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
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) {
|
|
225
|
+
// The subtype holds no backtick, so a colon inside a quoted host and port
|
|
226
|
+
// stays in the path. A colon in an unquoted database, schema or table
|
|
227
|
+
// name stays in its segment when it comes after the name's first backtick.
|
|
228
|
+
const [, prefix, subtype, path] = fqn.match(/^([a-z]+):(?:([a-z0-9_-]+):)?(.*)$/s) ?? [];
|
|
229
|
+
const parts = TABLE_FQN_PARTS.get(prefix ?? '');
|
|
230
|
+
const seg = (0, loader_1.catalogNameSegments)(path ?? '', true);
|
|
231
|
+
const tableSubtype = subtype === undefined ||
|
|
232
|
+
(prefix === 'databricks' && (subtype === 'table' || subtype === 'view'));
|
|
233
|
+
if (!parts || !tableSubtype || seg.length !== parts.length)
|
|
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;
|
|
246
|
+
const n = prefix === 'postgresql' ? 2 : 1;
|
|
247
|
+
// A Snowflake account and a host name ignore letter case.
|
|
248
|
+
if (prefix !== 'databricks')
|
|
249
|
+
parts[0] = parts[0].toLowerCase();
|
|
250
|
+
return [prefix, ...parts.slice(0, n)].join('/');
|
|
251
|
+
}
|
|
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.
|
|
260
|
+
function sourcesToLookUp(texts) {
|
|
261
|
+
const found = new Set();
|
|
262
|
+
// `key` names the member that holds `value`. `opaque` marks a value whose
|
|
263
|
+
// `source` names no table: one inside ai_context, an action parameter's
|
|
264
|
+
// default or another vendor's block, or in JSON written anywhere but a
|
|
265
|
+
// `data` member.
|
|
266
|
+
const visit = (value, key, opaque = false) => {
|
|
267
|
+
if (typeof value === 'string') {
|
|
268
|
+
if ((0, loader_1.cloudSqlTableOf)(value) ||
|
|
269
|
+
(key === 'source' && !opaque &&
|
|
270
|
+
((0, loader_1.thirdPartyEntryOf)(value) || (0, loader_1.spannerDatabaseOf)(value)))) {
|
|
271
|
+
found.add(value.trim());
|
|
272
|
+
}
|
|
273
|
+
else if (/^\s*[[{]/.test(value)) {
|
|
274
|
+
try {
|
|
275
|
+
visit(JSON.parse(value), undefined, opaque || key !== 'data');
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// Not JSON: an ordinary string.
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else if (Array.isArray(value)) {
|
|
283
|
+
value.forEach(v => visit(v, undefined, opaque));
|
|
284
|
+
}
|
|
285
|
+
else if (value && typeof value === 'object') {
|
|
286
|
+
const vendor = 'vendor_name' in value && !(0, loader_1.isGoogleVendor)(value.vendor_name);
|
|
287
|
+
for (const [k, v] of Object.entries(value)) {
|
|
288
|
+
visit(v, k, opaque || vendor || k === 'ai_context' || k === 'default');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
for (const text of texts) {
|
|
293
|
+
// Parsed as the loader parses, so both read the same values.
|
|
294
|
+
for (const doc of yaml.parseAllDocuments(text, yaml_options_1.YAML_OPTIONS)) {
|
|
295
|
+
if (doc.errors.length)
|
|
296
|
+
continue;
|
|
297
|
+
// Reading the values can still fail: on an alias whose anchor is not
|
|
298
|
+
// set, on more aliases than a load expands, or, while walking them, on
|
|
299
|
+
// an alias that refers to itself, which makes the values cyclic. The
|
|
300
|
+
// load reports that file in its own words, so the rest of it is skipped
|
|
301
|
+
// here rather than stopping the command.
|
|
302
|
+
try {
|
|
303
|
+
visit(doc.toJS());
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return [...found];
|
|
311
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BigLakeClient } from '../gcp/biglake';
|
|
2
|
+
import type { BigQueryClient } from '../gcp/bigquery';
|
|
3
|
+
import type { SpannerClient } from '../gcp/spanner';
|
|
4
|
+
import { EntryLookup } from './catalog_entry';
|
|
5
|
+
import type { SemanticModel } from './ir';
|
|
6
|
+
export interface SourceReaders {
|
|
7
|
+
catalog: EntryLookup;
|
|
8
|
+
bigquery: Pick<BigQueryClient, 'getTable'>;
|
|
9
|
+
biglake: Pick<BigLakeClient, 'getCatalog' | 'getHiveCatalog'>;
|
|
10
|
+
spanner: Pick<SpannerClient, 'getInstance' | 'getInstanceConfig'>;
|
|
11
|
+
}
|
|
12
|
+
export declare function sourceEntryErrors(model: Pick<SemanticModel, 'name' | 'entities' | 'profiles'>, readers: SourceReaders): Promise<string[]>;
|