@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.
Files changed (67) hide show
  1. package/README.md +18 -20
  2. package/build/ts/kcmd/cli.js +6164 -5631
  3. package/build/ts/kcmd/gcp/biglake.d.ts +2 -0
  4. package/build/ts/kcmd/gcp/biglake.js +13 -4
  5. package/build/ts/kcmd/gcp/spanner.d.ts +10 -0
  6. package/build/ts/kcmd/gcp/spanner.js +6 -3
  7. package/build/ts/kcmd/layouts/okf.js +49 -7
  8. package/build/ts/kcmd/layouts/semantic-model.d.ts +13 -0
  9. package/build/ts/kcmd/layouts/semantic-model.js +129 -50
  10. package/build/ts/kcmd/okf/concept.d.ts +1 -1
  11. package/build/ts/kcmd/okf/concept.js +6 -0
  12. package/build/ts/kcmd/okf/concept_to_entry.d.ts +2 -0
  13. package/build/ts/kcmd/okf/concept_to_entry.js +29 -2
  14. package/build/ts/kcmd/okf/init_okf_workspace.js +7 -9
  15. package/build/ts/kcmd/okf/okf_metadata.js +17 -0
  16. package/build/ts/kcmd/semantic/bigquery.js +139 -195
  17. package/build/ts/kcmd/semantic/binding.js +14 -19
  18. package/build/ts/kcmd/semantic/deploy_bigquery.js +6 -13
  19. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +10 -35
  20. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +105 -162
  21. package/build/ts/kcmd/semantic/deploy_spanner.js +7 -14
  22. package/build/ts/kcmd/semantic/deployment_target.d.ts +1 -4
  23. package/build/ts/kcmd/semantic/deployment_target.js +6 -62
  24. package/build/ts/kcmd/semantic/graph_ddl.d.ts +10 -0
  25. package/build/ts/kcmd/semantic/graph_ddl.js +152 -59
  26. package/build/ts/kcmd/semantic/import_model.d.ts +26 -4
  27. package/build/ts/kcmd/semantic/import_model.js +531 -111
  28. package/build/ts/kcmd/semantic/ir.d.ts +2 -4
  29. package/build/ts/kcmd/semantic/kc_converter.d.ts +7 -1
  30. package/build/ts/kcmd/semantic/kc_converter.js +32 -27
  31. package/build/ts/kcmd/semantic/kc_custom_types.d.ts +5 -12
  32. package/build/ts/kcmd/semantic/kc_custom_types.js +43 -114
  33. package/build/ts/kcmd/semantic/kc_ids.d.ts +4 -5
  34. package/build/ts/kcmd/semantic/kc_ids.js +4 -7
  35. package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +0 -3
  36. package/build/ts/kcmd/semantic/knowledge_catalog.js +3 -86
  37. package/build/ts/kcmd/semantic/loader.d.ts +7 -11
  38. package/build/ts/kcmd/semantic/loader.js +282 -171
  39. package/build/ts/kcmd/semantic/osi_converter.js +14 -65
  40. package/build/ts/kcmd/semantic/parameters.d.ts +1 -0
  41. package/build/ts/kcmd/semantic/parameters.js +22 -3
  42. package/build/ts/kcmd/semantic/pull_kc.d.ts +1 -5
  43. package/build/ts/kcmd/semantic/pull_kc.js +38 -112
  44. package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +7 -2
  45. package/build/ts/kcmd/semantic/resolve_inheritance.js +37 -21
  46. package/build/ts/kcmd/semantic/resolve_profiles.d.ts +51 -10
  47. package/build/ts/kcmd/semantic/resolve_profiles.js +213 -32
  48. package/build/ts/kcmd/semantic/runtime/runtime.d.ts +4 -12
  49. package/build/ts/kcmd/semantic/runtime/runtime.js +4 -21
  50. package/build/ts/kcmd/semantic/runtime/store.d.ts +14 -19
  51. package/build/ts/kcmd/semantic/runtime/store.js +21 -37
  52. package/build/ts/kcmd/semantic/source_dialect.d.ts +9 -2
  53. package/build/ts/kcmd/semantic/source_dialect.js +83 -33
  54. package/build/ts/kcmd/semantic/source_entries.d.ts +1 -1
  55. package/build/ts/kcmd/semantic/source_entries.js +95 -36
  56. package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +13 -7
  57. package/build/ts/kcmd/semantic/sql_expr_utils.js +240 -67
  58. package/build/ts/kcmd/semantic/sql_parser.d.ts +5 -2
  59. package/build/ts/kcmd/semantic/sql_parser.js +22 -0
  60. package/build/ts/kcmd/semantic/transpile.d.ts +12 -10
  61. package/build/ts/kcmd/semantic/transpile.js +79 -61
  62. package/build/ts/kcmd/semantic/validate.d.ts +37 -2
  63. package/build/ts/kcmd/semantic/validate.js +467 -126
  64. package/build/ts/kcmd/snapshot.d.ts +4 -0
  65. package/build/ts/kcmd/snapshot.js +7 -0
  66. package/build/ts/kcmd/sync.js +44 -7
  67. 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 equal to an existing field reuses that property instead of
