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