@sap/cds-compiler 6.3.4 → 6.4.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 (55) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/LICENSE +32 -0
  3. package/README.md +14 -2
  4. package/bin/cdsse.js +0 -3
  5. package/doc/CHANGELOG_BETA.md +1 -1
  6. package/doc/CHANGELOG_DEPRECATED.md +1 -1
  7. package/lib/base/message-registry.js +7 -0
  8. package/lib/base/messages.js +1 -1
  9. package/lib/base/model.js +2 -0
  10. package/lib/compiler/assert-consistency.js +1 -0
  11. package/lib/compiler/checks.js +37 -26
  12. package/lib/compiler/define.js +1 -1
  13. package/lib/compiler/extend.js +39 -50
  14. package/lib/compiler/finalize-parse-cdl.js +1 -1
  15. package/lib/compiler/lsp-api.js +1 -1
  16. package/lib/compiler/populate.js +2 -2
  17. package/lib/compiler/propagator.js +29 -6
  18. package/lib/compiler/resolve.js +13 -3
  19. package/lib/compiler/shared.js +31 -25
  20. package/lib/compiler/tweak-assocs.js +86 -28
  21. package/lib/compiler/xpr-rewrite.js +70 -38
  22. package/lib/edm/annotations/edmJson.js +206 -37
  23. package/lib/edm/csn2edm.js +13 -0
  24. package/lib/edm/edmUtils.js +2 -2
  25. package/lib/gen/BaseParser.js +106 -72
  26. package/lib/gen/CdlGrammar.checksum +1 -1
  27. package/lib/gen/CdlParser.js +1500 -1509
  28. package/lib/json/to-csn.js +8 -5
  29. package/lib/language/genericAntlrParser.js +0 -0
  30. package/lib/main.js +19 -16
  31. package/lib/model/csnRefs.js +589 -521
  32. package/lib/model/csnUtils.js +26 -7
  33. package/lib/model/enrichCsn.js +1 -0
  34. package/lib/parsers/AstBuildingParser.js +72 -27
  35. package/lib/render/toCdl.js +2 -1
  36. package/lib/render/toHdbcds.js +6 -3
  37. package/lib/render/toSql.js +5 -0
  38. package/lib/transform/db/applyTransformations.js +1 -1
  39. package/lib/transform/db/assertUnique.js +4 -1
  40. package/lib/transform/db/cdsPersistence.js +17 -18
  41. package/lib/transform/db/expansion.js +179 -3
  42. package/lib/transform/db/flattening.js +16 -5
  43. package/lib/transform/db/rewriteCalculatedElements.js +79 -283
  44. package/lib/transform/effective/main.js +8 -1
  45. package/lib/transform/forOdata.js +1 -1
  46. package/lib/transform/forRelationalDB.js +21 -80
  47. package/lib/transform/localized.js +65 -110
  48. package/lib/transform/odata/foreignKeyRefsInXprAnnos.js +89 -63
  49. package/lib/transform/transformUtils.js +23 -21
  50. package/lib/transform/translateAssocsToJoins.js +7 -5
  51. package/lib/transform/tupleExpansion.js +16 -3
  52. package/package.json +1 -1
  53. package/doc/DeprecatedOptions_v2.md +0 -150
  54. package/doc/NameResolution.md +0 -837
  55. package/lib/transform/parseExpr.js +0 -415