73
- // duplicating.
74
- function newLowering(entity) {
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 fieldNames = new Set();
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
- fieldNames.add(f.name);
81
- const expr = fieldExpression(f);
82
- if (expr === undefined)
83
- continue;
84
- const local = (0, sql_expr_utils_1.stripQualifier)(expr, entity.name);
85
- if (!byLocalExpr.has(local))
86
- byLocalExpr.set(local, f.name);
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
- fieldNames,
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 `<name>.` qualifier the expression uses. String literals
117
- // are ignored, so a value like 'orders.x' does not reference `orders`.
118
- let referenced = (0, sql_expr_utils_1.referencedEntityNames)(expression, [...entityByName.keys()]);
119
- // A qualifier-free aggregate (e.g. COUNT(*)) names no entity in its
120
- // expression; fall back to `metric.entity` when the IR provides one (an
121
- // authored `entity` in `0.2.0.dev0/google`, an entity inferred from another
122
- // dialect entry, or hand-built IR), so it can still be placed rather than
123
- // dropped as "references no entity".
124
- if (referenced.length === 0 && metric.entity) {
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.stripQualifier)(expression, entityName);
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
- const agg = extractAggregate(body);
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) ?? newLowering(entity);
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. COUNT(*) has no
196
- // operand column, so it counts the first column of the node's key instead:
197
- // the primary key, else the first unique key (nodeKey).
198
- let operandExpr = agg.operand;
199
- const countsKey = agg.operand === '*';
200
- if (countsKey) {
201
- // The entity is already known to have a key (checked above), so only the
202
- // aggregate itself can disqualify a `*` operand: BigQuery lowers `COUNT(*)`
203
- // to a count over the key, but no other aggregate has a `*` form.
204
- if (agg.fn.toUpperCase() !== 'COUNT') {
205
- warnings.push(`metric '${metric.name}': '${agg.fn}(*)' is not supported; skipped ` +
206
- `(only COUNT(*) over a keyed node can be lowered)`);
207
- return;
208
- }
209
- operandExpr = (0, graph_ddl_1.nodeKey)(entity)[0];
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
- // A field with no BigQuery SQL is left out of the node table, so a measure
212
- // over it would aggregate a property the table does not declare.
213
- if (!countsKey &&
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 = countsKey ?
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
- // Ensures `operandExpr` is exposed as a node property and returns its property
243
- // name. Reuses an existing field (or a previously derived property) with the
244
- // same expression; a bare column is exposed under its own name; any other
245
- // expression gets a synthesized, unique name derived from the metric.
246
- function exposeOperand(lowering, operandExpr, metricName) {
247
- const existing = lowering.byLocalExpr.get(operandExpr) ??
248
- lowering.operandToName.get(operandExpr);
249
- if (existing)
250
- return existing;
251
- // An operand that names a declared field is already an exposed property, so a
252
- // MEASURE may aggregate it directly by name -- even when a profile bound that
253
- // field to a differently named physical column (the property is
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". (A raw column that is not a field still falls through
258
- // to be exposed under its own name, which BigQuery requires.)
259
- if (lowering.fieldNames.has(operandExpr))
260
- return operandExpr;
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)(operandExpr) && !lowering.taken.has(operandExpr)) {
263
- // A bare column not already declared: expose it under its own name.
264
- name = operandExpr;
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`, lowering.taken);
269
- lowering.derivedProperties.push(`${(0, sql_identifiers_1.quoteIfReserved)(operandExpr)} AS ${name}`);
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(operandExpr, name);
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
- // Extracts the function name and operand from a single-aggregate expression, or
293
- // returns null when the body is not exactly one supported aggregate wrapping
294
- // one operand (a non-aggregate, a compound like `SUM(x)/SUM(y)`, a call with no
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 (!taken.has(candidate))
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
- // Maps the IR's `dataSource` to the BARE table name a statement references. A
19
- // property graph names input tables within its own database, so only the final
20
- // segment of a qualified `project.dataset.table` (or any dotted source) is
21
- // meaningful; the leading qualifiers name where a BigQuery copy lives and have
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
- // A BigQuery or BigLake source reaches here dotted, but a Spanner, AlloyDB or
40
- // Cloud SQL source as its resource URI (see isResourceUri), so reduce a URI
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 last = splitDotted(source).map(unquote).pop() ?? source;
44
- return quote(last);
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, or a legacy bare name, as its dotted name, a Spanner, AlloyDB or Cloud
54
- // SQL table as its resource URI, with the leading `//`, and a third-party table
55
- // as its entry name (see `parseSource` and `sourceToDataSource`). It rejects
56
- // any `scheme://` source, so only a hand-built IR reaches that arm.
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 GOOGLE deployment target.
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 GOOGLE deployment-target reader lives in its own module so both deploy
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
- let targets;
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 an extension they
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
- `deploymentTarget(s) that could not be parsed: ` +
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
- `deploymentTarget in a GOOGLE custom_extension; nothing to deploy.`);
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 { CatalogClient, Entry } from '../gcp/dataplex';
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 and that each file
59
- * copy fits in one aspect, builds the dry-run plan, and returns early
60
- * when `opts.validateOnly` is set.
61
- * 3. Lists the destination entry group and fails if an unrecognized model's
62
- * anchor entry is present without `--force-remove`.
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. Skips the file-copy type when
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
- * Checks a push's file copies before anything is written. Returns the error
102
- * to report, or undefined when every copy fits in one aspect and no two file
103
- * names map to one entry id.
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>;