@google-cloud/kcmd 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -20
- package/build/ts/kcmd/cli.js +6164 -5631
- package/build/ts/kcmd/gcp/biglake.d.ts +2 -0
- package/build/ts/kcmd/gcp/biglake.js +13 -4
- package/build/ts/kcmd/gcp/spanner.d.ts +10 -0
- package/build/ts/kcmd/gcp/spanner.js +6 -3
- package/build/ts/kcmd/layouts/okf.js +49 -7
- package/build/ts/kcmd/layouts/semantic-model.d.ts +13 -0
- package/build/ts/kcmd/layouts/semantic-model.js +129 -50
- package/build/ts/kcmd/okf/concept.d.ts +1 -1
- package/build/ts/kcmd/okf/concept.js +6 -0
- package/build/ts/kcmd/okf/concept_to_entry.d.ts +2 -0
- package/build/ts/kcmd/okf/concept_to_entry.js +29 -2
- package/build/ts/kcmd/okf/init_okf_workspace.js +7 -9
- package/build/ts/kcmd/okf/okf_metadata.js +17 -0
- package/build/ts/kcmd/semantic/bigquery.js +139 -195
- package/build/ts/kcmd/semantic/binding.js +14 -19
- package/build/ts/kcmd/semantic/deploy_bigquery.js +6 -13
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +10 -35
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +105 -162
- package/build/ts/kcmd/semantic/deploy_spanner.js +7 -14
- package/build/ts/kcmd/semantic/deployment_target.d.ts +1 -4
- package/build/ts/kcmd/semantic/deployment_target.js +6 -62
- package/build/ts/kcmd/semantic/graph_ddl.d.ts +10 -0
- package/build/ts/kcmd/semantic/graph_ddl.js +152 -59
- package/build/ts/kcmd/semantic/import_model.d.ts +26 -4
- package/build/ts/kcmd/semantic/import_model.js +531 -111
- package/build/ts/kcmd/semantic/ir.d.ts +2 -4
- package/build/ts/kcmd/semantic/kc_converter.d.ts +7 -1
- package/build/ts/kcmd/semantic/kc_converter.js +32 -27
- package/build/ts/kcmd/semantic/kc_custom_types.d.ts +5 -12
- package/build/ts/kcmd/semantic/kc_custom_types.js +43 -114
- package/build/ts/kcmd/semantic/kc_ids.d.ts +4 -5
- package/build/ts/kcmd/semantic/kc_ids.js +4 -7
- package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +0 -3
- package/build/ts/kcmd/semantic/knowledge_catalog.js +3 -86
- package/build/ts/kcmd/semantic/loader.d.ts +7 -11
- package/build/ts/kcmd/semantic/loader.js +282 -171
- package/build/ts/kcmd/semantic/osi_converter.js +14 -65
- package/build/ts/kcmd/semantic/parameters.d.ts +1 -0
- package/build/ts/kcmd/semantic/parameters.js +22 -3
- package/build/ts/kcmd/semantic/pull_kc.d.ts +1 -5
- package/build/ts/kcmd/semantic/pull_kc.js +38 -112
- package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +7 -2
- package/build/ts/kcmd/semantic/resolve_inheritance.js +37 -21
- package/build/ts/kcmd/semantic/resolve_profiles.d.ts +51 -10
- package/build/ts/kcmd/semantic/resolve_profiles.js +213 -32
- package/build/ts/kcmd/semantic/runtime/runtime.d.ts +4 -12
- package/build/ts/kcmd/semantic/runtime/runtime.js +4 -21
- package/build/ts/kcmd/semantic/runtime/store.d.ts +14 -19
- package/build/ts/kcmd/semantic/runtime/store.js +21 -37
- package/build/ts/kcmd/semantic/source_dialect.d.ts +9 -2
- package/build/ts/kcmd/semantic/source_dialect.js +83 -33
- package/build/ts/kcmd/semantic/source_entries.d.ts +1 -1
- package/build/ts/kcmd/semantic/source_entries.js +95 -36
- package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +13 -7
- package/build/ts/kcmd/semantic/sql_expr_utils.js +240 -67
- package/build/ts/kcmd/semantic/sql_parser.d.ts +5 -2
- package/build/ts/kcmd/semantic/sql_parser.js +22 -0
- package/build/ts/kcmd/semantic/transpile.d.ts +12 -10
- package/build/ts/kcmd/semantic/transpile.js +79 -61
- package/build/ts/kcmd/semantic/validate.d.ts +37 -2
- package/build/ts/kcmd/semantic/validate.js +467 -126
- package/build/ts/kcmd/snapshot.d.ts +4 -0
- package/build/ts/kcmd/snapshot.js +7 -0
- package/build/ts/kcmd/sync.js +44 -7
- package/package.json +1 -1
|
@@ -45,6 +45,15 @@ const HEADING = '## OKF Metadata';
|
|
|
45
45
|
const OPEN = '```yaml';
|
|
46
46
|
const CLOSE = '```';
|
|
47
47
|
const MARKER = `\n\n${HEADING}\n\n${OPEN}\n`;
|
|
48
|
+
// Frontmatter keys stored natively on the catalog entry or in kcmd residue/links,
|
|
49
|
+
// so they are not duplicated into the trailing `## OKF Metadata` block.
|
|
50
|
+
const NON_CARRIED_KEYS = new Set([
|
|
51
|
+
'type',
|
|
52
|
+
...concept_1.ENTRY_SOURCE_KEYS,
|
|
53
|
+
concept_1.RESIDUE_KEY,
|
|
54
|
+
concept_1.EXTRA_KEY,
|
|
55
|
+
'links',
|
|
56
|
+
]);
|
|
48
57
|
/** Appends the section. The body is returned unchanged when there is none. */
|
|
49
58
|
function appendOkfMetadata(body, frontmatter) {
|
|
50
59
|
const carried = {};
|
|
@@ -52,6 +61,14 @@ function appendOkfMetadata(body, frontmatter) {
|
|
|
52
61
|
if (frontmatter[key] !== undefined)
|
|
53
62
|
carried[key] = frontmatter[key];
|
|
54
63
|
}
|
|
64
|
+
// Preserve `x-*` producer extensions (SPEC §4.1) and any other unknown OKF
|
|
65
|
+
// frontmatter keys across push -> pull.
|
|
66
|
+
for (const [key, value] of Object.entries(frontmatter)) {
|
|
67
|
+
if (value === undefined || key in carried || NON_CARRIED_KEYS.has(key)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
carried[key] = value;
|
|
71
|
+
}
|
|
55
72
|
if (!Object.keys(carried).length)
|
|
56
73
|
return body;
|
|
57
74
|
const block = yaml.stringify(carried);
|
|
@@ -27,6 +27,7 @@ const graph_ddl_1 = require("./graph_ddl");
|
|
|
27
27
|
const ir_1 = require("./ir");
|
|
28
28
|
const sql_expr_utils_1 = require("./sql_expr_utils");
|
|
29
29
|
const sql_identifiers_1 = require("./sql_identifiers");
|
|
30
|
+
const sql_parser_1 = require("./sql_parser");
|
|
30
31
|
/**
|
|
31
32
|
* Generates the BigQuery property-graph DDL for a semantic model.
|
|
32
33
|
*
|
|
@@ -58,8 +59,9 @@ function bigQueryEngine(opts) {
|
|
|
58
59
|
// it to a MEASURE over an exposed property (see placeMetric).
|
|
59
60
|
function placeMetrics(metrics, input, warnings) {
|
|
60
61
|
const loweringByEntity = new Map();
|
|
62
|
+
const metricNames = new Set(metrics.map(m => m.name));
|
|
61
63
|
for (const metric of metrics) {
|
|
62
|
-
placeMetric(metric, input, loweringByEntity, warnings);
|
|
64
|
+
placeMetric(metric, input, metricNames, loweringByEntity, warnings);
|
|
63
65
|
}
|
|
64
66
|
// The operand properties come first: the measures aggregate them.
|
|
65
67
|
const placed = new Map();
|
|
@@ -69,36 +71,65 @@ function placeMetrics(metrics, input, warnings) {
|
|
|
69
71
|
return placed;
|
|
70
72
|
}
|
|
71
73
|
// Builds the lowering state for an entity, seeded with its declared fields so
|
|
72
|
-
// an operand
|
|
73
|
-
// duplicating.
|
|
74
|
-
|
|
74
|
+
// an operand with the same SQL as a field reuses that property instead of
|
|
75
|
+
// duplicating it. A field the node table leaves out has no property to reuse,
|
|
76
|
+
// and neither has an ambiguous one.
|
|
77
|
+
//
|
|
78
|
+
// A field is ambiguous when the node's table has a column of its name that the
|
|
79
|
+
// field does not read whole, such as `amount` bound to `amount_usd` while
|
|
80
|
+
// `amount_local` is bound to the column `amount`. A measure that names it
|
|
81
|
+
// names both, and BigQuery rejects the graph: "Name amount is ambiguous
|
|
82
|
+
// because it exists as both a column and a graph property". The columns the
|
|
83
|
+
// table is known to have are its key columns and those the fields' SQL reads.
|
|
84
|
+
function newLowering(entity, input, metricNames) {
|
|
75
85
|
const taken = new Set();
|
|
76
|
-
const
|
|
77
|
-
const byLocalExpr = new Map();
|
|
86
|
+
const sqlOf = new Map();
|
|
78
87
|
for (const f of entity.fields) {
|
|
79
88
|
taken.add(f.name);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
const read = input.renderedFieldSql(entity, f);
|
|
90
|
+
if ('sql' in read)
|
|
91
|
+
sqlOf.set(f.name, read.sql);
|
|
92
|
+
}
|
|
93
|
+
const columns = new Set((0, graph_ddl_1.nodeKey)(entity).map(k => k.replace(/^`(.*)`$/, '$1').toLowerCase()));
|
|
94
|
+
for (const sql of sqlOf.values()) {
|
|
95
|
+
// The SQL is local to the table, so `a.b` reads the column `a`.
|
|
96
|
+
for (const c of (0, sql_expr_utils_1.columnReferences)((0, sql_identifiers_1.quoteIfReserved)(sql), 'BIGQUERY') ?? []) {
|
|
97
|
+
columns.add((c.qualifier ?? c.name).toLowerCase());
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const ambiguous = new Set();
|
|
101
|
+
const byLocalExpr = new Map();
|
|
102
|
+
for (const [name, sql] of sqlOf) {
|
|
103
|
+
if (columns.has(name.toLowerCase()) &&
|
|
104
|
+
loneColumn(sql)?.toLowerCase() !== name.toLowerCase()) {
|
|
105
|
+
ambiguous.add(name);
|
|
106
|
+
}
|
|
107
|
+
else if (!byLocalExpr.has(sql)) {
|
|
108
|
+
byLocalExpr.set(sql, name);
|
|
109
|
+
}
|
|
87
110
|
}
|
|
88
111
|
return {
|
|
89
112
|
derivedProperties: [],
|
|
90
113
|
measures: [],
|
|
91
114
|
taken,
|
|
92
|
-
|
|
115
|
+
metricNames,
|
|
116
|
+
ambiguous,
|
|
93
117
|
byLocalExpr,
|
|
94
118
|
operandToName: new Map()
|
|
95
119
|
};
|
|
96
120
|
}
|
|
121
|
+
// The column `sql`, BigQuery SQL local to a table, is when it is one column of
|
|
122
|
+
// that table and nothing else, without its quotes, as in `amount` or
|
|
123
|
+
// `` `order` ``. Undefined otherwise.
|
|
124
|
+
function loneColumn(sql) {
|
|
125
|
+
const column = (0, sql_parser_1.soleColumn)((0, sql_identifiers_1.quoteIfReserved)(sql), 'BIGQUERY');
|
|
126
|
+
return column && column.qualifier === undefined ? column.name : undefined;
|
|
127
|
+
}
|
|
97
128
|
// Assigns a metric to the node table of the single entity its aggregate
|
|
98
129
|
// references, lowering it to a MEASURE over an exposed property. Records a
|
|
99
130
|
// warning (and skips) when it references zero/multiple entities or is not a
|
|
100
131
|
// single supported aggregate over one operand.
|
|
101
|
-
function placeMetric(metric, input, loweringByEntity, warnings) {
|
|
132
|
+
function placeMetric(metric, input, metricNames, loweringByEntity, warnings) {
|
|
102
133
|
const { entityByName, skipped, ancestorsUsed } = input;
|
|
103
134
|
// A measure is emitted from the metric's BigQuery SQL (see
|
|
104
135
|
// metricExpression). A metric with SQL only for other engines is skipped
|
|
@@ -113,17 +144,15 @@ function placeMetric(metric, input, loweringByEntity, warnings) {
|
|
|
113
144
|
`to emit as a MEASURE)`);
|
|
114
145
|
return;
|
|
115
146
|
}
|
|
116
|
-
// The entities whose
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
// A qualifier-free aggregate (e.g. COUNT(*)) names no
|
|
120
|
-
// expression;
|
|
121
|
-
// authored `entity` in `0.2.0.dev0/google`, an entity
|
|
122
|
-
// dialect entry, or hand-built IR), so it can still be
|
|
123
|
-
// dropped as "references no entity".
|
|
124
|
-
|
|
125
|
-
referenced = [metric.entity];
|
|
126
|
-
}
|
|
147
|
+
// The entities whose qualifier the expression writes before a column, as
|
|
148
|
+
// the SQL parser reads it (see measureEntities), so neither a value like
|
|
149
|
+
// 'orders.x' nor a function namespace such as `NET.` in `NET.HOST(...)`
|
|
150
|
+
// references an entity. A qualifier-free aggregate (e.g. COUNT(*)) names no
|
|
151
|
+
// entity in its expression; it falls back to `metric.entity` when the IR
|
|
152
|
+
// provides one (an authored `entity` in `0.2.0.dev0/google`, an entity
|
|
153
|
+
// inferred from another dialect entry, or hand-built IR), so it can still be
|
|
154
|
+
// placed rather than dropped as "references no entity".
|
|
155
|
+
const referenced = (0, sql_expr_utils_1.measureEntities)(expression, [...entityByName.keys()], metric.entity);
|
|
127
156
|
if (referenced.length !== 1) {
|
|
128
157
|
const detail = referenced.length === 0 ?
|
|
129
158
|
'references no known entity' :
|
|
@@ -180,42 +209,49 @@ function placeMetric(metric, input, loweringByEntity, warnings) {
|
|
|
180
209
|
`subtype tables; skipped`);
|
|
181
210
|
return;
|
|
182
211
|
}
|
|
183
|
-
const body = (0, sql_expr_utils_1.
|
|
212
|
+
const body = (0, sql_expr_utils_1.withoutQualifier)(expression, entityName);
|
|
184
213
|
// A graph measure must be one supported aggregate wrapping a single operand;
|
|
185
214
|
// anything else (a non-aggregate, or a compound of aggregates like a ratio)
|
|
186
215
|
// cannot be a single MEASURE. Flag it rather than emit DDL BigQuery rejects.
|
|
187
|
-
|
|
216
|
+
// The SQL parser finds the aggregate and its operand with the entity's
|
|
217
|
+
// qualifier renamed to a name it does not reserve, since it cannot read one
|
|
218
|
+
// it reserves, as in `SUM(Date.days)`.
|
|
219
|
+
const stand = (0, sql_expr_utils_1.unusedName)(expression, 'kcmd_entity');
|
|
220
|
+
const agg = (0, sql_expr_utils_1.singleAggregate)((0, sql_expr_utils_1.requalified)(expression, entityName, stand), 'BIGQUERY');
|
|
188
221
|
if (!agg) {
|
|
189
222
|
warnings.push(`metric '${metric.name}' expression '${body}' is not a single supported aggregate ` +
|
|
190
223
|
`(${sql_expr_utils_1.SUPPORTED_AGGREGATES.join(', ')}) over one operand; skipped (cannot be a single MEASURE)`);
|
|
191
224
|
return;
|
|
192
225
|
}
|
|
193
|
-
const lowering = loweringByEntity.get(entityName) ??
|
|
226
|
+
const lowering = loweringByEntity.get(entityName) ??
|
|
227
|
+
newLowering(entity, input, metricNames);
|
|
194
228
|
loweringByEntity.set(entityName, lowering);
|
|
195
|
-
// Resolve the aggregate's operand to an exposed property.
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
229
|
+
// Resolve the aggregate's operand to an exposed property. It is read as
|
|
230
|
+
// written, qualifiers kept, so readOperand can tell the entity's fields from
|
|
231
|
+
// its columns. COUNT(*) has no operand column, so it counts the first column
|
|
232
|
+
// of the node's key instead: the primary key, else the first unique key
|
|
233
|
+
// (nodeKey). singleAggregate gives a `*` operand for COUNT(*) alone.
|
|
234
|
+
const operand = (0, sql_expr_utils_1.requalified)(agg.operand, stand, entityName);
|
|
235
|
+
let read;
|
|
236
|
+
if (operand === '*') {
|
|
237
|
+
// A key names a column, never a field, so a field of the same name does
|
|
238
|
+
// not stand in for it. A simple name is quoted, if reserved, where it is
|
|
239
|
+
// exposed (exposeOperand).
|
|
240
|
+
const key = (0, graph_ddl_1.nodeKey)(entity)[0];
|
|
241
|
+
read = {
|
|
242
|
+
sql: (0, sql_identifiers_1.isSimpleIdentifier)(key) ? key : (0, graph_ddl_1.physicalColumn)(key),
|
|
243
|
+
column: true
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
// A field the node table leaves out declares no property, so neither a
|
|
248
|
+
// measure over it nor a property derived from SQL that names it can read
|
|
249
|
+
// it.
|
|
250
|
+
read = readOperand(entity, operand, input);
|
|
210
251
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
entity.fields.some(f => f.name === operandExpr && (0, ir_1.isFieldBound)(f) &&
|
|
215
|
-
fieldExpression(f) === undefined)) {
|
|
216
|
-
warnings.push(`metric '${metric.name}' aggregates field '${operandExpr}', which ` +
|
|
217
|
-
`has no 'BIGQUERY' or 'ANSI_SQL' expression; skipped (nothing to ` +
|
|
218
|
-
`emit as a MEASURE)`);
|
|
252
|
+
if ('skip' in read) {
|
|
253
|
+
warnings.push(`metric '${metric.name}' ${read.skip}; skipped (nothing to emit as a ` +
|
|
254
|
+
`MEASURE)`);
|
|
219
255
|
return;
|
|
220
256
|
}
|
|
221
257
|
// The measure is itself exposed as a node property named `metric.name`, so it
|
|
@@ -231,168 +267,76 @@ function placeMetric(metric, input, loweringByEntity, warnings) {
|
|
|
231
267
|
return;
|
|
232
268
|
}
|
|
233
269
|
lowering.taken.add(metric.name);
|
|
234
|
-
const propName =
|
|
235
|
-
exposeKeyColumn(lowering, operandExpr, metric.name) :
|
|
236
|
-
exposeOperand(lowering, operandExpr, metric.name);
|
|
270
|
+
const propName = exposeOperand(lowering, read, metric.name);
|
|
237
271
|
const aggregate = `${agg.fn}(${agg.distinct ? 'DISTINCT ' : ''}${(0, sql_identifiers_1.quoteIfReserved)(propName)})`;
|
|
238
272
|
const opts = optionsClause(elementDescription(metric.description, metric.aiContext), metric.aiContext?.synonyms);
|
|
239
273
|
const measure = `MEASURE(${aggregate}) AS ${(0, sql_identifiers_1.quoteIfReserved)(metric.name)}`;
|
|
240
274
|
lowering.measures.push(opts ? `${measure} ${opts}` : measure);
|
|
241
275
|
}
|
|
242
|
-
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
276
|
+
function readOperand(entity, operand, input) {
|
|
277
|
+
// Checked before any column: a field is read through its own SQL, which may
|
|
278
|
+
// name another column, while a column of the same name may hold something
|
|
279
|
+
// else. The SQL parser reads the entity's qualifier renamed, as placeMetric
|
|
280
|
+
// does, since it cannot read one it reserves, as in `Date.days`.
|
|
281
|
+
const stand = (0, sql_expr_utils_1.unusedName)(operand, 'kcmd_entity');
|
|
282
|
+
const column = (0, sql_parser_1.soleColumn)((0, sql_expr_utils_1.requalified)(operand, entity.name, stand), 'BIGQUERY');
|
|
283
|
+
const own = column !== undefined &&
|
|
284
|
+
(column.qualifier === undefined ||
|
|
285
|
+
column.qualifier.toLowerCase() === stand);
|
|
286
|
+
const field = own ? entity.fields.find(f => f.name === column.name) : undefined;
|
|
287
|
+
if (field) {
|
|
288
|
+
const read = input.renderedFieldSql(entity, field);
|
|
289
|
+
return 'sql' in read ?
|
|
290
|
+
{ field: field.name, sql: read.sql, column: !!loneColumn(read.sql) } :
|
|
291
|
+
{ skip: `aggregates ${read.reads}` };
|
|
292
|
+
}
|
|
293
|
+
const read = input.localSql(entity, operand);
|
|
294
|
+
return 'sql' in read ? { sql: read.sql, column: own } :
|
|
295
|
+
{ skip: `reads ${read.reads}` };
|
|
296
|
+
}
|
|
297
|
+
// Ensures the operand `read` is exposed as a node property and returns its
|
|
298
|
+
// property name. A field is one already, unless it is ambiguous (see
|
|
299
|
+
// newLowering); its SQL is then exposed as any other SQL is. Other SQL reuses
|
|
300
|
+
// a field (or a previously derived property) with the same SQL; a column no
|
|
301
|
+
// field names is exposed under its own name; any other SQL gets a
|
|
302
|
+
// synthesized, unique name derived from the metric.
|
|
303
|
+
function exposeOperand(lowering, read, metricName) {
|
|
304
|
+
// A MEASURE may aggregate a field directly by name -- even when a profile
|
|
305
|
+
// bound that field to a differently named physical column (the property is
|
|
254
306
|
// `<column> AS <field>`, and a MEASURE may reference a sibling alias).
|
|
255
307
|
// Synthesizing an input property here would emit `<field> AS ..._input`, and
|
|
256
308
|
// an alias is illegal inside a property expression -- BigQuery rejects it with
|
|
257
|
-
// "Unrecognized name".
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
309
|
+
// "Unrecognized name".
|
|
310
|
+
if (read.field !== undefined && !lowering.ambiguous.has(read.field)) {
|
|
311
|
+
return read.field;
|
|
312
|
+
}
|
|
313
|
+
const { sql } = read;
|
|
314
|
+
const existing = lowering.byLocalExpr.get(sql) ?? lowering.operandToName.get(sql);
|
|
315
|
+
if (existing)
|
|
316
|
+
return existing;
|
|
317
|
+
const isFree = (name) => !lowering.taken.has(name) && !lowering.metricNames.has(name);
|
|
261
318
|
let name;
|
|
262
|
-
if ((0, sql_identifiers_1.isSimpleIdentifier)(
|
|
263
|
-
// A
|
|
264
|
-
name =
|
|
319
|
+
if (read.column && (0, sql_identifiers_1.isSimpleIdentifier)(sql) && isFree(sql)) {
|
|
320
|
+
// A column not already declared: expose it under its own name.
|
|
321
|
+
name = sql;
|
|
265
322
|
lowering.derivedProperties.push((0, sql_identifiers_1.quoteIfReserved)(name));
|
|
266
323
|
}
|
|
267
324
|
else {
|
|
268
|
-
name = uniqueName(`${metricName}_input`,
|
|
269
|
-
|
|
325
|
+
name = uniqueName(`${metricName}_input`, isFree);
|
|
326
|
+
// Only a column is quoted: a keyword such as TRUE stays a keyword.
|
|
327
|
+
lowering.derivedProperties.push(`${read.column ? (0, sql_identifiers_1.quoteIfReserved)(sql) : sql} AS ${name}`);
|
|
270
328
|
}
|
|
271
329
|
lowering.taken.add(name);
|
|
272
|
-
lowering.operandToName.set(
|
|
273
|
-
return name;
|
|
274
|
-
}
|
|
275
|
-
// Exposes the key column COUNT(*) counts and returns its property name,
|
|
276
|
-
// reusing a field whose expression is exactly that column, or the property an
|
|
277
|
-
// earlier metric exposed for it.
|
|
278
|
-
function exposeKeyColumn(lowering, column, metricName) {
|
|
279
|
-
const existing = lowering.byLocalExpr.get(column) ?? lowering.operandToName.get(column);
|
|
280
|
-
if (existing)
|
|
281
|
-
return existing;
|
|
282
|
-
const bare = (0, sql_identifiers_1.isSimpleIdentifier)(column) && !lowering.taken.has(column);
|
|
283
|
-
const name = bare ? column : uniqueName(`${metricName}_input`, lowering.taken);
|
|
284
|
-
lowering.derivedProperties.push(bare ? (0, sql_identifiers_1.quoteIfReserved)(column) : `${(0, graph_ddl_1.physicalColumn)(column)} AS ${name}`);
|
|
285
|
-
lowering.taken.add(name);
|
|
286
|
-
// An operand named like a field reads that field (exposeOperand), so only a
|
|
287
|
-
// column no field is named after is shared with a later metric's operand.
|
|
288
|
-
if (!lowering.fieldNames.has(column))
|
|
289
|
-
lowering.operandToName.set(column, name);
|
|
330
|
+
lowering.operandToName.set(sql, name);
|
|
290
331
|
return name;
|
|
291
332
|
}
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
// operand, or a multi-argument call). `COUNT(DISTINCT x)` is recognized,
|
|
296
|
-
// yielding operand `x` with distinct.
|
|
297
|
-
function extractAggregate(body) {
|
|
298
|
-
const s = body.trim();
|
|
299
|
-
const head = s.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*\(/);
|
|
300
|
-
if (!head)
|
|
301
|
-
return null;
|
|
302
|
-
const fn = head[1];
|
|
303
|
-
if (!sql_expr_utils_1.SUPPORTED_AGGREGATES.includes(fn.toUpperCase()))
|
|
304
|
-
return null;
|
|
305
|
-
// Find the operand bounded by the aggregate's outer parentheses, tracking
|
|
306
|
-
// string literals and backtick-quoted identifiers and nesting so inner
|
|
307
|
-
// parens/quotes/backticks don't end it early.
|
|
308
|
-
let depth = 0, inStr = false, inTick = false, start = -1, end = -1;
|
|
309
|
-
for (let i = head[0].length - 1; i < s.length; i++) {
|
|
310
|
-
const c = s[i];
|
|
311
|
-
if (inStr) {
|
|
312
|
-
if (c === '\'')
|
|
313
|
-
inStr = false;
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
if (inTick) {
|
|
317
|
-
if (c === '`')
|
|
318
|
-
inTick = false;
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
if (c === '\'') {
|
|
322
|
-
inStr = true;
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
if (c === '`') {
|
|
326
|
-
inTick = true;
|
|
327
|
-
continue;
|
|
328
|
-
}
|
|
329
|
-
if (c === '(') {
|
|
330
|
-
if (depth === 0)
|
|
331
|
-
start = i + 1;
|
|
332
|
-
depth++;
|
|
333
|
-
}
|
|
334
|
-
else if (c === ')') {
|
|
335
|
-
if (--depth === 0) {
|
|
336
|
-
end = i;
|
|
337
|
-
break;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
if (start < 0 || end < 0)
|
|
342
|
-
return null;
|
|
343
|
-
if (s.slice(end + 1).trim() !== '')
|
|
344
|
-
return null; // trailing ops => not a single aggregate
|
|
345
|
-
let operand = s.slice(start, end).trim();
|
|
346
|
-
let distinct = false;
|
|
347
|
-
const dm = operand.match(/^DISTINCT(?:\s+(.*))?$/is);
|
|
348
|
-
if (dm) {
|
|
349
|
-
distinct = true;
|
|
350
|
-
operand = (dm[1] ?? '').trim();
|
|
351
|
-
}
|
|
352
|
-
if (operand === '')
|
|
353
|
-
return null; // no operand, as in SUM() or SUM(DISTINCT)
|
|
354
|
-
if (operand !== '*' && hasTopLevelComma(operand))
|
|
355
|
-
return null; // multi-arg aggregate
|
|
356
|
-
return { fn, operand, distinct };
|
|
357
|
-
}
|
|
358
|
-
// True if `expr` contains a comma outside any nested parentheses, string
|
|
359
|
-
// literal, or backtick-quoted identifier (i.e. it is really several arguments,
|
|
360
|
-
// not one operand). A backtick-quoted column like `` `weird,name` `` must not
|
|
361
|
-
// be mistaken for a multi-argument aggregate.
|
|
362
|
-
function hasTopLevelComma(expr) {
|
|
363
|
-
let depth = 0, inStr = false, inTick = false;
|
|
364
|
-
for (const c of expr) {
|
|
365
|
-
if (inStr) {
|
|
366
|
-
if (c === '\'')
|
|
367
|
-
inStr = false;
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
if (inTick) {
|
|
371
|
-
if (c === '`')
|
|
372
|
-
inTick = false;
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
if (c === '\'')
|
|
376
|
-
inStr = true;
|
|
377
|
-
else if (c === '`')
|
|
378
|
-
inTick = true;
|
|
379
|
-
else if (c === '(')
|
|
380
|
-
depth++;
|
|
381
|
-
else if (c === ')')
|
|
382
|
-
depth--;
|
|
383
|
-
else if (c === ',' && depth === 0)
|
|
384
|
-
return true;
|
|
385
|
-
}
|
|
386
|
-
return false;
|
|
387
|
-
}
|
|
388
|
-
// Returns `base` if free, else the first `base_2`, `base_3`, ... not in
|
|
389
|
-
// `taken`.
|
|
390
|
-
function uniqueName(base, taken) {
|
|
391
|
-
if (!taken.has(base))
|
|
333
|
+
// Returns `base` if free, else the first of `base_2`, `base_3`, ... that is.
|
|
334
|
+
function uniqueName(base, isFree) {
|
|
335
|
+
if (isFree(base))
|
|
392
336
|
return base;
|
|
393
337
|
for (let i = 2;; i++) {
|
|
394
338
|
const candidate = `${base}_${i}`;
|
|
395
|
-
if (
|
|
339
|
+
if (isFree(candidate))
|
|
396
340
|
return candidate;
|
|
397
341
|
}
|
|
398
342
|
}
|
|
@@ -14,17 +14,12 @@
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.boundTable = boundTable;
|
|
16
16
|
exports.spannerTable = spannerTable;
|
|
17
|
+
const loader_1 = require("./loader");
|
|
17
18
|
const sql_identifiers_1 = require("./sql_identifiers");
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// no bearing on the operational table. A resource-name URI
|
|
23
|
-
// (`//spanner.googleapis.com/.../tables/<t>`, or any other `scheme://.../<t>`)
|
|
24
|
-
// names its table by the final PATH segment for the same reason — the leading
|
|
25
|
-
// path locates the store, not the table. A verbatim query (contains whitespace)
|
|
26
|
-
// cannot back a graph element table, so it is passed through parenthesized with
|
|
27
|
-
// a warning.
|
|
19
|
+
// The table `dataSource` names within its own database: its final dotted or
|
|
20
|
+
// path segment, except that a Spanner table keeps its schema (`ops.stations`),
|
|
21
|
+
// each part quoted. A query, which holds whitespace, is returned in
|
|
22
|
+
// parentheses with a warning.
|
|
28
23
|
function boundTable(dataSource, warnings, context, quote) {
|
|
29
24
|
const trimmed = (dataSource ?? '').trim();
|
|
30
25
|
if (!trimmed) {
|
|
@@ -36,12 +31,12 @@ function boundTable(dataSource, warnings, context, quote) {
|
|
|
36
31
|
`emitting it verbatim (a graph element table requires a table)`);
|
|
37
32
|
return `(${trimmed})`;
|
|
38
33
|
}
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// to its final path segment before the dotted reduction below.
|
|
34
|
+
// Reduce a URI to its final path segment. A Spanner source keeps every
|
|
35
|
+
// dotted part of it, and any other source keeps the last.
|
|
42
36
|
const source = isResourceUri(trimmed) ? finalPathSegment(trimmed) : trimmed;
|
|
43
|
-
const
|
|
44
|
-
|
|
37
|
+
const parts = splitDotted(source).map(unquote);
|
|
38
|
+
const kept = (0, loader_1.sourceTableOf)(trimmed)?.system === 'spanner' ? parts : parts.slice(-1);
|
|
39
|
+
return kept.map(quote).join('.');
|
|
45
40
|
}
|
|
46
41
|
// `boundTable` in GoogleSQL, which is what the Spanner DDL generator wants and
|
|
47
42
|
// what every caller wanted when there was one backend.
|
|
@@ -50,10 +45,10 @@ function spannerTable(dataSource, warnings, context) {
|
|
|
50
45
|
}
|
|
51
46
|
// A resource URI (`//host/...`) or any `scheme://...` URI, as opposed to a bare
|
|
52
47
|
// or dotted table reference. The loader gives the IR a BigQuery or BigLake
|
|
53
|
-
// table
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
48
|
+
// table as its dotted name, a Spanner, AlloyDB or Cloud SQL table as its
|
|
49
|
+
// resource URI, with the leading `//`, and a third-party table as its entry
|
|
50
|
+
// name (see `parseSource` and `sourceToDataSource`). It rejects any `scheme://`
|
|
51
|
+
// source, so only a hand-built IR reaches that arm.
|
|
57
52
|
function isResourceUri(source) {
|
|
58
53
|
return source.startsWith('//') || /^[a-z][\w+.-]*:\/\//i.test(source);
|
|
59
54
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// shared with the Knowledge Catalog leg so a multi-destination push parses each
|
|
7
7
|
// document once), lowers each to `CREATE OR REPLACE PROPERTY GRAPH` DDL
|
|
8
8
|
// (generator), and executes that DDL against the BigQuery project named by the
|
|
9
|
-
// model's
|
|
9
|
+
// model's deployment target.
|
|
10
10
|
//
|
|
11
11
|
// This is a library module: it emits no console output. The generated DDL and
|
|
12
12
|
// any loader/generator warnings are returned in `DeployResult` for the CLI
|
|
@@ -18,7 +18,7 @@ exports.datasetLocation = datasetLocation;
|
|
|
18
18
|
exports.deployBigQuery = deployBigQuery;
|
|
19
19
|
const bigquery_1 = require("../gcp/bigquery");
|
|
20
20
|
const bigquery_2 = require("./bigquery");
|
|
21
|
-
// The
|
|
21
|
+
// The deployment-target reader lives in its own module so both deploy
|
|
22
22
|
// legs (BigQuery, Spanner) and the push-time validator share one parse. The
|
|
23
23
|
// BigQuery-facing views are re-exported here so existing importers
|
|
24
24
|
// (validate.ts, knowledge_catalog.ts, and the deploy tests) keep their
|
|
@@ -150,25 +150,18 @@ async function deployBigQuery(models, ctx, options = {}) {
|
|
|
150
150
|
};
|
|
151
151
|
for (const { document, model } of models) {
|
|
152
152
|
modelsSeen++;
|
|
153
|
-
|
|
154
|
-
let malformed;
|
|
155
|
-
try {
|
|
156
|
-
({ targets, malformed } = (0, deployment_target_1.bigQueryGraphTargets)(model));
|
|
157
|
-
}
|
|
158
|
-
catch (err) {
|
|
159
|
-
return fail(`Model '${model.name}' (${document}): ${err.message || err}`);
|
|
160
|
-
}
|
|
153
|
+
const { targets, malformed } = (0, deployment_target_1.bigQueryGraphTargets)(model);
|
|
161
154
|
if (!targets.length) {
|
|
162
155
|
// A malformed-but-present target must not be reported as "none
|
|
163
|
-
// declared" -- that sends the author hunting for
|
|
156
|
+
// declared" -- that sends the author hunting for a deployment they
|
|
164
157
|
// already wrote. Name the URIs that failed to parse.
|
|
165
158
|
if (malformed.length) {
|
|
166
159
|
return fail(`Model '${model.name}' (${document}) declares BigQuery Graph ` +
|
|
167
|
-
`
|
|
160
|
+
`deployment target(s) that could not be parsed: ` +
|
|
168
161
|
`${malformed.join(', ')}. Expected ${deployment_target_1.BQ_GRAPH_FORM}.`);
|
|
169
162
|
}
|
|
170
163
|
return fail(`Model '${model.name}' (${document}) declares no BigQuery Graph ` +
|
|
171
|
-
`
|
|
164
|
+
`deployment target; nothing to deploy.`);
|
|
172
165
|
}
|
|
173
166
|
// Malformed targets alongside valid ones: surface them so a typo'd URI is
|
|
174
167
|
// not silently dropped when the deploy otherwise succeeds.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as context from '../gcp/context';
|
|
2
|
-
import {
|
|
2
|
+
import { Entry } from '../gcp/dataplex';
|
|
3
3
|
import type { OverviewSidecar } from '../layouts/semantic-model';
|
|
4
|
-
import { FileCopy } from './kc_custom_types';
|
|
5
4
|
import * as kcEmit from './knowledge_catalog';
|
|
6
5
|
import { LoadedModel } from './loader';
|
|
7
6
|
export interface KcDeployOptions {
|
|
@@ -12,9 +11,7 @@ export interface KcDeployOptions {
|
|
|
12
11
|
systemTypeLocation?: string;
|
|
13
12
|
emitExpressions?: boolean;
|
|
14
13
|
v2Aspects?: boolean;
|
|
15
|
-
sourceFiles?: FileCopy[];
|
|
16
14
|
overviews?: ReadonlyMap<string, OverviewSidecar>;
|
|
17
|
-
fileCopyTypeChecked?: boolean;
|
|
18
15
|
onWarnings?: (warnings: readonly string[]) => void;
|
|
19
16
|
validateOnly?: boolean;
|
|
20
17
|
forceRemove?: boolean;
|
|
@@ -55,14 +52,13 @@ export interface KcPreparedDeploy {
|
|
|
55
52
|
*
|
|
56
53
|
* The method performs the following actions:
|
|
57
54
|
* 1. Translates the IR models into catalog resources.
|
|
58
|
-
* 2. Verifies that exactly one model is being deployed
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
55
|
+
* 2. Verifies that exactly one model is being deployed, builds the dry-run
|
|
56
|
+
* plan, and returns early when `opts.validateOnly` is set.
|
|
57
|
+
* 3. Fails when the destination entry group does not exist. Lists it and
|
|
58
|
+
* fails if an unrecognized model's anchor entry is present without
|
|
59
|
+
* `--force-remove`.
|
|
63
60
|
* 4. Verifies that every custom entry type and aspect type required by the
|
|
64
|
-
* model exists in the target project.
|
|
65
|
-
* `opts.fileCopyTypeChecked` is set.
|
|
61
|
+
* model exists in the target project.
|
|
66
62
|
* 5. Returns the emitted resources, entry-group listing, warnings, and plan
|
|
67
63
|
* as `prepared` for `applyKnowledgeCatalog`.
|
|
68
64
|
* With `opts.v2Aspects` set, step 2 also checks every aspect's size.
|
|
@@ -98,32 +94,11 @@ export declare function deployEmittedModels(emitted: EmittedModel[], emitWarning
|
|
|
98
94
|
export declare function oneModelPerEntryGroupMessage(entryGroup: string, models: number): string;
|
|
99
95
|
export declare const MAX_ASPECT_DATA_BYTES: number;
|
|
100
96
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* Push calls it before any graph deploys. Every copy of a model shares the
|
|
106
|
-
* model's part of the entry id, so two file names collide when their slugs
|
|
107
|
-
* do.
|
|
108
|
-
*/
|
|
109
|
-
export declare function checkFileCopies(files: readonly FileCopy[]): string | undefined;
|
|
110
|
-
/**
|
|
111
|
-
* The emitter's warnings for `models` with v2Aspects on, and why push's
|
|
112
|
-
* catalog leg cannot write them: an emit error or an aspect too large to
|
|
113
|
-
* store. Calls no service.
|
|
97
|
+
* The emitter's warnings for `models`, and the first aspect too large to
|
|
98
|
+
* store. With v2Aspects on, that error is why push's catalog leg cannot write
|
|
99
|
+
* them. Calls no service.
|
|
114
100
|
*/
|
|
115
101
|
export declare function checkCatalogAspects(models: LoadedModel[], opts: KcDeployOptions): {
|
|
116
102
|
error?: string;
|
|
117
103
|
warnings: string[];
|
|
118
104
|
};
|
|
119
|
-
/**
|
|
120
|
-
* Checks that each custom type in `typeIds` exists in the destination
|
|
121
|
-
* project. Returns the error to report, or undefined when every one exists.
|
|
122
|
-
*
|
|
123
|
-
* Push also calls it before any graph deploys, for the file-copy type every
|
|
124
|
-
* push with v2Aspects off needs. Push then sets `fileCopyTypeChecked`, so the
|
|
125
|
-
* catalog preflight looks up only the other custom types.
|
|
126
|
-
*/
|
|
127
|
-
export declare function checkCustomTypes(cat: CatalogClient, dest: {
|
|
128
|
-
project: string;
|
|
129
|
-
}, typeIds: readonly string[]): Promise<string | undefined>;
|