@sap/cds-compiler 5.1.2 → 5.3.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 (68) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/bin/cdsc.js +7 -2
  3. package/bin/cdshi.js +24 -17
  4. package/bin/cdsse.js +17 -18
  5. package/doc/CHANGELOG_BETA.md +9 -4
  6. package/lib/api/main.js +19 -2
  7. package/lib/api/options.js +4 -1
  8. package/lib/api/validate.js +5 -0
  9. package/lib/base/builtins.js +1 -0
  10. package/lib/base/message-registry.js +40 -3
  11. package/lib/base/messages.js +1 -1
  12. package/lib/base/model.js +0 -11
  13. package/lib/checks/actionsFunctions.js +0 -12
  14. package/lib/checks/structuredAnnoExpressions.js +10 -14
  15. package/lib/compiler/assert-consistency.js +21 -13
  16. package/lib/compiler/builtins.js +2 -2
  17. package/lib/compiler/checks.js +25 -6
  18. package/lib/compiler/define.js +27 -31
  19. package/lib/compiler/extend.js +16 -18
  20. package/lib/compiler/generate.js +3 -3
  21. package/lib/compiler/populate.js +22 -16
  22. package/lib/compiler/propagator.js +3 -2
  23. package/lib/compiler/resolve.js +87 -94
  24. package/lib/compiler/shared.js +12 -13
  25. package/lib/compiler/tweak-assocs.js +390 -86
  26. package/lib/compiler/utils.js +41 -33
  27. package/lib/compiler/xpr-rewrite.js +45 -58
  28. package/lib/edm/annotations/genericTranslation.js +17 -13
  29. package/lib/edm/csn2edm.js +28 -4
  30. package/lib/edm/edm.js +68 -28
  31. package/lib/edm/edmInboundChecks.js +5 -8
  32. package/lib/edm/edmPreprocessor.js +66 -40
  33. package/lib/edm/edmUtils.js +1 -1
  34. package/lib/gen/BaseParser.js +778 -0
  35. package/lib/gen/CdlParser.js +4477 -0
  36. package/lib/gen/language.checksum +1 -1
  37. package/lib/gen/language.interp +1 -1
  38. package/lib/gen/languageParser.js +4072 -4024
  39. package/lib/inspect/inspectPropagation.js +1 -1
  40. package/lib/json/from-csn.js +5 -3
  41. package/lib/json/to-csn.js +7 -10
  42. package/lib/language/antlrParser.js +96 -0
  43. package/lib/language/errorStrategy.js +1 -1
  44. package/lib/language/genericAntlrParser.js +32 -4
  45. package/lib/language/multiLineStringParser.js +1 -1
  46. package/lib/main.d.ts +23 -0
  47. package/lib/model/cloneCsn.js +22 -13
  48. package/lib/model/csnUtils.js +2 -0
  49. package/lib/model/revealInternalProperties.js +2 -0
  50. package/lib/modelCompare/utils/filter.js +70 -42
  51. package/lib/optionProcessor.js +16 -10
  52. package/lib/parsers/AstBuildingParser.js +1290 -0
  53. package/lib/parsers/CdlGrammar.g4 +2013 -0
  54. package/lib/parsers/Lexer.js +249 -0
  55. package/lib/render/toCdl.js +46 -45
  56. package/lib/render/toSql.js +5 -5
  57. package/lib/transform/addTenantFields.js +4 -4
  58. package/lib/transform/db/applyTransformations.js +54 -16
  59. package/lib/transform/draft/odata.js +10 -11
  60. package/lib/transform/effective/flattening.js +10 -14
  61. package/lib/transform/forRelationalDB.js +7 -6
  62. package/lib/transform/odata/flattening.js +42 -31
  63. package/lib/transform/odata/toFinalBaseType.js +7 -6
  64. package/lib/transform/universalCsn/universalCsnEnricher.js +1 -0
  65. package/lib/utils/moduleResolve.js +1 -1
  66. package/package.json +2 -2
  67. package/share/messages/redirected-to-ambiguous.md +5 -4
  68. package/share/messages/redirected-to-complex.md +6 -3
@@ -41,10 +41,13 @@ function tweakAssocs( model ) {
41
41
  extendForeignKeys,
42
42
  createRemainingAnnotateStatements,
43
43
  mergeSpecifiedForeignKeys,
44
+ navigationEnv,
45
+ redirectionChain,
44
46
  } = model.$functions;
45
47
 
46
48
  Object.assign(model.$functions, {
47
- firstProjectionForPath,
49
+ findRewriteTarget,
50
+ cachedRedirectionChain,
48
51
  });
