@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,15 @@
|
|
|
1
|
+
import * as context from '../gcp/context';
|
|
2
|
+
import { LoadedModel } from './loader';
|
|
3
|
+
export interface DeployOptions {
|
|
4
|
+
validateOnly?: boolean;
|
|
5
|
+
maxOperationPolls?: number;
|
|
6
|
+
pollBackoffMs?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface DeployResult {
|
|
9
|
+
success: boolean;
|
|
10
|
+
details?: string;
|
|
11
|
+
ddl: string[];
|
|
12
|
+
warnings: string[];
|
|
13
|
+
deployed: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function deploySpanner(models: LoadedModel[], ctx: context.ApiContext, options?: DeployOptions): Promise<DeployResult>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Deploys a semantic model's Spanner Graph.
|
|
3
|
+
//
|
|
4
|
+
// This is the Spanner leg of `kcmd push` for the semantic-model scope, a
|
|
5
|
+
// sibling to deploy_bigquery: it consumes models already parsed into the
|
|
6
|
+
// semantic IR (see loadSemanticModels, shared across legs so a multi-destination
|
|
7
|
+
// push parses each document once), lowers each to `CREATE OR REPLACE PROPERTY
|
|
8
|
+
// GRAPH` DDL (generator: ./spanner), and applies that DDL to the Spanner
|
|
9
|
+
// database named by the model's GOOGLE deployment target.
|
|
10
|
+
//
|
|
11
|
+
// Spanner DDL is asynchronous: updateDatabaseDdl returns a long-running
|
|
12
|
+
// operation which this leg polls to completion, unlike the BigQuery leg's
|
|
13
|
+
// jobs.query.
|
|
14
|
+
//
|
|
15
|
+
// This is a library module: it emits no console output. The generated DDL and
|
|
16
|
+
// any loader/generator warnings are returned in `DeployResult` for the CLI
|
|
17
|
+
// (src/tool/commands.ts) to print.
|
|
18
|
+
//
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.deploySpanner = deploySpanner;
|
|
21
|
+
const spanner_1 = require("../gcp/spanner");
|
|
22
|
+
const deployment_target_1 = require("./deployment_target");
|
|
23
|
+
const spanner_2 = require("./spanner");
|
|
24
|
+
// Upper bound on getOperation polls before we give up on an operation that
|
|
25
|
+
// never reports completion.
|
|
26
|
+
const MAX_OPERATION_POLLS = 60;
|
|
27
|
+
// Pause between polls of a running operation.
|
|
28
|
+
const POLL_BACKOFF_MS = 1000;
|
|
29
|
+
function sleep(ms) {
|
|
30
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
31
|
+
}
|
|
32
|
+
// Waits for an updateDatabaseDdl long-running operation to complete and reports
|
|
33
|
+
// any terminal error. The initial POST returns an Operation (usually not yet
|
|
34
|
+
// `done`); we poll getOperation on its `name` until it is, then judge success
|
|
35
|
+
// by its `error` field.
|
|
36
|
+
async function awaitOperationDone(spanner, started, maxPolls, backoffMs) {
|
|
37
|
+
if (started.status !== 200) {
|
|
38
|
+
return { ok: false, error: started.message || String(started.status) };
|
|
39
|
+
}
|
|
40
|
+
let op = started.result;
|
|
41
|
+
if (!op) {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
error: started.message ||
|
|
45
|
+
'updateDatabaseDdl returned status 200 with no operation body',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const opName = op.name;
|
|
49
|
+
let polls = 0;
|
|
50
|
+
while (!op?.done && opName && polls < maxPolls) {
|
|
51
|
+
if (polls > 0) {
|
|
52
|
+
await sleep(backoffMs);
|
|
53
|
+
}
|
|
54
|
+
polls++;
|
|
55
|
+
const res = await spanner.getOperation(opName);
|
|
56
|
+
if (res.status !== 200) {
|
|
57
|
+
return { ok: false, error: res.message || String(res.status) };
|
|
58
|
+
}
|
|
59
|
+
op = res.result;
|
|
60
|
+
}
|
|
61
|
+
if (!op?.done) {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
error: opName ?
|
|
65
|
+
`operation ${opName} did not complete after ${maxPolls} polls` :
|
|
66
|
+
'updateDatabaseDdl returned no operation name to poll',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (op.error) {
|
|
70
|
+
return { ok: false, error: op.error.message || `code ${op.error.code}` };
|
|
71
|
+
}
|
|
72
|
+
return { ok: true };
|
|
73
|
+
}
|
|
74
|
+
// Deploys the Spanner Graph for each pre-loaded model. Emits no console output;
|
|
75
|
+
// the generated DDL and any warnings are returned for the caller to print. The
|
|
76
|
+
// models are parsed once by the caller (loadSemanticModels) and shared with the
|
|
77
|
+
// other legs; each carries its originating document name for diagnostics.
|
|
78
|
+
async function deploySpanner(models, ctx, options = {}) {
|
|
79
|
+
const spanner = new spanner_1.SpannerClient(ctx);
|
|
80
|
+
const ddl = [];
|
|
81
|
+
const warnings = [];
|
|
82
|
+
let deployed = 0;
|
|
83
|
+
let modelsSeen = 0;
|
|
84
|
+
const fail = (details) => ({ success: false, details, ddl, warnings, deployed });
|
|
85
|
+
const maxPolls = options.maxOperationPolls ?? MAX_OPERATION_POLLS;
|
|
86
|
+
const backoffMs = options.pollBackoffMs ?? POLL_BACKOFF_MS;
|
|
87
|
+
for (const { document, model } of models) {
|
|
88
|
+
modelsSeen++;
|
|
89
|
+
let targets;
|
|
90
|
+
let malformed;
|
|
91
|
+
try {
|
|
92
|
+
({ targets, malformed } = (0, deployment_target_1.spannerGraphTargets)(model));
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
return fail(`Model '${model.name}' (${document}): ${err.message || err}`);
|
|
96
|
+
}
|
|
97
|
+
if (!targets.length) {
|
|
98
|
+
// A malformed-but-present target must not be reported as "none declared".
|
|
99
|
+
if (malformed.length) {
|
|
100
|
+
return fail(`Model '${model.name}' (${document}) declares Spanner Graph ` +
|
|
101
|
+
`deploymentTarget(s) that could not be parsed: ` +
|
|
102
|
+
`${malformed.join(', ')}. Expected ${deployment_target_1.SPANNER_GRAPH_FORM}.`);
|
|
103
|
+
}
|
|
104
|
+
return fail(`Model '${model.name}' (${document}) declares no Spanner Graph ` +
|
|
105
|
+
`deploymentTarget in a GOOGLE custom_extension; nothing to deploy.`);
|
|
106
|
+
}
|
|
107
|
+
for (const bad of malformed) {
|
|
108
|
+
warnings.push(`[${model.name}] ignoring unparseable Spanner Graph target: ${bad}`);
|
|
109
|
+
}
|
|
110
|
+
for (const target of targets) {
|
|
111
|
+
const gen = (0, spanner_2.generateSpannerPropertyGraph)(model, {
|
|
112
|
+
graphName: target.graphName,
|
|
113
|
+
});
|
|
114
|
+
for (const w of gen.warnings) {
|
|
115
|
+
warnings.push(`[${model.name} -> ${target.graphName}] ${w}`);
|
|
116
|
+
}
|
|
117
|
+
ddl.push(`-- ${target.uri}\n${gen.ddl}`);
|
|
118
|
+
if (options.validateOnly) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// updateDatabaseDdl takes each statement WITHOUT a trailing semicolon:
|
|
122
|
+
// Spanner's DDL parser rejects one ("Expecting 'EOF' but found ';'" --
|
|
123
|
+
// verified live), unlike BigQuery's jobs.query, which accepts it. The
|
|
124
|
+
// generator terminates its DDL with `;` for the printed/golden artifact,
|
|
125
|
+
// so strip it here for the wire statement only.
|
|
126
|
+
const statement = gen.ddl.replace(/;\s*$/, '');
|
|
127
|
+
const started = await spanner.updateDatabaseDdl(target.project, target.instance, target.database, [statement]);
|
|
128
|
+
const outcome = await awaitOperationDone(spanner, started, maxPolls, backoffMs);
|
|
129
|
+
if (!outcome.ok) {
|
|
130
|
+
// CREATE OR REPLACE statements are applied one graph at a time, so any
|
|
131
|
+
// graphs already deployed in this run have mutated the database and are
|
|
132
|
+
// not rolled back. Surface that alongside the failing target.
|
|
133
|
+
const partial = deployed ?
|
|
134
|
+
` (${deployed} graph(s) already deployed in this run; ` +
|
|
135
|
+
`CREATE OR REPLACE changes are not rolled back)` :
|
|
136
|
+
'';
|
|
137
|
+
return fail(`Failed to deploy '${target.graphName}': ${outcome.error}${partial}`);
|
|
138
|
+
}
|
|
139
|
+
deployed++;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!modelsSeen) {
|
|
143
|
+
if (options.validateOnly) {
|
|
144
|
+
warnings.push('No semantic model documents found; nothing to validate.');
|
|
145
|
+
return { success: true, ddl, warnings, deployed };
|
|
146
|
+
}
|
|
147
|
+
return fail('No semantic model documents found; nothing to deploy.');
|
|
148
|
+
}
|
|
149
|
+
return { success: true, ddl, warnings, deployed };
|
|
150
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { CustomExtension, DeploymentSpec, SemanticModel } from './ir';
|
|
2
|
+
export declare const BQ_GRAPH_FORM = "bigquery.googleapis.com/projects/<p>/datasets/<d>/propertyGraphs/<g>";
|
|
3
|
+
export declare const SPANNER_GRAPH_FORM = "spanner.googleapis.com/projects/<p>/instances/<i>/databases/<db>/graphs/<g>";
|
|
4
|
+
export declare const ALLOYDB_FORM = "alloydb.googleapis.com/projects/<p>/locations/<l>/clusters/<c>/databases/<db>";
|
|
5
|
+
export interface BigQueryGraphTarget {
|
|
6
|
+
project: string;
|
|
7
|
+
dataset: string;
|
|
8
|
+
graphName: string;
|
|
9
|
+
uri: string;
|
|
10
|
+
}
|
|
11
|
+
export interface SpannerGraphTarget {
|
|
12
|
+
project: string;
|
|
13
|
+
instance: string;
|
|
14
|
+
database: string;
|
|
15
|
+
graphName: string;
|
|
16
|
+
uri: string;
|
|
17
|
+
}
|
|
18
|
+
export interface AlloyDbTarget {
|
|
19
|
+
project: string;
|
|
20
|
+
location: string;
|
|
21
|
+
cluster: string;
|
|
22
|
+
database: string;
|
|
23
|
+
uri: string;
|
|
24
|
+
}
|
|
25
|
+
export interface GoogleDeploymentTargets {
|
|
26
|
+
uris: string[];
|
|
27
|
+
bigQuery: BigQueryGraphTarget[];
|
|
28
|
+
spanner: SpannerGraphTarget[];
|
|
29
|
+
alloyDb: AlloyDbTarget[];
|
|
30
|
+
malformed: string[];
|
|
31
|
+
}
|
|
32
|
+
export type ParsedDeploymentTarget = {
|
|
33
|
+
engine: 'bigquery';
|
|
34
|
+
coordinates: BigQueryGraphTarget;
|
|
35
|
+
} | {
|
|
36
|
+
engine: 'spanner';
|
|
37
|
+
coordinates: SpannerGraphTarget;
|
|
38
|
+
} | {
|
|
39
|
+
engine: 'alloydb';
|
|
40
|
+
coordinates: AlloyDbTarget;
|
|
41
|
+
};
|
|
42
|
+
export type ResolvedDeployment = ParsedDeploymentTarget & {
|
|
43
|
+
name: string;
|
|
44
|
+
profile: string;
|
|
45
|
+
};
|
|
46
|
+
/** Why a deployment's target is not accepted in a `deployments:` list. */
|
|
47
|
+
export declare function deploymentTargetError(model: SemanticModel, d: DeploymentSpec): string;
|
|
48
|
+
/** Why a target in a GOOGLE block's `deploymentTargets` is not accepted. */
|
|
49
|
+
export declare function googleBlockTargetError(model: SemanticModel, target: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Why a push would reject `target`, or undefined when it accepts it. `at` says
|
|
52
|
+
* where it is written.
|
|
53
|
+
*/
|
|
54
|
+
export declare function targetProblem(at: string, target: string): string | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* The deployment names a `--deployment` flag selects, or undefined when the
|
|
57
|
+
* flag is absent or negated. The flag is repeated or comma-separated. It
|
|
58
|
+
* arrives as a string, as an array of them, or as a boolean when
|
|
59
|
+
* it is absent or negated. Pieces are trimmed, empty ones dropped, and a
|
|
60
|
+
* repeated name kept once, at its first position. Throws when any occurrence
|
|
61
|
+
* of the flag names no deployment. A bare occurrence in an array
|
|
62
|
+
* arrives as `true` and names none.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseDeploymentSelection(raw?: string | boolean | Array<string | boolean>): string[] | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* A model's deployments with each target parsed into its engine and
|
|
67
|
+
* coordinates. Returns every deployment in declaration order, or only the
|
|
68
|
+
* `selected` names, in the order given. Fails on a selected name the model
|
|
69
|
+
* does not declare, and on a target in no accepted form.
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolveModelDeployments(model: SemanticModel, selected?: string[]): {
|
|
72
|
+
deployments: ResolvedDeployment[];
|
|
73
|
+
} | {
|
|
74
|
+
error: string;
|
|
75
|
+
};
|
|
76
|
+
export declare function googleBlockTargets(exts: readonly CustomExtension[] | undefined, modelName: string): string[];
|
|
77
|
+
export declare function unnamedDeployments(targets: readonly string[]): Array<{
|
|
78
|
+
name: string;
|
|
79
|
+
target: string;
|
|
80
|
+
}>;
|
|
81
|
+
export declare function googleDeploymentTargets(model: SemanticModel): GoogleDeploymentTargets;
|
|
82
|
+
export declare function bigQueryGraphTargets(model: SemanticModel): {
|
|
83
|
+
targets: BigQueryGraphTarget[];
|
|
84
|
+
malformed: string[];
|
|
85
|
+
};
|
|
86
|
+
export declare function spannerGraphTargets(model: SemanticModel): {
|
|
87
|
+
targets: SpannerGraphTarget[];
|
|
88
|
+
malformed: string[];
|
|
89
|
+
};
|
|
90
|
+
export declare function alloyDbTargets(model: SemanticModel): {
|
|
91
|
+
targets: AlloyDbTarget[];
|
|
92
|
+
malformed: string[];
|
|
93
|
+
};
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Reads where a model deploys and classifies each declared target.
|
|
3
|
+
//
|
|
4
|
+
// A semantic model names where it deploys with its `deployments:` list. Each
|
|
5
|
+
// entry has a name, a target and optionally the binding profile it uses. The
|
|
6
|
+
// loader also folds every target into `deploymentTargets` in a GOOGLE
|
|
7
|
+
// `custom_extensions` block, which is also where a vanilla document's GOOGLE
|
|
8
|
+
// block carries them. Three destination types are recognized here, each a
|
|
9
|
+
// GCP resource name, written with no leading `//`:
|
|
10
|
+
//
|
|
11
|
+
// BigQuery Graph:
|
|
12
|
+
// bigquery.googleapis.com/projects/<p>/datasets/<d>/propertyGraphs/<g>
|
|
13
|
+
// Spanner Graph:
|
|
14
|
+
// spanner.googleapis.com/projects/<p>/instances/<i>/databases/<db>/graphs/<g>
|
|
15
|
+
// AlloyDB database:
|
|
16
|
+
// alloydb.googleapis.com/projects/<p>/locations/<l>/clusters/<c>/databases/<db>
|
|
17
|
+
//
|
|
18
|
+
// The first two name a GRAPH; the third names a DATABASE, and the difference is
|
|
19
|
+
// not an oversight. AlloyDB has no property-graph DDL, so there is no graph to
|
|
20
|
+
// address and a URI pretending otherwise would name something that cannot be
|
|
21
|
+
// created. What an AlloyDB target says is where the model's rows live -- which
|
|
22
|
+
// is the whole of what a runtime needs in order to read an entity and carry out
|
|
23
|
+
// an action. A model bound to it deploys to Knowledge Catalog and runs; it does
|
|
24
|
+
// not deploy a graph, and push says so in a note rather than reporting the URI
|
|
25
|
+
// as a typo.
|
|
26
|
+
//
|
|
27
|
+
// The deploy legs (deploy_bigquery, deploy_spanner) and the push-time validator
|
|
28
|
+
// all derive their targets from this one reader, so a multi-destination push
|
|
29
|
+
// parses each model's targets once rather than once per reader.
|
|
30
|
+
//
|
|
31
|
+
// The capture groups are restricted to valid identifier characters: the
|
|
32
|
+
// components are interpolated into DDL unescaped (see the generators), so a
|
|
33
|
+
// permissive `[^/]+` would let backticks or semicolons through. A URI that
|
|
34
|
+
// matches neither destination type is collected as `malformed` (rejected by the
|
|
35
|
+
// validator and named in errors) rather than silently skipped.
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.ALLOYDB_FORM = exports.SPANNER_GRAPH_FORM = exports.BQ_GRAPH_FORM = void 0;
|
|
38
|
+
exports.deploymentTargetError = deploymentTargetError;
|
|
39
|
+
exports.googleBlockTargetError = googleBlockTargetError;
|
|
40
|
+
exports.targetProblem = targetProblem;
|
|
41
|
+
exports.parseDeploymentSelection = parseDeploymentSelection;
|
|
42
|
+
exports.resolveModelDeployments = resolveModelDeployments;
|
|
43
|
+
exports.googleBlockTargets = googleBlockTargets;
|
|
44
|
+
exports.unnamedDeployments = unnamedDeployments;
|
|
45
|
+
exports.googleDeploymentTargets = googleDeploymentTargets;
|
|
46
|
+
exports.bigQueryGraphTargets = bigQueryGraphTargets;
|
|
47
|
+
exports.spannerGraphTargets = spannerGraphTargets;
|
|
48
|
+
exports.alloyDbTargets = alloyDbTargets;
|
|
49
|
+
const loader_1 = require("./loader");
|
|
50
|
+
const resolve_profiles_1 = require("./resolve_profiles");
|
|
51
|
+
// Our own vendor tag in the Ossie `custom_extensions` list.
|
|
52
|
+
const GOOGLE_VENDOR = 'GOOGLE';
|
|
53
|
+
// A BigQuery Graph deployment target.
|
|
54
|
+
const BQ_GRAPH_TARGET = /^\/\/bigquery\.googleapis\.com\/projects\/([A-Za-z0-9_-]+)\/datasets\/([A-Za-z0-9_-]+)\/propertyGraphs\/([A-Za-z0-9_-]+)$/;
|
|
55
|
+
// A Spanner Graph deployment target, named as Dataplex names the graph's
|
|
56
|
+
// entry: `graphs/<g>`, where a BigQuery graph is `propertyGraphs/<g>`.
|
|
57
|
+
// Instance and database ids follow Spanner's resource-id grammar (lowercase
|
|
58
|
+
// letters, digits, hyphens); the graph name is a GoogleSQL identifier. As with
|
|
59
|
+
// BigQuery, the segments are interpolated into DDL (the graph name) and into
|
|
60
|
+
// the Spanner Admin API path (project/instance/database), so the character
|
|
61
|
+
// classes stay strict.
|
|
62
|
+
const SPANNER_GRAPH_TARGET = /^\/\/spanner\.googleapis\.com\/projects\/([A-Za-z0-9_-]+)\/instances\/([A-Za-z0-9_-]+)\/databases\/([A-Za-z0-9_-]+)\/graphs\/([A-Za-z0-9_-]+)$/;
|
|
63
|
+
// A Spanner graph written with BigQuery's `propertyGraphs` segment, which names
|
|
64
|
+
// no Dataplex entry. Push rejects it and names the `graphs` form. Its segments
|
|
65
|
+
// follow SPANNER_GRAPH_TARGET's, so the name it suggests is accepted.
|
|
66
|
+
const SPANNER_PROPERTY_GRAPH = /^(spanner\.googleapis\.com\/projects\/[A-Za-z0-9_-]+\/instances\/[A-Za-z0-9_-]+\/databases\/[A-Za-z0-9_-]+)\/propertyGraphs\/([A-Za-z0-9_-]+)$/;
|
|
67
|
+
// An AlloyDB database. A database belongs to its cluster, and AlloyDB locates
|
|
68
|
+
// a cluster by region, which is why there is a `locations` segment here and
|
|
69
|
+
// none in the Spanner target. A database name is a PostgreSQL identifier, and
|
|
70
|
+
// the strict class serves the same purpose as it does above: the name is
|
|
71
|
+
// interpolated into a connection string and into generated SQL, so a
|
|
72
|
+
// permissive class would let a quote character through.
|
|
73
|
+
const ALLOYDB_TARGET = /^\/\/alloydb\.googleapis\.com\/projects\/([A-Za-z0-9_-]+)\/locations\/([A-Za-z0-9_-]+)\/clusters\/([A-Za-z0-9_-]+)\/databases\/([A-Za-z0-9_-]+)$/;
|
|
74
|
+
// The instance-based AlloyDB URI kcmd used before. It names no real resource,
|
|
75
|
+
// and no target parses as it. It exists only so targetError can name the
|
|
76
|
+
// cluster-based form to write instead.
|
|
77
|
+
const LEGACY_ALLOYDB_TARGET = /^\/\/alloydb\.googleapis\.com\/projects\/([A-Za-z0-9_-]+)\/locations\/([A-Za-z0-9_-]+)\/clusters\/([A-Za-z0-9_-]+)\/instances\/([A-Za-z0-9_-]+)\/databases\/([A-Za-z0-9_-]+)$/;
|
|
78
|
+
// The GCP resource name form of each destination type, for messages.
|
|
79
|
+
exports.BQ_GRAPH_FORM = 'bigquery.googleapis.com/projects/<p>/datasets/<d>/propertyGraphs/<g>';
|
|
80
|
+
exports.SPANNER_GRAPH_FORM = 'spanner.googleapis.com/projects/<p>/instances/<i>/databases/<db>/graphs/<g>';
|
|
81
|
+
exports.ALLOYDB_FORM = 'alloydb.googleapis.com/projects/<p>/locations/<l>/clusters/<c>/databases/<db>';
|
|
82
|
+
// The Knowledge Catalog name prefixes of a graph or an AlloyDB database, each
|
|
83
|
+
// with the resource name form to write instead. kcmd accepts no catalog name as
|
|
84
|
+
// a target. `bigquery:propertygraph:` is a deprecated synonym for
|
|
85
|
+
// `bigquery:graph:`.
|
|
86
|
+
const CATALOG_PREFIXES = [
|
|
87
|
+
['bigquery:graph:', exports.BQ_GRAPH_FORM],
|
|
88
|
+
['bigquery:propertygraph:', exports.BQ_GRAPH_FORM],
|
|
89
|
+
['spanner:graph:', exports.SPANNER_GRAPH_FORM],
|
|
90
|
+
['alloydb:', exports.ALLOYDB_FORM],
|
|
91
|
+
];
|
|
92
|
+
// Parses a deployment target written as a GCP resource name. Returns undefined
|
|
93
|
+
// for any other form, the resource URI with its leading `//` included. `uri`
|
|
94
|
+
// in the result is the target as written.
|
|
95
|
+
function parseTarget(uri) {
|
|
96
|
+
if (uri.startsWith('//'))
|
|
97
|
+
return undefined;
|
|
98
|
+
const resource = (0, loader_1.asResourceUri)(uri);
|
|
99
|
+
const bq = resource.match(BQ_GRAPH_TARGET);
|
|
100
|
+
if (bq) {
|
|
101
|
+
return {
|
|
102
|
+
engine: 'bigquery',
|
|
103
|
+
coordinates: { project: bq[1], dataset: bq[2], graphName: bq[3], uri },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const sp = resource.match(SPANNER_GRAPH_TARGET);
|
|
107
|
+
if (sp) {
|
|
108
|
+
return {
|
|
109
|
+
engine: 'spanner',
|
|
110
|
+
coordinates: {
|
|
111
|
+
project: sp[1],
|
|
112
|
+
instance: sp[2],
|
|
113
|
+
database: sp[3],
|
|
114
|
+
graphName: sp[4],
|
|
115
|
+
uri,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const ad = resource.match(ALLOYDB_TARGET);
|
|
120
|
+
if (ad) {
|
|
121
|
+
return {
|
|
122
|
+
engine: 'alloydb',
|
|
123
|
+
coordinates: {
|
|
124
|
+
project: ad[1],
|
|
125
|
+
location: ad[2],
|
|
126
|
+
cluster: ad[3],
|
|
127
|
+
database: ad[4],
|
|
128
|
+
uri,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
// The cluster-based URI of the database a legacy instance-based AlloyDB URI
|
|
135
|
+
// names.
|
|
136
|
+
function clusterUri(legacy) {
|
|
137
|
+
return `//alloydb.googleapis.com/projects/${legacy[1]}/locations/${legacy[2]}/clusters/${legacy[3]}/databases/${legacy[5]}`;
|
|
138
|
+
}
|
|
139
|
+
/** Why a deployment's target is not accepted in a `deployments:` list. */
|
|
140
|
+
function deploymentTargetError(model, d) {
|
|
141
|
+
return targetError(`Model '${model.name}': deployment '${d.name}' targets '${d.target}'`, d.target);
|
|
142
|
+
}
|
|
143
|
+
/** Why a target in a GOOGLE block's `deploymentTargets` is not accepted. */
|
|
144
|
+
function googleBlockTargetError(model, target) {
|
|
145
|
+
return targetError(`Model '${model.name}': the GOOGLE custom_extension's ` +
|
|
146
|
+
`'deploymentTargets' lists '${target}'`, target);
|
|
147
|
+
}
|
|
148
|
+
// The Dataplex name of a Spanner graph `target` names with `propertyGraphs`,
|
|
149
|
+
// or undefined for any other target.
|
|
150
|
+
function spannerGraphName(target) {
|
|
151
|
+
const m = (0, loader_1.asResourceName)(target).match(SPANNER_PROPERTY_GRAPH);
|
|
152
|
+
return m ? `${m[1]}/graphs/${m[2]}` : undefined;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Why a push would reject `target`, or undefined when it accepts it. `at` says
|
|
156
|
+
* where it is written.
|
|
157
|
+
*/
|
|
158
|
+
function targetProblem(at, target) {
|
|
159
|
+
return parseTarget(target) ? undefined : targetError(at, target);
|
|
160
|
+
}
|
|
161
|
+
/** Why `target` is not accepted. `at` says where it is written. */
|
|
162
|
+
function targetError(at, target) {
|
|
163
|
+
const legacy = (0, loader_1.asResourceUri)(target).match(LEGACY_ALLOYDB_TARGET);
|
|
164
|
+
if (legacy) {
|
|
165
|
+
return `${at}, the instance-based AlloyDB name, which names no real ` +
|
|
166
|
+
`resource. An AlloyDB database belongs to its cluster; write '${(0, loader_1.asResourceName)(clusterUri(legacy))}'.`;
|
|
167
|
+
}
|
|
168
|
+
const name = (0, loader_1.asResourceName)(target);
|
|
169
|
+
if (name !== target && parseTarget(name)) {
|
|
170
|
+
return `${at}, which is ${(0, loader_1.resourceUriFix)(target)}.`;
|
|
171
|
+
}
|
|
172
|
+
const graph = spannerGraphName(target);
|
|
173
|
+
if (graph) {
|
|
174
|
+
return `${at}, which names a Spanner graph by 'propertyGraphs'; write ` +
|
|
175
|
+
`its Dataplex name: '${graph}'.`;
|
|
176
|
+
}
|
|
177
|
+
const form = CATALOG_PREFIXES.find(([prefix]) => target.startsWith(prefix))?.[1];
|
|
178
|
+
if (form) {
|
|
179
|
+
return `${at}, which is a Knowledge Catalog name. Write the GCP resource ` +
|
|
180
|
+
`name instead: '${form}'.`;
|
|
181
|
+
}
|
|
182
|
+
return `${at}, which is not a BigQuery graph, a Spanner graph or an ` +
|
|
183
|
+
`AlloyDB database. Write a GCP resource name: '${exports.BQ_GRAPH_FORM}', '${exports.SPANNER_GRAPH_FORM}' or '${exports.ALLOYDB_FORM}'.`;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The deployment names a `--deployment` flag selects, or undefined when the
|
|
187
|
+
* flag is absent or negated. The flag is repeated or comma-separated. It
|
|
188
|
+
* arrives as a string, as an array of them, or as a boolean when
|
|
189
|
+
* it is absent or negated. Pieces are trimmed, empty ones dropped, and a
|
|
190
|
+
* repeated name kept once, at its first position. Throws when any occurrence
|
|
191
|
+
* of the flag names no deployment. A bare occurrence in an array
|
|
192
|
+
* arrives as `true` and names none.
|
|
193
|
+
*/
|
|
194
|
+
function parseDeploymentSelection(raw) {
|
|
195
|
+
if (raw === undefined || typeof raw === 'boolean')
|
|
196
|
+
return undefined;
|
|
197
|
+
const noNames = `--deployment names no deployment; give one or more ` +
|
|
198
|
+
`deployment names, comma-separated.`;
|
|
199
|
+
const names = [];
|
|
200
|
+
const parts = Array.isArray(raw) ? raw : [raw];
|
|
201
|
+
for (const part of parts) {
|
|
202
|
+
const pieces = typeof part === 'boolean' ?
|
|
203
|
+
[] :
|
|
204
|
+
String(part).split(',').map(p => p.trim()).filter(p => p);
|
|
205
|
+
if (!pieces.length)
|
|
206
|
+
throw new Error(noNames);
|
|
207
|
+
for (const name of pieces) {
|
|
208
|
+
if (!names.includes(name))
|
|
209
|
+
names.push(name);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (!names.length)
|
|
213
|
+
throw new Error(noNames);
|
|
214
|
+
return names;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* A model's deployments with each target parsed into its engine and
|
|
218
|
+
* coordinates. Returns every deployment in declaration order, or only the
|
|
219
|
+
* `selected` names, in the order given. Fails on a selected name the model
|
|
220
|
+
* does not declare, and on a target in no accepted form.
|
|
221
|
+
*/
|
|
222
|
+
function resolveModelDeployments(model, selected) {
|
|
223
|
+
const declared = model.deployments ?? [];
|
|
224
|
+
let chosen = declared;
|
|
225
|
+
if (selected !== undefined) {
|
|
226
|
+
const byName = new Map(declared.map(d => [d.name, d]));
|
|
227
|
+
chosen = [];
|
|
228
|
+
for (const name of selected) {
|
|
229
|
+
const d = byName.get(name);
|
|
230
|
+
if (!d) {
|
|
231
|
+
const available = declared.length ? declared.map(x => x.name).join(', ') : 'none';
|
|
232
|
+
return {
|
|
233
|
+
error: `Model '${model.name}' has no deployment named '${name}' (available: ${available}).`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
chosen.push(d);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const deployments = [];
|
|
240
|
+
for (const d of chosen) {
|
|
241
|
+
const parsed = parseTarget(d.target);
|
|
242
|
+
if (!parsed)
|
|
243
|
+
return { error: deploymentTargetError(model, d) };
|
|
244
|
+
deployments.push({
|
|
245
|
+
...parsed,
|
|
246
|
+
name: d.name,
|
|
247
|
+
profile: d.profile ?? resolve_profiles_1.DEFAULT_PROFILE,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return { deployments };
|
|
251
|
+
}
|
|
252
|
+
// Memoizes the parse per model object so the several callers within one push
|
|
253
|
+
// (validation, source-project lookup, per-leg routing, each deploy leg) share a
|
|
254
|
+
// single parse rather than re-running JSON.parse + the regexes once per reader.
|
|
255
|
+
// Keyed by model identity: the IR object is stable through a push (the loader
|
|
256
|
+
// and inheritance passes each hand on a fixed set of objects) and
|
|
257
|
+
// neither its deployments nor its customExtensions are mutated after loading.
|
|
258
|
+
// A caller that narrows `deployments` builds a new model object. The returned
|
|
259
|
+
// value is read-only by convention -- no caller mutates its arrays.
|
|
260
|
+
const targetsCache = new WeakMap();
|
|
261
|
+
// The targets in a model's GOOGLE custom_extension(s). The extension `data` is
|
|
262
|
+
// an opaque, vendor-serialized JSON string (the loader keeps it verbatim); we
|
|
263
|
+
// own its `deploymentTargets` shape. Throws on malformed extension JSON.
|
|
264
|
+
function extensionTargets(model) {
|
|
265
|
+
return googleBlockTargets(model.customExtensions, model.name);
|
|
266
|
+
}
|
|
267
|
+
// The targets listed in the GOOGLE blocks among `exts`, in order. A string
|
|
268
|
+
// target is trimmed, and a blank one is dropped. A padded target is the same
|
|
269
|
+
// target, so trimming here keeps the duplicate check and the stored list in
|
|
270
|
+
// step. An entry that is not a string is kept as its JSON text, which parses
|
|
271
|
+
// as no supported destination. Throws when a GOOGLE block's data is not valid
|
|
272
|
+
// JSON.
|
|
273
|
+
function googleBlockTargets(exts, modelName) {
|
|
274
|
+
const uris = [];
|
|
275
|
+
for (const ext of exts ?? []) {
|
|
276
|
+
if (ext.vendorName !== GOOGLE_VENDOR) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
let data;
|
|
280
|
+
try {
|
|
281
|
+
data = JSON.parse(ext.data);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
throw new Error(`Model '${modelName}': GOOGLE custom_extension 'data' is not valid JSON.`);
|
|
285
|
+
}
|
|
286
|
+
const list = data?.deploymentTargets;
|
|
287
|
+
if (!Array.isArray(list)) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
for (const uri of list) {
|
|
291
|
+
if (typeof uri !== 'string') {
|
|
292
|
+
uris.push(JSON.stringify(uri));
|
|
293
|
+
}
|
|
294
|
+
else if (uri.trim() !== '') {
|
|
295
|
+
uris.push(uri.trim());
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return uris;
|
|
300
|
+
}
|
|
301
|
+
// Deployments for targets whose names are not known: a pull reads only the
|
|
302
|
+
// targets back from the catalog. One target is `default`; several are
|
|
303
|
+
// `deployment_1`, `deployment_2`, and so on. Targets are trimmed, and blank
|
|
304
|
+
// ones dropped. A repeated target is dropped too, so the list loads as a
|
|
305
|
+
// `deployments:` list.
|
|
306
|
+
function unnamedDeployments(targets) {
|
|
307
|
+
const seen = new Set();
|
|
308
|
+
const kept = targets.map(t => t.trim()).filter(t => {
|
|
309
|
+
if (t === '' || seen.has(t))
|
|
310
|
+
return false;
|
|
311
|
+
seen.add(t);
|
|
312
|
+
return true;
|
|
313
|
+
});
|
|
314
|
+
return kept.map((target, i) => ({
|
|
315
|
+
name: kept.length === 1 ? 'default' : `deployment_${i + 1}`,
|
|
316
|
+
target,
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
// Reads a model's deployment targets in a single pass and returns the facts
|
|
320
|
+
// every caller derives from them. The targets come from `model.deployments`
|
|
321
|
+
// when the model has it, and otherwise from its GOOGLE custom_extension(s).
|
|
322
|
+
// Never both: in the Google flavor, the loader's GOOGLE block holds a copy of
|
|
323
|
+
// the list. Throws on malformed extension JSON.
|
|
324
|
+
function googleDeploymentTargets(model) {
|
|
325
|
+
const cached = targetsCache.get(model);
|
|
326
|
+
if (cached)
|
|
327
|
+
return cached;
|
|
328
|
+
const uris = model.deployments !== undefined ?
|
|
329
|
+
model.deployments.map(d => d.target) :
|
|
330
|
+
extensionTargets(model);
|
|
331
|
+
const bigQuery = [];
|
|
332
|
+
const spanner = [];
|
|
333
|
+
const alloyDb = [];
|
|
334
|
+
const malformed = [];
|
|
335
|
+
for (const uri of uris) {
|
|
336
|
+
const parsed = parseTarget(uri);
|
|
337
|
+
if (parsed?.engine === 'bigquery') {
|
|
338
|
+
bigQuery.push(parsed.coordinates);
|
|
339
|
+
}
|
|
340
|
+
else if (parsed?.engine === 'spanner') {
|
|
341
|
+
spanner.push(parsed.coordinates);
|
|
342
|
+
}
|
|
343
|
+
else if (parsed?.engine === 'alloydb') {
|
|
344
|
+
alloyDb.push(parsed.coordinates);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
// Matches no supported destination type: collect it as malformed
|
|
348
|
+
// (rejected by the validator) rather than silently ignoring it.
|
|
349
|
+
malformed.push(uri);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const result = { uris, bigQuery, spanner, alloyDb, malformed };
|
|
353
|
+
targetsCache.set(model, result);
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
// The BigQuery Graph deployment targets a model declares, plus any URIs that
|
|
357
|
+
// parse as no supported destination type. A view over googleDeploymentTargets,
|
|
358
|
+
// preserving the historical shape the BigQuery leg and Knowledge Catalog leg
|
|
359
|
+
// consume. Note `malformed` excludes a valid Spanner Graph or AlloyDB target:
|
|
360
|
+
// each is a recognized destination, just not a BigQuery one, so neither is
|
|
361
|
+
// reported as a BigQuery typo.
|
|
362
|
+
function bigQueryGraphTargets(model) {
|
|
363
|
+
const { bigQuery, malformed } = googleDeploymentTargets(model);
|
|
364
|
+
return { targets: bigQuery, malformed };
|
|
365
|
+
}
|
|
366
|
+
// The Spanner Graph deployment targets a model declares, plus any URIs that
|
|
367
|
+
// parse as no supported destination type. Symmetric to bigQueryGraphTargets.
|
|
368
|
+
function spannerGraphTargets(model) {
|
|
369
|
+
const { spanner, malformed } = googleDeploymentTargets(model);
|
|
370
|
+
return { targets: spanner, malformed };
|
|
371
|
+
}
|
|
372
|
+
// The AlloyDB database targets a model declares. Symmetric to the two above in
|
|
373
|
+
// shape, but there is no deploy leg behind it: an AlloyDB target is a store to
|
|
374
|
+
// run against, not a graph to create, so the only callers are store resolution
|
|
375
|
+
// and the messages that explain why a graph push has nothing to do.
|
|
376
|
+
function alloyDbTargets(model) {
|
|
377
|
+
const { alloyDb, malformed } = googleDeploymentTargets(model);
|
|
378
|
+
return { targets: alloyDb, malformed };
|
|
379
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { AiContext, Entity, Field, Metric, SemanticModel, SqlDialect } from './ir';
|
|
2
|
+
export interface GenerateResult {
|
|
3
|
+
ddl: string;
|
|
4
|
+
warnings: string[];
|
|
5
|
+
}
|
|
6
|
+
export interface MetricPlacementInput {
|
|
7
|
+
entityByName: ReadonlyMap<string, Entity>;
|
|
8
|
+
skipped: ReadonlySet<string>;
|
|
9
|
+
ancestorsUsed: ReadonlySet<string>;
|
|
10
|
+
}
|
|
11
|
+
export interface GraphEngine {
|
|
12
|
+
name: string;
|
|
13
|
+
table(dataSource: string, warnings: string[], context: string): string;
|
|
14
|
+
graphName(model: SemanticModel): string;
|
|
15
|
+
placeMetrics(metrics: Metric[], input: MetricPlacementInput, warnings: string[]): Map<string, string[]>;
|
|
16
|
+
elementOptions(description: string | undefined, ai: AiContext | undefined): string | undefined;
|
|
17
|
+
fieldOptions(field: Field): string | undefined;
|
|
18
|
+
dialect: SqlDialect;
|
|
19
|
+
fieldExpression(field: Field): string | undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Generates the property-graph DDL for a semantic model on one engine.
|
|
23
|
+
*
|
|
24
|
+
* The generated statement references the entities' existing base tables; it
|
|
25
|
+
* does not create them. Returns the DDL plus any warnings collected while
|
|
26
|
+
* mapping the IR (e.g. metrics that could not be placed on a single table).
|
|
27
|
+
*
|
|
28
|
+
* Throws when the model yields no valid node table -- it declares no entities,
|
|
29
|
+
* or every entity lacks a KEY -- since a property graph with an empty NODE
|
|
30
|
+
* TABLES block is invalid DDL that the engine would reject.
|
|
31
|
+
*/
|
|
32
|
+
export declare function generateGraphDdl(model: SemanticModel, engine: GraphEngine): GenerateResult;
|
|
33
|
+
export declare function nodeKey(entity: Pick<Entity, 'keys' | 'uniqueKeys'>): string[];
|
|
34
|
+
export declare function physicalColumn(column: string): string;
|