@sap/cds-compiler 2.11.4 → 2.12.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 (80) hide show
  1. package/CHANGELOG.md +58 -1
  2. package/bin/cds_update_identifiers.js +7 -7
  3. package/bin/cdsc.js +9 -10
  4. package/doc/CHANGELOG_ARCHIVE.md +1 -1
  5. package/doc/CHANGELOG_BETA.md +12 -0
  6. package/lib/api/main.js +2 -0
  7. package/lib/api/options.js +2 -2
  8. package/lib/base/message-registry.js +31 -2
  9. package/lib/base/model.js +1 -0
  10. package/lib/base/optionProcessorHelper.js +97 -69
  11. package/lib/checks/.eslintrc.json +2 -0
  12. package/lib/checks/actionsFunctions.js +2 -1
  13. package/lib/checks/foreignKeys.js +4 -4
  14. package/lib/checks/managedInType.js +4 -4
  15. package/lib/checks/queryNoDbArtifacts.js +1 -3
  16. package/lib/checks/sql-snippets.js +93 -0
  17. package/lib/checks/validator.js +8 -0
  18. package/lib/compiler/assert-consistency.js +5 -3
  19. package/lib/compiler/base.js +0 -1
  20. package/lib/compiler/checks.js +32 -9
  21. package/lib/compiler/definer.js +25 -4
  22. package/lib/compiler/index.js +1 -1
  23. package/lib/compiler/propagator.js +3 -2
  24. package/lib/compiler/resolver.js +97 -6
  25. package/lib/compiler/shared.js +12 -1
  26. package/lib/compiler/utils.js +7 -0
  27. package/lib/edm/annotations/genericTranslation.js +34 -17
  28. package/lib/edm/annotations/preprocessAnnotations.js +1 -1
  29. package/lib/edm/csn2edm.js +1 -1
  30. package/lib/edm/edm.js +8 -8
  31. package/lib/edm/edmPreprocessor.js +30 -23
  32. package/lib/edm/edmUtils.js +11 -12
  33. package/lib/gen/Dictionary.json +82 -40
  34. package/lib/gen/language.checksum +1 -1
  35. package/lib/gen/language.interp +3 -1
  36. package/lib/gen/language.tokens +15 -14
  37. package/lib/gen/languageLexer.interp +9 -1
  38. package/lib/gen/languageLexer.js +830 -779
  39. package/lib/gen/languageLexer.tokens +7 -6
  40. package/lib/gen/languageParser.js +2401 -2282
  41. package/lib/json/from-csn.js +47 -16
  42. package/lib/json/to-csn.js +17 -5
  43. package/lib/language/antlrParser.js +3 -3
  44. package/lib/language/docCommentParser.js +1 -1
  45. package/lib/language/genericAntlrParser.js +68 -51
  46. package/lib/language/language.g4 +128 -74
  47. package/lib/language/multiLineStringParser.js +536 -0
  48. package/lib/main.d.ts +5 -3
  49. package/lib/main.js +3 -2
  50. package/lib/model/csnRefs.js +116 -68
  51. package/lib/model/csnUtils.js +40 -48
  52. package/lib/model/enrichCsn.js +30 -14
  53. package/lib/optionProcessor.js +3 -3
  54. package/lib/render/DuplicateChecker.js +1 -1
  55. package/lib/render/manageConstraints.js +1 -1
  56. package/lib/render/toCdl.js +193 -79
  57. package/lib/render/toHdbcds.js +179 -95
  58. package/lib/render/toRename.js +7 -10
  59. package/lib/render/toSql.js +57 -40
  60. package/lib/render/utils/common.js +24 -5
  61. package/lib/render/utils/sql.js +6 -4
  62. package/lib/transform/braceExpression.js +4 -2
  63. package/lib/transform/db/associations.js +389 -0
  64. package/lib/transform/db/cdsPersistence.js +150 -0
  65. package/lib/transform/db/constraints.js +6 -4
  66. package/lib/transform/db/draft.js +3 -2
  67. package/lib/transform/db/expansion.js +4 -5
  68. package/lib/transform/db/flattening.js +5 -6
  69. package/lib/transform/db/temporal.js +236 -0
  70. package/lib/transform/db/transformExists.js +36 -23
  71. package/lib/transform/forHanaNew.js +35 -626
  72. package/lib/transform/forOdataNew.js +5 -4
  73. package/lib/transform/localized.js +3 -14
  74. package/lib/transform/odata/generateForeignKeyElements.js +2 -2
  75. package/lib/transform/transformUtilsNew.js +13 -13
  76. package/lib/transform/translateAssocsToJoins.js +8 -8
  77. package/lib/transform/universalCsnEnricher.js +217 -47
  78. package/lib/utils/file.js +2 -1
  79. package/lib/utils/timetrace.js +8 -2
  80. package/package.json +1 -1
