@google-cloud/kcmd 0.0.0-stage → 0.1.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 +266 -2
- package/build/ts/kcmd/cli.js +19578 -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 +16 -0
- package/build/ts/kcmd/gcp/biglake.js +50 -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 +63 -0
- package/build/ts/kcmd/gcp/spanner.js +123 -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 +344 -0
- package/build/ts/kcmd/layouts/semantic-model.d.ts +61 -0
- package/build/ts/kcmd/layouts/semantic-model.js +542 -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 +104 -0
- package/build/ts/kcmd/okf/concept_to_entry.d.ts +8 -0
- package/build/ts/kcmd/okf/concept_to_entry.js +254 -0
- package/build/ts/kcmd/okf/init_okf_workspace.d.ts +15 -0
- package/build/ts/kcmd/okf/init_okf_workspace.js +115 -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 +82 -0
- package/build/ts/kcmd/semantic/bigquery.d.ts +22 -0
- package/build/ts/kcmd/semantic/bigquery.js +525 -0
- package/build/ts/kcmd/semantic/binding.d.ts +3 -0
- package/build/ts/kcmd/semantic/binding.js +97 -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 +129 -0
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +1396 -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 +34 -0
- package/build/ts/kcmd/semantic/graph_ddl.js +563 -0
- package/build/ts/kcmd/semantic/import_model.d.ts +50 -0
- package/build/ts/kcmd/semantic/import_model.js +1275 -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 +31 -0
- package/build/ts/kcmd/semantic/kc_converter.js +824 -0
- package/build/ts/kcmd/semantic/kc_custom_types.d.ts +70 -0
- package/build/ts/kcmd/semantic/kc_custom_types.js +828 -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 +103 -0
- package/build/ts/kcmd/semantic/kc_ids.js +191 -0
- package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +32 -0
- package/build/ts/kcmd/semantic/knowledge_catalog.js +1043 -0
- package/build/ts/kcmd/semantic/loader.d.ts +262 -0
- package/build/ts/kcmd/semantic/loader.js +3440 -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 +911 -0
- package/build/ts/kcmd/semantic/parameters.d.ts +18 -0
- package/build/ts/kcmd/semantic/parameters.js +152 -0
- package/build/ts/kcmd/semantic/pull_kc.d.ts +14 -0
- package/build/ts/kcmd/semantic/pull_kc.js +346 -0
- package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +54 -0
- package/build/ts/kcmd/semantic/resolve_inheritance.js +330 -0
- package/build/ts/kcmd/semantic/resolve_profiles.d.ts +250 -0
- package/build/ts/kcmd/semantic/resolve_profiles.js +1822 -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 +94 -0
- package/build/ts/kcmd/semantic/runtime/runtime.js +318 -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 +10 -0
- package/build/ts/kcmd/semantic/source_dialect.js +261 -0
- package/build/ts/kcmd/semantic/source_entries.d.ts +12 -0
- package/build/ts/kcmd/semantic/source_entries.js +282 -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 +17 -0
- package/build/ts/kcmd/semantic/sql_expr_utils.js +221 -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 +44 -0
- package/build/ts/kcmd/semantic/sql_parser.js +378 -0
- package/build/ts/kcmd/semantic/transpile.d.ts +74 -0
- package/build/ts/kcmd/semantic/transpile.js +563 -0
- package/build/ts/kcmd/semantic/validate.d.ts +35 -0
- package/build/ts/kcmd/semantic/validate.js +1899 -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 +33 -0
- package/build/ts/kcmd/snapshot.js +379 -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 +697 -0
- package/package.json +62 -4
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { EntryLookup } from './catalog_entry';
|
|
2
|
+
export interface SourceDialects {
|
|
3
|
+
dialects: Map<string, string>;
|
|
4
|
+
databases: Map<string, string>;
|
|
5
|
+
errors: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function sourceDialects(texts: Iterable<string>, catalog: EntryLookup, { thirdParty }?: {
|
|
8
|
+
thirdParty?: boolean | undefined;
|
|
9
|
+
}): Promise<SourceDialects>;
|
|
10
|
+
export declare function sourcesToLookUp(texts: Iterable<string>): string[];
|
|
@@ -0,0 +1,261 @@
|
|
|
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.
|
|
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
|
+
// This is the only place kcmd makes a network call to learn a dialect. A
|
|
20
|
+
// command looks every such source up once, before it loads the model, and
|
|
21
|
+
// passes the dialects to the load (LoadOptions.sourceDialects).
|
|
22
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
25
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
26
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
27
|
+
}
|
|
28
|
+
Object.defineProperty(o, k2, desc);
|
|
29
|
+
}) : (function(o, m, k, k2) {
|
|
30
|
+
if (k2 === undefined) k2 = k;
|
|
31
|
+
o[k2] = m[k];
|
|
32
|
+
}));
|
|
33
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
34
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
35
|
+
}) : function(o, v) {
|
|
36
|
+
o["default"] = v;
|
|
37
|
+
});
|
|
38
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
39
|
+
var ownKeys = function(o) {
|
|
40
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
41
|
+
var ar = [];
|
|
42
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
43
|
+
return ar;
|
|
44
|
+
};
|
|
45
|
+
return ownKeys(o);
|
|
46
|
+
};
|
|
47
|
+
return function (mod) {
|
|
48
|
+
if (mod && mod.__esModule) return mod;
|
|
49
|
+
var result = {};
|
|
50
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
51
|
+
__setModuleDefault(result, mod);
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
})();
|
|
55
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
56
|
+
exports.sourceDialects = sourceDialects;
|
|
57
|
+
exports.sourcesToLookUp = sourcesToLookUp;
|
|
58
|
+
const yaml = __importStar(require("yaml"));
|
|
59
|
+
const loader_1 = require("./loader");
|
|
60
|
+
const catalog_entry_1 = require("./catalog_entry");
|
|
61
|
+
const yaml_options_1 = require("./yaml_options");
|
|
62
|
+
// The dialect each entrySource.system gives, in lower case.
|
|
63
|
+
const DIALECT_OF_ENTRY_SYSTEM = new Map([...loader_1.DIALECT_OF_FQN_PREFIX, ['postgres', 'POSTGRES']]);
|
|
64
|
+
// The parts of a table's fullyQualifiedName after each prefix, as Dataplex
|
|
65
|
+
// reads it. A view's name has the same parts as a table's.
|
|
66
|
+
const TABLE_FQN_PARTS = new Map([
|
|
67
|
+
['snowflake', ['account', 'database', 'schema', 'table']],
|
|
68
|
+
['databricks', ['metastore', 'catalog', 'schema', 'table']],
|
|
69
|
+
['mysql', ['host', 'schema', 'table']],
|
|
70
|
+
['postgresql', ['host', 'database', 'schema', 'table']],
|
|
71
|
+
['sqlserver', ['host', 'database', 'schema', 'table']],
|
|
72
|
+
]);
|
|
73
|
+
// Looks up the dialect of every Cloud SQL and third-party table the model and
|
|
74
|
+
// profile files `texts` write (see sourcesToLookUp), once per table, in
|
|
75
|
+
// parallel. With `thirdParty` false, looks up Cloud SQL tables only. Makes no
|
|
76
|
+
// call when they write none. Never throws: a lookup that fails is reported in
|
|
77
|
+
// `errors`.
|
|
78
|
+
async function sourceDialects(texts, catalog, { thirdParty = true } = {}) {
|
|
79
|
+
// Keyed as dialectLookupKey gives, so both spellings of one table are one
|
|
80
|
+
// lookup.
|
|
81
|
+
const lookups = new Map();
|
|
82
|
+
for (const source of sourcesToLookUp(texts)) {
|
|
83
|
+
const table = (0, loader_1.cloudSqlTableOf)(source);
|
|
84
|
+
if (table) {
|
|
85
|
+
lookups.set(table.resourceName, () => cloudSqlDialect(catalog, table));
|
|
86
|
+
}
|
|
87
|
+
const entry = thirdParty ? (0, loader_1.thirdPartyEntryOf)(source) : undefined;
|
|
88
|
+
if (entry)
|
|
89
|
+
lookups.set(entry.name, () => thirdPartyDialect(catalog, entry));
|
|
90
|
+
}
|
|
91
|
+
const dialects = new Map();
|
|
92
|
+
const databases = new Map();
|
|
93
|
+
const errors = [];
|
|
94
|
+
await Promise.all([...lookups].map(async ([key, lookUp]) => {
|
|
95
|
+
const got = await lookUp();
|
|
96
|
+
if ('error' in got)
|
|
97
|
+
errors.push(got.error);
|
|
98
|
+
else
|
|
99
|
+
dialects.set(key, got.dialect);
|
|
100
|
+
if ('database' in got && got.database)
|
|
101
|
+
databases.set(key, got.database);
|
|
102
|
+
}));
|
|
103
|
+
errors.sort();
|
|
104
|
+
return { dialects, databases, errors };
|
|
105
|
+
}
|
|
106
|
+
// The dialect of a Cloud SQL table, by its entry's type.
|
|
107
|
+
async function cloudSqlDialect(catalog, table) {
|
|
108
|
+
const at = `source '${table.resourceName}'`;
|
|
109
|
+
const res = await (0, catalog_entry_1.lookupSystemEntry)(catalog, 'cloudsql', table.project, table.location, table.resourceName);
|
|
110
|
+
if ('error' in res)
|
|
111
|
+
return { error: `${at}: ${res.error}` };
|
|
112
|
+
// Dataplex answers 403 both when the entry does not exist and when the
|
|
113
|
+
// caller may not read it, so the message names both causes.
|
|
114
|
+
if ('unreadable' in res) {
|
|
115
|
+
return {
|
|
116
|
+
error: `${at}: kcmd reads a Cloud SQL table's dialect from its ` +
|
|
117
|
+
`Knowledge Catalog entry, and could not read it. Turn on Dataplex ` +
|
|
118
|
+
`integration for instance '${table.instance}' (gcloud sql ` +
|
|
119
|
+
`instances patch ${table.instance} --enable-dataplex-integration ` +
|
|
120
|
+
`--project ${table.project}) and wait for the table to appear in ` +
|
|
121
|
+
`the catalog, or check that you can read the table. If integration ` +
|
|
122
|
+
`is on, check the name: the project ID, the instance's region, and ` +
|
|
123
|
+
`a schema only for PostgreSQL and SQL Server.`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// The type comes back as a full name; its last part says the engine.
|
|
127
|
+
const type = (res.result.entryType ?? '').split('/').pop() ?? '';
|
|
128
|
+
const dialect = loader_1.DIALECT_OF_ENTRY_TYPE[type];
|
|
129
|
+
return dialect ? { dialect } : {
|
|
130
|
+
error: `${at}: its Knowledge Catalog entry has type '${type}', which is ` +
|
|
131
|
+
`not a Cloud SQL table or view type kcmd knows (${Object.keys(loader_1.DIALECT_OF_ENTRY_TYPE).join(', ')}).`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// The dialect of a third-party table, by its entry's fullyQualifiedName or
|
|
135
|
+
// entrySource.system, and the database the fullyQualifiedName names. An entry
|
|
136
|
+
// whose fullyQualifiedName names no table or view is rejected.
|
|
137
|
+
async function thirdPartyDialect(catalog, { project, location, name }) {
|
|
138
|
+
const at = `source '${name}'`;
|
|
139
|
+
const res = await (0, catalog_entry_1.read)('looking up its Knowledge Catalog entry', () => catalog.lookupEntry(project, location, name));
|
|
140
|
+
if ('error' in res)
|
|
141
|
+
return { error: `${at}: ${res.error}` };
|
|
142
|
+
if ('unreadable' in res) {
|
|
143
|
+
return {
|
|
144
|
+
error: `${at} names no table Knowledge Catalog knows about. The entry ` +
|
|
145
|
+
`may not exist, or you may not be allowed to read it. kcmd reads a ` +
|
|
146
|
+
`third-party table's dialect from its entry, so add the table to ` +
|
|
147
|
+
`Knowledge Catalog, or fix the source.`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const { fullyQualifiedName: fqn, entrySource } = res.result;
|
|
151
|
+
const system = entrySource?.system;
|
|
152
|
+
const prefix = fqn?.match(/^([a-z]+):/)?.[1] ?? '';
|
|
153
|
+
const dialect = fqn ? loader_1.DIALECT_OF_FQN_PREFIX.get(prefix) :
|
|
154
|
+
DIALECT_OF_ENTRY_SYSTEM.get(system?.toLowerCase() ?? '');
|
|
155
|
+
if (dialect && fqn) {
|
|
156
|
+
const database = databaseOfFqn(fqn);
|
|
157
|
+
if (database)
|
|
158
|
+
return { dialect, database };
|
|
159
|
+
const parts = TABLE_FQN_PARTS.get(prefix).map(p => `<${p}>`).join('.');
|
|
160
|
+
return {
|
|
161
|
+
error: `${at}: its Knowledge Catalog entry has fullyQualifiedName ` +
|
|
162
|
+
`'${fqn}', which names no table or view. A table's ` +
|
|
163
|
+
`fullyQualifiedName has the form ${prefix}:${parts}. Write the ` +
|
|
164
|
+
`entry name of the table's own entry.`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (dialect)
|
|
168
|
+
return { dialect };
|
|
169
|
+
const has = fqn ? `fullyQualifiedName '${fqn}'` :
|
|
170
|
+
system ? `entrySource.system '${system}' and no fullyQualifiedName` :
|
|
171
|
+
'neither a fullyQualifiedName nor an entrySource.system';
|
|
172
|
+
return {
|
|
173
|
+
error: `${at}: kcmd cannot tell the table's SQL dialect from its ` +
|
|
174
|
+
`Knowledge Catalog entry, which has ${has}. kcmd reads the prefix of ` +
|
|
175
|
+
`the entry's fullyQualifiedName (snowflake:, databricks:, mysql:, ` +
|
|
176
|
+
`postgresql: or sqlserver:), or entrySource.system when there is no ` +
|
|
177
|
+
`fullyQualifiedName.`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// The database the fullyQualifiedName `fqn` of a third-party table or view
|
|
181
|
+
// names: a Snowflake account, a MySQL or SQL Server host, a PostgreSQL host
|
|
182
|
+
// and database, or a Databricks metastore. Each account and host is in lower
|
|
183
|
+
// case. Undefined when `fqn` names no table or view. Such a name has another
|
|
184
|
+
// number of parts than TABLE_FQN_PARTS gives, or a subtype, such as the
|
|
185
|
+
// `routine:` of a routine. Dataplex reads a `databricks:table:` or
|
|
186
|
+
// `databricks:view:` name as the same table as the `databricks:` name.
|
|
187
|
+
function databaseOfFqn(fqn) {
|
|
188
|
+
// The subtype holds no backtick, so a colon inside a quoted host and port
|
|
189
|
+
// stays in the path. A colon in an unquoted database, schema or table
|
|
190
|
+
// name stays in its segment when it comes after the name's first backtick.
|
|
191
|
+
const [, prefix, subtype, path] = fqn.match(/^([a-z]+):(?:([a-z0-9_-]+):)?(.*)$/s) ?? [];
|
|
192
|
+
const parts = TABLE_FQN_PARTS.get(prefix ?? '');
|
|
193
|
+
const seg = (0, loader_1.catalogNameSegments)(path ?? '', true);
|
|
194
|
+
const tableSubtype = subtype === undefined ||
|
|
195
|
+
(prefix === 'databricks' && (subtype === 'table' || subtype === 'view'));
|
|
196
|
+
if (!parts || !tableSubtype || seg.length !== parts.length)
|
|
197
|
+
return undefined;
|
|
198
|
+
const n = prefix === 'postgresql' ? 2 : 1;
|
|
199
|
+
// A Snowflake account and a host name ignore letter case.
|
|
200
|
+
if (prefix !== 'databricks')
|
|
201
|
+
seg[0] = seg[0].toLowerCase();
|
|
202
|
+
return [prefix, ...seg.slice(0, n)].join('/');
|
|
203
|
+
}
|
|
204
|
+
// Every Cloud SQL table and third-party entry the model and profile files
|
|
205
|
+
// `texts` write. A Cloud SQL table counts as a resource name or resource URI
|
|
206
|
+
// anywhere, JSON strings included. An entry name counts as a `source` value in
|
|
207
|
+
// the YAML or in a GOOGLE block's JSON `data`, unless it sits in ai_context,
|
|
208
|
+
// an action parameter's default or another vendor's block. Values are read
|
|
209
|
+
// from the parsed YAML, so a name in a comment is not collected. A file that
|
|
210
|
+
// does not parse collects nothing; the load reports it.
|
|
211
|
+
function sourcesToLookUp(texts) {
|
|
212
|
+
const found = new Set();
|
|
213
|
+
// `key` names the member that holds `value`. `opaque` marks a value whose
|
|
214
|
+
// `source` names no table: one inside ai_context, an action parameter's
|
|
215
|
+
// default or another vendor's block, or in JSON written anywhere but a
|
|
216
|
+
// `data` member.
|
|
217
|
+
const visit = (value, key, opaque = false) => {
|
|
218
|
+
if (typeof value === 'string') {
|
|
219
|
+
if ((0, loader_1.cloudSqlTableOf)(value) ||
|
|
220
|
+
(key === 'source' && !opaque && (0, loader_1.thirdPartyEntryOf)(value))) {
|
|
221
|
+
found.add(value.trim());
|
|
222
|
+
}
|
|
223
|
+
else if (/^\s*[[{]/.test(value)) {
|
|
224
|
+
try {
|
|
225
|
+
visit(JSON.parse(value), undefined, opaque || key !== 'data');
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Not JSON: an ordinary string.
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else if (Array.isArray(value)) {
|
|
233
|
+
value.forEach(v => visit(v, undefined, opaque));
|
|
234
|
+
}
|
|
235
|
+
else if (value && typeof value === 'object') {
|
|
236
|
+
const vendor = 'vendor_name' in value && !(0, loader_1.isGoogleVendor)(value.vendor_name);
|
|
237
|
+
for (const [k, v] of Object.entries(value)) {
|
|
238
|
+
visit(v, k, opaque || vendor || k === 'ai_context' || k === 'default');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
for (const text of texts) {
|
|
243
|
+
// Parsed as the loader parses, so both read the same values.
|
|
244
|
+
for (const doc of yaml.parseAllDocuments(text, yaml_options_1.YAML_OPTIONS)) {
|
|
245
|
+
if (doc.errors.length)
|
|
246
|
+
continue;
|
|
247
|
+
// Reading the values can still fail: on an alias whose anchor is not
|
|
248
|
+
// set, on more aliases than a load expands, or, while walking them, on
|
|
249
|
+
// an alias that refers to itself, which makes the values cyclic. The
|
|
250
|
+
// load reports that file in its own words, so the rest of it is skipped
|
|
251
|
+
// here rather than stopping the command.
|
|
252
|
+
try {
|
|
253
|
+
visit(doc.toJS());
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return [...found];
|
|
261
|
+
}
|
|
@@ -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'>;
|
|
10
|
+
spanner: Pick<SpannerClient, 'getInstance' | 'getInstanceConfig'>;
|
|
11
|
+
}
|
|
12
|
+
export declare function sourceEntryErrors(model: Pick<SemanticModel, 'name' | 'entities' | 'profiles'>, readers: SourceReaders): Promise<string[]>;
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Checks that Knowledge Catalog has an entry for every table a push reads
|
|
3
|
+
// (b/568769073), so a source that names no table stops the push before it
|
|
4
|
+
// writes anything. The same reads find each profile whose BigQuery and
|
|
5
|
+
// BigLake tables share no location (b/578688979).
|
|
6
|
+
//
|
|
7
|
+
// Each first-party table's entry sits in its system's entry group, with its
|
|
8
|
+
// resource name as the entry id, in the table's own location:
|
|
9
|
+
//
|
|
10
|
+
// - BigQuery: the table's location, which getTable reads, such as `US`. The
|
|
11
|
+
// table read needs only access to the table, not to its dataset.
|
|
12
|
+
// - BigLake: the region of one of the catalog's replicas, which getCatalog
|
|
13
|
+
// reads: `us-central1` for a regional catalog. A federated catalog lists
|
|
14
|
+
// one replica, such as `US`, with no state. A US catalog of Cloud Storage
|
|
15
|
+
// tables keeps a table's entry in the region that was its primary when the
|
|
16
|
+
// table was created, so after a failover an older table's entry can sit in
|
|
17
|
+
// a former primary's region, now a secondary's. The entry is looked up in
|
|
18
|
+
// each replica's region, the primary's first, until one finds it.
|
|
19
|
+
// - Spanner: the default leader region of the instance's config, which
|
|
20
|
+
// getInstance reads the config's name from. A regional config is named for
|
|
21
|
+
// its region, `regional-us-central1` for us-central1, so it is not read;
|
|
22
|
+
// any other config is, with getInstanceConfig: `nam3` gives us-east4. That
|
|
23
|
+
// read needs spanner.instanceConfigs.get, which the Spanner database roles
|
|
24
|
+
// lack. A database's own default leader does not move the entry.
|
|
25
|
+
// - AlloyDB: the location the source names.
|
|
26
|
+
//
|
|
27
|
+
// Dataplex keeps locations in lower case. A lookup in the wrong location
|
|
28
|
+
// answers 403, as a missing entry does (live, 2026-10-27), so each location
|
|
29
|
+
// must be read, not guessed. Every read is a direct lookup: Dataplex search
|
|
30
|
+
// misses entries a lookup finds, and is not exact.
|
|
31
|
+
//
|
|
32
|
+
// When a location read answers 404, the source names a table, catalog or
|
|
33
|
+
// instance that does not exist. When it answers 403, the read was refused and
|
|
34
|
+
// the entry was never looked up, so the message says which read it was, and
|
|
35
|
+
// what the service said: Spanner answers 403 for a project the caller may not
|
|
36
|
+
// read, whether or not the instance exists, so a 403 does not say the table
|
|
37
|
+
// is missing. A read that fails another way says to try again when a retry
|
|
38
|
+
// may pass (a thrown call, a 429 or a 5xx), and to fix the source otherwise.
|
|
39
|
+
//
|
|
40
|
+
// A Cloud SQL or third-party source is not looked up here: sourceDialects
|
|
41
|
+
// already looks up its entry directly, and fails the push when it is missing.
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.sourceEntryErrors = sourceEntryErrors;
|
|
44
|
+
const api_1 = require("../gcp/api");
|
|
45
|
+
const catalog_entry_1 = require("./catalog_entry");
|
|
46
|
+
const loader_1 = require("./loader");
|
|
47
|
+
const resolve_profiles_1 = require("./resolve_profiles");
|
|
48
|
+
// One message for each entity, in the inline bindings (named `default`) or in
|
|
49
|
+
// a profile, whose source names a table with no Knowledge Catalog entry, in
|
|
50
|
+
// that order. Then one message for each of those profiles whose BigQuery and
|
|
51
|
+
// BigLake tables share no location (b/578688979): BigQuery runs each query in
|
|
52
|
+
// one location, so no query through the profile can join them.
|
|
53
|
+
//
|
|
54
|
+
// A named profile is judged by its own sources, since push requires it to
|
|
55
|
+
// bind every concrete entity. A BigLake table can be queried in any replica
|
|
56
|
+
// region of its catalog. A table whose location could not be read is
|
|
57
|
+
// reported above and left out.
|
|
58
|
+
//
|
|
59
|
+
// Seen live in kc-looker-external, for a US catalog whose replicas are
|
|
60
|
+
// us-central1 (primary) and us-east1:
|
|
61
|
+
// - A job in US cannot read its tables (2026-11-03).
|
|
62
|
+
// - Once a table has replicated, which took a few minutes, a job in either
|
|
63
|
+
// region can read it, join it with a dataset in that region, and create a
|
|
64
|
+
// property graph over both (2026-11-04).
|
|
65
|
+
// - A job with no location runs in the primary.
|
|
66
|
+
//
|
|
67
|
+
// Reads each table's location and looks its entry up once per location it
|
|
68
|
+
// may sit in, and reads each catalog and instance once; tables are checked in
|
|
69
|
+
// parallel, and a table's locations in turn. Skips an abstract entity, an
|
|
70
|
+
// entity with no source, and a source that names no table this module knows
|
|
71
|
+
// (a Cloud SQL or third-party table, or a legacy bare name). Never throws.
|
|
72
|
+
async function sourceEntryErrors(model, readers) {
|
|
73
|
+
const uses = [];
|
|
74
|
+
const use = (entity, profile, source) => {
|
|
75
|
+
const table = source ? (0, loader_1.sourceTableOf)(source) : undefined;
|
|
76
|
+
if (table)
|
|
77
|
+
uses.push({ entity, profile, table });
|
|
78
|
+
};
|
|
79
|
+
for (const e of model.entities) {
|
|
80
|
+
if (!e.abstract)
|
|
81
|
+
use(e.name, resolve_profiles_1.DEFAULT_PROFILE, (0, loader_1.writtenSource)(e));
|
|
82
|
+
}
|
|
83
|
+
for (const p of model.profiles ?? []) {
|
|
84
|
+
for (const b of p.entities)
|
|
85
|
+
use(b.name, p.name, b.source);
|
|
86
|
+
}
|
|
87
|
+
const catalogAndInstanceReads = new Map();
|
|
88
|
+
const locations = new Map();
|
|
89
|
+
const problems = new Map();
|
|
90
|
+
for (const { table } of uses) {
|
|
91
|
+
if (!problems.has(table.resourceName)) {
|
|
92
|
+
const located = locationsOf(table, readers, catalogAndInstanceReads);
|
|
93
|
+
locations.set(table.resourceName, located);
|
|
94
|
+
problems.set(table.resourceName, problemOf(table, located, readers));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const errors = [];
|
|
98
|
+
// By profile, each list of regions BigQuery can query one of its tables in,
|
|
99
|
+
// keyed by the list, with the entities bound to such tables, in the order
|
|
100
|
+
// bound.
|
|
101
|
+
const placed = new Map();
|
|
102
|
+
for (const { entity, profile, table } of uses) {
|
|
103
|
+
const problem = await problems.get(table.resourceName);
|
|
104
|
+
if (problem) {
|
|
105
|
+
errors.push(`entity '${entity}' in profile '${profile}': source '${table.resourceName}' ${problem}`);
|
|
106
|
+
}
|
|
107
|
+
if (table.system !== 'bigquery' && table.system !== 'biglake')
|
|
108
|
+
continue;
|
|
109
|
+
const located = await locations.get(table.resourceName);
|
|
110
|
+
if (!('result' in located))
|
|
111
|
+
continue;
|
|
112
|
+
const regions = located.result;
|
|
113
|
+
const key = regions.join(' ');
|
|
114
|
+
const at = placed.get(profile) ?? new Map();
|
|
115
|
+
at.set(key, { regions, entities: [...at.get(key)?.entities ?? [], entity] });
|
|
116
|
+
placed.set(profile, at);
|
|
117
|
+
}
|
|
118
|
+
for (const [profile, at] of placed) {
|
|
119
|
+
const [first, ...rest] = at.values();
|
|
120
|
+
if (first.regions.some(r => rest.every(g => g.regions.includes(r)))) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const listed = [...at.values()].map(({ regions, entities }) => `${regions.map(shown).join(' or ')} (${entities.map(e => `'${e}'`).join(', ')})`);
|
|
124
|
+
errors.push(`profile '${profile}' of model '${model.name}' binds tables that ` +
|
|
125
|
+
`share no BigQuery location: ${listed.join(', ')}. ` +
|
|
126
|
+
`BigQuery runs each query in one location, so no query can join ` +
|
|
127
|
+
`them. Bind them to tables that share a location, or pass ` +
|
|
128
|
+
`--skip-source-check.`);
|
|
129
|
+
}
|
|
130
|
+
return errors;
|
|
131
|
+
}
|
|
132
|
+
// What is wrong with `table`'s source, or undefined when its catalog entry
|
|
133
|
+
// is there. `located` is what locationsOf gave for `table`. Looks the entry
|
|
134
|
+
// up in each of its locations in turn, and stops at the first that finds it:
|
|
135
|
+
// a lookup in the wrong location answers 403, so trying each cannot pass a
|
|
136
|
+
// missing entry.
|
|
137
|
+
async function problemOf(table, located, readers) {
|
|
138
|
+
const locations = await located;
|
|
139
|
+
if ('problem' in locations)
|
|
140
|
+
return locations.problem;
|
|
141
|
+
for (const location of locations.result) {
|
|
142
|
+
const res = await (0, catalog_entry_1.lookupSystemEntry)(readers.catalog, table.system, table.project, location, table.resourceName);
|
|
143
|
+
if ('result' in res)
|
|
144
|
+
return undefined;
|
|
145
|
+
if ('error' in res)
|
|
146
|
+
return failedProblem(res).problem;
|
|
147
|
+
}
|
|
148
|
+
return `names no table Knowledge Catalog knows about. The table may not ` +
|
|
149
|
+
`exist, or you may not be allowed to read its catalog entry.${table.system === 'alloydb' ?
|
|
150
|
+
' A new AlloyDB cluster can take hours to appear in the ' +
|
|
151
|
+
'catalog after Dataplex integration is turned on.' :
|
|
152
|
+
''} Fix the source, or pass --skip-source-check.`;
|
|
153
|
+
}
|
|
154
|
+
// `location`, which is kept in lower case, as BigQuery writes it: a
|
|
155
|
+
// multi-region, which has no hyphen, in upper case (`US`, `EU`).
|
|
156
|
+
function shown(location) {
|
|
157
|
+
return location.includes('-') ? location : location.toUpperCase();
|
|
158
|
+
}
|
|
159
|
+
// Said when reading an instance config is refused (live on
|
|
160
|
+
// bigquery-public-data and dataplex-staging-types, 2026-10-27: Spanner answers
|
|
161
|
+
// 403 naming this permission, even for a Google-managed config such as nam3).
|
|
162
|
+
const CONFIG_REFUSED_HINT = ' Reading it needs the permission ' +
|
|
163
|
+
'spanner.instanceConfigs.get on the project, which roles/spanner.viewer ' +
|
|
164
|
+
'includes and the Spanner database roles do not.';
|
|
165
|
+
// The locations where `table`'s catalog entry may sit, as Dataplex writes
|
|
166
|
+
// them, in lower case, in the order to look in: one for a BigQuery, Spanner
|
|
167
|
+
// or AlloyDB table, and one per replica for a BigLake table. `cache` keeps
|
|
168
|
+
// each catalog and instance's read, so each is read once; each table is read
|
|
169
|
+
// once already.
|
|
170
|
+
async function locationsOf(table, readers, cache) {
|
|
171
|
+
const once = (key, readIt) => {
|
|
172
|
+
if (!cache.has(key))
|
|
173
|
+
cache.set(key, readIt());
|
|
174
|
+
return cache.get(key);
|
|
175
|
+
};
|
|
176
|
+
const { project } = table;
|
|
177
|
+
switch (table.system) {
|
|
178
|
+
case 'alloydb':
|
|
179
|
+
return { result: [table.location] };
|
|
180
|
+
case 'bigquery': {
|
|
181
|
+
const what = `reading table '${project}.${table.dataset}.${table.table}'`;
|
|
182
|
+
const res = await readLocation(what, 'a table', () => readers.bigquery.getTable(project, table.dataset, table.table));
|
|
183
|
+
return 'result' in res ?
|
|
184
|
+
located(res.result.location, what, 'no location') :
|
|
185
|
+
res;
|
|
186
|
+
}
|
|
187
|
+
case 'biglake':
|
|
188
|
+
return once(`biglake/${project}/${table.catalog}`, async () => {
|
|
189
|
+
const what = `reading the region of catalog '${table.catalog}'`;
|
|
190
|
+
const res = await readLocation(what, 'a catalog', () => readers.biglake.getCatalog(project, table.catalog));
|
|
191
|
+
return 'result' in res ? replicaRegions(res.result, what) : res;
|
|
192
|
+
});
|
|
193
|
+
case 'spanner':
|
|
194
|
+
return once(`spanner/${project}/${table.instance}`, async () => {
|
|
195
|
+
const what = `reading instance '${table.instance}'`;
|
|
196
|
+
const instance = await readLocation(what, 'an instance', () => readers.spanner.getInstance(project, table.instance));
|
|
197
|
+
if ('problem' in instance)
|
|
198
|
+
return instance;
|
|
199
|
+
const configName = instance.result.config;
|
|
200
|
+
if (!configName) {
|
|
201
|
+
return transientProblem(`${what} gave no instance config.`);
|
|
202
|
+
}
|
|
203
|
+
// A regional config keeps every replica, so its leader, in the region
|
|
204
|
+
// it is named for: `regional-us-central1` leads in us-central1 (live,
|
|
205
|
+
// 2026-10-27).
|
|
206
|
+
const region = configName.match(/\/instanceConfigs\/regional-(.+)$/);
|
|
207
|
+
if (region)
|
|
208
|
+
return { result: [region[1]] };
|
|
209
|
+
const configWhat = `reading instance config '${configName}'`;
|
|
210
|
+
const config = await readLocation(configWhat, 'an instance config', () => readers.spanner.getInstanceConfig(configName), CONFIG_REFUSED_HINT);
|
|
211
|
+
// A config marks exactly one replica as its default leader.
|
|
212
|
+
return 'result' in config ?
|
|
213
|
+
located(config.result.replicas?.find(r => r.defaultLeaderLocation)
|
|
214
|
+
?.location, configWhat, 'no default leader') :
|
|
215
|
+
config;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Makes the location read `call`, which `what` names. A 404 says the source
|
|
220
|
+
// names `kind`, such as `a table`, that does not exist (the services answer
|
|
221
|
+
// 404 for a missing table, dataset, catalog or instance; live, 2026-10-27 and
|
|
222
|
+
// 2026-11-03). A 403 says the read was refused, followed by the service's
|
|
223
|
+
// message and `refusedHint`.
|
|
224
|
+
async function readLocation(what, kind, call, refusedHint = '') {
|
|
225
|
+
const res = await (0, catalog_entry_1.read)(what, call);
|
|
226
|
+
if ('result' in res)
|
|
227
|
+
return res;
|
|
228
|
+
if ('error' in res)
|
|
229
|
+
return failedProblem(res);
|
|
230
|
+
return {
|
|
231
|
+
problem: res.status === 404 ?
|
|
232
|
+
`names ${kind} that does not exist: ${what} found nothing (HTTP ` +
|
|
233
|
+
`404). Fix the source, or pass --skip-source-check.` :
|
|
234
|
+
`could not be checked: ${what} was refused (HTTP 403).${res.message ? ` ${res.message}` : ''}${refusedHint} The ` +
|
|
235
|
+
`source's catalog entry was not looked up. Fix the access or ` +
|
|
236
|
+
`the source, or pass --skip-source-check.`,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
// The regions of `catalog`'s replicas, in the order to look in: a replica
|
|
240
|
+
// marked STATE_PRIMARY first, then one in another primary state, such as
|
|
241
|
+
// STATE_PRIMARY_IN_PROGRESS, then the rest, each group in the order the
|
|
242
|
+
// replicas come. A regional or federated catalog lists one replica with no
|
|
243
|
+
// state.
|
|
244
|
+
//
|
|
245
|
+
// After a failover, a table's entry stays only in the region that was the
|
|
246
|
+
// catalog's primary when the table was created, until the table is next
|
|
247
|
+
// written; for a few seconds during a failover the new primary reads
|
|
248
|
+
// STATE_PRIMARY_IN_PROGRESS, so no replica is STATE_PRIMARY (from review on
|
|
249
|
+
// #623, 2026-11-03; not tested live by us).
|
|
250
|
+
function replicaRegions(catalog, what) {
|
|
251
|
+
const rank = (state = '') => state === 'STATE_PRIMARY' ? 0 : state.includes('PRIMARY') ? 1 : 2;
|
|
252
|
+
const regions = (catalog.replicas ?? [])
|
|
253
|
+
.filter(r => r.region)
|
|
254
|
+
.sort((a, b) => rank(a.state) - rank(b.state))
|
|
255
|
+
.map(r => r.region.toLowerCase());
|
|
256
|
+
return regions.length ? { result: regions } :
|
|
257
|
+
transientProblem(`${what} gave no region.`);
|
|
258
|
+
}
|
|
259
|
+
// `location` in lower case, or a problem when the read `what` gave none.
|
|
260
|
+
// The services have always given one; this keeps a response without it from
|
|
261
|
+
// stopping the push with a crash.
|
|
262
|
+
function located(location, what, missing) {
|
|
263
|
+
return location ? { result: [location.toLowerCase()] } :
|
|
264
|
+
transientProblem(`${what} gave ${missing}.`);
|
|
265
|
+
}
|
|
266
|
+
// The problem for the failed read `res`. A thrown call, a 429 or a 5xx may
|
|
267
|
+
// pass on retry; any other status, such as BigQuery's 400 for an invalid
|
|
268
|
+
// table ID, never will, so the source must change.
|
|
269
|
+
function failedProblem(res) {
|
|
270
|
+
return res.status === undefined || (0, api_1.isTransientStatus)(res.status) ?
|
|
271
|
+
transientProblem(res.error) :
|
|
272
|
+
{
|
|
273
|
+
problem: `could not be checked: ${res.error} Fix the source, or ` +
|
|
274
|
+
`pass --skip-source-check.`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
// The problem for a read that failed with `detail`, which may pass on retry.
|
|
278
|
+
function transientProblem(detail) {
|
|
279
|
+
return {
|
|
280
|
+
problem: `could not be checked; try again, or pass --skip-source-check: ${detail}`,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { GenerateResult } from './graph_ddl';
|
|
2
|
+
import { SemanticModel } from './ir';
|
|
3
|
+
export interface GenerateOptions {
|
|
4
|
+
graphName?: string;
|
|
5
|
+
}
|
|
6
|
+
export type { GenerateResult };
|
|
7
|
+
/**
|
|
8
|
+
* Generates the Spanner Graph DDL for a semantic model.
|
|
9
|
+
*
|
|
10
|
+
* The generated statement references the entities' existing Spanner tables; it
|
|
11
|
+
* does not create them. Returns the DDL plus any warnings collected while
|
|
12
|
+
* mapping the IR (e.g. metrics dropped because Spanner Graph has no measure).
|
|
13
|
+
*
|
|
14
|
+
* Throws when the model yields no valid node table -- it declares no entities,
|
|
15
|
+
* or every entity lacks a KEY -- since a property graph with an empty NODE
|
|
16
|
+
* TABLES block is invalid DDL that Spanner would reject.
|
|
17
|
+
*/
|
|
18
|
+
export declare function generateSpannerPropertyGraph(model: SemanticModel, opts?: GenerateOptions): GenerateResult;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Generates Spanner Graph DDL from the Semantic Model IR.
|
|
3
|
+
//
|
|
4
|
+
// The shared GoogleSQL generator (./graph_ddl) writes the graph structure. This
|
|
5
|
+
// module supplies its Spanner engine: bare table and graph names, no measures,
|
|
6
|
+
// no per-element OPTIONS, and a field's SPANNER text, else its ANSI_SQL text.
|
|
7
|
+
//
|
|
8
|
+
// See:
|
|
9
|
+
// https://docs.cloud.google.com/spanner/docs/reference/standard-sql/graph-schema-statements
|
|
10
|
+
//
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.generateSpannerPropertyGraph = generateSpannerPropertyGraph;
|
|
13
|
+
const binding_1 = require("./binding");
|
|
14
|
+
const graph_ddl_1 = require("./graph_ddl");
|
|
15
|
+
const ir_1 = require("./ir");
|
|
16
|
+
const sql_expr_utils_1 = require("./sql_expr_utils");
|
|
17
|
+
const sql_identifiers_1 = require("./sql_identifiers");
|
|
18
|
+
/**
|
|
19
|
+
* Generates the Spanner Graph DDL for a semantic model.
|
|
20
|
+
*
|
|
21
|
+
* The generated statement references the entities' existing Spanner tables; it
|
|
22
|
+
* does not create them. Returns the DDL plus any warnings collected while
|
|
23
|
+
* mapping the IR (e.g. metrics dropped because Spanner Graph has no measure).
|
|
24
|
+
*
|
|
25
|
+
* Throws when the model yields no valid node table -- it declares no entities,
|
|
26
|
+
* or every entity lacks a KEY -- since a property graph with an empty NODE
|
|
27
|
+
* TABLES block is invalid DDL that Spanner would reject.
|
|
28
|
+
*/
|
|
29
|
+
function generateSpannerPropertyGraph(model, opts = {}) {
|
|
30
|
+
return (0, graph_ddl_1.generateGraphDdl)(model, spannerEngine(opts));
|
|
31
|
+
}
|
|
32
|
+
// The Spanner engine for the shared generator.
|
|
33
|
+
function spannerEngine(opts) {
|
|
34
|
+
return {
|
|
35
|
+
name: 'Spanner',
|
|
36
|
+
table: binding_1.spannerTable,
|
|
37
|
+
graphName: model => (0, sql_identifiers_1.quoteIdentifier)(opts.graphName ?? model.name),
|
|
38
|
+
placeMetrics: (metrics, _input, warnings) => skipMetrics(metrics, warnings),
|
|
39
|
+
elementOptions: () => undefined,
|
|
40
|
+
fieldOptions: () => undefined,
|
|
41
|
+
dialect: 'SPANNER',
|
|
42
|
+
fieldExpression,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// Warns that each metric is dropped, since Spanner Graph has no MEASURE.
|
|
46
|
+
function skipMetrics(metrics, warnings) {
|
|
47
|
+
for (const metric of metrics) {
|
|
48
|
+
warnings.push(`metric '${metric.name}' is not emitted: Spanner Graph has no ` +
|
|
49
|
+
`MEASURE, so model-level metrics have no home in it`);
|
|
50
|
+
}
|
|
51
|
+
return new Map();
|
|
52
|
+
}
|
|
53
|
+
// The field's SPANNER text, else its ANSI_SQL text, read as Spanner SQL with
|
|
54
|
+
// its comments removed. A line comment left in would swallow the ` AS <name>`
|
|
55
|
+
// and comma the DDL writes after the expression.
|
|
56
|
+
function fieldExpression(f) {
|
|
57
|
+
const text = (0, ir_1.expressionForDialect)(f, 'SPANNER');
|
|
58
|
+
return text === undefined ? undefined :
|
|
59
|
+
(0, sql_expr_utils_1.sqlWithoutComments)(text, 'SPANNER');
|
|
60
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Field } from './ir';
|
|
2
|
+
import { SqlColumn } from './sql_parser';
|
|
3
|
+
export declare const STRING_LITERAL: RegExp;
|
|
4
|
+
export declare function escapeRegExp(s: string): string;
|
|
5
|
+
export declare function blankStringLiterals(expression: string): string;
|
|
6
|
+
export declare function mapOutsideStringLiterals(expression: string, fn: (segment: string) => string): string;
|
|
7
|
+
export declare function referencedEntityNames(expression: string, entityNames: string[]): string[];
|
|
8
|
+
export declare function stripQualifier(expression: string, entity: string): string;
|
|
9
|
+
export declare function sqlWithoutComments(text: string, dialect?: string): string;
|
|
10
|
+
export declare function fieldBindingWithoutComments(field: Pick<Field, 'expression' | 'importedExpression' | 'importedDialect'>, targetDialect?: string): string | undefined;
|
|
11
|
+
export declare function columnReferences(expression: string, dialect: string): ReadonlyArray<SqlColumn> | undefined;
|
|
12
|
+
export declare function columnsForSource(text: string, dialect: string, impliedDialect?: string): ReadonlyArray<SqlColumn> | undefined;
|
|
13
|
+
export declare function entitiesReached(columns: ReadonlyArray<SqlColumn>, entityNames: ReadonlyArray<string> | ReadonlySet<string>): string[];
|
|
14
|
+
export declare function fieldsReadOn(columns: ReadonlyArray<SqlColumn>, entityName: string): string[];
|
|
15
|
+
export declare const SUPPORTED_AGGREGATES: string[];
|
|
16
|
+
export declare function isSingleAggregate(expression: string, dialect: string): boolean;
|
|
17
|
+
export declare function isColumnName(value: unknown): boolean;
|