@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.
Files changed (150) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +264 -2
  3. package/build/ts/kcmd/cli.js +20252 -0
  4. package/build/ts/kcmd/gcp/alloydb.d.ts +12 -0
  5. package/build/ts/kcmd/gcp/alloydb.js +70 -0
  6. package/build/ts/kcmd/gcp/api.d.ts +29 -0
  7. package/build/ts/kcmd/gcp/api.js +162 -0
  8. package/build/ts/kcmd/gcp/biglake.d.ts +18 -0
  9. package/build/ts/kcmd/gcp/biglake.js +59 -0
  10. package/build/ts/kcmd/gcp/bigquery.d.ts +57 -0
  11. package/build/ts/kcmd/gcp/bigquery.js +134 -0
  12. package/build/ts/kcmd/gcp/context.d.ts +10 -0
  13. package/build/ts/kcmd/gcp/context.js +74 -0
  14. package/build/ts/kcmd/gcp/crm.d.ts +13 -0
  15. package/build/ts/kcmd/gcp/crm.js +91 -0
  16. package/build/ts/kcmd/gcp/dataplex.d.ts +147 -0
  17. package/build/ts/kcmd/gcp/dataplex.js +406 -0
  18. package/build/ts/kcmd/gcp/index.d.ts +3 -0
  19. package/build/ts/kcmd/gcp/index.js +21 -0
  20. package/build/ts/kcmd/gcp/spanner.d.ts +73 -0
  21. package/build/ts/kcmd/gcp/spanner.js +126 -0
  22. package/build/ts/kcmd/index.d.ts +7 -0
  23. package/build/ts/kcmd/index.js +46 -0
  24. package/build/ts/kcmd/layout.d.ts +16 -0
  25. package/build/ts/kcmd/layout.js +31 -0
  26. package/build/ts/kcmd/layouts/documents.d.ts +24 -0
  27. package/build/ts/kcmd/layouts/documents.js +264 -0
  28. package/build/ts/kcmd/layouts/okf.d.ts +31 -0
  29. package/build/ts/kcmd/layouts/okf.js +386 -0
  30. package/build/ts/kcmd/layouts/semantic-model.d.ts +74 -0
  31. package/build/ts/kcmd/layouts/semantic-model.js +621 -0
  32. package/build/ts/kcmd/layouts/standard.d.ts +13 -0
  33. package/build/ts/kcmd/layouts/standard.js +108 -0
  34. package/build/ts/kcmd/manifest.d.ts +34 -0
  35. package/build/ts/kcmd/manifest.js +262 -0
  36. package/build/ts/kcmd/metadata.d.ts +25 -0
  37. package/build/ts/kcmd/metadata.js +8 -0
  38. package/build/ts/kcmd/okf/catalog_residue.d.ts +14 -0
  39. package/build/ts/kcmd/okf/catalog_residue.js +124 -0
  40. package/build/ts/kcmd/okf/concept.d.ts +14 -0
  41. package/build/ts/kcmd/okf/concept.js +110 -0
  42. package/build/ts/kcmd/okf/concept_to_entry.d.ts +10 -0
  43. package/build/ts/kcmd/okf/concept_to_entry.js +281 -0
  44. package/build/ts/kcmd/okf/init_okf_workspace.d.ts +15 -0
  45. package/build/ts/kcmd/okf/init_okf_workspace.js +113 -0
  46. package/build/ts/kcmd/okf/links.d.ts +23 -0
  47. package/build/ts/kcmd/okf/links.js +468 -0
  48. package/build/ts/kcmd/okf/okf_metadata.d.ts +12 -0
  49. package/build/ts/kcmd/okf/okf_metadata.js +99 -0
  50. package/build/ts/kcmd/semantic/bigquery.d.ts +22 -0
  51. package/build/ts/kcmd/semantic/bigquery.js +469 -0
  52. package/build/ts/kcmd/semantic/binding.d.ts +3 -0
  53. package/build/ts/kcmd/semantic/binding.js +92 -0
  54. package/build/ts/kcmd/semantic/catalog_entry.d.ts +24 -0
  55. package/build/ts/kcmd/semantic/catalog_entry.js +35 -0
  56. package/build/ts/kcmd/semantic/converters/owl/convert.d.ts +26 -0
  57. package/build/ts/kcmd/semantic/converters/owl/convert.js +41 -0
  58. package/build/ts/kcmd/semantic/converters/owl/model.d.ts +98 -0
  59. package/build/ts/kcmd/semantic/converters/owl/model.js +30 -0
  60. package/build/ts/kcmd/semantic/converters/owl/parse.d.ts +11 -0
  61. package/build/ts/kcmd/semantic/converters/owl/parse.js +634 -0
  62. package/build/ts/kcmd/semantic/converters/owl/to_ir.d.ts +19 -0
  63. package/build/ts/kcmd/semantic/converters/owl/to_ir.js +390 -0
  64. package/build/ts/kcmd/semantic/deploy_bigquery.d.ts +20 -0
  65. package/build/ts/kcmd/semantic/deploy_bigquery.js +226 -0
  66. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +104 -0
  67. package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +1367 -0
  68. package/build/ts/kcmd/semantic/deploy_spanner.d.ts +15 -0
  69. package/build/ts/kcmd/semantic/deploy_spanner.js +150 -0
  70. package/build/ts/kcmd/semantic/deployment_target.d.ts +93 -0
  71. package/build/ts/kcmd/semantic/deployment_target.js +379 -0
  72. package/build/ts/kcmd/semantic/graph_ddl.d.ts +44 -0
  73. package/build/ts/kcmd/semantic/graph_ddl.js +656 -0
  74. package/build/ts/kcmd/semantic/import_model.d.ts +72 -0
  75. package/build/ts/kcmd/semantic/import_model.js +1698 -0
  76. package/build/ts/kcmd/semantic/ir.d.ts +589 -0
  77. package/build/ts/kcmd/semantic/ir.js +247 -0
  78. package/build/ts/kcmd/semantic/kc_actions.d.ts +45 -0
  79. package/build/ts/kcmd/semantic/kc_actions.js +531 -0
  80. package/build/ts/kcmd/semantic/kc_constraints.d.ts +44 -0
  81. package/build/ts/kcmd/semantic/kc_constraints.js +279 -0
  82. package/build/ts/kcmd/semantic/kc_converter.d.ts +37 -0
  83. package/build/ts/kcmd/semantic/kc_converter.js +839 -0
  84. package/build/ts/kcmd/semantic/kc_custom_types.d.ts +63 -0
  85. package/build/ts/kcmd/semantic/kc_custom_types.js +751 -0
  86. package/build/ts/kcmd/semantic/kc_entries.d.ts +155 -0
  87. package/build/ts/kcmd/semantic/kc_entries.js +284 -0
  88. package/build/ts/kcmd/semantic/kc_ids.d.ts +102 -0
  89. package/build/ts/kcmd/semantic/kc_ids.js +188 -0
  90. package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +29 -0
  91. package/build/ts/kcmd/semantic/knowledge_catalog.js +960 -0
  92. package/build/ts/kcmd/semantic/loader.d.ts +258 -0
  93. package/build/ts/kcmd/semantic/loader.js +3576 -0
  94. package/build/ts/kcmd/semantic/names.d.ts +10 -0
  95. package/build/ts/kcmd/semantic/names.js +85 -0
  96. package/build/ts/kcmd/semantic/osi_converter.d.ts +60 -0
  97. package/build/ts/kcmd/semantic/osi_converter.js +910 -0
  98. package/build/ts/kcmd/semantic/parameters.d.ts +19 -0
  99. package/build/ts/kcmd/semantic/parameters.js +171 -0
  100. package/build/ts/kcmd/semantic/pull_kc.d.ts +10 -0
  101. package/build/ts/kcmd/semantic/pull_kc.js +272 -0
  102. package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +59 -0
  103. package/build/ts/kcmd/semantic/resolve_inheritance.js +346 -0
  104. package/build/ts/kcmd/semantic/resolve_profiles.d.ts +291 -0
  105. package/build/ts/kcmd/semantic/resolve_profiles.js +2003 -0
  106. package/build/ts/kcmd/semantic/runtime/agent_tools.d.ts +93 -0
  107. package/build/ts/kcmd/semantic/runtime/agent_tools.js +243 -0
  108. package/build/ts/kcmd/semantic/runtime/dialect.d.ts +13 -0
  109. package/build/ts/kcmd/semantic/runtime/dialect.js +39 -0
  110. package/build/ts/kcmd/semantic/runtime/runtime.d.ts +88 -0
  111. package/build/ts/kcmd/semantic/runtime/runtime.js +301 -0
  112. package/build/ts/kcmd/semantic/runtime/store.d.ts +112 -0
  113. package/build/ts/kcmd/semantic/runtime/store.js +344 -0
  114. package/build/ts/kcmd/semantic/skills.d.ts +110 -0
  115. package/build/ts/kcmd/semantic/skills.js +697 -0
  116. package/build/ts/kcmd/semantic/source_dialect.d.ts +17 -0
  117. package/build/ts/kcmd/semantic/source_dialect.js +311 -0
  118. package/build/ts/kcmd/semantic/source_entries.d.ts +12 -0
  119. package/build/ts/kcmd/semantic/source_entries.js +341 -0
  120. package/build/ts/kcmd/semantic/spanner.d.ts +18 -0
  121. package/build/ts/kcmd/semantic/spanner.js +60 -0
  122. package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +23 -0
  123. package/build/ts/kcmd/semantic/sql_expr_utils.js +394 -0
  124. package/build/ts/kcmd/semantic/sql_identifiers.d.ts +6 -0
  125. package/build/ts/kcmd/semantic/sql_identifiers.js +267 -0
  126. package/build/ts/kcmd/semantic/sql_parser.d.ts +47 -0
  127. package/build/ts/kcmd/semantic/sql_parser.js +400 -0
  128. package/build/ts/kcmd/semantic/transpile.d.ts +76 -0
  129. package/build/ts/kcmd/semantic/transpile.js +581 -0
  130. package/build/ts/kcmd/semantic/validate.d.ts +70 -0
  131. package/build/ts/kcmd/semantic/validate.js +2266 -0
  132. package/build/ts/kcmd/semantic/yaml_options.d.ts +6 -0
  133. package/build/ts/kcmd/semantic/yaml_options.js +58 -0
  134. package/build/ts/kcmd/semantic/yaml_update.d.ts +26 -0
  135. package/build/ts/kcmd/semantic/yaml_update.js +782 -0
  136. package/build/ts/kcmd/snapshot.d.ts +37 -0
  137. package/build/ts/kcmd/snapshot.js +386 -0
  138. package/build/ts/kcmd/source.d.ts +31 -0
  139. package/build/ts/kcmd/source.js +119 -0
  140. package/build/ts/kcmd/sources/bq-dataset.d.ts +17 -0
  141. package/build/ts/kcmd/sources/bq-dataset.js +131 -0
  142. package/build/ts/kcmd/sources/entrygroup.d.ts +21 -0
  143. package/build/ts/kcmd/sources/entrygroup.js +83 -0
  144. package/build/ts/kcmd/sources/kb.d.ts +21 -0
  145. package/build/ts/kcmd/sources/kb.js +82 -0
  146. package/build/ts/kcmd/sources/semantic-model.d.ts +18 -0
  147. package/build/ts/kcmd/sources/semantic-model.js +48 -0
  148. package/build/ts/kcmd/sync.d.ts +35 -0
  149. package/build/ts/kcmd/sync.js +734 -0
  150. package/package.json +62 -4