49
52
 
50
53
  // Phase 5: rewrite associations
@@ -73,7 +76,6 @@ function tweakAssocs( model ) {
73
76
  // Only top-level queries and sub queries in FROM
74
77
 
75
78
  function rewriteArtifact( art ) {
76
- // return;
77
79
  if (!art.query) {
78
80
  rewriteAssociation( art );
79
81
  }
@@ -126,7 +128,7 @@ function tweakAssocs( model ) {
126
128
  // ID published! Used in stakeholder project; if renamed, add to oldMessageIds
127
129
  info( 'assoc-outside-service', loc, { '#': text, target, service: main._service }, {
128
130
  std: 'Association target $(TARGET) is outside any service',
129
- // eslint-disable-next-line max-len
131
+ // eslint-disable-next-line @stylistic/js/max-len
130
132
  exposed: 'If association is published in service $(SERVICE), its target $(TARGET) is outside any service',
131
133
  } );
132
134
  }
@@ -143,7 +145,7 @@ function tweakAssocs( model ) {
143
145
  if (assoc && assoc.foreignKeys) {
144
146
  error( 'rewrite-key-for-unmanaged', [ elem.on.location, elem ],
145
147
  { keyword: 'on', art: assocWithExplicitSpec( assoc ) },
146
- // eslint-disable-next-line max-len
148
+ // eslint-disable-next-line @stylistic/js/max-len
147
149
  'Do not specify an $(KEYWORD) condition when redirecting the managed association $(ART)' );
148
150
  }
149
151
  checkIgnoredFilter( elem );
@@ -333,27 +335,62 @@ function tweakAssocs( model ) {
333
335
 
334
336
  // TODO: split this function: create foreign keys without `targetElement`
335
337
  // already in Phase 2: redirectImplicitly()
336
- // console.log(message( null, elem.location, elem, {art:assoc,target:assoc.target},
337
- // 'Info','FK').toString())
338
338
  elem.foreignKeys = Object.create(null); // set already here (also for zero foreign keys)
339
339
  forEachInOrder( assoc, 'foreignKeys', ( orig, name ) => {
340
340
  const location = weakRefLocation( elem.target );
341
341
  const fk = linkToOrigin( orig, name, elem, 'foreignKeys', location );
342
342
  fk.$inferred = 'rewrite'; // Override existing value; TODO: other $inferred value?
343
343
  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;
344
+ fk.targetElement = copyExpr( orig.targetElement, location );
345
+ if (elem._redirected)
346
+ rewriteKey( elem, fk.targetElement );
352
347
  } );
353
348
  if (elem.foreignKeys) // Possibly no fk was set
354
349
  elem.foreignKeys[$inferred] = 'rewrite';
355
350
  }
356
351
 
352
+ function rewriteKey( elem, targetElement ) {
353
+ let projectedKey = null;
354
+ // rewrite along redirection chain
355
+ for (const alias of elem._redirected) {
356
+ if (alias.kind !== '$tableAlias')
357
+ continue;
358
+
359
+ projectedKey = firstProjectionForPath( targetElement.path, 0, alias, null );
360
+ if (projectedKey.elem) {
361
+ const item = targetElement.path[projectedKey.index];
362
+ item.id = projectedKey.elem.name.id;
363
+ if (projectedKey.index > 0)
364
+ targetElement.path.splice(0, projectedKey.index);
365
+ }
366
+ else {
367
+ setArtifactLink( targetElement.path[0], null );
368
+ setArtifactLink( targetElement, null );
369
+
370
+ const culprit = !elem.target.$inferred && elem.target ||
371
+ elem.value?.path?.[elem.value.path.length - 1] ||
372
+ elem;
373
+ // TODO: probably better to collect the non-projected foreign keys
374
+ // and have one message for all
375
+ error('rewrite-undefined-key', [ weakLocation( culprit.location ), elem ], {
376
+ '#': 'std',
377
+ id: targetElement.path.map(p => p.id).join('.'),
378
+ target: alias._main,
379
+ name: elem.name.id,
380
+ });
381
+ return null;
382
+ }
383
+ }
384
+
385
+ if (projectedKey?.elem) {
386
+ const item = targetElement.path[0];
387
+ setArtifactLink( item, projectedKey.elem );
388
+ setArtifactLink( targetElement, projectedKey.elem );
389
+ return projectedKey.elem;
390
+ }
391
+ return null;
392
+ }
393
+
357
394
  // TODO: there is no need to rewrite the on condition of non-leading queries,
358
395
  // i.e. we could just have on = {…}
359
396
  // TODO: re-check $self rewrite (with managed composition of aspects),
@@ -364,11 +401,18 @@ function tweakAssocs( model ) {
364
401
  // same (TODO later: set status whether rewrite changes anything),
365
402
  // especially problematic are refs starting with $self:
366
403
  setExpandStatus( elem, 'target' );
404
+
405
+ // There were previous issues in resolving the target artifact.
406
+ // Avoid further compiler messages.
407
+ if (!elem.target._artifact)
408
+ return;
409
+
367
410
  if (elem._parent?.kind === 'element') {
368
411
  // managed association as sub element not supported yet
369
412
  // TODO: Only report once for multi-include chains, see
370
413
  // Associations/SubElements/UnmanagedInSubElement.err.cds
371
414
  error( 'type-unsupported-rewrite', [ elem.location, elem ], { '#': 'sub-element' } );
415
+ removeArtifactLinks();
372
416
  return;
373
417
  }
374
418
  const nav = (elem._main?.query && elem.value)
@@ -380,31 +424,45 @@ function tweakAssocs( model ) {
380
424
  elem.on.$inferred = 'copy';
381
425
 
382
426
  const { navigation } = nav;
383
- if (!navigation) // TODO: what about $projection.assoc as myAssoc ?
427
+ if (!navigation) { // TODO: what about $projection.assoc as myAssoc ?
428
+ if (elem._columnParent) {
429
+ error( 'rewrite-not-supported', [ elem.target.location, elem ], { '#': 'inline-expand' } );
430
+ removeArtifactLinks();
431
+ }
384
432
  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
433
  }
393
- else if (!nav.tableAlias || nav.tableAlias.path) {
434
+
435
+ if (!nav.tableAlias || nav.tableAlias.path) {
436
+ const navEnv = followNavigationPath( elem.value?.path, nav ) || nav.tableAlias;
394
437
  traverseExpr( elem.on, 'rewrite-on', elem,
395
- expr => rewriteExpr( expr, elem, nav.tableAlias ) );
438
+ expr => rewriteExpr( expr, elem, nav.tableAlias, navEnv ) );
396
439
  }
397
- else if (elem._pathHead) {
398
- error( 'rewrite-not-supported', [ elem.target.location, elem ] );
440
+ else if (elem._columnParent) {
441
+ error( 'rewrite-not-supported', [ elem.target.location, elem ], { '#': 'inline-expand' } );
442
+ removeArtifactLinks();
443
+ return;
399
444
  }
400
445
  else {
401
446
  // TODO: support that, now that the ON condition is rewritten in the right order
402
447
  error( null, [ elem.value.location, elem ], {},
403
448
  'Selecting unmanaged associations from a sub query is not supported' );
449
+ removeArtifactLinks();
450
+ return;
404
451
  }
405
452
 
406
453
  addConditionFromAssocPublishing( elem, assoc, nav );
407
454
  elem.on.$inferred = 'rewrite';
455
+
456
+ /**
457
+ * Clear all `_artifact` links in the ON-condition to avoid follow-up
458
+ * issues during ON-condition rewriting of associations that inherit
459
+ * the ON-condition.
460
+ */
461
+ function removeArtifactLinks() {
462
+ traverseExpr( elem.on, 'rewrite-on', elem, (expr) => {
463
+ setArtifactLink( expr, null );
464
+ } );
465
+ }
408
466
  }
409
467
 
410
468
  /**
@@ -495,6 +553,7 @@ function tweakAssocs( model ) {
495
553
  function filterToCondition( assocPathStep, elem, nav ) {
496
554
  const cond = copyExpr( assocPathStep.where );
497
555
  cond.$parens = [ assocPathStep.location ];
556
+ const navEnv = nav && followNavigationPath( elem.value?.path, nav ) || nav?.tableAlias;
498
557
  traverseExpr( cond, 'rewrite-filter', elem, (expr) => {
499
558
  if (!expr.path || expr.path.length === 0)
500
559
  return;
@@ -518,7 +577,7 @@ function tweakAssocs( model ) {
518
577
  setLink( expr.path[0], '_navigation', assocPathStep._navigation );
519
578
  }
520
579
  // up to here, filter is relative to original association
521
- rewriteExpr( expr, elem, nav?.tableAlias );
580
+ rewriteExpr( expr, elem, nav?.tableAlias, navEnv );
522
581
  }
523
582
  } );
524
583
 
@@ -528,7 +587,7 @@ function tweakAssocs( model ) {
528
587
 
529
588
  // Caller must ensure ON-condition correctness via rewriteExpr()!
530
589
  function foreignKeysToOnCondition( elem, assoc, nav ) {
531
- if (model.options.testMode && !nav.tableAlias && !elem._pathHead && elem.$syntax !== 'calc')
590
+ if (model.options.testMode && !nav.tableAlias && !elem._columnParent && elem.$syntax !== 'calc')
532
591
  throw new CompilerAssertion('rewriting keys to cond: no tableAlias but not inline/calc');
533
592
 
534
593
  if ((!nav.tableAlias && elem.$syntax !== 'calc') || elem._parent?.kind === 'element' ||
@@ -571,8 +630,11 @@ function tweakAssocs( model ) {
571
630
  setLink( rhs.path[0], '_artifact', assoc );
572
631
  setLink( rhs, '_artifact', rhs.path[rhs.path.length - 1]._artifact );
573
632
 
574
- if (elem.$syntax !== 'calc') { // different to lhs!
575
- const projectedFk = firstProjectionForPath( rhs.path, 0, nav.tableAlias, elem );
633
+ if (elem.$syntax !== 'calc') {
634
+ // Not passing an element, as we don't want to use our own filtered association here!
635
+ // That's done for lhs.
636
+ const projectedFk = firstProjectionForPath( rhs.path, 0, nav.tableAlias, null );
637
+ // different to lhs!
576
638
  rewritePath( rhs, projectedFk.item, elem, projectedFk.elem, elem.value.location );
577
639
  }
578
640
 
@@ -607,33 +669,33 @@ function tweakAssocs( model ) {
607
669
  return cond;
608
670
  }
609
671
 
610
- function rewriteExpr( expr, assoc, tableAlias ) {
672
+ /**
673
+ * @param expr
674
+ * @param assoc
675
+ * @param tableAlias
676
+ * @param navEnv Navigation element / table alias, used to traverse/rewrite the path.
677
+ */
678
+ function rewriteExpr( expr, assoc, tableAlias, navEnv = tableAlias ) {
611
679
  // Rewrite ON condition (resulting in outside perspective) for association
612
680
  // 'assoc' in query or including entity from ON cond of mixin element /
613
681
  // element in included structure / element in source ref/d by table alias.
614
682
 
615
683
  // TODO: complain about $self (unclear semantics)
616
- // console.log( info(null, [assoc.name.location, assoc],
617
- // { art: expr._artifact, names: expr.path.map(i=>i.id) }, 'A').toString(), expr.path)
618
684
 
619
685
  if (!expr.path || !expr._artifact)
620
686
  return;
621
687
  if (!assoc._main)
622
688
  return;
623
- if (tableAlias) { // from ON cond of element in source ref/d by table alias
624
- const source = tableAlias._origin;
689
+ if (navEnv) { // from ON cond of element in source ref/d by table alias
625
690
  const root = expr.path[0]._navigation || expr.path[0]._artifact;
626
- if (!root || root._main !== source)
627
- return; // not $self or source element
691
+ if (!root || root.kind === 'builtin')
692
+ return; // not $self or source element, e.g. builtin
693
+
694
+ // parameters are not allowed in ON-conditions; error emitted elsewhere already
628
695
  if (expr.scope === 'param' || root.kind === '$parameters')
629
- return; // are not allowed anyway - there was an error before
630
- const startIndex = (root.kind === '$self' ? 1 : 0);
631
- const result = firstProjectionForPath( expr.path, startIndex, tableAlias, assoc );
632
- // For `assoc[…]`, ensure that we don't rewrite to another projection on `assoc`.
633
- if (result.item && assoc._origin === result.item._artifact)
634
- result.elem = assoc;
696
+ return;
635
697
 
636
- rewritePath( expr, result.item, assoc, result.elem, assoc.value.location );
698
+ rewritePathForEnv( expr, navEnv, assoc );
637
699
  }
638
700
  else if (assoc._main.query) { // from ON cond of mixin element in query
639
701
  const root = expr.path[0]._navigation || expr.path[0]._artifact;
@@ -641,15 +703,16 @@ function tweakAssocs( model ) {
641
703
  if (assoc.$errorReported !== 'assoc-unexpected-scope') {
642
704
  error( 'assoc-unexpected-scope', [ assoc.value.location, assoc ],
643
705
  { id: assoc.value._artifact.name.id },
644
- // eslint-disable-next-line max-len
706
+ // eslint-disable-next-line @stylistic/js/max-len
645
707
  'Association $(ID) can\'t be projected because its ON-condition refers to a parameter' );
646
708
  assoc.$errorReported = 'assoc-unexpected-scope';
647
709
  }
648
710
  return;
649
711
  }
650
- const nav = pathNavigation( expr );
651
- if (nav.navigation || nav.tableAlias) { // rewrite src elem, mixin, $self[.elem]
712
+ if (expr.path[0]._navigation) { // rewrite src elem, mixin, $self[.elem]
713
+ const nav = pathNavigation( expr );
652
714
  const elem = (assoc._origin === root) ? assoc : navProjection( nav.navigation, assoc );
715
+ // TODO: Use rewritePathForEnv(); make it handle mixins
653
716
  rewritePath( expr, nav.item, assoc, elem,
654
717
  nav.item ? nav.item.location : expr.path[0].location );
655
718
  }
@@ -673,11 +736,11 @@ function tweakAssocs( model ) {
673
736
  if (!(elem === item._artifact || // redirection for explicit def
674
737
  elem._origin === item._artifact)) {
675
738
  const art = assoc._origin;
676
- // eslint-disable-next-line max-len
739
+ // eslint-disable-next-line @stylistic/js/max-len
677
740
  warning( 'rewrite-shadowed', [ elem.name.location, elem ], { art: art && effectiveType( art ) }, {
678
- // eslint-disable-next-line max-len
741
+ // eslint-disable-next-line @stylistic/js/max-len
679
742
  std: 'This element is not originally referred to in the ON-condition of association $(ART)',
680
- // eslint-disable-next-line max-len
743
+ // eslint-disable-next-line @stylistic/js/max-len
681
744
  element: 'This element is not originally referred to in the ON-condition of association $(MEMBER) of $(ART)',
682
745
  } );
683
746
  }
@@ -685,17 +748,147 @@ function tweakAssocs( model ) {
685
748
  }
686
749
  }
687
750
 
751
+ /**
752
+ * Rewrite the given reference by using projected elements of the given
753
+ * navigation environment.
754
+ *
755
+ * @param {XSN.Expression} ref
756
+ * @param {object} navEnv
757
+ * @param {XSN.Artifact} user
758
+ */
759
+ function rewritePathForEnv( ref, navEnv, user ) {
760
+ // TODO: combine with rewriteGenericAnnoPath() of xpr-rewrite
761
+
762
+ // reset artifact link; we'll set it again if there are no errors
763
+ setArtifactLink( ref, null );
764
+
765
+ const rootItem = ref.path[0];
766
+ const root = ref.path[0]._navigation || ref.path[0]._artifact;
767
+ const startIndex = (root.kind === '$self' ? 1 : 0);
768
+
769
+ if (root.kind === '$self') {
770
+ let rootEnv = navEnv;
771
+ while (rootEnv?.kind === '$navElement') {
772
+ if (rootEnv._origin?.target?._artifact === root._origin)
773
+ break;
774
+ rootEnv = rootEnv._parent;
775
+ }
776
+ navEnv = rootEnv;
777
+ }
778
+
779
+ // Store the original artifact, so that we can use it to
780
+ // calculate a redirection chain later on.
781
+ ref.path.forEach((item) => {
782
+ if (item._artifact)
783
+ setLink( item, '_originalArtifact', item._artifact );
784
+ });
785
+
786
+ let env = navEnv;
787
+ let art = rootItem._artifact;
788
+ let isTargetSide = null;
789
+
790
+ for (let i = startIndex; i < ref.path.length; ++i) {
791
+ if (i > startIndex && art.target) {
792
+ // if the current artifact is an association, we need to respect the redirection
793
+ // chain from original target to new one.
794
+ // FIXME: Won't work with associations in projected structures.
795
+ const origTarget = ref.path[i - 1]?._originalArtifact?.target?._artifact;
796
+ const chain = cachedRedirectionChain( art, origTarget );
797
+ if (!chain) {
798
+ missingProjection( ref, i, user, false );
799
+ return;
800
+ }
801
+ for (const alias of chain) {
802
+ art = rewritePathItemForEnv( ref, alias, i, user );
803
+ isTargetSide ??= (art === user);
804
+ if (!art) {
805
+ missingProjection( ref, i, user, isTargetSide );
806
+ return;
807
+ }
808
+ }
809
+ }
810
+
811
+ art = rewritePathItemForEnv( ref, env, i, user );
812
+ isTargetSide ??= (art === user);
813
+ if (!art) {
814
+ missingProjection( ref, i, user, isTargetSide );
815
+ return;
816
+ }
817
+ env = navigationEnv( art, null, null, 'nav' );
818
+ }
819
+ setArtifactLink( ref, art );
820
+
821
+ if (startIndex === 0 && rootItem.id.startsWith('$')) {
822
+ // TODO: What about filters? Also rewritten there?
823
+ // After rewriting, if an element starts with `$` -> add root prefix
824
+ // FIXME: "user" not correct for association inside sub-element,
825
+ // because `user._parent` is assumed to be the query
826
+ prependSelfToPath( ref.path, user );
827
+ }
828
+ }
829
+
830
+ function rewritePathItemForEnv( ref, navEnv, index, user ) {
831
+ const rewriteTarget = findRewriteTarget( ref, index, navEnv, user );
832
+ const found = rewriteTarget[0];
833
+ if (!found) {
834
+ setArtifactLink( ref.path[index], found );
835
+ return found;
836
+ }
837
+
838
+ if (rewriteTarget[1] > index) {
839
+ // we keep the last segment, in case it has non-enumerable properties
840
+ ref.path[index] = ref.path[rewriteTarget[1]];
841
+ ref.path.splice(index + 1, rewriteTarget[1] - index);
842
+ }
843
+
844
+ const item = ref.path[index];
845
+ if (item.id !== found.name.id || (rewriteTarget[1] - index) !== 0)
846
+ item.id = found.name.id;
847
+
848
+ return setArtifactLink( ref.path[index], found );
849
+ }
850
+
851
+ /**
852
+ * @param {XSN.Path} ref
853
+ * @param {number} index
854
+ * @param {XSN.Artifact} user
855
+ * @param {boolean} isTargetSide
856
+ */
857
+ function missingProjection( ref, index, user, isTargetSide ) {
858
+ const item = ref.path[index];
859
+ if (!isTargetSide) {
860
+ const { location } = user.value;
861
+ const rootItem = ref.path[0];
862
+ const elemref = rootItem._navigation?.kind === '$self' ? ref.path.slice(1) : ref.path;
863
+ // TODO: Fix message for sub-elements: `s: { a: Association on x=1, x: Integer};` for x
864
+ error( 'rewrite-not-projected', [ location, user ], {
865
+ name: user.name.id,
866
+ art: item._artifact || item._originalArtifact,
867
+ elemref: { ref: elemref },
868
+ } );
869
+ }
870
+ else {
871
+ const isExplicit = user.target && !user.target.$inferred;
872
+ const loc = isExplicit ? user.target.location : item.location;
873
+ error( 'query-undefined-element', [ loc, user ], {
874
+ '#': isExplicit ? 'redirected' : 'std',
875
+ id: item.id,
876
+ name: user.name.id,
877
+ target: user.target._artifact,
878
+ keyword: 'redirected to',
879
+ } );
880
+ }
881
+ }
882
+
688
883
  function rewritePath( ref, item, assoc, elem, location ) {
689
884
  const { path } = ref;
690
885
  const root = path[0];
691
886
  if (!elem) {
692
887
  if (location) {
693
888
  const elemref = root._navigation?.kind === '$self' ? path.slice(1) : path;
889
+ // TODO: Fix message for sub-elements: `s: { a: Association on x=1, x: Integer};` for x
694
890
  error( 'rewrite-not-projected', [ location, assoc ], {
695
- name: assoc.name.id, art: item._artifact, elemref: { ref: elemref },
696
- }, {
697
- std: 'Projected association $(NAME) uses non-projected element $(ELEMREF)',
698
- element: 'Projected association $(NAME) uses non-projected element $(ELEMREF) of $(ART)',
891
+ name: assoc.name.id, art: elemref[0]._artifact, elemref: { ref: elemref },
699
892
  } );
700
893
  }
701
894
  delete root._navigation;
@@ -729,14 +922,11 @@ function tweakAssocs( model ) {
729
922
  if (i === item)
730
923
  state = setArtifactLink( i, elem );
731
924
  }
732
- else if (i) {
733
- state = rewriteItem( state, i, i.id, assoc, false );
925
+ else {
926
+ state = rewriteItem( state, i, assoc );
734
927
  if (!state || state === true)
735
928
  break;
736
929
  }
737
- else {
738
- return;
739
- }
740
930
  }
741
931
  if (state !== true)
742
932
  setArtifactLink( ref, state );
@@ -749,9 +939,15 @@ function tweakAssocs( model ) {
749
939
  path.unshift( root );
750
940
  }
751
941
 
752
- function rewriteItem( elem, item, name, assoc, forKeys ) {
942
+ /**
943
+ * @param elem "Navigation environment" (element) for `item`.
944
+ * @param item Path segment to rewrite.
945
+ * @param assoc Published association of query.
946
+ */
947
+ function rewriteItem( elem, item, assoc ) {
753
948
  if (!elem._redirected)
754
949
  return true;
950
+ let name = item.id;
755
951
  for (const alias of elem._redirected) {
756
952
  // TODO: a message for the same situation as msg 'rewrite-shadowed'?
757
953
  if (alias.kind === '$tableAlias') { // _redirected also contains structures for includes
@@ -760,21 +956,8 @@ function tweakAssocs( model ) {
760
956
  // but its origins, too.
761
957
  const proj = navProjection( alias.elements[name], assoc );
762
958
  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
- }
959
+ if (!name)
960
+ break;
778
961
  item.id = name;
779
962
  // TODO: Why not break here? Test test3/scenarios/AFC/db/view/consumption/C_ScopedRole.cds
780
963
  }
@@ -798,20 +981,98 @@ function tweakAssocs( model ) {
798
981
  } );
799
982
  return null;
800
983
  }
984
+
985
+ /**
986
+ * Get the redirection chain between the element's target and the original target.
987
+ * Returns `null` if there is no valid chain.
988
+ * Uses `_redirected` if valid.
989
+ *
990
+ * @param {XSN.Artifact} elem
991
+ * @param {XSN.Artifact} origTarget
992
+ * @returns {null|XSN.Artifact[]}
993
+ */
994
+ function cachedRedirectionChain( elem, origTarget ) {
995
+ const target = elem.target?._artifact;
996
+ if (!target || !origTarget)
997
+ return null;
998
+ if (target === origTarget)
999
+ return [];
1000
+
1001
+ if (elem._redirected === null) {
1002
+ // means: "don't touch paths after assoc"
1003
+ // TODO: figure out if we can assume that here as well
1004
+ return [];
1005
+ }
1006
+
1007
+ if (elem._redirected) {
1008
+ // No need to recalculate if the original target is already in '_redirected'.
1009
+ const i = elem._redirected.findIndex(ta => ta._origin === origTarget);
1010
+ if (i > -1)
1011
+ return elem._redirected.slice(i); // TODO: check if it is always "i===0".
1012
+ }
1013
+
1014
+ return redirectionChain( elem, target, origTarget, true );
1015
+ }
801
1016
  }
802
1017
 
803
1018
  function navProjection( navigation, preferred ) {
804
1019
  // TODO: Info if more than one possibility?
805
- // console.log(navigation,navigation._projections)
806
1020
  if (!navigation)
807
1021
  return {};
808
- else if (!navigation._projections)
1022
+
1023
+ if (!navigation._projections && !navigation._complexProjections)
809
1024
  return null;
810
- return (preferred && navigation._projections.includes( preferred ))
811
- ? preferred
812
- : navigation._projections[0] || null;
1025
+
1026
+ // _complexProjections contains projections that are not "simple",
1027
+ // i.e. contain a filter or arguments. Only used if it contains our
1028
+ // preferred association.
1029
+ if (preferred && ( navigation._complexProjections?.includes( preferred ) ||
1030
+ navigation._projections?.includes( preferred )))
1031
+ return preferred;
1032
+
1033
+ return navigation._projections?.[0] || null;
813
1034
  }
814
1035
 
1036
+ function findRewriteTarget( expr, index, env, user ) {
1037
+ if (env.kind === '$navElement' || env.kind === '$tableAlias') {
1038
+ const r = firstProjectionForPath( expr.path, index, env, user );
1039
+ return [ r.elem, r.index ];
1040
+ }
1041
+
1042
+ const item = expr.path[index];
1043
+ // If the artifact is already in the same definition, we must not check the query.
1044
+ // Or if it is not a query -> no $navElement -> use `elements`
1045
+ if (item._artifact?._main === env || !env.query && env.kind !== 'select') {
1046
+ if (env.elements?.[item.id])
1047
+ return [ env.elements[item.id], index ];
1048
+ return [ null, expr.path.length ];
1049
+ }
1050
+ const items = (env._leadingQuery || env)._combined?.[item.id];
1051
+ const allNavs = !items || Array.isArray(items) ? items : [ items ];
1052
+
1053
+ // If the annotation target itself has a table alias, require projections of that
1054
+ // table alias. Of course, that only works if we're talking about the same query.
1055
+ const tableAlias = (user._main?._origin === item._artifact?._main &&
1056
+ user.value?.path[0]?._navigation?.kind === '$tableAlias')
1057
+ ? user.value.path[0]._navigation : null;
1058
+
1059
+ // Look at all table aliase that could project `item` and only select
1060
+ // those that have actual projections.
1061
+ const navs = allNavs?.filter(p => p._origin === item._artifact &&
1062
+ (!tableAlias || tableAlias === p._parent));
1063
+ if (!navs || navs.length === 0)
1064
+ return [ null, expr.path.length ];
1065
+
1066
+ // If there are multiple navigations for the element, just use the first that matches.
1067
+ // In case of table aliases, it's just one.
1068
+ for (const nav of navs) {
1069
+ const r = firstProjectionForPath( expr.path, index, nav._parent, user );
1070
+ if (r.elem)
1071
+ return [ r.elem, r.index ];
1072
+ }
1073
+
1074
+ return [ null, expr.path.length ];
1075
+ }
815
1076
 
816
1077
  /**
817
1078
  * For a path `a.b.c.d`, return a projection for the first path item that is projected,
@@ -824,6 +1085,9 @@ function navProjection( navigation, preferred ) {
824
1085
  * The returned object `ret` has `ret.item`, which is the path item at index `ret.index`
825
1086
  * that is projected. `ret.elem` is the element projection.
826
1087
  *
1088
+ * If nothing was found, `ret.elem` is null, and `ret.item` is the last segment for which
1089
+ * there was a $navElement.
1090
+ *
827
1091
  * @param {any[]} path
828
1092
  * @param {number} startIndex
829
1093
  * @param {object} nav
@@ -844,32 +1108,72 @@ function firstProjectionForPath( path, startIndex, nav, elem ) {
844
1108
 
845
1109
  let proj = null;
846
1110
  let navItem = nav;
847
- for (let i = startIndex; i < path.length; ++i) {
848
- const item = path[i];
1111
+ let navIndex = startIndex;
1112
+ for (; navIndex < path.length; ++navIndex) {
1113
+ const item = path[navIndex];
849
1114
  navItem = item?.id && navItem.elements?.[item.id];
850
1115
  if (!navItem) {
851
1116
  break;
852
1117
  }
853
- else if (navItem._projections) {
1118
+ else if (navItem._projections || navItem._complexProjections) {
854
1119
  const projElem = navProjection( navItem, elem );
855
1120
  if (projElem && projElem === elem) {
856
1121
  // in case the specified association is found, _always_ use it.
857
- return { index: i, item, elem };
1122
+ return { index: navIndex, item, elem };
858
1123
  }
859
1124
  else if (projElem) {
860
1125
  const queryIndex = selectedElements.indexOf(projElem);
861
1126
  if (!proj || queryIndex < proj.queryIndex) {
862
1127
  proj = {
863
- index: i, item, elem: projElem, queryIndex,
1128
+ index: navIndex, item, elem: projElem, queryIndex,
864
1129
  };
865
1130
  }
866
1131
  }
867
1132
  }
868
1133
  }
1134
+ if (proj)
1135
+ return proj;
869
1136
 
870
- return proj || { index: startIndex, item: path[startIndex], elem: null };
1137
+ const index = (navIndex - 1) <= startIndex ? startIndex : (navIndex - 1);
1138
+ return { index, item: path[index], elem: null };
871
1139
  }
872
1140
 
1141
+ /**
1142
+ * Follow the navigation along the given path to its N-1 path step, so
1143
+ * that the last step can be resolved against the returned navigation like
1144
+ * `returnValue.elements[last.id]`.
1145
+ *
1146
+ * @param {XSN.Path} path
1147
+ * @param {object} nav
1148
+ * @returns {object|null}
1149
+ */
1150
+ function followNavigationPath( path, nav ) {
1151
+ if (!nav.item || !path || path.length === 1)
1152
+ return nav.tableAlias;
1153
+
1154
+ const startIndex = path.indexOf(nav.item);
1155
+ if (startIndex === -1)
1156
+ return null;
1157
+
1158
+ // navigation is already at last path step
1159
+ if (startIndex === path.length - 1) {
1160
+ return nav.navigation?.kind === '$navElement'
1161
+ ? nav.navigation._parent
1162
+ : nav.tableAlias;
1163
+ }
1164
+
1165
+ let navItem = nav.navigation || nav.tableAlias;
1166
+ for (let i = startIndex + 1; i < path.length - 1; ++i) {
1167
+ const item = path[i];
1168
+ navItem = item?.id && navItem.elements?.[item.id];
1169
+ if (!navItem)
1170
+ return null;
1171
+ }
1172
+
1173
+ return navItem;
1174
+ }
1175
+
1176
+
873
1177
  /**
874
1178
  * Return condensed info about reference in select item
875
1179
  * - tableAlias.elem -> { navigation: navElem, item: path[1], tableAlias }