@sap/cds-compiler 4.9.0 → 4.9.4

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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@
7
7
  Note: `beta` fixes, changes and features are usually not listed in this ChangeLog but [here](doc/CHANGELOG_BETA.md).
8
8
  The compiler behavior concerning `beta` features can change at any time without notice.
9
9
 
10
+ ## Version 4.9.4 - 2024-05-21
11
+
12
+ ### Fixed
13
+
14
+ - to.sql:
15
+ + always include `tenant` column in foreign key references.
16
+ + reject `tenantDiscriminator` option only if sql dialect is `hana` and if `withHanaAssociations` option is set.
17
+
18
+ ## Version 4.9.2 - 2024-05-13
19
+
20
+ ### Fixed
21
+
22
+ - compiler: Rewriting annotation expression paths in structures of projections has been improved.
23
+ - to.edm(x):
24
+ + Operator `/` represents `DivBy` operator, explicit `DivBy` is replaced with `Div` as integer division.
25
+ - to.sql: consider all associations in tenant dependent entity for referential constraint generation
26
+
10
27
  ## Version 4.9.0 - 2024-04-25
11
28
 
12
29
  ### Added
package/bin/cdsc.js CHANGED
@@ -578,22 +578,12 @@ function executeCommandLine( command, options, args ) {
578
578
  // Depending on 'options.rawOutput', the model is either compacted to 'name.json' or
579
579
  // written in raw form to '<name>_raw.txt'.
580
580
  function displayNamedXsn( xsn, name ) {
581
- if (options.rawOutput) {
581
+ if (options.rawOutput)
582
582
  writeToFileOrDisplay(options.out, `${name}_raw.txt`, util.inspect(reveal(xsn, options.rawOutput), false, null), true);
583
- }
584
- else if (options.internalMsg) {
583
+ else if (options.internalMsg)
585
584
  writeToFileOrDisplay(options.out, `${name}_raw.txt`, util.inspect(reveal(xsn).messages, { depth: null, maxArrayLength: null }), true);
586
- }
587
- else if (!options.parseOnly) { // no output if parseOnly but not rawOutput
588
- const csn = compactModel(xsn, options);
589
- if (command === 'toCsn' && options.tenantDiscriminator)
590
- addTenantFields(csn, options);
591
- if (command === 'toCsn' && options.withLocalized)
592
- addLocalizationViews(csn, options);
593
- if (options.enrichCsn)
594
- enrichCsn( csn, options );
595
- writeToFileOrDisplay(options.out, `${name}.json`, csn, true);
596
- }
585
+ else if (!options.parseOnly) // no output if parseOnly but not rawOutput
586
+ displayNamedCsn(compactModel(xsn, options), name);
597
587
  }
598
588
 
599
589
  /**
@@ -603,12 +593,22 @@ function executeCommandLine( command, options, args ) {
603
593
  function displayNamedCsn( csn, name ) {
604
594
  if (!csn) // only print CSN if it is set.
605
595
  return;
596
+
597
+ if (command === 'toCsn' ) {
598
+ // If requested, run some CSN postprocessing.
599
+ if (options.tenantDiscriminator)
600
+ addTenantFields(csn, options); // always _before_ localized convenience views are added
601
+ if (options.withLocalized)
602
+ addLocalizationViews(csn, options);
603
+ }
604
+
605
+ if (options.enrichCsn)
606
+ enrichCsn( csn, options );
607
+
606
608
  if (options.internalMsg) {
607
609
  writeToFileOrDisplay(options.out, `${name}_raw.txt`, options.messages, true);
608
610
  }
609
611
  else if (!options.internalMsg) {
610
- if (command === 'toCsn' && options.withLocalized)
611
- addLocalizationViews(csn, options);
612
612
  writeToFileOrDisplay(options.out, `${name}.json`, csn, true);
613
613
  }
614
614
  }
package/lib/api/main.js CHANGED
@@ -1293,7 +1293,7 @@ function lazyload( moduleName ) {
1293
1293
  * @param {object} messageFunctions Message functions
1294
1294
  */
1295
1295
  function handleTenantDiscriminator( options, internalOptions, messageFunctions ) {
1296
- if (options.tenantDiscriminator && options.withHanaAssociations) {
1296
+ if (options.tenantDiscriminator && options.withHanaAssociations && internalOptions.sqlDialect === 'hana') {
1297
1297
  messageFunctions.error('api-invalid-combination', null, {
1298
1298
  option: 'tenantDiscriminator',
1299
1299
  prop: 'withHanaAssociations',
@@ -172,6 +172,7 @@ module.exports = {
172
172
  },
173
173
  hdi: (options) => {
174
174
  const hardOptions = { src: 'hdi', toSql: true, forHana: true };
175
+ // TODO: sqlDialect should be a hard option!
175
176
  const defaultOptions = {
176
177
  sqlMapping: 'plain', sqlDialect: 'hana', generatedByComment: false, withHanaAssociations: true,
177
178
  };
@@ -179,6 +180,7 @@ module.exports = {
179
180
  },
180
181
  hdbcds: (options) => {
181
182
  const hardOptions = { forHana: true };
183
+ // TODO: sqlDialect should be a hard option!
182
184
  const defaultOptions = { sqlMapping: 'plain', sqlDialect: 'hana' };
183
185
  return translateOptions(options, defaultOptions, hardOptions, { sqlDialect: generateStringValidator([ 'hana' ]) }, undefined, 'to.hdbcds');
184
186
  },
@@ -298,8 +298,9 @@ const centralMessageTexts = {
298
298
  },
299
299
 
300
300
  'anno-missing-rewrite': {
301
- std: 'Assign a value for $(ANNO); the value inherited from $(ART) would contain invalid or unrelated references like $(ELEMREF)',
301
+ std: 'Assign a value for $(ANNO); the value inherited from $(ART) can\'t be rewritten due to $(ELEMREF)',
302
302
  unsupported: 'Assign a value for $(ANNO); the value inherited from $(ART) can\'t be rewritten due to unsupported $(ELEMREF)',
303
+ param: 'Assign a value for $(ANNO); the value inherited from $(ART) can\'t be rewritten due to parameter reference $(ELEMREF)',
303
304
  },
304
305
 
305
306
  'chained-array-of': '"Array of"/"many" must not be chained with another "array of"/"many" inside a service',
@@ -717,6 +718,8 @@ const centralMessageTexts = {
717
718
  min: 'Expecting argument $(PROP) for type $(TYPE) to be greater than or equal to $(NUMBER)',
718
719
  'incorrect-type': 'Expected $(NAMES) for argument $(PROP), but found $(CODE)',
719
720
  },
721
+ 'type-unexpected-foreign-keys': 'A managed aspect composition can\'t have a foreign keys specification. Use composition-of-entity or remove foreign keys',
722
+ 'type-unexpected-on-condition': 'A managed aspect composition can\'t have a specified ON-condition. Use composition-of-entity or remove the ON-condition',
720
723
 
721
724
  'type-invalid-items': {
722
725
  std: 'Unexpected $(PROP)', // unused
@@ -876,7 +879,7 @@ const centralMessageTexts = {
876
879
  sub: 'Expecting an entity as target; a target aspect can\'t be specified for a sub element',
877
880
  },
878
881
  'ref-invalid-include': {
879
- std: 'A type, entity, aspect or event with direct elements is expected here',
882
+ std: 'An explicitly structured entity, type, aspect, or event is expected here',
880
883
  bare : 'An aspect without elements is expected here',
881
884
  param: 'A type, entity, aspect or event without parameters is expected here',
882
885
  },
@@ -1166,7 +1169,8 @@ const centralMessageTexts = {
1166
1169
  }
1167
1170
  ,
1168
1171
  'odata-anno-xpr-type': {
1169
- 'std': 'Expected one qualified type name for $(OP) in $(ANNO)'
1172
+ 'std': 'Expected one qualified type name for $(OP) in $(ANNO)',
1173
+ 'edm': 'Expected a qualified EDM type name for $(OP) in $(ANNO) but found $(TYPE)'
1170
1174
  },
1171
1175
  'odata-anno-xpr-args': {
1172
1176
  'std': 'Unexpected arguments for $(OP) in $(ANNO)',
@@ -890,7 +890,7 @@ function transformElementRef( arg ) {
890
890
  return quoted( arg );
891
891
  // Can be used by CSN backends or compiler to create a simple path such as E:elem
892
892
  return quoted(
893
- (arg.param ? ':' : '') +
893
+ ((arg.scope === 'param' || arg.param) ? ':' : '') +
894
894
  ref.map(
895
895
  item => (typeof item !== 'string'
896
896
  ? `${ item.id }${item.args ? '(…)' : ''}${item.where ? '[…]' : ''}`
@@ -207,7 +207,7 @@ function assertConsistency( model, stage ) {
207
207
  optional: [
208
208
  'elements', '$autoElement', '$uncheckedElements', '_origin', '_extensions',
209
209
  '$requireElementAccess', '_effectiveType', '$effectiveSeqNo', '_deps',
210
- '$calcDepElement', '$filtered', '_parent',
210
+ '$calcDepElement', '$filtered', '$enclosed', '_parent',
211
211
  ],
212
212
  schema: {
213
213
  kind: { test: isString, enum: [ 'builtin' ] },
@@ -261,6 +261,7 @@ function assertConsistency( model, stage ) {
261
261
  foreignKeys: { kind: true, inherits: 'definitions', instanceOf: 'ignore' },
262
262
  $keysNavigation: { kind: true, test: TODO },
263
263
  $filtered: { kind: true, inherits: 'value' }, // for assoc+filter
264
+ $enclosed: { kind: true, inherits: 'value' }, // for comp+filter
264
265
  params: { kind: true, inherits: 'definitions' },
265
266
  _extendType: { kind: true, test: TODO },
266
267
  mixin: { inherits: 'definitions' },
@@ -1024,15 +1024,11 @@ function define( model ) {
1024
1024
  const { targetAspect } = obj;
1025
1025
  if (targetAspect) {
1026
1026
  if (obj.foreignKeys) {
1027
- error( 'type-unexpected-foreign-keys', [ obj.foreignKeys[$location], construct ],
1028
- {},
1029
- 'A managed aspect composition can\'t have a foreign keys specification' );
1027
+ error( 'type-unexpected-foreign-keys', [ obj.foreignKeys[$location], construct ] );
1030
1028
  delete obj.foreignKeys; // continuation semantics: not specified
1031
1029
  }
1032
1030
  if (obj.on && !obj.target) {
1033
- error( 'type-unexpected-on-condition', [ obj.on.location, construct ],
1034
- {},
1035
- 'A managed aspect composition can\'t have a specified ON-condition' );
1031
+ error( 'type-unexpected-on-condition', [ obj.on.location, construct ] );
1036
1032
  delete obj.on; // continuation semantics: not specified
1037
1033
  }
1038
1034
  if (targetAspect.elements)
@@ -1182,8 +1178,8 @@ function define( model ) {
1182
1178
  // TODO: it is not just "syntax" - maybe better test for `$calcDepElement`?
1183
1179
  createAndLinkCalcDepElement( elem );
1184
1180
 
1185
- // Special case (hack) for calculated elements that use associations+filter:
1186
- // See "Notes on `$filtered`" in `ExposingAssocWithFilter.md` for details.
1181
+ // Special case (hack) for calculated elements that use composition+filter:
1182
+ // See "Notes on `$enclosed`" in `ExposingAssocWithFilter.md` for details.
1187
1183
  if (elem.target && elem.value.path?.[elem.value.path.length - 1]?.where) {
1188
1184
  delete elem.type;
1189
1185
  delete elem.on;
@@ -617,8 +617,6 @@ function populate( model ) {
617
617
 
618
618
  function resolveTabRef( alias ) {
619
619
  // effectiveType() must not be called on $self, is unnecessary for mixins:
620
- // TODO: have a test for `select from E { a, $self.a as b, $self.{ b as c } }`
621
- // TODO: have a negative test for `select from E { $self.*, assoc.* }`
622
620
  // (we might have those already)
623
621
  if (alias.kind === 'mixin' || alias.kind === '$self')
624
622
  return;
@@ -56,7 +56,8 @@ function propagate( model ) {
56
56
  enum: expensive,
57
57
  params: expensive, // actually only with parent action
58
58
  returns,
59
- $filtered: annotation,
59
+ $filtered: annotation, // TODO(v5): Remove
60
+ $enclosed: annotation,
60
61
  };
61
62
  const ruleToFunction = {
62
63
  __proto__: null,
@@ -281,7 +282,7 @@ function propagate( model ) {
281
282
  if (target.targetAspect)
282
283
  return;
283
284
  if (target.type?._artifact === model.definitions['cds.Association'])
284
- return; // don't propagate targetAspect to associations (e.g. via $filtered)
285
+ return; // don't propagate targetAspect to associations (e.g. via $enclosed)
285
286
  const ta = source.targetAspect;
286
287
  if (!ta.elements && !ta._origin) { // _origin set for elements in source
287
288
  notWithExpand( prop, target, source );
@@ -182,9 +182,12 @@ function resolve( model ) {
182
182
  // Path could start with table alias; get start index
183
183
  let index = path.indexOf(nav.item);
184
184
  if (index === -1)
185
- return;
185
+ return; // should not happen
186
186
 
187
187
  let navItem = nav.navigation;
188
+ if (!nav.item._navigation) // first non-table-alias
189
+ setLink( nav.item, '_navigation', navItem );
190
+
188
191
  if (path[index].where || path[index].args)
189
192
  return;
190
193
  ++index;
@@ -566,7 +569,7 @@ function resolve( model ) {
566
569
  const iType = iTypeArt || inferredElement;
567
570
  // FIXME: The coding above returns incorrect iType for expand on associations
568
571
 
569
- // $filtered may change composition to association; we allow that change here.
572
+ // $enclosed: maybe composition was changed to association; we allow that change here.
570
573
  const compToAssoc = sType === model.definitions['cds.Association'] && inferredElement.target;
571
574
 
572
575
  // xor: could be missing a type;
@@ -5,6 +5,7 @@
5
5
  const {
6
6
  forEachGeneric,
7
7
  forEachInOrder,
8
+ isBetaEnabled,
8
9
  } = require('../base/model');
9
10
  const { dictLocation, weakLocation, weakRefLocation } = require('../base/location');
10
11
 
@@ -18,6 +19,7 @@ const {
18
19
  traverseQueryPost,
19
20
  traverseQueryExtra,
20
21
  setExpandStatus,
22
+ getUnderlyingBuiltinType,
21
23
  } = require('./utils');
22
24
  const { Location } = require('../base/location');
23
25
  const { CompilerAssertion } = require('../base/error');
@@ -39,6 +41,12 @@ function tweakAssocs( model ) {
39
41
  getOrigin,
40
42
  } = model.$functions;
41
43
 
44
+ Object.assign(model.$functions, {
45
+ firstProjectionForPath,
46
+ });
47
+
48
+ const isV5preview = isBetaEnabled( model.options, 'v5preview' );
49
+
42
50
  // Phase 5: rewrite associations
43
51
  model._entities.forEach( rewriteArtifact );
44
52
  // Think hard whether an on condition rewrite can lead to a new cyclic
@@ -448,14 +456,27 @@ function tweakAssocs( model ) {
448
456
  };
449
457
  setArtifactLink( elem.type, assocType._artifact );
450
458
 
451
- elem.$filtered = {
452
- val: true,
453
- literal: 'boolean',
454
- location,
455
- $inferred: '$generated',
456
- };
459
+ if (!isV5preview) { // TODO(v5): Remove, only use $enclosed
460
+ elem.$filtered = {
461
+ val: true,
462
+ literal: 'boolean',
463
+ location,
464
+ $inferred: '$generated',
465
+ };
466
+ }
467
+
468
+ const isComp = (getUnderlyingBuiltinType( assoc )?.name?.id === 'cds.Composition');
469
+ if (isComp) {
470
+ elem.$enclosed = {
471
+ val: true,
472
+ literal: 'boolean',
473
+ location,
474
+ $inferred: '$generated',
475
+ };
476
+ }
457
477
  }
458
478
 
479
+
459
480
  /**
460
481
  * Transform a filter on `assocPathStep` into an ON-condition.
461
482
  * Paths inside the filter are rewritten relative to `assoc`, so they can be redirected
@@ -541,7 +562,7 @@ function tweakAssocs( model ) {
541
562
  setLink( rhs, '_artifact', rhs.path[rhs.path.length - 1]._artifact );
542
563
 
543
564
  if (elem.$syntax !== 'calc') { // different to lhs!
544
- const projectedFk = firstProjectionForPath( rhs.path, nav.tableAlias, elem );
565
+ const projectedFk = firstProjectionForPath( rhs.path, 0, nav.tableAlias, elem );
545
566
  rewritePath( rhs, projectedFk.item, elem, projectedFk.elem, elem.value.location );
546
567
  }
547
568
 
@@ -596,7 +617,8 @@ function tweakAssocs( model ) {
596
617
  return; // not $self or source element
597
618
  if (expr.scope === 'param' || root.kind === '$parameters')
598
619
  return; // are not allowed anyway - there was an error before
599
- const result = firstProjectionForPath( expr.path, tableAlias, assoc );
620
+ const startIndex = (root.kind === '$self' ? 1 : 0);
621
+ const result = firstProjectionForPath( expr.path, startIndex, tableAlias, assoc );
600
622
  // For `assoc[…]`, ensure that we don't rewrite to another projection on `assoc`.
601
623
  if (result.item && assoc._origin === result.item._artifact)
602
624
  result.elem = assoc;
@@ -782,51 +804,60 @@ function navProjection( navigation, preferred ) {
782
804
 
783
805
 
784
806
  /**
785
- * For a path `a.b.c.d`, return a projection for the first path item that is projected.
807
+ * For a path `a.b.c.d`, return a projection for the first path item that is projected,
808
+ * starting at `startIndex` in this path using the given navigation (table alias or
809
+ * navigation element).
786
810
  * For example, if a query has multiple projections such as `a.b, a, a.b.c`, the
787
811
  * _first_ possible projection will be used and the caller can rewrite `a.b.c.d` to `b.c.d`.
788
- * This avoids that `extend`s affect the ON-condition.
812
+ * This avoids `extend`s affect the ON-condition.
789
813
  *
790
- * The returned object `ret` has `ret.item`, which is the path item that is projected.
791
- * `ret.elem` is the element projection.
814
+ * The returned object `ret` has `ret.item`, which is the path item at index `ret.index`
815
+ * that is projected. `ret.elem` is the element projection.
792
816
  *
793
817
  * @param {any[]} path
794
- * @param {object} tableAlias
795
- * @param {object} assoc Preferred association that should be used if projected.
818
+ * @param {number} startIndex
819
+ * @param {object} nav
820
+ * @param {object} elem Preferred association/element that should be used if projected.
796
821
  * @return {{elem: object, item: object}|null}
797
822
  */
798
- function firstProjectionForPath( path, tableAlias, assoc ) {
799
- const viaSelf = (path[0]._navigation || path[0]._artifact).kind === '$self';
800
- const root = viaSelf ? 1 : 0;
801
- if (root >= path.length) // e.g. just `$self` path item
823
+ function firstProjectionForPath( path, startIndex, nav, elem ) {
824
+ if (startIndex >= path.length) // e.g. just `$self` path item
802
825
  return { item: undefined, elem: {} };
803
826
 
827
+ let tableAlias = nav;
828
+ while (tableAlias.kind === '$navElement')
829
+ tableAlias = tableAlias._parent;
830
+
804
831
  // We want to use the _first_ valid projection that is written by the user (if the preferred
805
- // `assoc` is not directly projected). To achieve that, look into the table alias' elements.
832
+ // `assoc` is not directly projected). To achieve that, look into the query's elements.
806
833
  const selectedElements = Object.values(tableAlias._parent.elements);
807
- const proj = [];
808
- let navItem = tableAlias;
809
- for (const item of path.slice(root)) {
834
+
835
+ let proj = null;
836
+ let navItem = nav;
837
+ for (let i = startIndex; i < path.length; ++i) {
838
+ const item = path[i];
810
839
  navItem = item?.id && navItem.elements?.[item.id];
811
840
  if (!navItem) {
812
841
  break;
813
842
  }
814
843
  else if (navItem._projections) {
815
- const elem = navProjection( navItem, assoc );
816
- if (elem && elem === assoc) {
844
+ const projElem = navProjection( navItem, elem );
845
+ if (projElem && projElem === elem) {
817
846
  // in case the specified association is found, _always_ use it.
818
- return { item, elem };
847
+ return { index: i, item, elem };
819
848
  }
820
- else if (elem) {
821
- const index = selectedElements.indexOf(elem);
822
- proj.push({ item, elem, index });
849
+ else if (projElem) {
850
+ const queryIndex = selectedElements.indexOf(projElem);
851
+ if (!proj || queryIndex < proj.queryIndex) {
852
+ proj = {
853
+ index: i, item, elem: projElem, queryIndex,
854
+ };
855
+ }
823
856
  }
824
857
  }
825
858
  }
826
859
 
827
- return (proj.length === 0)
828
- ? { item: path[root], elem: null }
829
- : proj.reduce( (acc, curr) => (acc.index > curr.index ? curr : acc), proj[0] ); // first
860
+ return proj || { index: startIndex, item: path[startIndex], elem: null };
830
861
  }
831
862
 
832
863
  /**
@@ -98,15 +98,21 @@
98
98
  //
99
99
  // Select Item via Origin
100
100
  // ----------------------
101
- // A bare select item that gets an annotation via propagation from its origin behaves
102
- // similar to an element that gets it via an include.
101
+ // A bare select item of path length one, that gets an annotation via propagation from
102
+ // its origin, behaves similar to an element that gets it via an include.
103
103
  // However, elements may have been renamed or may not be available at all.
104
104
  // On top of that, they may be inside nested projections (expand).
105
+ // Or even simpler: sub-elements may have been selected.
105
106
  //
106
107
  // Instead of changing the path prefix, we need to check if the referenced path
107
108
  // was projected or if a prefix was projected (e.g. for structures or associations).
108
109
  // The same rules as for ON-condition rewriting apply.
109
110
  //
111
+ // Furthermore, as the target is a select item, and this select item belongs to a table
112
+ // alias, we should rewrite all annotation paths only to projected elements of that
113
+ // table alias. Cross-rewriting between table aliases should not be done.
114
+ // This is the same we do for association rewriting.
115
+ //
110
116
  // TODO:
111
117
  // For now, we do not rewrite sub-structure elements. The whole structure needs
112
118
  // to be projected or the select item isn't considered. That is, `expand {*}`
@@ -158,6 +164,7 @@ class AnnoRewriteConfig {
158
164
  viaExpand;
159
165
  viaExpandType;
160
166
  isInFilter;
167
+ tokenExpr;
161
168
  }
162
169
 
163
170
  function xprRewriteFns( model ) {
@@ -167,6 +174,7 @@ function xprRewriteFns( model ) {
167
174
  resolvePath,
168
175
  navigationEnv,
169
176
  resolvePathRoot,
177
+ firstProjectionForPath,
170
178
  } = model.$functions;
171
179
 
172
180
  return {
@@ -275,6 +283,11 @@ function xprRewriteFns( model ) {
275
283
  root._parent.kind !== 'function'))
276
284
  return reportAnnoRewriteError( expr, config, 'unsupported' );
277
285
 
286
+ // magic variables / replacement variables are never rewritten; they can't
287
+ // have filters nor can they point to elements.
288
+ if (expr._artifact?.kind === 'builtin')
289
+ return null;
290
+
278
291
  let hasError = false;
279
292
  if (config.isViaType || config.isViaCalcElement)
280
293
  hasError = adaptPathPrefixViaType( expr, config );
@@ -335,12 +348,41 @@ function xprRewriteFns( model ) {
335
348
  */
336
349
  function getRootEnv( expr, config ) {
337
350
  const { target } = config;
351
+
352
+ if (expr.scope === 'param') // path is absolute
353
+ return navigationEnv( config.targetRoot, null, null, 'nav' );
354
+
355
+ // On select items, use navigation elements or table alias
356
+ // TODO: Expand/inline paths don't have a `_navigation` property on their last
357
+ // path step, yet. We need to implement expand/inline.
358
+ const isSimpleSelectItem = target.value?.path && target._main?.query && !target._pathHead;
359
+ if (isSimpleSelectItem) {
360
+ const isSelfPath = (expr.path[0]?._navigation?.kind === '$self');
361
+ if (isSelfPath) {
362
+ // Path is absolute, use table alias to resolve it.
363
+ let tableAlias = target.value.path[0]._navigation;
364
+ while (tableAlias && tableAlias.kind === '$navElement')
365
+ tableAlias = tableAlias._parent;
366
+ if (tableAlias)
367
+ return tableAlias;
368
+ }
369
+ else {
370
+ // Path is relative
371
+ const nav = target.value.path[target.value.path.length - 1]._navigation?._parent;
372
+ if (nav)
373
+ return nav;
374
+ }
375
+ }
376
+
377
+ if (isSimpleSelectItem && model.options.testMode)
378
+ throw new CompilerAssertion(`select item has no table alias: ${ JSON.stringify(target.value.path) }`);
379
+
338
380
  if (isAnnoPathAbsolute( expr ))
339
381
  return navigationEnv( config.targetRoot, null, null, 'nav' );
340
- // root item is element reference (others were already rejected)
341
- if (isAnnoRootArt( target ))
342
- return navigationEnv( target, null, null, 'nav' );
343
- return navigationEnv( target._parent, null, null, 'nav' );
382
+
383
+ // anno path is relative / element reference (others were already rejected)
384
+ // if the target is a root artifact, use it. Otherwise, use its parent.
385
+ return navigationEnv( isAnnoRootArt( target ) ? target : target._parent, null, null, 'nav' );
344
386
  }
345
387
 
346
388
  /**
@@ -350,15 +392,20 @@ function xprRewriteFns( model ) {
350
392
  * @returns {boolean}
351
393
  */
352
394
  function rewriteGenericAnnoPath( expr, config, refCtx ) {
353
- const rootIndex = isAnnoPathAbsolute( expr ) ? 1 : 0;
354
- let env = getRootEnv( expr, config );
395
+ const isAbsolute = isAnnoPathAbsolute( expr );
396
+ const rootIndex = isAbsolute ? 1 : 0;
355
397
 
356
- // reset artifact link
398
+ // We get the root environment now, even though below we resolve the root item
399
+ // again if it was absolute (e.g. $self). We do so, because for queries, we
400
+ // want to respect the select item's corresponding table alias.
401
+ const rootEnv = getRootEnv( expr, config );
402
+
403
+ // reset artifact link; we'll set it again
357
404
  setArtifactLink( expr, null );
358
405
 
359
406
  // Adapt root path, as it isn't rewritten in rewriteItem
360
407
  const rootItem = expr.path[0];
361
- if (rootIndex === 1) {
408
+ if (isAbsolute) {
362
409
  delete rootItem._artifact;
363
410
  delete rootItem._navigation;
364
411
  // TODO: What about `up_`? Shouldn't we set `_navigation` as well?
@@ -368,10 +415,10 @@ function xprRewriteFns( model ) {
368
415
  return reportAnnoRewriteError( expr, config );
369
416
  }
370
417
 
418
+ let env = rootEnv;
371
419
  let art = rootItem._artifact;
372
420
  for (let i = rootIndex; i < expr.path.length; ++i) {
373
- const item = expr.path[i];
374
- art = rewriteItem( expr, config, env, item );
421
+ art = rewriteItem( expr, config, env, i );
375
422
  if (!art)
376
423
  return reportAnnoRewriteError( expr, config );
377
424
  env = navigationEnv( art, null, null, 'nav' );
@@ -581,48 +628,74 @@ function xprRewriteFns( model ) {
581
628
  return self;
582
629
  }
583
630
 
584
- function rewriteItem( expr, config, env, item ) {
585
- const found = setArtifactLink( item, findRewriteTarget( item, env, config.target ));
586
- if (found) {
587
- if (item.id !== found.name.id) {
588
- // Path was rewritten; original token text string is no longer accurate
589
- config.tokenExpr.$tokenTexts = true;
590
- item.id = found.name.id;
591
- }
592
- return found;
631
+ /**
632
+ * Rewrite the item in `expr.path` at the given index.
633
+ * This function may splice the array if more than one path segment
634
+ * is replaced by a single item (e.g. in queries).
635
+ *
636
+ * @param {XSN.Expression} expr
637
+ * @param {AnnoRewriteConfig} config
638
+ * @param {object} env
639
+ * @param {number} index
640
+ * @returns {*|null}
641
+ */
642
+ function rewriteItem( expr, config, env, index ) {
643
+ const item = expr.path[index];
644
+ const rewriteTarget = findRewriteTarget( expr, index, env, config.target );
645
+ const found = setArtifactLink( item, rewriteTarget[0] );
646
+ if (!found)
647
+ return null;
648
+
649
+ if (item.id !== found.name.id) {
650
+ // Path was rewritten; original token text string is no longer accurate
651
+ config.tokenExpr.$tokenTexts = true;
652
+ item.id = found.name.id;
593
653
  }
594
- return null;
654
+
655
+ if (rewriteTarget[1] > index)
656
+ expr.path.splice(index + 1, rewriteTarget[1] - index);
657
+
658
+ return rewriteTarget[0];
595
659
  }
596
660
 
597
- function findRewriteTarget( item, env, target ) {
598
- if (!env.query && env.kind !== 'select')
599
- return env.elements?.[item.id] || null;
661
+ function findRewriteTarget( expr, index, env, target ) {
662
+ if (env.kind === '$navElement' || env.kind === '$tableAlias') {
663
+ const r = firstProjectionForPath( expr.path, index, env, target );
664
+ return [ r.elem, r.index ];
665
+ }
666
+
667
+ const item = expr.path[index];
668
+ // Not a query -> no $navElement -> use `elements`
669
+ if (!env.query && env.kind !== 'select') {
670
+ if (env.elements?.[item.id])
671
+ return [ env.elements[item.id], index ];
672
+ return [ null, expr.path.length ];
673
+ }
600
674
  const items = (env._leadingQuery || env)._combined?.[item.id];
601
- const navs = !items || Array.isArray(items) ? items : [ items ];
675
+ const allNavs = !items || Array.isArray(items) ? items : [ items ];
676
+
677
+ // If the annotation target itself has a table alias, require projections of that
678
+ // table alias. Of course, that only works if we're talking about the same query.
679
+ const tableAlias = (target._main?._origin === item._artifact._main &&
680
+ target.value?.path[0]?._navigation?.kind === '$tableAlias')
681
+ ? target.value.path[0]._navigation : null;
602
682
 
603
683
  // Look at all table aliase that could project `item` and only select
604
684
  // those that have actual projections.
605
- let projected = navs?.filter(p => p._origin === item._artifact && p._projections);
606
- if (!projected || projected.length === 0)
607
- return null;
608
-
609
- // If the annotation target itself has a table alias, prefer projections
610
- // of that table alias over others when rewriting.
611
- const tableAlias = target.value?.path[0]?._navigation;
612
- if (tableAlias?.kind === '$tableAlias') {
613
- // TODO: Is the _parent always a table alias?
614
- const taProjected = projected.filter(p => p._parent === tableAlias);
615
- if (taProjected.length)
616
- projected = taProjected;
685
+ const navs = allNavs?.filter(p => p._origin === item._artifact &&
686
+ (!tableAlias || tableAlias === p._parent));
687
+ if (!navs || navs.length === 0)
688
+ return [ null, expr.path.length ];
689
+
690
+ // If there are multiple navigations for the element, just use the first that matches.
691
+ // In case of table aliases, it's just one.
692
+ for (const nav of navs) {
693
+ const r = firstProjectionForPath( expr.path, index, nav._parent, target );
694
+ if (r.elem)
695
+ return [ r.elem, r.index ];
617
696
  }
618
697
 
619
- // Of the possible entries, choose the first one
620
- projected = projected[0];
621
-
622
- // If there are multiple projections, check if the annotation target is
623
- // projected as well, otherwise, simply take the first one.
624
- return projected._projections.find(proj => proj === target) ||
625
- projected._projections[0] || null;
698
+ return [ null, expr.path.length ];
626
699
  }
627
700
  }
628
701
 
@@ -248,8 +248,15 @@ function xpr2edmJson( carrier, anno, location, options, messageFunctions ) {
248
248
  transform['<='] = op('$Le');
249
249
  transform.$Le = noOp;
250
250
  transform.in = (parent, prop, xpr) => {
251
- evalArgs({ min: 1 }, xpr[1].list, prop);
252
- parent.$In = [ xpr[0], ...xpr[1].list ];
251
+ let args = xpr[1].list;
252
+ if (!args) {
253
+ if (Array.isArray(xpr[1].xpr))
254
+ args = xpr[1].xpr;
255
+ else
256
+ args = [ xpr[1] ];
257
+ }
258
+ evalArgs({ min: 1 }, args, prop);
259
+ parent.$In = [ xpr[0], args ];
253
260
  delete parent[prop];
254
261
  transformExpression(parent, undefined, transform);
255
262
  };
@@ -293,9 +300,9 @@ function xpr2edmJson( carrier, anno, location, options, messageFunctions ) {
293
300
  transform.$Neg = noOp;
294
301
  transform['*'] = op('$Mul');
295
302
  transform.$Mul = noOp;
296
- transform['/'] = op('$Div');
297
- transform.$Div = noOp;
298
- // $DivBy, $Mod are functions
303
+ transform['/'] = op('$DivBy');
304
+ transform.$DivBy = noOp;
305
+ // $Div, $Mod are functions
299
306
  //----------------------------------
300
307
  // LITERALS
301
308
  transform.val = (parent, prop, xpr, csnPath, parentparent, parentprop) => {
@@ -575,6 +582,15 @@ function xpr2edmJson( carrier, anno, location, options, messageFunctions ) {
575
582
  }
576
583
  }
577
584
  else {
585
+ // Error out for arbitrary types until we know better
586
+ // probably todo: Check for reachability of arb type names such as namespace
587
+ // reqDef entry etc...
588
+ if (typeDef) { // eslint-disable-line no-lonely-if
589
+ error('odata-anno-xpr-type', location, {
590
+ anno, op: `${xpr}(…)`, type: `${typeDef}`, '#': 'edm',
591
+ });
592
+ }
593
+ /*
578
594
  typeFacets.forEach((facet) => {
579
595
  if (facet.args.length === 1 && facet.args[0].val) {
580
596
  const facetName = facet.func.startsWith('$') ? facet.func.slice(1) : facet.func;
@@ -582,6 +598,7 @@ function xpr2edmJson( carrier, anno, location, options, messageFunctions ) {
582
598
  parent[`$${facetName}`] = facet.args[0].val;
583
599
  }
584
600
  });
601
+ */
585
602
  }
586
603
 
587
604
  delete typeProp.args;
@@ -648,8 +665,8 @@ function xpr2edmJson( carrier, anno, location, options, messageFunctions ) {
648
665
  const funcDefs = {
649
666
  $Has: twoArgs,
650
667
  Has: [ twoArgs, dollar ],
651
- $DivBy: twoArgs,
652
- DivBy: [ twoArgs, dollar ],
668
+ $Div: twoArgs,
669
+ Div: [ twoArgs, dollar ],
653
670
  $Mod: twoArgs,
654
671
  Mod: [ twoArgs, dollar ],
655
672
  $Apply: () => {
@@ -1034,7 +1034,7 @@ function csn2annotationEdm( reqDefs, reqDefsUtils, csnVocabularies, serviceName,
1034
1034
  typeName = dTypeName.split('.')[1];
1035
1035
  }
1036
1036
 
1037
- if (value) {
1037
+ if (typeof value === 'string') {
1038
1038
  // replace all occurrences of '.' by '/' up to first '@'
1039
1039
  value = value.split('@').map((o, i) => (i === 0 ? o.replace(/\./g, '/') : o)).join('@');
1040
1040
  }
@@ -93,7 +93,8 @@ const transformers = {
93
93
  cardinality, // also in pathItem: after 'id', before 'where'
94
94
  targetAspect,
95
95
  target,
96
- $filtered: value, // assoc+filter
96
+ $filtered: value, // assoc+filter v4
97
+ $enclosed: value, // comp+filter v5
97
98
  foreignKeys,
98
99
  enum: enumDict,
99
100
  items,
@@ -917,11 +917,11 @@ function csnRefs( csn, universalReady ) {
917
917
  : { $aliases: Object.create(null), $next: pcache.$next };
918
918
  }
919
919
 
920
- function initColumnElement( col, colIndex, parentElementOrQueryCache ) {
920
+ function initColumnElement( col, colIndex, parentElementOrQueryCache, externalElements ) {
921
921
  if (col === '*')
922
922
  return;
923
923
  if (col.inline) {
924
- col.inline.map( c => initColumnElement( c, null, parentElementOrQueryCache ) );
924
+ col.inline.map( c => initColumnElement( c, null, parentElementOrQueryCache, externalElements ) );
925
925
  return;
926
926
  }
927
927
  setCache( col, '_parent', // not set for query (has property _select)
@@ -936,11 +936,18 @@ function csnRefs( csn, universalReady ) {
936
936
 
937
937
  while (type.items)
938
938
  type = type.items;
939
+ if (!type.elements) {
940
+ // in OData backend, the sub elements from a column with expand might have
941
+ // been “externalized” into a named type. No backward _column link is
942
+ // possible this way, of course...
943
+ type = artifactRef( type.type );
944
+ externalElements = true;
945
+ }
939
946
  const elem = setCache( col, '_element', type.elements[as] );
940
- if (elem) // TODO to.sql: something is strange if this is not set
947
+ if (elem && !externalElements) // TODO to.sql: something is strange if `elem` is not set
941
948
  setCache( elem, '_column', col );
942
949
  if (col.expand)
943
- col.expand.map( c => initColumnElement( c, null, elem ) );
950
+ col.expand.map( c => initColumnElement( c, null, elem, externalElements ) );
944
951
  }
945
952
 
946
953
  // property name convention in cache:
@@ -614,7 +614,7 @@ function csnToCdl( csn, options, msg ) {
614
614
  else if (element['#'] !== undefined) { // enum symbol reference
615
615
  result += ` = #${element['#']}`;
616
616
  }
617
- else if (!isCalcElement || !isDirectAssocOrComp(element.type) && !element.$filtered) {
617
+ else if (!isCalcElement || !isDirectAssocOrComp(element.type) && !element.$filtered && !element.$enclosed) {
618
618
  // If the element is a calculated element _and_ a direct association or
619
619
  // composition, we'd render `Association to F on (cond) = calcValue;` which
620
620
  // would alter the ON-condition.
@@ -187,13 +187,23 @@ function createReferentialConstraints( csn, options ) {
187
187
  * @param {CSN.PathSegment | object} upLinkFor the name of the composition which used this association in a `$self = <comp>.<up_>` comparison
188
188
  * it is used for a comment in the constraint, which is only printed out in test-mode
189
189
  */
190
- function attachConstraintsToDependentKeys( dependentKeys, parentKeys, parentTable, sourceAssociation, upLinkFor = null ) {
190
+ function attachConstraintsToDependentKeys(
191
+ dependentKeys,
192
+ parentKeys,
193
+ parentTable,
194
+ sourceAssociation,
195
+ upLinkFor = null
196
+ ) {
191
197
  while (dependentKeys.length > 0) {
192
198
  const dependentKeyValuePair = dependentKeys.pop();
193
199
  const dependentKey = dependentKeyValuePair[1];
194
200
  // if it already has a dependent key assigned, do not overwrite it.
195
201
  // this is the case for <up_> associations in on-conditions of compositions
196
- if (Object.prototype.hasOwnProperty.call(dependentKey, '$foreignKeyConstraint'))
202
+ const { $foreignKeyConstraint } = dependentKey;
203
+ // in contrast to foreign keys which stem from managed associations,
204
+ // a tenant foreign key column may have multiple parent keys as partners
205
+ const tenantForeignKey = isTenant && dependentKeyValuePair[0] === 'tenant';
206
+ if ($foreignKeyConstraint && (!tenantForeignKey || $foreignKeyConstraint.upLinkFor))
197
207
  return;
198
208
 
199
209
  const parentKeyValuePair = parentKeys.pop();
@@ -208,7 +218,15 @@ function createReferentialConstraints( csn, options ) {
208
218
  validated,
209
219
  enforced,
210
220
  };
211
- dependentKey.$foreignKeyConstraint = constraint;
221
+ if (tenantForeignKey) {
222
+ const dontOverwriteUp = dependentKey.$foreignKeyConstraint && dependentKey.$foreignKeyConstraint.some(c => c.sourceAssociation === sourceAssociation && c.parentTable === parentTable);
223
+ const dontOverwriteTexts = dependentKey.$foreignKeyConstraint && dependentKey.$foreignKeyConstraint.some(c => c.sourceAssociation === 'texts' && c.upLinkFor.texts);
224
+ if (!dontOverwriteUp && !dontOverwriteTexts)
225
+ dependentKey.$foreignKeyConstraint = dependentKey.$foreignKeyConstraint ? [ ...dependentKey.$foreignKeyConstraint, constraint ] : [ constraint ];
226
+ }
227
+ else {
228
+ dependentKey.$foreignKeyConstraint = constraint;
229
+ }
212
230
  }
213
231
  }
214
232
 
@@ -473,11 +491,11 @@ function createReferentialConstraints( csn, options ) {
473
491
  }
474
492
 
475
493
  /**
476
- * Creates the final referential constraints from all dependent key <-> parent key pairs stemming from the same $sourceAssociation
494
+ * Creates the final referential constraints from all dependent key <-> parent key pairs stemming from the same sourceAssociation
477
495
  * and attaches it to the given artifact.
478
496
  *
479
497
  * Go over all elements with $foreignKeyConstraint property:
480
- * - Find all other elements in artifact with the same $sourceAssociation
498
+ * - Find all other elements in artifact with the same sourceAssociation
481
499
  * - Create constraints with the information supplied by $parentKey, $parentTable and $onDelete
482
500
  *
483
501
  * @param {CSN.Artifact} artifact
@@ -485,6 +503,21 @@ function createReferentialConstraints( csn, options ) {
485
503
  */
486
504
  function collectAndAttachReferentialConstraints( artifact, artifactName ) {
487
505
  const referentialConstraints = Object.create(null);
506
+
507
+ // tenant foreign keys may have multiple parent keys
508
+ // process tenant foreign key first
509
+ if (isTenant && artifact.elements?.tenant) {
510
+ const element = artifact.elements.tenant;
511
+ if (element.$foreignKeyConstraint) {
512
+ const tenantConstraints = element.$foreignKeyConstraint;
513
+ delete element.$foreignKeyConstraint;
514
+ // create (multiple) foreign key constraint(s) for the tenant column with each association in the dependent entity
515
+ tenantConstraints.forEach((c) => {
516
+ createReferentialConstraints(c, 'tenant');
517
+ });
518
+ }
519
+ }
520
+
488
521
  for (const elementName in artifact.elements) {
489
522
  const element = artifact.elements[elementName];
490
523
  if (!element.$foreignKeyConstraint)
@@ -492,16 +525,34 @@ function createReferentialConstraints( csn, options ) {
492
525
  // copy constraint property, and delete it from the element
493
526
  const $foreignKeyConstraint = Object.assign({}, element.$foreignKeyConstraint);
494
527
  delete element.$foreignKeyConstraint;
528
+ createReferentialConstraints($foreignKeyConstraint, elementName);
529
+ }
530
+ if (Object.keys(referentialConstraints).length) {
531
+ if (!('$tableConstraints' in artifact))
532
+ artifact.$tableConstraints = Object.create(null);
533
+
534
+ artifact.$tableConstraints.referential = referentialConstraints;
535
+ }
536
+
537
+ /**
538
+ * Creates referential constraints for database relationships. This function constructs constraints based on foreign key information and element names,
539
+ * and determines deletion rules based on the existing constraints and options. It manages dependencies and names for constraints dynamically during
540
+ * execution.
541
+ *
542
+ * @param {object} $foreignKeyConstraint - An object encapsulating details about the foreign key constraint
543
+ * @param {string} elementName - The name of the dependent element or table that is linked by the foreign key.
544
+ */
545
+ function createReferentialConstraints($foreignKeyConstraint, elementName) {
495
546
  const { parentTable } = $foreignKeyConstraint;
496
547
  const parentKey = [ $foreignKeyConstraint.parentKey ];
497
548
  const dependentKey = [ elementName ];
498
549
  const onDeleteRules = new Set();
499
550
  onDeleteRules.add($foreignKeyConstraint.onDelete);
500
551
  forEach(artifact.elements, (foreignKeyName, foreignKey) => {
501
- // find all other `$foreignKeyConstraint`s with same `$sourceAssociation` and same `parentTable`
552
+ // find all other `$foreignKeyConstraint`s with same `sourceAssociation` and same `parentTable`
502
553
  const matchingForeignKeyFound = foreignKey.$foreignKeyConstraint &&
503
- foreignKey.$foreignKeyConstraint.sourceAssociation === $foreignKeyConstraint.sourceAssociation &&
504
- foreignKey.$foreignKeyConstraint.parentTable === $foreignKeyConstraint.parentTable;
554
+ foreignKey.$foreignKeyConstraint.sourceAssociation === $foreignKeyConstraint.sourceAssociation &&
555
+ foreignKey.$foreignKeyConstraint.parentTable === $foreignKeyConstraint.parentTable;
505
556
  if (!matchingForeignKeyFound)
506
557
  return;
507
558
 
@@ -536,12 +587,6 @@ function createReferentialConstraints( csn, options ) {
536
587
  enforced: $foreignKeyConstraint.enforced,
537
588
  };
538
589
  }
539
- if (Object.keys(referentialConstraints).length) {
540
- if (!('$tableConstraints' in artifact))
541
- artifact.$tableConstraints = Object.create(null);
542
-
543
- artifact.$tableConstraints.referential = referentialConstraints;
544
- }
545
590
  }
546
591
  }
547
592
 
@@ -242,7 +242,7 @@ function flattenAllStructStepsInRefs( csn, options, messageFunctions, resolved,
242
242
  // full path into target, uncomment this line and
243
243
  // comment/remove setProp in expansion.js
244
244
  // setProp(parent, '$structRef', parent.ref);
245
- parent.ref = flattenStructStepsInRef(ref, scopedPath, links, scope, resolvedLinkTypes);
245
+ [ parent.ref ] = flattenStructStepsInRef(ref, scopedPath, links, scope, resolvedLinkTypes);
246
246
  resolved.set(parent, { links, art, scope });
247
247
  // Explicitly set implicit alias for things that are now flattened - but only in columns
248
248
  // TODO: Can this be done elegantly during expand phase already?
@@ -543,7 +543,7 @@ function handleManagedAssociationsAndCreateForeignKeys( csn, options, messageFun
543
543
  if (clone.ref) {
544
544
  clone.ref[clone.ref.length - 1] = flatElemName;
545
545
  // Now we need to properly flatten the whole ref
546
- clone.ref = flattenStructStepsInRef(clone.ref, pathToKey);
546
+ [ clone.ref ] = flattenStructStepsInRef(clone.ref, pathToKey);
547
547
  }
548
548
  if (!clone.as) {
549
549
  clone.as = flatElemName;
@@ -951,7 +951,7 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
951
951
  if (isFulltextIndex)
952
952
  error(null, path, { name: artName }, 'A fulltext index can\'t be defined on a structured element $(NAME)');
953
953
  // First, compute the name from the path, e.g ['s', 's1', 's2' ] will result in 'S_s1_s2' ...
954
- const refPath = flattenStructStepsInRef(val.ref, path);
954
+ const [ refPath ] = flattenStructStepsInRef(val.ref, path);
955
955
  // ... and take this as the prefix for all elements
956
956
  const flattenedElems = flattenStructuredElement(art, refPath, [], ['definitions', artName, 'elements']);
957
957
  Object.keys(flattenedElems).forEach((elem, i, elems) => {
@@ -967,7 +967,7 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
967
967
  }
968
968
  else {
969
969
  // The reference is not structured, so just replace it by a ref to the combined prefix path
970
- const refPath = flattenStructStepsInRef(val.ref, path);
970
+ const [ refPath ] = flattenStructStepsInRef(val.ref, path);
971
971
  flattenedIndex.push({ ref: refPath });
972
972
  }
973
973
  }
@@ -290,6 +290,7 @@ function allInOneFlattening(csn, refFlattener, adaptRefs, inspectRef, isExternal
290
290
  // Later, the query can/must be rewritten as long as a flat OData CSN is published
291
291
  // but this then operates on the entity/view which has all struct infos available
292
292
  function flattenAndPrefixExprPaths(carrier, propNames, csnPath, rootPrefix, typeIdx, refParentIsItems = false) {
293
+
293
294
  const refCheck = {
294
295
  ref: (elemref, prop, xpr, path) => {
295
296
  const { art, scope } = inspectRef(path);
@@ -316,53 +317,60 @@ function allInOneFlattening(csn, refFlattener, adaptRefs, inspectRef, isExternal
316
317
  return `${head}`;
317
318
  })();
318
319
 
320
+ let refChanged = false;
319
321
  const absolutifier = {
320
322
  ref : (parent, prop, xpr) => {
321
323
  const head = xpr[0].id || xpr[0];
322
- if (typeIdx < rootPrefix.length && head === '$self' && !isMagicVariable(head)) {
323
- const [xprHead, ...xprTail] = xpr.slice(1, xpr.length);
324
- if(xprHead) {
325
- if (xprHead.id) {
326
- xprHead.id = rootPrefix.slice(1, typeIdx).concat(xprHead.id).join('_');
327
- parent[prop] = [ xprHead, ...xprTail ];
324
+ let isPrefixed = false;
325
+ if(!isMagicVariable(head)) {
326
+ if (head === '$self' && typeIdx < rootPrefix.length) {
327
+ isPrefixed = true;
328
+ const [xprHead, ...xprTail] = xpr.slice(1, xpr.length);
329
+ if(xprHead) {
330
+ if (xprHead.id) {
331
+ xprHead.id = rootPrefix.slice(1, typeIdx).concat(xprHead.id).join('_');
332
+ parent[prop] = [ xprHead, ...xprTail ];
333
+ }
334
+ else
335
+ parent[prop] = [ rootPrefix.slice(1, typeIdx).concat(xprHead).join('_'), ...xprTail];
336
+ }
337
+ }
338
+ else if (head !== '$self' && !parent.param && rootPrefix.length > 2) {
339
+ isPrefixed = true;
340
+ const [xprHead, ...xprTail] = xpr;
341
+ if (!refParentIsItems) {
342
+ if (xprHead.id) {
343
+ xprHead.id = rootPrefix.slice(1, -1).concat(xprHead.id).join('_');
344
+ parent[prop] = [ xprHead, ...xprTail ];
345
+ }
346
+ else
347
+ parent[prop] = [ rootPrefix.slice(1, -1).concat(xprHead).join('_'), ...xprTail];
328
348
  }
329
349
  else
330
- parent[prop] = [ rootPrefix.slice(1, typeIdx).concat(xprHead).join('_'), ...xprTail];
350
+ parent[prop] = [ ...rootPrefix.slice(0, rootPrefix.length-1), ...xpr];
351
+ }
352
+ if(isPrefixed) {
331
353
  if (carrier.$scope === 'params')
332
354
  parent.param = true;
333
355
  else
334
356
  parent[prop].unshift('$self');
335
357
  }
336
358
  }
337
- else if (rootPrefix.length > 2 && head !== '$self' && !parent.param && !isMagicVariable(head)) {
338
- const [xprHead, ...xprTail] = xpr;
339
- if (!refParentIsItems) {
340
- if (xprHead.id) {
341
- xprHead.id = rootPrefix.slice(1, -1).concat(xprHead.id).join('_');
342
- parent[prop] = [ xprHead, ...xprTail ];
343
- }
344
- else
345
- parent[prop] = [ rootPrefix.slice(1, -1).concat(xprHead).join('_'), ...xprTail];
346
- }
347
- else
348
- parent[prop] = [ ...rootPrefix.slice(0, rootPrefix.length-1), ...xpr];
349
- if (carrier.$scope === 'params')
350
- parent.param = true;
351
- else
352
- parent[prop].unshift('$self');
353
- }
359
+ if(isPrefixed)
360
+ refChanged = isPrefixed;
354
361
  }
355
362
  }
356
-
357
363
  propNames.forEach(pn => {
364
+ refChanged = false;
358
365
  refCheck.anno = pn;
359
366
  transformExpression(carrier, pn, [ refCheck, refFlattener ], csnPath);
360
- });
361
- adaptRefs.forEach(fn => fn(refParentIsItems));
362
- adaptRefs.length = 0;
363
- propNames.forEach(pn => {
367
+ adaptRefs.forEach(fn =>
368
+ { if( fn(refParentIsItems)) refChanged = true });
369
+ adaptRefs.length = 0;
364
370
  transformExpression(carrier, pn, absolutifier, csnPath)
365
- })
371
+ if(refChanged && carrier[pn]['='])
372
+ carrier[pn]['='] = true;
373
+ });
366
374
  }
367
375
 
368
376
  // TODO: This should be part of the generic path rewriting algorithm
@@ -437,13 +445,17 @@ function flattenAllStructStepsInRefs( csn, refFlattener, adaptRefs, inspectRef,
437
445
  typeNames.forEach(tn => {
438
446
  forEachMemberRecursively(csn.definitions[tn], (member, memberName, prop, csnPath) => {
439
447
  Object.keys(member).filter(pn => pn[0] === '@').forEach(pn => {
448
+ let refChanged = false;
440
449
  refCheck.anno = pn;
441
450
  transformExpression(member, pn, [ refCheck, refFlattener ], csnPath);
451
+ adaptRefs.forEach(fn => {
452
+ if (fn(true, 1)) refChanged = true });
453
+ adaptRefs.length = 0;
454
+ if(refChanged && member[pn]['='])
455
+ member[pn]['='] = true;
442
456
  });
443
457
  }, [ 'definitions', tn ]);
444
458
  })
445
- adaptRefs.forEach(fn => fn(true, 1));
446
- adaptRefs.length = 0;
447
459
  }
448
460
 
449
461
  function getStructRefFlatteningTransformer(csn, inspectRef, effectiveType, options, resolved, pathDelimiter) {
@@ -468,6 +480,7 @@ function getStructRefFlatteningTransformer(csn, inspectRef, effectiveType, optio
468
480
  const adaptRefs = [];
469
481
  const transformer = {
470
482
  ref: (parent, prop, ref, path) => {
483
+ let refChanged = false;
471
484
  const { links, art, scope } = inspectRef(path);
472
485
  const resolvedLinkTypes = resolveLinkTypes(links);
473
486
  setProp(parent, '$path', [ ...path ]);
@@ -480,7 +493,7 @@ function getStructRefFlatteningTransformer(csn, inspectRef, effectiveType, optio
480
493
  // full path into target, uncomment this line and
481
494
  // comment/remove setProp in expansion.js
482
495
  // setProp(parent, '$structRef', parent.ref);
483
- parent.ref = flattenStructStepsInRef(ref,
496
+ [ parent.ref, refChanged ] = flattenStructStepsInRef(ref,
484
497
  scopedPath, links, scope, resolvedLinkTypes,
485
498
  suspend, suspendPos, parent.$bparam);
486
499
  resolved.set(parent, { links, art, scope });
@@ -496,9 +509,10 @@ function getStructRefFlatteningTransformer(csn, inspectRef, effectiveType, optio
496
509
  (insideColumns(scopedPath) || insideKeys(scopedPath)) &&
497
510
  !parent.as) {
498
511
  parent.as = lastRef;
499
- }
512
+ }
513
+ return refChanged;
500
514
  }
501
-
515
+ return false;
502
516
  /**
503
517
  * Return true if the path points inside columns
504
518
  *
@@ -326,12 +326,12 @@ function getTransformers(model, options, msgFunctions, pathDelimiter = '_') {
326
326
  * @param {bool} [suspend] suspend flattening by caller until association path step
327
327
  * @param {int} [suspendPos] suspend if starting pos is lower or equal to suspendPos and suspend is true
328
328
  * @param {bool} [revokeAtSuspendPos] revoke suspension after suspendPos (binding parameter path use case)
329
- * @returns {string[]}
329
+ * @returns [string[], bool]
330
330
  */
331
331
  function flattenStructStepsInRef(ref, path, links, scope, resolvedLinkTypes=new WeakMap(), suspend=false, suspendPos=0, revokeAtSuspendPos=false) {
332
332
  // Refs of length 1 cannot contain steps - no need to check
333
333
  if (ref.length < 2 || (scope === '$self' && ref.length === 2)) {
334
- return ref;
334
+ return [ ref, false ];
335
335
  }
336
336
 
337
337
  const result = scope === '$self' ? [ref[0]] : [];
@@ -342,7 +342,7 @@ function getTransformers(model, options, msgFunctions, pathDelimiter = '_') {
342
342
  scope = res.scope;
343
343
  }
344
344
  if (scope === '$magic')
345
- return ref;
345
+ return [ ref, false ];
346
346
 
347
347
  // Don't process a leading $self - it will a .art with .elements!
348
348
  let i = scope === '$self' ? 1 : 0;
@@ -353,7 +353,8 @@ function getTransformers(model, options, msgFunctions, pathDelimiter = '_') {
353
353
  effectiveType(links[i].art)[propName] ||
354
354
  (resolvedLinkTypes.get(links[i])||{})[propName]);
355
355
 
356
- let flattenStep = false;
356
+ let refChanged = false
357
+ let flattenStep = false;
357
358
  suspend = !!art('items') || (suspend && i <= suspendPos);
358
359
  for(; i < links.length; i++) {
359
360
 
@@ -365,6 +366,7 @@ function getTransformers(model, options, msgFunctions, pathDelimiter = '_') {
365
366
  ref[i].id = result[result.length-1];
366
367
  result[result.length-1] = ref[i];
367
368
  }
369
+ refChanged = true;
368
370
  // suspend flattening if the next path step has some 'items'
369
371
  suspend = !!art('items');
370
372
  }
@@ -381,7 +383,7 @@ function getTransformers(model, options, msgFunctions, pathDelimiter = '_') {
381
383
  !links[i].art?.from &&
382
384
  art('elements');
383
385
  }
384
- return result;
386
+ return [ result, refChanged ];
385
387
  }
386
388
 
387
389
  /**
@@ -280,7 +280,8 @@ module.exports = (csn, options) => {
280
280
  },
281
281
  params: (parent, prop, params) => {
282
282
  forEachValue(params, (param) => {
283
- const propagateToParams = typeof param.type === 'string' ? csn.definitions[param.type]?.kind !== 'entity' : true;
283
+ const propagateToParams = (param.type !== '$self' || csn.definitions.$self) &&
284
+ (typeof param.type !== 'string' || csn.definitions[param.type]?.kind !== 'entity');
284
285
  propagateMemberPropsFromOrigin(param, {
285
286
  '@': !propagateToParams, items: true, elements: true, enum: true, virtual: true,
286
287
  });
@@ -339,8 +340,8 @@ module.exports = (csn, options) => {
339
340
  copyProperties(origin, target, getMemberPropagationRuleFor, except, force);
340
341
 
341
342
  // For a `type of` with .items, we want to take stuff from types (which we skip for "normal" propagation, see specialItemsRules).
342
- // So for a type of we also propagate stuff from the virtual origin (which we don't give a "kind", therefore skipping that part of the check)
343
- if (target.type && target.type.ref)
343
+ // So for a `type of` we also propagate stuff from the virtual origin (which we don't give a "kind", therefore skipping that part of the check)
344
+ if (target.type?.ref)
344
345
  copyProperties(virtualOrigin, target, getMemberPropagationRuleFor, except);
345
346
 
346
347
  if (!target.kind)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds-compiler",
3
- "version": "4.9.0",
3
+ "version": "4.9.4",
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)",