@sap/cds-compiler 5.1.2 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/bin/cdsc.js +2 -2
  3. package/bin/cdshi.js +24 -17
  4. package/bin/cdsse.js +17 -18
  5. package/lib/api/main.js +19 -2
  6. package/lib/api/options.js +4 -1
  7. package/lib/base/builtins.js +1 -0
  8. package/lib/base/message-registry.js +16 -3
  9. package/lib/base/model.js +0 -10
  10. package/lib/checks/actionsFunctions.js +0 -12
  11. package/lib/checks/structuredAnnoExpressions.js +10 -14
  12. package/lib/compiler/assert-consistency.js +19 -11
  13. package/lib/compiler/builtins.js +1 -1
  14. package/lib/compiler/define.js +3 -3
  15. package/lib/compiler/extend.js +5 -5
  16. package/lib/compiler/populate.js +9 -9
  17. package/lib/compiler/propagator.js +1 -0
  18. package/lib/compiler/resolve.js +29 -34
  19. package/lib/compiler/shared.js +7 -8
  20. package/lib/compiler/tweak-assocs.js +155 -64
  21. package/lib/compiler/utils.js +1 -1
  22. package/lib/compiler/xpr-rewrite.js +4 -3
  23. package/lib/edm/annotations/genericTranslation.js +13 -9
  24. package/lib/edm/csn2edm.js +26 -2
  25. package/lib/edm/edm.js +23 -8
  26. package/lib/edm/edmInboundChecks.js +5 -7
  27. package/lib/edm/edmPreprocessor.js +43 -30
  28. package/lib/gen/BaseParser.js +720 -0
  29. package/lib/gen/CdlParser.js +4421 -0
  30. package/lib/gen/language.checksum +1 -1
  31. package/lib/gen/language.interp +1 -1
  32. package/lib/gen/languageParser.js +4006 -4001
  33. package/lib/language/antlrParser.js +62 -0
  34. package/lib/language/genericAntlrParser.js +28 -0
  35. package/lib/model/csnUtils.js +2 -0
  36. package/lib/model/revealInternalProperties.js +2 -0
  37. package/lib/modelCompare/utils/filter.js +70 -42
  38. package/lib/optionProcessor.js +9 -3
  39. package/lib/parsers/AstBuildingParser.js +1172 -0
  40. package/lib/parsers/CdlGrammar.g4 +1940 -0
  41. package/lib/parsers/Lexer.js +239 -0
  42. package/lib/render/toCdl.js +23 -27
  43. package/lib/render/toSql.js +5 -5
  44. package/lib/transform/db/applyTransformations.js +54 -16
  45. package/lib/transform/draft/odata.js +10 -11
  46. package/lib/transform/effective/flattening.js +10 -14
  47. package/lib/transform/odata/flattening.js +42 -31
  48. package/lib/transform/odata/toFinalBaseType.js +7 -6
  49. package/lib/transform/universalCsn/universalCsnEnricher.js +1 -0
  50. package/package.json +2 -2
  51. package/share/messages/redirected-to-ambiguous.md +5 -4
@@ -789,8 +789,8 @@ function populate( model ) {
789
789
  return col.name.id;
790
790
  }
791
791
  }
