@google-cloud/kcmd 0.0.0-stage → 0.1.0

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