@sap/cds-compiler 3.6.0 → 3.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/README.md +3 -0
- package/bin/cdsc.js +9 -5
- package/doc/CHANGELOG_BETA.md +20 -2
- package/doc/CHANGELOG_DEPRECATED.md +2 -2
- package/lib/api/main.js +2 -1
- package/lib/api/options.js +3 -2
- package/lib/base/dictionaries.js +10 -0
- package/lib/base/message-registry.js +56 -12
- package/lib/base/messages.js +39 -20
- package/lib/base/model.js +1 -0
- package/lib/base/shuffle.js +2 -1
- package/lib/checks/elements.js +29 -1
- package/lib/checks/{emptyOrOnlyVirtual.js → hasPersistedElements.js} +9 -5
- package/lib/checks/nonexpandableStructured.js +1 -1
- package/lib/checks/onConditions.js +8 -5
- package/lib/checks/types.js +6 -1
- package/lib/checks/validator.js +7 -3
- package/lib/compiler/assert-consistency.js +20 -23
- package/lib/compiler/base.js +1 -2
- package/lib/compiler/builtins.js +2 -2
- package/lib/compiler/checks.js +237 -242
- package/lib/compiler/define.js +63 -75
- package/lib/compiler/extend.js +325 -22
- package/lib/compiler/finalize-parse-cdl.js +1 -55
- package/lib/compiler/kick-start.js +6 -7
- package/lib/compiler/populate.js +284 -288
- package/lib/compiler/propagator.js +15 -13
- package/lib/compiler/resolve.js +136 -306
- package/lib/compiler/shared.js +42 -44
- package/lib/compiler/tweak-assocs.js +29 -27
- package/lib/compiler/utils.js +29 -3
- package/lib/edm/annotations/genericTranslation.js +7 -13
- package/lib/edm/annotations/preprocessAnnotations.js +3 -0
- package/lib/edm/csn2edm.js +0 -4
- package/lib/edm/edm.js +6 -4
- package/lib/edm/edmAnnoPreprocessor.js +1 -0
- package/lib/edm/edmPreprocessor.js +1 -5
- package/lib/gen/Dictionary.json +34 -2
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +1 -1
- package/lib/gen/languageParser.js +2429 -2401
- package/lib/inspect/inspectPropagation.js +2 -0
- package/lib/json/from-csn.js +87 -41
- package/lib/json/to-csn.js +47 -16
- package/lib/language/errorStrategy.js +1 -0
- package/lib/language/genericAntlrParser.js +109 -28
- package/lib/language/language.g4 +20 -4
- package/lib/model/csnRefs.js +30 -2
- package/lib/model/csnUtils.js +1 -0
- package/lib/model/revealInternalProperties.js +1 -2
- package/lib/modelCompare/compare.js +2 -1
- package/lib/optionProcessor.js +3 -0
- package/lib/render/manageConstraints.js +5 -2
- package/lib/render/toCdl.js +20 -7
- package/lib/render/toHdbcds.js +2 -8
- package/lib/render/toSql.js +6 -5
- package/lib/render/utils/common.js +9 -5
- package/lib/transform/db/assertUnique.js +2 -1
- package/lib/transform/db/expansion.js +2 -0
- package/lib/transform/db/flattening.js +37 -36
- package/lib/transform/db/rewriteCalculatedElements.js +559 -0
- package/lib/transform/db/transformExists.js +15 -6
- package/lib/transform/db/views.js +40 -37
- package/lib/transform/forRelationalDB.js +44 -30
- package/lib/transform/odata/typesExposure.js +50 -15
- package/lib/transform/parseExpr.js +14 -8
- package/lib/transform/transformUtilsNew.js +6 -5
- package/lib/transform/translateAssocsToJoins.js +49 -33
- package/package.json +1 -1
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isBetaEnabled, setProp } = require('../../base/model');
|
|
4
|
+
const { CompilerAssertion } = require('../../base/error');
|
|
5
|
+
const {
|
|
6
|
+
forEachDefinition, applyTransformationsOnNonDictionary, applyTransformationsOnDictionary, implicitAs, cloneCsnNonDict, getUtils,
|
|
7
|
+
} = require('../../model/csnUtils');
|
|
8
|
+
const { getBranches } = require('./flattening');
|
|
9
|
+
const { getColumnMap } = require('./views');
|
|
10
|
+
const cloneCsnOptions = { hiddenPropertiesToClone: [ '_art', '_links', '$env', '$scope' ] };
|
|
11
|
+
/**
|
|
12
|
+
* Rewrite usage of calculated Elements into the expression itself.
|
|
13
|
+
* Delete calculated elements in entities after processing so they don't materialize on the db.
|
|
14
|
+
*
|
|
15
|
+
* @param {CSN.Model} csn
|
|
16
|
+
* @param {CSN.Options} options
|
|
17
|
+
* @param {string} pathDelimiter
|
|
18
|
+
* @param {Function} error
|
|
19
|
+
*/
|
|
20
|
+
function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error ) {
|
|
21
|
+
if (!isBetaEnabled(options, 'calculatedElements'))
|
|
22
|
+
return;
|
|
23
|
+
|
|
24
|
+
const { inspectRef, effectiveType } = getUtils(csn, 'init-all');
|
|
25
|
+
|
|
26
|
+
const views = [];
|
|
27
|
+
const entities = [];
|
|
28
|
+
|
|
29
|
+
// In this first pass, we rewrite all the .value things in tables into their most basic form
|
|
30
|
+
forEachDefinition(csn, (artifact, artifactName) => {
|
|
31
|
+
if (artifact.kind === 'entity') {
|
|
32
|
+
if (artifact.query || artifact.projection) {
|
|
33
|
+
views.push({ artifact, artifactName });
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
rewriteInEntity(artifact);
|
|
37
|
+
entities.push({ artifact, artifactName });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Replace calculated elements in filters (if the root-association element is in an entity).
|
|
43
|
+
// Depends on the first pass!
|
|
44
|
+
entities.forEach(({ artifactName }) => {
|
|
45
|
+
applyTransformationsOnNonDictionary(csn.definitions, artifactName, {
|
|
46
|
+
where: (parent, prop) => {
|
|
47
|
+
applyTransformationsOnNonDictionary(parent, prop, {
|
|
48
|
+
ref: (_parent, _prop, ref, _path, root, index) => {
|
|
49
|
+
if (_parent._art && _parent._art.value) {
|
|
50
|
+
root[index] = _parent._art.value;
|
|
51
|
+
applyTransformationsOnNonDictionary(root, index, {
|
|
52
|
+
ref: (__parent, _, _ref) => {
|
|
53
|
+
if (_ref[0] === '$self' || _ref[0] === '$projection')
|
|
54
|
+
__parent.ref = _ref.slice(-1);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
}, { drillRef: true }, [ 'definitions' ]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
// In this third pass, we process our views, generate .columns if needed and replace usage
|
|
66
|
+
// of calculated elements with their respective `.value`.
|
|
67
|
+
// This depends on the first pass!
|
|
68
|
+
views.forEach(({ artifact, artifactName }) => {
|
|
69
|
+
applyTransformationsOnNonDictionary(csn.definitions, artifactName, {
|
|
70
|
+
SELECT: (parent, prop, SELECT, path) => {
|
|
71
|
+
rewriteInView(SELECT, SELECT.elements || artifact.elements, path);
|
|
72
|
+
},
|
|
73
|
+
projection: (parent, prop, projection, path) => {
|
|
74
|
+
parent.SELECT = projection; // Fake as SELECT so our path below will match in the applyTransformations...
|
|
75
|
+
rewriteInView(parent.SELECT, artifact.elements, path);
|
|
76
|
+
delete parent.SELECT;
|
|
77
|
+
},
|
|
78
|
+
}, {}, [ 'definitions' ]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Last pass, turn .value in tables into a simple val: 1 so we don't need to rewrite/flatten properly - will kill them later
|
|
82
|
+
entities.forEach(({ artifact, artifactName }) => {
|
|
83
|
+
dummifyInEntity(artifact, [ 'definitions', artifactName ]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Rewrite calculated-elements-columns in views/projections and replace them
|
|
88
|
+
* with their "root"-expression.
|
|
89
|
+
*
|
|
90
|
+
* As a first step, we ensure that all views/projections have a .columns (see {@link calculateColumns}) and that
|
|
91
|
+
* all calculated elements are addressed explicitly and not via a * (see {@link makeAllCalculatedElementsExplicitColumns}).
|
|
92
|
+
*
|
|
93
|
+
* Then, we check the `art` of each ref for a `.value` and rewrite accordingly.
|
|
94
|
+
* We need to ensure that the scope of the rewritten expressions is still correct!
|
|
95
|
+
* An `id` in the `.value` needs to point to the entity containing the element,
|
|
96
|
+
* not to some random view element named `id`. See {@link absolutifyPaths} for
|
|
97
|
+
* details on that.
|
|
98
|
+
*
|
|
99
|
+
* @param {CSN.QuerySelect} SELECT
|
|
100
|
+
* @param {CSN.Elements} elements
|
|
101
|
+
* @param {CSN.Path} path
|
|
102
|
+
*/
|
|
103
|
+
function rewriteInView( SELECT, elements, path ) {
|
|
104
|
+
const containsExpandInline = hasExpandInline(SELECT);
|
|
105
|
+
if (!SELECT.columns) // needs to happen for all subqueries!
|
|
106
|
+
calculateColumns(elements, SELECT);
|
|
107
|
+
else
|
|
108
|
+
makeAllCalculatedElementsExplicitColumns(elements, SELECT, containsExpandInline);
|
|
109
|
+
|
|
110
|
+
const name = SELECT.from.args ? undefined : SELECT.from.as || implicitAs(SELECT.from.ref);
|
|
111
|
+
|
|
112
|
+
if (!containsExpandInline) {
|
|
113
|
+
applyTransformationsOnNonDictionary({ SELECT }, 'SELECT', {
|
|
114
|
+
ref: (parent, prop, ref, p, root) => {
|
|
115
|
+
const {
|
|
116
|
+
art, env, links, scope,
|
|
117
|
+
} = getRefInfo(parent, p);
|
|
118
|
+
|
|
119
|
+
if (art?.value) {
|
|
120
|
+
const alias = parent.as || implicitAs(parent.ref);
|
|
121
|
+
// TODO: What about other scopes? expand/inline?
|
|
122
|
+
const value = (scope !== 'ref-target') ? absolutifyPaths(env, art, ref, links, name).value : art.value;
|
|
123
|
+
|
|
124
|
+
// Is a shallow copy enough?
|
|
125
|
+
if (art.value.cast)
|
|
126
|
+
root[p[p.length - 1]] = { xpr: [ value ] };
|
|
127
|
+
else
|
|
128
|
+
root[p[p.length - 1]] = { ...value };
|
|
129
|
+
|
|
130
|
+
if (p[p.length - 2] === 'columns')
|
|
131
|
+
root[p[p.length - 1]].as = alias;
|
|
132
|
+
else
|
|
133
|
+
delete root[p[p.length - 1]].as;
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
}, {}, path);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
*
|
|
142
|
+
* @param {CSN.QuerySelect} SELECT
|
|
143
|
+
* @returns {boolean}
|
|
144
|
+
*/
|
|
145
|
+
function hasExpandInline( SELECT ) {
|
|
146
|
+
if (!SELECT.columns)
|
|
147
|
+
return false;
|
|
148
|
+
|
|
149
|
+
for (let i = 0; i < SELECT.columns.length; i++) {
|
|
150
|
+
const column = SELECT.columns[i];
|
|
151
|
+
if (column.expand || column.inline)
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Replace all nested .value things (in .xpr, in .ref) with their most-direct thing:
|
|
160
|
+
* - A ref to a non-calculated element
|
|
161
|
+
* - A .val
|
|
162
|
+
* - An expression containing the above
|
|
163
|
+
*
|
|
164
|
+
* @param {CSN.Artifact} artifact The artifact currently being processed
|
|
165
|
+
*/
|
|
166
|
+
function rewriteInEntity( artifact ) {
|
|
167
|
+
applyTransformationsOnDictionary(artifact.elements, {
|
|
168
|
+
value: (parent, prop, value) => {
|
|
169
|
+
replaceValuesWithBaseValue(parent, value);
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Iteratively replace all .values with the most-basic form:
|
|
176
|
+
* - a .val thing
|
|
177
|
+
* - a .ref to a non-value thing
|
|
178
|
+
*
|
|
179
|
+
* @param {object | Array} parent
|
|
180
|
+
* @param {object} value
|
|
181
|
+
*/
|
|
182
|
+
function replaceValuesWithBaseValue( parent, value ) {
|
|
183
|
+
if (value.val && parent.value === value)
|
|
184
|
+
return;
|
|
185
|
+
|
|
186
|
+
const stack = [ { parent, value } ];
|
|
187
|
+
while (stack.length > 0) {
|
|
188
|
+
const current = stack.pop();
|
|
189
|
+
|
|
190
|
+
if (current.value.xpr) {
|
|
191
|
+
applyTransformationsOnNonDictionary(current.value, 'xpr', {
|
|
192
|
+
ref: (p, prop, ref, path, root ) => {
|
|
193
|
+
stack.push({
|
|
194
|
+
parent: root,
|
|
195
|
+
value: p,
|
|
196
|
+
isInXpr: true,
|
|
197
|
+
refBase: current.refBase,
|
|
198
|
+
linksBase: current.linksBase,
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
else if (current.value.ref && current.value._art?.value) {
|
|
204
|
+
const linksBase = current.value._links;
|
|
205
|
+
const refBase = current.value.ref;
|
|
206
|
+
const parentIndex = Array.isArray(current.parent) ? current.parent.indexOf(current.value) : -1;
|
|
207
|
+
|
|
208
|
+
replaceInRef(current.parent, current.value._art.value, current.isInXpr, refBase, linksBase, parentIndex);
|
|
209
|
+
|
|
210
|
+
stack.push(Object.assign(current, {
|
|
211
|
+
value: parentIndex > -1 ? current.parent[parentIndex] : current.parent.value,
|
|
212
|
+
refBase,
|
|
213
|
+
linksBase,
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
// No need for cloning here, as we don't rewrite this further and will later on kill all the stuff anyway
|
|
217
|
+
else if (current.value.val) { // this is the base case - or a ref to a non-calculated element
|
|
218
|
+
if (current.isInXpr) { // inside of expressions we directly need the val
|
|
219
|
+
current.parent.val = current.value.val;
|
|
220
|
+
delete current.parent.value;
|
|
221
|
+
}
|
|
222
|
+
else { // outside of expressions, i.e. as normal elements, we need it in a .value wrapper
|
|
223
|
+
current.parent.value = current.value;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* A value referenced via a ref is replaced here
|
|
231
|
+
* - kill the ref
|
|
232
|
+
* - explicitly mention the value
|
|
233
|
+
*
|
|
234
|
+
* We either "trick" it into the correct place in an .xpr or we simply overwrite the existing .ref
|
|
235
|
+
*
|
|
236
|
+
* @param {object} parent
|
|
237
|
+
* @param {object} newValue
|
|
238
|
+
* @param {boolean} isInXpr
|
|
239
|
+
* @param {Array} refBase
|
|
240
|
+
* @param {Array} linksBase
|
|
241
|
+
* @param {number} indexInParent
|
|
242
|
+
*/
|
|
243
|
+
function replaceInRef( parent, newValue, isInXpr, refBase, linksBase, indexInParent ) {
|
|
244
|
+
delete parent.ref;
|
|
245
|
+
const clone = {
|
|
246
|
+
value: cloneCsnNonDict({ value: newValue }, cloneCsnOptions).value,
|
|
247
|
+
};
|
|
248
|
+
const refPrefix = refBase.slice(0, -1);
|
|
249
|
+
const linksPrefix = linksBase.slice(0, -1);
|
|
250
|
+
if (newValue.xpr) {
|
|
251
|
+
// We need to adapt the scope of all refs in the new .xpr, as it might have been at a different "root"
|
|
252
|
+
applyTransformationsOnNonDictionary(clone, 'value', {
|
|
253
|
+
ref: (p, prop, ref) => {
|
|
254
|
+
if (ref[0] !== '$self' && ref[0] !== '$projection') {
|
|
255
|
+
p.ref = [ ...refPrefix, ...ref ];
|
|
256
|
+
if (p._links)
|
|
257
|
+
p._links = [ ...linksPrefix, ...p._links ]; // TODO: Make non-enum, increment idx
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
}, {
|
|
261
|
+
// Do not rewrite refs inside of an association-where; avoids endless loop
|
|
262
|
+
skipStandard: { where: true },
|
|
263
|
+
});
|
|
264
|
+
if (indexInParent > -1) // a .xpr in a .xpr
|
|
265
|
+
parent[indexInParent] = clone.value;
|
|
266
|
+
else
|
|
267
|
+
parent.value = clone.value;
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
if (indexInParent > -1) // a .ref in a .xpr
|
|
271
|
+
parent[indexInParent] = clone.value;
|
|
272
|
+
else
|
|
273
|
+
parent.value = clone.value;
|
|
274
|
+
if (clone.value.ref && clone.value.ref[0] !== '$self' && clone.value.ref[0] !== '$projection' ) {
|
|
275
|
+
clone.value.ref = [ ...refPrefix, ...clone.value.ref ];
|
|
276
|
+
clone.value._links = [ ...linksPrefix, ...clone.value._links ]; // TODO: Make non-enum, increment idx
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* For a `view V as select from E;` or a `entity P as projection on E;` calculate and
|
|
283
|
+
* attach the .columns if they contain a calculated element so we can rewrite them in
|
|
284
|
+
* the later steps.
|
|
285
|
+
*
|
|
286
|
+
* @param {CSN.Elements} elements Artifact elements
|
|
287
|
+
* @param {object} carrier The thing that will "carry" the columns - .SELECT or .projection
|
|
288
|
+
*/
|
|
289
|
+
function calculateColumns( elements, carrier ) {
|
|
290
|
+
carrier.columns = [ '*' ];
|
|
291
|
+
makeAllCalculatedElementsExplicitColumns(elements, carrier, false);
|
|
292
|
+
if (carrier.columns.length === 1 && carrier.columns[0] === '*')
|
|
293
|
+
delete carrier.columns;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
*
|
|
298
|
+
* @param {CSN.QuerySelect} SELECT
|
|
299
|
+
* @returns {object}
|
|
300
|
+
*/
|
|
301
|
+
function getDirectlyAdressableElements( SELECT ) {
|
|
302
|
+
const { from } = SELECT;
|
|
303
|
+
if (from.ref) {
|
|
304
|
+
return from._art.elements;
|
|
305
|
+
}
|
|
306
|
+
else if (from.SELECT) {
|
|
307
|
+
return from.SELECT.elements;
|
|
308
|
+
}
|
|
309
|
+
else if (from.SET) {
|
|
310
|
+
return from.SET.elements || getDirectlyAdressableElements(from.SET.args[0].SELECT);
|
|
311
|
+
}
|
|
312
|
+
else if (from.args) {
|
|
313
|
+
const mergedElements = Object.create(null);
|
|
314
|
+
for (const arg of from.args) {
|
|
315
|
+
if (arg.ref) {
|
|
316
|
+
for (const elementName in arg._art.elements)
|
|
317
|
+
mergedElements[elementName] = arg._art.elements[elementName];
|
|
318
|
+
}
|
|
319
|
+
else if (arg.SELECT) { // TODO: UNION
|
|
320
|
+
for (const elementName in arg.SELECT.elements)
|
|
321
|
+
mergedElements[elementName] = arg.SELECT.elements[elementName];
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
throw new CompilerAssertion(`Unhandled arg type: ${JSON.stringify(arg, null, 2)}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return mergedElements;
|
|
328
|
+
}
|
|
329
|
+
throw new CompilerAssertion(`Unhandled query type: ${JSON.stringify(SELECT, null, 2)}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Ensure that all elements of the query that are calculated elements have an explicit column that we can rewrite.
|
|
334
|
+
* If a field originally comes in via the *, then we need to add an explicit column for it.
|
|
335
|
+
*
|
|
336
|
+
* @param {CSN.Elements} elements
|
|
337
|
+
* @param {CSN.QuerySelect} SELECT
|
|
338
|
+
* @param {boolean} containsExpandInline
|
|
339
|
+
*/
|
|
340
|
+
function makeAllCalculatedElementsExplicitColumns( elements, SELECT, containsExpandInline ) {
|
|
341
|
+
const root = getDirectlyAdressableElements(SELECT);
|
|
342
|
+
const columnMap = getColumnMap( { SELECT });
|
|
343
|
+
const hasStar = SELECT.columns.includes('*');
|
|
344
|
+
const unfoldingMap = {};
|
|
345
|
+
let starContainsCalculated = false;
|
|
346
|
+
let containsCalculated = false;
|
|
347
|
+
for (const name in elements) {
|
|
348
|
+
const originalRef = columnMap[name] && columnMap[name].ref || [ name ];
|
|
349
|
+
|
|
350
|
+
if (columnMap[name] || hasStar) {
|
|
351
|
+
let element;
|
|
352
|
+
if (columnMap[name]?.expand || columnMap[name]?.inline)
|
|
353
|
+
element = elements[name]; // only the direct thing in .elements has the .excluding respected properly!
|
|
354
|
+
else
|
|
355
|
+
element = columnMap[name]?._art || columnMap[name]?._element || root[name] || elements[name];
|
|
356
|
+
const branches = getBranches(element, name, effectiveType, pathDelimiter); // TODO: is our elements[name] really the root[name]?
|
|
357
|
+
if (hasCalculatedLeaf(branches)) {
|
|
358
|
+
containsCalculated = true;
|
|
359
|
+
const columns = [];
|
|
360
|
+
for (const branchName in branches) {
|
|
361
|
+
if (columnMap[branchName]) // Existing column - don't overwrite, we need $env!
|
|
362
|
+
columns.push(columnMap[branchName]);
|
|
363
|
+
else // TODO: Hm, will we have a $env in the leaf of the thing then?
|
|
364
|
+
columns.push({ ref: [ ...originalRef, ...branches[branchName].ref.slice(1) ], as: branchName });
|
|
365
|
+
}
|
|
366
|
+
if (columnMap[name]) {
|
|
367
|
+
unfoldingMap[name] = [ false, [ ...columns ] ];
|
|
368
|
+
}
|
|
369
|
+
else if (hasStar) { // Via * - just append
|
|
370
|
+
starContainsCalculated = true;
|
|
371
|
+
unfoldingMap[name] = [ true, [ ...columns ] ];
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
else if (!columnMap[name] && hasStar) { // Via * - just append
|
|
375
|
+
unfoldingMap[name] = [ true, [ { ref: [ name ] } ] ];
|
|
376
|
+
}
|
|
377
|
+
else { // just a random column - keep
|
|
378
|
+
unfoldingMap[name] = [ false, [ columnMap[name] ] ];
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (containsExpandInline && containsCalculated) {
|
|
384
|
+
error('query-unsupported-calc', SELECT.$path, { '#': 'std' });
|
|
385
|
+
}
|
|
386
|
+
else if (containsCalculated) {
|
|
387
|
+
const newColumns = [];
|
|
388
|
+
if (hasStar && !starContainsCalculated)
|
|
389
|
+
newColumns.push('*');
|
|
390
|
+
for (const name in elements) {
|
|
391
|
+
const [ isViaStar, columns ] = unfoldingMap[name];
|
|
392
|
+
if (isViaStar && starContainsCalculated || !isViaStar)
|
|
393
|
+
newColumns.push(...columns);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
SELECT.columns = newColumns;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
*
|
|
403
|
+
* @param {object} branches
|
|
404
|
+
* @returns {boolean}
|
|
405
|
+
*/
|
|
406
|
+
function hasCalculatedLeaf( branches ) {
|
|
407
|
+
for (const branchName in branches) {
|
|
408
|
+
const branch = branches[branchName].steps;
|
|
409
|
+
const leaf = branch[branch.length - 1];
|
|
410
|
+
if (hasValue(leaf))
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* A leaf can reference a column which in turn references a real element - that might have a .value.
|
|
419
|
+
* Find such cases.
|
|
420
|
+
*
|
|
421
|
+
* @param {object} baseLeaf Leaf to start at
|
|
422
|
+
* @returns {boolean}
|
|
423
|
+
*/
|
|
424
|
+
function hasValue( baseLeaf ) {
|
|
425
|
+
const visited = new WeakSet();
|
|
426
|
+
const stack = [ baseLeaf ];
|
|
427
|
+
while (stack.length > 0) {
|
|
428
|
+
const leaf = stack.pop();
|
|
429
|
+
if (!visited.has(leaf)) { // Don't re-process things
|
|
430
|
+
if (leaf.value)
|
|
431
|
+
return true;
|
|
432
|
+
else if (leaf._art)
|
|
433
|
+
stack.push(leaf._art);
|
|
434
|
+
else if (leaf['@Core.Computed'] && leaf._column && leaf._column !== baseLeaf)
|
|
435
|
+
stack.push(leaf._column);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
visited.add(leaf);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* In order to just replace them in views, our calculated elements need to reference absolute things, i.e. have a table alias in front!
|
|
447
|
+
*
|
|
448
|
+
* @param {string | object} env
|
|
449
|
+
* @param {object} art
|
|
450
|
+
* @param {Array} artRef
|
|
451
|
+
* @param {Array} artLinks
|
|
452
|
+
* @param {string|undefined} name
|
|
453
|
+
* @todo this is probably very wonky and will break with some view hierarchy stuff etc!
|
|
454
|
+
* @returns {object}
|
|
455
|
+
*/
|
|
456
|
+
function absolutifyPaths( env, art, artRef, artLinks, name ) {
|
|
457
|
+
const clone = { value: cloneCsnNonDict(art.value, cloneCsnOptions) };
|
|
458
|
+
applyTransformationsOnNonDictionary(clone, 'value', {
|
|
459
|
+
ref: (parent, prop, ref) => {
|
|
460
|
+
const artifactName = typeof env === 'string' ? env : name;
|
|
461
|
+
if (parent._links) {
|
|
462
|
+
if (parent._links[0].art.kind !== 'entity') {
|
|
463
|
+
if (artLinks[0].art.kind === 'entity' || artifactName === undefined) {
|
|
464
|
+
parent.ref = [ ...artRef.slice(0, -1), ...ref ];
|
|
465
|
+
setProp(parent, '_links', [ ...artLinks.slice(0, -1), ...parent._links ]); // TODO: increment idx
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
parent.ref = [ artifactName, ...artRef.slice(0, -1), ...ref ];
|
|
469
|
+
setProp(parent, '_links', [ { idx: 0 }, ...artLinks.slice(0, -1), ...parent._links ]); // TODO: increment idx
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
else if (parent.$scope === '$self') {
|
|
473
|
+
if (artifactName !== undefined)
|
|
474
|
+
parent.ref[0] = artifactName;
|
|
475
|
+
else
|
|
476
|
+
parent.ref = parent.ref.slice(-1);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
}, {
|
|
481
|
+
skipStandard: { where: true }, // Do not rewrite refs inside of an association-where
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
return clone;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Get the ref-info
|
|
489
|
+
* - either the cached _art etc.
|
|
490
|
+
* - or calculate using inspectRef
|
|
491
|
+
*
|
|
492
|
+
* @param {object} parent
|
|
493
|
+
* @param {CSN.Path} path
|
|
494
|
+
* @returns {object}
|
|
495
|
+
*/
|
|
496
|
+
function getRefInfo( parent, path ) {
|
|
497
|
+
if (parent._art) {
|
|
498
|
+
return {
|
|
499
|
+
art: parent._art,
|
|
500
|
+
env: parent.$env,
|
|
501
|
+
links: parent._links,
|
|
502
|
+
scope: parent.$scope,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return inspectRef(path);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
*
|
|
512
|
+
* @param {CSN.Model} csn
|
|
513
|
+
* @param {CSN.Options} options
|
|
514
|
+
*/
|
|
515
|
+
function processCalculatedElementsInEntities( csn, options ) {
|
|
516
|
+
if (!isBetaEnabled(options, 'calculatedElements'))
|
|
517
|
+
return;
|
|
518
|
+
forEachDefinition(csn, (artifact, artifactName) => {
|
|
519
|
+
if (artifact.kind === 'entity' && !(artifact.query || artifact.projection))
|
|
520
|
+
killInEntity(artifact, [ 'definitions', artifactName ]);
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* In an entity, remove all instances of calculated elements.
|
|
527
|
+
*
|
|
528
|
+
* @param {CSN.Artifact} artifact
|
|
529
|
+
* @param {CSN.Path} path
|
|
530
|
+
* @todo calculated elements that "live" on the database?
|
|
531
|
+
* @todo error when artifact is empty afterwards? Probably better as a CSN check!
|
|
532
|
+
*/
|
|
533
|
+
function killInEntity( artifact, path ) {
|
|
534
|
+
applyTransformationsOnDictionary(artifact.elements, {
|
|
535
|
+
value: (parent, prop, value, p, root) => {
|
|
536
|
+
delete root[p[p.length - 1]];
|
|
537
|
+
},
|
|
538
|
+
}, {}, path);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* In an entity, turn all instances of calculated elements into an = 1. This way,
|
|
543
|
+
* we don't have to rewrite any scope there and can kill them after A2J, see {@link processCalculatedElementsInEntities}.
|
|
544
|
+
*
|
|
545
|
+
* @param {CSN.Artifact} artifact
|
|
546
|
+
* @param {CSN.Path} path
|
|
547
|
+
*/
|
|
548
|
+
function dummifyInEntity( artifact, path ) {
|
|
549
|
+
applyTransformationsOnDictionary(artifact.elements, {
|
|
550
|
+
value: (parent) => {
|
|
551
|
+
parent.value = { val: 1 };
|
|
552
|
+
},
|
|
553
|
+
}, {}, path);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
module.exports = {
|
|
557
|
+
rewriteCalculatedElementsInViews,
|
|
558
|
+
processCalculatedElementsInEntities,
|
|
559
|
+
};
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
const { forAllQueries, forEachDefinition, walkCsnPath } = require('../../model/csnUtils');
|
|
4
4
|
const { setProp } = require('../../base/model');
|
|
5
5
|
const { getRealName } = require('../../render/utils/common');
|
|
6
|
-
const { csnRefs } = require('../../model/csnRefs');
|
|
7
6
|
const { ModelError } = require('../../base/error');
|
|
8
7
|
|
|
9
8
|
/**
|
|
@@ -47,9 +46,11 @@ const { ModelError } = require('../../base/error');
|
|
|
47
46
|
* @param {CSN.Model} csn
|
|
48
47
|
* @param {CSN.Options} options
|
|
49
48
|
* @param {Function} error
|
|
49
|
+
* @param {Function} inspectRef
|
|
50
|
+
* @param {Function} initDefinition
|
|
51
|
+
* @param {Function} dropDefinitionCache
|
|
50
52
|
*/
|
|
51
|
-
function handleExists( csn, options, error ) {
|
|
52
|
-
let { inspectRef } = csnRefs(csn);
|
|
53
|
+
function handleExists( csn, options, error, inspectRef, initDefinition, dropDefinitionCache ) {
|
|
53
54
|
const generatedExists = new WeakMap();
|
|
54
55
|
forEachDefinition(csn, (artifact, artifactName) => {
|
|
55
56
|
if (artifact.projection) // do the same hack we do for the other stuff...
|
|
@@ -78,15 +79,19 @@ function handleExists( csn, options, error ) {
|
|
|
78
79
|
|
|
79
80
|
while (toProcess.length > 0) {
|
|
80
81
|
const [ queryPath, exprPath ] = toProcess.pop();
|
|
82
|
+
// Re-init caches for this artifact
|
|
83
|
+
dropDefinitionCache(artifact);
|
|
84
|
+
initDefinition(artifact);
|
|
81
85
|
// leftovers can happen with nested exists - we then need to drill down into the created SELECT
|
|
82
86
|
// to check for further exists
|
|
83
87
|
const { result, leftovers } = processExists(queryPath, exprPath);
|
|
84
88
|
walkCsnPath(csn, exprPath.slice(0, -1))[exprPath[exprPath.length - 1]] = result;
|
|
85
|
-
if (leftovers.length > 0)
|
|
86
|
-
inspectRef = csnRefs(csn).inspectRef; // Refresh caches - we need to resolve stuff in the newly created subquery
|
|
87
89
|
leftovers.reverse();
|
|
88
90
|
toProcess.push(...leftovers); // any leftovers - schedule for further processing
|
|
89
91
|
}
|
|
92
|
+
// Make sure we leave csnRefs usable
|
|
93
|
+
dropDefinitionCache(artifact);
|
|
94
|
+
initDefinition(artifact);
|
|
90
95
|
}
|
|
91
96
|
}, [ 'definitions', artifactName, 'query' ]);
|
|
92
97
|
}
|
|
@@ -719,6 +724,10 @@ function handleExists( csn, options, error ) {
|
|
|
719
724
|
where: [],
|
|
720
725
|
},
|
|
721
726
|
};
|
|
727
|
+
|
|
728
|
+
setProp(subselect.SELECT.from, '_art', csn.definitions[target]);
|
|
729
|
+
setProp(subselect.SELECT.from, '_links', [ { idx: 0, art: csn.definitions[target] } ]);
|
|
730
|
+
|
|
722
731
|
// Because the generated things don't have _links, _art etc. set
|
|
723
732
|
// We could also make getParent more robust to calculate the links JIT if they are missing
|
|
724
733
|
generatedExists.set(subselect, true);
|
|
@@ -764,7 +773,7 @@ function handleExists( csn, options, error ) {
|
|
|
764
773
|
*/
|
|
765
774
|
function remapExistingWhere( target, where ) {
|
|
766
775
|
return where.map((part) => {
|
|
767
|
-
if (part.ref) {
|
|
776
|
+
if (part.ref && part.$scope !== '$magic') {
|
|
768
777
|
part.ref = [ target, ...part.ref ];
|
|
769
778
|
return part;
|
|
770
779
|
}
|