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