792
- else if (col.expand || col.value && (col._pathHead || query._parent.kind !== 'select')) {
793
- // _pathHead => inline/expand; _parent -> only allowed in sub-selects
792
+ else if (col.expand || col.value && (col._columnParent || query._parent.kind !== 'select')) {
793
+ // _columnParent => inline/expand; _parent -> only allowed in sub-selects
794
794
  error( 'query-req-name', [ col.value?.location || col.location, query ], {},
795
795
  'Alias name is required for this select item' );
796
796
  }
@@ -867,7 +867,7 @@ function populate( model ) {
867
867
  const inferred = query._main.$inferred;
868
868
  const excludingDict = (colParent || query).excludingDict || Object.create( null );
869
869
 
870
- const envParent = wildcard._pathHead; // TODO: rename _pathHead to _columnParent
870
+ const envParent = wildcard._columnParent;
871
871
  const env = wildcardColumnEnv( wildcard, query );
872
872
  if (!env)
873
873
  return;
@@ -931,7 +931,7 @@ function populate( model ) {
931
931
  // already done in populateQuery (TODO: change that and check whether
932
932
  // `*` is allowed at all in definer)
933
933
  if (!colParent || colParent.value._artifact) {
934
- // avoid "not found" messages if pathHead can't be found
934
+ // avoid "not found" messages if columnParent can't be found
935
935
  const user = colParent || query;
936
936
  for (const name in user.excludingDict)
937
937
  resolveExcluding( name, env, excludingDict, query );
@@ -939,9 +939,9 @@ function populate( model ) {
939
939
  }
940
940
  }
941
941
 
942
- function wildcardColumnEnv( wildcard, query ) { // etc. wildcard._pathHead;
942
+ function wildcardColumnEnv( wildcard, query ) { // etc. wildcard._columnParent;
943
943
  // if (envParent) console.log( 'CE:', envParent._origin, query );
944
- const colParent = wildcard._pathHead;
944
+ const colParent = wildcard._columnParent;
945
945
  if (!colParent)
946
946
  return userQuery( query )._combined; // see combinedSourcesOrParentElements
947
947
 
@@ -985,14 +985,14 @@ function populate( model ) {
985
985
  }
986
986
  }
987
987
 
988
- function setWildcardExpandInline( queryElem, pathHead, origin, name, location ) {
989
- setLink( queryElem, '_pathHead', pathHead );
988
+ function setWildcardExpandInline( queryElem, columnParent, origin, name, location ) {
989
+ setLink( queryElem, '_columnParent', columnParent );
990
990
  const path = [ { id: name, location } ];
991
991
  queryElem.value = { path, location }; // TODO: can we omit that? We have _origin
992
992
  setArtifactLink( path[0], origin );
993
993
  setLink( queryElem, '_origin', origin );
994
994
  // set _projections when inline with table alias:
995
- // const alias = pathHead?.value?.path?.[0]?._navigation;
995
+ // const alias = columnParent?.value?.path?.[0]?._navigation;
996
996
  // if (alias?.kind === '$tableAlias')
997
997
  // pushLink( alias.elements[name], '_projections', queryElem );
998
998
  }
@@ -66,6 +66,7 @@ function propagate( model ) {
66
66
  __proto__: null,
67
67
  never,
68
68
  onlyViaArtifact,
69
+ onlyViaParent,
69
70
  notWithPersistenceTable,
70
71
  };
71
72
  for (const rule in propagationRules)
@@ -174,9 +174,10 @@ function resolve( model ) {
174
174
  // TODO: or should we push elems with `expand` sibling to extra list for
175
175
  // better messages? (Whatever that means exactly.)
176
176
 
177
- if (elem._pathHead) {
178
- if (elem._pathHead?.kind !== '$inline')
179
- // we're traversing top-level elements of the query; other _pathHead kinds can't happen
177
+ if (elem._columnParent) {
178
+ if (elem._columnParent?.kind !== '$inline')
179
+ // we're traversing top-level elements of the query;
180
+ // other _columnParent kinds can't happen
180
181
  throw new CompilerAssertion('found unexpected "expand", but expected "inline"');
181
182
 
182
183
  if (!isPathBreakout( elem.value )) {
@@ -205,13 +206,17 @@ function resolve( model ) {
205
206
  if (!nav.item._navigation) // first non-table-alias
206
207
  setLink( nav.item, '_navigation', navItem );
207
208
 
208
- if (path[index].where || path[index].args)
209
- return;
209
+ // We consider an element only projected if the path doesn't have
210
+ // either arguments or filters; but we build up the navigation env
211
+ // nonetheless, as it makes rewriting paths later on easier.
212
+ let isComplexPath = !!(path[index].where || path[index].args);
213
+
210
214
  ++index;
211
215
  while (navItem && index < path.length) {
212
216
  const step = path[index];
213
- if (!step?.id || step.where || step.args)
217
+ if (!step?.id)
214
218
  break;
219
+ isComplexPath ||= !!(step.where || step.args);
215
220
  if (!navItem.elements?.[step.id]) {
216
221
  const elements = navItem._origin?.elements ||
217
222
  navItem._origin?.target?._artifact?.elements;
@@ -228,26 +233,26 @@ function resolve( model ) {
228
233
  setLink( step, '_navigation', navItem );
229
234
  ++index;
230
235
  }
231
- // Last path step, if found, is a simple projection
236
+ // Last path step, if found, is a projected, either complex or simple.
232
237
  if (index === path.length && navItem)
233
- pushLink( navItem, '_projections', elem );
238
+ pushLink( navItem, isComplexPath ? '_complexProjections' : '_projections', elem );
234
239
  }
235
240
  }
236
241
 
237
242
  function columnParentPath( elem ) {
238
- if (!elem._pathHead || !elem.value?.path || isPathBreakout( elem.value ))
243
+ if (!elem._columnParent || !elem.value?.path || isPathBreakout( elem.value ))
239
244
  return elem.value?.path;
240
245
 
241
246
  const fullPath = [ ...elem.value.path ];
242
- let pathHead = elem._pathHead;
243
- while (pathHead) {
244
- if (pathHead.kind !== '$inline' || !pathHead.value?.path ||
245
- isPathBreakout( pathHead.value )) {
247
+ let columnParent = elem._columnParent;
248
+ while (columnParent) {
249
+ if (columnParent.kind !== '$inline' || !columnParent.value?.path ||
250
+ isPathBreakout( columnParent.value )) {
246
251
  // path breakout for e.g. `$self.{ foo }`, `1 as a .{ foo }`
247
252
  return null;
248
253
  }
249
- fullPath.unshift(...pathHead.value.path);
250
- pathHead = pathHead._pathHead;
254
+ fullPath.unshift(...columnParent.value.path);
255
+ columnParent = columnParent._columnParent;
251
256
  }
252
257
  return fullPath;
253
258
  }
@@ -285,16 +290,16 @@ function resolve( model ) {
285
290
  return false;
286
291
  }
287
292
 
288
- function inheritedSourceKeyProp( { value, _pathHead } ) {
293
+ function inheritedSourceKeyProp( { value, _columnParent } ) {
289
294
  if (!value || !value.path)
290
295
  return null;
291
- const nav = !_pathHead && pathNavigation( value );
296
+ const nav = !_columnParent && pathNavigation( value );
292
297
  const item = value.path[value.path.length - 1];
293
298
  if (nav?.navigation && nav.item === item)
294
299
  return item._artifact?.key;
295
- if (value.path.length !== 1 || _pathHead?.kind !== '$inline')
300
+ if (value.path.length !== 1 || _columnParent?.kind !== '$inline')
296
301
  return null;
297
- const hpath = _pathHead.value?.path;
302
+ const hpath = _columnParent.value?.path;
298
303
  const head = hpath?.length === 1 && hpath[0]._navigation;
299
304
  return head?.kind === '$tableAlias' && item._artifact?.key;
300
305
  }
@@ -344,7 +349,7 @@ function resolve( model ) {
344
349
  const elem = query.elements[name];
345
350
 
346
351
  if (!elem.$inferred && elem.value?.path) {
347
- const path = elem._pathHead ? columnParentPath( elem ) : elem.value.path;
352
+ const path = elem._columnParent ? columnParentPath( elem ) : elem.value.path;
348
353
  if (testExpr({ path }, selectTest, () => false, elem))
349
354
  propagateKeys = false;
350
355
  }
@@ -1008,7 +1013,7 @@ function resolve( model ) {
1008
1013
  }
1009
1014
  const target = resolvePath( obj.target, 'target', art );
1010
1015
 
1011
- if (obj._pathHead && obj.type && !obj.type.$inferred && art._main && art._main.query) {
1016
+ if (obj._columnParent && obj.type && !obj.type.$inferred && art._main && art._main.query) {
1012
1017
  // New association inside expand/inline: The on-condition can't be properly checked,
1013
1018
  // so abort early. See #8797
1014
1019
  error( 'query-unexpected-assoc', [ obj.name.location, art ], {},
@@ -1253,18 +1258,7 @@ function resolve( model ) {
1253
1258
  error( 'type-invalid-cast', [ elem.type.location, elem ], { '#': 'assoc' } );
1254
1259
  return;
1255
1260
  }
1256
- // console.log(message( null, elem.location, elem, {target,art:assoc}, 'Info','RE')
1257
- // .toString(), elem.value)
1258
- const nav = elem._main && elem._main.query && elem.value && pathNavigation( elem.value );
1259
- if (nav && nav.item !== elem.value.path[elem.value.path.length - 1]) {
1260
- if (!elem.on && origType.on) {
1261
- error( 'rewrite-not-supported', [ elem.target.location, elem ] );
1262
- return;
1263
- }
1264
- }
1265
1261
  const origTarget = origType.target._artifact;
1266
- // console.log(require('../model/revealInternalProperties').ref(elem),
1267
- // !!origTarget,!!origType._effectiveType,!!origType.target)
1268
1262
  if (!origTarget || !target)
1269
1263
  return;
1270
1264
 
@@ -1377,7 +1371,7 @@ function resolve( model ) {
1377
1371
  if (a.path && a.kind !== '$self' && a.kind !== 'mixin')
1378
1372
  sources.push( a );
1379
1373
  }
1380
- if (alias.kind === '$tablealias')
1374
+ if (alias.kind === '$tableAlias')
1381
1375
  news.push( { chain: [ alias, ...chain ], sources } );
1382
1376
  else
1383
1377
  news.push( { chain, sources } );
@@ -1593,7 +1587,8 @@ function pathNavigation( ref ) {
1593
1587
  if (root.kind === '$self')
1594
1588
  return { item, tableAlias: root };
1595
1589
  if (root.kind !== '$tableAlias' || ref.path.length < 2)
1596
- return {}; // should not happen
1590
+ return {}; // should not happen
1591
+ // table alias
1597
1592
  return { navigation: root.elements?.[item.id], item, tableAlias: root };
1598
1593
  }
1599
1594
 
@@ -1,5 +1,4 @@
1
1
  // Compiler functions and utilities shared across all phases
2
- // TODO: rename to paths.js and move non resolve-paths functions to somewhere else
3
2
 
4
3
  'use strict';
5
4
 
@@ -546,7 +545,7 @@ function fns( model ) {
546
545
  ruser = ruser._outer;
547
546
 
548
547
  // Handle expand/inline, `type of`, :param, global (internally for CDL):
549
- if (user._pathHead && !semantics.isMainRef) { // in expand/inline
548
+ if (user._columnParent && !semantics.isMainRef) { // in expand/inline
550
549
  const { name } = semantics;
551
550
  semantics = semantics.nestedColumn();
552
551
  semantics.name = name;
@@ -563,7 +562,7 @@ function fns( model ) {
563
562
 
564
563
  // Search in lexical environments, including $self/$projection:
565
564
  const { isMainRef } = semantics;
566
- const lexical = semantics.lexical?.( ruser ); // TODO: _pathHead?
565
+ const lexical = semantics.lexical?.( ruser ); // TODO: _columnParent?
567
566
  if (lexical) {
568
567
  const [ nextProp, dictProp ] = (isMainRef)
569
568
  ? [ '_block', 'artifacts' ]
@@ -608,7 +607,7 @@ function fns( model ) {
608
607
  // element item in the path)
609
608
  // TODO - think about setting _navigation for all $navElement – the
610
609
  // "ref: ['tabAlias']: inline: […]" handling might be easier
611
- // (no _pathHead consultation for key prop and renaming support)
610
+ // (no _columnParent consultation for key prop and renaming support)
612
611
  function getPathItem( ref, semantics, user ) {
613
612
  // let art = (headArt && headArt.kind === '$tableAlias') ? headArt._origin : headArt;
614
613
  const { path } = ref;
@@ -918,7 +917,7 @@ function fns( model ) {
918
917
  }
919
918
 
920
919
  function nestedElements( user ) {
921
- const colParent = user._pathHead;
920
+ const colParent = user._columnParent;
922
921
  Functions.effectiveType( colParent ); // set _origin
923
922
  const path = colParent?.value?.path;
924
923
  if (!path?.length)
@@ -1129,7 +1128,7 @@ function fns( model ) {
1129
1128
  }
1130
1129
 
1131
1130
  function undefinedNestedElement( user, head, valid, _dict, _art, path, semantics ) {
1132
- const art = user._pathHead._origin;
1131
+ const art = user._columnParent._origin;
1133
1132
  if (!art)
1134
1133
  return null; // no consequential error
1135
1134
  return undefinedItemElement( user, head, valid, null, art, path, semantics );
@@ -1551,7 +1550,7 @@ function fns( model ) {
1551
1550
  const location = (user.expand || user.inline)[$location];
1552
1551
  // mention `table alias` in text only with initial single path item ref,
1553
1552
  // but do not mention that $self { … } is allowed, shouldn't be advertised:
1554
- const txt = (path.length > 1 || user._pathHead) ? 'struct' : 'init';
1553
+ const txt = (path.length > 1 || user._columnParent) ? 'struct' : 'init';
1555
1554
  const code = (user.expand) ? '{ ‹expand› }' : '.{ ‹inline› }';
1556
1555
  message( 'def-unexpected-nested-proj', [ location, user ], { '#': txt, code } );
1557
1556
  }
@@ -1655,7 +1654,7 @@ function fns( model ) {
1655
1654
 
1656
1655
  function checkOnlyForeignKeyNavigation( user, path, startIndex = 0, msgPrefix = '' ) {
1657
1656
  // has to be run after foreign-key rewrite
1658
- const outer = user._pathHead?._origin;
1657
+ const outer = user._columnParent?._origin;
1659
1658
  let assoc = outer?.foreignKeys &&
1660
1659
  pathStartsWithSelf( { path } ) == null && // not $self or CDS var like $now
1661
1660
  outer;
@@ -73,7 +73,6 @@ function tweakAssocs( model ) {
73
73
  // Only top-level queries and sub queries in FROM
74
74
 
75
75
  function rewriteArtifact( art ) {
76
- // return;
77
76
  if (!art.query) {
78
77
  rewriteAssociation( art );
79
78
  }
@@ -333,27 +332,62 @@ function tweakAssocs( model ) {
333
332
 
334
333
  // TODO: split this function: create foreign keys without `targetElement`
335
334
  // already in Phase 2: redirectImplicitly()
336
- // console.log(message( null, elem.location, elem, {art:assoc,target:assoc.target},
337
- // 'Info','FK').toString())
338
335
  elem.foreignKeys = Object.create(null); // set already here (also for zero foreign keys)
339
336
  forEachInOrder( assoc, 'foreignKeys', ( orig, name ) => {
340
337
  const location = weakRefLocation( elem.target );
341
338
  const fk = linkToOrigin( orig, name, elem, 'foreignKeys', location );
342
339
  fk.$inferred = 'rewrite'; // Override existing value; TODO: other $inferred value?
343
340
  setLink( fk, '_effectiveType', fk );
344
- const te = copyExpr( orig.targetElement, location );
345
- if (elem._redirected) {
346
- const i = te.path[0]; // TODO: or also follow path like for ON?
347
- const state = rewriteItem( elem, i, i.id, elem, true );
348
- if (state && state !== true && te.path.length === 1)
349
- setArtifactLink( te, state );
350
- }
351
- fk.targetElement = te;
341
+ fk.targetElement = copyExpr( orig.targetElement, location );
342
+ if (elem._redirected)
343
+ rewriteKey( elem, fk.targetElement );
352
344
  } );
353
345
  if (elem.foreignKeys) // Possibly no fk was set
354
346
  elem.foreignKeys[$inferred] = 'rewrite';
355
347
  }
356
348
 
349
+ function rewriteKey( elem, targetElement ) {
350
+ let projectedKey = null;
351
+ // rewrite along redirection chain
352
+ for (const alias of elem._redirected) {
353
+ if (alias.kind !== '$tableAlias')
354
+ continue;
355
+
356
+ projectedKey = firstProjectionForPath( targetElement.path, 0, alias, null );
357
+ if (projectedKey.elem) {
358
+ const item = targetElement.path[projectedKey.index];
359
+ item.id = projectedKey.elem.name.id;
360
+ if (projectedKey.index > 0)
361
+ targetElement.path.splice(0, projectedKey.index);
362
+ }
363
+ else {
364
+ setArtifactLink( targetElement.path[0], null );
365
+ setArtifactLink( targetElement, null );
366
+
367
+ const culprit = !elem.target.$inferred && elem.target ||
368
+ elem.value?.path?.[elem.value.path.length - 1] ||
369
+ elem;
370
+ // TODO: probably better to collect the non-projected foreign keys
371
+ // and have one message for all
372
+ error('rewrite-undefined-key', [ weakLocation( culprit.location ), elem ], {
373
+ '#': 'std',
374
+ id: targetElement.path.map(p => p.id).join('.'),
375
+ target: alias._main,
376
+ name: elem.name.id,
377
+ });
378
+ return null;
379
+ }
380
+ }
381
+
382
+ if (projectedKey?.elem) {
383
+ const item = targetElement.path[0];
384
+ setArtifactLink( item, projectedKey.elem );
385
+ setArtifactLink( targetElement, projectedKey.elem );
386
+ return projectedKey.elem;
387
+ }
388
+ return null;
389
+ }
390
+
357
391
  // TODO: there is no need to rewrite the on condition of non-leading queries,
358
392
  // i.e. we could just have on = {…}
359
393
  // TODO: re-check $self rewrite (with managed composition of aspects),
@@ -380,27 +414,35 @@ function tweakAssocs( model ) {
380
414
  elem.on.$inferred = 'copy';
381
415
 
382
416
  const { navigation } = nav;
383
- if (!navigation) // TODO: what about $projection.assoc as myAssoc ?
417
+ if (!navigation) { // TODO: what about $projection.assoc as myAssoc ?
418
+ if (elem._columnParent)
419
+ error( 'rewrite-not-supported', [ elem.target.location, elem ], { '#': 'inline-expand' } );
384
420
  return; // should not happen: $projection, $magic, or ref to const
385
-
386
- // Currently, having an unmanaged association inside a struct is not
387
- // supported by this function:
388
- if (navigation !== assoc && navigation._origin !== assoc) { // TODO: re-check
389
- // For "assoc1.assoc2" and "struct.elem1.assoc2"
390
- if (elem._redirected !== null) // null = already reported
391
- error( 'rewrite-not-supported', [ elem.target.location, elem ] );
392
421
  }
393
- else if (!nav.tableAlias || nav.tableAlias.path) {
422
+ const isAssocInStruct = (navigation !== assoc && navigation._origin !== assoc);
423
+ if (isAssocInStruct) {
424
+ // For "[sub.]assoc1.assoc2": not supported, yet (#3977)
425
+ const multipleAssoc = elem.value.path.slice(0, -1).some(segment => segment._artifact?.target);
426
+ if (multipleAssoc && elem._redirected !== null) { // null = already reported
427
+ error('rewrite-not-supported', [ elem.target.location, elem ], { '#': 'secondary' });
428
+ return;
429
+ }
430
+ }
431
+
432
+ if (!nav.tableAlias || nav.tableAlias.path) {
433
+ const navEnv = followNavigationPath( elem.value?.path, nav ) || nav.tableAlias;
394
434
  traverseExpr( elem.on, 'rewrite-on', elem,
395
- expr => rewriteExpr( expr, elem, nav.tableAlias ) );
435
+ expr => rewriteExpr( expr, elem, nav.tableAlias, navEnv ) );
396
436
  }
397
- else if (elem._pathHead) {
398
- error( 'rewrite-not-supported', [ elem.target.location, elem ] );
437
+ else if (elem._columnParent) {
438
+ error( 'rewrite-not-supported', [ elem.target.location, elem ], { '#': 'inline-expand' } );
439
+ return;
399
440
  }
400
441
  else {
401
442
  // TODO: support that, now that the ON condition is rewritten in the right order
402
443
  error( null, [ elem.value.location, elem ], {},
403
444
  'Selecting unmanaged associations from a sub query is not supported' );
445
+ return;
404
446
  }
405
447
 
406
448
  addConditionFromAssocPublishing( elem, assoc, nav );
@@ -528,7 +570,7 @@ function tweakAssocs( model ) {
528
570
 
529
571
  // Caller must ensure ON-condition correctness via rewriteExpr()!
530
572
  function foreignKeysToOnCondition( elem, assoc, nav ) {
531
- if (model.options.testMode && !nav.tableAlias && !elem._pathHead && elem.$syntax !== 'calc')
573
+ if (model.options.testMode && !nav.tableAlias && !elem._columnParent && elem.$syntax !== 'calc')
532
574
  throw new CompilerAssertion('rewriting keys to cond: no tableAlias but not inline/calc');
533
575
 
534
576
  if ((!nav.tableAlias && elem.$syntax !== 'calc') || elem._parent?.kind === 'element' ||
@@ -571,8 +613,11 @@ function tweakAssocs( model ) {
571
613
  setLink( rhs.path[0], '_artifact', assoc );
572
614
  setLink( rhs, '_artifact', rhs.path[rhs.path.length - 1]._artifact );
573
615
 
574
- if (elem.$syntax !== 'calc') { // different to lhs!
575
- const projectedFk = firstProjectionForPath( rhs.path, 0, nav.tableAlias, elem );
616
+ if (elem.$syntax !== 'calc') {
617
+ // Not passing an element, as we don't want to use our own filtered association here!
618
+ // That's done for lhs.
619
+ const projectedFk = firstProjectionForPath( rhs.path, 0, nav.tableAlias, null );
620
+ // different to lhs!
576
621
  rewritePath( rhs, projectedFk.item, elem, projectedFk.elem, elem.value.location );
577
622
  }
578
623
 
@@ -607,7 +652,13 @@ function tweakAssocs( model ) {
607
652
  return cond;
608
653
  }
609
654
 
610
- function rewriteExpr( expr, assoc, tableAlias ) {
655
+ /**
656
+ * @param expr
657
+ * @param assoc
658
+ * @param tableAlias
659
+ * @param navEnv Navigation element / table alias, used to traverse/rewrite the path.
660
+ */
661
+ function rewriteExpr( expr, assoc, tableAlias, navEnv = tableAlias ) {
611
662
  // Rewrite ON condition (resulting in outside perspective) for association
612
663
  // 'assoc' in query or including entity from ON cond of mixin element /
613
664
  // element in included structure / element in source ref/d by table alias.
@@ -620,7 +671,7 @@ function tweakAssocs( model ) {
620
671
  return;
621
672
  if (!assoc._main)
622
673
  return;
623
- if (tableAlias) { // from ON cond of element in source ref/d by table alias
674
+ if (navEnv) { // from ON cond of element in source ref/d by table alias
624
675
  const source = tableAlias._origin;
625
676
  const root = expr.path[0]._navigation || expr.path[0]._artifact;
626
677
  if (!root || root._main !== source)
@@ -628,7 +679,8 @@ function tweakAssocs( model ) {
628
679
  if (expr.scope === 'param' || root.kind === '$parameters')
629
680
  return; // are not allowed anyway - there was an error before
630
681
  const startIndex = (root.kind === '$self' ? 1 : 0);
631
- const result = firstProjectionForPath( expr.path, startIndex, tableAlias, assoc );
682
+ const exprNavigation = (root.kind === '$self' ? tableAlias : navEnv);
683
+ const result = firstProjectionForPath( expr.path, startIndex, exprNavigation, assoc );
632
684
  // For `assoc[…]`, ensure that we don't rewrite to another projection on `assoc`.
633
685
  if (result.item && assoc._origin === result.item._artifact)
634
686
  result.elem = assoc;
@@ -647,8 +699,8 @@ function tweakAssocs( model ) {
647
699
  }
648
700
  return;
649
701
  }
650
- const nav = pathNavigation( expr );
651
- if (nav.navigation || nav.tableAlias) { // rewrite src elem, mixin, $self[.elem]
702
+ if (expr.path[0]._navigation) { // rewrite src elem, mixin, $self[.elem]
703
+ const nav = pathNavigation( expr );
652
704
  const elem = (assoc._origin === root) ? assoc : navProjection( nav.navigation, assoc );
653
705
  rewritePath( expr, nav.item, assoc, elem,
654
706
  nav.item ? nav.item.location : expr.path[0].location );
@@ -691,8 +743,9 @@ function tweakAssocs( model ) {
691
743
  if (!elem) {
692
744
  if (location) {
693
745
  const elemref = root._navigation?.kind === '$self' ? path.slice(1) : path;
746
+ // TODO: Fix message for sub-elements: `s: { a: Association on x=1, x: Integer};` for x
694
747
  error( 'rewrite-not-projected', [ location, assoc ], {
695
- name: assoc.name.id, art: item._artifact, elemref: { ref: elemref },
748
+ name: assoc.name.id, art: elemref[0]._artifact, elemref: { ref: elemref },
696
749
  }, {
697
750
  std: 'Projected association $(NAME) uses non-projected element $(ELEMREF)',
698
751
  element: 'Projected association $(NAME) uses non-projected element $(ELEMREF) of $(ART)',
@@ -729,14 +782,11 @@ function tweakAssocs( model ) {
729
782
  if (i === item)
730
783
  state = setArtifactLink( i, elem );
731
784
  }
732
- else if (i) {
733
- state = rewriteItem( state, i, i.id, assoc, false );
785
+ else {
786
+ state = rewriteItem( state, i, assoc );
734
787
  if (!state || state === true)
735
788
  break;
736
789
  }
737
- else {
738
- return;
739
- }
740
790
  }
741
791
  if (state !== true)
742
792
  setArtifactLink( ref, state );
@@ -749,9 +799,15 @@ function tweakAssocs( model ) {
749
799
  path.unshift( root );
750
800
  }
751
801
 
752
- function rewriteItem( elem, item, name, assoc, forKeys ) {
802
+ /**
803
+ * @param elem "Navigation environment" (element) for `item`.
804
+ * @param item Path segment to rewrite.
805
+ * @param assoc Published association of query.
806
+ */
807
+ function rewriteItem( elem, item, assoc ) {
753
808
  if (!elem._redirected)
754
809
  return true;
810
+ let name = item.id;
755
811
  for (const alias of elem._redirected) {
756
812
  // TODO: a message for the same situation as msg 'rewrite-shadowed'?
757
813
  if (alias.kind === '$tableAlias') { // _redirected also contains structures for includes
@@ -760,21 +816,8 @@ function tweakAssocs( model ) {
760
816
  // but its origins, too.
761
817
  const proj = navProjection( alias.elements[name], assoc );
762
818
  name = proj?.name?.id;
763
- if (!name) {
764
- if (!forKeys)
765
- break;
766
- setArtifactLink( item, null );
767
- const culprit = elem.target && !elem.target.$inferred && elem.target ||
768
- elem.value?.path?.[elem.value.path.length - 1] ||
769
- elem;
770
- // TODO: probably better to collect the non-projected foreign keys
771
- // and have one message for all
772
- error( 'rewrite-undefined-key', [ weakLocation( culprit.location ), assoc ], {
773
- '#': 'std', id: item.id, target: alias._main, name: assoc.name.id,
774
- });
775
- // ''
776
- return null;
777
- }
819
+ if (!name)
820
+ break;
778
821
  item.id = name;
779
822
  // TODO: Why not break here? Test test3/scenarios/AFC/db/view/consumption/C_ScopedRole.cds
780
823
  }
@@ -802,14 +845,20 @@ function tweakAssocs( model ) {
802
845
 
803
846
  function navProjection( navigation, preferred ) {
804
847
  // TODO: Info if more than one possibility?
805
- // console.log(navigation,navigation._projections)
806
848
  if (!navigation)
807
849
  return {};
808
- else if (!navigation._projections)
850
+
851
+ if (!navigation._projections && !navigation._complexProjections)
809
852
  return null;
810
- return (preferred && navigation._projections.includes( preferred ))
811
- ? preferred
812
- : navigation._projections[0] || null;
853
+
854
+ // _complexProjections contains projections that are not "simple",
855
+ // i.e. contain a filter or arguments. Only used if it contains our
856
+ // preferred association.
857
+ if (preferred && ( navigation._complexProjections?.includes( preferred ) ||
858
+ navigation._projections?.includes( preferred )))
859
+ return preferred;
860
+
861
+ return navigation._projections?.[0] || null;
813
862
  }
814
863
 
815
864
 
@@ -824,6 +873,9 @@ function navProjection( navigation, preferred ) {
824
873
  * The returned object `ret` has `ret.item`, which is the path item at index `ret.index`
825
874
  * that is projected. `ret.elem` is the element projection.
826
875
  *
876
+ * If nothing was found, `ret.elem` is null, and `ret.item` is the last segment for which
877
+ * there was a $navElement.
878
+ *
827
879
  * @param {any[]} path
828
880
  * @param {number} startIndex
829
881
  * @param {object} nav
@@ -844,30 +896,69 @@ function firstProjectionForPath( path, startIndex, nav, elem ) {
844
896
 
845
897
  let proj = null;
846
898
  let navItem = nav;
847
- for (let i = startIndex; i < path.length; ++i) {
848
- const item = path[i];
899
+ let navIndex = startIndex;
900
+ for (; navIndex < path.length; ++navIndex) {
901
+ const item = path[navIndex];
849
902
  navItem = item?.id && navItem.elements?.[item.id];
850
903
  if (!navItem) {
851
904
  break;
852
905
  }
853
- else if (navItem._projections) {
906
+ else if (navItem._projections || navItem._complexProjections) {
854
907
  const projElem = navProjection( navItem, elem );
855
908
  if (projElem && projElem === elem) {
856
909
  // in case the specified association is found, _always_ use it.
857
- return { index: i, item, elem };
910
+ return { index: navIndex, item, elem };
858
911
  }
859
912
  else if (projElem) {
860
913
  const queryIndex = selectedElements.indexOf(projElem);
861
914
  if (!proj || queryIndex < proj.queryIndex) {
862
915
  proj = {
863
- index: i, item, elem: projElem, queryIndex,
916
+ index: navIndex, item, elem: projElem, queryIndex,
864
917
  };
865
918
  }
866
919
  }
867
920
  }
868
921
  }
922
+ if (proj)
923
+ return proj;
924
+
925
+ const index = (navIndex - 1) <= startIndex ? startIndex : (navIndex - 1);
926
+ return { index, item: path[index], elem: null };
927
+ }
928
+
929
+ /**
930
+ * Follow the navigation along the given path to its N-1 path step, so
931
+ * that the last step can be resolved against the returned navigation like
932
+ * `returnValue.elements[last.id]`.
933
+ *
934
+ * @param {XSN.Path} path
935
+ * @param {object} nav
936
+ * @returns {object|null}
937
+ */
938
+ function followNavigationPath( path, nav ) {
939
+ if (!nav.item || !path || path.length === 1)
940
+ return nav.tableAlias;
941
+
942
+ const startIndex = path.indexOf(nav.item);
943
+ if (startIndex === -1)
944
+ return null;
945
+
946
+ // navigation is already at last path step
947
+ if (startIndex === path.length - 1) {
948
+ return nav.navigation?.kind === '$navElement'
949
+ ? nav.navigation._parent
950
+ : nav.tableAlias;
951
+ }
952
+
953
+ let navItem = nav.navigation || nav.tableAlias;
954
+ for (let i = startIndex + 1; i < path.length - 1; ++i) {
955
+ const item = path[i];
956
+ navItem = item?.id && navItem.elements?.[item.id];
957
+ if (!navItem)
958
+ return null;
959
+ }
869
960
 
870
- return proj || { index: startIndex, item: path[startIndex], elem: null };
961
+ return navItem;
871
962
  }
872
963
 
873
964
  /**
@@ -607,7 +607,7 @@ function pathStartsWithSelf( ref ) {
607
607
  }
608
608
 
609
609
  function columnRefStartsWithSelf( col ) {
610
- for (; col; col = col._pathHead) {
610
+ for (; col; col = col._columnParent) {
611
611
  const ref = col.value;
612
612
  const head = ref && !ref.scope && ref.path?.[0];
613
613
  if (head?._navigation?.kind === '$self')