@sap/cds-compiler 3.9.4 → 4.0.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 (94) hide show
  1. package/CHANGELOG.md +92 -4
  2. package/README.md +0 -1
  3. package/bin/cdsc.js +11 -23
  4. package/bin/cdsse.js +3 -3
  5. package/doc/API.md +5 -0
  6. package/doc/CHANGELOG_ARCHIVE.md +1 -1
  7. package/doc/CHANGELOG_BETA.md +17 -1
  8. package/doc/CHANGELOG_DEPRECATED.md +28 -0
  9. package/lib/api/.eslintrc.json +1 -1
  10. package/lib/api/main.js +26 -8
  11. package/lib/api/options.js +2 -0
  12. package/lib/base/error.js +2 -0
  13. package/lib/base/message-registry.js +143 -64
  14. package/lib/base/messages.js +213 -107
  15. package/lib/base/model.js +11 -11
  16. package/lib/checks/.eslintrc.json +1 -1
  17. package/lib/checks/annotationsOData.js +2 -2
  18. package/lib/checks/elements.js +1 -1
  19. package/lib/checks/enricher.js +26 -3
  20. package/lib/checks/onConditions.js +67 -12
  21. package/lib/checks/queryNoDbArtifacts.js +106 -105
  22. package/lib/checks/sql-snippets.js +2 -0
  23. package/lib/checks/types.js +12 -6
  24. package/lib/checks/validator.js +2 -2
  25. package/lib/compiler/assert-consistency.js +10 -8
  26. package/lib/compiler/builtins.js +8 -2
  27. package/lib/compiler/checks.js +52 -35
  28. package/lib/compiler/define.js +31 -26
  29. package/lib/compiler/extend.js +120 -65
  30. package/lib/compiler/finalize-parse-cdl.js +12 -43
  31. package/lib/compiler/generate.js +16 -5
  32. package/lib/compiler/index.js +8 -5
  33. package/lib/compiler/kick-start.js +4 -3
  34. package/lib/compiler/populate.js +96 -95
  35. package/lib/compiler/propagator.js +7 -8
  36. package/lib/compiler/resolve.js +377 -103
  37. package/lib/compiler/shared.js +794 -517
  38. package/lib/compiler/tweak-assocs.js +8 -6
  39. package/lib/compiler/utils.js +44 -0
  40. package/lib/edm/annotations/genericTranslation.js +12 -4
  41. package/lib/edm/csn2edm.js +34 -32
  42. package/lib/edm/edm.js +34 -31
  43. package/lib/edm/edmAnnoPreprocessor.js +0 -23
  44. package/lib/edm/edmInboundChecks.js +7 -2
  45. package/lib/edm/edmPreprocessor.js +18 -17
  46. package/lib/edm/edmUtils.js +8 -4
  47. package/lib/gen/Dictionary.json +18 -0
  48. package/lib/gen/language.checksum +1 -1
  49. package/lib/gen/language.interp +4 -2
  50. package/lib/gen/languageParser.js +5006 -4582
  51. package/lib/json/from-csn.js +157 -112
  52. package/lib/json/to-csn.js +60 -89
  53. package/lib/language/antlrParser.js +17 -13
  54. package/lib/language/docCommentParser.js +11 -1
  55. package/lib/language/genericAntlrParser.js +13 -10
  56. package/lib/language/language.g4 +168 -97
  57. package/lib/main.d.ts +128 -36
  58. package/lib/main.js +1 -1
  59. package/lib/model/csnRefs.js +24 -5
  60. package/lib/model/csnUtils.js +9 -8
  61. package/lib/model/revealInternalProperties.js +7 -12
  62. package/lib/modelCompare/compare.js +1 -1
  63. package/lib/modelCompare/utils/filter.js +40 -2
  64. package/lib/optionProcessor.js +0 -3
  65. package/lib/render/toCdl.js +247 -214
  66. package/lib/render/toHdbcds.js +197 -181
  67. package/lib/render/toSql.js +325 -289
  68. package/lib/render/utils/common.js +42 -4
  69. package/lib/render/utils/delta.js +1 -1
  70. package/lib/render/utils/sql.js +3 -3
  71. package/lib/transform/braceExpression.js +2 -2
  72. package/lib/transform/db/.eslintrc.json +1 -1
  73. package/lib/transform/db/applyTransformations.js +3 -3
  74. package/lib/transform/db/associations.js +24 -12
  75. package/lib/transform/db/expansion.js +17 -18
  76. package/lib/transform/db/flattening.js +17 -21
  77. package/lib/transform/db/rewriteCalculatedElements.js +171 -64
  78. package/lib/transform/db/views.js +3 -4
  79. package/lib/transform/draft/db.js +21 -12
  80. package/lib/transform/draft/odata.js +4 -0
  81. package/lib/transform/forOdataNew.js +11 -10
  82. package/lib/transform/forRelationalDB.js +12 -7
  83. package/lib/transform/localized.js +4 -2
  84. package/lib/transform/odata/toFinalBaseType.js +5 -5
  85. package/lib/transform/odata/typesExposure.js +3 -3
  86. package/lib/transform/parseExpr.js +3 -0
  87. package/lib/transform/transformUtilsNew.js +43 -23
  88. package/lib/transform/translateAssocsToJoins.js +7 -6
  89. package/lib/transform/universalCsn/.eslintrc.json +1 -1
  90. package/lib/transform/universalCsn/coreComputed.js +7 -5
  91. package/lib/transform/universalCsn/universalCsnEnricher.js +12 -12
  92. package/package.json +2 -2
  93. package/share/messages/{duplicate-autoexposed.md → def-duplicate-autoexposed.md} +5 -1
  94. package/share/messages/message-explanations.json +1 -1
