@sap/cds-compiler 6.5.2 → 6.6.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.
@@ -7,6 +7,7 @@ const {
7
7
  } = require('../../model/csnUtils');
8
8
  const { forEachKey } = require('../../utils/objectUtils');
9
9
  const { cloneCsnDict, cloneCsnNonDict } = require('../../model/cloneCsn');
10
+ const propertiesToSkipForCasts = { enum: true };
10
11
 
11
12
  /**
12
13
  * Resolve all references to structured types in entities to the underlying elements.
@@ -26,8 +27,14 @@ function resolveTypes( csn, csnUtils, transformers, options ) {
26
27
  const { flattenStructStepsInRef } = transformers;
27
28
  const later = [];
28
29
  applyTransformations(csn, {
30
+ cast: (parent, prop, cast, path, grandParent, grandParentProp) => {
31
+ if (cast.type && path.at(-2) !== 'columns' && typeof grandParentProp === 'number') // SQL cast - not happy with this :(
32
+ resolveType(csnUtils, cast, true);
33
+ else if (cast.type) // cdl style cast
34
+ resolveType(csnUtils, cast, false);
35
+ },
29
36
  type: (parent) => {
30
- resolveType(csnUtils, parent);
37
+ resolveType(csnUtils, parent, false);
31
38
  },
32
39
  }, [ (definitions, artifactName, artifact) => {
33
40
  // In a non-flat model, replacing types with some $self inside causes issues for actions (bound or unbound)
@@ -36,7 +43,7 @@ function resolveTypes( csn, csnUtils, transformers, options ) {
36
43
  later.push([ { [artifactName]: artifact }, [ 'definitions' ] ]);
37
44
  else if (artifact.actions)
38
45
  later.push([ artifact.actions, [ 'definitions', artifactName, 'actions' ] ]);
39
- } ], { skipStandard: { returns: true }, processAnnotations: true, skipDict: { actions: true } });
46
+ } ], { skipStandard: { returns: true, cast: true }, processAnnotations: true, skipDict: { actions: true } });
40
47
 
41
48
  // Type refs like E:struct.sub can not be resolved later, as struct will be flattened. So we rewrite it to E:struct_sub here.
42
49
  later.forEach(([ action, actionPath ]) => {
@@ -52,8 +59,8 @@ function resolveTypes( csn, csnUtils, transformers, options ) {
52
59
  return function resolveTypesInActions(refreshedCsnUtils) {
53
60
  later.forEach(([ action ]) => {
54
61
  applyTransformationsOnDictionary(action, {
55
- type: (parent) => {
56
- resolveType(refreshedCsnUtils, parent);
62
+ type: (parent, prop, type, path) => {
63
+ resolveType(refreshedCsnUtils, parent, path);
57
64
  },
58
65
  });
59
66
  });
@@ -70,7 +77,7 @@ function resolveTypes( csn, csnUtils, transformers, options ) {
70
77
  *
71
78
  * @param {object} parent Object with a .type property
72
79
  */
