@google-cloud/kcmd 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +266 -2
  3. package/build/ts/kcmd/cli.js +19578 -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 +16 -0
  9. package/build/ts/kcmd/gcp/biglake.js +50 -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 +63 -0
  21. package/build/ts/kcmd/gcp/spanner.js +123 -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 +344 -0
  30. package/build/ts/kcmd/layouts/semantic-model.d.ts +61 -0
  31. package/build/ts/kcmd/layouts/semantic-model.js +542 -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 +104 -0
  42. package/build/ts/kcmd/okf/concept_to_entry.d.ts +8 -0
  43. package/build/ts/kcmd/okf/concept_to_entry.js +254 -0
  44. package/build/ts/kcmd/okf/init_okf_workspace.d.ts +15 -0
  45. package/build/ts/kcmd/okf/init_okf_workspace.js +115 -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 +82 -0
  50. package/build/ts/kcmd/semantic/bigquery.d.ts +22 -0
  51. package/build/ts/kcmd/semantic/bigquery.js +525 -0
  52. package/build/ts/kcmd/semantic/binding.d.ts +3 -0
  53. package/build/ts/kcmd/semantic/binding.js +97 -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 +129 -0
  67. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +1396 -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 +34 -0
  73. package/build/ts/kcmd/semantic/graph_ddl.js +563 -0
  74. package/build/ts/kcmd/semantic/import_model.d.ts +50 -0
  75. package/build/ts/kcmd/semantic/import_model.js +1275 -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 +31 -0
  83. package/build/ts/kcmd/semantic/kc_converter.js +824 -0
  84. package/build/ts/kcmd/semantic/kc_custom_types.d.ts +70 -0
  85. package/build/ts/kcmd/semantic/kc_custom_types.js +828 -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 +103 -0
  89. package/build/ts/kcmd/semantic/kc_ids.js +191 -0
  90. package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +32 -0
  91. package/build/ts/kcmd/semantic/knowledge_catalog.js +1043 -0
  92. package/build/ts/kcmd/semantic/loader.d.ts +262 -0
  93. package/build/ts/kcmd/semantic/loader.js +3440 -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 +911 -0
  98. package/build/ts/kcmd/semantic/parameters.d.ts +18 -0
  99. package/build/ts/kcmd/semantic/parameters.js +152 -0
  100. package/build/ts/kcmd/semantic/pull_kc.d.ts +14 -0
  101. package/build/ts/kcmd/semantic/pull_kc.js +346 -0
  102. package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +54 -0
  103. package/build/ts/kcmd/semantic/resolve_inheritance.js +330 -0
  104. package/build/ts/kcmd/semantic/resolve_profiles.d.ts +250 -0
  105. package/build/ts/kcmd/semantic/resolve_profiles.js +1822 -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 +94 -0
  111. package/build/ts/kcmd/semantic/runtime/runtime.js +318 -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 +10 -0
  117. package/build/ts/kcmd/semantic/source_dialect.js +261 -0
  118. package/build/ts/kcmd/semantic/source_entries.d.ts +12 -0
  119. package/build/ts/kcmd/semantic/source_entries.js +282 -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 +17 -0
  123. package/build/ts/kcmd/semantic/sql_expr_utils.js +221 -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 +44 -0
  127. package/build/ts/kcmd/semantic/sql_parser.js +378 -0
  128. package/build/ts/kcmd/semantic/transpile.d.ts +74 -0
  129. package/build/ts/kcmd/semantic/transpile.js +563 -0
  130. package/build/ts/kcmd/semantic/validate.d.ts +35 -0
  131. package/build/ts/kcmd/semantic/validate.js +1899 -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 +33 -0
  137. package/build/ts/kcmd/snapshot.js +379 -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 +697 -0
  150. package/package.json +62 -4