@@ -4,17 +4,19 @@ const { setProp } = require('../../base/model');
4
4
  const { CompilerAssertion } = require('../../base/error');
5
5
  const {
6
6
  forEachDefinition, applyTransformationsOnNonDictionary, applyTransformationsOnDictionary, implicitAs, cloneCsnNonDict, getUtils,
7
+ forEachMemberRecursively,
7
8
  } = require('../../model/csnUtils');
8
9
  const { getBranches } = require('./flattening');
9
10
  const { getColumnMap } = require('./views');
11
+ const { checkForeignKeyAccess } = require('../../checks/onConditions');
10
12
 
11
13
  const cloneCsnOptions = { hiddenPropertiesToClone: [ '_art', '_links', '$env', '$scope' ] };
14
+
15
+
12
16
  /**
13
17
  * Rewrite usage of calculated Elements into the expression itself.
14
18
  * Delete calculated elements in entities after processing so they don't materialize on the db.
15
19
  *
16
- * TODO: Calculated elements on-write (`stored: true`)
17
- *
18
20
  * @param {CSN.Model} csn
19
21
  * @param {CSN.Options} options
20
22
  * @param {string} pathDelimiter
@@ -32,7 +34,7 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
32
34
  if (artifact.query || artifact.projection) {
33
35
  views.push({ artifact, artifactName });
34
36
  }
35
- else {
37
+ else if (artifact.elements) { // can happen with CSN input
36
38
  rewriteInEntity(artifact);
37
39
  entities.push({ artifact, artifactName });
38
40
  }
@@ -44,8 +46,17 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
44
46
  entities.forEach(({ artifactName }) => {
45
47
  applyTransformationsOnNonDictionary(csn.definitions, artifactName, {
46
48
  ref: (_parent, _prop, ref, _path, root, index) => {
47
- if (_parent._art && _parent._art.value) {
48
- root[index] = _parent._art.value;
49
+ if (_parent._art?.value && !_parent._art.value.stored) {
50
+ if (_parent._art.value.ref) {
51
+ // Ensure that we don't break any navigation by only replacing the real element at the end
52
+ const leafLength = getLeafLength(_parent._links);
53
+ root[index].ref = [ ...root[index].ref.slice(0, -1 * leafLength), ..._parent._art.value.ref ];
54
+ setProp(root[index], '_links', [ ...root[index]._links.slice(0, leafLength), ..._parent._art.value._links ]);
55
+ setProp(root[index], '_art', _parent._art);
56
+ }
57
+ else {
58
+ root[index] = _parent._art.value;
59
+ }
49
60
  // Note: Depends on A2J rejecting deeply nested filters
50
61
  applyTransformationsOnNonDictionary(root, index, {
51
62
  ref: (__parent, _, _ref) => {
@@ -55,7 +66,11 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
55
66
  });
56
67
  }
57
68
  },
58
- }, { drillRef: true }, [ 'definitions' ]);
69
+ }, {
70
+ drillRef: true,
71
+ // skip "type" to avoid going into type.ref
72
+ skipStandard: { type: 1 },
73
+ }, [ 'definitions' ]);
59
74
  });
60
75
 
61
76
 
@@ -75,11 +90,91 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
75
90
  }, {}, [ 'definitions' ]);
76
91
  });
77
92
 
78
- // Last pass, turn .value in tables into a simple val: 1 so we don't need to rewrite/flatten properly - will kill them later
93
+ /**
94
+ * Check that all paths in calculated elements-on write either access normal fields
95
+ * (structures are already rejected by the compiler) or access the foreign keys of
96
+ * associations. Non-fk fields must be rejected.
97
+ * Filters and parameters are not allowed.
98
+ *
99
+ * Note: This coding is similar to checks/onConditions.js, but does not check $self or other
100
+ * ON-condition related stuff.
101
+ *
102
+ * @param {object} parent
103
+ * @param {(string|object)[]} value
104
+ * @param {CSN.Path} csnPath
105
+ */
106
+ function checkPathsInStoredCalcElement( parent, value, csnPath ) {
107
+ const { _links } = parent;
108
+
109
+ // If there is only one path step, it's been checked before.
110
+ for (let i = 0; i < value.length - 1; ++i) {
111
+ let hasPathError = false;
112
+ const step = value[i];
113
+ const stepArt = _links[i].art;
114
+
115
+ if (stepArt.target) {
116
+ const id = step.id || step;
117
+ if (stepArt.on) {
118
+ // It's an unmanaged association - traversal is always forbidden
119
+ error('ref-unexpected-navigation', csnPath, { '#': 'calc-unmanaged', id, elemref: parent });
120
+ hasPathError = true;
121
+ }
122
+ else {
123
+ // It's a managed association - access of the foreign keys is allowed
124
+ checkForeignKeyAccess(parent, i, csnPath, (errorIndex) => {
125
+ error('ref-unexpected-navigation', csnPath, {
126
+ '#': 'calc-non-fk', id, elemref: parent, name: value[errorIndex].id || value[errorIndex],
127
+ });
128
+ hasPathError = true;
129
+ });
130
+ }
131
+ }
132
+ if (typeof step === 'object') {
133
+ if (step.where) {
134
+ error('ref-unexpected-filter', csnPath, { '#': 'calc', elemref: parent });
135
+ hasPathError = true;
136
+ }
137
+ if (step.args) {
138
+ error('ref-unexpected-args', csnPath, { '#': 'calc', elemref: parent });
139
+ hasPathError = true;
140
+ }
141
+ }
142
+ if (hasPathError)
143
+ break; // avoid too many consequent errors
144
+ }
145
+ }
146
+
147
+ // Last pass, turn .value in tables into a simple 'val' so we don't need to rewrite/flatten properly - will kill them later
79
148
  entities.forEach(({ artifact, artifactName }) => {
80
149
  dummifyInEntity(artifact, [ 'definitions', artifactName ]);
150
+
151
+ forEachMemberRecursively({ elements: artifact.elements }, (element, elementName, _prop, path) => {
152
+ if (element.value?.stored) {
153
+ applyTransformationsOnNonDictionary(element, 'value', {
154
+ ref(parent, prop, value, csnPath) {
155
+ checkPathsInStoredCalcElement(parent, value, csnPath);
156
+ },
157
+ }, {}, path);
158
+ }
159
+ }, [ 'definitions', artifactName ]);
81
160
  });
