@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,563 @@
1
+ "use strict";
2
+ // Generates GoogleSQL property-graph DDL from the Semantic Model IR, for any
3
+ // engine that takes the GoogleSQL `CREATE PROPERTY GRAPH` statement.
4
+ //
5
+ // The IR (./ir) is pure semantics. This module emits a single `CREATE OR
6
+ // REPLACE PROPERTY GRAPH` statement over the entities' existing base tables:
7
+ // node tables, edge tables, labels, properties, and key and join columns. Both
8
+ // BigQuery and Spanner take that same statement. A `GraphEngine` carries what
9
+ // differs between the engines:
10
+ //
11
+ // 1. Names. How a table and the graph itself are named (BigQuery writes
12
+ // `project.dataset.table` and qualifies the graph name; a Spanner graph
13
+ // lives in one database and names both bare).
14
+ // 2. Measures. Which metrics become properties (BigQuery lowers each to a
15
+ // `MEASURE(...)`; Spanner Graph has none).
16
+ // 3. Element metadata. The per-element `OPTIONS` clause carrying
17
+ // descriptions and synonyms (BigQuery has one; Spanner has none).
18
+ // 4. SQL text. Which SQL a field binds to: the engine's own dialect entry
19
+ // (`BIGQUERY` or `SPANNER`), falling back to the `ANSI_SQL` one.
20
+ //
21
+ // ./bigquery supplies the BigQuery engine, and ./spanner the Spanner engine.
22
+ //
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.generateGraphDdl = generateGraphDdl;
25
+ exports.nodeKey = nodeKey;
26
+ exports.physicalColumn = physicalColumn;
27
+ const ir_1 = require("./ir");
28
+ const resolve_inheritance_1 = require("./resolve_inheritance");
29
+ const sql_expr_utils_1 = require("./sql_expr_utils");
30
+ const sql_identifiers_1 = require("./sql_identifiers");
31
+ /**
32
+ * Generates the property-graph DDL for a semantic model on one engine.
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 the engine would reject.
41
+ */
42
+ function generateGraphDdl(model, engine) {
43
+ const warnings = [];
44
+ // Resolve entity-level inheritance (`extends`) before generating: this
45
+ // flattens each subclass's inherited fields down and expands `extends` to the
46
+ // full transitive ancestor set, so labels and property signatures can be read
47
+ // straight off the resolved model. Only the graph legs run this; the KC push
48
+ // consumes the raw model, so its output is unaffected.
49
+ const resolved = (0, resolve_inheritance_1.resolveInheritance)(model);
50
+ warnings.push(...resolved.warnings);
51
+ // The IR requires entities/relationships/metrics, but be defensive against a
52
+ // hand-built or partially-deserialized model rather than throwing on `.map`.
53
+ const entities = resolved.model.entities ?? [];
54
+ const relationships = resolved.model.relationships ?? [];
55
+ const metrics = resolved.model.metrics ?? [];
56
+ // Knowledge Catalog is the only system an action or a constraint reaches
57
+ // (kc_actions.ts, kc_constraints.ts), so this leg emits nothing for either.
58
+ // Warn once each, so an author who declared them learns where they go. A push
59
+ // that leaves Knowledge Catalog out warns separately that they reach nothing
60
+ // at all (see commands.ts).
61
+ const actions = resolved.model.actions ?? [];
62
+ if (actions.length) {
63
+ warnings.push(`${actions.length} action(s) reach Knowledge Catalog only; the ` +
64
+ `${engine.name} push deploys none of them.`);
65
+ }
66
+ const constraints = resolved.model.constraints ?? [];
67
+ if (constraints.length) {
68
+ warnings.push(`${constraints.length} constraint(s) reach Knowledge Catalog only; ` +
69
+ `the ${engine.name} push deploys none of them.`);
70
+ }
71
+ // An abstract entity is conceptual: it has no physical table and produces no
72
+ // NODE TABLE, surviving only as a LABEL on its concrete descendants (whose
73
+ // node tables carry its flattened fields). Collect the abstract names so the
74
+ // node-table filter drops them while label emission still sees them.
75
+ const abstractNames = new Set(entities.filter(e => e.abstract).map(e => e.name));
76
+ // A graph node table requires a non-empty KEY: the entity's primary key, else
77
+ // its first unique key (nodeKey). An entity with neither cannot form a valid
78
+ // node, so skip it (and, below, any edge that references it) rather than emit
79
+ // `KEY()` — invalid DDL. The loader already warns about the missing key; this
80
+ // records the resulting structural drop.
81
+ const skipped = new Set();
82
+ const validEntities = entities.filter(entity => {
83
+ // An abstract entity is intentionally table-less: skip its node table (it
84
+ // still contributes LABELs to descendants) without warning -- unlike a
85
+ // missing KEY, this is by design, not a defect.
86
+ if (abstractNames.has(entity.name)) {
87
+ skipped.add(entity.name);
88
+ return false;
89
+ }
90
+ if (nodeKey(entity).length)
91
+ return true;
92
+ warnings.push(`entity '${entity.name}': empty KEY (no primary key); node table skipped, ` +
93
+ `as a graph node requires a KEY`);
94
+ skipped.add(entity.name);
95
+ return false;
96
+ });
97
+ // The ancestors carried by a table-emitting entity. Two uses. First, measure
98
+ // placement: an engine's measure may be allowed only on a LEAF type -- one
99
+ // that no *table-emitting* entity extends (see the BigQuery engine).
100
+ // Building this from validEntities (not all entities) is deliberate: an
101
+ // abstract or empty-KEY subtype emits no table, so it does not duplicate its
102
+ // parent's label and must not mark the parent non-leaf. Second, the orphan
103
+ // check below: an abstract class that is no concrete entity's ancestor
104
+ // produces no graph element at all, so warn and drop it rather than let it
105
+ // vanish silently.
106
+ const ancestorsUsed = new Set();
107
+ for (const entity of validEntities) {
108
+ for (const a of entity.extends ?? [])
109
+ ancestorsUsed.add(a);
110
+ }
111
+ for (const name of abstractNames) {
112
+ if (!ancestorsUsed.has(name)) {
113
+ warnings.push(`abstract entity '${name}' is not a supertype of any concrete ` +
114
+ `entity; it has no table and no descendant to label, so it produces ` +
115
+ `no graph element`);
116
+ }
117
+ }
118
+ // A property graph must have at least one NODE TABLE; an empty `NODE TABLES
119
+ // ()` block is invalid DDL under any circumstance. When no entity can form a
120
+ // node — the model declares none, every one is abstract (a table-less
121
+ // supertype forms no node), or every one was skipped for an empty KEY — fail
122
+ // loudly rather than emit a graph the engine would reject. The collected skip
123
+ // reasons ride along in the error so the caller sees why each entity dropped.
124
+ if (!validEntities.length) {
125
+ let reason;
126
+ if (!entities.length) {
127
+ reason = 'the model declares no entities';
128
+ }
129
+ else if (entities.every(e => e.abstract)) {
130
+ reason =
131
+ 'every entity is abstract (a table-less supertype forms no node table)';
132
+ }
133
+ else {
134
+ reason =
135
+ 'every entity was skipped because it has no primary key (a graph node requires a KEY)';
136
+ }
137
+ throw new Error(`cannot generate a property graph: ${reason}; a graph requires at ` +
138
+ `least one NODE TABLE` +
139
+ (warnings.length ? `\n${warnings.join('\n')}` : ''));
140
+ }
141
+ // Metrics are model-level; the engine places each on a node table (or skips
142
+ // it). The result is keyed by entity name; entries on a skipped entity simply
143
+ // never render.
144
+ const entityByName = new Map(entities.map(e => [e.name, e]));
145
+ const measurePropertiesByEntity = engine.placeMetrics(metrics, { entityByName, skipped, ancestorsUsed }, warnings);
146
+ // A subclass's `LABEL <ancestor>` block lists that ancestor's own (flattened)
147
+ // signature, so the label renderer must reach every LABEL CARRIER by name:
148
+ // node-forming entities plus abstract (table-less) supertypes, which have no
149
+ // node table but still contribute a label signature. A concrete entity
150
+ // dropped for an empty KEY is deliberately excluded -- it was reported as a
151
+ // structural defect, so it must not silently reappear as a shared label on a
152
+ // subclass (which would contradict the drop and expose a half-defined class);
153
+ // a subclass extending it drops that LABEL with a warning (see
154
+ // renderNodeTable).
155
+ const labelByName = new Map(entities.filter(e => e.abstract || !skipped.has(e.name))
156
+ .map(e => [e.name, e]));
157
+ // A field one node table leaves out because this engine has no SQL for it
158
+ // is left out of each abstract ancestor's label on every table too, so the
159
+ // label lists the same properties everywhere it appears.
160
+ const leftOutUnder = new Map();
161
+ for (const e of validEntities) {
162
+ for (const f of e.fields) {
163
+ if (!hasOnlyOtherEnginesSql(f, engine))
164
+ continue;
165
+ for (const a of e.extends ?? []) {
166
+ leftOutUnder.set(a, (leftOutUnder.get(a) ?? new Set()).add(f.name));
167
+ }
168
+ }
169
+ }
170
+ const nodeTables = validEntities.map(entity => renderNodeTable(entity, measurePropertiesByEntity.get(entity.name) ?? [], labelByName, leftOutUnder, ancestorsUsed.has(entity.name), engine, warnings));
171
+ const entitiesByName = new Map(validEntities.map(e => [e.name, e]));
172
+ const edgeTables = relationships
173
+ .filter(rel => {
174
+ // An edge REFERENCES both endpoint nodes; if either was skipped the
175
+ // edge cannot resolve, so drop it too.
176
+ const dangling = [rel.source.entity, rel.destination.entity].filter(n => skipped.has(n));
177
+ if (!dangling.length)
178
+ return true;
179
+ warnings.push(`relationship '${rel.name}': references skipped entity ` +
180
+ `${dangling.map(n => `'${n}'`).join(', ')}; edge omitted`);
181
+ return false;
182
+ })
183
+ .map(rel => renderEdgeTable(rel, entitiesByName, engine, warnings));
184
+ const graphName = engine.graphName(resolved.model);
185
+ const blocks = [
186
+ `CREATE OR REPLACE PROPERTY GRAPH ${graphName}`,
187
+ `NODE TABLES (\n${nodeTables.join(',\n')}\n)`,
188
+ ];
189
+ if (edgeTables.length) {
190
+ blocks.push(`EDGE TABLES (\n${edgeTables.join(',\n')}\n)`);
191
+ }
192
+ // The model's own description and AI context go to Knowledge Catalog's model
193
+ // entry (see knowledge_catalog.ts), not the graph; bigquery.ts's
194
+ // optionsClause says why.
195
+ return { ddl: blocks.join('\n') + ';\n', warnings: dedupe(warnings) };
196
+ }
197
+ function renderNodeTable(entity, measureProperties, labelByName, leftOutUnder, isSupertype, engine, warnings) {
198
+ const table = engine.table(entity.dataSource, warnings, `entity '${entity.name}'`);
199
+ // Canonical rendering of every inherited property: its supertype's OWN
200
+ // definition, keyed by property name (nearest ancestor wins, matching field
201
+ // flattening). A property that appears under a label shared across element
202
+ // tables must have one identical definition everywhere it appears, so the
203
+ // supertype is authoritative -- both this node's DEFAULT LABEL and its
204
+ // `LABEL <ancestor>` blocks render an inherited property exactly as the
205
+ // supertype's node table does. resolveInheritance localizes the inherited
206
+ // copies on this entity, so in a well-formed hierarchy `renderOwn` below is a
207
+ // no-op; the map's real job is to catch and neutralize a genuine override.
208
+ const inheritedRender = new Map();
209
+ const inheritedCore = new Map();
210
+ for (const ancestorName of entity.extends ?? []) {
211
+ const ancestor = labelByName.get(ancestorName);
212
+ // Skip an ABSTRACT ancestor: it has no table and no binding of its own, so
213
+ // it cannot be authoritative for an inherited property's rendering. This
214
+ // subtype's own binding is the canonical one (e.g. `c_name AS name`), so we
215
+ // leave the inherited names out of this map and let `renderOwn` render them
216
+ // straight from this table. A concrete ancestor keeps its authority.
217
+ if (!ancestor || ancestor.abstract)
218
+ continue;
219
+ for (const f of ancestor.fields) {
220
+ // The ancestor's node table leaves out a field this engine has no SQL
221
+ // for, so there is no rendering to defer to.
222
+ if (engine.fieldExpression(f) === undefined)
223
+ continue;
224
+ if (!inheritedRender.has(f.name)) {
225
+ inheritedRender.set(f.name, renderFieldProperty(f, ancestor.name, engine));
226
+ inheritedCore.set(f.name, renderFieldPropertyCore(f, ancestor.name, engine));
227
+ }
228
+ }
229
+ }
230
+ // Renders one of this entity's own fields, deferring to the supertype's
231
+ // canonical definition for any inherited name. A subclass CANNOT give an
232
+ // inherited property a different definition -- BigQuery requires one
233
+ // definition per property under a shared label -- so a divergent override is
234
+ // warned and dropped in favor of the supertype's, keeping the DDL valid. The
235
+ // warning distinguishes a STRUCTURAL remap (a different column/expression --
236
+ // the property is genuinely rebound) from a METADATA-only refinement (same
237
+ // column, different description/synonyms) so it states precisely what the
238
+ // subclass loses, rather than mislabeling an added description as a redefined
239
+ // column.
240
+ const renderOwn = (f) => {
241
+ const own = renderFieldProperty(f, entity.name, engine);
242
+ const canonical = inheritedRender.get(f.name);
243
+ if (canonical === undefined)
244
+ return own; // not inherited: render as-is
245
+ if (canonical === own)
246
+ return canonical; // identical: nothing to warn
247
+ if (renderFieldPropertyCore(f, entity.name, engine) !==
248
+ inheritedCore.get(f.name)) {
249
+ warnings.push(`entity '${entity.name}' remaps inherited property '${f.name}' to a different column/expression than its supertype; ` +
250
+ `${engine.name} allows only one definition per property under a ` +
251
+ `shared label, so the supertype's is used and the subclass's ` +
252
+ `remapping is dropped`);
253
+ }
254
+ else {
255
+ warnings.push(`entity '${entity.name}' overrides the description/synonyms of ` +
256
+ `inherited property '${f.name}'; under a shared label every binding ` +
257
+ `must declare it identically, so the supertype's metadata is used ` +
258
+ `and the subclass's is dropped`);
259
+ }
260
+ return canonical;
261
+ };
262
+ // Order: declared fields, then the engine's measure properties (any operand
263
+ // properties synthesized for measures, then the measures themselves, which
264
+ // reference those operand properties).
265
+ // A field is kept when the engine has SQL for it. That is narrower than
266
+ // isFieldBound, which availability pruning tests: a field bound only by
267
+ // another engine's SQL is bound, yet this engine has nothing to emit for it.
268
+ // Skip such a field with a warning naming the engine's dialect rather than
269
+ // emit that other engine's SQL. A field with no SQL at all is unbound;
270
+ // pruning normally strips it before generation, but a caller that generates
271
+ // DDL straight from a bindingOptional load without pruning could still reach
272
+ // here with one, such as a purely logical field. Skip it too rather than emit
273
+ // `<name>` as a phantom bare column the source table does not have.
274
+ const boundFields = entity.fields.filter(f => {
275
+ if (engine.fieldExpression(f) !== undefined)
276
+ return true;
277
+ warnings.push((0, ir_1.isFieldBound)(f) ?
278
+ `entity '${entity.name}': field '${f.name}' has no '${engine.dialect}' or 'ANSI_SQL' expression; omitted from the ` +
279
+ (inheritedRender.has(f.name) ?
280
+ `node table's own label, and the supertype's label keeps ` +
281
+ `the supertype's definition` :
282
+ `node table`) :
283
+ `entity '${entity.name}': field '${f.name}' has no column under ` +
284
+ `this binding; omitted from the node table (bind it, or ` +
285
+ `govern the logical model in Knowledge Catalog instead)`);
286
+ return false;
287
+ });
288
+ const properties = [...boundFields.map(renderOwn), ...measureProperties];
289
+ const lines = [
290
+ line(1, `${table} AS ${(0, sql_identifiers_1.quoteIfReserved)(entity.name)}`),
291
+ line(2, `KEY(${physicalColumns(nodeKey(entity), engine, warnings, `entity '${entity.name}'`)
292
+ .join(', ')})`),
293
+ ];
294
+ // Element-table description and synonyms attach to the node's DEFAULT LABEL
295
+ // -- UNLESS this entity is a supertype whose label is shared by subclass
296
+ // tables. BigQuery forbids a label that carries an OPTIONS clause from being
297
+ // bound to more than one element table (verified live -- "the label ... is
298
+ // defined with OPTIONS clause in one of the element tables and cannot be
299
+ // bound to another element table"), so a shared supertype label must be
300
+ // options-free; its description/synonyms are dropped (with a warning). The
301
+ // rule is BigQuery's, but it applies to any engine whose elementOptions
302
+ // returns a clause.
303
+ let labelOpts = engine.elementOptions(entity.description, entity.aiContext);
304
+ if (isSupertype && labelOpts) {
305
+ warnings.push(`entity '${entity.name}' is a supertype in a class hierarchy; its ` +
306
+ `description/synonyms are dropped from the shared '${entity.name}' ` +
307
+ `label (${engine.name} forbids OPTIONS on a label bound by multiple tables)`);
308
+ labelOpts = undefined;
309
+ }
310
+ // A node participating in the hierarchy -- as a subclass (it declares
311
+ // ancestor LABELs) or as a shared supertype (a subclass binds its label) --
312
+ // must use an EXPLICIT `DEFAULT LABEL`: BigQuery rejects a bare (implicit
313
+ // default) PROPERTIES clause immediately followed by explicit LABEL clauses
314
+ // (verified live -- "Expected \")\" ... but got keyword LABEL"), and the
315
+ // validated shared-label shape is `DEFAULT LABEL PROPERTIES(...)`. A node
316
+ // outside any hierarchy keeps the implicit form so existing output is
317
+ // byte-for-byte unchanged.
318
+ const hasAncestors = !!(entity.extends && entity.extends.length);
319
+ if (hasAncestors || isSupertype)
320
+ lines.push(line(2, 'DEFAULT LABEL'));
321
+ if (labelOpts)
322
+ lines.push(line(2, labelOpts));
323
+ lines.push(...propertiesClause(properties, entity.fields.some(f => hasOnlyOtherEnginesSql(f, engine))));
324
+ // Inheritance: declare one LABEL per transitive ancestor (resolveInheritance
325
+ // expanded `extends` to the full ancestor set), each re-listing that
326
+ // ancestor's properties so `MATCH (:Ancestor)` also matches this subclass
327
+ // node. The block is rendered straight from the ANCESTOR's own fields with
328
+ // the ancestor as qualifier, so it is byte-for-byte identical to the
329
+ // ancestor's own DEFAULT LABEL -- the exact match BigQuery requires for a
330
+ // property shared across the element tables that bind a label ("same set of
331
+ // property declarations under the same label"). This node's DEFAULT LABEL
332
+ // renders the same inherited names via `renderOwn`, which also defers to the
333
+ // ancestor, so the label is consistent within this table too.
334
+ for (const ancestorName of entity.extends ?? []) {
335
+ const ancestor = labelByName.get(ancestorName);
336
+ if (!ancestor) {
337
+ // The ancestor exists in the model (an unknown parent hard-fails in
338
+ // resolveInheritance, so it never reaches here) but is not a label
339
+ // carrier -- it was dropped for an
340
+ // empty KEY. Its fields still flattened onto this node (they render as
341
+ // own properties above); it just forms no queryable label, so omit the
342
+ // LABEL and say so rather than reference a class reported as gone.
343
+ warnings.push(`entity '${entity.name}' extends '${ancestorName}', which has no ` +
344
+ `node table and is not abstract (it was dropped, e.g. for an empty ` +
345
+ `KEY); the '${ancestorName}' label is omitted from '${entity.name}'`);
346
+ continue;
347
+ }
348
+ lines.push(line(2, `LABEL ${(0, sql_identifiers_1.quoteIfReserved)(ancestorName)}`));
349
+ let signature;
350
+ let anyLeftOut = false;
351
+ if (ancestor.abstract) {
352
+ // Render each inherited field from THIS subtype's own binding, keyed by
353
+ // the abstract ancestor's field names. Skip any the subtype leaves
354
+ // unbound so the block matches the DEFAULT LABEL (which lists only bound
355
+ // fields); a profile that binds the subtype but not an inherited field
356
+ // simply does not expose that property under the shared label.
357
+ signature = [];
358
+ for (const af of ancestor.fields) {
359
+ if (leftOutUnder.get(ancestorName)?.has(af.name)) {
360
+ anyLeftOut = true;
361
+ continue;
362
+ }
363
+ const child = boundFields.find(cf => cf.name === af.name);
364
+ if (child) {
365
+ signature.push(renderFieldProperty(child, entity.name, engine));
366
+ }
367
+ }
368
+ }
369
+ else {
370
+ // Only the fields the ancestor's own node table lists.
371
+ signature =
372
+ ancestor.fields.filter(f => engine.fieldExpression(f) !== undefined)
373
+ .map(f => renderFieldProperty(f, ancestor.name, engine));
374
+ anyLeftOut =
375
+ ancestor.fields.some(f => hasOnlyOtherEnginesSql(f, engine));
376
+ }
377
+ lines.push(...propertiesClause(signature, anyLeftOut));
378
+ }
379
+ return lines.join('\n');
380
+ }
381
+ // The properties clause of a table or label. When `anyLeftOut` is set, an
382
+ // empty list gives NO PROPERTIES, since a missing clause exposes every column.
383
+ function propertiesClause(properties, anyLeftOut) {
384
+ if (properties.length)
385
+ return [propertiesBlock(properties)];
386
+ return anyLeftOut ? [line(2, 'NO PROPERTIES')] : [];
387
+ }
388
+ // Whether a field has SQL, but none the engine reads.
389
+ function hasOnlyOtherEnginesSql(field, engine) {
390
+ return (0, ir_1.isFieldBound)(field) && engine.fieldExpression(field) === undefined;
391
+ }
392
+ // Renders a field as a graph property: a bare column when the expression is
393
+ // just the column, else `<expr> AS <name>`. Attaches the engine's metadata
394
+ // clause (label, temporal role, description, AI-first annotations) after it
395
+ // per the `derived_property` rule. A node table leaves out a field with no
396
+ // expression, with a warning. An association edge exposes it as a bare column
397
+ // under its name.
398
+ function renderFieldProperty(field, entity, engine) {
399
+ const core = renderFieldPropertyCore(field, entity, engine);
400
+ const opts = engine.fieldOptions(field);
401
+ return opts ? `${core} ${opts}` : core;
402
+ }
403
+ // Renders just the STRUCTURAL core of a field property -- the bare column, or
404
+ // `<expr> AS <name>` when the field maps a non-trivial expression -- with no
405
+ // trailing OPTIONS. This is the part BigQuery requires to be byte-for-byte
406
+ // identical across every element table binding a shared label; metadata
407
+ // (OPTIONS) is compared separately so a subclass's metadata-only refinement is
408
+ // reported distinctly from a structural remap (see renderNodeTable's
409
+ // renderOwn). This is also the single place a field expression is lowered to
410
+ // its table-local form: the resolve-inheritance pass first rewrites an
411
+ // inherited field's expression into the child's frame (localizeInheritedField),
412
+ // then this strips the entity's own qualifier -- one normalization, in one
413
+ // place, applied to own and inherited fields alike.
414
+ function renderFieldPropertyCore(field, entity, engine) {
415
+ const expr = engine.fieldExpression(field);
416
+ const local = expr !== undefined ? (0, sql_expr_utils_1.stripQualifier)(expr, entity) : field.name;
417
+ const alias = (0, sql_identifiers_1.quoteIfReserved)(field.name);
418
+ return local === field.name ? alias :
419
+ `${(0, sql_identifiers_1.quoteIfReserved)(local)} AS ${alias}`;
420
+ }
421
+ // The key an entity's node is keyed by: its primary key, else its first
422
+ // non-empty unique key. Empty when the entity states neither, and then it gets
423
+ // no node table.
424
+ function nodeKey(entity) {
425
+ if (entity.keys?.length)
426
+ return entity.keys;
427
+ return entity.uniqueKeys?.find(k => k.length > 0) ?? [];
428
+ }
429
+ // Writes a key or join column as the column it names. An unquoted name is
430
+ // backtick-quoted when it is reserved or is not a simple identifier, and a
431
+ // backtick-quoted name is written as it is.
432
+ function physicalColumn(column) {
433
+ return /^`[^`]+`$/.test(column) ? column : (0, sql_identifiers_1.quoteIdentifier)(column);
434
+ }
435
+ // Writes each key or join column with physicalColumn. A value isColumnName
436
+ // rejects is an expression rather than a column; push rejects it, and a caller
437
+ // that generates without validating gets it as written, with a warning naming
438
+ // it, rather than opaque DDL the engine rejects at deploy.
439
+ function physicalColumns(columns, engine, warnings, context) {
440
+ return columns.map(c => {
441
+ if ((0, sql_expr_utils_1.isColumnName)(c))
442
+ return physicalColumn(c);
443
+ warnings.push(`${context}: key or join column '${c}' is not a physical column ` +
444
+ `name; a KEY/REFERENCES site requires a column, so ${engine.name} will reject the generated DDL`);
445
+ return c;
446
+ });
447
+ }
448
+ function renderEdgeTable(rel, entitiesByName, engine, warnings) {
449
+ // A many-to-many relationship is backed by its own association table rather
450
+ // than a source entity's foreign key; render it from that block.
451
+ if (rel.association) {
452
+ return renderAssociationEdge(rel, rel.association, entitiesByName, engine, warnings);
453
+ }
454
+ // A relationship is a direct foreign key: the SOURCE entity's own base table
455
+ // backs the edge (one edge row per source row). Its FK columns
456
+ // (`rel.source.columns`) reference the destination's key columns
457
+ // (`rel.destination.columns`); the edge is keyed by, and references its
458
+ // source node through, the key of the source entity's node (nodeKey, looked up
459
+ // here rather than duplicated onto the relationship).
460
+ const sourceEntity = entitiesByName.get(rel.source.entity);
461
+ const relCtx = `relationship '${rel.name}'`;
462
+ let backing;
463
+ let sourceKey;
464
+ if (!sourceEntity) {
465
+ // Only hand-built IR gets here: the loader rejects a relationship whose
466
+ // endpoint is not one of the model's entities. Name the table after the
467
+ // entity.
468
+ warnings.push(`relationship '${rel.name}': unknown source entity '${rel.source.entity}'`);
469
+ backing = engine.table(rel.source.entity, warnings, relCtx);
470
+ sourceKey = rel.source.columns;
471
+ }
472
+ else {
473
+ backing = engine.table(sourceEntity.dataSource, warnings, relCtx);
474
+ sourceKey = nodeKey(sourceEntity);
475
+ }
476
+ // Every key clause names physical columns: the edge is the source entity's own
477
+ // table, so its KEY and SOURCE KEY are the source node's key, and its
478
+ // DESTINATION KEY is the FK, the relationship's `from_columns`, referencing
479
+ // the destination's `to_columns`.
480
+ const key = physicalColumns(sourceKey, engine, warnings, relCtx).join(', ');
481
+ const destFk = physicalColumns(rel.source.columns, engine, warnings, relCtx).join(', ');
482
+ const destRef = physicalColumns(rel.destination.columns, engine, warnings, relCtx)
483
+ .join(', ');
484
+ const lines = [
485
+ line(1, `${backing} AS ${(0, sql_identifiers_1.quoteIfReserved)(rel.name)}`),
486
+ line(2, `KEY(${key})`),
487
+ line(2, `SOURCE KEY(${key}) REFERENCES ${(0, sql_identifiers_1.quoteIfReserved)(rel.source.entity)}(${key})`),
488
+ line(2, `DESTINATION KEY(${destFk}) REFERENCES ${(0, sql_identifiers_1.quoteIfReserved)(rel.destination.entity)}(${destRef})`),
489
+ ];
490
+ // Edge description and synonyms attach to the DEFAULT LABEL: after the
491
+ // SOURCE/DESTINATION clauses (grammar: element_table_definition).
492
+ const labelOpts = engine.elementOptions(rel.description, rel.aiContext);
493
+ if (labelOpts)
494
+ lines.push(line(2, labelOpts));
495
+ return lines.join('\n');
496
+ }
497
+ // Renders a many-to-many edge backed by an association (junction) table. Unlike
498
+ // a direct FK, the edge has its OWN backing table and KEY, each endpoint's
499
+ // SOURCE/DESTINATION KEY names the junction columns referencing the key of
500
+ // that entity's node, and the junction's own `fields` become edge PROPERTIES.
501
+ function renderAssociationEdge(rel, assoc, entitiesByName, engine, warnings) {
502
+ const relCtx = `relationship '${rel.name}'`;
503
+ const backing = engine.table(assoc.dataSource, warnings, relCtx);
504
+ if (!assoc.keys?.length) {
505
+ warnings.push(`relationship '${rel.name}': association table has no KEY; the edge ` +
506
+ `table will be invalid (an edge requires a KEY)`);
507
+ }
508
+ // The REFERENCES target is the key of each endpoint's node; fall back to
509
+ // the endpoint's own columns (and warn) when the entity cannot be resolved.
510
+ const refColumns = (end) => {
511
+ const entity = entitiesByName.get(end.entity);
512
+ if (!entity) {
513
+ warnings.push(`relationship '${rel.name}': unknown entity '${end.entity}'`);
514
+ }
515
+ return physicalColumns(entity ? nodeKey(entity) : end.columns, engine, warnings, relCtx)
516
+ .join(', ');
517
+ };
518
+ // The junction table's own key and join columns.
519
+ const columns = (names) => physicalColumns(names, engine, warnings, relCtx).join(', ');
520
+ const lines = [
521
+ line(1, `${backing} AS ${(0, sql_identifiers_1.quoteIfReserved)(rel.name)}`),
522
+ line(2, `KEY(${columns(assoc.keys)})`),
523
+ line(2, `SOURCE KEY(${columns(assoc.sourceColumns)}) ` +
524
+ `REFERENCES ${(0, sql_identifiers_1.quoteIfReserved)(rel.source.entity)}(${refColumns(rel.source)})`),
525
+ line(2, `DESTINATION KEY(${columns(assoc.destinationColumns)}) ` +
526
+ `REFERENCES ${(0, sql_identifiers_1.quoteIfReserved)(rel.destination.entity)}(${refColumns(rel.destination)})`),
527
+ ];
528
+ // Edge description and synonyms attach to the DEFAULT LABEL: after the
529
+ // SOURCE/DESTINATION clauses, before PROPERTIES (grammar:
530
+ // element_table_definition).
531
+ const labelOpts = engine.elementOptions(rel.description, rel.aiContext);
532
+ if (labelOpts)
533
+ lines.push(line(2, labelOpts));
534
+ // The junction's own non-key fields are the edge's properties.
535
+ const properties = (assoc.fields ?? [])
536
+ .filter(f => {
537
+ if (engine.fieldExpression(f) !== undefined || !(0, ir_1.isFieldBound)(f)) {
538
+ return true;
539
+ }
540
+ warnings.push(`relationship '${rel.name}': field '${f.name}' has no '${engine.dialect}' or 'ANSI_SQL' expression; omitted from ` +
541
+ `the edge table`);
542
+ return false;
543
+ })
544
+ .map(f => renderFieldProperty(f, rel.name, engine));
545
+ lines.push(...propertiesClause(properties, (assoc.fields ?? []).some(f => hasOnlyOtherEnginesSql(f, engine))));
546
+ return lines.join('\n');
547
+ }
548
+ // All generated indentation flows through this single mechanism: one nesting
549
+ // level == one INDENT. `line` indents one line to a depth; `list` indents a set
550
+ // of lines and joins them comma-separated; `propertiesBlock` is the shared
551
+ // `PROPERTIES( ... )` shape used by both node and edge tables. Keeping every
552
+ // indent derived from `depth` (rather than hardcoded spaces) makes the output
553
+ // indentation consistent by construction.
554
+ const INDENT = ' ';
555
+ const pad = (depth) => INDENT.repeat(depth);
556
+ const line = (depth, text) => `${pad(depth)}${text}`;
557
+ const list = (depth, lines) => lines.map(l => line(depth, l)).join(',\n');
558
+ function propertiesBlock(properties) {
559
+ return `${line(2, 'PROPERTIES(')}\n${list(3, properties)}\n${line(2, ')')}`;
560
+ }
561
+ function dedupe(items) {
562
+ return [...new Set(items)];
563
+ }
@@ -0,0 +1,50 @@
1
+ import { SerializedFile } from './osi_converter';
2
+ /** Options controlling how a semantic model YAML document is imported. */
3
+ export interface ImportSemanticModelOptions {
4
+ /**
5
+ * Target output flavor (`'ossie'` or `'google'`); defaults to input flavor.
6
+ */
7
+ flavor?: 'ossie' | 'google';
8
+ /**
9
+ * First-party GCP container resource name used to qualify bare table
10
+ * sources.
11
+ */
12
+ sourcePrefix?: string;
13
+ /** When true, translates vendor or double-quoted ANSI SQL to `BIGQUERY`. */
14
+ transpile?: boolean;
15
+ /**
16
+ * When true, treats any third-party repair warning as a hard error, except
17
+ * completing a bare table source from `sourcePrefix`.
18
+ */
19
+ strict?: boolean;
20
+ /**
21
+ * Sibling `<model>.profile.<name>.yaml` files located beside the input file,
22
+ * used when converting a Google-flavor model to vanilla (`'ossie'`).
23
+ */
24
+ profileFiles?: SerializedFile[];
25
+ /**
26
+ * The input file's name. The warning that a JSON input was written as YAML
27
+ * names it. Defaults to `<model>.yaml`.
28
+ */
29
+ fileName?: string;
30
+ }
31
+ /** Result of importing a semantic model YAML document. */
32
+ export interface ImportSemanticModelResult {
33
+ /** True when `errors` is empty and `files` was produced. */
34
+ ok: boolean;
35
+ /**
36
+ * Generated output files (`<model>.yaml` and any
37
+ * `<model>.profile.<name>.yaml` files), or `[]` when `ok` is false.
38
+ */
39
+ files: SerializedFile[];
40
+ /** Non-fatal repair and translation diagnostics. */
41
+ warnings: string[];
42
+ /** Validation or strict-mode errors that prevented file output. */
43
+ errors: string[];
44
+ }
45
+ /**
46
+ * Repairs, converts, optionally transpiles, and validates a semantic model YAML
47
+ * document, returning the normalized output file(s) along with any warnings or
48
+ * errors.
49
+ */
50
+ export declare function importSemanticModelYaml(rawYaml: string, opts?: ImportSemanticModelOptions): Promise<ImportSemanticModelResult>;