@google-cloud/kcmd 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +264 -2
  3. package/build/ts/kcmd/cli.js +20252 -0
  4. package/build/ts/kcmd/gcp/alloydb.d.ts +12 -0
  5. package/build/ts/kcmd/gcp/alloydb.js +70 -0
  6. package/build/ts/kcmd/gcp/api.d.ts +29 -0
  7. package/build/ts/kcmd/gcp/api.js +162 -0
  8. package/build/ts/kcmd/gcp/biglake.d.ts +18 -0
  9. package/build/ts/kcmd/gcp/biglake.js +59 -0
  10. package/build/ts/kcmd/gcp/bigquery.d.ts +57 -0
  11. package/build/ts/kcmd/gcp/bigquery.js +134 -0
  12. package/build/ts/kcmd/gcp/context.d.ts +10 -0
  13. package/build/ts/kcmd/gcp/context.js +74 -0
  14. package/build/ts/kcmd/gcp/crm.d.ts +13 -0
  15. package/build/ts/kcmd/gcp/crm.js +91 -0
  16. package/build/ts/kcmd/gcp/dataplex.d.ts +147 -0
  17. package/build/ts/kcmd/gcp/dataplex.js +406 -0
  18. package/build/ts/kcmd/gcp/index.d.ts +3 -0
  19. package/build/ts/kcmd/gcp/index.js +21 -0
  20. package/build/ts/kcmd/gcp/spanner.d.ts +73 -0
  21. package/build/ts/kcmd/gcp/spanner.js +126 -0
  22. package/build/ts/kcmd/index.d.ts +7 -0
  23. package/build/ts/kcmd/index.js +46 -0
  24. package/build/ts/kcmd/layout.d.ts +16 -0
  25. package/build/ts/kcmd/layout.js +31 -0
  26. package/build/ts/kcmd/layouts/documents.d.ts +24 -0
  27. package/build/ts/kcmd/layouts/documents.js +264 -0
  28. package/build/ts/kcmd/layouts/okf.d.ts +31 -0
  29. package/build/ts/kcmd/layouts/okf.js +386 -0
  30. package/build/ts/kcmd/layouts/semantic-model.d.ts +74 -0
  31. package/build/ts/kcmd/layouts/semantic-model.js +621 -0
  32. package/build/ts/kcmd/layouts/standard.d.ts +13 -0
  33. package/build/ts/kcmd/layouts/standard.js +108 -0
  34. package/build/ts/kcmd/manifest.d.ts +34 -0
  35. package/build/ts/kcmd/manifest.js +262 -0
  36. package/build/ts/kcmd/metadata.d.ts +25 -0
  37. package/build/ts/kcmd/metadata.js +8 -0
  38. package/build/ts/kcmd/okf/catalog_residue.d.ts +14 -0
  39. package/build/ts/kcmd/okf/catalog_residue.js +124 -0
  40. package/build/ts/kcmd/okf/concept.d.ts +14 -0
  41. package/build/ts/kcmd/okf/concept.js +110 -0
  42. package/build/ts/kcmd/okf/concept_to_entry.d.ts +10 -0
  43. package/build/ts/kcmd/okf/concept_to_entry.js +281 -0
  44. package/build/ts/kcmd/okf/init_okf_workspace.d.ts +15 -0
  45. package/build/ts/kcmd/okf/init_okf_workspace.js +113 -0
  46. package/build/ts/kcmd/okf/links.d.ts +23 -0
  47. package/build/ts/kcmd/okf/links.js +468 -0
  48. package/build/ts/kcmd/okf/okf_metadata.d.ts +12 -0
  49. package/build/ts/kcmd/okf/okf_metadata.js +99 -0
  50. package/build/ts/kcmd/semantic/bigquery.d.ts +22 -0
  51. package/build/ts/kcmd/semantic/bigquery.js +469 -0
  52. package/build/ts/kcmd/semantic/binding.d.ts +3 -0
  53. package/build/ts/kcmd/semantic/binding.js +92 -0
  54. package/build/ts/kcmd/semantic/catalog_entry.d.ts +24 -0
  55. package/build/ts/kcmd/semantic/catalog_entry.js +35 -0
  56. package/build/ts/kcmd/semantic/converters/owl/convert.d.ts +26 -0
  57. package/build/ts/kcmd/semantic/converters/owl/convert.js +41 -0
  58. package/build/ts/kcmd/semantic/converters/owl/model.d.ts +98 -0
  59. package/build/ts/kcmd/semantic/converters/owl/model.js +30 -0
  60. package/build/ts/kcmd/semantic/converters/owl/parse.d.ts +11 -0
  61. package/build/ts/kcmd/semantic/converters/owl/parse.js +634 -0
  62. package/build/ts/kcmd/semantic/converters/owl/to_ir.d.ts +19 -0
  63. package/build/ts/kcmd/semantic/converters/owl/to_ir.js +390 -0
  64. package/build/ts/kcmd/semantic/deploy_bigquery.d.ts +20 -0
  65. package/build/ts/kcmd/semantic/deploy_bigquery.js +226 -0
  66. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +104 -0
  67. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +1367 -0
  68. package/build/ts/kcmd/semantic/deploy_spanner.d.ts +15 -0
  69. package/build/ts/kcmd/semantic/deploy_spanner.js +150 -0
  70. package/build/ts/kcmd/semantic/deployment_target.d.ts +93 -0
  71. package/build/ts/kcmd/semantic/deployment_target.js +379 -0
  72. package/build/ts/kcmd/semantic/graph_ddl.d.ts +44 -0
  73. package/build/ts/kcmd/semantic/graph_ddl.js +656 -0
  74. package/build/ts/kcmd/semantic/import_model.d.ts +72 -0
  75. package/build/ts/kcmd/semantic/import_model.js +1698 -0
  76. package/build/ts/kcmd/semantic/ir.d.ts +589 -0
  77. package/build/ts/kcmd/semantic/ir.js +247 -0
  78. package/build/ts/kcmd/semantic/kc_actions.d.ts +45 -0
  79. package/build/ts/kcmd/semantic/kc_actions.js +531 -0
  80. package/build/ts/kcmd/semantic/kc_constraints.d.ts +44 -0
  81. package/build/ts/kcmd/semantic/kc_constraints.js +279 -0
  82. package/build/ts/kcmd/semantic/kc_converter.d.ts +37 -0
  83. package/build/ts/kcmd/semantic/kc_converter.js +839 -0
  84. package/build/ts/kcmd/semantic/kc_custom_types.d.ts +63 -0
  85. package/build/ts/kcmd/semantic/kc_custom_types.js +751 -0
  86. package/build/ts/kcmd/semantic/kc_entries.d.ts +155 -0
  87. package/build/ts/kcmd/semantic/kc_entries.js +284 -0
  88. package/build/ts/kcmd/semantic/kc_ids.d.ts +102 -0
  89. package/build/ts/kcmd/semantic/kc_ids.js +188 -0
  90. package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +29 -0
  91. package/build/ts/kcmd/semantic/knowledge_catalog.js +960 -0
  92. package/build/ts/kcmd/semantic/loader.d.ts +258 -0
  93. package/build/ts/kcmd/semantic/loader.js +3576 -0
  94. package/build/ts/kcmd/semantic/names.d.ts +10 -0
  95. package/build/ts/kcmd/semantic/names.js +85 -0
  96. package/build/ts/kcmd/semantic/osi_converter.d.ts +60 -0
  97. package/build/ts/kcmd/semantic/osi_converter.js +910 -0
  98. package/build/ts/kcmd/semantic/parameters.d.ts +19 -0
  99. package/build/ts/kcmd/semantic/parameters.js +171 -0
  100. package/build/ts/kcmd/semantic/pull_kc.d.ts +10 -0
  101. package/build/ts/kcmd/semantic/pull_kc.js +272 -0
  102. package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +59 -0
  103. package/build/ts/kcmd/semantic/resolve_inheritance.js +346 -0
  104. package/build/ts/kcmd/semantic/resolve_profiles.d.ts +291 -0
  105. package/build/ts/kcmd/semantic/resolve_profiles.js +2003 -0
  106. package/build/ts/kcmd/semantic/runtime/agent_tools.d.ts +93 -0
  107. package/build/ts/kcmd/semantic/runtime/agent_tools.js +243 -0
  108. package/build/ts/kcmd/semantic/runtime/dialect.d.ts +13 -0
  109. package/build/ts/kcmd/semantic/runtime/dialect.js +39 -0
  110. package/build/ts/kcmd/semantic/runtime/runtime.d.ts +88 -0
  111. package/build/ts/kcmd/semantic/runtime/runtime.js +301 -0
  112. package/build/ts/kcmd/semantic/runtime/store.d.ts +112 -0
  113. package/build/ts/kcmd/semantic/runtime/store.js +344 -0
  114. package/build/ts/kcmd/semantic/skills.d.ts +110 -0
  115. package/build/ts/kcmd/semantic/skills.js +697 -0
  116. package/build/ts/kcmd/semantic/source_dialect.d.ts +17 -0
  117. package/build/ts/kcmd/semantic/source_dialect.js +311 -0
  118. package/build/ts/kcmd/semantic/source_entries.d.ts +12 -0
  119. package/build/ts/kcmd/semantic/source_entries.js +341 -0
  120. package/build/ts/kcmd/semantic/spanner.d.ts +18 -0
  121. package/build/ts/kcmd/semantic/spanner.js +60 -0
  122. package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +23 -0
  123. package/build/ts/kcmd/semantic/sql_expr_utils.js +394 -0
  124. package/build/ts/kcmd/semantic/sql_identifiers.d.ts +6 -0
  125. package/build/ts/kcmd/semantic/sql_identifiers.js +267 -0
  126. package/build/ts/kcmd/semantic/sql_parser.d.ts +47 -0
  127. package/build/ts/kcmd/semantic/sql_parser.js +400 -0
  128. package/build/ts/kcmd/semantic/transpile.d.ts +76 -0
  129. package/build/ts/kcmd/semantic/transpile.js +581 -0
  130. package/build/ts/kcmd/semantic/validate.d.ts +70 -0
  131. package/build/ts/kcmd/semantic/validate.js +2266 -0
  132. package/build/ts/kcmd/semantic/yaml_options.d.ts +6 -0
  133. package/build/ts/kcmd/semantic/yaml_options.js +58 -0
  134. package/build/ts/kcmd/semantic/yaml_update.d.ts +26 -0
  135. package/build/ts/kcmd/semantic/yaml_update.js +782 -0
  136. package/build/ts/kcmd/snapshot.d.ts +37 -0
  137. package/build/ts/kcmd/snapshot.js +386 -0
  138. package/build/ts/kcmd/source.d.ts +31 -0
  139. package/build/ts/kcmd/source.js +119 -0
  140. package/build/ts/kcmd/sources/bq-dataset.d.ts +17 -0
  141. package/build/ts/kcmd/sources/bq-dataset.js +131 -0
  142. package/build/ts/kcmd/sources/entrygroup.d.ts +21 -0
  143. package/build/ts/kcmd/sources/entrygroup.js +83 -0
  144. package/build/ts/kcmd/sources/kb.d.ts +21 -0
  145. package/build/ts/kcmd/sources/kb.js +82 -0
  146. package/build/ts/kcmd/sources/semantic-model.d.ts +18 -0
  147. package/build/ts/kcmd/sources/semantic-model.js +48 -0
  148. package/build/ts/kcmd/sync.d.ts +35 -0
  149. package/build/ts/kcmd/sync.js +734 -0
  150. package/package.json +62 -4