82
161
 
162
+ /**
163
+ * Get the length of the effective leaf - since structures are not flat yet it might be more than 1.
164
+ * Walk from the back until we find the first association/composition.
165
+ *
166
+ * @param {object[]} links
167
+ * @returns {number}
168
+ */
169
+ function getLeafLength( links ) {
170
+ for (let i = links.length - 1; i >= 0; i--) {
171
+ const { art } = links[i];
172
+ if (art.target)
173
+ return links.length - i - 1;
174
+ }
175
+ return links.length;
176
+ }
177
+
83
178
  /**
84
179
  * Rewrite calculated-elements-columns in views/projections and replace them
85
180
  * with their "root"-expression.
@@ -173,11 +268,33 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
173
268
  * @param {CSN.Artifact} artifact The artifact currently being processed
174
269
  */
175
270
  function rewriteInEntity( artifact ) {
271
+ let reorderElements = false;
176
272
  applyTransformationsOnDictionary(artifact.elements, {
177
273
  value: (parent, prop, value) => {
178
- replaceValuesWithBaseValue(parent, value);
274
+ if (value.stored)
275
+ reorderElements = true;
276
+ replaceValuesWithBaseValue(parent);
179
277
  },
180
278
  });
279
+ // on-write must appear at the end of the elements. Order of the on-write between themselves
280
+ // should be as written.
281
+ if (reorderElements) {
282
+ const newElements = Object.create(null);
283
+ const onWrite = [];
284
+ for (const name in artifact.elements) {
285
+ const element = artifact.elements[name];
286
+ if (element.value?.stored)
287
+ onWrite.push(name);
288
+ else
289
+ newElements[name] = element;
290
+ }
291
+ // Add the on-write to the end
292
+ onWrite.forEach((name) => {
293
+ newElements[name] = artifact.elements[name];
294
+ });
295
+
296
+ artifact.elements = newElements;
297
+ }
181
298
  }
182
299
 
183
300
  /**
@@ -185,14 +302,13 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
185
302
  * - a .val thing
186
303
  * - a .ref to a non-value thing
187
304
  *
188
- * @param {object | Array} parent
189
- * @param {object} value
305
+ * @param {object} parent
190
306
  */
191
- function replaceValuesWithBaseValue( parent, value ) {
192
- if (value.val && parent.value === value)
193
- return;
307
+ function replaceValuesWithBaseValue( parent ) {
308
+ if (parent.value.val !== undefined)
309
+ return; // literal; no need to traverse
194
310
 
195
- const stack = [ { parent, value } ];
311
+ const stack = [ { parent, value: parent.value } ];
196
312
  while (stack.length > 0) {
197
313
  const current = stack.pop();
198
314
 
@@ -209,25 +325,20 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
209
325
  },
210
326
  });
