@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,390 @@
1
+ "use strict";
2
+ // OwlModel -> Semantic Model IR mapping.
3
+ //
4
+ // This is the whole OWL -> OSI policy layer. The parser (parse.ts) is
5
+ // mechanical; every decision about how an ontology becomes a semantic model
6
+ // lives here, in one place, so the mapping is easy to read and to evolve.
7
+ //
8
+ // The mapping (see the user guide's table). Each line is one construct, kept
9
+ // short so a comment reflow cannot run the columns together:
10
+ // owl:Class -> dataset (entity), no source (logical)
11
+ // owl:DatatypeProperty -> field on each domain class's dataset
12
+ // owl:ObjectProperty -> relationship (edge), domain -> range
13
+ // rdfs:range xsd:* -> field datatype (see XSD_DATATYPES)
14
+ // owl:hasKey -> dataset primary_key
15
+ // owl:InverseFunctionalProperty -> dataset unique_keys (or primary_key)
16
+ // rdfs:subClassOf -> dataset extends (entity inheritance)
17
+ // rdfs:label -> field label / synonym (no label slot)
18
+ // rdfs:comment/skos:definition/dcterms:/dc: -> description
19
+ // skos:example -> ai_context.examples
20
+ // owl:Ontology header -> model description / ai_context
21
+ //
22
+ // The importer is IMPORT-ONLY: it maps the constructs above and DROPS every
23
+ // other OWL construct rather than carrying it. Facts with no native OSI home --
24
+ // rdfs:subPropertyOf, owl:inverseOf, owl:equivalentClass / owl:disjointWith /
25
+ // owl:equivalentProperty / owl:propertyDisjointWith, the property
26
+ // characteristics (symmetric, transitive, ...), owl:oneOf,
27
+ // owl:propertyChainAxiom, the owl:AllDisjoint* / owl:AllDifferent set axioms,
28
+ // rdfs:seeAlso / isDefinedBy, and owl:deprecated / versionInfo -- are NOT
29
+ // imported. An earlier version carried them verbatim in a GOOGLE custom
30
+ // extension; that was removed so an imported model is a clean OSI model with no
31
+ // opaque carrier. The user guide's table documents what maps and what drops.
32
+ //
33
+ // The result is a purely LOGICAL model: an ontology declares meaning, not
34
+ // physical tables, so entities carry no source, fields no expression, and
35
+ // relationships no join columns -- only the logical shape (entities, fields,
36
+ // keys, and edges by direction). `kcmd push` publishes it as-is.
37
+ // A BigQuery or Spanner Graph deploy needs each edge's join columns added to
38
+ // the model (logical grain the model owns) plus a physical binding (sources,
39
+ // field columns) and a deployment target; that binding is a separate step (a
40
+ // binding profile, see the user guide, "Going from ontology to a running
41
+ // graph").
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.owlToIr = owlToIr;
44
+ // --- Datatype mapping. ------------------------------------------------------
45
+ const XSD = 'http://www.w3.org/2001/XMLSchema#';
46
+ // The xsd:* ranges we map to an OSI datatype. Everything else falls back to
47
+ // Opaque (a valid, lossless "unknown logical type" per the IR). Kept as a table
48
+ // so adding ranges is a one-line change; the user guide documents this set.
49
+ //
50
+ // The OSI DataType vocabulary is closed (String / Integer / Decimal / Float /
51
+ // Boolean / Date / Time / DateTime / DateTimeTz / Opaque), so several xsd types
52
+ // collapse onto one OSI type (e.g. every bounded/unsigned integer -> Integer,
53
+ // float and double -> Float): the logical type is preserved, physical width is
54
+ // not (it belongs to the bound source, not the ontology).
55
+ const XSD_DATATYPES = {
56
+ // Text.
57
+ [`${XSD}string`]: 'String',
58
+ [`${XSD}normalizedString`]: 'String',
59
+ [`${XSD}token`]: 'String',
60
+ [`${XSD}language`]: 'String',
61
+ [`${XSD}Name`]: 'String',
62
+ [`${XSD}NCName`]: 'String',
63
+ [`${XSD}anyURI`]: 'String',
64
+ // Integers (all widths / signednesses collapse to Integer).
65
+ [`${XSD}integer`]: 'Integer',
66
+ [`${XSD}int`]: 'Integer',
67
+ [`${XSD}long`]: 'Integer',
68
+ [`${XSD}short`]: 'Integer',
69
+ [`${XSD}byte`]: 'Integer',
70
+ [`${XSD}nonNegativeInteger`]: 'Integer',
71
+ [`${XSD}nonPositiveInteger`]: 'Integer',
72
+ [`${XSD}positiveInteger`]: 'Integer',
73
+ [`${XSD}negativeInteger`]: 'Integer',
74
+ [`${XSD}unsignedLong`]: 'Integer',
75
+ [`${XSD}unsignedInt`]: 'Integer',
76
+ [`${XSD}unsignedShort`]: 'Integer',
77
+ [`${XSD}unsignedByte`]: 'Integer',
78
+ // Exact and approximate numerics.
79
+ [`${XSD}decimal`]: 'Decimal',
80
+ [`${XSD}float`]: 'Float',
81
+ [`${XSD}double`]: 'Float',
82
+ // Boolean.
83
+ [`${XSD}boolean`]: 'Boolean',
84
+ // Temporal.
85
+ [`${XSD}date`]: 'Date',
86
+ [`${XSD}time`]: 'Time',
87
+ [`${XSD}dateTime`]: 'DateTime',
88
+ [`${XSD}dateTimeStamp`]: 'DateTimeTz',
89
+ };
90
+ function datatypeFor(rangeIri) {
91
+ if (rangeIri && XSD_DATATYPES[rangeIri])
92
+ return XSD_DATATYPES[rangeIri];
93
+ return 'Opaque';
94
+ }
95
+ // The temporal OSI datatypes. A field of one of these is a time dimension by
96
+ // OSI's own rule (see ir.isTimeDimension), so the mapper marks it with a
97
+ // dimension block; OWL itself has no dimension concept.
98
+ const TEMPORAL_TYPES = new Set(['Date', 'Time', 'DateTime', 'DateTimeTz']);
99
+ // --- Label / synonym policy. ------------------------------------------------
100
+ // True when an rdfs:label carries nothing over the term's own name -- e.g. a
101
+ // class named `Customer` labeled "Customer", or an object property `placedBy`
102
+ // labeled "placed by". Compared case-insensitively with non-alphanumerics
103
+ // stripped, so a spaced/cased human rendering of the same name is treated as
104
+ // redundant and dropped rather than duplicated as a label or synonym.
105
+ function isRedundantLabel(label, name) {
106
+ const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
107
+ return norm(label) === norm(name);
108
+ }
109
+ // Drops synonyms that merely respace/recase the term's own name -- the same
110
+ // redundancy rule the primary label gets -- so an alternate label identical to
111
+ // the name is not emitted as a synonym.
112
+ function nonRedundant(names, name) {
113
+ return names.filter(s => !isRedundantLabel(s, name));
114
+ }
115
+ // The display label for a FIELD: the OSI `label` slot exists only on fields, so
116
+ // a datatype property's rdfs:label lands here -- unless it is redundant with
117
+ // the field name, in which case it is dropped.
118
+ function fieldLabel(label, name) {
119
+ if (label && !isRedundantLabel(label, name))
120
+ return label;
121
+ return undefined;
122
+ }
123
+ // The ai_context for a term that has NO label slot (classes, relationships): a
124
+ // non-redundant rdfs:label becomes an alternate name, joined with any explicit
125
+ // synonyms (extra labels / skos labels) and examples. Returns undefined when
126
+ // there is nothing, so no empty ai_context is emitted.
127
+ function synonymAiContext(label, name, synonyms, examples) {
128
+ const names = [];
129
+ if (label && !isRedundantLabel(label, name))
130
+ names.push(label);
131
+ names.push(...nonRedundant(synonyms, name));
132
+ return buildAiContext(undefined, names, examples);
133
+ }
134
+ // The ai_context for a FIELD (which already consumed its primary label into the
135
+ // `label` slot): only the explicit synonyms and examples remain. Undefined when
136
+ // empty.
137
+ function fieldAiContext(synonyms, name, examples) {
138
+ return buildAiContext(undefined, nonRedundant(synonyms, name), examples);
139
+ }
140
+ // The ai_context for a RELATIONSHIP. Unlike datasets/fields, the OSI
141
+ // relationship has no `description` slot (see the Apache OSI schema), so an
142
+ // object property's comment is carried as ai_context `instructions`, and its
143
+ // non-redundant label/synonyms/examples as the remaining fields. Undefined when
144
+ // all are empty.
145
+ function relationshipAiContext(label, name, synonyms, comment, examples) {
146
+ const names = [];
147
+ if (label && !isRedundantLabel(label, name))
148
+ names.push(label);
149
+ names.push(...nonRedundant(synonyms, name));
150
+ return buildAiContext(comment, names, examples);
151
+ }
152
+ // The ai_context for the MODEL, from the ontology header: labels/synonyms and
153
+ // examples (the description rides in the model `description`, not here).
154
+ function ontologyAiContext(ontology, modelName) {
155
+ if (!ontology)
156
+ return undefined;
157
+ return buildAiContext(undefined, nonRedundant(ontology.synonyms, modelName), ontology.examples);
158
+ }
159
+ // Assembles an AiContext from its parts, deduping names/examples and dropping
160
+ // empties, and returns undefined when nothing is left -- the single place the
161
+ // {instructions, synonyms, examples} shape is built.
162
+ function buildAiContext(instructions, synonyms, examples) {
163
+ const ai = {};
164
+ if (instructions)
165
+ ai.instructions = instructions;
166
+ if (synonyms.length)
167
+ ai.synonyms = dedupe(synonyms);
168
+ if (examples.length)
169
+ ai.examples = dedupe(examples);
170
+ return ai.instructions || ai.synonyms || ai.examples ? ai : undefined;
171
+ }
172
+ function dedupe(items) {
173
+ return [...new Set(items)];
174
+ }
175
+ function arraysEqual(a, b) {
176
+ return a.length === b.length && a.every((v, i) => v === b[i]);
177
+ }
178
+ // --- Mapping. ---------------------------------------------------------------
179
+ /**
180
+ * Maps a parsed OwlModel to a Semantic Model IR (see ../../ir.ts).
181
+ *
182
+ * `modelName` names the resulting semantic model (the CLI derives it from the
183
+ * source filename). The IR it returns serializes, via osi_converter, to the
184
+ * OSI YAML shown in the user guide.
185
+ */
186
+ function owlToIr(owl, modelName) {
187
+ const warnings = [];
188
+ const classNames = new Set(owl.classes.map(c => c.localName));
189
+ // Entities, one per class, in class-declaration order. Fields are attached in
190
+ // datatype-property-declaration order below. Two classes can share a local
191
+ // name (e.g. same name in different namespaces); OSI dataset names must be
192
+ // unique, so the first wins and the rest are warned and skipped rather than
193
+ // silently emitting a duplicate the loader would reject.
194
+ const entitiesByName = new Map();
195
+ const entities = [];
196
+ for (const c of owl.classes) {
197
+ if (entitiesByName.has(c.localName)) {
198
+ warnings.push(`class '${c.localName}' is declared more than once (same local name, ` +
199
+ `possibly across namespaces); keeping the first and skipping the rest.`);
200
+ continue;
201
+ }
202
+ const entity = {
203
+ name: c.localName,
204
+ // No source: a class is a logical entity. A binding profile supplies the
205
+ // backing table before a graph deploy; KC push needs none.
206
+ dataSource: '',
207
+ keys: dedupe(c.keys), // owl:hasKey -> primary_key (grain)
208
+ description: c.comment,
209
+ aiContext: synonymAiContext(c.label, c.localName, c.synonyms, c.examples),
210
+ fields: [],
211
+ };
212
+ // rdfs:subClassOf -> entity-level `extends`. Kept AS DECLARED (parent
213
+ // local names, deduped); parents are not flattened here -- a later
214
+ // resolution pass expands inherited fields (see ir.ts Entity.extends). Only
215
+ // parents defined as an owl:Class in this ontology are kept: an unknown
216
+ // parent (a typo, or a superclass imported from another ontology) cannot be
217
+ // resolved, and the loader hard-fails on an unknown supertype, so it is
218
+ // DROPPED here with a warning rather than emitted into a model that could
219
+ // not be pushed.
220
+ if (c.subClassOf.length) {
221
+ const parents = dedupe(c.subClassOf);
222
+ const known = parents.filter(name => classNames.has(name));
223
+ const unknown = parents.filter(name => !classNames.has(name));
224
+ if (known.length)
225
+ entity.extends = known;
226
+ if (unknown.length) {
227
+ warnings.push(`class '${c.localName}' declares rdfs:subClassOf a non-class ` +
228
+ `superclass (${unknown.join(', ')}); dropped, as it is not an owl:Class in ` +
229
+ `this ontology and cannot be resolved.`);
230
+ }
231
+ }
232
+ entitiesByName.set(c.localName, entity);
233
+ entities.push(entity);
234
+ }
235
+ // Datatype properties -> fields on each domain's entity. A property with more
236
+ // than one domain appears on each; one with none has nowhere to live.
237
+ let datatypePropertiesConverted = 0;
238
+ for (const p of owl.datatypeProperties) {
239
+ if (!p.domains.length) {
240
+ warnings.push(`datatype property '${p.localName}' has no rdfs:domain; skipped ` +
241
+ `(a field must belong to a class).`);
242
+ continue;
243
+ }
244
+ // A property counts as converted once if it produces at least one field,
245
+ // regardless of how many domains it lands on.
246
+ let produced = false;
247
+ for (const domain of p.domains) {
248
+ const entity = entitiesByName.get(domain);
249
+ if (!entity) {
250
+ warnings.push(`datatype property '${p.localName}' has domain '${domain}', ` +
251
+ `which is not an owl:Class in this ontology; skipped.`);
252
+ continue;
253
+ }
254
+ if (entity.fields.some(f => f.name === p.localName)) {
255
+ warnings.push(`datatype property '${p.localName}' on '${domain}' duplicates an ` +
256
+ `existing field name; skipped (field names must be unique).`);
257
+ continue;
258
+ }
259
+ const type = datatypeFor(p.rangeIri);
260
+ const field = {
261
+ name: p.localName,
262
+ // No expression: the field is logical. A binding profile maps it to a
263
+ // column when a real source is bound.
264
+ type,
265
+ // A temporal field is a time dimension by OSI's own rule; mark it so
266
+ // downstream (BigQuery Graph, BI) treats it as one.
267
+ dimension: TEMPORAL_TYPES.has(type) ? { isTime: true } : undefined,
268
+ label: fieldLabel(p.label, p.localName),
269
+ description: p.comment,
270
+ aiContext: fieldAiContext(p.synonyms, p.localName, p.examples),
271
+ };
272
+ entity.fields.push(field);
273
+ produced = true;
274
+ // An inverse-functional property uniquely identifies its subject -> a
275
+ // unique_keys constraint, unless it is already the primary key.
276
+ if (p.inverseFunctional && !arraysEqual(entity.keys, [p.localName])) {
277
+ (entity.uniqueKeys ??= []).push([p.localName]);
278
+ }
279
+ }
280
+ if (produced)
281
+ datatypePropertiesConverted++;
282
+ }
283
+ // Reconcile each entity's keys with the fields that actually exist. Both
284
+ // corrections are fail-soft (warn, never throw), matching the converter's
285
+ // per-element policy:
286
+ // 1. owl:hasKey may name a property that is not a datatype property on the
287
+ // class (undeclared, or declared only on a different domain). That
288
+ // column has no field, so it would name a phantom column that only
289
+ // errors later at graph generation. Drop the ENTIRE primary_key, not
290
+ // just the phantom member -- keeping the survivors would silently narrow
291
+ // a composite key to a possibly non-unique one, changing the grain.
292
+ // 2. A class with no usable owl:hasKey but exactly one single-column
293
+ // inverse-functional property still has a natural identifier; promote
294
+ // that unique key to the primary_key so the entity has a grain.
295
+ // (Ambiguous cases -- several unique keys, or a composite one -- are
296
+ // left alone.)
297
+ for (const entity of entities) {
298
+ const fieldNames = new Set(entity.fields.map(f => f.name));
299
+ const missing = entity.keys.filter(k => !fieldNames.has(k));
300
+ if (missing.length) {
301
+ warnings.push(`class '${entity.name}' owl:hasKey names ${missing.map(m => `'${m}'`).join(', ')} which ${missing.length > 1 ? 'are' : 'is'} not a datatype property on ` +
302
+ `the class; dropping the entire primary_key (keeping only the ` +
303
+ `remaining columns would change the entity's grain).`);
304
+ entity.keys = [];
305
+ }
306
+ if (!entity.keys.length && entity.uniqueKeys?.length === 1 &&
307
+ entity.uniqueKeys[0].length === 1) {
308
+ entity.keys = [...entity.uniqueKeys[0]];
309
+ entity.uniqueKeys = undefined;
310
+ warnings.push(`class '${entity.name}' has no usable owl:hasKey; using the ` +
311
+ `inverse-functional property '${entity.keys[0]}' as its primary_key.`);
312
+ }
313
+ }
314
+ // Object properties -> relationships (edges). Both endpoints must be known
315
+ // classes. The edge is logical: it carries only its direction (source entity
316
+ // -> destination entity), no join columns. The foreign-key / key columns are
317
+ // added to the model (logical grain, not a binding) before a graph deploy.
318
+ const relationships = [];
319
+ for (const p of owl.objectProperties) {
320
+ const domain = p.domains[0];
321
+ const range = p.ranges[0];
322
+ if (!domain || !range) {
323
+ warnings.push(`object property '${p.localName}' is missing an rdfs:domain or ` +
324
+ `rdfs:range; skipped (a relationship needs both endpoints).`);
325
+ continue;
326
+ }
327
+ // A relationship maps ONE source to ONE destination. Multiple domains or
328
+ // ranges mean an intersection in OWL, which has no clean single-edge shape,
329
+ // so keep the first of each and say what was dropped rather than losing it
330
+ // silently.
331
+ const ignored = [
332
+ ...p.domains.slice(1).map(d => `domain '${d}'`),
333
+ ...p.ranges.slice(1).map(r => `range '${r}'`),
334
+ ];
335
+ if (ignored.length) {
336
+ warnings.push(`object property '${p.localName}' declares more than one endpoint ` +
337
+ `(${ignored.join(', ')}); a relationship maps one source to one ` +
338
+ `destination, so only domain '${domain}' -> range '${range}' is kept.`);
339
+ }
340
+ if (!classNames.has(domain) || !classNames.has(range)) {
341
+ warnings.push(`object property '${p.localName}' references a non-class endpoint ` +
342
+ `(domain '${domain}', range '${range}'); skipped.`);
343
+ continue;
344
+ }
345
+ if (relationships.some(r => r.name === p.localName)) {
346
+ warnings.push(`object property '${p.localName}' duplicates an existing relationship ` +
347
+ `name; skipped (relationship names must be unique).`);
348
+ continue;
349
+ }
350
+ const relationship = {
351
+ name: p.localName,
352
+ // A logical edge: direction only, no join columns. The source
353
+ // foreign-key and destination key columns are added to the model (logical
354
+ // grain, not a binding) before a graph deploy.
355
+ source: { entity: domain, columns: [] },
356
+ destination: { entity: range, columns: [] },
357
+ // No `description`: the OSI relationship has no such slot, so the comment
358
+ // rides in ai_context.instructions (see relationshipAiContext).
359
+ aiContext: relationshipAiContext(p.label, p.localName, p.synonyms, p.comment, p.examples),
360
+ };
361
+ relationships.push(relationship);
362
+ }
363
+ const model = {
364
+ name: modelName,
365
+ description: modelDescription(owl),
366
+ aiContext: ontologyAiContext(owl.ontology, modelName),
367
+ entities,
368
+ relationships,
369
+ metrics: [],
370
+ };
371
+ return {
372
+ model,
373
+ warnings,
374
+ stats: {
375
+ classes: entities.length,
376
+ datatypeProperties: datatypePropertiesConverted,
377
+ objectProperties: relationships.length,
378
+ },
379
+ };
380
+ }
381
+ // The model description: the ontology header's own description when it has one,
382
+ // otherwise a provenance line naming the source base IRI. An owl:versionInfo is
383
+ // appended as provenance in either case.
384
+ function modelDescription(owl) {
385
+ const base = owl.ontology?.comment ??
386
+ (owl.baseIri ? `Imported from OWL ontology ${owl.baseIri}` :
387
+ 'Imported from OWL ontology');
388
+ const version = owl.ontology?.version;
389
+ return version ? `${base} (ontology version ${version})` : base;
390
+ }
@@ -0,0 +1,20 @@
1
+ import { BigQueryClient } from '../gcp/bigquery';
2
+ import * as context from '../gcp/context';
3
+ import { BigQueryGraphTarget, bigQueryGraphTargets, googleDeploymentTargets } from './deployment_target';
4
+ import { LoadedModel } from './loader';
5
+ export { bigQueryGraphTargets, googleDeploymentTargets };
6
+ export type { BigQueryGraphTarget };
7
+ export interface DeployOptions {
8
+ validateOnly?: boolean;
9
+ maxQueryPolls?: number;
10
+ pollBackoffMs?: number;
11
+ }
12
+ export interface DeployResult {
13
+ success: boolean;
14
+ details?: string;
15
+ ddl: string[];
16
+ warnings: string[];
17
+ deployed: number;
18
+ }
19
+ export declare function datasetLocation(bigQuery: BigQueryClient, target: BigQueryGraphTarget, warnings: string[]): Promise<string | undefined>;
20
+ export declare function deployBigQuery(models: LoadedModel[], ctx: context.ApiContext, options?: DeployOptions): Promise<DeployResult>;
@@ -0,0 +1,226 @@
1
+ "use strict";
2
+ // Deploys a semantic model's BigQuery Graph.
3
+ //
4
+ // This is the BigQuery leg of `kcmd push` for the semantic-model scope: it
5
+ // consumes models already parsed into the semantic IR (see loadSemanticModels,
6
+ // shared with the Knowledge Catalog leg so a multi-destination push parses each
7
+ // document once), lowers each to `CREATE OR REPLACE PROPERTY GRAPH` DDL
8
+ // (generator), and executes that DDL against the BigQuery project named by the
9
+ // model's GOOGLE deployment target.
10
+ //
11
+ // This is a library module: it emits no console output. The generated DDL and
12
+ // any loader/generator warnings are returned in `DeployResult` for the CLI
13
+ // (src/tool/commands.ts) to print.
14
+ //
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.googleDeploymentTargets = exports.bigQueryGraphTargets = void 0;
17
+ exports.datasetLocation = datasetLocation;
18
+ exports.deployBigQuery = deployBigQuery;
19
+ const bigquery_1 = require("../gcp/bigquery");
20
+ const bigquery_2 = require("./bigquery");
21
+ // The GOOGLE deployment-target reader lives in its own module so both deploy
22
+ // legs (BigQuery, Spanner) and the push-time validator share one parse. The
23
+ // BigQuery-facing views are re-exported here so existing importers
24
+ // (validate.ts, knowledge_catalog.ts, and the deploy tests) keep their
25
+ // historical import path.
26
+ const deployment_target_1 = require("./deployment_target");
27
+ Object.defineProperty(exports, "bigQueryGraphTargets", { enumerable: true, get: function () { return deployment_target_1.bigQueryGraphTargets; } });
28
+ Object.defineProperty(exports, "googleDeploymentTargets", { enumerable: true, get: function () { return deployment_target_1.googleDeploymentTargets; } });
29
+ // Upper bound on getQueryResults polls before we give up on a job that never
30
+ // reports completion (each poll long-polls the server for up to 10s).
31
+ const MAX_QUERY_POLLS = 30;
32
+ // Pause between polls. getQueryResults long-polls server-side, so this is a
33
+ // backstop for the case where it returns promptly (which would otherwise fire
34
+ // MAX_QUERY_POLLS requests back-to-back with no pause).
35
+ const POLL_BACKOFF_MS = 1000;
36
+ function sleep(ms) {
37
+ return new Promise(resolve => setTimeout(resolve, ms));
38
+ }
39
+ // Waits for a jobs.query response to reach completion and reports any terminal
40
+ // error. jobs.query can return before a slow DDL statement finishes; in that
41
+ // case the response carries `jobComplete: false` and a job reference, and we
42
+ // poll getQueryResults until the job is done. Errors are only judged once the
43
+ // job has actually completed.
44
+ async function awaitQueryDone(bigQuery, project, started, maxPolls, backoffMs) {
45
+ if (started.status !== 200) {
46
+ return { ok: false, error: started.message || String(started.status) };
47
+ }
48
+ let result = started.result;
49
+ // A 200 with no body is not a verified success: without jobComplete/errors we
50
+ // cannot confirm the DDL ran, so treat the missing body as a failure rather
51
+ // than falling through to the `{ok: true}` return below.
52
+ if (!result) {
53
+ return {
54
+ ok: false,
55
+ error: started.message || 'query returned status 200 with no response body',
56
+ };
57
+ }
58
+ const jobId = result?.jobReference?.jobId;
59
+ const location = result?.jobReference?.location;
60
+ let polls = 0;
61
+ while (result?.jobComplete === false && jobId && polls < maxPolls) {
62
+ if (polls > 0) {
63
+ await sleep(backoffMs);
64
+ }
65
+ polls++;
66
+ const res = await bigQuery.getQueryResults(project, jobId, location);
67
+ if (res.status !== 200) {
68
+ return { ok: false, error: res.message || String(res.status) };
69
+ }
70
+ result = res.result;
71
+ }
72
+ if (result?.jobComplete === false) {
73
+ // Distinguish "polled to exhaustion" from "never had a job to poll": with
74
+ // no jobId the loop above ran zero times, so a "did not complete after N
75
+ // polls" message would be misleading.
76
+ return {
77
+ ok: false,
78
+ error: jobId ?
79
+ `job ${jobId} did not complete after ${maxPolls} polls` :
80
+ 'query did not complete and returned no job reference to poll',
81
+ };
82
+ }
83
+ // errors[] mixes fatal errors and non-fatal warnings. When it is populated,
84
+ // consult the job's status.errorResult -- the definitive fatal signal -- so a
85
+ // deploy that only produced warnings is not reported as a failure.
86
+ const errors = result?.errors;
87
+ if (errors && errors.length) {
88
+ if (jobId) {
89
+ const job = await bigQuery.getJob(project, jobId, location);
90
+ if (job.status !== 200) {
91
+ return { ok: false, error: job.message || String(job.status) };
92
+ }
93
+ const fatal = job.result?.status?.errorResult;
94
+ return fatal ? { ok: false, error: fatal.message } : { ok: true };
95
+ }
96
+ // No job reference to disambiguate; treat the errors as fatal.
97
+ return { ok: false, error: errors.map(e => e.message).join('; ') };
98
+ }
99
+ return { ok: true };
100
+ }
101
+ // Looks up a dataset's BigQuery location so the query job, its result polls,
102
+ // and the job lookup all agree on a region. jobs.query would otherwise infer
103
+ // the location from the referenced tables, which can pick the wrong region for
104
+ // a non-US dataset.
105
+ //
106
+ // A 404 is a precise, free pre-flight: a typo'd dataset in the deployment
107
+ // target would otherwise surface later as a murkier DDL error, so we fail fast
108
+ // on it. Other failures (e.g. a 403 when the caller lacks
109
+ // bigquery.datasets.get) are non-fatal: fall back to letting BigQuery infer the
110
+ // location, but record a warning so the degraded path is visible -- otherwise a
111
+ // silent fallback can route a non-US CREATE to the wrong region and fail later
112
+ // with an opaque location error.
113
+ async function datasetLocation(bigQuery, target, warnings) {
114
+ const res = await bigQuery.getDataset(target.project, target.dataset);
115
+ if (res.status === 200) {
116
+ return res.result?.location;
117
+ }
118
+ if (res.status === 404) {
119
+ throw new Error(`dataset ${target.project}.${target.dataset} not found`);
120
+ }
121
+ warnings.push(`could not read location of dataset ${target.project}.${target.dataset} (${res.message || res.status}); ` +
122
+ `letting BigQuery infer the query location`);
123
+ return undefined;
124
+ }
125
+ // Deploys the BigQuery Graph for each pre-loaded model. Emits no console
126
+ // output; the generated DDL and any warnings are returned for the caller to
127
+ // print. The models are parsed once by the caller (loadSemanticModels) and
128
+ // shared with the Knowledge Catalog leg; each carries its originating document
129
+ // name for diagnostics.
130
+ async function deployBigQuery(models, ctx, options = {}) {
131
+ const bigQuery = new bigquery_1.BigQueryClient(ctx);
132
+ const ddl = [];
133
+ const warnings = [];
134
+ let deployed = 0;
135
+ let modelsSeen = 0;
136
+ const fail = (details) => ({ success: false, details, ddl, warnings, deployed });
137
+ const maxPolls = options.maxQueryPolls ?? MAX_QUERY_POLLS;
138
+ const backoffMs = options.pollBackoffMs ?? POLL_BACKOFF_MS;
139
+ // A model can declare several graphs in one dataset; cache the location so we
140
+ // issue one datasets.get per dataset rather than one per target.
141
+ const locationCache = new Map();
142
+ const locationFor = async (target) => {
143
+ const key = `${target.project}/${target.dataset}`;
144
+ if (locationCache.has(key)) {
145
+ return locationCache.get(key);
146
+ }
147
+ const loc = await datasetLocation(bigQuery, target, warnings);
148
+ locationCache.set(key, loc);
149
+ return loc;
150
+ };
151
+ for (const { document, model } of models) {
152
+ modelsSeen++;
153
+ let targets;
154
+ let malformed;
155
+ try {
156
+ ({ targets, malformed } = (0, deployment_target_1.bigQueryGraphTargets)(model));
157
+ }
158
+ catch (err) {
159
+ return fail(`Model '${model.name}' (${document}): ${err.message || err}`);
160
+ }
161
+ if (!targets.length) {
162
+ // A malformed-but-present target must not be reported as "none
163
+ // declared" -- that sends the author hunting for an extension they
164
+ // already wrote. Name the URIs that failed to parse.
165
+ if (malformed.length) {
166
+ return fail(`Model '${model.name}' (${document}) declares BigQuery Graph ` +
167
+ `deploymentTarget(s) that could not be parsed: ` +
168
+ `${malformed.join(', ')}. Expected ${deployment_target_1.BQ_GRAPH_FORM}.`);
169
+ }
170
+ return fail(`Model '${model.name}' (${document}) declares no BigQuery Graph ` +
171
+ `deploymentTarget in a GOOGLE custom_extension; nothing to deploy.`);
172
+ }
173
+ // Malformed targets alongside valid ones: surface them so a typo'd URI is
174
+ // not silently dropped when the deploy otherwise succeeds.
175
+ for (const bad of malformed) {
176
+ warnings.push(`[${model.name}] ignoring unparseable BigQuery Graph target: ${bad}`);
177
+ }
178
+ for (const target of targets) {
179
+ const gen = (0, bigquery_2.generatePropertyGraph)(model, {
180
+ project: target.project,
181
+ dataset: target.dataset,
182
+ graphName: target.graphName,
183
+ });
184
+ for (const w of gen.warnings) {
185
+ warnings.push(`[${model.name} -> ${target.graphName}] ${w}`);
186
+ }
187
+ ddl.push(`-- ${target.uri}\n${gen.ddl}`);
188
+ if (options.validateOnly) {
189
+ continue;
190
+ }
191
+ let location;
192
+ try {
193
+ location = await locationFor(target);
194
+ }
195
+ catch (err) {
196
+ return fail(`Failed to deploy '${target.graphName}': ${err.message || err}`);
197
+ }
198
+ const started = await bigQuery.query(target.project, gen.ddl, location);
199
+ const outcome = await awaitQueryDone(bigQuery, target.project, started, maxPolls, backoffMs);
200
+ if (!outcome.ok) {
201
+ // These are CREATE OR REPLACE statements executed one at a time, so
202
+ // any graphs already deployed in this run have mutated production and
203
+ // are not rolled back. Surface that alongside the failing target.
204
+ const partial = deployed ?
205
+ ` (${deployed} graph(s) already deployed in this run; ` +
206
+ `CREATE OR REPLACE changes are not rolled back)` :
207
+ '';
208
+ return fail(`Failed to deploy '${target.graphName}': ${outcome.error}${partial}`);
209
+ }
210
+ deployed++;
211
+ }
212
+ }
213
+ // A parsed document always yields at least one model (the loader enforces
214
+ // `semantic_model` min 1), so modelsSeen is 0 only when no documents were
215
+ // found at all. validateOnly mutates nothing, so an empty workspace is a
216
+ // clean no-op there (exit 0); a real push treats "nothing to deploy" as a
217
+ // configuration error worth flagging.
218
+ if (!modelsSeen) {
219
+ if (options.validateOnly) {
220
+ warnings.push('No semantic model documents found; nothing to validate.');
221
+ return { success: true, ddl, warnings, deployed };
222
+ }
223
+ return fail('No semantic model documents found; nothing to deploy.');
224
+ }
225
+ return { success: true, ddl, warnings, deployed };
226
+ }