@@ -0,0 +1,469 @@
1
+ "use strict";
2
+ // Generates BigQuery property-graph DDL (with inline measures) from the
3
+ // Semantic Model IR.
4
+ //
5
+ // The shared GoogleSQL generator (./graph_ddl) writes the graph structure: node
6
+ // tables, edge tables, labels, properties, and key and join columns. This
7
+ // module supplies its BigQuery engine: qualified names, measures, the
8
+ // per-element OPTIONS clause, and the SQL text a field binds to.
9
+ //
10
+ // BigQuery graph measures bind an aggregate to exactly one table's KEY; the
11
+ // cross-entity rollup happens at query time via GRAPH_EXPAND(...) + AGG(...).
12
+ // A metric therefore lands on whichever single table its aggregate columns
13
+ // reference; a metric whose aggregate genuinely spans multiple tables cannot be
14
+ // expressed as one MEASURE and is skipped (reported in `warnings`).
15
+ //
16
+ // A measure can only aggregate an EXPOSED PROPERTY of its node (not a raw
17
+ // column or an inline expression), so each metric is lowered into a derived
18
+ // operand property plus a MEASURE over it (see placeMetric / exposeOperand).
19
+ //
20
+ // See: https://docs.cloud.google.com/bigquery/docs/graph-measures
21
+ //
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.generatePropertyGraph = generatePropertyGraph;
24
+ exports.elementDescription = elementDescription;
25
+ exports.fieldDescription = fieldDescription;
26
+ const graph_ddl_1 = require("./graph_ddl");
27
+ const ir_1 = require("./ir");
28
+ const sql_expr_utils_1 = require("./sql_expr_utils");
29
+ const sql_identifiers_1 = require("./sql_identifiers");
30
+ const sql_parser_1 = require("./sql_parser");
31
+ /**
32
+ * Generates the BigQuery property-graph DDL for a semantic model.
33
+ *
34
+ * The generated statement references the entities' existing base tables; it
35
+ * does not create them. Returns the DDL plus any warnings collected while
36
+ * mapping the IR (e.g. metrics that could not be placed on a single table).
37
+ *
38
+ * Throws when the model yields no valid node table -- it declares no entities,
39
+ * or every entity lacks a KEY -- since a property graph with an empty NODE
40
+ * TABLES block is invalid DDL that BigQuery would reject.
41
+ */
42
+ function generatePropertyGraph(model, opts = {}) {
43
+ return (0, graph_ddl_1.generateGraphDdl)(model, bigQueryEngine(opts));
44
+ }
45
+ // The BigQuery engine for the shared generator.
46
+ function bigQueryEngine(opts) {
47
+ return {
48
+ name: 'BigQuery',
49
+ table: qualifyTable,
50
+ graphName: model => qualifyGraph(model, opts),
51
+ placeMetrics,
52
+ elementOptions: (description, ai) => optionsClause(elementDescription(description, ai), ai?.synonyms),
53
+ fieldOptions: field => optionsClause(fieldDescription(field), field.aiContext?.synonyms),
54
+ dialect: 'BIGQUERY',
55
+ fieldExpression,
56
+ };
57
+ }
58
+ // Places each metric on the single entity its aggregate references, lowering
59
+ // it to a MEASURE over an exposed property (see placeMetric).
60
+ function placeMetrics(metrics, input, warnings) {
61
+ const loweringByEntity = new Map();
62
+ const metricNames = new Set(metrics.map(m => m.name));
63
+ for (const metric of metrics) {
64
+ placeMetric(metric, input, metricNames, loweringByEntity, warnings);
65
+ }
66
+ // The operand properties come first: the measures aggregate them.
67
+ const placed = new Map();
68
+ for (const [name, lowering] of loweringByEntity) {
69
+ placed.set(name, [...lowering.derivedProperties, ...lowering.measures]);
70
+ }
71
+ return placed;
72
+ }
73
+ // Builds the lowering state for an entity, seeded with its declared fields so
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) {
85
+ const taken = new Set();
86
+ const sqlOf = new Map();
87
+ for (const f of entity.fields) {
88
+ taken.add(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
+ }
110
+ }
111
+ return {
112
+ derivedProperties: [],
113
+ measures: [],
114
+ taken,
115
+ metricNames,
116
+ ambiguous,
117
+ byLocalExpr,
118
+ operandToName: new Map()
119
+ };
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
+ }
128
+ // Assigns a metric to the node table of the single entity its aggregate
129
+ // references, lowering it to a MEASURE over an exposed property. Records a
130
+ // warning (and skips) when it references zero/multiple entities or is not a
131
+ // single supported aggregate over one operand.
132
+ function placeMetric(metric, input, metricNames, loweringByEntity, warnings) {
133
+ const { entityByName, skipped, ancestorsUsed } = input;
134
+ // A measure is emitted from the metric's BigQuery SQL (see
135
+ // metricExpression). A metric with SQL only for other engines is skipped
136
+ // with a warning naming the dialects it lacks, rather than emitting that
137
+ // other engine's SQL into a BigQuery graph.
138
+ const expression = metricExpression(metric);
139
+ if (expression === undefined) {
140
+ warnings.push(metric.dialects?.length || metric.importedExpression !== undefined ?
141
+ `metric '${metric.name}' has no 'BIGQUERY' or 'ANSI_SQL' ` +
142
+ `expression; skipped (nothing to emit as a MEASURE)` :
143
+ `metric '${metric.name}' has no expression; skipped (nothing ` +
144
+ `to emit as a MEASURE)`);
145
+ return;
146
+ }
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);
156
+ if (referenced.length !== 1) {
157
+ const detail = referenced.length === 0 ?
158
+ 'references no known entity' :
159
+ `spans multiple tables (${referenced.join(', ')})`;
160
+ warnings.push(`metric '${metric.name}' ${detail}; skipped (cannot be a single MEASURE)`);
161
+ return;
162
+ }
163
+ const entityName = referenced[0];
164
+ // The IR also declares a single attach entity (metric.entity). Placement is
165
+ // driven by the qualifiers actually present in the expression (that is what
166
+ // we strip and attach), but a disagreement with the declared entity signals
167
+ // an inconsistent model, so surface it. Emit this only now that we know the
168
+ // metric will actually be placed, so a metric that is subsequently dropped
169
+ // does not carry a contradictory "placing per the expression" note.
170
+ if (metric.entity && entityName !== metric.entity) {
171
+ warnings.push(`metric '${metric.name}' declares entity '${metric.entity}' but its ` +
172
+ `expression references '${entityName}'; placing per the expression`);
173
+ }
174
+ const entity = entityByName.get(entityName);
175
+ if (!entity) {
176
+ warnings.push(`metric '${metric.name}' references unknown entity '${entityName}'; skipped (cannot be a single MEASURE)`);
177
+ return;
178
+ }
179
+ // An abstract entity is a table-less supertype with no node table, so it can
180
+ // carry no MEASURE. Report that specifically -- it is also in `skipped`, so
181
+ // this must precede the generic no-KEY message below to stay accurate.
182
+ if (entity.abstract) {
183
+ warnings.push(`metric '${metric.name}' targets entity '${entityName}', which is ` +
184
+ `abstract (a table-less supertype with no node table to carry a ` +
185
+ `MEASURE); metric dropped`);
186
+ return;
187
+ }
188
+ // A metric can only become a MEASURE on a node table, but an entity that was
189
+ // skipped upstream (empty KEY) has no node table to carry it. Check the skip
190
+ // set already computed, and report the drop directly, instead of letting it
191
+ // fail later with a misleading "aggregate not supported" message.
192
+ if (skipped.has(entityName)) {
193
+ warnings.push(`metric '${metric.name}' targets entity '${entityName}', which has no KEY and was skipped; metric dropped`);
194
+ return;
195
+ }
196
+ // A measure is allowed only on a LEAF type -- one that no table-emitting
197
+ // entity extends. A metric lowers to a MEASURE on the target's DEFAULT LABEL;
198
+ // when that entity is a supertype its label is shared with every subtype
199
+ // table, and BigQuery forbids a MEASURE on a label carried by more than one
200
+ // element table (verified live: "defined as MEASURE, but there are other
201
+ // declarations with the same name"). Drop it with a warning rather than emit
202
+ // DDL BigQuery rejects. Push validation rejects such a metric before
203
+ // generation; this guard covers a caller that generates DDL without
204
+ // validating first.
205
+ if (ancestorsUsed.has(entityName)) {
206
+ warnings.push(`metric '${metric.name}' targets entity '${entityName}', which is not ` +
207
+ `a leaf type (another entity extends it); a measure is allowed only on ` +
208
+ `a leaf type, because a supertype's label is shared across its ` +
209
+ `subtype tables; skipped`);
210
+ return;
211
+ }
212
+ const body = (0, sql_expr_utils_1.withoutQualifier)(expression, entityName);
213
+ // A graph measure must be one supported aggregate wrapping a single operand;
214
+ // anything else (a non-aggregate, or a compound of aggregates like a ratio)
215
+ // cannot be a single MEASURE. Flag it rather than emit DDL BigQuery rejects.
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');
221
+ if (!agg) {
222
+ warnings.push(`metric '${metric.name}' expression '${body}' is not a single supported aggregate ` +
223
+ `(${sql_expr_utils_1.SUPPORTED_AGGREGATES.join(', ')}) over one operand; skipped (cannot be a single MEASURE)`);
224
+ return;
225
+ }
226
+ const lowering = loweringByEntity.get(entityName) ??
227
+ newLowering(entity, input, metricNames);
228
+ loweringByEntity.set(entityName, lowering);
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);
251
+ }
252
+ if ('skip' in read) {
253
+ warnings.push(`metric '${metric.name}' ${read.skip}; skipped (nothing to emit as a ` +
254
+ `MEASURE)`);
255
+ return;
256
+ }
257
+ // The measure is itself exposed as a node property named `metric.name`, so it
258
+ // must not collide with a field, a prior measure, or a synthesized operand
259
+ // property — a duplicate property makes BigQuery reject the whole node table.
260
+ // A clash with an already-taken name is a model error we cannot paper over by
261
+ // renaming (the measure name is exactly what a reader selects via
262
+ // GRAPH_EXPAND + AGG(<label>_<name>)), so skip + warn. Reserve it up front so
263
+ // the operand lowering below never synthesizes a property with the same name.
264
+ if (lowering.taken.has(metric.name)) {
265
+ warnings.push(`metric '${metric.name}' collides with an existing property of entity ` +
266
+ `'${entityName}'; skipped (rename the metric to avoid a duplicate graph property)`);
267
+ return;
268
+ }
269
+ lowering.taken.add(metric.name);
270
+ const propName = exposeOperand(lowering, read, metric.name);
271
+ const aggregate = `${agg.fn}(${agg.distinct ? 'DISTINCT ' : ''}${(0, sql_identifiers_1.quoteIfReserved)(propName)})`;
272
+ const opts = optionsClause(elementDescription(metric.description, metric.aiContext), metric.aiContext?.synonyms);
273
+ const measure = `MEASURE(${aggregate}) AS ${(0, sql_identifiers_1.quoteIfReserved)(metric.name)}`;
274
+ lowering.measures.push(opts ? `${measure} ${opts}` : measure);
275
+ }
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
306
+ // `<column> AS <field>`, and a MEASURE may reference a sibling alias).
307
+ // Synthesizing an input property here would emit `<field> AS ..._input`, and
308
+ // an alias is illegal inside a property expression -- BigQuery rejects it with
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);
318
+ let 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;
322
+ lowering.derivedProperties.push((0, sql_identifiers_1.quoteIfReserved)(name));
323
+ }
324
+ else {
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}`);
328
+ }
329
+ lowering.taken.add(name);
330
+ lowering.operandToName.set(sql, name);
331
+ return name;
332
+ }
333
+ // Returns `base` if free, else the first of `base_2`, `base_3`, ... that is.
334
+ function uniqueName(base, isFree) {
335
+ if (isFree(base))
336
+ return base;
337
+ for (let i = 2;; i++) {
338
+ const candidate = `${base}_${i}`;
339
+ if (isFree(candidate))
340
+ return candidate;
341
+ }
342
+ }
343
+ // The SQL BigQuery runs for a field or metric: its `BIGQUERY` dialect entry,
344
+ // else its `ANSI_SQL` one (see expressionForDialect), read as BigQuery with its
345
+ // comments removed. Another engine's vendor SQL is never returned. The DDL puts
346
+ // ` AS <name>` and a comma after the expression on the same line, and a line
347
+ // comment left in would swallow them.
348
+ function fieldExpression(f) {
349
+ return bigQuerySql(f);
350
+ }
351
+ function metricExpression(m) {
352
+ return bigQuerySql(m);
353
+ }
354
+ function bigQuerySql(item) {
355
+ const text = (0, ir_1.expressionForDialect)(item, 'BIGQUERY');
356
+ return text === undefined ? undefined : (0, sql_expr_utils_1.sqlWithoutComments)(text);
357
+ }
358
+ // Builds a backtick-quoted table reference from the IR's `dataSource` string.
359
+ // The IR contract guarantees `dataSource` is already canonical and
360
+ // fully-qualified: the producer (loader) normalizes a BigQuery or BigLake
361
+ // source into a dotted `project.dataset.table` (or four-part BigLake catalog
362
+ // name). For BigQuery that canonical form IS the target addressing scheme, so
363
+ // this emits it verbatim and NEVER prepends the graph's own project/dataset --
364
+ // those name where the graph is CREATED, not where a source table lives, and
365
+ // the two can differ (e.g. a graph in `proj.demo` over source tables in
366
+ // `samples.tpch`). Qualifying a source table is the producer's job, not the
367
+ // emitter's.
368
+ function qualifyTable(dataSource, warnings, context) {
369
+ const trimmed = (dataSource ?? '').trim();
370
+ if (!trimmed) {
371
+ warnings.push(`${context}: empty data source; the table reference will be invalid`);
372
+ return '``';
373
+ }
374
+ return `\`${trimmed}\``;
375
+ }
376
+ // Builds the dataset-qualified graph name, filling a missing project/dataset
377
+ // from opts and then from the first entity's qualified dataSource.
378
+ function qualifyGraph(model, opts) {
379
+ const name = opts.graphName ?? model.name;
380
+ let project = opts.project;
381
+ let dataset = opts.dataset;
382
+ // Pick the first entity with a non-empty `dataSource` to derive the graph's
383
+ // location; skip abstract entities (empty source).
384
+ const first = (model.entities ?? []).find(e => e.dataSource);
385
+ if ((!project || !dataset) && first) {
386
+ const p = first.dataSource.trim().split('.');
387
+ if (p.length >= 3) {
388
+ project = project ?? p[0];
389
+ dataset = dataset ?? p[1];
390
+ }
391
+ else if (p.length === 2) {
392
+ dataset = dataset ?? p[0];
393
+ }
394
+ }
395
+ // A graph name is `name`, `dataset.name`, or `project.dataset.name`; a
396
+ // project with no dataset cannot be expressed (`project.name` would be read
397
+ // as `dataset.name`), so drop a lone project rather than emit a malformed
398
+ // reference.
399
+ if (project && !dataset)
400
+ project = undefined;
401
+ const parts = [project, dataset, name].filter((p) => !!p);
402
+ return `\`${parts.join('.')}\``;
403
+ }
404
+ // Composes the folded `description` text for a graph element from its base
405
+ // description and the AI-first annotations that have no dedicated BigQuery
406
+ // option: instructions and examples. Synonyms are NOT folded in here --
407
+ // BigQuery's PropertyGraph{Label,Property}Options carry a structured `synonyms`
408
+ // array, so they are emitted as their own option (see optionsClause and the
409
+ // call sites).
410
+ function elementDescription(description, ai) {
411
+ return composeText([
412
+ description,
413
+ ai?.instructions,
414
+ examplesLine(ai?.examples),
415
+ ]);
416
+ }
417
+ // A field additionally carries a human `label` and a temporal-dimension role,
418
+ // which lead the composed text (matching the authored order) ahead of its
419
+ // description and instructions/examples. Synonyms are emitted structurally, as
420
+ // for elementDescription.
421
+ function fieldDescription(field) {
422
+ return composeText([
423
+ field.label,
424
+ (0, ir_1.isTimeDimension)(field) ? 'Time dimension.' : undefined,
425
+ field.description,
426
+ field.aiContext?.instructions,
427
+ examplesLine(field.aiContext?.examples),
428
+ ]);
429
+ }
430
+ function examplesLine(examples) {
431
+ return examples && examples.length ? `Examples: ${examples.join('; ')}` :
432
+ undefined;
433
+ }
434
+ // Joins the non-empty parts into one description, separated by blank lines so
435
+ // each reads as its own paragraph in the emitted metadata.
436
+ function composeText(parts) {
437
+ const kept = parts.map(p => (p === undefined ? undefined : p.trim()))
438
+ .filter((p) => !!p);
439
+ return kept.length ? kept.join('\n\n') : undefined;
440
+ }
441
+ // Renders the trailing `OPTIONS(...)` clause for a graph element -- a folded
442
+ // `description` string and/or a structured `synonyms` array -- or undefined
443
+ // when there is nothing to emit. Both map to BigQuery's
444
+ // PropertyGraph{Label,Property}Options (`description` singular, `synonyms`
445
+ // repeated), honored on element-table labels (DEFAULT LABEL, before PROPERTIES)
446
+ // and on derived properties and measures (after the alias). Statement-level
447
+ // graph OPTIONS is intentionally not emitted: it parses but BigQuery silently
448
+ // drops it (verified live; create_property_graph_options is off for BigQuery).
449
+ function optionsClause(description, synonyms) {
450
+ const parts = [];
451
+ if (description)
452
+ parts.push(`description=${quote(description)}`);
453
+ if (synonyms && synonyms.length) {
454
+ parts.push(`synonyms=[${synonyms.map(quote).join(', ')}]`);
455
+ }
456
+ return parts.length ? `OPTIONS(${parts.join(', ')})` : undefined;
457
+ }
458
+ // Renders a value as a BigQuery double-quoted string literal. Backslash and the
459
+ // quote are escaped, and control characters that cannot appear raw inside a
460
+ // quoted literal (newline, carriage return, tab) are escaped too, so a
461
+ // multi-line description does not produce a broken literal.
462
+ function quote(s) {
463
+ const escaped = s.replace(/\\/g, '\\\\')
464
+ .replace(/"/g, '\\"')
465
+ .replace(/\n/g, '\\n')
466
+ .replace(/\r/g, '\\r')
467
+ .replace(/\t/g, '\\t');
468
+ return `"${escaped}"`;
469
+ }
@@ -0,0 +1,3 @@
1
+ export type QuoteFn = (identifier: string) => string;
2
+ export declare function boundTable(dataSource: string, warnings: string[], context: string, quote: QuoteFn): string;
3
+ export declare function spannerTable(dataSource: string, warnings: string[], context: string): string;
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ // Which physical table an entity is bound to.
3
+ //
4
+ // A binding profile gives an entity a `source`, and every caller that touches
5
+ // real rows has to turn that string into a table name: the DDL generator
6
+ // naming an input table, the runtime building a statement, the agent tool
7
+ // derivation describing what a lookup reads. They must agree, so the rule
8
+ // lives in one place rather than in each of them.
9
+ //
10
+ // This is a binding concern, not a Spanner one. It sits outside the deploy
11
+ // path deliberately -- `runtime/` reads it, and a runtime that had to import a
12
+ // DDL generator to learn a table name would be describing a layering that is
13
+ // not real.
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.boundTable = boundTable;
16
+ exports.spannerTable = spannerTable;
17
+ const loader_1 = require("./loader");
18
+ const sql_identifiers_1 = require("./sql_identifiers");
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.
23
+ function boundTable(dataSource, warnings, context, quote) {
24
+ const trimmed = (dataSource ?? '').trim();
25
+ if (!trimmed) {
26
+ warnings.push(`${context}: empty data source; the table reference will be invalid`);
27
+ return quote('');
28
+ }
29
+ if (/\s/.test(trimmed)) {
30
+ warnings.push(`${context}: data source '${trimmed}' looks like a query, not a table reference; ` +
31
+ `emitting it verbatim (a graph element table requires a table)`);
32
+ return `(${trimmed})`;
33
+ }
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.
36
+ const source = isResourceUri(trimmed) ? finalPathSegment(trimmed) : trimmed;
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('.');
40
+ }
41
+ // `boundTable` in GoogleSQL, which is what the Spanner DDL generator wants and
42
+ // what every caller wanted when there was one backend.
43
+ function spannerTable(dataSource, warnings, context) {
44
+ return boundTable(dataSource, warnings, context, sql_identifiers_1.quoteIdentifier);
45
+ }
46
+ // A resource URI (`//host/...`) or any `scheme://...` URI, as opposed to a bare
47
+ // or dotted table reference. The loader gives the IR a BigQuery or BigLake
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.
52
+ function isResourceUri(source) {
53
+ return source.startsWith('//') || /^[a-z][\w+.-]*:\/\//i.test(source);
54
+ }
55
+ // The final non-empty path segment of a URI (`.../tables/Customer` →
56
+ // `Customer`).
57
+ function finalPathSegment(uri) {
58
+ const segments = uri.split('/').filter(s => s.length > 0);
59
+ return segments.pop() ?? uri;
60
+ }
61
+ // Splits a dotted source into its segments, treating a backtick- or
62
+ // double-quote-delimited segment as opaque so a dot INSIDE quotes does not
63
+ // split an identifier: `proj.ds.`weird.name`` yields ['proj', 'ds',
64
+ // '`weird.name`'], not a spurious break inside the quoted table name.
65
+ function splitDotted(source) {
66
+ const parts = [];
67
+ let cur = '';
68
+ let quote = null;
69
+ for (const ch of source) {
70
+ if (quote) {
71
+ cur += ch;
72
+ if (ch === quote)
73
+ quote = null;
74
+ }
75
+ else if (ch === '`' || ch === '"') {
76
+ quote = ch;
77
+ cur += ch;
78
+ }
79
+ else if (ch === '.') {
80
+ parts.push(cur);
81
+ cur = '';
82
+ }
83
+ else {
84
+ cur += ch;
85
+ }
86
+ }
87
+ parts.push(cur);
88
+ return parts;
89
+ }
90
+ function unquote(part) {
91
+ return part.replace(/^[`"]/, '').replace(/[`"]$/, '');
92
+ }
@@ -0,0 +1,24 @@
1
+ import type { ApiResult } from '../gcp/api';
2
+ export interface EntryLookup {
3
+ lookupEntry(project: string, location: string, name: string): Promise<ApiResult<{
4
+ entryType?: string;
5
+ fullyQualifiedName?: string;
6
+ entrySource?: {
7
+ system?: string;
8
+ };
9
+ }>>;
10
+ }
11
+ export type Read<T> = {
12
+ result: T;
13
+ } | {
14
+ unreadable: true;
15
+ status: 403 | 404;
16
+ message?: string;
17
+ } | {
18
+ error: string;
19
+ status?: number;
20
+ };
21
+ export declare function read<T>(what: string, call: () => Promise<ApiResult<T>>): Promise<Read<T>>;
22
+ export declare function lookupSystemEntry(catalog: EntryLookup, system: string, project: string, location: string, resourceName: string): Promise<Read<{
23
+ entryType?: string;
24
+ }>>;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ // Reading a table's Knowledge Catalog entry, which the dialect lookup and the
3
+ // source check share, so both read an entry, and the answer, the same way.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.read = read;
6
+ exports.lookupSystemEntry = lookupSystemEntry;
7
+ const dataplex_1 = require("../gcp/dataplex");
8
+ // Makes the call `call`, which `what` names in a message. Never throws.
9
+ async function read(what, call) {
10
+ let res;
11
+ try {
12
+ res = await call();
13
+ }
14
+ catch (err) {
15
+ return { error: `${what} failed: ${err?.message ?? err}` };
16
+ }
17
+ // Dataplex answers 403 both when an entry does not exist and when the caller
18
+ // may not read it; the admin APIs answer 404 for a missing resource.
19
+ if (res.status === 403 || res.status === 404) {
20
+ return { unreadable: true, status: res.status, message: res.message };
21
+ }
22
+ if (res.status !== 200 || !res.result) {
23
+ return {
24
+ error: `${what} failed (HTTP ${res.status}${res.message ? `: ${res.message}` : ''}).`,
25
+ ...(res.status !== 200 && { status: res.status }),
26
+ };
27
+ }
28
+ return { result: res.result };
29
+ }
30
+ // Looks up the catalog entry of the table whose resource name is
31
+ // `resourceName`, in the entry group `@<system>` at `location`.
32
+ function lookupSystemEntry(catalog, system, project, location, resourceName) {
33
+ const name = `${(0, dataplex_1.catalogContainer)(project, location, `@${system}`)}/entries/${resourceName}`;
34
+ return read('looking up its Knowledge Catalog entry', () => catalog.lookupEntry(project, location, name));
35
+ }
@@ -0,0 +1,26 @@
1
+ export interface ConvertResult {
2
+ yaml: string;
3
+ stats: {
4
+ classes: number;
5
+ datatypeProperties: number;
6
+ objectProperties: number;
7
+ };
8
+ warnings: string[];
9
+ }
10
+ /**
11
+ * Converts a Turtle (.ttl) OWL ontology to an OSI YAML document.
12
+ *
13
+ * `modelName` names the resulting semantic model (the CLI derives it from the
14
+ * source filename). The output is a purely LOGICAL model -- see to_ir.ts and
15
+ * the user guide -- with no physical binding; it loads and pushes to Knowledge
16
+ * Catalog as-is, and a graph deploy adds bindings on top.
17
+ *
18
+ * `opts.compactFlow` emits the compact flow YAML layout (the `owl import
19
+ * --compact` flag) instead of the default block layout; both parse identically.
20
+ *
21
+ * Throws only on malformed Turtle (the parser's error); mapping gaps are
22
+ * reported as warnings, not failures.
23
+ */
24
+ export declare function convertOwlToOsi(turtle: string, modelName: string, opts?: {
25
+ compactFlow?: boolean;
26
+ }): ConvertResult;