73
- function resolveType( csnUtils, parent ) {
80
+ function resolveType( csnUtils, parent, isCast ) {
74
81
  // TODO: I assume there can be cases with a type ref but still having .elements already? Subelement anno?
75
82
  const final = csnUtils.getFinalTypeInfo(parent.type);
76
83
  if (final?.elements) {
@@ -86,7 +93,7 @@ function resolveTypes( csn, csnUtils, transformers, options ) {
86
93
  }
87
94
  else if (final?.type && (options.resolveSimpleTypes || parent.type.ref?.length > 1)) {
88
95
  forEachKey(final, (key) => { // copy `type` + properties (default, etc.)
89
- if (parent[key] === undefined || key === 'type')
96
+ if ((parent[key] === undefined || key === 'type') && (isCast && !propertiesToSkipForCasts[key] || !isCast))
90
97
  parent[key] = final[key];
91
98
  });
92
99
  }
@@ -95,17 +102,31 @@ function resolveTypes( csn, csnUtils, transformers, options ) {
95
102
  const stack = [ ];
96
103
  if (parent.elements || parent.items)
97
104
  stack.push(parent);
105
+ const typeTransformer = getTypeTransformer(csnUtils, stack);
106
+ const typeInCastTransformer = getTypeTransformer(csnUtils, stack, true);
98
107
  while (stack.length > 0) {
99
108
  const obj = stack.pop();
100
109
  if (obj.elements) {
101
110
  applyTransformationsOnDictionary(obj.elements, {
102
- type: getTypeTransformer(csnUtils, stack),
111
+ cast: (parent, prop, cast, path, grandParent, grandParentProp) => {
112
+ if (cast.type && path.at(-2) !== 'columns' && typeof grandParentProp === 'number')
113
+ typeInCastTransformer(cast, 'type', cast.type);
114
+ else if (cast.type)
115
+ typeTransformer(cast, 'type', cast.type);
116
+ },
117
+ type: typeTransformer,
103
118
  }, { skipDict: { actions: true } });
104
119
  }
105
120
  else if (obj.items) {
106
121
  applyTransformationsOnNonDictionary(obj, 'items', {
107
- type: getTypeTransformer(csnUtils, stack),
108
- }, { skipDict: { actions: true } });
122
+ cast: (parent, prop, cast, path, grandParent, grandParentProp) => {
123
+ if (cast.type && path.at(-2) !== 'columns' && typeof grandParentProp === 'number')
124
+ typeInCastTransformer(cast, 'type', cast.type);
125
+ else if (cast.type)
126
+ typeTransformer(cast, 'type', cast.type);
127
+ },
128
+ type: typeTransformer,
129
+ }, { skipDict: { actions: true }, skipStandard: { cast: true } });
109
130
  }
110
131
  }
111
132
  }
@@ -115,7 +136,7 @@ function resolveTypes( csn, csnUtils, transformers, options ) {
115
136
  * @param {object[]} stack
116
137
  * @returns {Function}
117
138
  */
118
- function getTypeTransformer( csnUtils, stack ) {
139
+ function getTypeTransformer( csnUtils, stack, isCast ) {
119
140
  return function typeTransformer( _parent, _prop, _type ) {
120
141
  const finalSub = csnUtils.getFinalTypeInfo(_type);
121
142
 
@@ -132,8 +153,11 @@ function resolveTypes( csn, csnUtils, transformers, options ) {
132
153
  delete _parent.type;
133
154
  stack.push( _parent );
134
155
  }
135
- else if (finalSub?.type) {
136
- _parent.type = finalSub.type;
156
+ else if (finalSub?.type && (options.resolveSimpleTypes || _parent.type.ref?.length > 1)) {
157
+ forEachKey(finalSub, (key) => { // copy `type` + properties (default, etc.)
158
+ if ((_parent[key] === undefined || key === 'type') && (isCast && !propertiesToSkipForCasts[key] || !isCast))
159
+ _parent[key] = finalSub[key];
160
+ });
137
161
  }
138
162
  };
139
163
  }
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { isBetaEnabled } = require('../base/model');
4
+ const { isBuiltinType } = require('../base/builtins');
4
5
  const transformUtils = require('./transformUtils');
5
6
  const {
6
7
  forEachDefinition,
@@ -91,9 +92,10 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
91
92
 
92
93
  const transformers = transformUtils.getTransformers(csn, options, messageFunctions, '_');
93
94
  const {
94
- addDefaultTypeFacets, checkMultipleAssignments,
95
- recurseElements, setAnnotation, renameAnnotation,
96
- expandStructsInExpression,
95
+ addDefaultTypeFacets, addElement,
96
+ checkMultipleAssignments, createScalarElement,
97
+ recurseElements, setAnnotation,
98
+ renameAnnotation, expandStructsInExpression,
97
99
  csnUtils,
98
100
  } = transformers;
99
101
 
@@ -293,6 +295,9 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
293
295
  // Resolve annotation shorthands for entities, types, annotations, ...
294
296
  renameShorthandAnnotations(def);
295
297
 
298
+ // Generate annotations and fields needed for the Fiori Tree Views out of the @hierarchy annotation
299
+ generateFioriTreeViewAnnotationsAndFields(def, defName);
300
+
296
301
  // Annotate artifacts with their DB names if requested.
297
302
  // Skip artifacts that have no DB equivalent anyway
298
303
  if (options.sqlMapping && !(def.kind in skipPersNameKinds))
@@ -360,6 +365,124 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
360
365
  //--------------------------------------------------------------------
361
366
  // HELPER SECTION STARTS HERE
362
367
 
368
+ // =====================================================================
369
+ // Generate annotations and fields needed for the Fiori Tree Views out of the @hierarchy annotation
370
+ function generateFioriTreeViewAnnotationsAndFields(def, defName) {
371
+ const re = new RegExp('^@hierarchy#.+$');
372
+ if (Object.keys(def).some(key => re.test(key)))
373
+ error(null, [ 'definitions', defName ], {}, 'Assigning qualifier with the @hierarchy annotation is not supported');
374
+
375
+ if (!def['@hierarchy'])
376
+ return;
377
+ if (!(def.projection || def.query))
378
+ return;
379
+
380
+ const defKeys = [];
381
+ const assocs = [];
382
+ Object.entries(def.elements).forEach(([ name, elem ]) => {
383
+ if (elem.key)
384
+ defKeys.push([ name, elem ]);
385
+ if (isAssociation(elem) && elem.target === defName)
386
+ assocs.push([ name, elem ]);
387
+ });
388
+
389
+ // the definition must have exactly one scalar key field
390
+ if (defKeys.length !== 1 && !isBuiltinType(defKeys[0].type))
391
+ return;
392
+ const defKeyName = defKeys[0][0];
393
+
394
+ // if the @hierarchy annotation has a true value, we need to make sure the definition has exacly one
395
+ // association pointing to self and has the single key field as a foreign key
396
+ if (typeof def['@hierarchy'] === 'boolean' && def['@hierarchy'] === true) {
397
+ if (assocs.length !== 1)
398
+ return;
399
+ const assocName = assocs[0][0];
400
+ const assoc = assocs[0][1];
401
+ if (!isValidHierarchyAssociation(assoc, defKeyName))
402
+ return;
403
+ addHierarchyAnnotations(def, defName, defKeyName, assocName);
404
+ addHierarchyFields(def, defName);
405
+ }
406
+ else if (typeof def['@hierarchy'] === 'object' && def['@hierarchy']['=']) {
407
+ const assocName = def['@hierarchy']['='];
408
+ if (!isValidHierarchyAssociation(assocs.find(a => a[0] === assocName)[1], defKeyName))
409
+ return;
410
+ addHierarchyAnnotations(def, defName, defKeyName, assocName);
411
+ addHierarchyFields(def, defName);
412
+ }
413
+ }
414
+
415
+ // returns true if assoc has one key and that one key is the corresponding 'keyName'
416
+ function isValidHierarchyAssociation(assoc, keyName) {
417
+ return assoc.keys.length === 1 && assoc.keys[0].ref.join() === keyName;
418
+ }
419
+
420
+ function addHierarchyAnnotations(def, defName, defKeyName, assocName) {
421
+ const qualifier = `${ defName.split('.').pop() }Hierarchy`;
422
+
423
+ [ `@Aggregation.RecursiveHierarchy#${ qualifier }`,
424
+ `@Aggregation.RecursiveHierarchy#${ qualifier }.NodeProperty`,
425
+ `@Aggregation.RecursiveHierarchy#${ qualifier }.ParentNavigationProperty`,
426
+ `@Hierarchy.RecursiveHierarchy#${ qualifier }`,
427
+ `@Hierarchy.RecursiveHierarchy#${ qualifier }.LimitedDescendantCount`,
428
+ `@Hierarchy.RecursiveHierarchy#${ qualifier }.DistanceFromRoot`,
429
+ `@Hierarchy.RecursiveHierarchy#${ qualifier }.DrillState`,
430
+ `@Hierarchy.RecursiveHierarchy#${ qualifier }.LimitedRank` ].forEach(anno => checkAndReportErrorWhenAnnotationExists(def, defName, anno));
431
+
432
+ setAnnotation(def, `@Aggregation.RecursiveHierarchy#${ qualifier }.NodeProperty`, { '=': defKeyName });
433
+ setAnnotation(def, `@Aggregation.RecursiveHierarchy#${ qualifier }.ParentNavigationProperty`, { '=': assocName });
434
+ setAnnotation(def, `@Hierarchy.RecursiveHierarchy#${ qualifier }.LimitedDescendantCount`, { '=': 'LimitedDescendantCount' });
435
+ setAnnotation(def, `@Hierarchy.RecursiveHierarchy#${ qualifier }.DistanceFromRoot`, { '=': 'DistanceFromRoot' });
436
+ setAnnotation(def, `@Hierarchy.RecursiveHierarchy#${ qualifier }.DrillState`, { '=': 'DrillState' });
437
+ setAnnotation(def, `@Hierarchy.RecursiveHierarchy#${ qualifier }.LimitedRank`, { '=': 'LimitedRank' });
438
+
439
+ // if @Capabilities.FilterRestrictions.NonFilterableProperties or @Capabilities.SortRestrictions.NonSortableProperties
440
+ // are already defined, we append to the existing value the created elements
441
+ if (def['@Capabilities.FilterRestrictions.NonFilterableProperties'] && Array.isArray(def['@Capabilities.FilterRestrictions.NonFilterableProperties'])) {
442
+ def['@Capabilities.FilterRestrictions.NonFilterableProperties'].push('LimitedDescendantCount', 'DistanceFromRoot', 'DrillState', 'LimitedRank');
443
+ }
444
+ else {
445
+ setAnnotation(def, '@Capabilities.FilterRestrictions.NonFilterableProperties',
446
+ [ 'LimitedDescendantCount', 'DistanceFromRoot', 'DrillState', 'LimitedRank' ]);
447
+ }
448
+
449
+ if (def['@Capabilities.SortRestrictions.NonSortableProperties'] && Array.isArray(def['@Capabilities.SortRestrictions.NonSortableProperties'])) {
450
+ def['@Capabilities.FilterRestrictions.NonFilterableProperties'].push('LimitedDescendantCount', 'DistanceFromRoot', 'DrillState', 'LimitedRank');
451
+ }
452
+ else {
453
+ setAnnotation(def, '@Capabilities.SortRestrictions.NonSortableProperties',
454
+ [ 'LimitedDescendantCount', 'DistanceFromRoot', 'DrillState', 'LimitedRank' ]);
455
+ }
456
+ }
457
+
458
+ function addHierarchyFields(def, defName) {
459
+ const limitedDescendantCount = createScalarElement('LimitedDescendantCount', 'cds.Integer');
460
+ limitedDescendantCount.LimitedDescendantCount['@Core.Computed'] = true;
461
+ limitedDescendantCount.LimitedDescendantCount.$calc = { val: null };
462
+ addElement(limitedDescendantCount, def, defName);
463
+
464
+ const distanceFromRoot = createScalarElement('DistanceFromRoot', 'cds.Integer');
465
+ distanceFromRoot.DistanceFromRoot['@Core.Computed'] = true;
466
+ distanceFromRoot.DistanceFromRoot.$calc = { val: null };
467
+ addElement(distanceFromRoot, def, defName);
468
+
469
+ const drillState = createScalarElement('DrillState', 'cds.String');
470
+ drillState.DrillState['@Core.Computed'] = true;
471
+ drillState.DrillState.$calc = { val: null };
472
+ addElement(drillState, def, defName);
473
+
474
+ const limitedRank = createScalarElement('LimitedRank', 'cds.Integer');
475
+ limitedRank.LimitedRank['@Core.Computed'] = true;
476
+ limitedRank.LimitedRank.$calc = { val: null };
477
+ addElement(limitedRank, def, defName);
478
+ }
479
+
480
+ function checkAndReportErrorWhenAnnotationExists(def, defName, anno) {
481
+ if (def[anno])
482
+ error(null, [ 'definitions', defName ], { anno, name: defName }, 'Annotation $(ANNO) is already defined on the hierarchy entity $(NAME)');
483
+ }
484
+ // =====================================================================
485
+
363
486
  // Transform @readonly/@mandatory/@disabled into @Common.FieldControl annotation
364
487
  // with a when/then/else expression consisting of the input from the annotations.
365
488
  function processDynamicFieldControlAnnotations(node) {
@@ -81,6 +81,7 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
81
81
  let expandStructsInExpression;
82
82
  let flattenStructuredElement;
83
83
  let flattenStructStepsInRef;
84
+ let expandManagedToFksInArray;
84
85
 
85
86
  bindCsnReference();
86
87
 
@@ -219,8 +220,6 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
219
220
  }
220
221
  });
221
222
 
222
- processCalculatedElementsInEntities(csn, options);
223
-
224
223
  timetrace.start('Transform CSN');
225
224
 
226
225
  // Rename primitive types, make UUID a String; replace `items` by cds.LargeString
@@ -255,6 +254,15 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
255
254
  forEachDefinition(csn, flattenIndexes);
256
255
  // Managed associations get an on-condition - in views and entities
257
256
  associations.attachOnConditions(csn, csnUtils, pathDelimiter);
257
+
258
+ // Add the foreign keys also to the columns if the association itself was explicitly selected
259
+ // TODO: Extend the expansion to also expand managed to their foreign
260
+ // TODO: Remember where in .elements we had associations and do it then?
261
+ applyTransformations(csn, {
262
+ columns: expandManagedToFksInArray(),
263
+ groupBy: expandManagedToFksInArray(true),
264
+ orderBy: expandManagedToFksInArray(true),
265
+ }, [], { allowArtifact: artifact => artifact.query !== undefined || artifact.projection !== undefined });
258
266
  }
259
267
 
260
268
  {
@@ -265,11 +273,11 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
265
273
  // To be done after handleAssociations, since then the foreign keys of the managed assocs
266
274
  // are part of the elements
267
275
  if (doA2J)
268
- fns.push(associations.getFKAccessFinalizer(csn, options, csnUtils, pathDelimiter));
276
+ fns.push(associations.getFKAccessFinalizer(csn, options, csnUtils, pathDelimiter, true));
269
277
 
270
278
  forEachDefinition(csn, fns);
271
279
  }
272
-
280
+ processCalculatedElementsInEntities(csn, options);
273
281
  // Create convenience views for localized entities/views.
274
282
  // To be done after getFKAccessFinalizer because associations are
275
283
  // handled and before handleDBChecks which removes the localized attribute.
@@ -474,6 +482,7 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
474
482
  addDefaultTypeFacets,
475
483
  expandStructsInExpression,
476
484
  csnUtils,
485
+ expandManagedToFksInArray,
477
486
  } = transformUtils.getTransformers(csn, options, messageFunctions, pathDelimiter));