@@ -156,14 +156,14 @@ function _addLocalizationViews(csn, options, config) {
156
156
 
157
157
  /**
158
158
  * Add a localized convenience view for the given artifact.
159
- * Can either be an entity or view. `textElements` are the elements which
159
+ * Can either be an entity or view. `localizedElements` are the elements which
160
160
  * are needed for creating a horizontal convenience view, i.e. only required
161
161
  * for entities.
162
162
  *
163
163
  * @param {string} artName
164
- * @param {string[]} [textElements=[]]
164
+ * @param {string[]} [localizedElements=[]]
165
165
  */
166
- function addLocalizedView( artName, textElements = [] ) {
166
+ function addLocalizedView( artName, localizedElements = [] ) {
167
167
  const art = csn.definitions[artName];
168
168
  const artPath = [ 'definitions', artName ];
169
169
  const viewName = `localized.${ artName }`;
@@ -183,32 +183,32 @@ function _addLocalizationViews(csn, options, config) {
183
183
  if (art.query || art.projection)
184
184
  view = createLocalizedViewForView(art, viewName);
185
185
  else
186
- view = createLocalizedViewForEntity(art, artName, viewName, textElements);
186
+ view = createLocalizedViewForEntity(art, artName, viewName, localizedElements);
187
187
 
188
188
  copyPersistenceAnnotations(view, art);
189
189
  csn.definitions[viewName] = view;
190
190
  }
191
191
 
192
192
  /**
193
- * Create a localized data view for the given entity `art` with `textElements`.
193
+ * Create a localized data view for the given entity `art` with `localizedElements`.
194
194
  * In JOIN mode the FROM query is rewritten to remove associations and the
195
195
  * columns are expanded.
196
196
  *
197
197
  * @param {CSN.Definition} entity
198
198
  * @param {string} entityName
199
199
  * @param {string} viewName Name of the localized view.
200
- * @param {string[]} [textElements]
200
+ * @param {string[]} [localizedElements]
201
201
  * @returns {CSN.View}
202
202
  */
203
- function createLocalizedViewForEntity( entity, entityName, viewName, textElements = [] ) {
203
+ function createLocalizedViewForEntity( entity, entityName, viewName, localizedElements = [] ) {
204
204
  // Only use joins if requested and text elements are provided.
205
- const shouldUseJoin = useJoins && !!textElements.length;
205
+ const shouldUseJoin = useJoins && !!localizedElements.length;
206
206
  const columns = [ ];
207
207
 
208
208
  const convenienceView = {
209
209
  '@odata.draft.enabled': false,
210
210
  kind: 'entity',
211
- query: { // TODO: Use projection
211
+ query: {
212
212
  SELECT: {
213
213
  from: createFromClauseForEntity(),
214
214
  columns,
@@ -222,20 +222,12 @@ function _addLocalizationViews(csn, options, config) {
222
222
 
223
223
  if (shouldUseJoin)
224
224
  // Expand elements; (variant 1)
225
- columns.push( ...columnsForEntityWithExcludeList( entity, 'L_0', textElements ) );
225
+ columns.push( ...columnsForEntityWithExcludeList( entity, 'L_0', localizedElements ) );
226
226
  else
227
227
  columns.push( '*' ); // (variant 2)
228
228
 
229
- for (const originalElement of textElements) {
230
- const elem = entity.elements[originalElement];
231
- // Note: $key is used by forRelationalDB.js to indicate that this element was a key in the original,
232
- // user's entity. Keys may have been changed by the backends (e.g. by `@cds.valid.key`)
233
- if (!elem.key && !elem.$key)
234
- columns.push( createColumnLocalizedElement( originalElement, shouldUseJoin ) );
235
- else if (shouldUseJoin)
236
- // In JOIN mode we also want to add keys.
237
- columns.push( createColumnRef( [ 'L_0', originalElement ] ));
238
-
229
+ for (const originalElement of localizedElements) {
230
+ columns.push( createColumnLocalizedElement( originalElement, shouldUseJoin ) );
239
231
  addCoreComputedIfNecessary(convenienceView.elements, originalElement);
240
232
  }
241
233
 
@@ -254,22 +246,43 @@ function _addLocalizationViews(csn, options, config) {
254
246
  on: [],
255
247
  };
256
248
 
257
- for (const originalElement of textElements) {
258
- const elem = entity.elements[originalElement];
259
- if (elem.key || elem.$key) {
260
- from.on.push( createColumnRef( [ 'localized_1', originalElement ] ));
261
- from.on.push( '=' );
262
- from.on.push( createColumnRef( [ 'L_0', originalElement ] ));
263
- from.on.push( 'and' );
264
- }
265
- }
266
-
267
- from.on.push( createColumnRef( [ 'localized_1', 'locale' ] ) );
268
- from.on.push( '=' );
269
- from.on.push( createColumnRef( [ '$user', 'locale' ] ) );
249
+ from.on.push(...createJoinConditionFromLocaleElement());
270
250
 
271
251
  return from;
272
252
  }
253
+
254
+ function createJoinConditionFromLocaleElement() {
255
+ return adaptExpr(entity.elements.localized.on);
256
+
257
+ function adaptExpr(expr) {
258
+ // We only support a few specific ON-conditions, not generic expressions.
259
+ // In case of unsupported ON-conditions, we emit an error.
260
+ return expr.map((x) => {
261
+ if (!x || typeof x === 'string')
262
+ return x;
263
+ if (x.xpr)
264
+ return { xpr: adaptExpr(x.xpr) };
265
+ if (x.ref && !x.ref.some(ref => ref.args || ref.where))
266
+ return adaptRef(x);
267
+
268
+ messageFunctions.error(
269
+ 'def-invalid-localized',
270
+ [ 'definitions', entityName, 'elements', 'localized', 'on' ],
271
+ { name: 'localized', alias: entityName },
272
+ 'Element $(NAME) of entity $(ALIAS) does not have a supported ON-condition'
273
+ );
274
+ return x;
275
+ });
276
+ }
277
+
278
+ function adaptRef(expr) {
279
+ if (expr.ref[0].charAt(0) === '$') // variable
280
+ return { ref: [ ...expr.ref ] };
281
+ if (expr.ref[0] === 'localized') // target side
282
+ return { ref: [ 'localized_1', ...expr.ref.slice(1) ] };
283
+ return { ref: [ 'L_0', ...expr.ref ] }; // source side
284
+ }
285
+ }
273
286
  }
274
287
 
275
288
  /**
@@ -316,7 +329,6 @@ function _addLocalizationViews(csn, options, config) {
316
329
  if (noCoalesce)
317
330
  return createColumnRef( [ ...localizedNames, elementName ], elementName );
318
331
 
319
-
320
332
  return {
321
333
  func: 'coalesce',
322
334
  args: [
@@ -357,8 +369,8 @@ function _addLocalizationViews(csn, options, config) {
357
369
 
358
370
  /**
359
371
  * Returns all text element names for a definition `<artName>` if its texts entity
360
- * exists and `<artName>` has localized fields. Otherwise `null` is returned.
361
- * Text elements are localized elements as well as keys.
372
+ * exists and `<artName>` has localized fields. Otherwise, `null` is returned.
373
+ * Text elements are non-key localized elements.
362
374
  *
363
375
  * @param {string} artName Artifact name
364
376
  * @return {string[] | null}
@@ -367,49 +379,31 @@ function _addLocalizationViews(csn, options, config) {
367
379
  const art = csn.definitions[artName];
368
380
  const artPath = [ 'definitions', artName ];
369
381
 
370
- let keyCount = 0;
371
- let textElements = [];
382
+ const localizedElements = [];
372
383
 
373
384
  forEachGeneric(art, 'elements', (elem, elemName, _prop) => {
374
385
  if (elem.$ignore) // from SAP HANA backend
375
386
  return;
376
-
377
- if (elem.key || elem.$key)
378
- keyCount += 1;
379
-
380
- if (elem.key || elem.$key || elem.localized)
381
- textElements.push( elemName );
387
+ if (elem.localized && !elem.key && !elem.$key)
388
+ localizedElements.push( elemName );
382
389
  }, artPath);
383
390
 
384
- if (textElements.length <= keyCount || keyCount <= 0)
391
+ if (!localizedElements.length) {
385
392
  // Nothing to do: no localized fields or all localized fields are keys
386
393
  return null;
387
-
388
- if (!isEntityPreprocessed( art )) {
389
- messageFunctions.info(
390
- null, artPath, { name: artName },
391
- 'Skipped creation of convenience view for $(NAME) because the artifact is missing localization elements'
392
- );
394
+ }
395
+ if (!art.elements.localized) {
396
+ messageFunctions.info('def-expected-localized', artPath, { '#': 'missing', name: artName, alias: 'localized' });
397
+ return null;
398
+ }
399
+ if (!art.elements.localized.target) {
400
+ messageFunctions.info('def-expected-localized', artPath, { '#': 'non-assoc', name: artName, alias: 'localized' });
393
401
  return null;
394
402
  }
395
403
 
396
404
  const textsName = textsEntityName( artName );
397
405
  const textsEntity = csn.definitions[textsName];
398
406
 
399
- if (!textsEntity) {
400
- messageFunctions.info(
401
- null, artPath, { name: artName },
402
- 'Skipped creation of convenience view for $(NAME) because its texts entity could not be found'
403
- );
404
- return null;
405
- }
406
- if (!isValidTextsEntity( textsEntity )) {
407
- messageFunctions.info(
408
- null, [ 'definitions', textsName ], { name: artName },
409
- 'Skipped creation of convenience view for $(NAME) because its texts entity does not appear to be valid'
410
- );
411
- return null;
412
- }
413
407
  if (!art[annoPersistenceSkip] && textsEntity[annoPersistenceSkip]) {
414
408
  messageFunctions.message(
415
409
  'anno-unexpected-localized-skip', artPath,
@@ -418,22 +412,10 @@ function _addLocalizationViews(csn, options, config) {
418
412
  return null;
419
413
  }
420
414
 
421
- // There may be keys in the original artifact that were added by the core compiler,
422
- // for example elements that are marked @cds.valid.from.
423
- // These keys are not present in the texts entity generated by the compiler.
424
- // So if we don't filter them out, we may generate invalid SQL.
425
- textElements = textElements.filter((elemName) => {
426
- const hasElement = !!textsEntity.elements[elemName];
427
- if (!hasElement && (art.elements[elemName].key || art.elements[elemName].$key))
428
- keyCount--;
429
- return hasElement;
430
- });
431
-
432
- if (textElements.length <= keyCount || keyCount <= 0)
433
- // Repeat the check already used above as the number of keys may have changed.
434
- return null;
435
-
436
- return textElements;
415
+ // Due to recompilation / flattening, properties may have been propagated from "type-of".
416
+ // That means we have localized elements with no corresponding element in the texts-entity.
417
+ // Hence, we simply filter here.
418
+ return localizedElements.filter(elemName => textsEntity.elements[elemName]);
437
419
  }
438
420
 
439
421
  /**
@@ -646,8 +628,8 @@ function _addLocalizationViews(csn, options, config) {
646
628
  * @param {string} artName
647
629
  */
648
630
  function textsEntityName(artName) {
649
- // We can assume that the element exists. This is checked in isEntityPreprocessed().
650
- return csn.definitions[artName].elements.texts.target;
631
+ // We can assume that the element exists.
632
+ return csn.definitions[artName].elements.localized.target;
651
633
  }
652
634
 
653
635
  /**
@@ -808,33 +790,6 @@ function checkExistingLocalizationViews(csn, options, messageFunctions) {
808
790
  return hasExistingViews || hasNonViews;
809
791
  }
810
792
 
811
- /**
812
- * Returns true if the given entity appears to be a valid texts entity.
813
- *
814
- * @param {CSN.Artifact} entity
815
- */
816
- function isValidTextsEntity(entity) {
817
- if (!entity)
818
- return false;
819
- const requiredTextsProps = [ 'locale' ];
820
- return requiredTextsProps.some( prop => !!entity.elements[prop]);
821
- }
822
-
823
- /**
824
- * Returns true if the localized entity has elements that are generated by
825
- * the core-compiler. If elements are missing but the entity is localized
826
- * then the pre-processing by the core-compiler was not done.
827
- *
828
- * @param {CSN.Artifact} entity
829
- */
830
- function isEntityPreprocessed(entity) {
831
- if (!entity)
832
- return false;
833
- if (!entity.elements.localized)
834
- return false;
835
- return entity.elements.texts && entity.elements.texts.target;
836
- }
837
-
838
793
  /**
839
794
  * @param {string} name
840
795
  * @returns {boolean}
@@ -1,15 +1,20 @@
1
1
  'use strict';
2
2
 
3
- const { applyTransformations, transformAnnotationExpression } = require('../../model/csnUtils');
4
- const { isBuiltinType } = require('../../base/builtins');
3
+ const { applyTransformations, transformAnnotationExpression, implicitAs } = require('../../model/csnUtils');
5
4
 
6
5
 
7
- function replaceForeignKeyRefsInExpressionAnnotations(csn, options, messageFunctions, csnUtils, iterateOptions = {}) {
6
+ /**
7
+ * If a path in an annotation expression can be interpreted as accessing a local foreign key, then
8
+ * the foreign key reference is replaced with the foreign key itself.
9
+ * Exception is when the association path step has a filter.
10
+ *
11
+ * @param {CSN} csn
12
+ * @param {Object} csnUtils
13
+ * @param {Object} iterateOptions
14
+ */
15
+ function replaceForeignKeyRefsInExpressionAnnotations(csn, csnUtils, iterateOptions = {}) {
8
16
  const transformers = {
9
- elements: processRef,
10
- params: processRef,
11
- actions: processRef,
12
- // '@': processRef
17
+ '@': processRef,
13
18
  };
14
19
  applyTransformations(csn, transformers, [ processRef ], iterateOptions);
15
20
 
@@ -17,29 +22,27 @@ function replaceForeignKeyRefsInExpressionAnnotations(csn, options, messageFunct
17
22
  transformAnnotationExpression(parent, prop, {
18
23
  ref: (parent, _prop, ref, path, _p, _ppn, ctx) => {
19
24
  const { art, links }
20
- = (parent._art && parent._links)
25
+ = parent._art && parent._links
21
26
  ? { art: parent._art, links: parent._links }
22
27
  : csnUtils.inspectRef(path);
28
+
23
29
  // if a reference points to a structure(managed assoc or structured element), then we do not process
24
30
  // as we can't guess which specific foreign key is targeted
25
- if (!art || csnUtils.isManagedAssociation(art) || csnUtils.isStructured(art))
31
+ if (
32
+ !art ||
33
+ csnUtils.isManagedAssociation(art) ||
34
+ csnUtils.isStructured(art)
35
+ )
26
36
  return;
27
37
 
28
- const allMngAssocsInRef = links.filter(link => csnUtils.isManagedAssociation(link.art));
29
- if (!allMngAssocsInRef.length)
30
- return;
31
- let firstAssocToProcess = allMngAssocsInRef[0];
38
+ const modifiedRef = replaceRefsWithFKs(ref, links, art);
32
39
 
33
- const mngAssocsWithFilter = allMngAssocsInRef.filter(assoc => typeof ref[assoc.idx] !== 'string');
34
- if (mngAssocsWithFilter.length) {
35
- const refTail = links.slice(mngAssocsWithFilter.at(-1).idx + 1);
36
- firstAssocToProcess = refTail.find(link => csnUtils.isManagedAssociation(link.art));
37
- }
38
-
39
- const match = findMatchingForeignKeyForAssoc(firstAssocToProcess, art, ref, links);
40
- if (match) {
41
- const refHead = ref.slice(0, match.idx);
42
- parent.ref = [ ...refHead, match.fkName ];
40
+ // update the ref and string token to true if there was FK replacement
41
+ if (
42
+ modifiedRef.length !== ref.length ||
43
+ !modifiedRef.every((val, index) => val === ref[index])
44
+ ) {
45
+ parent.ref = modifiedRef;
43
46
  if (ctx?.annoExpr?.['='])
44
47
  ctx.annoExpr['='] = true;
45
48
  }
@@ -48,50 +51,73 @@ function replaceForeignKeyRefsInExpressionAnnotations(csn, options, messageFunct
48
51
  path);
49
52
  }
50
53
 
51
- function findMatchingForeignKeyForAssoc(assoc, refArt, ref, links) {
52
- if (!assoc)
53
- return undefined;
54
-
55
- const expectedFkName = findExpectedFkName(assoc, ref, links);
56
- const gfks = assoc.art?.$generatedForeignKeys;
57
- if (!gfks)
58
- return undefined;
59
- const matchedFk = gfks.find(fk => fk.source === refArt && fk.name === expectedFkName);
60
- if (matchedFk)
61
- return { fkName: matchedFk.name, idx: assoc.idx };
62
-
63
- // try to find FK substitution in the next assoc in the ref (if there is such assoc)
64
- const refTail = links.slice(assoc.idx + 1);
65
- const nextAssoc = refTail.find(link => csnUtils.isManagedAssociation(link.art));
66
- return findMatchingForeignKeyForAssoc(nextAssoc, refArt, ref, links);
67
-
68
-
69
- function findExpectedFkName(assoc, ref, links) {
70
- let expectedFkName = ref[assoc.idx];
71
- const refAliasMapping = assoc.art.keys.reduce( (acc, key) => {
72
- acc[key.ref.join('_')] = key.as;
73
- return acc;
74
- }, {});
75
- let bufferRef = [];
76
- for (let i = assoc.idx + 1; i < links.length; i++) {
77
- const link = links[i];
78
- bufferRef.push(ref[i]);
79
- if (csnUtils.isManagedAssociation(link.art)) {
80
- const subFkName = findExpectedFkName(link, ref, links);
81
- if (!subFkName)
82
- return undefined;
83
- expectedFkName += bufferRef.length > 1
84
- ? `_${ bufferRef.slice(0, -1).join('_') }_${ subFkName }`
85
- : `_${ subFkName }`;
86
- break;
54
+ // Replace references to foreign keys
55
+ function replaceRefsWithFKs(originalRef, links, expectedFkArt) {
56
+ let result = [ ...originalRef ];
57
+ // stringify the tail of the ref for finding the potential foreign key
58
+ const refTail = [ originalRef[originalRef.length - 1] ];
59
+
60
+ for (let i = originalRef.length - 2; i >= 0; i--) {
61
+ const currentRef = originalRef[i];
62
+ const currentLink = links[i].art;
63
+
64
+ // skip processing if the current reference is a filter
65
+ if (typeof currentRef !== 'string')
66
+ return result;
67
+
68
+ // check if the current link is a managed association
69
+ if (csnUtils.isManagedAssociation(currentLink)) {
70
+ const matchedForeignKey = findMatchingForeignKeyForAssoc(currentLink, currentRef, refTail, expectedFkArt);
71
+
72
+ if (matchedForeignKey) {
73
+ // update the result and refTailAsStr with the matched foreign key
74
+ result = [ ...result.slice(0, i), matchedForeignKey.name ];
75
+ refTail.unshift(currentRef);
87
76
  }
88
- else if (isBuiltinType(link.art.type)) {
89
- expectedFkName += `_${ refAliasMapping[bufferRef.join('_')] || ref[i] }`;
90
- bufferRef = [];
77
+ else {
78
+ return result; // return if no matching foreign key is found
91
79
  }
92
80
  }
93
- return expectedFkName;
81
+ else {
82
+ // update refTail for non-association links
83
+ refTail.unshift(currentRef);
84
+ }
85
+ }
86
+ return result;
87
+ }
88
+
89
+ // Lookup the foreign key in the association's generated foreign keys
90
+ function findMatchingForeignKeyForAssoc(assoc, assocName, refTail, expectedFkArt) {
91
+ const expectedFkName = getExpectedForeignKeyName(assoc, assocName, refTail);
92
+ const matchedFk = assoc.$generatedForeignKeys?.find(fk => fk.source === expectedFkArt && fk.name === expectedFkName);
93
+ return matchedFk;
94
+ }
95
+
96
+ // Generate the expected foreign key name, considering aliases, tuple expansion name changes, etc.
97
+ function getExpectedForeignKeyName(assoc, assocName, refTail) {
98
+ const refAliasMapping = assoc.keys.reduce( (acc, key) => {
99
+ acc[key.ref.join('_')] = key.as || implicitAs(key.ref);
100
+ return acc;
101
+ }, {});
102
+ // generate the string representation of the reference tail
103
+ const refTailAsStr = replaceRefsIfAliased(refTail, refAliasMapping) || refTail.join('_');
104
+ return `${ assocName }_${ refTailAsStr }`;
105
+ }
106
+
107
+ // Check if any prefix of refTail matches an alias in refAliasMapping and replace it
108
+ function replaceRefsIfAliased(refTail, refAliasMapping) {
109
+ // loop through refTail and try to find a match in the refAliasMapping
110
+ // no need to look for the longest match as it is not allowed to declare
111
+ // duplicate key references in one FKs scope
112
+ let candidate = '';
113
+ for (let idx = 0; idx < refTail.length; idx++) {
114
+ candidate = candidate ? `${ candidate }_${ refTail[idx] }` : refTail[idx];
115
+ if (refAliasMapping[candidate]) {
116
+ refTail.splice(0, idx + 1, refAliasMapping[candidate]);
117
+ return refTail.join('_');
118
+ }
94
119
  }
120
+ return undefined;
95
121
  }
96
122
  }
97
123
 
@@ -54,7 +54,7 @@ function getTransformers(model, options, msgFunctions, pathDelimiter = '_') {
54
54
  extractValidFromToKeyElement,
55
55
  checkMultipleAssignments,
56
56
  checkAssignment,
57
- recurseElements,
57
+ recurseElements, // standalone-function
58
58
  renameAnnotation,
59
59
  setAnnotation,
60
60
  resetAnnotation,
@@ -808,26 +808,6 @@ function getTransformers(model, options, msgFunctions, pathDelimiter = '_') {
808
808
  }
809
809
  }
810
810
 
811
- /**
812
- * Calls `callback` for each element in `elements` property of `artifact` recursively.
813
- *
814
- * @param {CSN.Artifact} artifact the artifact
815
- * @param {CSN.Path} path path to get to `artifact` (mainly used for error messages)
816
- * @param {(art: CSN.Artifact, path: CSN.Path) => any} callback Function called for each element recursively.
817
- */
818
- function recurseElements(artifact, path, callback) {
819
- callback(artifact, path);
820
- const { elements } = artifact;
821
- if (elements) {
822
- path.push('elements', null);
823
- forEach(elements, (name, obj) => {
824
- path[path.length - 1] = name;
825
- recurseElements(obj, path, callback);
826
- });
827
- // reset path for subsequent usages
828
- path.length -= 2; // equivalent to 2x pop()
829
- }
830
- }
831
811
 
832
812
  // Rename annotation 'fromName' in 'node' to 'toName' (both names including '@')
833
813
  function renameAnnotation(node, fromName, toName) {
@@ -920,8 +900,30 @@ function rewriteBuiltinTypeRef(csn) {
920
900
  });
921
901
  }
922
902
 
903
+ /**
904
+ * Calls `callback` for each element in `elements` property of `artifact` recursively.
905
+ *
906
+ * @param {CSN.Artifact} artifact the artifact
907
+ * @param {CSN.Path} path path to get to `artifact` (mainly used for error messages)
908
+ * @param {(art: CSN.Artifact, path: CSN.Path) => any} callback Function called for each element recursively.
909
+ */
910
+ function recurseElements(artifact, path, callback) {
911
+ callback(artifact, path);
912
+ const { elements } = artifact;
913
+ if (elements) {
914
+ path.push('elements', null);
915
+ forEach(elements, (name, obj) => {
916
+ path[path.length - 1] = name;
917
+ recurseElements(obj, path, callback);
918
+ });
919
+ // reset path for subsequent usages
920
+ path.length -= 2; // equivalent to 2x pop()
921
+ }
922
+ }
923
+
923
924
  module.exports = {
924
925
  // This function retrieves the actual exports
925
926
  getTransformers,
926
927
  rewriteBuiltinTypeRef,
928
+ recurseElements,
927
929
  };
@@ -487,10 +487,12 @@ function translateAssocsToJoins(model, inputOptions = {}) {
487
487
  // create a new toplevel AND op otherwise
488
488
  const onCond = (Array.isArray(node.on) ? node.on[0] : node.on);
489
489
 
490
- if (onCond.op.val === 'and')
491
- onCond.args.push(parenthesise(filter));
492
- else
493
- node.on = parenthesise({ op: { val: 'and' }, args: [ parenthesise(onCond), parenthesise(filter) ] });
490
+ if (filter.args?.length !== 0) {
491
+ if (onCond.op.val === 'and')
492
+ onCond.args.push(parenthesise(filter));
493
+ else
494
+ node.on = parenthesise({ op: { val: 'and' }, args: [ parenthesise(onCond), parenthesise(filter) ] });
495
+ }
494
496
  }
495
497
  return node;
496
498
 
@@ -1496,7 +1498,7 @@ function translateAssocsToJoins(model, inputOptions = {}) {
1496
1498
  */
1497
1499
  let qatName = pathStep.id;
1498
1500
 
1499
- if (pathStep.where)
1501
+ if (pathStep.where && pathStep.where?.args?.length !== 0)
1500
1502
  qatName += JSON.stringify(compactExpr(pathStep.where));
1501
1503
 
1502
1504
  if (pathStep.args) {
@@ -14,7 +14,7 @@ const { cloneCsnNonDict } = require('../model/cloneCsn');
14
14
  *
15
15
  * @type {string[]}
16
16
  */
17
- const RelationalOperators = [ '=', '<>', '==', '!=', 'is', 'is not' /* , 'like' */ ];
17
+ const RelationalOperators = [ '=', '<>', '==', '!=', 'is', 'is not' ];
18
18
 
19
19
  /**
20
20
  * Operators that to be used to combine expanded expressions by keeping logical relations.
@@ -64,6 +64,11 @@ function tupleExpansion(csn, csnUtils, msgFunctions) {
64
64
  having: expandExpr,
65
65
  where: expandExpr,
66
66
  xpr: expandExpr,
67
+ list: (parent, name, args, path) => {
68
+ // Don't iterate `group by (foo, bar)`
69
+ if (path.at(-2) !== 'groupBy' && path.at(-2) !== 'orderBy')
70
+ expandExpr(parent, name, args, path);
71
+ },
67
72
  args: (parent, name, args, path) => {
68
73
  if (!parent.id && !parent.func)
69
74
  return; // ensure we're not in JOIN
@@ -262,6 +267,10 @@ function tupleExpansion(csn, csnUtils, msgFunctions) {
262
267
  }
263
268
  }
264
269
 
270
+ /**
271
+ * @param expr
272
+ * @param {CSN.Path} location
273
+ */
265
274
  function rejectAnyDirectStructureReference(expr, location) {
266
275
  if (expr[0] === 'exists') {
267
276
  // we ignore WHERE EXISTS clauses; they are not relevant for OData,
@@ -318,11 +327,15 @@ function tupleExpansion(csn, csnUtils, msgFunctions) {
318
327
  * `{ _art: <leaf_artifact>, ref: [...] }`
319
328
  * with `_art` identifying `ref[ref.length-1]`
320
329
  *
321
- * A produced path has the form `{ _art: <ref>, ref: [ <id> (, <id>)* ] }`
330
+ * A produced path has the form `{ _art: <ref>, ref: [ <id> (, <id>)* ], comparisonRef: [ <id> (, <id>)* ] }`
322
331
  *
323
332
  * Flattening stops on all non-structured elements, if followMgdAssoc=false.
324
333
  *
325
- * If fullRef is true, a path step is produced as `{ id: <id>, _art: <link> }`
334
+ * If fullRef is true, a path step is produced as `{ id: <id>, _art: <link> }`.
335
+ *
336
+ * The returned paths will have a property 'comparisonRef', that may differ from 'ref'
337
+ * for managed associations (as it uses the foreign key name).
338
+ * The caller may need to delete that property.
326
339
  */
327
340
  function flattenPath(path, fullRef = false, followMgdAssoc = false) {
328
341
  let art = path._art;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds-compiler",
3
- "version": "6.3.4",
3
+ "version": "6.4.2",
4
4
  "description": "CDS (Core Data Services) compiler and backends",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "author": "SAP SE (https://www.sap.com)",