@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.
Files changed (70) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +3 -0
  3. package/bin/cdsc.js +9 -5
  4. package/doc/CHANGELOG_BETA.md +20 -2
  5. package/doc/CHANGELOG_DEPRECATED.md +2 -2
  6. package/lib/api/main.js +2 -1
  7. package/lib/api/options.js +3 -2
  8. package/lib/base/dictionaries.js +10 -0
  9. package/lib/base/message-registry.js +56 -12
  10. package/lib/base/messages.js +39 -20
  11. package/lib/base/model.js +1 -0
  12. package/lib/base/shuffle.js +2 -1
  13. package/lib/checks/elements.js +29 -1
  14. package/lib/checks/{emptyOrOnlyVirtual.js → hasPersistedElements.js} +9 -5
  15. package/lib/checks/nonexpandableStructured.js +1 -1
  16. package/lib/checks/onConditions.js +8 -5
  17. package/lib/checks/types.js +6 -1
  18. package/lib/checks/validator.js +7 -3
  19. package/lib/compiler/assert-consistency.js +20 -23
  20. package/lib/compiler/base.js +1 -2
  21. package/lib/compiler/builtins.js +2 -2
  22. package/lib/compiler/checks.js +237 -242
  23. package/lib/compiler/define.js +63 -75
  24. package/lib/compiler/extend.js +325 -22
  25. package/lib/compiler/finalize-parse-cdl.js +1 -55
  26. package/lib/compiler/kick-start.js +6 -7
  27. package/lib/compiler/populate.js +284 -288
  28. package/lib/compiler/propagator.js +15 -13
  29. package/lib/compiler/resolve.js +136 -306
  30. package/lib/compiler/shared.js +42 -44
  31. package/lib/compiler/tweak-assocs.js +29 -27
  32. package/lib/compiler/utils.js +29 -3
  33. package/lib/edm/annotations/genericTranslation.js +7 -13
  34. package/lib/edm/annotations/preprocessAnnotations.js +3 -0
  35. package/lib/edm/csn2edm.js +0 -4
  36. package/lib/edm/edm.js +6 -4
  37. package/lib/edm/edmAnnoPreprocessor.js +1 -0
  38. package/lib/edm/edmPreprocessor.js +1 -5
  39. package/lib/gen/Dictionary.json +34 -2
  40. package/lib/gen/language.checksum +1 -1
  41. package/lib/gen/language.interp +1 -1
  42. package/lib/gen/languageParser.js +2429 -2401
  43. package/lib/inspect/inspectPropagation.js +2 -0
  44. package/lib/json/from-csn.js +87 -41
  45. package/lib/json/to-csn.js +47 -16
  46. package/lib/language/errorStrategy.js +1 -0
  47. package/lib/language/genericAntlrParser.js +109 -28
  48. package/lib/language/language.g4 +20 -4
  49. package/lib/model/csnRefs.js +30 -2
  50. package/lib/model/csnUtils.js +1 -0
  51. package/lib/model/revealInternalProperties.js +1 -2
  52. package/lib/modelCompare/compare.js +2 -1
  53. package/lib/optionProcessor.js +3 -0
  54. package/lib/render/manageConstraints.js +5 -2
  55. package/lib/render/toCdl.js +20 -7
  56. package/lib/render/toHdbcds.js +2 -8
  57. package/lib/render/toSql.js +6 -5
  58. package/lib/render/utils/common.js +9 -5
  59. package/lib/transform/db/assertUnique.js +2 -1
  60. package/lib/transform/db/expansion.js +2 -0
  61. package/lib/transform/db/flattening.js +37 -36
  62. package/lib/transform/db/rewriteCalculatedElements.js +559 -0
  63. package/lib/transform/db/transformExists.js +15 -6
  64. package/lib/transform/db/views.js +40 -37
  65. package/lib/transform/forRelationalDB.js +44 -30
  66. package/lib/transform/odata/typesExposure.js +50 -15
  67. package/lib/transform/parseExpr.js +14 -8
  68. package/lib/transform/transformUtilsNew.js +6 -5
  69. package/lib/transform/translateAssocsToJoins.js +49 -33
  70. 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
  }