478
487
  }
479
488
 
@@ -7,7 +7,7 @@
7
7
  const { setProp } = require('../base/model');
8
8
 
9
9
  const { copyAnnotations, applyTransformations, isDeepEqual } = require('../model/csnUtils');
10
- const { getUtils } = require('../model/csnUtils');
10
+ const { getUtils, implicitAs } = require('../model/csnUtils');
11
11
  const { typeParameters } = require('../compiler/builtins');
12
12
  const { isBuiltinType } = require('../base/builtins');
13
13
  const { ModelError, CompilerAssertion } = require('../base/error');
@@ -60,6 +60,7 @@ function getTransformers(model, options, msgFunctions, pathDelimiter = '_') {
60
60
  resetAnnotation,
61
61
  expandStructsInExpression: tuples.expandStructsInExpression,
62
62
  flattenPath: tuples.flattenPath,
63
+ expandManagedToFksInArray,
63
64
  };
64
65
 
65
66
  /**
@@ -876,6 +877,55 @@ function getTransformers(model, options, msgFunctions, pathDelimiter = '_') {
876
877
  }
877
878
  return wasOverwritten;
878
879
  }
880
+
881
+ /**
882
+ * Expand managed associations in an array and insert them in-place
883
+ *
884
+ * If requested, leave out the assocs themselves
885
+ *
886
+ * @param {boolean} [killAssoc=false]
887
+ * @returns {Function} applyTransformationsCallback
888
+ */
889
+ function expandManagedToFksInArray( killAssoc = false ) {
890
+ return function expand(parent, prop, array, path) {
891
+ const newColumns = [];
892
+ for (let i = 0; i < array.length; i++) {
893
+ const col = array[i];
894
+ const element = csnUtils.getElement(col) || col.ref && csnUtils.inspectRef(path.concat(prop, i)).art;
895
+ if (!killAssoc || !element?.keys)
896
+ newColumns.push(col);
897
+ if (element?.keys)
898
+ element.keys.forEach(fk => addForeignKeyToColumns(fk, newColumns, col, options));
899
+ }
900
+ parent[prop] = newColumns;
901
+ };
902
+ }
903
+
904
+ /**
905
+ * FKs need to be added to the .columns
906
+ * @todo stolen from lib/transform/db/views.js
907
+ * @todo Can we maybe do this during expansion already?
908
+ * @param {object} foreignKey
909
+ * @param {object[]} columns
910
+ * @param {CSN.Column} associationColumn
911
+ * @param {CSN.Options} options
912
+ */
913
+ function addForeignKeyToColumns( foreignKey, columns, associationColumn, options ) {
914
+ const ref = cloneCsnNonDict(associationColumn.ref, options);
915
+ ref[ref.length - 1] = [ ref.at(-1).id || ref.at(-1) ].concat(foreignKey.as || foreignKey.ref).join(pathDelimiter);
916
+ const result = {
917
+ ref,
918
+ };
919
+ if (associationColumn.as) {
920
+ const columnName = `${ associationColumn.as }${ pathDelimiter }${ foreignKey.as || implicitAs(foreignKey.ref) }`;
921
+ result.as = columnName;
922
+ }
923
+
924
+ if (associationColumn.key)
925
+ result.key = true;
926
+
927
+ columns.push(result);
928
+ }
879
929
  }