211
327
  }
212
- else if (current.value.ref && current.value._art?.value) {
213
- // TODO: Check for calculated elements on-write
328
+ else if (current.value.ref && current.value._art?.value && !current.value._art?.value.stored) {
214
329
  const linksBase = current.value._links;
215
330
  const refBase = current.value.ref;
216
- const parentIndex = Array.isArray(current.parent) ? current.parent.indexOf(current.value) : -1;
217
-
218
- replaceInRef(current.parent, current.value._art.value, current.isInXpr, refBase, linksBase, parentIndex);
219
-
220
- stack.push(Object.assign(current, {
221
- value: parentIndex > -1 ? current.parent[parentIndex] : current.parent.value,
222
- refBase,
223
- linksBase,
224
- }));
331
+ const newValue = replaceInRef(current.value, current.value._art.value, current.isInXpr, refBase, linksBase);
332
+ const prop = Array.isArray(current.parent) ? current.parent.indexOf(current.value) : 'value';
333
+ if (prop === -1)
334
+ throw new CompilerAssertion('Calculated Elements: Value not in parent; should never happen!');
335
+ current.parent[prop] = newValue;
336
+ stack.push(Object.assign(current, { value: newValue, refBase, linksBase }));
225
337
  }
226
- // No need for cloning here, as we don't rewrite this further and will later on kill all the stuff anyway
227
- else if (current.value.val) { // this is the base case - or a ref to a non-calculated element
338
+ else if (current.value.val) {
228
339
  if (current.isInXpr) { // inside of expressions we directly need the val
229
340
  current.parent.val = current.value.val;
230
- delete current.parent.value;
341
+ delete current.parent.value; // TODO: current.parent could be an array!
231
342
  }
232
343
  else { // outside of expressions, i.e. as normal elements, we need it in a .value wrapper
233
344
  current.parent.value = current.value;
@@ -243,18 +354,18 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
243
354
  *
244
355
  * We either "trick" it into the correct place in an .xpr or we simply overwrite the existing .ref
245
356
  *
246
- * @param {object} parent
357
+ * @param {object} oldValue
247
358
  * @param {object} newValue
248
359
  * @param {boolean} isInXpr
249
360
  * @param {Array} refBase
250
361
  * @param {Array} linksBase
251
- * @param {number} indexInParent
362
+ * @returns {object|Array}
252
363
  */
253
- function replaceInRef( parent, newValue, isInXpr, refBase, linksBase, indexInParent ) {
254
- delete parent.ref;
255
- const clone = {
256
- value: cloneCsnNonDict({ value: newValue }, cloneCsnOptions).value,
257
- };
364
+ function replaceInRef( oldValue, newValue, isInXpr, refBase, linksBase ) {
365
+ const clone = { value: cloneCsnNonDict({ value: newValue }, cloneCsnOptions).value };
366
+ if (oldValue.stored)
367
+ clone.value.stored = oldValue.stored;
368
+
258
369
  const refPrefix = refBase.slice(0, -1);
259
370
  const linksPrefix = linksBase.slice(0, -1);
260
371
  if (newValue.xpr) {
@@ -268,24 +379,15 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
268
379
  }
269
380
  },
270
381
  }, {
271
- // Do not rewrite refs inside of an association-where; avoids endless loop
382
+ // Do not rewrite refs inside an association-where; avoids endless loop
272
383
  skipStandard: { where: true },
273
384
  });
274
- if (indexInParent > -1) // a .xpr in a .xpr
275
- parent[indexInParent] = clone.value;
276
- else
277
- parent.value = clone.value;
278
385
  }
279
- else {
280
- if (indexInParent > -1) // a .ref in a .xpr
281
- parent[indexInParent] = clone.value;
282
- else
283
- parent.value = clone.value;
284
- if (clone.value.ref && clone.value.ref[0] !== '$self' && clone.value.ref[0] !== '$projection' ) {
285
- clone.value.ref = [ ...refPrefix, ...clone.value.ref ];
286
- clone.value._links = [ ...linksPrefix, ...clone.value._links ]; // TODO: Make non-enum, increment idx
287
- }
386
+ else if (clone.value.ref && clone.value.ref[0] !== '$self' && clone.value.ref[0] !== '$projection' ) {
387
+ clone.value.ref = [ ...refPrefix, ...clone.value.ref ];
388
+ clone.value._links = [ ...linksPrefix, ...clone.value._links ]; // TODO: Make non-enum, increment idx
288
389
  }
390
+ return clone.value;
289
391
  }
290
392
 
291
393
  /**
@@ -295,6 +397,7 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
295
397
  *
296
398
  * @param {CSN.Elements} elements Artifact elements
297
399
  * @param {object} carrier The thing that will "carry" the columns - .SELECT or .projection
400
+ * @returns {Function[]} Cleanup callbacks that remove `_`-links.
298
401
  */
299
402
  function calculateColumns( elements, carrier ) {
300
403
  carrier.columns = [ '*' ];
@@ -358,6 +461,7 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
358
461
  * @param {CSN.Elements} elements
359
462
  * @param {CSN.QuerySelect} SELECT
360
463
  * @param {boolean} containsExpandInline
464
+ * @returns {Function[]} Cleanup callbacks that remove `_`-links.
361
465
  */
362
466
  function makeAllCalculatedElementsExplicitColumns( elements, SELECT, containsExpandInline ) {
363
467
  const cleanupCallbacks = [];
@@ -366,7 +470,7 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
366
470
  const hasStar = SELECT.columns.includes('*');
367
471
  const unfoldingMap = {};
368
472
  let starContainsCalculated = false;
369
- let containsCalculated = false;
473
+ let containsCalcOnRead = false;
370
474
  for (const name in elements) {
371
475
  const originalRef = columnMap[name] && columnMap[name].ref || [ name ];
372
476
 
@@ -377,17 +481,19 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
377
481
  else
378
482
  element = columnMap[name]?._art || columnMap[name]?._element || root[name] || elements[name];
379
483
  const branches = getBranches(element, name, effectiveType, pathDelimiter); // TODO: is our elements[name] really the root[name]?
380
- if (hasCalculatedLeaf(branches)) {
381
- containsCalculated = true;
484
+ if (hasCalcOnReadLeaf(branches)) {
485
+ containsCalcOnRead = true;
382
486
  const columns = [];
383
487
  for (const branchName in branches) {
488
+ const branch = branches[branchName];
489
+ const leafElement = branch.steps[branch.steps.length - 1];
384
490
  if (columnMap[branchName]) { // Existing column - don't overwrite, we need $env!
385
491
  columns.push(columnMap[branchName]);
386
492
  }
387
493
  else {
388
494
  // TODO: Hm, will we have a $env in the leaf of the thing then?
389
495
  const column = { ref: [ ...originalRef, ...branches[branchName].ref.slice(1) ], as: branchName };
390
- setProp(column, '_element', element);
496
+ setProp(column, '_element', leafElement);
391
497
  cleanupCallbacks.push(() => delete column._element);
392
498
  columns.push(column);
393
499
  }
@@ -409,10 +515,10 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
409
515
  }
410
516
  }
411
517
 
412
- if (containsExpandInline && containsCalculated) {
518
+ if (containsExpandInline && containsCalcOnRead) {
413
519
  error('query-unsupported-calc', SELECT.$path, { '#': 'std' });
414
520
  }
415
- else if (containsCalculated) {
521
+ else if (containsCalcOnRead) {
416
522
  const newColumns = [];
417
523
  if (hasStar && !starContainsCalculated)
418
524
  newColumns.push('*');
@@ -422,22 +528,23 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
422
528
  newColumns.push(...columns);
423
529
  }
424
530
 
425
-
426
531
  SELECT.columns = newColumns;
427
532
  }
428
533
  return cleanupCallbacks;
429
534
  }
430
535
 
431
536
  /**
537
+ * Returns true if any leaf node is a calculated element on-read.
538
+ * On-write behaves like regular elements, hence they do not count here.
432
539
  *
433
540
  * @param {object} branches
434
541
  * @returns {boolean}
435
542
  */
436
- function hasCalculatedLeaf( branches ) {
543
+ function hasCalcOnReadLeaf( branches ) {
437
544
  for (const branchName in branches) {
438
545
  const branch = branches[branchName].steps;
439
546
  const leaf = branch[branch.length - 1];
440
- if (hasValue(leaf))
547
+ if (hasOnReadValue(leaf))
441
548
  return true;
442
549
  }
443
550
 
@@ -451,13 +558,13 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
451
558
  * @param {object} baseLeaf Leaf to start at
452
559
  * @returns {boolean}
453
560
  */
454
- function hasValue( baseLeaf ) {
561
+ function hasOnReadValue( baseLeaf ) {
455
562
  const visited = new WeakSet();
456
563
  const stack = [ baseLeaf ];
457
564
  while (stack.length > 0) {
458
565
  const leaf = stack.pop();
459
566
  if (!visited.has(leaf)) { // Don't re-process things
460
- if (leaf.value)
567
+ if (leaf.value && !leaf.value.stored)
461
568
  return true;
462
569
  else if (leaf._art)
463
570
  stack.push(leaf._art);
@@ -575,9 +682,9 @@ function rewriteCalculatedElementsInViews( csn, options, pathDelimiter, error )
575
682
  * @param {CSN.Model} csn
576
683
  */
577
684
  function processCalculatedElementsInEntities( csn ) {
578
- forEachDefinition(csn, (artifact, artifactName) => {
685
+ forEachDefinition(csn, (artifact, definitionName) => {
579
686
  if (artifact.kind === 'entity' && !(artifact.query || artifact.projection))
580
- killInEntity(artifact, [ 'definitions', artifactName ]);
687
+ removeDummyValueInEntity(artifact, [ 'definitions', definitionName ]);
581
688
  });
582
689
  }
583
690
 
@@ -590,13 +697,13 @@ function processCalculatedElementsInEntities( csn ) {
590
697
  * @todo calculated elements that "live" on the database?
591
698
  * @todo error when artifact is empty afterwards? Probably better as a CSN check!
592
699
  */
593
- function killInEntity( artifact, path ) {
700
+ function removeDummyValueInEntity( artifact, path ) {
594
701
  applyTransformationsOnDictionary(artifact.elements, {
595
702
  value: (parent, prop, value, p, root) => {
596
703
  if (!value.stored)
597
704
  delete root[p[p.length - 1]];
598
705
  },
599
- }, {}, path);
706
+ }, {}, path.concat( 'elements' ));
600
707
  }
601
708
 
602
709
  /**
@@ -4,7 +4,6 @@ const {
4
4
  getUtils, cloneCsnNonDict, applyTransformationsOnNonDictionary,
5
5
  } = require('../../model/csnUtils');
6
6
  const { implicitAs } = require('../../model/csnRefs');
7
- const { isBetaEnabled } = require('../../base/model');
8
7
  const { ModelError } = require('../../base/error');
9
8
 
10
9
  /**
@@ -208,11 +207,11 @@ function getViewTransformer( csn, options, messageFunctions, transformCommon ) {
208
207
  */
209
208
  function handleAssociationElement( query, elements, columnMap, publishedMixins, elem, elemName, elementsPath, queryPath ) {
210
209
  if (isUnion(queryPath) && options.transformation === 'hdbcds') {
211
- if (isBetaEnabled(options, 'ignoreAssocPublishingInUnion') && doA2J) {
210
+ if (doA2J) {
212
211
  if (elem.keys)
213
- info(null, queryPath, { name: elemName }, 'Managed association $(NAME), published in a UNION, will be ignored');
212
+ info('query-ignoring-assoc-in-union', queryPath, { name: elemName, '#': 'managed' });
214
213
  else
215
- info(null, queryPath, { name: elemName }, 'Association $(NAME), published in a UNION, will be ignored');
214
+ info('query-ignoring-assoc-in-union', queryPath, { name: elemName, '#': 'std' });
216
215
 
217
216
  elem._ignore = true;
218
217
  }
@@ -126,7 +126,7 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
126
126
  keys.push(elt);
127
127
  }, [ 'definitions', artifactName ], true, { elementsOnly: true });
128
128
 
129
- // In contrast to EDM, the DB entity may have more than one technical keys but should have idealy exactly one key of type cds.UUID
129
+ // In contrast to EDM, the DB entity may have more than one technical keys but should have ideally exactly one key of type cds.UUID
130
130
  if (keys.length !== 1)
131
131
  warning(null, [ 'definitions', artifactName ], 'Entity annotated with “@odata.draft.enabled” should have exactly one key element');
132
132
 
@@ -183,20 +183,27 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
183
183
  if (artifact.$location)
184
184
  setProp(draftsArtifact, '$location', artifact.$location);
185
185
 
186
+ const calcOnWriteElements = [];
187
+
186
188
  // Copy all elements
187
189
  for (const elemName in artifact.elements) {
188
190
  const origElem = artifact.elements[elemName];
189
- let elem;
190
- if ((isDeprecatedEnabled(options, '_renderVirtualElements') && origElem.virtual) || !origElem.virtual)
191
- elem = copyAndAddElement(origElem, draftsArtifact, draftsArtifactName, elemName)[elemName];
192
- if (elem) {
193
- // Remove "virtual" - cap/issues 4956
194
- if (elem.virtual)
195
- delete elem.virtual;
196
-
197
- // explicitly set nullable if not key and not unmanaged association
198
- if (!elem.key && !elem.on)
199
- elem.notNull = false;
191
+ if (origElem.value?.stored) {
192
+ calcOnWriteElements.push(elemName);
193
+ }
194
+ else {
195
+ let elem;
196
+ if ((isDeprecatedEnabled(options, '_renderVirtualElements') && origElem.virtual) || !origElem.virtual)
197
+ elem = copyAndAddElement(origElem, draftsArtifact, draftsArtifactName, elemName)[elemName];
198
+ if (elem) {
199
+ // Remove "virtual" - cap/issues 4956
200
+ if (elem.virtual)
201
+ delete elem.virtual;
202
+
203
+ // explicitly set nullable if not key and not unmanaged association
204
+ if (!elem.key && !elem.on)
205
+ elem.notNull = false;
206
+ }
200
207
  }
201
208
  }
202
209
 
@@ -276,6 +283,8 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
276
283
  // we don't delete them (but mark them as implicit so that toCdl does not render them)
277
284
  // draftAdministrativeData.DraftAdministrativeData.implicitForeignKeys = true;
278
285
  }
286
+
287
+ calcOnWriteElements.forEach(elemName => copyAndAddElement(artifact.elements[elemName], draftsArtifact, draftsArtifactName, elemName)[elemName]);
279
288
  }
280
289
 
281
290
  /**
@@ -16,6 +16,10 @@ const { makeMessageFunction } = require('../../base/messages');
16
16
  * - Element must not be an 'array of' for OData V2 TODO: move to the validator
17
17
  * - Perform checks for exposed non-abstract entities and views - check media type and key-ness
18
18
  *
19
+ * ATTENTION: generateDrafts propagates annotations from the draft nodes to the
20
+ * returns element of the draft actions. Shortcut/Convenience annotations
21
+ * are NOT replaced/expanded (eg. @label => @Common.Label).
22
+ *
19
23
  * @param {CSN.Model} csn
20
24
  * @param {CSN.Options} options
21
25
  * @param {Array} [services] Will be calculated JIT if not provided
@@ -51,7 +51,7 @@ const { addLocalizationViews } = require('./localized');
51
51
  // -- exposed associations do not point to non-exposed targets
52
52
  // -- structured types must not contain associations for OData V2
53
53
  // - Element must not be an 'array of' for OData V2 TODO: move to the validator
54
- // (Linter Candiate, move as hard error into EdmPreproc on V2 generation)
54
+ // (Linter Candidate, move as hard error into EdmPreproc on V2 generation)
55
55
  // - Perform checks for exposed non-abstract entities and views - check media type and
56
56
  // key-ness (requires that containers have been identified) (Linter candidate, scenario check)
57
57
  // Annotations related:
@@ -201,7 +201,7 @@ function transform4odataWithCsn(inputModel, options) {
201
201
  // Flatten on-conditions in unmanaged associations
202
202
  /* FIXME (HJB): Is this comment still correct? processOnCond only strips $self
203
203
  We should not remove $self prefixes in structured OData to not
204
- interfer with path resolution
204
+ interfere with path resolution
205
205
  */
206
206
  // This must be done before all the draft logic as all
207
207
  // composition targets are annotated with @odata.draft.enabled in this step
@@ -230,12 +230,13 @@ function transform4odataWithCsn(inputModel, options) {
230
230
  def['@cds.persistence.name'] = getArtifactDatabaseNameOf(defName, options.sqlMapping, csn, 'hana'); // hana to allow naming mode "hdbcds"
231
231
 
232
232
  forEachMemberRecursively(def, (member, memberName, propertyName) => {
233
- if (memberName === '' && propertyName === 'params')
234
- return; // ignore "returns" type
235
233
  // Annotate elements, foreign keys, parameters, etc. with their DB names if requested
236
234
  // Only these are actually required and don't annotate virtual elements in entities or types
237
235
  // as they have no DB representation (although in views)
238
- if (options.sqlMapping && typeof member === 'object' && !(member.kind === 'action' || member.kind === 'function') && propertyName !== 'enum' && (!member.virtual || def.query)) {
236
+ if (options.sqlMapping && typeof member === 'object' &&
237
+ !(member.kind === 'action' || member.kind === 'function') &&
238
+ !(propertyName === 'enum' || propertyName === 'returns') &&
239
+ (!member.virtual || def.query)) {
239
240
  // If we have a 'preserved dotted name' (i.e. we are a result of flattening), use that for the @cds.persistence.name annotation
240
241
  member['@cds.persistence.name'] = getElementDatabaseNameOf(member._flatElementNameWithDots || memberName, options.sqlMapping, 'hana'); // hana to allow "hdbcds"
241
242
  }
@@ -387,12 +388,12 @@ function transform4odataWithCsn(inputModel, options) {
387
388
  }
388
389
 
389
390
  // Apply default type facets to each type definition and every member
390
- // But do not apply default string length 5000 (as in DB)
391
+ // But do not apply default string length (as in DB)
391
392
  function setDefaultTypeFacets(def) {
392
- addDefaultTypeFacets(def.items || def, false)
393
- forEachMemberRecursively(def, m=>addDefaultTypeFacets(m.items || m, false));
393
+ addDefaultTypeFacets(def.items || def, null)
394
+ forEachMemberRecursively(def, m=>addDefaultTypeFacets(m.items || m, null));
394
395
  if(def.returns)
395
- addDefaultTypeFacets(def.returns.items || def.returns, false);
396
+ addDefaultTypeFacets(def.returns.items || def.returns, null);
396
397
  }
397
398
 
398
399
  // Handles on-conditions in unmanaged associations
@@ -409,7 +410,7 @@ function transform4odataWithCsn(inputModel, options) {
409
410
  // TODO: Shouldn't this only run on the on-condition and not the whole assoc-node?
410
411
  applyTransformationsOnNonDictionary({ assoc }, 'assoc', {
411
412
  ref: (node, prop, ref) => {
412
- // remove leading $self when at the begining of a ref
413
+ // remove leading $self when at the beginning of a ref
413
414
  if (ref.length > 1 && ref[0] === '$self')
414
415
  node.ref.splice(0, 1);
415
416
  }
@@ -31,6 +31,7 @@ const cdsPersistence = require('./db/cdsPersistence');
31
31
  const temporal = require('./db/temporal');
32
32
  const associations = require('./db/associations')
33
33
  const { ModelError } = require('../base/error');
34
+ const { getDefaultTypeLengths } = require('../render/utils/common');
34
35
 
35
36
  // By default: Do not process non-entities/views
36
37
  function forEachDefinition(csn, cb) {
@@ -61,7 +62,7 @@ function forEachDefinition(csn, cb) {
61
62
  * - (045) The query is stripped from entities that are annotated with '@cds.persistence.table',
62
63
  * essentially converting views to entities.
63
64
  * - (060) Users of primitive type 'UUID' (which is renamed to 'String' in 000) get length 36'.
64
- * - (070) Default length 5000 is supplied for strings if not specified.
65
+ * - (070) Default length N is supplied for strings if not specified.
65
66
  * - (080) Annotation definitions are ignored (note that annotation assignments are filtered out by toCdl).
66
67
  * - (090) Compositions become associations.
67
68
  * - (100) 'masked' is ignored (a), and attribute 'localized' is removed (b)
@@ -113,6 +114,8 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
113
114
  checkCSNVersion(csn, options);
114
115
 
115
116
  const pathDelimiter = (options.sqlMapping === 'hdbcds') ? '.' : '_';
117
+ // There is also an explicit default length via options.defaultStringLength
118
+ const implicitDefaultLengths = getDefaultTypeLengths(options.sqlDialect);
116
119
 
117
120
  let csnUtils;
118
121
  let message, error, warning, info; // message functions
@@ -160,6 +163,8 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
160
163
  });
161
164
  timetrace.stop('Validate');
162
165
 
166
+ rewriteCalculatedElementsInViews(csn, options, pathDelimiter, error);
167
+
163
168
  // Needs to happen before tuple expansion, so the newly generated WHERE-conditions have it applied
164
169
  handleExists(csn, options, error, inspectRef, initDefinition, dropDefinitionCache);
165
170
 
@@ -184,8 +189,6 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
184
189
  assertUnique.prepare(csn, options, error, info)
185
190
  ]);
186
191
 
187
- rewriteCalculatedElementsInViews(csn, options, pathDelimiter, error);
188
-
189
192
  if(doA2J) {
190
193
  // Expand a structured thing in: keys, columns, order by, group by
191
194
  expansion.expandStructureReferences(csn, options, pathDelimiter, {error, info, throwWithAnyError}, csnUtils);
@@ -252,9 +255,11 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
252
255
  transformCsn(csn, {
253
256
  type: (val, node, key) => {
254
257
  renamePrimitiveTypesAndUuid(val, node, key);
255
- addDefaultTypeFacets(node);
258
+ addDefaultTypeFacets(node, implicitDefaultLengths);
256
259
  },
257
- // HANA/SQLite do not support array-of - turn into CLOB/Text
260
+ // no support for array-of - turn into CLOB/Text
261
+ // must be done after A2J or compiler checks could change
262
+ // (e.g. annotation def checks for arrayed types)
258
263
  items: (val, node) => {
259
264
  node.type = 'cds.LargeString';
260
265
  delete node.items;
@@ -572,7 +577,7 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
572
577
  addStringAnnotationTo('@cds.persistence.name', getArtifactDatabaseNameOf(artifactName, options.sqlMapping, csn, options.sqlDialect), artifact);
573
578
 
574
579
  forEachMemberRecursively(artifact, (member, memberName, property, path) => {
575
- if (memberName === '' && property === 'params')
580
+ if (property === 'returns')
576
581
  return; // ignore "returns" type
577
582
  transformCommon(member, memberName, path);
578
583
  // (240 b) Annotate elements, foreign keys, parameters etc with their DB names
@@ -1146,7 +1151,7 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
1146
1151
  // if it's not the first entry, add a ',' ...
1147
1152
  if (i)
1148
1153
  flattenedIndex.push(',');
1149
- // ... then add the flattend element name as a single ref
1154
+ // ... then add the flattened element name as a single ref
1150
1155
  flattenedIndex.push({ ref: [ elem ] });
1151
1156
  // ... then check if we have to propagate a 'asc'/'desc', omitting the last, which will be copied automatically
1152
1157
  if ((idx + 1) < index.length && (index[idx + 1] === 'asc' || index[idx + 1] === 'desc') && i < elems.length - 1)