@@ -0,0 +1,236 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ getUtils, getNormalizedQuery, hasAnnotationValue, forEachMember,
5
+ } = require('../../model/csnUtils');
6
+ const { implicitAs } = require('../../model/csnRefs');
7
+ const { setProp } = require('../../base/model');
8
+ const { getTransformers } = require('../transformUtilsNew');
9
+
10
+ const validToString = '@cds.valid.to';
11
+ const validFromString = '@cds.valid.from';
12
+ /**
13
+ * Get the forEachDefinition callback function that adds a where condition to views that
14
+ * - are annotated with @cds.valid.from and @cds.valid.to,
15
+ * - have only one @cds.valid.from and @cds.valid.to,
16
+ * - and both annotations come from the same entity
17
+ *
18
+ * If the view has one of the annotations but the other conditions are not met, an error will be raised.
19
+ *
20
+ * @param {CSN.Model} csn
21
+ * @param {object} messageFunctions
22
+ * @param {Function} messageFunctions.info
23
+ * @returns {(artifact: CSN.Artifact, artifactName: string) => void} Callback for forEachDefinition applying the where-condition to views.
24
+ */
25
+ function getViewDecorator(csn, messageFunctions) {
26
+ const { info } = messageFunctions;
27
+ const { get$combined } = getUtils(csn);
28
+ return addTemporalWhereConditionToView;
29
+ /**
30
+ * Add a where condition to views that
31
+ * - are annotated with @cds.valid.from and @cds.valid.to,
32
+ * - have only one @cds.valid.from and @cds.valid.to,
33
+ * - and both annotations come from the same entity
34
+ *
35
+ * If the view has one of the annotations but the other conditions are not met, an error will be raised.
36
+ *
37
+ * @param {CSN.Artifact} artifact
38
+ * @param {string} artifactName
39
+ */
40
+ function addTemporalWhereConditionToView(artifact, artifactName) {
41
+ const normalizedQuery = getNormalizedQuery(artifact);
42
+ if (normalizedQuery && normalizedQuery.query && normalizedQuery.query.SELECT) {
43
+ // BLOCKER: We need information to handle $combined
44
+ // What we are trying to achieve by this:
45
+ // Forbid joining/selecting from two or more temporal entities
46
+ // Idea: Follow the query-tree and check each from
47
+ // Collect all source-entities and compute our own $combined
48
+ const $combined = get$combined(normalizedQuery.query);
49
+ const [ from, to ] = getFromToElements($combined);
50
+ // exactly one validFrom & validTo
51
+ if (from.length === 1 && to.length === 1) {
52
+ // and both are from the same origin
53
+ if (from[0].source === to[0].source && from[0].parent === to[0].parent) {
54
+ if (!hasFalsyTemporalAnnotations(normalizedQuery.query.SELECT, artifact.elements, from[0], to[0])) {
55
+ const fromPath = {
56
+ ref: [
57
+ from[0].parent,
58
+ from[0].name,
59
+ ],
60
+ };
61
+
62
+ const toPath = {
63
+ ref: [
64
+ to[0].parent,
65
+ to[0].name,
66
+ ],
67
+ };
68
+
69
+
70
+ const atFrom = { ref: [ '$at', 'from' ] };
71
+ const atTo = { ref: [ '$at', 'to' ] };
72
+
73
+ const cond = [ '(', fromPath, '<', atTo, 'and', toPath, '>', atFrom, ')' ];
74
+
75
+ if (normalizedQuery.query.SELECT.where) { // if there is an existing where-clause, extend it by adding 'and (temporal clause)'
76
+ normalizedQuery.query.SELECT.where = [ '(', ...normalizedQuery.query.SELECT.where, ')', 'and', ...cond ];
77
+ }
78
+ else {
79
+ normalizedQuery.query.SELECT.where = cond;
80
+ }
81
+ }
82
+ }
83
+ else {
84
+ info(null, [ 'definitions', artifactName ], `No temporal WHERE clause added as "${from[0].error_parent}"."${from[0].name}" and "${to[0].error_parent}"."${to[0].name}" are not of same origin`);
85
+ }
86
+ }
87
+ else if (from.length > 0 || to.length > 0) {
88
+ const missingAnnotation = from.length > to.length ? validToString : validFromString;
89
+ info(null, [ 'definitions', artifactName ],
90
+ { anno: missingAnnotation },
91
+ 'No temporal WHERE clause added because $(ANNO) is missing');
92
+ }
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Get all elements tagged with @cds.valid.from/to from the union of all entities of the from-clause.
98
+ *
99
+ * @param {any} combined union of all entities of the from-clause
100
+ * @returns {Array[]} Array where first field is array of elements with @cds.valid.from, second field is array of elements with @cds.valid.to.
101
+ */
102
+ function getFromToElements(combined) {
103
+ const from = [];
104
+ const to = [];
105
+ for (const name in combined) {
106
+ let elt = combined[name];
107
+ if (!Array.isArray(elt))
108
+ elt = [ elt ];
109
+ elt.forEach((e) => {
110
+ if (hasAnnotationValue(e.element, validFromString))
111
+ from.push(e);
112
+
113
+ if (hasAnnotationValue(e.element, validToString))
114
+ to.push(e);
115
+ });
116
+ }
117
+
118
+ return [ from, to ];
119
+ }
120
+
121
+ /**
122
+ * Check if the given SELECT has a falsy @cds.valid.from and a falsy @cds.valid.to
123
+ *
124
+ * @param {CSN.QuerySelect} SELECT
125
+ * @param {CSN.Elements} elements
126
+ * @param {object} from
127
+ * @param {object} to
128
+ * @returns {boolean} True if both are present and false.
129
+ */
130
+ function hasFalsyTemporalAnnotations(SELECT, elements, from, to) {
131
+ let fromElement = elements[from.name];
132
+ let toElement = elements[to.name];
133
+
134
+ if (SELECT.columns) {
135
+ for (const col of SELECT.columns) {
136
+ if (col.ref) {
137
+ const implicitAlias = implicitAs(col.ref);
138
+ if (implicitAlias === from.name)
139
+ fromElement = elements[col.as || implicitAlias];
140
+ else if (implicitAlias === to.name)
141
+ toElement = elements[col.as || implicitAlias];
142
+ }
143
+ }
144
+ }
145
+ return fromElement && toElement &&
146
+ hasAnnotationValue(fromElement, validFromString, false) &&
147
+ hasAnnotationValue(toElement, validToString, false);
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Get the forEachDefinition callback function that collects all usages of @cds.valid.from/to/key and checks that
153
+ * - the assignment is on a valid element
154
+ * - the annotation is only assigned once
155
+ * - key is only used in conjunction with from and to
156
+ *
157
+ * Furthermore, @cds.valid.from and @cds.valid.key is processed - @cds.valid.from is marked as key or marked as unique if @cds.valid.key is used.
158
+ * If @cds.valid.key is used, the real key-elements have their key-property removed (set non-enumerable as $key) and instead the @cds.valid.key-marked elements have it added.
159
+ *
160
+ * @param {CSN.Model} csn
161
+ * @param {CSN.Options} options
162
+ * @param {string} pathDelimiter
163
+ * @param {object} messageFunctions
164
+ * @param {Function} messageFunctions.error
165
+ * @returns {(artifact: CSN.Artifact, artifactName: string) => void} Callback for forEachDefinition processing the annotations.
166
+ */
167
+ function getAnnotationHandler(csn, options, pathDelimiter, messageFunctions) {
168
+ const { error } = messageFunctions;
169
+ const {
170
+ extractValidFromToKeyElement, checkAssignment, checkMultipleAssignments, recurseElements,
171
+ } = getTransformers(csn, options, pathDelimiter);
172
+
173
+ return handleTemporalAnnotations;
174
+ /**
175
+ * @param {CSN.Artifact} artifact
176
+ * @param {string} artifactName
177
+ */
178
+ function handleTemporalAnnotations(artifact, artifactName) {
179
+ const validFrom = [];
180
+ const validTo = [];
181
+ const validKey = [];
182
+
183
+ recurseElements(artifact, [ 'definitions', artifactName ], (member, path) => {
184
+ const [ f, t, k ] = extractValidFromToKeyElement(member, path);
185
+ validFrom.push(...f);
186
+ validTo.push(...t);
187
+ validKey.push(...k);
188
+ });
189
+
190
+ if (artifact.kind === 'entity' && !artifact.query) {
191
+ validFrom.forEach(obj => checkAssignment(validFromString, obj.element, obj.path, artifact));
192
+ validTo.forEach(obj => checkAssignment(validToString, obj.element, obj.path, artifact));
193
+ validKey.forEach(obj => checkAssignment('@cds.valid.key', obj.element, obj.path, artifact));
194
+ checkMultipleAssignments(validFrom, validFromString, artifact, artifactName);
195
+ checkMultipleAssignments(validTo, validToString, artifact, artifactName, true);
196
+ checkMultipleAssignments(validKey, '@cds.valid.key', artifact, artifactName);
197
+ }
198
+
199
+ // if there is an cds.valid.key, make this the only primary key
200
+ // otherwise add all cds.valid.from to primary key tuple
201
+ if (validKey.length) {
202
+ if (!validFrom.length || !validTo.length) {
203
+ error(null, [ 'definitions', artifactName ],
204
+ 'Expecting “@cds.valid.from” and “@cds.valid.to” if “@cds.valid.key” is used');
205
+ }
206
+
207
+ forEachMember(artifact, (member) => {
208
+ if (member.key) {
209
+ member.unique = true;
210
+ delete member.key;
211
+ // Remember that this element was a key in the original artifact.
212
+ // This is needed for localized convenience view generation.
213
+ setProp(member, '$key', true);
214
+ }
215
+ });
216
+ validKey.forEach((member) => {
217
+ member.element.key = true;
218
+ });
219
+
220
+ validFrom.forEach((member) => {
221
+ member.element.unique = true;
222
+ });
223
+ }
224
+ else {
225
+ validFrom.forEach((member) => {
226
+ member.element.key = true;
227
+ });
228
+ }
229
+ }
230
+ }
231
+
232
+
233
+ module.exports = {
234
+ getViewDecorator,
235
+ getAnnotationHandler,
236
+ };
@@ -365,16 +365,23 @@ function handleExists(csn, options, error) {
365
365
  const subselect = getSubselect(root.target, ref, sources);
366
366
 
367
367
  const target = subselect.SELECT.from.as; // use subquery alias as target - prevent shadowing
368
- if (root.keys) { // managed assoc
369
- translateManagedAssocToWhere(root, target, subselect, isPrefixedWithTableAlias, base, current);
370
- }
371
- else { // unmanaged assoc
372
- translateUnmanagedAssocToWhere(root, target, subselect, isPrefixedWithTableAlias, base, current);
373
- }
368
+ const extension = root.keys ? translateManagedAssocToWhere(root, target, isPrefixedWithTableAlias, base, current) : translateUnmanagedAssocToWhere(root, target, isPrefixedWithTableAlias, base, current);
369
+ if (extension.length > 3)
370
+ subselect.SELECT.where.push('('); // add braces around the on-condition part to ensure precedence is kept
371
+
372
+ subselect.SELECT.where.push(...extension);
373
+
374
+ if (extension.length > 3)
375
+ subselect.SELECT.where.push(')');
374
376
 
375
377
  newExpr.push('exists');
376
- if (ref && ref.where)
377
- subselect.SELECT.where.push(...[ 'and', ...remapExistingWhere(target, ref.where) ]);
378
+ if (ref && ref.where) {
379
+ const remappedWhere = remapExistingWhere(target, ref.where);
380
+ if (remappedWhere.length > 3)
381
+ subselect.SELECT.where.push(...[ 'and', '(', ...remappedWhere, ')' ]);
382
+ else
383
+ subselect.SELECT.where.push(...[ 'and', ...remappedWhere ]);
384
+ }
378
385
 
379
386
  newExpr.push(subselect);
380
387
  toContinue.push([ exprPath.concat(newExpr.length - 1), exprPath.concat([ newExpr.length - 1, 'SELECT', 'where' ]) ]);
@@ -412,21 +419,24 @@ function handleExists(csn, options, error) {
412
419
  *
413
420
  * @param {CSN.Element} root
414
421
  * @param {string} target
415
- * @param {CSN.Query} subselect This subselect will in the end replace <assoc> in EXISTS <assoc>
416
422
  * @param {boolean} isPrefixedWithTableAlias
417
423
  * @param {string} base
418
424
  * @param {Token} current
425
+ * @returns {object[]} The stuff to add to the where
419
426
  */
420
- function translateManagedAssocToWhere(root, target, subselect, isPrefixedWithTableAlias, base, current) {
427
+ function translateManagedAssocToWhere(root, target, isPrefixedWithTableAlias, base, current) {
428
+ const whereExtension = [];
421
429
  for (let j = 0; j < root.keys.length; j++) {
422
430
  const lop = { ref: [ target, ...root.keys[j].ref ] }; // target side
423
431
  const rop = { ref: (isPrefixedWithTableAlias ? [] : [ base ]).concat([ ...toRawRef(current.ref), ...root.keys[j].ref ]) }; // source side
424
432
 
425
433
  if (j > 0)
426
- subselect.SELECT.where.push('and');
434
+ whereExtension.push('and');
427
435
 
428
- subselect.SELECT.where.push(...[ lop, '=', rop ]);
436
+ whereExtension.push(...[ lop, '=', rop ]);
429
437
  }
438
+
439
+ return whereExtension;
430
440
  }
431
441
 
432
442
  /**
@@ -452,12 +462,13 @@ function handleExists(csn, options, error) {
452
462
  *
453
463
  * @param {CSN.Element} root
454
464
  * @param {string} target
455
- * @param {CSN.Query} subselect This subselect will in the end replace <assoc> in EXISTS <assoc>
456
465
  * @param {boolean} isPrefixedWithTableAlias
457
466
  * @param {string} base
458
467
  * @param {Token} current
468
+ * @returns {object[]} The stuff to add to the where
459
469
  */
460
- function translateUnmanagedAssocToWhere(root, target, subselect, isPrefixedWithTableAlias, base, current) {
470
+ function translateUnmanagedAssocToWhere(root, target, isPrefixedWithTableAlias, base, current) {
471
+ const whereExtension = [];
461
472
  for (let j = 0; j < root.on.length; j++) {
462
473
  const part = root.on[j];
463
474
 
@@ -465,7 +476,7 @@ function handleExists(csn, options, error) {
465
476
  // but also keep along stuff like null and undefined, so compiler
466
477
  // can have a chance to complain/ we can fail later nicely maybe
467
478
  if (!(part && part.ref)) {
468
- subselect.SELECT.where.push(part);
479
+ whereExtension.push(part);
469
480
  continue;
470
481
  }
471
482
 
@@ -475,30 +486,32 @@ function handleExists(csn, options, error) {
475
486
  // Dollar Self Backlink
476
487
  if (isValidDollarSelf(root.on[j], root.$path.concat([ 'on', j ]), root.on[j + 1], root.on[j + 2], root.$path.concat([ 'on', j + 2 ]))) {
477
488
  if (root.on[j].ref[0] === '$self' && root.on[j].ref.length === 1)
478
- subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j + 2], root.$path.concat([ 'on', j + 2 ])));
489
+ whereExtension.push(...translateDollarSelfToWhere(base, target, root.on[j + 2], root.$path.concat([ 'on', j + 2 ])));
479
490
  else
480
- subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j], root.$path.concat([ 'on', j ])));
491
+ whereExtension.push(...translateDollarSelfToWhere(base, target, root.on[j], root.$path.concat([ 'on', j ])));
481
492
 
482
493
  j += 2;
483
494
  }
484
495
  else if (links && links[0].art === root) { // target side
485
- subselect.SELECT.where.push({ ref: [ target, ...part.ref.slice(1) ] });
496
+ whereExtension.push({ ref: [ target, ...part.ref.slice(1) ] });
486
497
  }
487
498
  else if (part.$scope === '$self') { // source side - "absolute" scope
488
499
  // cut off the $self, as we prefix the entity name now
489
- subselect.SELECT.where.push({ ref: [ base, ...part.ref.slice(1) ] });
500
+ whereExtension.push({ ref: [ base, ...part.ref.slice(1) ] });
490
501
  }
491
502
  else if (art) { // source side - with local scope
492
503
  if (isPrefixedWithTableAlias)
493
- subselect.SELECT.where.push({ ref: [ ...current.ref.slice(0, -1), ...part.ref ] });
504
+ whereExtension.push({ ref: [ ...current.ref.slice(0, -1), ...part.ref ] });
494
505
  else
495
- subselect.SELECT.where.push({ ref: [ base, ...current.ref.slice(0, -1), ...part.ref ] });
506
+ whereExtension.push({ ref: [ base, ...current.ref.slice(0, -1), ...part.ref ] });
496
507
  }
497
508
  else { // operator - or any other leftover
498
- subselect.SELECT.where.push(part);
509
+ whereExtension.push(part);
499
510
  }
500
511
  }
501
512
 
513
+ return whereExtension;
514
+
502
515
  /**
503
516
  * Check that an expression triple is a valid $self
504
517
  *
@@ -706,7 +719,7 @@ function handleExists(csn, options, error) {
706
719
  *
707
720
  * @param {string} base The source entity/query source name
708
721
  * @param {string} target The target entity/query source name
709
- * @param {CSN.Element} assoc The association element - the "not-$self" side of the comparison
722
+ * @param {object} assoc The association element - the "not-$self" side of the comparison
710
723
  * @param {CSN.Path} path
711
724
  * @returns {TokenStream} The WHERE representing the $self comparison
712
725
  */