880
930
 
881
931
  /**
@@ -152,7 +152,7 @@ function translateAssocsToJoins(model, inputOptions = {}) {
152
152
  // (same rewrite as (injected) assoc ON cond paths but with different table alias).
153
153
  // 5) Prepend table alias to all remaining paths
154
154
  env.walkover = {
155
- from: false, onCondFrom: true, select: true, filter: false,
155
+ from: false, onCondFrom: true, select: true, filter: false, virtual: true,
156
156
  };
157
157
  env.callback = substituteDollarSelf;
158
158
  forEachQuery(walkQuery, env);
@@ -340,19 +340,38 @@ function translateAssocsToJoins(model, inputOptions = {}) {
340
340
  // if the assoc is not join relevant, we should have found the foreign key
341
341
  if (tail[i]._navigation.$njr && !fk)
342
342
  throw new CompilerAssertion('Debug me: No FK found for FK rewriting');
343
-
344
- if (fk && fk.name.id !== tail[i + 1].id)
345
- tail[i + 1].id = fk.name.id; // fk renamed
346
343
  }
347
344
  }
348
- tail = [
349
- {
350
- id: tail.map(p => p.id).join(pathDelimiter),
351
- _artifact: tail[tail.length - 1]._artifact,
352
- },
353
- ];
354
- replaceNodeContent(pathNode,
355
- constructPathNode([ constructTableAliasPathStep(QA), ...tail ]));
345
+ // leave virtual paths as is
346
+ if ( tail.at(-1)._artifact.virtual?.val === true ) {
347
+ replaceNodeContent(pathNode, constructPathNode([ constructTableAliasPathStep(QA), ...tail ]));
348
+ }
349
+ else {
350
+ const newTail = [];
351
+ // construct the tail (segment after last join relevant navigation) and preserve association steps
352
+ // each association may have a structured prefix
353
+ // `struct.foo.bar.assoc. otherStruct.baz.otherAssoc.fk`
354
+ // --> `[otherStruct_baz_otherAssoc, fk]
355
+ for (let i = 0; i < tail.length; i++) {
356
+ if (tail[i]._navigation) {
357
+ const nextNavigation = tail.slice(i + 1).findIndex(ps => ps._navigation);
358
+ if (nextNavigation >= 0) {
359
+ newTail.push({
360
+ id: tail.slice(i, i + 1 + nextNavigation).map(ps => ps.id).join(pathDelimiter),
361
+ _artifact: tail[i + nextNavigation]._artifact,
362
+ });
363
+ }
364
+ else {
365
+ newTail.push({
366
+ id: tail.slice(i).map(ps => ps.id).join(pathDelimiter),
367
+ _artifact: tail.at(-1)._artifact,
368
+ });
369
+ }
370
+ }
371
+ }
372
+ replaceNodeContent(pathNode,
373
+ constructPathNode([ constructTableAliasPathStep(QA), ...newTail ]));
374
+ }
356
375
  }
357
376
 
358
377
  function findForeignKey(assoc, fk) {
@@ -777,13 +796,18 @@ function translateAssocsToJoins(model, inputOptions = {}) {
777
796
  // points to the signature of the current view and ensures that the ON cond is
778
797
  // resolvable) make sure that the original assoc target is contained in the local
779
798
  // query source
780
- if (assoc.kind === 'mixin' && i > 0 && fwdOrigin.kind !== 'mixin') {
799
+ const fwdTarget = fwdOrigin.target._artifact;
800
+ const assocSource = assoc._main;
801
+ if (assoc.kind === 'mixin' && (i > 0 || fwdTarget !== assocSource) && fwdOrigin.kind !== 'mixin') {
781
802
  const tas = Object.values(env.lead.$tableAliases);
782
- const i = tas.findIndex(ta => ta._artifact === fwdOrigin.target._artifact);
783
- if (i >= 0 && tas[i].$QA) {
784
- newTgtAlias.id = tas[i].$QA.name.id;
785
- newTgtAlias._artifact = tas[i]._effectiveType;
786
- newTgtAlias._navigation = tas[i].$QA.path[0]._navigation;
803
+ const j = tas.findIndex(ta => ta._artifact === fwdOrigin.target._artifact);
804
+ if (j >= 0 && tas[j].$QA) {
805
+ newTgtAlias.id = tas[j].$QA.name.id;
806
+ newTgtAlias._artifact = tas[j]._effectiveType;
807
+ newTgtAlias._navigation = tas[j].$QA.path[0]._navigation;
808
+ }
809
+ else if (i === 0 && fwdTarget !== assocSource) {
810
+ newTgtAlias = Object.assign(newTgtAlias, srcAlias);
787
811
  }
788
812
  else {
789
813
  error(null, [ assocQAT._origin.location, assocQAT._origin ], { name: fwdOrigin.target._artifact.name.id, art: assoc.name.id },
@@ -1823,7 +1847,7 @@ function walkPath(node, env) {
1823
1847
  const art = path?.length && path[path.length - 1]._artifact;
1824
1848
 
1825
1849
  // regardless of the position in the query, ignore paths that have virtual path steps
1826
- if (art && path.some(ps => ps._artifact?.virtual?.val))
1850
+ if (art && path.some(ps => ps._artifact?.virtual?.val) && !env.walkover.virtual)
1827
1851
  return path;
1828
1852
  if (art && !internalArtifactKinds.includes(art.kind) && node.scope !== 'param') {
1829
1853
  if (env.callback) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds-compiler",
3
- "version": "6.5.2",
3
+ "version": "6.6.0",
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)",