@@ -0,0 +1,525 @@
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
+ /**
31
+ * Generates the BigQuery property-graph DDL for a semantic model.
32
+ *
33
+ * The generated statement references the entities' existing base tables; it
34
+ * does not create them. Returns the DDL plus any warnings collected while
35
+ * mapping the IR (e.g. metrics that could not be placed on a single table).
36
+ *
37
+ * Throws when the model yields no valid node table -- it declares no entities,
38
+ * or every entity lacks a KEY -- since a property graph with an empty NODE
39
+ * TABLES block is invalid DDL that BigQuery would reject.
40
+ */
41
+ function generatePropertyGraph(model, opts = {}) {
42
+ return (0, graph_ddl_1.generateGraphDdl)(model, bigQueryEngine(opts));
43
+ }
44
+ // The BigQuery engine for the shared generator.
45
+ function bigQueryEngine(opts) {
46
+ return {
47
+ name: 'BigQuery',
48
+ table: qualifyTable,
49
+ graphName: model => qualifyGraph(model, opts),
50
+ placeMetrics,
51
+ elementOptions: (description, ai) => optionsClause(elementDescription(description, ai), ai?.synonyms),
52
+ fieldOptions: field => optionsClause(fieldDescription(field), field.aiContext?.synonyms),
53
+ dialect: 'BIGQUERY',
54
+ fieldExpression,
55
+ };
56
+ }
57
+ // Places each metric on the single entity its aggregate references, lowering
58
+ // it to a MEASURE over an exposed property (see placeMetric).
59
+ function placeMetrics(metrics, input, warnings) {
60
+ const loweringByEntity = new Map();
61
+ for (const metric of metrics) {
62
+ placeMetric(metric, input, loweringByEntity, warnings);
63
+ }
64
+ // The operand properties come first: the measures aggregate them.
65
+ const placed = new Map();
66
+ for (const [name, lowering] of loweringByEntity) {
67
+ placed.set(name, [...lowering.derivedProperties, ...lowering.measures]);
68
+ }
69
+ return placed;
70
+ }
71
+ // 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) {
75
+ const taken = new Set();
76
+ const fieldNames = new Set();
77
+ const byLocalExpr = new Map();
78
+ for (const f of entity.fields) {
79
+ 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);
87
+ }
88
+ return {
89
+ derivedProperties: [],
90
+ measures: [],
91
+ taken,
92
+ fieldNames,
93
+ byLocalExpr,
94
+ operandToName: new Map()
95
+ };
96
+ }
97
+ // Assigns a metric to the node table of the single entity its aggregate
98
+ // references, lowering it to a MEASURE over an exposed property. Records a
99
+ // warning (and skips) when it references zero/multiple entities or is not a
100
+ // single supported aggregate over one operand.
101
+ function placeMetric(metric, input, loweringByEntity, warnings) {
102
+ const { entityByName, skipped, ancestorsUsed } = input;
103
+ // A measure is emitted from the metric's BigQuery SQL (see
104
+ // metricExpression). A metric with SQL only for other engines is skipped
105
+ // with a warning naming the dialects it lacks, rather than emitting that
106
+ // other engine's SQL into a BigQuery graph.
107
+ const expression = metricExpression(metric);
108
+ if (expression === undefined) {
109
+ warnings.push(metric.dialects?.length || metric.importedExpression !== undefined ?
110
+ `metric '${metric.name}' has no 'BIGQUERY' or 'ANSI_SQL' ` +
111
+ `expression; skipped (nothing to emit as a MEASURE)` :
112
+ `metric '${metric.name}' has no expression; skipped (nothing ` +
113
+ `to emit as a MEASURE)`);
114
+ return;
115
+ }
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
+ }
127
+ if (referenced.length !== 1) {
128
+ const detail = referenced.length === 0 ?
129
+ 'references no known entity' :
130
+ `spans multiple tables (${referenced.join(', ')})`;
131
+ warnings.push(`metric '${metric.name}' ${detail}; skipped (cannot be a single MEASURE)`);
132
+ return;
133
+ }
134
+ const entityName = referenced[0];
135
+ // The IR also declares a single attach entity (metric.entity). Placement is
136
+ // driven by the qualifiers actually present in the expression (that is what
137
+ // we strip and attach), but a disagreement with the declared entity signals
138
+ // an inconsistent model, so surface it. Emit this only now that we know the
139
+ // metric will actually be placed, so a metric that is subsequently dropped
140
+ // does not carry a contradictory "placing per the expression" note.
141
+ if (metric.entity && entityName !== metric.entity) {
142
+ warnings.push(`metric '${metric.name}' declares entity '${metric.entity}' but its ` +
143
+ `expression references '${entityName}'; placing per the expression`);
144
+ }
145
+ const entity = entityByName.get(entityName);
146
+ if (!entity) {
147
+ warnings.push(`metric '${metric.name}' references unknown entity '${entityName}'; skipped (cannot be a single MEASURE)`);
148
+ return;
149
+ }
150
+ // An abstract entity is a table-less supertype with no node table, so it can
151
+ // carry no MEASURE. Report that specifically -- it is also in `skipped`, so
152
+ // this must precede the generic no-KEY message below to stay accurate.
153
+ if (entity.abstract) {
154
+ warnings.push(`metric '${metric.name}' targets entity '${entityName}', which is ` +
155
+ `abstract (a table-less supertype with no node table to carry a ` +
156
+ `MEASURE); metric dropped`);
157
+ return;
158
+ }
159
+ // A metric can only become a MEASURE on a node table, but an entity that was
160
+ // skipped upstream (empty KEY) has no node table to carry it. Check the skip
161
+ // set already computed, and report the drop directly, instead of letting it
162
+ // fail later with a misleading "aggregate not supported" message.
163
+ if (skipped.has(entityName)) {
164
+ warnings.push(`metric '${metric.name}' targets entity '${entityName}', which has no KEY and was skipped; metric dropped`);
165
+ return;
166
+ }
167
+ // A measure is allowed only on a LEAF type -- one that no table-emitting
168
+ // entity extends. A metric lowers to a MEASURE on the target's DEFAULT LABEL;
169
+ // when that entity is a supertype its label is shared with every subtype
170
+ // table, and BigQuery forbids a MEASURE on a label carried by more than one
171
+ // element table (verified live: "defined as MEASURE, but there are other
172
+ // declarations with the same name"). Drop it with a warning rather than emit
173
+ // DDL BigQuery rejects. Push validation rejects such a metric before
174
+ // generation; this guard covers a caller that generates DDL without
175
+ // validating first.
176
+ if (ancestorsUsed.has(entityName)) {
177
+ warnings.push(`metric '${metric.name}' targets entity '${entityName}', which is not ` +
178
+ `a leaf type (another entity extends it); a measure is allowed only on ` +
179
+ `a leaf type, because a supertype's label is shared across its ` +
180
+ `subtype tables; skipped`);
181
+ return;
182
+ }
183
+ const body = (0, sql_expr_utils_1.stripQualifier)(expression, entityName);
184
+ // A graph measure must be one supported aggregate wrapping a single operand;
185
+ // anything else (a non-aggregate, or a compound of aggregates like a ratio)
186
+ // cannot be a single MEASURE. Flag it rather than emit DDL BigQuery rejects.
187
+ const agg = extractAggregate(body);
188
+ if (!agg) {
189
+ warnings.push(`metric '${metric.name}' expression '${body}' is not a single supported aggregate ` +
190
+ `(${sql_expr_utils_1.SUPPORTED_AGGREGATES.join(', ')}) over one operand; skipped (cannot be a single MEASURE)`);
191
+ return;
192
+ }
193
+ const lowering = loweringByEntity.get(entityName) ?? newLowering(entity);
194
+ 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];
210
+ }
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)`);
219
+ return;
220
+ }
221
+ // The measure is itself exposed as a node property named `metric.name`, so it
222
+ // must not collide with a field, a prior measure, or a synthesized operand
223
+ // property — a duplicate property makes BigQuery reject the whole node table.
224
+ // A clash with an already-taken name is a model error we cannot paper over by
225
+ // renaming (the measure name is exactly what a reader selects via
226
+ // GRAPH_EXPAND + AGG(<label>_<name>)), so skip + warn. Reserve it up front so
227
+ // the operand lowering below never synthesizes a property with the same name.
228
+ if (lowering.taken.has(metric.name)) {
229
+ warnings.push(`metric '${metric.name}' collides with an existing property of entity ` +
230
+ `'${entityName}'; skipped (rename the metric to avoid a duplicate graph property)`);
231
+ return;
232
+ }
233
+ lowering.taken.add(metric.name);
234
+ const propName = countsKey ?
235
+ exposeKeyColumn(lowering, operandExpr, metric.name) :
236
+ exposeOperand(lowering, operandExpr, metric.name);
237
+ const aggregate = `${agg.fn}(${agg.distinct ? 'DISTINCT ' : ''}${(0, sql_identifiers_1.quoteIfReserved)(propName)})`;
238
+ const opts = optionsClause(elementDescription(metric.description, metric.aiContext), metric.aiContext?.synonyms);
239
+ const measure = `MEASURE(${aggregate}) AS ${(0, sql_identifiers_1.quoteIfReserved)(metric.name)}`;
240
+ lowering.measures.push(opts ? `${measure} ${opts}` : measure);
241
+ }
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
254
+ // `<column> AS <field>`, and a MEASURE may reference a sibling alias).
255
+ // Synthesizing an input property here would emit `<field> AS ..._input`, and
256
+ // 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;
261
+ 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;
265
+ lowering.derivedProperties.push((0, sql_identifiers_1.quoteIfReserved)(name));
266
+ }
267
+ else {
268
+ name = uniqueName(`${metricName}_input`, lowering.taken);
269
+ lowering.derivedProperties.push(`${(0, sql_identifiers_1.quoteIfReserved)(operandExpr)} AS ${name}`);
270
+ }
271
+ 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);
290
+ return name;
291
+ }
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))
392
+ return base;
393
+ for (let i = 2;; i++) {
394
+ const candidate = `${base}_${i}`;
395
+ if (!taken.has(candidate))
396
+ return candidate;
397
+ }
398
+ }
399
+ // The SQL BigQuery runs for a field or metric: its `BIGQUERY` dialect entry,
400
+ // else its `ANSI_SQL` one (see expressionForDialect), read as BigQuery with its
401
+ // comments removed. Another engine's vendor SQL is never returned. The DDL puts
402
+ // ` AS <name>` and a comma after the expression on the same line, and a line
403
+ // comment left in would swallow them.
404
+ function fieldExpression(f) {
405
+ return bigQuerySql(f);
406
+ }
407
+ function metricExpression(m) {
408
+ return bigQuerySql(m);
409
+ }
410
+ function bigQuerySql(item) {
411
+ const text = (0, ir_1.expressionForDialect)(item, 'BIGQUERY');
412
+ return text === undefined ? undefined : (0, sql_expr_utils_1.sqlWithoutComments)(text);
413
+ }
414
+ // Builds a backtick-quoted table reference from the IR's `dataSource` string.
415
+ // The IR contract guarantees `dataSource` is already canonical and
416
+ // fully-qualified: the producer (loader) normalizes a BigQuery or BigLake
417
+ // source into a dotted `project.dataset.table` (or four-part BigLake catalog
418
+ // name). For BigQuery that canonical form IS the target addressing scheme, so
419
+ // this emits it verbatim and NEVER prepends the graph's own project/dataset --
420
+ // those name where the graph is CREATED, not where a source table lives, and
421
+ // the two can differ (e.g. a graph in `proj.demo` over source tables in
422
+ // `samples.tpch`). Qualifying a source table is the producer's job, not the
423
+ // emitter's.
424
+ function qualifyTable(dataSource, warnings, context) {
425
+ const trimmed = (dataSource ?? '').trim();
426
+ if (!trimmed) {
427
+ warnings.push(`${context}: empty data source; the table reference will be invalid`);
428
+ return '``';
429
+ }
430
+ return `\`${trimmed}\``;
431
+ }
432
+ // Builds the dataset-qualified graph name, filling a missing project/dataset
433
+ // from opts and then from the first entity's qualified dataSource.
434
+ function qualifyGraph(model, opts) {
435
+ const name = opts.graphName ?? model.name;
436
+ let project = opts.project;
437
+ let dataset = opts.dataset;
438
+ // Pick the first entity with a non-empty `dataSource` to derive the graph's
439
+ // location; skip abstract entities (empty source).
440
+ const first = (model.entities ?? []).find(e => e.dataSource);
441
+ if ((!project || !dataset) && first) {
442
+ const p = first.dataSource.trim().split('.');
443
+ if (p.length >= 3) {
444
+ project = project ?? p[0];
445
+ dataset = dataset ?? p[1];
446
+ }
447
+ else if (p.length === 2) {
448
+ dataset = dataset ?? p[0];
449
+ }
450
+ }
451
+ // A graph name is `name`, `dataset.name`, or `project.dataset.name`; a
452
+ // project with no dataset cannot be expressed (`project.name` would be read
453
+ // as `dataset.name`), so drop a lone project rather than emit a malformed
454
+ // reference.
455
+ if (project && !dataset)
456
+ project = undefined;
457
+ const parts = [project, dataset, name].filter((p) => !!p);
458
+ return `\`${parts.join('.')}\``;
459
+ }
460
+ // Composes the folded `description` text for a graph element from its base
461
+ // description and the AI-first annotations that have no dedicated BigQuery
462
+ // option: instructions and examples. Synonyms are NOT folded in here --
463
+ // BigQuery's PropertyGraph{Label,Property}Options carry a structured `synonyms`
464
+ // array, so they are emitted as their own option (see optionsClause and the
465
+ // call sites).
466
+ function elementDescription(description, ai) {
467
+ return composeText([
468
+ description,
469
+ ai?.instructions,
470
+ examplesLine(ai?.examples),
471
+ ]);
472
+ }
473
+ // A field additionally carries a human `label` and a temporal-dimension role,
474
+ // which lead the composed text (matching the authored order) ahead of its
475
+ // description and instructions/examples. Synonyms are emitted structurally, as
476
+ // for elementDescription.
477
+ function fieldDescription(field) {
478
+ return composeText([
479
+ field.label,
480
+ (0, ir_1.isTimeDimension)(field) ? 'Time dimension.' : undefined,
481
+ field.description,
482
+ field.aiContext?.instructions,
483
+ examplesLine(field.aiContext?.examples),
484
+ ]);
485
+ }
486
+ function examplesLine(examples) {
487
+ return examples && examples.length ? `Examples: ${examples.join('; ')}` :
488
+ undefined;
489
+ }
490
+ // Joins the non-empty parts into one description, separated by blank lines so
491
+ // each reads as its own paragraph in the emitted metadata.
492
+ function composeText(parts) {
493
+ const kept = parts.map(p => (p === undefined ? undefined : p.trim()))
494
+ .filter((p) => !!p);
495
+ return kept.length ? kept.join('\n\n') : undefined;
496
+ }
497
+ // Renders the trailing `OPTIONS(...)` clause for a graph element -- a folded
498
+ // `description` string and/or a structured `synonyms` array -- or undefined
499
+ // when there is nothing to emit. Both map to BigQuery's
500
+ // PropertyGraph{Label,Property}Options (`description` singular, `synonyms`
501
+ // repeated), honored on element-table labels (DEFAULT LABEL, before PROPERTIES)
502
+ // and on derived properties and measures (after the alias). Statement-level
503
+ // graph OPTIONS is intentionally not emitted: it parses but BigQuery silently
504
+ // drops it (verified live; create_property_graph_options is off for BigQuery).
505
+ function optionsClause(description, synonyms) {
506
+ const parts = [];
507
+ if (description)
508
+ parts.push(`description=${quote(description)}`);
509
+ if (synonyms && synonyms.length) {
510
+ parts.push(`synonyms=[${synonyms.map(quote).join(', ')}]`);
511
+ }
512
+ return parts.length ? `OPTIONS(${parts.join(', ')})` : undefined;
513
+ }
514
+ // Renders a value as a BigQuery double-quoted string literal. Backslash and the
515
+ // quote are escaped, and control characters that cannot appear raw inside a
516
+ // quoted literal (newline, carriage return, tab) are escaped too, so a
517
+ // multi-line description does not produce a broken literal.
518
+ function quote(s) {
519
+ const escaped = s.replace(/\\/g, '\\\\')
520
+ .replace(/"/g, '\\"')
521
+ .replace(/\n/g, '\\n')
522
+ .replace(/\r/g, '\\r')
523
+ .replace(/\t/g, '\\t');
524
+ return `"${escaped}"`;
525
+ }
@@ -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,97 @@
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 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.
28
+ function boundTable(dataSource, warnings, context, quote) {
29
+ const trimmed = (dataSource ?? '').trim();
30
+ if (!trimmed) {
31
+ warnings.push(`${context}: empty data source; the table reference will be invalid`);
32
+ return quote('');
33
+ }
34
+ if (/\s/.test(trimmed)) {
35
+ warnings.push(`${context}: data source '${trimmed}' looks like a query, not a table reference; ` +
36
+ `emitting it verbatim (a graph element table requires a table)`);
37
+ return `(${trimmed})`;
38
+ }
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.
42
+ const source = isResourceUri(trimmed) ? finalPathSegment(trimmed) : trimmed;
43
+ const last = splitDotted(source).map(unquote).pop() ?? source;
44
+ return quote(last);
45
+ }
46
+ // `boundTable` in GoogleSQL, which is what the Spanner DDL generator wants and
47
+ // what every caller wanted when there was one backend.
48
+ function spannerTable(dataSource, warnings, context) {
49
+ return boundTable(dataSource, warnings, context, sql_identifiers_1.quoteIdentifier);
50
+ }
51
+ // A resource URI (`//host/...`) or any `scheme://...` URI, as opposed to a bare
52
+ // 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.
57
+ function isResourceUri(source) {
58
+ return source.startsWith('//') || /^[a-z][\w+.-]*:\/\//i.test(source);
59
+ }
60
+ // The final non-empty path segment of a URI (`.../tables/Customer` →
61
+ // `Customer`).
62
+ function finalPathSegment(uri) {
63
+ const segments = uri.split('/').filter(s => s.length > 0);
64
+ return segments.pop() ?? uri;
65
+ }
66
+ // Splits a dotted source into its segments, treating a backtick- or
67
+ // double-quote-delimited segment as opaque so a dot INSIDE quotes does not
68
+ // split an identifier: `proj.ds.`weird.name`` yields ['proj', 'ds',
69
+ // '`weird.name`'], not a spurious break inside the quoted table name.
70
+ function splitDotted(source) {
71
+ const parts = [];
72
+ let cur = '';
73
+ let quote = null;
74
+ for (const ch of source) {
75
+ if (quote) {
76
+ cur += ch;
77
+ if (ch === quote)
78
+ quote = null;
79
+ }
80
+ else if (ch === '`' || ch === '"') {
81
+ quote = ch;
82
+ cur += ch;
83
+ }
84
+ else if (ch === '.') {
85
+ parts.push(cur);
86
+ cur = '';
87
+ }
88
+ else {
89
+ cur += ch;
90
+ }
91
+ }
92
+ parts.push(cur);
93
+ return parts;
94
+ }
95
+ function unquote(part) {
96
+ return part.replace(/^[`"]/, '').replace(/[`"]$/, '');
97
+ }
@@ -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
+ }>>;