@@ -0,0 +1,2003 @@
1
+ "use strict";
2
+ // Applies a chosen binding profile to a logical semantic model, and prunes what
3
+ // the resulting binding cannot answer.
4
+ //
5
+ // A model is authored as ONE logical declaration (entities, fields,
6
+ // relationships, metrics, the grain, and the graph shape) plus zero or more
7
+ // BINDING PROFILES that supply the physical facets it leaves open: each entity's
8
+ // `source`, each field's column (`expression`) and each action's `executor`.
9
+ // `kcmd push` applies the profile each deployment names to the logical model
10
+ // BY NAME and deploys the result. See docs/semantic-model/profiles.md.
11
+ //
12
+ // Two passes live here:
13
+ // - applyProfile applies a profile, read into a ProfileSpec, to a loaded
14
+ // model. Push, `kcmd profiles`, the runtime and the profile checks apply a
15
+ // profile file this way, and a profile held in memory is applied the same
16
+ // way. Push, `kcmd profiles` and the runtime start at profileForDocument,
17
+ // which finds the profile by name and loads the model file on its own
18
+ // (applyToDocument). A file is read with loadProfileFile.
19
+ // applyProfileSpec then checks what the profile may set
20
+ // (profileApplyError), sets the values (applyBindings) and gives the
21
+ // warnings (warningsAfterProfile). applyProfile and applyProfileFile take
22
+ // the same steps for a model already loaded, and the completeness check
23
+ // applies the bindings with applyBindings.
24
+ // - pruneUnavailable runs over the loaded IR and drops each field left unbound
25
+ // or excluded, every field that reads one of those, and every metric whose
26
+ // expression reads any of them, returning the pruned model and a
27
+ // per-profile availability report. Keys and join columns are physical
28
+ // columns rather than fields, so pruning fields never removes an entity or
29
+ // a relationship.
30
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
31
+ if (k2 === undefined) k2 = k;
32
+ var desc = Object.getOwnPropertyDescriptor(m, k);
33
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
34
+ desc = { enumerable: true, get: function() { return m[k]; } };
35
+ }
36
+ Object.defineProperty(o, k2, desc);
37
+ }) : (function(o, m, k, k2) {
38
+ if (k2 === undefined) k2 = k;
39
+ o[k2] = m[k];
40
+ }));
41
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
42
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
43
+ }) : function(o, v) {
44
+ o["default"] = v;
45
+ });
46
+ var __importStar = (this && this.__importStar) || (function () {
47
+ var ownKeys = function(o) {
48
+ ownKeys = Object.getOwnPropertyNames || function (o) {
49
+ var ar = [];
50
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
51
+ return ar;
52
+ };
53
+ return ownKeys(o);
54
+ };
55
+ return function (mod) {
56
+ if (mod && mod.__esModule) return mod;
57
+ var result = {};
58
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
59
+ __setModuleDefault(result, mod);
60
+ return result;
61
+ };
62
+ })();
63
+ Object.defineProperty(exports, "__esModule", { value: true });
64
+ exports.DEFAULT_PROFILE = void 0;
65
+ exports.plainCopy = plainCopy;
66
+ exports.pruneUnavailable = pruneUnavailable;
67
+ exports.withoutExclusions = withoutExclusions;
68
+ exports.resolvedEntities = resolvedEntities;
69
+ exports.inheritedNamesOf = inheritedNamesOf;
70
+ exports.loadProfileFile = loadProfileFile;
71
+ exports.applyProfile = applyProfile;
72
+ exports.applyProfileFile = applyProfileFile;
73
+ exports.declaredProfiles = declaredProfiles;
74
+ exports.profileForDocument = profileForDocument;
75
+ exports.appliedProfileWarnings = appliedProfileWarnings;
76
+ exports.profileErrorMessage = profileErrorMessage;
77
+ exports.undeclaredProfileError = undeclaredProfileError;
78
+ exports.unknownProfileMessage = unknownProfileMessage;
79
+ exports.reservedProfileNameError = reservedProfileNameError;
80
+ exports.readerMessage = readerMessage;
81
+ exports.profileSpecOf = profileSpecOf;
82
+ exports.expressionSources = expressionSources;
83
+ exports.noTextFor = noTextFor;
84
+ exports.tableDialects = tableDialects;
85
+ exports.fieldsOnTables = fieldsOnTables;
86
+ exports.importCommand = importCommand;
87
+ exports.importTranspiles = importTranspiles;
88
+ exports.importHint = importHint;
89
+ exports.sourcesDialect = sourcesDialect;
90
+ exports.validateProfileCompleteness = validateProfileCompleteness;
91
+ exports.validateProfileConsistency = validateProfileConsistency;
92
+ exports.namedProfileErrors = namedProfileErrors;
93
+ exports.inheritanceErrorOf = inheritanceErrorOf;
94
+ exports.columnBinding = columnBinding;
95
+ exports.statesKey = statesKey;
96
+ const yaml = __importStar(require("yaml"));
97
+ const ir_1 = require("./ir");
98
+ const loader_1 = require("./loader");
99
+ const names_1 = require("./names");
100
+ const resolve_inheritance_1 = require("./resolve_inheritance");
101
+ const sql_expr_utils_1 = require("./sql_expr_utils");
102
+ const transpile_1 = require("./transpile");
103
+ const yaml_options_1 = require("./yaml_options");
104
+ // The implicit profile: the inline bindings already in the model document (the
105
+ // combined single-file form).
106
+ exports.DEFAULT_PROFILE = 'default';
107
+ // Keys a profile may carry at each level. Everything else is a logical
108
+ // declaration the model owns; setting it in a profile is rejected so swapping a
109
+ // profile can move data but never change what the model means.
110
+ const PROFILE_FILE_KEYS = new Set([
111
+ 'name', 'entities', 'relationships', 'metrics_exclude', 'actions',
112
+ ]);
113
+ const PROFILE_ENTITY_KEYS = new Set([
114
+ 'name', 'source', 'primary_key', 'unique_keys', 'fields', 'fields_exclude',
115
+ ]);
116
+ const PROFILE_RELATIONSHIP_KEYS = new Set(['name', 'from_columns', 'to_columns']);
117
+ const PROFILE_FIELD_KEYS = new Set(['name', 'expression']);
118
+ const PROFILE_ACTION_KEYS = new Set(['name', 'executor']);
119
+ // A deep copy in which every node is its own object. structuredClone keeps a
120
+ // YAML anchor and its aliases as one object, so an edit through one alias
121
+ // would change every node that shares it.
122
+ function plainCopy(value) {
123
+ if (Array.isArray(value))
124
+ return value.map(plainCopy);
125
+ if (value && typeof value === 'object') {
126
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, plainCopy(v)]));
127
+ }
128
+ return value;
129
+ }
130
+ function notAllowed(where, key) {
131
+ return `${where} sets '${key}', which a profile may not set; a profile ` +
132
+ `carries only physical bindings, and the logical model owns everything ` +
133
+ `else`;
134
+ }
135
+ /**
136
+ * Returns a clone of `model` reduced to what `profileName` can answer: unbound
137
+ * and excluded fields taken off their entities, with every field that reads
138
+ * one of them, directly or through other fields; every excluded metric and
139
+ * every metric that reads an unavailable field dropped; and every action with
140
+ * no executor dropped. A field counts whether the entity declares or inherits
141
+ * it. What a field or metric reads comes from the text `dialect` evaluates
142
+ * (see textsFor). `dialect` defaults to the dialect the model's sources
143
+ * imply. When the sources are in systems of different dialects, or one names
144
+ * no system, there is no dialect and every text counts. When no lookup read
145
+ * the dialect, it is null and no text counts.
146
+ * The input is never mutated. The report names each dropped block
147
+ * and the unbound field that stops it, so a caller can state the withheld
148
+ * coverage. A model with nothing unbound resolves unchanged.
149
+ */
150
+ function pruneUnavailable(model, profileName, dialect = sourcesDialect(model)) {
151
+ const clone = structuredClone(model);
152
+ const report = {
153
+ profile: profileName,
154
+ unboundFields: [],
155
+ excludedFields: [],
156
+ droppedEntities: [],
157
+ droppedMetrics: [],
158
+ droppedRelationships: [],
159
+ droppedActions: [],
160
+ droppedFields: [],
161
+ warnings: [],
162
+ };
163
+ // A field is bound when isFieldBound says so; otherwise it is unbound
164
+ // (structurally absent under this profile). A field awaiting transpilation
165
+ // (its column carried on the imported expression) or bound only through a
166
+ // dialect list counts as bound, not dropped. The graph generators ask a
167
+ // narrower question, whether the field has SQL for their engine, and leave
168
+ // out a bound field that has none with a warning of their own.
169
+ //
170
+ // Availability is judged on each entity's resolved fields, declared plus
171
+ // inherited, since a metric can read an inherited field as readily as a
172
+ // declared one. A field the profile excludes on an entity is unavailable on
173
+ // that entity.
174
+ const unbound = new Set();
175
+ const resolved = resolvedEntities(clone);
176
+ for (const e of resolved) {
177
+ // An abstract entity has no table and no bindings by design: it survives
178
+ // only as a label on its subtypes (which bind its inherited fields on their
179
+ // own tables). Its fields are legitimately column-less, so they are not
180
+ // "unbound" in the pruning sense.
181
+ if (e.abstract)
182
+ continue;
183
+ for (const f of e.fields ?? []) {
184
+ if (!(0, ir_1.isFieldBound)(f))
185
+ unbound.add(`${e.name}.${f.name}`);
186
+ }
187
+ for (const name of e.excludedFields ?? []) {
188
+ unbound.add(`${e.name}.${name}`);
189
+ report.excludedFields.push(`${e.name}.${name}`);
190
+ }
191
+ }
192
+ report.unboundFields = [...unbound];
193
+ // A field that reads an unavailable field of its entity is unavailable too,
194
+ // and so is every field and metric that reads it in turn. Each field's
195
+ // dependencies are read once, and only when something is unbound.
196
+ const unavailable = new Set(unbound);
197
+ const unreadable = new Set();
198
+ const dependencies = new Map();
199
+ if (unbound.size) {
200
+ for (const e of resolved) {
201
+ if (e.abstract)
202
+ continue;
203
+ for (const f of e.fields ?? []) {
204
+ const key = `${e.name}.${f.name}`;
205
+ if (unbound.has(key))
206
+ continue;
207
+ dependencies.set(key, fieldDependencies(clone, e.name, f.name, unreadable, dialect)
208
+ .map(dep => `${e.name}.${dep}`));
209
+ }
210
+ }
211
+ }
212
+ let grew = true;
213
+ while (grew) {
214
+ grew = false;
215
+ for (const [key, deps] of dependencies) {
216
+ if (unavailable.has(key))
217
+ continue;
218
+ const reached = deps.find(target => unavailable.has(target));
219
+ if (reached) {
220
+ unavailable.add(key);
221
+ report.droppedFields.push({ name: key, reason: `reads ${reached}, which is unavailable` });
222
+ grew = true;
223
+ }
224
+ }
225
+ }
226
+ for (const what of unreadable) {
227
+ report.warnings.push(`${what}: kcmd could not read its SQL, so the fields it reads were ` +
228
+ `not checked`);
229
+ }
230
+ // Keys and join columns are physical column names, not fields, so whether a
231
+ // field is bound says nothing about them. Excluding a field that happens to
232
+ // share a key column's name drops the field and nothing else.
233
+ const allEntityNames = (clone.entities ?? []).map(e => e.name);
234
+ // Take unbound and excluded fields off the entities that declare them. (A
235
+ // bound field whose value is null still emits a column -- unbound is not
236
+ // null.) A declaration that a descendant inherits stays, so the descendant
237
+ // keeps the definition and whatever binding it has, and is marked excluded on
238
+ // the declaring entity instead, since a binding is a fact about one table.
239
+ const extendedNames = new Set(resolvedEntities(clone).flatMap(e => e.extends ?? []));
240
+ for (const e of clone.entities ?? []) {
241
+ // Keep an abstract entity's fields intact: they are column-less by design
242
+ // and name the shared label's property set for the emitter (see above).
243
+ if (e.abstract)
244
+ continue;
245
+ // An inherited field that is unavailable on this entity is marked excluded
246
+ // here, whether it is unbound, excluded, or reads such a field. Removing
247
+ // this entity's line alone, or having none to remove, would let inheritance
248
+ // hand back the ancestor's definition and binding.
249
+ const inheritedUnavailable = [...inheritedNamesOf(clone, e.name)].filter(name => unavailable.has(`${e.name}.${name}`));
250
+ const excluded = new Set(e.excludedFields ?? []);
251
+ const gone = (f) => !(0, ir_1.isFieldBound)(f) || excluded.has(f.name) ||
252
+ unavailable.has(`${e.name}.${f.name}`);
253
+ if (extendedNames.has(e.name)) {
254
+ const kept = (e.fields ?? []).filter(gone).map(f => f.name);
255
+ if (kept.length) {
256
+ e.excludedFields = [...new Set([...excluded, ...kept])];
257
+ }
258
+ }
259
+ else {
260
+ e.fields = (e.fields ?? []).filter(f => !gone(f));
261
+ }
262
+ if (inheritedUnavailable.length) {
263
+ e.excludedFields =
264
+ [...new Set([...(e.excludedFields ?? []), ...inheritedUnavailable])];
265
+ }
266
+ }
267
+ // A relationship's join columns are its own binding, so pruning fields never
268
+ // removes one.
269
+ const keptRels = clone.relationships ?? [];
270
+ // A metric is available only when the profile does not exclude it, every
271
+ // field it reads is available, and -- when it spans entities -- a
272
+ // relationship connects them.
273
+ const excludedMetrics = new Set(clone.excludedMetrics ?? []);
274
+ delete clone.excludedMetrics;
275
+ const keptMetrics = [];
276
+ for (const mt of clone.metrics ?? []) {
277
+ if (excludedMetrics.has(mt.name)) {
278
+ report.droppedMetrics.push({ name: mt.name, reason: 'excluded' });
279
+ continue;
280
+ }
281
+ const columns = [];
282
+ // A text with no SQL has nothing to read.
283
+ const sources = textsFor(mt, dialect)
284
+ .filter(s => (0, loader_1.noSqlReason)(s.text, s.dialect) === undefined);
285
+ let unread = 0;
286
+ for (const { text, dialect } of sources) {
287
+ const read = (0, sql_expr_utils_1.columnReferences)(text, dialect);
288
+ if (read) {
289
+ columns.push(...read);
290
+ }
291
+ else {
292
+ unread++;
293
+ }
294
+ }
295
+ const hit = firstUnboundReferenced(columns, unavailable);
296
+ if (hit) {
297
+ report.droppedMetrics.push({
298
+ name: mt.name,
299
+ reason: `field ${hit} is ${unbound.has(hit) ? 'unbound' : 'unavailable'}`,
300
+ field: hit,
301
+ });
302
+ continue;
303
+ }
304
+ const refs = (0, sql_expr_utils_1.entitiesReached)(columns, allEntityNames);
305
+ if (refs.length > 1 && !connectingRelationshipKept(refs, keptRels)) {
306
+ report.droppedMetrics.push({
307
+ name: mt.name,
308
+ reason: `no available relationship connects ${refs.join(', ')}`,
309
+ });
310
+ continue;
311
+ }
312
+ // A text the parser cannot read is skipped, so what it reads is unknown.
313
+ if (unread) {
314
+ report.warnings.push(unread === sources.length ?
315
+ `metric '${mt.name}': kcmd could not read its SQL, so the ` +
316
+ `fields it reads were not checked` :
317
+ `metric '${mt.name}': kcmd could not read some of its SQL, so ` +
318
+ `the fields that SQL reads were not checked`);
319
+ }
320
+ keptMetrics.push(mt);
321
+ }
322
+ clone.metrics = keptMetrics;
323
+ // An action is available only where a binding performs it. The executor is
324
+ // the action's binding, so an action without one is unavailable for the same
325
+ // reason a column-less field is: it is declared, and there is nothing here to
326
+ // carry it out.
327
+ const keptActions = [];
328
+ for (const a of clone.actions ?? []) {
329
+ if (a.executor === undefined) {
330
+ report.droppedActions.push({ name: a.name, reason: 'no executor is bound under this profile' });
331
+ continue;
332
+ }
333
+ keptActions.push(a);
334
+ }
335
+ clone.actions = keptActions;
336
+ return { model: clone, report };
337
+ }
338
+ // `model` without the exclusions a profile marked on it.
339
+ function withoutExclusions(model) {
340
+ const out = structuredClone(model);
341
+ for (const e of out.entities ?? [])
342
+ delete e.excludedFields;
343
+ delete out.excludedMetrics;
344
+ return out;
345
+ }
346
+ // Each entity resolved for inheritance, or as declared when the hierarchy is
347
+ // broken, which push reports elsewhere. `extends` is expanded to every
348
+ // ancestor.
349
+ function resolvedEntities(model) {
350
+ const entities = model.entities ?? [];
351
+ if (!entities.some(e => e.extends?.length))
352
+ return entities;
353
+ try {
354
+ return (0, resolve_inheritance_1.resolveInheritance)(model).model.entities ?? entities;
355
+ }
356
+ catch (err) {
357
+ if (!(err instanceof resolve_inheritance_1.InheritanceError))
358
+ throw err;
359
+ return entities;
360
+ }
361
+ }
362
+ // The names of the fields `entityName` inherits from every ancestor its
363
+ // `extends` reaches. A caller asking for many entities passes `byName`, the
364
+ // model's entities by name, built once.
365
+ function inheritedNamesOf(model, entityName, byName = new Map((model.entities ?? []).map(e => [e.name, e]))) {
366
+ return new Set(ancestorsOf(entityName, byName)
367
+ .flatMap(a => (a.fields ?? []).map(f => f.name)));
368
+ }
369
+ // Every ancestor `entityName`'s `extends` reaches, each once. `byName` is the
370
+ // model's entities by name.
371
+ function ancestorsOf(entityName, byName) {
372
+ const ancestors = [];
373
+ const seen = new Set([entityName]);
374
+ const queue = [...(byName.get(entityName)?.extends ?? [])];
375
+ while (queue.length) {
376
+ const name = queue.shift();
377
+ if (seen.has(name))
378
+ continue;
379
+ seen.add(name);
380
+ const ancestor = byName.get(name);
381
+ if (!ancestor)
382
+ continue;
383
+ ancestors.push(ancestor);
384
+ queue.push(...(ancestor.extends ?? []));
385
+ }
386
+ return ancestors;
387
+ }
388
+ // The first unbound "Entity.field" among the columns a metric expression
389
+ // reads, or null.
390
+ function firstUnboundReferenced(columns, unbound) {
391
+ for (const key of unbound) {
392
+ const dot = key.indexOf('.');
393
+ if ((0, sql_expr_utils_1.fieldsReadOn)(columns, key.slice(0, dot)).includes(key.slice(dot + 1))) {
394
+ return key;
395
+ }
396
+ }
397
+ return null;
398
+ }
399
+ // The dialect `text` is written in on `f`: the dialect of the list entry that
400
+ // holds it, the imported dialect for the imported text, and BigQuery
401
+ // otherwise, since the loader fills `expression` from a `BIGQUERY` or
402
+ // `ANSI_SQL` entry.
403
+ function dialectOf(f, text) {
404
+ const entry = f.dialects?.find(d => d.expression === text);
405
+ if (entry)
406
+ return entry.dialect;
407
+ if (text === f.importedExpression && f.importedDialect) {
408
+ return f.importedDialect;
409
+ }
410
+ return 'BIGQUERY';
411
+ }
412
+ // Whether any relationship directly connects two of the referenced entities --
413
+ // the minimal check that a cross-entity metric has a join path.
414
+ function connectingRelationshipKept(refEntities, kept) {
415
+ const set = new Set(refEntities);
416
+ return kept.some(r => set.has(r.source.entity) && set.has(r.destination.entity));
417
+ }
418
+ /**
419
+ * Reads a profile file into the IR's `ProfileSpec`. Throws on anything a
420
+ * profile may not say: an unknown key at any level, a value of the wrong shape,
421
+ * a field in both `fields` and `fields_exclude`, `"*"` inside a
422
+ * `metrics_exclude` list, or a `name` other than `profileName`. A field
423
+ * expression and an action's executor are read by the loader's own readers, so
424
+ * each follows the rules it follows in a model file. A source is kept as
425
+ * written, and applyProfile reads it with the loader's reader too. A field
426
+ * expression's short form means `ANSI_SQL` in either flavor. Actions are out of
427
+ * scope for the preview, so nothing more is checked. With `skipActions`, the
428
+ * profile's actions are not read.
429
+ */
430
+ function loadProfileFile(text, profileName, opts = {}) {
431
+ let doc;
432
+ try {
433
+ doc = yaml.parse(text, yaml_options_1.YAML_OPTIONS);
434
+ }
435
+ catch (err) {
436
+ throw new loader_1.SemanticModelError(`profile '${profileName}' does not parse: ${err?.message ?? err}`);
437
+ }
438
+ if (opts.skipActions && doc && typeof doc === 'object' &&
439
+ !Array.isArray(doc)) {
440
+ const { actions: _, ...rest } = doc;
441
+ doc = rest;
442
+ }
443
+ return profileSpecOf(doc, profileName);
444
+ }
445
+ /**
446
+ * Returns a copy of `model` with `profile` applied: each named entity's
447
+ * source, its keys where the profile states them, its field bindings, as a
448
+ * binding-only redeclaration for a field the entity inherits, and its
449
+ * `fields_exclude`; each relationship's join columns; `metrics_exclude`; and
450
+ * each action's executor, which is taken as given, since actions are out of
451
+ * preview scope. A field a profile binds keeps every dialect text the profile
452
+ * wrote. An excluded field is marked in `excludedFields`, and loses its
453
+ * binding when no entity inherits it. The input is never mutated.
454
+ *
455
+ * Throws, naming the profile, when the profile repeats a name, leaves a field
456
+ * entry with nothing to bind, both binds and excludes a field, puts "*" inside
457
+ * a list, names something the model does not declare, binds an abstract
458
+ * entity, states one of a relationship's join column lists without the
459
+ * other or gives both lists with different lengths, gives a source or
460
+ * expression the loader's readers reject, or is applied to a model that
461
+ * declares a `sql` executor, which belongs in each profile.
462
+ */
463
+ function applyProfile(model, profile, opts = {}) {
464
+ const res = applyProfileSpec(model, profile, opts);
465
+ if ('error' in res)
466
+ throw new loader_1.SemanticModelError(res.error);
467
+ if (opts.warnings) {
468
+ // With no `from`, these are the applied model's entity and metric
469
+ // warnings, judged on its tables in the load dialect, as the callers
470
+ // judge them. Keep those the model did not give already.
471
+ const before = new Set((0, loader_1.entityAndMetricWarnings)(model, opts.load?.dialect ?? loader_1.DEFAULT_DIALECT));
472
+ opts.warnings.push(...res.warnings.filter(w => !before.has(w)));
473
+ }
474
+ return res.model;
475
+ }
476
+ /**
477
+ * Reads the profile file `text` and applies it to `model`, or returns the
478
+ * error that stops it, naming the profile. The warnings are those of the
479
+ * model file's load (`opts.from`), less the ones about values the profile
480
+ * replaces, followed by those for the values it sets, each prefixed with the
481
+ * document name.
482
+ */
483
+ function applyProfileFile(model, text, profileName, opts = {}) {
484
+ // Checked before the profile is read, so a model with a `sql` executor
485
+ // reports that first even when the profile file does not parse.
486
+ // profileApplyError checks it again for a profile built in memory.
487
+ const sql = logicalSqlExecutorError(model, profileName);
488
+ if (sql)
489
+ return { error: sql };
490
+ let profile;
491
+ try {
492
+ profile = loadProfileFile(text, profileName);
493
+ }
494
+ catch (err) {
495
+ if (!(err instanceof loader_1.SemanticModelError))
496
+ throw err;
497
+ return { error: err.message };
498
+ }
499
+ return applyProfileSpec(model, profile, opts);
500
+ }
501
+ // applyProfile and applyProfileFile's shared work: the reasons a profile
502
+ // cannot apply, the bindings, and the warnings that follow.
503
+ function applyProfileSpec(model, profile, opts) {
504
+ const error = profileApplyError(model, profile);
505
+ if (error)
506
+ return { error };
507
+ const applied = applyBindings(model, profile, opts);
508
+ if (applied.errors.length) {
509
+ return { error: `profile '${profile.name}': ${applied.errors[0]}` };
510
+ }
511
+ // A field the profile binds is checked on each entity that inherits it, as
512
+ // a load checks a model file's. A model whose inheritance does not resolve
513
+ // is reported by validation instead.
514
+ let concepts;
515
+ try {
516
+ concepts = (0, resolve_inheritance_1.declaredConceptFields)(applied.model);
517
+ }
518
+ catch (err) {
519
+ if (!(err instanceof resolve_inheritance_1.InheritanceError))
520
+ throw err;
521
+ }
522
+ const inherited = concepts && (0, loader_1.inheritedNoSqlProblems)(applied.model.entities, concepts)[0];
523
+ if (inherited)
524
+ return { error: `profile '${profile.name}': ${inherited}` };
525
+ return {
526
+ model: applied.model,
527
+ warnings: warningsAfterProfile(model, applied.model, opts),
528
+ };
529
+ }
530
+ /**
531
+ * Loads the model document `doc` on its own and applies `profile` to it. An
532
+ * error says whether the document failed to load or the profile failed to
533
+ * apply, since the callers word the two differently.
534
+ */
535
+ function applyProfileToDocument(doc, profile, load) {
536
+ return applyToDocument(doc, (base, from) => applyProfileSpec(base, profile, { from, load }), load);
537
+ }
538
+ /** applyProfileToDocument for the profile file `text`. */
539
+ function applyProfileFileToDocument(doc, text, profileName, load) {
540
+ return applyToDocument(doc, (base, from) => applyProfileFile(base, text, profileName, { from, load }), load);
541
+ }
542
+ function applyToDocument(doc, apply, load) {
543
+ const loaded = (0, loader_1.loadSemanticModels)([{ name: doc.name, text: doc.text }], { bindingOptional: true, ...load });
544
+ if (loaded.error)
545
+ return { error: loaded.error, stage: 'load' };
546
+ const res = apply(loaded.models[0].model, { document: doc.name, warnings: loaded.warnings });
547
+ if ('error' in res)
548
+ return { error: res.error, stage: 'apply' };
549
+ return { model: res.model, warnings: res.warnings };
550
+ }
551
+ /**
552
+ * The profiles the model document `doc` declares: `files`, its profile files,
553
+ * then the profiles a vanilla model carries in its GOOGLE block, by name, as
554
+ * the layout orders profile files. Push, `kcmd profiles` and the runtime all
555
+ * read them here, so they agree. Throws SemanticModelError, naming the
556
+ * document as a load does, when googleBlockProfiles throws, such as on block
557
+ * profiles a load rejects.
558
+ */
559
+ function declaredProfiles(doc, files) {
560
+ let blockProfiles;
561
+ try {
562
+ blockProfiles = (0, loader_1.googleBlockProfiles)(doc.text);
563
+ }
564
+ catch (err) {
565
+ if (!(err instanceof loader_1.SemanticModelError))
566
+ throw err;
567
+ throw new loader_1.SemanticModelError(`Model document '${doc.name}': ${err.message}`);
568
+ }
569
+ return [
570
+ ...files,
571
+ ...blockProfiles.map(spec => ({ name: spec.name, spec }))
572
+ .sort((a, b) => a.name.localeCompare(b.name)),
573
+ ];
574
+ }
575
+ /**
576
+ * Finds the profile `profileName` among `profiles`, the ones the model
577
+ * document `doc` declares, and applies it, as push, `kcmd profiles` and the
578
+ * runtime do. It applies to the document loaded on its own.
579
+ */
580
+ function profileForDocument(doc, profiles, profileName, load = {}) {
581
+ const chosen = profiles.find(p => p.name === profileName);
582
+ if (!chosen)
583
+ return { kind: 'missing', declared: profiles.map(p => p.name) };
584
+ const res = 'spec' in chosen ?
585
+ applyProfileToDocument(doc, chosen.spec, load) :
586
+ applyProfileFileToDocument(doc, chosen.text, profileName, load);
587
+ return 'error' in res ? { kind: 'error', ...res } : { kind: 'file', ...res };
588
+ }
589
+ /**
590
+ * The warnings that applying the profile `profileName` among `profiles` to the
591
+ * model document `doc` gives, each labeled with the profile. The warnings in
592
+ * `own`, which the document's own load gives, are left out. Empty unless
593
+ * profileForDocument applies the profile as a profile file.
594
+ */
595
+ function appliedProfileWarnings(doc, profiles, profileName, own, load = {}) {
596
+ const res = profileForDocument(doc, profiles, profileName, load);
597
+ if (res.kind !== 'file')
598
+ return [];
599
+ return res.warnings.filter(w => !own.includes(w))
600
+ .map(w => `profile '${profileName}': ${w}`);
601
+ }
602
+ /**
603
+ * `error`, from loading a model document on its own before a profile file
604
+ * applies to it, with a note that says so. A model file that a profile used to
605
+ * complete, such as one with a half-given join, fails that load. Names
606
+ * `profileName` when one profile is selected.
607
+ */
608
+ function modelFileAloneMessage(error, profileName) {
609
+ return profileName === undefined ?
610
+ `${error} (the model file must load on its own before a profile file ` +
611
+ `applies)` :
612
+ `${error} (under profile '${profileName}', the model file must load ` +
613
+ `on its own before the profile applies)`;
614
+ }
615
+ /**
616
+ * The error a command gives when a profile cannot be applied to the model
617
+ * document `docName`, whose profile files are `profileFiles`. An error applying
618
+ * a profile file names that file, as the profile checks do. An error applying
619
+ * a profile from a GOOGLE block names the model document.
620
+ */
621
+ function profileErrorMessage(docName, res, profileName, profileFiles) {
622
+ if (res.stage === 'load') {
623
+ return modelFileAloneMessage(res.error, profileName);
624
+ }
625
+ const file = profileFiles.find(p => p.name === profileName);
626
+ return `[${file?.fileName ?? docName}] ${res.error}`;
627
+ }
628
+ /**
629
+ * Why `model` cannot be used because a deployment names a binding profile
630
+ * `declared` does not hold, or undefined when each names `default` or a
631
+ * declared profile. `document` is the model's document, for the message.
632
+ */
633
+ function undeclaredProfileError(document, model, declared) {
634
+ for (const d of model.deployments ?? []) {
635
+ const profile = d.profile ?? exports.DEFAULT_PROFILE;
636
+ if (profile === exports.DEFAULT_PROFILE || declared.includes(profile))
637
+ continue;
638
+ return `[${document}] model '${model.name}': deployment '${d.name}' names unknown binding profile '${profile}'; ` +
639
+ (declared.length ? `defined profiles: ${declared.join(', ')}.` :
640
+ `no profiles are defined for this model.`);
641
+ }
642
+ return undefined;
643
+ }
644
+ /** The error for a profile name a model document does not declare. */
645
+ function unknownProfileMessage(profileName, model, declared) {
646
+ return `unknown binding profile '${profileName}' for model '${model}'; ` +
647
+ (declared.length ? `defined profiles: ${declared.join(', ')}.` :
648
+ `no profiles are defined for this model.`);
649
+ }
650
+ /**
651
+ * The error for a model document `docName` whose `profiles` include one named
652
+ * `default` in any letter case. Push refuses every graph push of such a
653
+ * model, and the runtime refuses the model too. Undefined when no profile has
654
+ * that name.
655
+ */
656
+ function reservedProfileNameError(docName, profiles) {
657
+ if (!profiles.some(p => p.name.toLowerCase() === exports.DEFAULT_PROFILE)) {
658
+ return undefined;
659
+ }
660
+ return `[${docName}] a binding profile may not be named '${exports.DEFAULT_PROFILE}' -- that name refers to the model's inline ` +
661
+ `bindings. Rename the profile.`;
662
+ }
663
+ // The warnings a load of the applied model would give, each prefixed with the
664
+ // document name: those about its entities and metrics, from
665
+ // entityAndMetricWarnings, which builds them with the helpers the load uses,
666
+ // then the model file's other load warnings, about actions, which a profile
667
+ // does not change. That is the order a load gives them. The applied model is
668
+ // judged on its own tables, in the load's dialect.
669
+ function warningsAfterProfile(model, applied, opts) {
670
+ const from = opts.from;
671
+ const prefix = from ? `[${from.document}] ` : '';
672
+ const preferred = opts.load?.dialect ?? loader_1.DEFAULT_DIALECT;
673
+ const before = new Set((0, loader_1.entityAndMetricWarnings)(model, preferred).map(w => prefix + w));
674
+ const kept = (from?.warnings ?? []).filter(w => !before.has(w));
675
+ const after = (0, loader_1.entityAndMetricWarnings)(applied, preferred, true).map(w => prefix + w);
676
+ return [...new Set([...after, ...kept])];
677
+ }
678
+ // Every reason `profile` names something `model` cannot take, in this order:
679
+ // a `sql` executor in the model, the profile's own
680
+ // structure, and each action, entity, field, relationship and metric it names
681
+ // that the model does not declare or a profile cannot bind. applyProfile
682
+ // throws the first, and validateProfileCompleteness lists them all.
683
+ function profileProblems(model, profile) {
684
+ const at = `profile '${profile.name}'`;
685
+ const problems = [];
686
+ const sql = logicalSqlExecutorError(model, profile.name);
687
+ if (sql)
688
+ problems.push(sql);
689
+ problems.push(...profileStructureErrors(profile));
690
+ const actions = new Set((model.actions ?? []).map(a => a.name));
691
+ for (const pa of profile.actions ?? []) {
692
+ if (!actions.has(pa.name)) {
693
+ problems.push(`${at}: action '${pa.name}' is not in the logical model`);
694
+ }
695
+ }
696
+ const byName = new Map((model.entities ?? []).map(e => [e.name, e]));
697
+ for (const pe of profile.entities) {
698
+ const e = byName.get(pe.name);
699
+ if (!e) {
700
+ problems.push(`${at}: entity '${pe.name}' is not in the logical model`);
701
+ continue;
702
+ }
703
+ if (e.abstract) {
704
+ problems.push(`${at}: entity '${pe.name}' is abstract, so a profile cannot ` +
705
+ `bind it`);
706
+ continue;
707
+ }
708
+ const declared = new Set(e.fields.map(f => f.name));
709
+ const inherited = inheritedNamesOf(model, e.name, byName);
710
+ for (const pf of pe.fields ?? []) {
711
+ if (!declared.has(pf.name) && !inherited.has(pf.name)) {
712
+ problems.push(`${at}: field '${pe.name}.${pf.name}' is not in the logical model`);
713
+ }
714
+ }
715
+ for (const name of pe.fieldsExclude ?? []) {
716
+ if (!declared.has(name) && !inherited.has(name)) {
717
+ problems.push(`${at}: field '${pe.name}.${name}' in 'fields_exclude' is not in the logical model`);
718
+ }
719
+ }
720
+ }
721
+ const relationships = new Set((model.relationships ?? []).map(r => r.name));
722
+ for (const pr of profile.relationships) {
723
+ if (!relationships.has(pr.name)) {
724
+ problems.push(`${at}: relationship '${pr.name}' is not in the logical model`);
725
+ }
726
+ }
727
+ if (Array.isArray(profile.metricsExclude)) {
728
+ const metrics = new Set((model.metrics ?? []).map(m => m.name));
729
+ for (const name of profile.metricsExclude) {
730
+ if (!metrics.has(name)) {
731
+ problems.push(`${at}: metric '${name}' in 'metrics_exclude' is not in the logical model`);
732
+ }
733
+ }
734
+ }
735
+ return problems;
736
+ }
737
+ // Why `profile` cannot apply to `model`, or undefined: the first of
738
+ // profileProblems, then the join columns the profile states for each
739
+ // relationship. One list without the other gets the completeness check's
740
+ // error, and two lists must have equal length. A relationship the profile
741
+ // gives neither list keeps the model file's join columns, which the
742
+ // completeness check reports.
743
+ function profileApplyError(model, profile) {
744
+ const problems = profileProblems(model, profile);
745
+ if (problems.length)
746
+ return problems[0];
747
+ const at = `profile '${profile.name}'`;
748
+ for (const { name, fromColumns: from, toColumns: to } of profile.relationships) {
749
+ if (!from.length !== !to.length) {
750
+ return `${at}: ${profileJoinMessage(name)}`;
751
+ }
752
+ if (from.length !== to.length) {
753
+ return `${at}: ${(0, loader_1.joinLengthMessage)(name, from.length, to.length)}`;
754
+ }
755
+ }
756
+ return undefined;
757
+ }
758
+ // The error for a relationship a named profile does not bind with both
759
+ // 'from_columns' and 'to_columns'.
760
+ function profileJoinMessage(relationship) {
761
+ return `relationship '${relationship}' has no join columns in this ` +
762
+ `profile; a named profile states both 'from_columns' and ` +
763
+ `'to_columns' itself`;
764
+ }
765
+ // What loadProfileFile rejects in a profile's structure, in the order it
766
+ // checks, with its messages: a repeated name, a field entry with nothing to
767
+ // bind, a repeated dialect, a field both bound and excluded, and `"*"` inside
768
+ // a list. A profile file never has these; a profile built in memory may.
769
+ function profileStructureErrors(profile) {
770
+ const at = `profile '${profile.name}'`;
771
+ const errors = [];
772
+ const twice = (names) => names.find((n, i) => names.indexOf(n) !== i);
773
+ const dupEntity = twice(profile.entities.map(e => e.name));
774
+ if (dupEntity)
775
+ errors.push(`${at} 'entities' lists '${dupEntity}' twice`);
776
+ for (const pe of profile.entities) {
777
+ const where = `${at}: entity '${pe.name}'`;
778
+ const bound = (pe.fields ?? []).map(f => f.name);
779
+ const dupField = twice(bound);
780
+ if (dupField)
781
+ errors.push(`${where} 'fields' lists '${dupField}' twice`);
782
+ for (const pf of pe.fields ?? []) {
783
+ const fat = `${where}: field '${pf.name}'`;
784
+ if (!(0, ir_1.isFieldBound)(pf)) {
785
+ errors.push(`${fat} has no 'expression'; bind it with one, or list it in ` +
786
+ `'fields_exclude'`);
787
+ }
788
+ // Checked by the loader's reader, as a profile file's is. A binding
789
+ // with no dialect list, which only a profile built in memory has, is
790
+ // read as parseFieldBinding reads it.
791
+ if ((0, loader_1.expressionDocOf)(pf) !== undefined) {
792
+ try {
793
+ (0, loader_1.parseFieldBinding)(pf, fat, loader_1.DEFAULT_DIALECT, pe.name);
794
+ }
795
+ catch (err) {
796
+ if (!(err instanceof loader_1.SemanticModelError))
797
+ throw err;
798
+ errors.push(err.message);
799
+ }
800
+ }
801
+ }
802
+ const both = (pe.fieldsExclude ?? []).find(n => bound.includes(n));
803
+ if (both) {
804
+ errors.push(`${where}: field '${both}' is in both 'fields' and ` +
805
+ `'fields_exclude'`);
806
+ }
807
+ }
808
+ const dupRel = twice(profile.relationships.map(r => r.name));
809
+ if (dupRel)
810
+ errors.push(`${at} 'relationships' lists '${dupRel}' twice`);
811
+ if (Array.isArray(profile.metricsExclude) &&
812
+ profile.metricsExclude.includes('*')) {
813
+ errors.push(`${at}: 'metrics_exclude' takes "*" on its own, not inside a list`);
814
+ }
815
+ return errors;
816
+ }
817
+ // The error for a model that declares a `sql` executor when a profile binds
818
+ // it: a statement names one database's tables, so it belongs in the profile.
819
+ function logicalSqlExecutorError(model, profileName) {
820
+ const action = (model.actions ?? []).find(a => a.executor?.kind === 'sql');
821
+ return action && sqlExecutorMessage(profileName, action.name, model.name);
822
+ }
823
+ // The error for a `sql` executor in a model that a profile applies to.
824
+ function sqlExecutorMessage(profileName, action, model) {
825
+ return `profile '${profileName}': action '${action}' in model '${model}' declares a 'sql' executor. A statement names one ` +
826
+ `database's own tables and columns, so it belongs in the profile that ` +
827
+ `binds them, not in the model. Move the executor into each profile ` +
828
+ `that performs this write as DML.`;
829
+ }
830
+ // Applies what `profile` sets on the names `model` declares, and skips any
831
+ // name it does not. applyProfile checks the names first; the profile checks
832
+ // report each unknown name themselves. With `noSqlAsWritten`, a field binding
833
+ // the reader rejects only for holding no SQL is applied as a valid one is.
834
+ function applyBindings(model, profile, opts) {
835
+ const errors = [];
836
+ const out = structuredClone(model);
837
+ const load = opts.load ?? {};
838
+ const preferred = load.dialect ?? loader_1.DEFAULT_DIALECT;
839
+ // A source or expression the loader's readers reject is left unapplied. A
840
+ // rejected source is listed in `errors`. A rejected expression is not:
841
+ // profileStructureErrors reports it first, in the words a profile file gets,
842
+ // and every caller runs it before this.
843
+ const read = (readValue, list) => {
844
+ try {
845
+ return readValue();
846
+ }
847
+ catch (err) {
848
+ if (!(err instanceof loader_1.SemanticModelError))
849
+ throw err;
850
+ if (list)
851
+ errors.push(readerMessage(err));
852
+ return undefined;
853
+ }
854
+ };
855
+ // An executor the profile leaves out stays as the model declares it; `null`
856
+ // withdraws it.
857
+ for (const pa of profile.actions ?? []) {
858
+ const a = (out.actions ?? []).find(x => x.name === pa.name);
859
+ if (!a || pa.executor === undefined)
860
+ continue;
861
+ if (pa.executor === null) {
862
+ delete a.executor;
863
+ }
864
+ else {
865
+ a.executor = structuredClone(pa.executor);
866
+ }
867
+ }
868
+ const byName = new Map((out.entities ?? []).map(e => [e.name, e]));
869
+ const extended = entitiesWithDescendants(out);
870
+ for (const pe of profile.entities) {
871
+ const e = byName.get(pe.name);
872
+ if (!e || e.abstract)
873
+ continue;
874
+ const parsed = pe.source === undefined ?
875
+ undefined :
876
+ read(() => (0, loader_1.parseSource)(pe.source, load, `entity '${e.name}'`), true);
877
+ if (parsed) {
878
+ e.dataSource = parsed.dataSource;
879
+ e.authoredSource = parsed.authoredSource;
880
+ // The profile's source replaces the model file's, and so does its
881
+ // looked-up dialect, or the lack of one.
882
+ if (parsed.sourceDialect) {
883
+ e.sourceDialect = parsed.sourceDialect;
884
+ }
885
+ else {
886
+ delete e.sourceDialect;
887
+ }
888
+ }
889
+ if (pe.primaryKey !== undefined) {
890
+ e.keys = [...pe.primaryKey];
891
+ }
892
+ if (pe.uniqueKeys !== undefined) {
893
+ if (pe.uniqueKeys.length) {
894
+ e.uniqueKeys = pe.uniqueKeys.map(k => [...k]);
895
+ }
896
+ else {
897
+ delete e.uniqueKeys;
898
+ }
899
+ }
900
+ const inherited = inheritedNamesOf(out, e.name, byName);
901
+ for (const pf of pe.fields ?? []) {
902
+ // An entry with nothing to bind leaves the field as it is. applyProfile
903
+ // rejects one, and validateProfileCompleteness reports it, through
904
+ // profileStructureErrors.
905
+ if (!(0, ir_1.isFieldBound)(pf))
906
+ continue;
907
+ // Read as a load reads the same expression in a model file. One the
908
+ // reader rejects is left unapplied, also when a profile built in memory
909
+ // gives it with no dialect list. Under `noSqlAsWritten`, one the reader
910
+ // rejects only for holding no SQL is read as the schema accepts it.
911
+ const where = `field '${e.name}.${pf.name}'`;
912
+ const binding = read(() => (0, loader_1.parseFieldBinding)(pf, where, preferred, e.name), false) ??
913
+ (opts.noSqlAsWritten ?
914
+ read(() => (0, loader_1.readFieldExpression)((0, loader_1.expressionDocOf)(pf), where, preferred, () => undefined), false) :
915
+ undefined);
916
+ if (!binding)
917
+ continue;
918
+ let f = e.fields.find(x => x.name === pf.name);
919
+ if (!f) {
920
+ if (!inherited.has(pf.name))
921
+ continue;
922
+ // Binding an inherited field redeclares it on this entity with only a
923
+ // binding, which inheritance merges over the ancestor's definition.
924
+ f = { name: pf.name };
925
+ e.fields.push(f);
926
+ }
927
+ for (const k of ir_1.FIELD_BINDING_KEYS)
928
+ delete f[k];
929
+ if (pf.dialects?.length) {
930
+ Object.assign(f, binding);
931
+ }
932
+ else {
933
+ // A profile built in memory may carry a binding with no dialect list,
934
+ // which is applied as written.
935
+ for (const k of ir_1.FIELD_BINDING_KEYS) {
936
+ if (pf[k] !== undefined)
937
+ f[k] = structuredClone(pf[k]);
938
+ }
939
+ }
940
+ }
941
+ // An exclusion applies to this entity only. A field no entity inherits
942
+ // from this one also loses its binding; one a descendant inherits keeps
943
+ // the binding the descendant reads.
944
+ const below = extended.has(e.name);
945
+ for (const name of pe.fieldsExclude ?? []) {
946
+ const f = e.fields.find(x => x.name === name);
947
+ if (!f || below)
948
+ continue;
949
+ if (inherited.has(name)) {
950
+ // A line that only redeclares an inherited field goes with its
951
+ // binding. The entity still inherits the declaration.
952
+ e.fields = e.fields.filter(x => x !== f);
953
+ }
954
+ else {
955
+ for (const k of ir_1.FIELD_BINDING_KEYS)
956
+ delete f[k];
957
+ }
958
+ }
959
+ if (pe.fieldsExclude?.length) {
960
+ e.excludedFields =
961
+ [...new Set([...(e.excludedFields ?? []), ...pe.fieldsExclude])];
962
+ }
963
+ }
964
+ // A side the profile leaves empty keeps the model file's columns.
965
+ for (const pr of profile.relationships) {
966
+ const r = (out.relationships ?? []).find(x => x.name === pr.name);
967
+ if (!r)
968
+ continue;
969
+ if (pr.fromColumns.length)
970
+ r.source.columns = [...pr.fromColumns];
971
+ if (pr.toColumns.length)
972
+ r.destination.columns = [...pr.toColumns];
973
+ }
974
+ if (profile.metricsExclude !== undefined) {
975
+ const names = profile.metricsExclude === '*' ?
976
+ (out.metrics ?? []).map(m => m.name) :
977
+ profile.metricsExclude;
978
+ if (names.length) {
979
+ out.excludedMetrics =
980
+ [...new Set([...(out.excludedMetrics ?? []), ...names])];
981
+ }
982
+ }
983
+ // The load read each ANSI_SQL metric in the dialect the model file's sources
984
+ // imply, or with the generic parser when they imply none. The profile's
985
+ // sources can imply another dialect, so each metric with no authored entity
986
+ // is inferred again, as a load of the model file with these sources would
987
+ // infer it. A metric whose new reading does not pin exactly one entity has
988
+ // none.
989
+ const entityNames = (out.entities ?? []).map(e => e.name);
990
+ const impliedDialect = sourcesDialect(out);
991
+ for (const m of out.metrics ?? []) {
992
+ if (m.authoredEntity !== undefined)
993
+ continue;
994
+ const { referenced } = (0, loader_1.inferMetricEntities)(m, entityNames, preferred, impliedDialect, out.version);
995
+ if (referenced.length === 1) {
996
+ m.entity = referenced[0];
997
+ }
998
+ else {
999
+ delete m.entity;
1000
+ }
1001
+ }
1002
+ return { model: out, errors };
1003
+ }
1004
+ // The message of an error the loader's source or expression reader throws,
1005
+ // without the prefix a model file's load gives it.
1006
+ function readerMessage(err) {
1007
+ return err.message.replace(/^Semantic model load error: /, '');
1008
+ }
1009
+ // The entities another entity extends, directly or through others: exactly
1010
+ // those named in some other entity's `extends`.
1011
+ function entitiesWithDescendants(model) {
1012
+ return new Set((model.entities ?? []).flatMap(e => (e.extends ?? []).filter(p => p !== e.name)));
1013
+ }
1014
+ // Reads a parsed profile file into a ProfileSpec, throwing on anything a
1015
+ // profile may not say. Shared by loadProfileFile and the loader's reading of a
1016
+ // vanilla GOOGLE block's profiles, so the two agree on what a valid profile is.
1017
+ function profileSpecOf(doc, profileName) {
1018
+ const where = `profile '${profileName}'`;
1019
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc) ||
1020
+ doc.semantic_model !== undefined) {
1021
+ throw new loader_1.SemanticModelError(`${where} is not a profile file`);
1022
+ }
1023
+ if (doc.version !== undefined) {
1024
+ throw new loader_1.SemanticModelError(`${where} sets 'version'; a profile file takes its version from the ` +
1025
+ `model file beside it, so remove the line`);
1026
+ }
1027
+ if (doc.datasets !== undefined) {
1028
+ throw new loader_1.SemanticModelError(`${where} lists its bindings under 'datasets'; a profile lists them ` +
1029
+ `under 'entities'`);
1030
+ }
1031
+ // The profile's own name is checked against the file name below.
1032
+ requireKeys(doc, PROFILE_FILE_KEYS, where, false);
1033
+ if (doc.name !== profileName) {
1034
+ throw new loader_1.SemanticModelError(`${where} declares name '${doc.name ?? ''}', which does not match ` +
1035
+ `its file's profile name '${profileName}'`);
1036
+ }
1037
+ const spec = {
1038
+ name: profileName,
1039
+ entities: listOf(doc.entities, `${where} 'entities'`)
1040
+ .map(e => profileEntity(e, where)),
1041
+ relationships: listOf(doc.relationships, `${where} 'relationships'`).map(r => {
1042
+ requireKeys(r, PROFILE_RELATIONSHIP_KEYS, `${where}: relationship`);
1043
+ const at = `${where}: relationship '${r.name}'`;
1044
+ // An empty list is rejected as a load rejects one, so an empty side
1045
+ // here can only mean a side the profile leaves out.
1046
+ for (const key of ['from_columns', 'to_columns']) {
1047
+ if (Array.isArray(r[key]) && !r[key].length) {
1048
+ throw new loader_1.SemanticModelError(`${at} '${key}' lists no column`);
1049
+ }
1050
+ }
1051
+ // A side the profile leaves out is empty, which completeness
1052
+ // rejects: a named profile states both join column lists itself.
1053
+ const binding = {
1054
+ name: r.name,
1055
+ fromColumns: stringList(r.from_columns, `${at} 'from_columns'`),
1056
+ toColumns: stringList(r.to_columns, `${at} 'to_columns'`),
1057
+ };
1058
+ return binding;
1059
+ }),
1060
+ };
1061
+ if (doc.metrics_exclude !== undefined) {
1062
+ const ex = doc.metrics_exclude;
1063
+ if (ex !== '*' &&
1064
+ !(Array.isArray(ex) &&
1065
+ ex.every((n) => typeof n === 'string'))) {
1066
+ throw new loader_1.SemanticModelError(`${where}: 'metrics_exclude' must be "*" or a list of metric names`);
1067
+ }
1068
+ spec.metricsExclude = ex;
1069
+ }
1070
+ if (doc.actions !== undefined) {
1071
+ // Actions are out of preview scope and gain no new checks, so when a
1072
+ // profile lists an action twice, the last entry wins.
1073
+ spec.actions = listOf(doc.actions, `${where} 'actions'`).map(a => {
1074
+ requireKeys(a, PROFILE_ACTION_KEYS, `${where}: action`);
1075
+ if (a.executor === undefined)
1076
+ return { name: a.name };
1077
+ return {
1078
+ name: a.name,
1079
+ executor: a.executor === null ?
1080
+ null :
1081
+ (0, loader_1.parseExecutor)(a.executor, `${where}: action '${a.name}'`),
1082
+ };
1083
+ });
1084
+ }
1085
+ // A repeated name, a field with nothing to bind, a field both bound and
1086
+ // excluded, and "*" inside a list, judged as for a profile held in memory.
1087
+ const problems = profileStructureErrors(spec);
1088
+ if (problems.length)
1089
+ throw new loader_1.SemanticModelError(problems[0]);
1090
+ return spec;
1091
+ }
1092
+ function profileEntity(e, where) {
1093
+ requireKeys(e, PROFILE_ENTITY_KEYS, `${where}: entity`);
1094
+ const at = `${where}: entity '${e.name}'`;
1095
+ const binding = { name: e.name };
1096
+ if (e.source !== undefined) {
1097
+ if (typeof e.source !== 'string') {
1098
+ throw new loader_1.SemanticModelError(`${at} 'source' must be a string`);
1099
+ }
1100
+ binding.source = e.source;
1101
+ }
1102
+ if (e.primary_key !== undefined) {
1103
+ binding.primaryKey = stringList(e.primary_key, `${at} 'primary_key'`);
1104
+ }
1105
+ if (e.unique_keys !== undefined) {
1106
+ binding.uniqueKeys = listOf(e.unique_keys, `${at} 'unique_keys'`)
1107
+ .map(k => stringList(k, `${at} 'unique_keys'`));
1108
+ }
1109
+ if (e.fields !== undefined) {
1110
+ binding.fields = listOf(e.fields, `${at} 'fields'`).map(f => {
1111
+ requireKeys(f, PROFILE_FIELD_KEYS, `${at}: field`);
1112
+ const fat = `${at}: field '${f.name}'`;
1113
+ return f.expression === undefined ?
1114
+ { name: f.name } :
1115
+ {
1116
+ name: f.name,
1117
+ ...(0, loader_1.parseFieldExpression)(f.expression, fat, loader_1.DEFAULT_DIALECT, e.name),
1118
+ };
1119
+ });
1120
+ }
1121
+ if (e.fields_exclude !== undefined) {
1122
+ binding.fieldsExclude =
1123
+ stringList(e.fields_exclude, `${at} 'fields_exclude'`);
1124
+ }
1125
+ return binding;
1126
+ }
1127
+ // Checks that `obj` is a mapping carrying only `allowed` keys and, unless
1128
+ // `named` is false, a string `name`.
1129
+ function requireKeys(obj, allowed, where, named = true) {
1130
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
1131
+ throw new loader_1.SemanticModelError(`${where} is not a mapping`);
1132
+ }
1133
+ if (named && typeof obj.name !== 'string') {
1134
+ throw new loader_1.SemanticModelError(`${where} has no 'name'`);
1135
+ }
1136
+ for (const k of Object.keys(obj)) {
1137
+ if (!allowed.has(k)) {
1138
+ throw new loader_1.SemanticModelError(notAllowed(named && obj.name !== undefined ? `${where} '${obj.name}'` : where, k));
1139
+ }
1140
+ }
1141
+ }
1142
+ function listOf(value, where) {
1143
+ if (value === undefined)
1144
+ return [];
1145
+ if (!Array.isArray(value)) {
1146
+ throw new loader_1.SemanticModelError(`${where} must be a list`);
1147
+ }
1148
+ return value;
1149
+ }
1150
+ function stringList(value, where) {
1151
+ const list = listOf(value, where);
1152
+ if (!list.every(v => typeof v === 'string')) {
1153
+ throw new loader_1.SemanticModelError(`${where} must be a list of names`);
1154
+ }
1155
+ return list;
1156
+ }
1157
+ // ---------------------------------------------------------------------------
1158
+ // Whether a profile can be deployed.
1159
+ // ---------------------------------------------------------------------------
1160
+ // The dialect each system's sources infer, keyed by the
1161
+ // system databaseOf puts first.
1162
+ const DIALECT_OF_SYSTEM = {
1163
+ bigquery: 'BIGQUERY',
1164
+ spanner: 'SPANNER',
1165
+ alloydb: 'ALLOYDB',
1166
+ cloudsql_mysql: 'MYSQL',
1167
+ cloudsql_postgresql: 'POSTGRES',
1168
+ };
1169
+ // Each text a field's or metric's expression carries, with the dialect it is
1170
+ // written in.
1171
+ function expressionSources(f) {
1172
+ const out = (f.dialects ?? []).map(d => ({ text: d.expression, dialect: d.dialect }));
1173
+ for (const text of [f.expression, f.importedExpression]) {
1174
+ if (text !== undefined && !out.some(o => o.text === text)) {
1175
+ out.push({ text, dialect: dialectOf(f, text) });
1176
+ }
1177
+ }
1178
+ return out;
1179
+ }
1180
+ // The texts a profile of `dialect` evaluates for a field or metric, each with
1181
+ // the dialect to read it in. A profile has one dialect, so it evaluates the
1182
+ // entry for that dialect, or the ANSI_SQL entry when there is none, and its
1183
+ // database runs an ANSI_SQL text as its own SQL. A field held without a
1184
+ // dialect list offers its imported text when that is in the profile's dialect,
1185
+ // and otherwise the text the loader picked. With no known dialect, every text
1186
+ // counts, each read in the dialect it is written in. With a null dialect, which
1187
+ // no lookup read, the text the database runs is not known, and none counts.
1188
+ function textsFor(f, dialect) {
1189
+ if (dialect === null)
1190
+ return [];
1191
+ if (dialect === undefined)
1192
+ return expressionSources(f);
1193
+ // The text a database of this dialect runs: its own entry, or the
1194
+ // ANSI_SQL one.
1195
+ const text = (0, ir_1.expressionForDialect)(f, dialect);
1196
+ return text !== undefined ? [{ text, dialect }] : [];
1197
+ }
1198
+ // Whether a field or metric holds SQL but has no text in any of `dialects`
1199
+ // and no ANSI_SQL text, so no table of those dialects runs it.
1200
+ function noTextFor(f, dialects) {
1201
+ return (0, ir_1.isFieldBound)(f) && dialects.every(d => !textsFor(f, d).length);
1202
+ }
1203
+ // The table of `entity`, or undefined when the load does not know its
1204
+ // dialect. With `withUnread`, a Cloud SQL, Spanner or third-party table no
1205
+ // lookup has read is given too, and a Cloud SQL name with no schema gives
1206
+ // MySQL.
1207
+ function tableDialects(entity, withUnread) {
1208
+ const dialect = sourcesDialect({ entities: [entity] });
1209
+ if (dialect)
1210
+ return { name: dialect, dialects: [dialect] };
1211
+ if (!withUnread)
1212
+ return undefined;
1213
+ const source = (0, loader_1.writtenSource)(entity);
1214
+ const table = (0, loader_1.cloudSqlTableOf)(source);
1215
+ if (table) {
1216
+ // Only a MySQL table's name has no schema.
1217
+ const mysql = DIALECT_OF_SYSTEM.cloudsql_mysql;
1218
+ if (!table.inSchema)
1219
+ return { name: mysql, dialects: [mysql], guessed: true };
1220
+ const dialects = [...new Set(Object.values(loader_1.DIALECT_OF_ENTRY_TYPE))].filter(d => d !== mysql);
1221
+ return { name: 'Cloud SQL', dialects, unread: true };
1222
+ }
1223
+ if ((0, loader_1.thirdPartyEntryOf)(source)) {
1224
+ const dialects = [...loader_1.DIALECT_OF_FQN_PREFIX.values()];
1225
+ return { name: 'third-party', dialects, unread: true };
1226
+ }
1227
+ if ((0, loader_1.spannerDatabaseOf)(source)) {
1228
+ return { name: 'Spanner', dialects: ['SPANNER', 'POSTGRES'], unread: true };
1229
+ }
1230
+ return undefined;
1231
+ }
1232
+ // Each field of each entity whose table `tableDialects(entity, withUnread)`
1233
+ // gives, with that table. An inherited field is listed on each entity that
1234
+ // inherits it.
1235
+ function fieldsOnTables(model, withUnread = false) {
1236
+ return resolvedEntities(model).flatMap(entity => {
1237
+ const table = tableDialects(entity, withUnread);
1238
+ if (!table)
1239
+ return [];
1240
+ return (entity.fields ?? []).map(field => ({ entity, field, table }));
1241
+ });
1242
+ }
1243
+ // The import command, ending in `flags`, that keeps a model file of `version`
1244
+ // in its own flavor: `--flavor google` for a Google model, `--flavor ossie` for
1245
+ // a vanilla one, and both flavors when no model is known. Push reads only files
1246
+ // written for kcmd, so `--input first-party` fits each of them.
1247
+ function importCommand(flags, version) {
1248
+ const flavor = version === loader_1.GOOGLE_VERSION ? 'google' :
1249
+ version === loader_1.OSSIE_VERSION ? 'ossie' :
1250
+ '<ossie|google>';
1251
+ return `kcmd import <file> --input first-party --flavor ${flavor} ${flags}`;
1252
+ }
1253
+ // Whether `kcmd import --transpile` gives BigQuery SQL to a field or metric
1254
+ // of `model` with no text for `dialect`: true when `dialect` is BIGQUERY and
1255
+ // import's translator turns its imported text into BigQuery SQL.
1256
+ function importTranspiles(f, dialect, model) {
1257
+ return dialect === 'BIGQUERY' && !!f.importedExpression &&
1258
+ (0, transpile_1.translatesToBigQuery)(f, (model.entities ?? []).map(e => e.name));
1259
+ }
1260
+ // The hint that import can add a text in `dialect` to `f`, a field of
1261
+ // `model`, or ''.
1262
+ function importHint(f, dialect, model) {
1263
+ if (!importTranspiles(f, dialect, model))
1264
+ return '';
1265
+ return `; give it one, or add one with '${importCommand('--transpile', model.version)}'`;
1266
+ }
1267
+ // The dialect a model's sources imply: that of the one database every concrete
1268
+ // entity reads from, or undefined when they name several, or a source names
1269
+ // none. It is null when every source is a table whose dialect no lookup read
1270
+ // (see dialectOfSources).
1271
+ function sourcesDialect(model) {
1272
+ return dialectOfSources((model.entities ?? [])
1273
+ .filter(e => !e.abstract)
1274
+ .map(e => ({
1275
+ source: (0, loader_1.writtenSource)(e),
1276
+ lookedUp: e.sourceDialect,
1277
+ })));
1278
+ }
1279
+ // The dialect every one of `sources` implies, or undefined when one names no
1280
+ // database in an accepted form or two imply different dialects. Two databases
1281
+ // of one system, such as two AlloyDB databases, imply that system's dialect.
1282
+ // A three-part dotted name names BigQuery.
1283
+ //
1284
+ // A Cloud SQL table written as its resource name, a third-party table and a
1285
+ // Spanner table imply the dialect a lookup gave in `lookedUp`
1286
+ // (LoadOptions.sourceDialects): the one their catalog entry or Spanner
1287
+ // database gives. When every source is such a table and none was looked up,
1288
+ // as when `kcmd import` cannot read them, the dialect is null: how their
1289
+ // database reads an ANSI_SQL text is not known, so a check reads no such text.
1290
+ function dialectOfSources(sources) {
1291
+ if (sources.length &&
1292
+ sources.every(s => (0, loader_1.dialectLookupKey)(s.source.trim()) && !s.lookedUp)) {
1293
+ return null;
1294
+ }
1295
+ const dialects = new Set(sources.map(({ source, lookedUp }) => {
1296
+ const s = source.trim();
1297
+ if ((0, loader_1.dialectLookupKey)(s))
1298
+ return lookedUp;
1299
+ const db = (0, loader_1.databaseOf)(s) ??
1300
+ (/^[^./:\s`]+\.[^./:\s`]+\.[^./:\s`]+$/.test(s) ? 'bigquery' :
1301
+ undefined);
1302
+ return db ? DIALECT_OF_SYSTEM[db.split('/')[0]] : undefined;
1303
+ }));
1304
+ if (dialects.size !== 1)
1305
+ return undefined;
1306
+ return [...dialects][0];
1307
+ }
1308
+ /**
1309
+ * Returns one message per reason `profile` cannot be deployed against
1310
+ * `baseModel`, or an empty list. Checks a single profile: nothing unknown,
1311
+ * every field it binds holding SQL on each entity that inherits it, every
1312
+ * concrete entity bound and all in one database, its own keys where the model
1313
+ * file states keys, every relationship bound, every field the model file
1314
+ * leaves unbound accounted for, every field whose expression reads a column
1315
+ * restated or excluded, and exclusions closed under dependency, metrics
1316
+ * included. The check that join columns cover a key of the target is in
1317
+ * validateProfileConsistency. An expression the SQL parser cannot read
1318
+ * is skipped by the rules that read references, with a message in `warnings`.
1319
+ * `opts` says what the caller checks itself, so that nothing is reported
1320
+ * twice.
1321
+ */
1322
+ function validateProfileCompleteness(baseModel, profile, warnings = [], opts = {}) {
1323
+ const errors = [];
1324
+ // Each field or metric whose SQL the parser cannot read, reported once, by
1325
+ // the label fieldLabel or metricLabel gives it.
1326
+ const unreadable = new Set();
1327
+ const at = `profile '${profile.name}'`;
1328
+ // What a profile file could not say, for a profile built in memory, and
1329
+ // every name the model does not take, as applyProfile judges them.
1330
+ errors.push(...profileProblems(baseModel, profile));
1331
+ const entities = baseModel.entities ?? [];
1332
+ const byName = new Map(entities.map(e => [e.name, e]));
1333
+ // Declared plus inherited fields, in the model file and as this profile sees
1334
+ // the model: the profile applied as push applies it, and each excluded field
1335
+ // left off the entity that names it. Push reports a broken hierarchy in the
1336
+ // model file. A profile breaks the hierarchy itself when it binds a field on
1337
+ // two unrelated branches. That is reported here once the model file's
1338
+ // hierarchy resolves. The rules below still run on a broken hierarchy, apart
1339
+ // from the check that each inherited field holds SQL, which push also skips
1340
+ // then. An entity whose own ancestors do not resolve shows only its declared
1341
+ // fields, so the rules that need what it inherits leave it out.
1342
+ const fieldsOf = fieldsByEntity(baseModel);
1343
+ // The entities whose own ancestors do not resolve in the model file, so
1344
+ // what they inherit is not known.
1345
+ const unknownAncestry = unresolvedEntities(baseModel);
1346
+ const { model: profiled, errors: unread } = applyBindings(baseModel, profile, { load: { sourceDialects: opts.sourceDialects } });
1347
+ errors.push(...unread.map(e => `${at}: ${e}`));
1348
+ const profiledError = inheritanceErrorOf(profiled);
1349
+ if (profiledError && !inheritanceErrorOf(baseModel)) {
1350
+ errors.push(`${at}: ${profiledError}`);
1351
+ }
1352
+ // A field the profile binds is checked on each entity that inherits it, as
1353
+ // applyProfile checks it. A binding the reader rejects only for holding no
1354
+ // SQL is reported where the profile writes it and left unapplied above.
1355
+ // Applied here, it is also reported on an entity where its expression, read
1356
+ // as a document writes it, has a further problem.
1357
+ if (!profiledError) {
1358
+ const asWritten = applyBindings(baseModel, profile, { noSqlAsWritten: true }).model;
1359
+ const checked = inheritanceErrorOf(asWritten) ? profiled : asWritten;
1360
+ const inherited = (0, loader_1.inheritedNoSqlProblems)(checked.entities, (0, resolve_inheritance_1.declaredConceptFields)(checked));
1361
+ errors.push(...inherited.map(p => `${at}: ${p}`));
1362
+ }
1363
+ const seenOf = fieldsByEntity(profiled);
1364
+ const bindingOf = new Map(profile.entities.map(e => [e.name, e]));
1365
+ // Every concrete entity bound, all in one database. The profile's dialect
1366
+ // is the one its own sources imply, so a source the model file states for an
1367
+ // entity the profile leaves out does not decide it.
1368
+ const databases = new Map();
1369
+ // Each table whose entry no lookup read, with the keys of the databases it
1370
+ // may be in: its PostgreSQL database or its SQL Server instance for a Cloud
1371
+ // SQL table in a schema, and any third-party database for a third-party
1372
+ // table. `key` is the one it takes when it counts as a database of its own.
1373
+ const unreadTables = [];
1374
+ const isThirdParty = (k) => k.startsWith('thirdparty/') ||
1375
+ [...loader_1.DIALECT_OF_FQN_PREFIX.keys()].some(p => k.startsWith(`${p}/`));
1376
+ const stated = [];
1377
+ const profiledByName = new Map((profiled.entities ?? []).map(e => [e.name, e]));
1378
+ // The entities whose source the loader's reader rejects.
1379
+ const rejected = new Set();
1380
+ for (const e of entities) {
1381
+ if (e.abstract)
1382
+ continue;
1383
+ const pe = bindingOf.get(e.name);
1384
+ if (pe?.source === undefined) {
1385
+ errors.push(`${at}: entity '${e.name}' has no source in this profile`);
1386
+ continue;
1387
+ }
1388
+ // A source the loader's reader rejects is already reported, and its
1389
+ // entity keeps the model file's table and dialect. databaseOf still reads
1390
+ // some rejected sources, such as one with a leading `//`. An entity whose
1391
+ // source is rejected or names no database is left out of the one-database
1392
+ // and dialect rules.
1393
+ const db = (0, loader_1.databaseOf)(pe.source.trim());
1394
+ const profiledEntity = profiledByName.get(e.name);
1395
+ if (!db || profiledEntity?.authoredSource !== pe.source.trim()) {
1396
+ rejected.add(e.name);
1397
+ continue;
1398
+ }
1399
+ const lookedUp = profiledEntity.sourceDialect;
1400
+ stated.push({ source: pe.source, lookedUp });
1401
+ // One query reaches every database of a SQL Server instance, so a SQL
1402
+ // Server table is keyed by its instance. A third-party table is in the
1403
+ // database its entry's fullyQualifiedName names. A Cloud SQL table in a
1404
+ // schema may be either engine's, so for it and for a third-party table
1405
+ // the entry decides which database of its system it is in. A table whose
1406
+ // entry no lookup read is compared after the loop.
1407
+ const table = (0, loader_1.cloudSqlTableOf)(pe.source);
1408
+ const instance = table?.resourceName.split('/databases/')[0];
1409
+ if (lookedUp === undefined && table?.inSchema) {
1410
+ const fits = (k) => k === db || k === instance;
1411
+ unreadTables.push({ entity: e.name, key: instance, fits });
1412
+ continue;
1413
+ }
1414
+ if (lookedUp === undefined && db === 'thirdparty') {
1415
+ unreadTables.push({ entity: e.name, key: 'thirdparty/', fits: isThirdParty });
1416
+ continue;
1417
+ }
1418
+ const key = table && lookedUp === 'SQLSERVER' ? instance :
1419
+ db !== 'thirdparty' ? db :
1420
+ opts.sourceDatabases?.get(pe.source.trim()) ?? `thirdparty/${lookedUp}`;
1421
+ if (!databases.has(key))
1422
+ databases.set(key, e.name);
1423
+ }
1424
+ // A third-party table whose entry names no database is in the database of
1425
+ // the other tables of its system.
1426
+ for (const [prefix, systemDialect] of loader_1.DIALECT_OF_FQN_PREFIX) {
1427
+ if ([...databases.keys()].some(k => k.startsWith(`${prefix}/`))) {
1428
+ databases.delete(`thirdparty/${systemDialect}`);
1429
+ }
1430
+ }
1431
+ // A table whose entry no lookup read counts as another database only when
1432
+ // none of the databases it may be in holds another table. Tables of two
1433
+ // systems are never in one database, so it always counts beside a table of
1434
+ // another system.
1435
+ for (const u of unreadTables) {
1436
+ if (![...databases.keys()].some(u.fits))
1437
+ databases.set(u.key, u.entity);
1438
+ }
1439
+ if (databases.size > 1) {
1440
+ const named = [...databases.values()].map(n => `'${n}'`).join(', ');
1441
+ errors.push(`${at} binds entities in more than one database (${named}); a ` +
1442
+ `profile reads from one system a single query can reach`);
1443
+ }
1444
+ const dialect = dialectOfSources(stated);
1445
+ // The expressions of each entity the profile may bind.
1446
+ for (const pe of profile.entities) {
1447
+ const e = byName.get(pe.name);
1448
+ if (!e || e.abstract)
1449
+ continue;
1450
+ const known = fieldsOf.get(pe.name);
1451
+ // A profile field's expression may be anything the model file's could be:
1452
+ // its own table's columns and its own entity's fields, never another
1453
+ // entity's fields, with each qualifier in its entity's case.
1454
+ // A name after a dot is a struct path segment, such as `customer` in
1455
+ // `details.customer.id`, and reads no entity.
1456
+ // These rules check every text the profile wrote, since each must be a
1457
+ // valid field expression wherever it runs; the dependency rules below read
1458
+ // only the text the profile's dialect evaluates. An ANSI_SQL text is read
1459
+ // as the profile's own SQL, and not at all when no lookup read its
1460
+ // dialect.
1461
+ const others = entities.map(x => x.name).filter(n => n !== pe.name);
1462
+ for (const pf of pe.fields ?? []) {
1463
+ // A text with no SQL is reported above, and has nothing to read.
1464
+ const readable = expressionSources(pf).filter(s => (0, loader_1.noSqlReason)(s.text, s.dialect, pe.name) === undefined &&
1465
+ !(s.dialect === 'ANSI_SQL' && dialect === null));
1466
+ for (const { text, dialect: written } of readable) {
1467
+ const columns = (0, sql_expr_utils_1.columnReferences)(text, written === 'ANSI_SQL' && dialect ? dialect : written);
1468
+ if (!columns) {
1469
+ unreadable.add(fieldLabel(pe.name, pf.name));
1470
+ continue;
1471
+ }
1472
+ for (const other of others) {
1473
+ if ((0, sql_expr_utils_1.fieldsReadOn)(columns, other).length) {
1474
+ errors.push(`${at}: field '${pe.name}.${pf.name}' reads entity '${other}'; a field expression reads only its own entity`);
1475
+ }
1476
+ }
1477
+ errors.push(...new Set(columns.flatMap(c => (0, names_1.qualifierCaseError)(`${at}: field '${pe.name}.${pf.name}'`, c, entities.map(x => x.name)) ?? [])));
1478
+ for (const ref of (0, sql_expr_utils_1.fieldsReadOn)(columns, pe.name)) {
1479
+ if (!known.has(ref) && !unknownAncestry.has(pe.name)) {
1480
+ errors.push(`${at}: field '${pe.name}.${pf.name}' reads '${pe.name}.${ref}', which is not a field of '${pe.name}'`);
1481
+ }
1482
+ else if (ref === pf.name) {
1483
+ errors.push(`${at}: ${(0, sql_expr_utils_1.selfReferenceMessage)(pe.name, [pf.name])}`);
1484
+ }
1485
+ }
1486
+ }
1487
+ }
1488
+ }
1489
+ // A profile's key columns are physical column names.
1490
+ for (const pe of profile.entities) {
1491
+ const keyColumns = [...(pe.primaryKey ?? []), ...(pe.uniqueKeys ?? []).flat()];
1492
+ const bad = keyColumns.filter(c => !(0, sql_expr_utils_1.isColumnName)(c));
1493
+ if (bad.length) {
1494
+ errors.push(`${at}: entity '${pe.name}' key column ${bad.map(c => `'${c}'`).join(', ')} is not a physical column ` +
1495
+ `name`);
1496
+ }
1497
+ }
1498
+ // The profile's own keys, where the model file states keys: those name
1499
+ // columns of the model file's table.
1500
+ for (const e of entities) {
1501
+ const pe = bindingOf.get(e.name);
1502
+ if (!pe || e.abstract)
1503
+ continue;
1504
+ const missing = [];
1505
+ if (e.keys.length && !pe.primaryKey?.length)
1506
+ missing.push('primary_key');
1507
+ if (nonEmptyKeys(e.uniqueKeys).length &&
1508
+ !nonEmptyKeys(pe.uniqueKeys).length) {
1509
+ missing.push('unique_keys');
1510
+ }
1511
+ if (missing.length) {
1512
+ errors.push(`${at}: entity '${e.name}' has no ${missing.join(' or ')} in this ` +
1513
+ `profile; the model file's keys are columns of its own table, so ` +
1514
+ `state this profile's keys`);
1515
+ }
1516
+ }
1517
+ // Every relationship bound, by the profile itself: a named profile states
1518
+ // both join column lists, and the model file's do not stand in for them.
1519
+ for (const r of baseModel.relationships ?? []) {
1520
+ if (r.association)
1521
+ continue;
1522
+ const pr = profile.relationships.find(x => x.name === r.name);
1523
+ const from = pr?.fromColumns ?? [];
1524
+ const to = pr?.toColumns ?? [];
1525
+ if (!from.length || !to.length) {
1526
+ errors.push(`${at}: ${profileJoinMessage(r.name)}`);
1527
+ }
1528
+ else if (from.length !== to.length) {
1529
+ errors.push(`${at}: ${(0, loader_1.joinLengthMessage)(r.name, from.length, to.length)}`);
1530
+ }
1531
+ else {
1532
+ const bad = [...from, ...to].filter(c => !(0, sql_expr_utils_1.isColumnName)(c));
1533
+ if (bad.length) {
1534
+ errors.push(`${at}: relationship '${r.name}' join column ${bad.map(c => `'${c}'`).join(', ')} is not a physical ` +
1535
+ `column name`);
1536
+ }
1537
+ }
1538
+ }
1539
+ // Every field the model file leaves unbound is accounted for, and exclusions
1540
+ // are closed under dependency. An exclusion applies to the entity that names
1541
+ // it, not to that entity's descendants.
1542
+ const excluded = new Set();
1543
+ // The entities whose own ancestors do not resolve with the profile applied.
1544
+ // A line on one that restates an inherited field is not judged unbound.
1545
+ const unknownProfiled = unresolvedEntities(profiled);
1546
+ for (const e of entities) {
1547
+ if (e.abstract)
1548
+ continue;
1549
+ const pe = bindingOf.get(e.name);
1550
+ for (const name of pe?.fieldsExclude ?? []) {
1551
+ excluded.add(`${e.name}.${name}`);
1552
+ }
1553
+ // A field the profile lists in `fields`, on this entity or on a concrete
1554
+ // ancestor that has the field, is bound by it. One whose expression the
1555
+ // reader rejected is reported once, as that, where the profile lists it.
1556
+ const listedOn = [e, ...ancestorsOf(e.name, byName).filter(a => !a.abstract)];
1557
+ const rebound = new Set(listedOn.flatMap(x => (bindingOf.get(x.name)?.fields ?? [])
1558
+ .map(f => f.name)
1559
+ .filter(n => x === e || fieldsOf.get(x.name)?.has(n))));
1560
+ for (const f of seenOf.get(e.name)?.values() ?? []) {
1561
+ if (!(0, ir_1.isFieldBound)(f) && !rebound.has(f.name) &&
1562
+ !(unknownProfiled.has(e.name) &&
1563
+ inheritedNamesOf(profiled, e.name).has(f.name))) {
1564
+ errors.push(`${at}: field '${e.name}.${f.name}' has no binding in the model ` +
1565
+ `file; bind it in 'fields' or leave it out in 'fields_exclude'`);
1566
+ }
1567
+ }
1568
+ }
1569
+ // A profile restates or excludes each field whose binding reads a column,
1570
+ // since that column belongs to another table. seenOf leaves out excluded
1571
+ // fields.
1572
+ for (const e of entities) {
1573
+ const pe = bindingOf.get(e.name);
1574
+ if (!pe || e.abstract || unknownProfiled.has(e.name))
1575
+ continue;
1576
+ const restated = new Set((pe.fields ?? []).map(f => f.name));
1577
+ for (const name of seenOf.get(e.name)?.keys() ?? []) {
1578
+ const read = !restated.has(name) &&
1579
+ columnBinding(profiled, e.name, name, dialect);
1580
+ if (!read)
1581
+ continue;
1582
+ const through = read.owner === e.name ?
1583
+ 'its model file expression' :
1584
+ `the expression it inherits from '${read.owner}'`;
1585
+ errors.push(`${at}: field '${e.name}.${name}' reads column '${read.column}' ` +
1586
+ `through ${through}; bind it in 'fields' for this profile, or ` +
1587
+ `leave it out in 'fields_exclude'`);
1588
+ }
1589
+ }
1590
+ // Each field or metric, as "field 'Entity.field'" or "metric 'name'", with
1591
+ // a text this check cannot read that push reads in another dialect: an
1592
+ // ANSI_SQL text, which push reads in its own dialect. Push reads every
1593
+ // other entry in the dialect it is written in, as this check does.
1594
+ const readOtherwise = new Set();
1595
+ const noteUnread = (what, owner, text) => {
1596
+ const asWritten = text.dialect !== 'ANSI_SQL' &&
1597
+ (owner.dialects ?? []).some(d => d.dialect === text.dialect && d.expression === text.text);
1598
+ // The caller warns about an ANSI_SQL text it cannot read either.
1599
+ const callerFails = !!opts.ownSqlOnly &&
1600
+ !(0, sql_expr_utils_1.columnsForSource)(text.text, 'ANSI_SQL', opts.ownSqlOnly.dialect);
1601
+ if (!asWritten && !callerFails)
1602
+ readOtherwise.add(what);
1603
+ };
1604
+ // Every bound field, and every metric the profile keeps, has a text this
1605
+ // profile's database can run, checked once its sources name one database.
1606
+ if (dialect) {
1607
+ for (const e of entities) {
1608
+ if (e.abstract || rejected.has(e.name))
1609
+ continue;
1610
+ for (const f of seenOf.get(e.name)?.values() ?? []) {
1611
+ // validateInlineDialects reports a field inherited unchanged.
1612
+ const asInModelFile = fieldsOf.get(e.name)?.get(f.name);
1613
+ const reported = !!asInModelFile &&
1614
+ JSON.stringify(expressionSources(f)) ===
1615
+ JSON.stringify(expressionSources(asInModelFile)) &&
1616
+ sourcesDialect({ entities: [e] }) === dialect;
1617
+ if (noTextFor(f, [dialect]) && !reported) {
1618
+ errors.push(`${at}: field '${e.name}.${f.name}' has no expression for ${dialect} or ANSI_SQL, and this profile's sources are ${dialect}${importHint(f, dialect, baseModel)}`);
1619
+ }
1620
+ }
1621
+ }
1622
+ if (profile.metricsExclude !== '*') {
1623
+ const excludedMetrics = new Set(profile.metricsExclude ?? []);
1624
+ for (const m of baseModel.metrics ?? []) {
1625
+ if (!excludedMetrics.has(m.name) && noTextFor(m, [dialect])) {
1626
+ const viaImport = importTranspiles(m, dialect, baseModel) ?
1627
+ ` add one with '${importCommand('--transpile', baseModel.version)}',` :
1628
+ '';
1629
+ errors.push(`${at}: metric '${m.name}' has no expression for ${dialect} or ` +
1630
+ `ANSI_SQL, and this profile's sources are ${dialect}; give it ` +
1631
+ `one,${viaImport} or add it to 'metrics_exclude'`);
1632
+ }
1633
+ }
1634
+ }
1635
+ }
1636
+ // Each field that depends on an excluded one, and the excluded field it
1637
+ // reaches.
1638
+ const dangling = new Map();
1639
+ let grew = true;
1640
+ while (grew) {
1641
+ grew = false;
1642
+ for (const e of entities) {
1643
+ if (e.abstract)
1644
+ continue;
1645
+ for (const f of seenOf.get(e.name)?.values() ?? []) {
1646
+ const key = `${e.name}.${f.name}`;
1647
+ if (excluded.has(key) || dangling.has(key))
1648
+ continue;
1649
+ // A field reads only its own entity's fields. An
1650
+ // inherited field is read in the form its declaring ancestor wrote it,
1651
+ // because inheritance strips the qualifier when it copies one down.
1652
+ for (const dep of fieldDependencies(profiled, e.name, f.name, unreadable, dialect, noteUnread)) {
1653
+ const target = `${e.name}.${dep}`;
1654
+ if (excluded.has(target) || dangling.has(target)) {
1655
+ dangling.set(key, dangling.get(target) ?? target);
1656
+ grew = true;
1657
+ break;
1658
+ }
1659
+ }
1660
+ }
1661
+ }
1662
+ }
1663
+ for (const [field, reached] of dangling) {
1664
+ errors.push(`${at}: field '${field}' depends on '${reached}', which this ` +
1665
+ `profile excludes; exclude '${field}' too, or rebind it`);
1666
+ }
1667
+ const why = (target) => excluded.has(target) ?
1668
+ 'which this profile excludes' :
1669
+ `which depends on '${dangling.get(target)}', an excluded field`;
1670
+ if (profile.metricsExclude !== '*') {
1671
+ const metricExcluded = new Set(profile.metricsExclude ?? []);
1672
+ for (const m of baseModel.metrics ?? []) {
1673
+ if (metricExcluded.has(m.name))
1674
+ continue;
1675
+ const read = [];
1676
+ for (const text of textsFor(m, dialect)) {
1677
+ // A text with no SQL has nothing to read.
1678
+ if ((0, loader_1.noSqlReason)(text.text, text.dialect) !== undefined)
1679
+ continue;
1680
+ const columns = (0, sql_expr_utils_1.columnReferences)(text.text, text.dialect);
1681
+ if (columns) {
1682
+ read.push(columns);
1683
+ }
1684
+ else {
1685
+ unreadable.add(metricLabel(m.name));
1686
+ noteUnread(metricLabel(m.name), m, text);
1687
+ }
1688
+ }
1689
+ const reached = [...excluded, ...dangling.keys()].filter(target => {
1690
+ const [te, tf] = target.split('.');
1691
+ return read.some(columns => (0, sql_expr_utils_1.fieldsReadOn)(columns, te).includes(tf));
1692
+ });
1693
+ if (reached.length) {
1694
+ errors.push(`${at}: metric '${m.name}' reaches ${reached.map(r => `'${r}', ${why(r)}`).join('; ')}; add '${m.name}' to 'metrics_exclude'`);
1695
+ }
1696
+ }
1697
+ }
1698
+ // The fields this profile binds, whose SQL the profile writes.
1699
+ const own = new Set(profile.entities.flatMap(pe => (pe.fields ?? []).map(pf => fieldLabel(pe.name, pf.name))));
1700
+ // Whether the caller warns itself about this model file SQL.
1701
+ const caller = opts.ownSqlOnly;
1702
+ const callerWarns = (what) => !!caller &&
1703
+ (caller.dialect === dialect || !readOtherwise.has(what));
1704
+ for (const what of unreadable) {
1705
+ if (!own.has(what) && callerWarns(what)) {
1706
+ continue;
1707
+ }
1708
+ warnings.push(`${at}: kcmd could not read the SQL of ${what}, so the fields it ` +
1709
+ `reads were not checked`);
1710
+ }
1711
+ return errors;
1712
+ }
1713
+ /**
1714
+ * Returns one message for each binding, the model file's included, whose
1715
+ * `to_columns` for a relationship cover no primary or unique key of its
1716
+ * target, or an empty list. A superset of a key covers it (see coversKey).
1717
+ * Keys and join columns are compared as written. A binding whose key or join
1718
+ * columns are not all column names (isColumnName) is left out. `applied` names
1719
+ * the profile the push applies. The "covers no key" check then leaves out that
1720
+ * profile's join, or the model file's when the profile states none.
1721
+ */
1722
+ function validateProfileConsistency(baseModel, profiles, applied) {
1723
+ const errors = [];
1724
+ const entities = baseModel.entities ?? [];
1725
+ const byName = new Map(entities.map(e => [e.name, e]));
1726
+ for (const r of baseModel.relationships ?? []) {
1727
+ if (r.association)
1728
+ continue;
1729
+ const target = byName.get(r.destination.entity);
1730
+ if (!target || target.abstract)
1731
+ continue;
1732
+ const appliedJoin = profiles.find(p => p.name === applied)
1733
+ ?.relationships.find(x => x.name === r.name);
1734
+ // Push's own key check reports this binding's join, so it is left out here.
1735
+ const checkedByPush = applied === undefined ? undefined :
1736
+ appliedJoin?.toColumns.length ? `profile '${applied}'` :
1737
+ 'the model file';
1738
+ // A binding that states no keys is checked against the model file's,
1739
+ // since completeness reports the missing keys. A binding with no join
1740
+ // columns is left out; completeness reports it for a named profile.
1741
+ const record = (who, to, pk, uks) => {
1742
+ // The column name checks report a value that is not a column name.
1743
+ if (!to.length || ![...to, ...pk, ...uks.flat()].every(sql_expr_utils_1.isColumnName)) {
1744
+ return;
1745
+ }
1746
+ if (!(0, ir_1.coversKey)(to, pk, uks) && who !== checkedByPush) {
1747
+ errors.push(`relationship '${r.name}': ${who} joins on to_columns ${JSON.stringify(to)}, which cover no primary or unique key ` +
1748
+ `of '${target.name}'`);
1749
+ }
1750
+ };
1751
+ record('the model file', r.destination.columns, target.keys, target.uniqueKeys ?? []);
1752
+ for (const p of profiles) {
1753
+ const pr = p.relationships.find(x => x.name === r.name);
1754
+ const pe = p.entities.find(e => e.name === target.name);
1755
+ record(`profile '${p.name}'`, pr?.toColumns ?? [], pe?.primaryKey?.length ? pe.primaryKey : target.keys, nonEmptyKeys(pe?.uniqueKeys).length ? pe.uniqueKeys :
1756
+ target.uniqueKeys ?? []);
1757
+ }
1758
+ }
1759
+ return errors;
1760
+ }
1761
+ // The errors the named profiles of each model in `docs` give: each profile
1762
+ // file, and each profile a vanilla model carries in its GOOGLE block, checked
1763
+ // on its own for completeness. An error about a profile file names that file,
1764
+ // and one about a block profile names its label or the model document.
1765
+ // A model's profiles are checked against its model file as loaded on its own,
1766
+ // which `baseByDoc` holds. A file under the reserved name 'default', which no
1767
+ // push deploys, is not read here. Push also checks the
1768
+ // keys and join columns of the model file and of each named profile. The
1769
+ // warnings go to `warnings`. `profileDocs` gives a model's profile files, and
1770
+ // `declared` the profiles a document declares, as push reads them.
1771
+ function namedProfileErrors(docs, baseByDoc, profileDocs, declared, warnings, load) {
1772
+ const errors = [];
1773
+ for (const d of docs) {
1774
+ // Each named profile, with the label its errors carry and a function that
1775
+ // reads it. declared() lists the profile files too, so only its block
1776
+ // profiles are taken from it. A block profile is already read. Its actions
1777
+ // are dropped, as loadProfileFile skips a profile file's.
1778
+ let named;
1779
+ try {
1780
+ named = [
1781
+ ...profileDocs(d.name).map(p => ({
1782
+ name: p.name,
1783
+ label: p.fileName,
1784
+ read: () => loadProfileFile(p.text, p.name, { skipActions: true }),
1785
+ })),
1786
+ ...declared(d).flatMap(p => 'spec' in p ? [{
1787
+ name: p.name,
1788
+ label: p.label ?? d.name,
1789
+ read: () => {
1790
+ const { actions: _, ...spec } = p.spec;
1791
+ return spec;
1792
+ },
1793
+ }] : []),
1794
+ ].filter(p => p.name.toLowerCase() !== exports.DEFAULT_PROFILE);
1795
+ }
1796
+ catch (err) {
1797
+ // A profile file that breaks one of the layout's file rules, or block
1798
+ // profiles a load rejects.
1799
+ errors.push({ error: `${err.message || err}`, document: d.name });
1800
+ continue;
1801
+ }
1802
+ // A document that holds no model has nothing to check its profiles
1803
+ // against.
1804
+ const base = baseByDoc.get(d.name);
1805
+ if (!base)
1806
+ continue;
1807
+ // Actions are out of scope for the preview. Push checks a profile's
1808
+ // actions only when it applies the profile, so these checks leave them
1809
+ // out.
1810
+ const logical = { ...base, actions: [] };
1811
+ // Push warns about the SQL it cannot read in the model file, so these
1812
+ // checks leave out a warning about model file SQL that push reads in the
1813
+ // same dialect. Push stops reading what the SQL reads when the model's
1814
+ // hierarchy does not resolve, and these checks then give every warning.
1815
+ const ownSqlOnly = !inheritanceErrorOf(base) ?
1816
+ { dialect: sourcesDialect(base) } :
1817
+ undefined;
1818
+ const specs = [];
1819
+ for (const p of named) {
1820
+ const at = (m) => `[${p.label}] ${m}`;
1821
+ const tag = (m) => ({ error: at(m), document: d.name, profile: p.name });
1822
+ let spec;
1823
+ try {
1824
+ spec = p.read();
1825
+ }
1826
+ catch (err) {
1827
+ if (!(err instanceof loader_1.SemanticModelError))
1828
+ throw err;
1829
+ errors.push(tag(err.message));
1830
+ continue;
1831
+ }
1832
+ specs.push(spec);
1833
+ const profileWarnings = [];
1834
+ errors.push(...validateProfileCompleteness(logical, spec, profileWarnings, {
1835
+ ownSqlOnly,
1836
+ ...load,
1837
+ }).map(tag));
1838
+ warnings.push(...profileWarnings.map(at));
1839
+ }
1840
+ errors.push(...validateProfileConsistency(logical, specs, exports.DEFAULT_PROFILE)
1841
+ .map(e => ({ error: `[${d.name}] ${e}`, document: d.name })));
1842
+ }
1843
+ return errors;
1844
+ }
1845
+ // Each entity's fields, declared plus inherited, by name. Resolution throws on a
1846
+ // broken hierarchy, which push reports elsewhere. Each entity is then resolved
1847
+ // with only its own ancestors, and one whose own ancestors do not resolve
1848
+ // keeps its declared fields.
1849
+ function fieldsByEntity(model) {
1850
+ let resolved = model.entities ?? [];
1851
+ if (resolved.some(e => e.extends?.length)) {
1852
+ try {
1853
+ resolved = (0, resolve_inheritance_1.resolveInheritance)(model).model.entities ?? resolved;
1854
+ }
1855
+ catch (err) {
1856
+ // Anything but a broken hierarchy is a bug.
1857
+ if (!(err instanceof resolve_inheritance_1.InheritanceError))
1858
+ throw err;
1859
+ resolved = resolved.map(e => resolvedAlone(model, e.name) ?? e);
1860
+ }
1861
+ }
1862
+ // Resolution already leaves excluded fields off; this covers a model with no
1863
+ // inheritance, which is not resolved, and a broken one.
1864
+ return new Map(resolved.map(e => {
1865
+ const excluded = new Set(e.excludedFields ?? []);
1866
+ return [
1867
+ e.name,
1868
+ new Map(e.fields.filter(f => !excluded.has(f.name)).map(f => [f.name, f])),
1869
+ ];
1870
+ }));
1871
+ }
1872
+ // The entity `name` resolved with only its ancestors, or undefined when its
1873
+ // own ancestors do not resolve.
1874
+ function resolvedAlone(model, name) {
1875
+ const byName = new Map((model.entities ?? []).map(e => [e.name, e]));
1876
+ const line = new Set();
1877
+ const queue = [name];
1878
+ while (queue.length) {
1879
+ const n = queue.shift();
1880
+ if (line.has(n))
1881
+ continue;
1882
+ line.add(n);
1883
+ queue.push(...(byName.get(n)?.extends ?? []));
1884
+ }
1885
+ try {
1886
+ const entities = (model.entities ?? []).filter(e => line.has(e.name));
1887
+ return (0, resolve_inheritance_1.resolveInheritance)({ ...model, entities })
1888
+ .model.entities?.find(e => e.name === name);
1889
+ }
1890
+ catch (err) {
1891
+ if (!(err instanceof resolve_inheritance_1.InheritanceError))
1892
+ throw err;
1893
+ return undefined;
1894
+ }
1895
+ }
1896
+ // The entities of `model` whose own ancestors do not resolve, so what they
1897
+ // inherit is not known. Empty when the hierarchy resolves.
1898
+ function unresolvedEntities(model) {
1899
+ if (!inheritanceErrorOf(model))
1900
+ return new Set();
1901
+ return new Set((model.entities ?? [])
1902
+ .filter(e => e.extends?.length &&
1903
+ !resolvedAlone(model, e.name))
1904
+ .map(e => e.name));
1905
+ }
1906
+ // The message of the inheritance error resolving `model` throws, or undefined
1907
+ // when it resolves.
1908
+ function inheritanceErrorOf(model) {
1909
+ if (!(model.entities ?? []).some(e => e.extends?.length))
1910
+ return undefined;
1911
+ try {
1912
+ (0, resolve_inheritance_1.resolveInheritance)(model);
1913
+ return undefined;
1914
+ }
1915
+ catch (err) {
1916
+ if (!(err instanceof resolve_inheritance_1.InheritanceError))
1917
+ throw err;
1918
+ return err.message;
1919
+ }
1920
+ }
1921
+ // How a warning names a field, "field 'Entity.field'". The checks that skip a
1922
+ // warning compare these labels, so each label comes from here.
1923
+ function fieldLabel(entity, field) {
1924
+ return `field '${entity}.${field}'`;
1925
+ }
1926
+ // How a warning names a metric, "metric 'name'".
1927
+ function metricLabel(name) {
1928
+ return `metric '${name}'`;
1929
+ }
1930
+ // The declaration of `field` whose binding `entityName` uses, and the entity
1931
+ // that declares it: the entity's own when it binds the field, otherwise, of
1932
+ // the ancestors that bind it, the one with the most ancestors. Undefined when
1933
+ // none binds it.
1934
+ function boundDeclaration(model, entityName, field) {
1935
+ const byName = new Map((model.entities ?? []).map(e => [e.name, e]));
1936
+ const bindingOn = (name) => byName.get(name)?.fields.find(f => f.name === field && (0, ir_1.isFieldBound)(f));
1937
+ let owner = bindingOn(entityName) ? entityName : undefined;
1938
+ if (!owner) {
1939
+ let depth = -1;
1940
+ for (const a of ancestorsOf(entityName, byName)) {
1941
+ const d = bindingOn(a.name) ? ancestorsOf(a.name, byName).length : -1;
1942
+ if (d > depth)
1943
+ [owner, depth] = [a.name, d];
1944
+ }
1945
+ }
1946
+ return owner ? { owner, field: bindingOn(owner) } : undefined;
1947
+ }
1948
+ // The first column `entityName`'s binding of `field` reads, as written, and
1949
+ // the entity that declares it (see boundDeclaration), or undefined when that
1950
+ // entity is abstract. A column is a reference no entity name qualifies, and an
1951
+ // ANSI_SQL text is read in `dialect` if set, and not at all if it is null.
1952
+ function columnBinding(model, entityName, field, dialect) {
1953
+ const byName = new Map((model.entities ?? []).map(e => [e.name, e]));
1954
+ const bound = boundDeclaration(model, entityName, field);
1955
+ if (!bound || byName.get(bound.owner)?.abstract)
1956
+ return undefined;
1957
+ for (const { text, dialect: written } of expressionSources(bound.field)) {
1958
+ const c = (0, sql_expr_utils_1.columnsForSource)(text, written, dialect)
1959
+ ?.find(c => !byName.has(c.qualifier ?? ''));
1960
+ if (c) {
1961
+ return {
1962
+ owner: bound.owner,
1963
+ column: c.qualifier ? `${c.qualifier}.${c.name}` : c.name,
1964
+ };
1965
+ }
1966
+ }
1967
+ return undefined;
1968
+ }
1969
+ // The fields of its own entity that `field` reads on `entityName`, from the
1970
+ // texts a profile of `dialect` evaluates (see textsFor) as written. The
1971
+ // declaration read is the one boundDeclaration gives, read with its entity's
1972
+ // qualifier.
1973
+ function fieldDependencies(model, entityName, field, unreadable, dialect, onUnread) {
1974
+ const bound = boundDeclaration(model, entityName, field);
1975
+ if (!bound)
1976
+ return [];
1977
+ const name = bound.owner;
1978
+ const deps = new Set();
1979
+ for (const text of textsFor(bound.field, dialect)) {
1980
+ // A text with no SQL has nothing to read.
1981
+ if ((0, loader_1.noSqlReason)(text.text, text.dialect, name) !== undefined)
1982
+ continue;
1983
+ const columns = (0, sql_expr_utils_1.columnReferences)(text.text, text.dialect);
1984
+ if (!columns) {
1985
+ unreadable.add(fieldLabel(name, field));
1986
+ onUnread?.(fieldLabel(name, field), bound.field, text);
1987
+ continue;
1988
+ }
1989
+ for (const dep of (0, sql_expr_utils_1.fieldsReadOn)(columns, name))
1990
+ deps.add(dep);
1991
+ }
1992
+ return [...deps];
1993
+ }
1994
+ // Whether a primary key and unique keys state a key. An empty unique key
1995
+ // counts as none.
1996
+ function statesKey(primaryKey, uniqueKeys) {
1997
+ return !!primaryKey?.length || nonEmptyKeys(uniqueKeys).length > 0;
1998
+ }
1999
+ // The unique keys in `keys` that hold a column. An empty unique key counts as
2000
+ // none.
2001
+ function nonEmptyKeys(keys) {
2002
+ return (keys ?? []).filter(k => k.length > 0);
2003
+ }