@sap/cds-compiler 5.7.4 → 5.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +60 -2
  2. package/bin/cdsse.js +13 -1
  3. package/doc/CHANGELOG_BETA.md +7 -0
  4. package/lib/api/options.js +2 -1
  5. package/lib/api/validate.js +9 -0
  6. package/lib/base/message-registry.js +55 -20
  7. package/lib/base/messages.js +5 -2
  8. package/lib/base/model.js +4 -1
  9. package/lib/checks/assocOutsideService.js +40 -0
  10. package/lib/checks/featureFlags.js +4 -1
  11. package/lib/checks/types.js +7 -4
  12. package/lib/checks/validator.js +3 -0
  13. package/lib/compiler/assert-consistency.js +11 -5
  14. package/lib/compiler/checks.js +79 -17
  15. package/lib/compiler/define.js +57 -3
  16. package/lib/compiler/extend.js +1 -2
  17. package/lib/compiler/generate.js +1 -1
  18. package/lib/compiler/populate.js +17 -6
  19. package/lib/compiler/propagator.js +1 -1
  20. package/lib/compiler/resolve.js +181 -150
  21. package/lib/compiler/shared.js +276 -22
  22. package/lib/compiler/tweak-assocs.js +15 -4
  23. package/lib/compiler/xpr-rewrite.js +76 -50
  24. package/lib/edm/annotations/edmJson.js +1 -1
  25. package/lib/edm/annotations/genericTranslation.js +2 -2
  26. package/lib/edm/csn2edm.js +2 -2
  27. package/lib/edm/edmPreprocessor.js +15 -9
  28. package/lib/edm/edmUtils.js +12 -5
  29. package/lib/gen/CdlGrammar.checksum +1 -0
  30. package/lib/gen/CdlParser.js +2234 -2233
  31. package/lib/gen/Dictionary.json +55 -8
  32. package/lib/json/from-csn.js +37 -17
  33. package/lib/json/to-csn.js +4 -0
  34. package/lib/language/genericAntlrParser.js +7 -0
  35. package/lib/main.d.ts +5 -0
  36. package/lib/model/cloneCsn.js +1 -0
  37. package/lib/model/csnRefs.js +1 -0
  38. package/lib/model/csnUtils.js +0 -5
  39. package/lib/modelCompare/utils/filter.js +2 -2
  40. package/lib/optionProcessor.js +2 -0
  41. package/lib/parsers/AstBuildingParser.js +47 -17
  42. package/lib/parsers/CdlGrammar.g4 +10 -12
  43. package/lib/parsers/XprTree.js +206 -0
  44. package/lib/render/toCdl.js +61 -89
  45. package/lib/render/toSql.js +59 -29
  46. package/lib/render/utils/standardDatabaseFunctions.js +252 -15
  47. package/lib/transform/addTenantFields.js +9 -3
  48. package/lib/transform/db/assocsToQueries/transformExists.js +3 -0
  49. package/lib/transform/db/assocsToQueries/utils.js +10 -3
  50. package/lib/transform/db/expansion.js +3 -1
  51. package/lib/transform/db/flattening.js +7 -3
  52. package/lib/transform/db/killAnnotations.js +1 -0
  53. package/lib/transform/db/processSqlServices.js +70 -17
  54. package/lib/transform/draft/db.js +8 -3
  55. package/lib/transform/draft/odata.js +27 -4
  56. package/lib/transform/effective/main.js +37 -10
  57. package/lib/transform/effective/misc.js +4 -9
  58. package/lib/transform/effective/service.js +34 -0
  59. package/lib/transform/effective/types.js +28 -17
  60. package/lib/transform/forOdata.js +36 -10
  61. package/lib/transform/forRelationalDB.js +30 -18
  62. package/lib/transform/odata/adaptAnnotationRefs.js +37 -21
  63. package/lib/transform/odata/createForeignKeys.js +121 -117
  64. package/lib/transform/odata/flattening.js +12 -9
  65. package/lib/transform/transformUtils.js +58 -25
  66. package/lib/transform/translateAssocsToJoins.js +10 -6
  67. package/lib/transform/universalCsn/coreComputed.js +5 -1
  68. package/package.json +1 -1
  69. package/share/messages/message-explanations.json +1 -0
  70. package/share/messages/rewrite-not-supported.md +5 -0
  71. package/share/messages/rewrite-undefined-key.md +94 -0
@@ -319,6 +319,7 @@ function fns( model ) {
319
319
 
320
320
  Object.assign( model.$functions, {
321
321
  traverseExpr,
322
+ traverseTypedExpr,
322
323
  resolveUncheckedPath,
323
324
  resolveTypeArgumentsUnchecked, // TODO: move to some other file
324
325
  resolvePathRoot,
@@ -330,28 +331,263 @@ function fns( model ) {
330
331
  nestedElements,
331
332
  attachAndEmitValidNames,
332
333
  } );
334
+ traverseExpr.STOP = Symbol( 'STOP' );
335
+ traverseExpr.SKIP = Symbol( 'SKIP' );
336
+ traverseTypedExpr.STOP = traverseExpr.STOP;
337
+ traverseTypedExpr.SKIP = traverseExpr.SKIP;
333
338
  return;
334
339
 
335
340
  // Expression traversal function ----------------------------------------------
341
+
342
+ /**
343
+ * Recursively traverse the expression `expr` and call `callback` on the expression nodes.
344
+ *
345
+ * …
346
+ *
347
+ * Sub queries are not further traversed, but `callback` is called on the
348
+ * expression node having the property `query`.
349
+ *
350
+ * Callbacks can influence the traversal by returning a symbol:
351
+ *
352
+ * - `traverseExpr.STOP`: the traversal is stopped immediately
353
+ * - `traverseExpr.SKIP` on a node with a `path` property: the path items
354
+ * with its filters and arguments are not traversed
355
+ * - `traverseExpr.SKIP` on a path item: the expression in the `where`
356
+ * condition is not traversed
357
+ */
336
358
  function traverseExpr( expr, exprCtx, user, callback ) {
337
359
  if (!expr || typeof expr === 'string') // parse error or keywords in {xpr:...}
338
360
  return null;
339
361
 
340
- if (expr.path) {
341
- // TODO: move arguments and filter traversal to here
342
- return callback( expr, exprCtx, user );
362
+ let exit = null;
363
+ // `type` property for `cast, `query` for sub query
364
+ if (expr.path || expr.type || expr.query) {
365
+ exit = callback( expr, exprCtx, user );
366
+ if (exit === traverseExpr.STOP)
367
+ return exit;
343
368
  }
344
- let first = (expr.type || expr.query) && callback( expr, exprCtx, user );
345
-
346
- if (expr.args && !first) {
369
+ if (expr.path && exit !== traverseExpr.SKIP) {
370
+ for (const step of expr.path) {
371
+ if (step && (step.args || step.where || step.cardinality) &&
372
+ traversePathItem( step, exprCtx, user, callback ))
373
+ return traverseExpr.STOP;
374
+ }
375
+ }
376
+ if (expr.args) {
347
377
  const args = Array.isArray( expr.args ) ? expr.args : Object.values( expr.args );
348
- // TODO: re-think $expected
349
- first = args.find( e => traverseExpr( e, exprCtx, user, callback ) );
378
+ for (const arg of args ) {
379
+ if (traverseExpr( arg, exprCtx, user, callback ) === traverseExpr.STOP)
380
+ return traverseExpr.STOP;
381
+ }
382
+ }
383
+ if (expr.suffix) {
384
+ for (const arg of expr.suffix) {
385
+ if (traverseExpr( arg, exprCtx, user, callback ) === traverseExpr.STOP)
386
+ return traverseExpr.STOP;
387
+ }
388
+ }
389
+ return false;
390
+ }
391
+
392
+ function traversePathItem( step, exprCtx, user, callback ) {
393
+ const exit = callback( step, exprCtx, user );
394
+ if (exit === traverseExpr.STOP)
395
+ return true;
396
+ if (step.where && exit !== traverseExpr.SKIP &&
397
+ traverseExpr( step.where,
398
+ // TODO: use property in fn dictionary above
399
+ ( exprCtx === 'calc' || exprCtx === 'calc-filter'
400
+ ? 'calc-filter'
401
+ : 'filter' ),
402
+ step, callback ) === traverseExpr.STOP)
403
+ return true;
404
+ if (step.args) {
405
+ const ctx = (exprCtx === 'from') ? 'from-args' : exprCtx;
406
+ const args = Array.isArray( step.args ) ? step.args : Object.values( step.args );
407
+ // TODO: there should be no array `args` on path item
408
+ for (const arg of args) {
409
+ if (traverseExpr( arg, ctx, user, callback ) === traverseExpr.STOP)
410
+ return true;
411
+ }
412
+ }
413
+ return false;
414
+ }
415
+
416
+ // Special expression traversal function for `resolveExpr`. Let's see
417
+ // later whether we can use this version as the general one.
418
+ // If we continue to have separate ones, remove the STOP stuff – it is not
419
+ // needed for `resolveExpr`.
420
+
421
+ function traverseTypedExpr( expr, exprCtx, user, type, callback ) {
422
+ if (!expr || typeof expr === 'string') // parse error or keywords in {xpr:...}
423
+ return null;
424
+
425
+ let { args } = expr;
426
+ let exit = null;
427
+ // `type` property for `cast, `query` for sub query
428
+ if (expr.path || expr.type || expr.sym || expr.query) {
429
+ exit = callback( expr, exprCtx, user, type );
430
+ if (exit === traverseExpr.STOP)
431
+ return exit;
432
+ // `args` with `cast` function
433
+ }
434
+ else if (!args) {
435
+ // empty on purpose
436
+ }
437
+ else if (expr.func) {
438
+ if (!Array.isArray( args ))
439
+ args = Object.values( args );
440
+ }
441
+ else if (expr.op?.val === 'list' || args.length === 1) {
442
+ exit = type;
443
+ }
444
+ else if (expr.op?.val === '?:') {
445
+ args = traverseChoiceArgs( args, exprCtx, user, type, callback );
446
+ exit = type;
447
+ }
448
+ else {
449
+ args = traverseSpecialArgs( args, exprCtx, user, type, callback );
450
+ }
451
+
452
+ if (expr.path && exit !== traverseExpr.SKIP) {
453
+ for (const step of expr.path) {
454
+ if (step && (step.args || step.where || step.cardinality) &&
455
+ traverseTypedPathItem( step, exprCtx, user, callback ))
456
+ return traverseExpr.STOP;
457
+ }
458
+ }
459
+ if (expr.args) {
460
+ if (!args)
461
+ return traverseExpr.STOP;
462
+ for (const arg of args) {
463
+ if (traverseTypedExpr( arg, exprCtx, user, exit, callback ) === traverseExpr.STOP)
464
+ return traverseExpr.STOP;
465
+ }
350
466
  }
467
+ if (expr.suffix) {
468
+ for (const arg of expr.suffix) {
469
+ if (traverseTypedExpr( arg, exprCtx, user, null, callback ) === traverseExpr.STOP)
470
+ return traverseExpr.STOP;
471
+ }
472
+ }
473
+ return exit;
474
+ }
475
+
476
+ /**
477
+ * Traverse arguments `args` if they match a specific pattern:
478
+ *
479
+ * - a (sub) expression is a comparison, i.e. uses one of the binary operators
480
+ * `=`, `<>`, `!=`, `in` or `not in`,
481
+ * - one side of the comparison is a reference or a `cast` function call when
482
+ * typed with an enum type,
483
+ * - the other side is a enum reference, an enum reference in parentheses, or a
484
+ * list of enum references.
485
+ *
486
+ * Return an array of the arguments which are to be traversed normally, or
487
+ * `null` if the traversal is stopped immediately
488
+ */
489
+ function traverseSpecialArgs( args, exprCtx, user, type, callback ) {
490
+ if (args.length <= 3) {
491
+ if (args.length === 3 && args[1].literal === 'token' &&
492
+ [ '=', '<>', '!=', 'in' ].includes( args[1].val ))
493
+ return traverseComparison( args[0], args[2], exprCtx, user, callback );
494
+ }
495
+ else if (args[0].val === 'case' && args[0].literal === 'token') {
496
+ return traverseCaseWhen( args, exprCtx, user, type, callback );
497
+ }
498
+ else if (args.length === 4 && args[1].val === 'not' && args[2].val === 'in' &&
499
+ args[1].literal === 'token' && args[2].literal === 'token') {
500
+ return traverseComparison( args[0], args[3], exprCtx, user, callback );
501
+ }
502
+ return args;
503
+ }
504
+
505
+ function traverseComparison( left, right, exprCtx, user, callback ) {
506
+ if (!left || !right) // can happen in old parser
507
+ return [ left || right ];
508
+ if (left.path || left.type) { // ref or cast fn
509
+ const type = traverseTypedExpr( left, exprCtx, user, null, callback );
510
+ if (type === traverseExpr.STOP ||
511
+ traverseTypedExpr( right, exprCtx, user, type, callback ) === traverseExpr.STOP)
512
+ return null;
513
+ return [];
514
+ }
515
+ if (right.path || right.type) { // ref or cast fn
516
+ const type = traverseTypedExpr( right, exprCtx, user, null, callback );
517
+ if (type === traverseExpr.STOP ||
518
+ traverseTypedExpr( left, exprCtx, user, type, callback ) === traverseExpr.STOP)
519
+ return null;
520
+ return [];
521
+ }
522
+ return [ left, right ];
523
+ }
524
+
525
+ // for '?:' operator, only via CDL (translates to `case…when` in CSN):
526
+ function traverseChoiceArgs( args, exprCtx, user, type, callback ) {
527
+ if (traverseTypedExpr( args[0], exprCtx, user, null, callback ) === traverseExpr.STOP)
528
+ return null;
529
+ return args.slice( 1 );
530
+ // TODO: adopt if we extend this to ?:?:…
531
+ }
532
+
533
+ function traverseCaseWhen( args, exprCtx, user, type, callback ) {
534
+ let idx = 1;
535
+ let when = null;
536
+ let node = args[1];
537
+ // For `CASE <expr> WHEN <…> THEN <…>`
538
+ if (node?.val !== 'when' || node.literal !== 'token') {
539
+ when = traverseTypedExpr( node, exprCtx, user, null, callback );
540
+ if (when === traverseExpr.STOP)
541
+ return null;
542
+ ++idx;
543
+ }
544
+ // Remark: no need to test `literal` in the following - ensured by CDL and CSN
545
+ // parser
546
+ while (args[idx]?.val === 'when' && ++idx < args.length) {
547
+ node = args[idx];
548
+ // be robust against corrupted sources:
549
+ if ((node.literal !== 'token' || ![ 'then', 'when', 'end' ].includes( node.val )) &&
550
+ traverseTypedExpr( args[idx++], exprCtx, user, when, callback ) === traverseExpr.STOP)
551
+ return null;
351
552
 
352
- first ??= expr.suffix && // fn( ) OVER …
353
- expr.suffix.find( e => traverseExpr( e, exprCtx, user, callback ) );
354
- return first;
553
+ if (args[idx]?.val !== 'then')
554
+ continue;
555
+ node = args[++idx];
556
+ if (node &&
557
+ (node.literal !== 'token' || node.val !== 'when' && node.val !== 'end') &&
558
+ traverseTypedExpr( args[idx++], exprCtx, user, type, callback ) === traverseExpr.STOP)
559
+ return null;
560
+ }
561
+ if (args[idx]?.val === 'else') {
562
+ if (++idx < args.length &&
563
+ traverseTypedExpr( args[idx], exprCtx, user, type, callback ) === traverseExpr.STOP)
564
+ return null;
565
+ }
566
+ return [];
567
+ }
568
+
569
+ function traverseTypedPathItem( step, exprCtx, user, callback ) {
570
+ const exit = callback( step, exprCtx, user, null );
571
+ if (exit === traverseExpr.STOP)
572
+ return true;
573
+ if (step.where && exit !== traverseExpr.SKIP &&
574
+ traverseTypedExpr( step.where,
575
+ // TODO: use property in fn dictionary above
576
+ ( exprCtx === 'calc' || exprCtx === 'calc-filter'
577
+ ? 'calc-filter'
578
+ : 'filter' ),
579
+ step, null, callback ) === traverseExpr.STOP)
580
+ return true;
581
+ if (step.args) {
582
+ const ctx = (exprCtx === 'from') ? 'from-args' : exprCtx;
583
+ const args = Array.isArray( step.args ) ? step.args : Object.values( step.args );
584
+ // TODO: there should be no array `args` on path item
585
+ for (const arg of args) {
586
+ if (traverseTypedExpr( arg, ctx, user, arg.name, callback ) === traverseExpr.STOP)
587
+ return true;
588
+ }
589
+ }
590
+ return false;
355
591
  }
356
592
 
357
593
  // Return absolute name for unchecked path `ref`. We first try searching for
@@ -740,7 +976,8 @@ function fns( model ) {
740
976
  case '$tableAlias': {
741
977
  // use a source element having that name if in `extend … with columns`:
742
978
  const { $extended } = user._user || user;
743
- const elem = $extended && art.elements[head.id];
979
+ // if query source has duplicates, table alias has no elements
980
+ const elem = $extended && art.elements?.[head.id];
744
981
  if (elem) {
745
982
  path.$prefix = art.name.id; // prepend alias name
746
983
  info( 'ref-special-in-extend', [ head.location, user ],
@@ -748,7 +985,7 @@ function fns( model ) {
748
985
  setLink( head, '_navigation', elem );
749
986
  return setArtifactLink( head, elem._origin );
750
987
  }
751
- else if ($extended) {
988
+ else if ($extended && art.elements) {
752
989
  warning( 'ref-deprecated-in-extend', [ head.location, user ], { id: head.id },
753
990
  // eslint-disable-next-line @stylistic/js/max-len
754
991
  'In an added column, do not use the table alias $(ID) to refer to source elements' );
@@ -783,7 +1020,7 @@ function fns( model ) {
783
1020
  }
784
1021
  case 'builtin': {
785
1022
  if (art.name.id === '$at') {
786
- warning( 'ref-deprecated-variable', [ head.location, user ],
1023
+ message( 'ref-deprecated-variable', [ head.location, user ],
787
1024
  { code: '$at', newcode: '$valid' },
788
1025
  '$(CODE) is deprecated; use $(NEWCODE) instead' );
789
1026
  }
@@ -1394,21 +1631,30 @@ function fns( model ) {
1394
1631
  const { path, scope } = ref;
1395
1632
  // see getPathItem(): how many path items are for the main artifact ref?
1396
1633
  const artItemsCount = (typeof scope === 'number' && scope) || (scope ? 1 : path.length);
1634
+
1397
1635
  // at least the last main definition should be an entity or an
1398
- // event (if the user is also an event)
1636
+ // event (if the user is an event) or type (if the user is a type)
1399
1637
  // an additional check for target would need effectiveType()
1400
1638
  const source = path[artItemsCount - 1]._artifact;
1401
- const variant = (user._main.kind === 'event') ? 'event' : 'std';
1402
- if (source.kind !== 'entity' && !acceptEventProjectionSource( source, user )) {
1403
- signalNotFound( 'ref-invalid-source', [ ref.location, user ], null, { '#': variant } );
1404
- return false;
1639
+ if (user._main?.kind === 'type') {
1640
+ if (!acceptTypeProjectionSource( source )) {
1641
+ signalNotFound( 'ref-invalid-source', [ ref.location, user ], null,
1642
+ { '#': 'type' } );
1643
+ return (source === art) ? art : false; // art to show cyclic issues
1644
+ }
1645
+ }
1646
+ else if (source.kind !== 'entity' &&
1647
+ !acceptEventProjectionSource( source, user )) {
1648
+ signalNotFound( 'ref-invalid-source', [ ref.location, user ], null,
1649
+ { '#': user._main.kind } );
1650
+ return (source === art) ? art : false; // art to show cyclic issues
1405
1651
  }
1406
1652
  if (source === art)
1407
1653
  return art;
1408
1654
  const assoc = Functions.effectiveType( art );
1409
1655
  if (assoc.target)
1410
1656
  return art; // TODO: use target here
1411
- signalNotFound( 'ref-invalid-source', [ ref.location, user ], null, { '#': variant } );
1657
+ signalNotFound( 'ref-invalid-source', [ ref.location, user ], null, { '#': user._main.kind } );
1412
1658
  return false;
1413
1659
  }
1414
1660
 
@@ -1422,6 +1668,12 @@ function fns( model ) {
1422
1668
  return (kind === 'entity' || kind === 'event' || (kind === 'type' && effectiveType.elements));
1423
1669
  }
1424
1670
 
1671
+ function acceptTypeProjectionSource( source ) {
1672
+ // We require the projection source to be structured.
1673
+ // TODO: Also allow all associations?
1674
+ return Functions.effectiveType( source )?.elements;
1675
+ }
1676
+
1425
1677
  function acceptTypeOrElement( art, user, ref ) { // for type
1426
1678
  // was ['action', 'function'].includes( user._parent?.kind ))
1427
1679
  while (user._outer)
@@ -1475,8 +1727,10 @@ function fns( model ) {
1475
1727
  const semantics = (typeof s === 'string') ? referenceSemantics[s] : s;
1476
1728
  const checkFn = semantics.check; // || !semantics.isMainRef && checkElementStd;
1477
1729
 
1478
- if (checkFn)
1479
- traverseExpr( expr, exprCtx, user, checkFn );
1730
+ if (checkFn) {
1731
+ traverseExpr( expr, exprCtx, user,
1732
+ ( ...args ) => (checkFn( ...args ) ? traverseExpr.STOP : traverseExpr.SKIP) );
1733
+ }
1480
1734
  }
1481
1735
 
1482
1736
  // TODO: Don't allow path args and filter!
@@ -43,6 +43,7 @@ function tweakAssocs( model ) {
43
43
  mergeSpecifiedForeignKeys,
44
44
  navigationEnv,
45
45
  redirectionChain,
46
+ resolveExprInAnnotations,
46
47
  } = model.$functions;
47
48
 
48
49
  Object.assign(model.$functions, {
@@ -245,8 +246,13 @@ function tweakAssocs( model ) {
245
246
  doRewriteAssociation( element );
246
247
  if (element.target) {
247
248
  extendForeignKeys( element );
248
- if (element.foreignKeys$)
249
+ if (element.foreignKeys$) {
250
+ // TODO: Also checkSpecifiedElement?
249
251
  mergeSpecifiedForeignKeys( element );
252
+ }
253
+ for (const key in element.foreignKeys)
254
+ // TODO: This will re-evaluate all annotations
255
+ resolveExprInAnnotations( element.foreignKeys[key] );
250
256
  }
251
257
  }
252
258
 
@@ -435,7 +441,10 @@ function tweakAssocs( model ) {
435
441
  if (!nav.tableAlias || nav.tableAlias.path) {
436
442
  const navEnv = followNavigationPath( elem.value?.path, nav ) || nav.tableAlias;
437
443
  traverseExpr( elem.on, 'rewrite-on', elem,
438
- expr => rewriteExpr( expr, elem, nav.tableAlias, navEnv ) );
444
+ ( expr ) => {
445
+ rewriteExpr( expr, elem, nav.tableAlias, navEnv );
446
+ return traverseExpr.SKIP; // TODO: really necessary?
447
+ } );
439
448
  }
440
449
  else if (elem._columnParent) {
441
450
  error( 'rewrite-not-supported', [ elem.target.location, elem ], { '#': 'inline-expand' } );
@@ -461,6 +470,7 @@ function tweakAssocs( model ) {
461
470
  function removeArtifactLinks() {
462
471
  traverseExpr( elem.on, 'rewrite-on', elem, (expr) => {
463
472
  setArtifactLink( expr, null );
473
+ return traverseExpr.SKIP; // TODO: necessary?
464
474
  } );
465
475
  }
466
476
  }
@@ -556,11 +566,11 @@ function tweakAssocs( model ) {
556
566
  const navEnv = nav && followNavigationPath( elem.value?.path, nav ) || nav?.tableAlias;
557
567
  traverseExpr( cond, 'rewrite-filter', elem, (expr) => {
558
568
  if (!expr.path || expr.path.length === 0)
559
- return;
569
+ return traverseExpr.SKIP;
560
570
 
561
571
  const root = expr.path[0]._navigation || expr.path[0]._artifact;
562
572
  if (!root)
563
- return; // only for compile error, e.g. missing definition
573
+ return traverseExpr.SKIP; // only for compile error, e.g. missing definition
564
574
  if (root.kind === '$self') {
565
575
  // $projection -> $self for recompilability
566
576
  expr.path[0].id = '$self';
@@ -579,6 +589,7 @@ function tweakAssocs( model ) {
579
589
  // up to here, filter is relative to original association
580
590
  rewriteExpr( expr, elem, nav?.tableAlias, navEnv );
581
591
  }
592
+ return traverseExpr.SKIP;
582
593
  } );
583
594
 
584
595
  checkOnCondition( cond, 'on', elem );
@@ -160,10 +160,10 @@ class AnnoRewriteConfig {
160
160
  target;
161
161
  targetRoot;
162
162
  origin;
163
- isViaType;
164
- isViaCalcElement;
165
- viaExpand;
166
- viaExpandType;
163
+ fromTargetType;
164
+ fromCalcElement;
165
+ expandedRoot;
166
+ expandedRootType;
167
167
  isInFilter;
168
168
  tokenExpr;
169
169
  }
@@ -217,21 +217,26 @@ function xprRewriteFns( model ) {
217
217
  if (!anno.kind || anno.$invalidPaths)
218
218
  return;
219
219
 
220
- const isViaType = target.type?._artifact === origin;
221
- const [ viaExpand, viaExpandType ] = !isViaType && getExpandRoot( target ) || [ null, null ];
222
- const isViaCalcElement = !isViaType && target.$calcDepElement &&
220
+ // Annotation comes from the target's type. That's important to know, because
221
+ // path prefixes need to be adapted.
222
+ const fromTargetType = target.type?._artifact === origin;
223
+ // Annotation comes from the target's calculated element. A special case propagation rule, e.g
224
+ // for `calcString: String = str;`. We also need to adapt path prefixes.
225
+ const fromCalcElement = !fromTargetType && target.$calcDepElement &&
223
226
  target.value?._artifact === origin;
224
227
 
228
+ const { expandedRoot, expandedRootType } = !fromTargetType && getExpandRoot( target ) || {};
229
+
225
230
  const config = {
226
231
  __proto__: AnnoRewriteConfig.prototype,
227
232
  anno: annoName,
228
233
  target,
229
234
  targetRoot: annoRootArt( target ),
230
235
  origin,
231
- isViaType,
232
- isViaCalcElement,
233
- viaExpand,
234
- viaExpandType,
236
+ fromTargetType,
237
+ fromCalcElement,
238
+ expandedRoot,
239
+ expandedRootType,
235
240
  };
236
241
 
237
242
  const hasError = rewriteAnnotationExpr( target[annoName], config );
@@ -259,10 +264,10 @@ function xprRewriteFns( model ) {
259
264
  else if (expr.$tokenTexts) {
260
265
  // used to set `$tokenText` to true in case of rewritten annotation
261
266
  config.tokenExpr = expr;
262
- return !!traverseExpr(
267
+ return traverseExpr.STOP === traverseExpr(
263
268
  expr, 'annoRewrite', config.target,
264
- (e, refCtx) => rewriteAnnoExpr( e, config, refCtx )
265
- );
269
+ // eslint-disable-next-line @stylistic/js/max-len, @stylistic/js/function-paren-newline
270
+ (e, refCtx) => (rewriteAnnoExpr( e, config, refCtx ) ? traverseExpr.STOP : traverseExpr.SKIP) );
266
271
  }
267
272
  return false;
268
273
  }
@@ -285,15 +290,22 @@ function xprRewriteFns( model ) {
285
290
  root._parent.kind !== 'function'))
286
291
  return reportAnnoRewriteError( expr, config, 'unsupported' );
287
292
 
293
+ if (root.kind === 'key') {
294
+ // Foreign keys can't be renamed and since we don't have absolute references to foreign keys,
295
+ // i.e. `$self.assoc.target_id` always refers to the target side, we don't have to rewrite
296
+ // them.
297
+ return null;
298
+ }
299
+
288
300
  // magic variables / replacement variables are never rewritten; they can't
289
301
  // have filters nor can they point to elements.
290
302
  if (expr._artifact?.kind === 'builtin')
291
303
  return null;
292
304
 
293
305
  let hasError = false;
294
- if (config.isViaType || config.isViaCalcElement)
306
+ if (config.fromTargetType || config.fromCalcElement)
295
307
  hasError = adaptPathPrefixViaType( expr, config );
296
- else if (config.viaExpand)
308
+ else if (config.expandedRoot)
297
309
  hasError = adaptPathPrefixViaTypeExpansion( expr, config );
298
310
 
299
311
  hasError ||= rewriteGenericAnnoPath( expr, config, refCtx );
@@ -309,8 +321,11 @@ function xprRewriteFns( model ) {
309
321
  const assocTarget = step._artifact.target._artifact;
310
322
  if (target) {
311
323
  const filterConfig = { ...config, target: assocTarget, isInFilter: true };
312
- if (traverseExpr( step.where, 'filter', step,
313
- (e, ctx) => expr.path && rewriteGenericAnnoPath( e, filterConfig, ctx )))
324
+ if (traverseExpr.STOP === traverseExpr(
325
+ step.where, 'filter', step,
326
+ // eslint-disable-next-line @stylistic/js/max-len
327
+ (e, ctx) => expr.path && (rewriteGenericAnnoPath( e, filterConfig, ctx ) ? traverseExpr.STOP : traverseExpr.SKIP)
328
+ ))
314
329
  return true;
315
330
  }
316
331
  else {
@@ -490,7 +505,7 @@ function xprRewriteFns( model ) {
490
505
  config.tokenExpr.$tokenTexts = true;
491
506
 
492
507
  const wasAbsolute = isAnnoPathAbsolute( expr );
493
- stripPrefixToNewRoot( expr, target, origin );
508
+ stripAbsolutePathPrefix( expr, origin );
494
509
 
495
510
  if (wasAbsolute) {
496
511
  prependRootPath( origin, target, expr );
@@ -519,28 +534,28 @@ function xprRewriteFns( model ) {
519
534
  return false;
520
535
  }
521
536
 
522
- // $self-paths via type expansion always need to be rewritten.
523
- config.tokenExpr.$tokenTexts = true;
524
-
525
- const { target } = config;
526
537
  // We reject $self-paths because they need to be rewritten.
527
538
  // However, with a special flag, we allow rewriting it for testing purposes.
528
539
  if (!isBetaEnabled( model.options, 'rewriteAnnotationExpressionsViaType' ))
529
540
  return reportAnnoRewriteError( expr, config, 'unsupported' );
530
541
 
531
- if (rejectOuterReference( expr, config.viaExpandType, config ))
542
+ if (rejectOuterReference( expr, config.expandedRootType, config ))
532
543
  return true;
533
544
 
534
- stripPrefixToNewRoot( expr, target, config.viaExpandType );
535
- prependRootPath( config.viaExpandType, config.viaExpand, expr );
536
- setExpandStatusAnnotate( target, 'annotate' );
545
+ stripAbsolutePathPrefix( expr, config.expandedRootType );
546
+ prependRootPath( config.expandedRootType, config.expandedRoot, expr );
547
+ setExpandStatusAnnotate( config.target, 'annotate' );
548
+
537
549
  config.target[config.anno].$inferred = 'anno-rewrite';
550
+ // $self-paths via type expansion always need to be rewritten.
551
+ config.tokenExpr.$tokenTexts = true;
552
+
538
553
  return false;
539
554
  }
540
555
 
541
556
  /**
542
557
  * Prepend a path to `expr.path` or replace the root item.
543
- * The path needs to have been run through exprstripPrefixToNewRoot(…)`.
558
+ * The path needs to have been run through stripPrefixToNewRoot(…)`.
544
559
  * It is prepended if the root item is not the origin.
545
560
  * Replaced otherwise.
546
561
  *
@@ -559,15 +574,14 @@ function xprRewriteFns( model ) {
559
574
 
560
575
  /**
561
576
  * Strips a prefix path from `expr.path`. The prefix is defined
562
- * by where `newRootArt` appears in the path.
577
+ * by where `art` appears in the path.
563
578
  *
564
579
  * @param {XSN.Expression} expr
565
- * @param {XSN.Artifact} target
566
- * @param {XSN.Artifact} newRootArt
580
+ * @param {XSN.Artifact} art
567
581
  */
568
- function stripPrefixToNewRoot( expr, target, newRootArt ) {
569
- const relativeRoot = findRelativeRoot( expr, newRootArt );
570
- if (relativeRoot === -1 && isAnnoRootArt( newRootArt ))
582
+ function stripAbsolutePathPrefix( expr, art ) {
583
+ const relativeRoot = findRelativeRoot( expr, art );
584
+ if (relativeRoot === -1 && isAnnoRootArt( art ))
571
585
  return; // no $self; root item is element
572
586
  if (relativeRoot >= 1)
573
587
  expr.path = expr.path.slice(relativeRoot);
@@ -614,17 +628,17 @@ function xprRewriteFns( model ) {
614
628
  * Returns -1 if `origin` isn't found in the path.
615
629
  *
616
630
  * @param {XSN.Expression} expr
617
- * @param {XSN.Artifact} newRootArt
631
+ * @param {XSN.Artifact} origin
618
632
  * @returns {number}
619
633
  */
620
- function findRelativeRoot( expr, newRootArt ) {
621
- if (!newRootArt._main) // main artifacts can't have outer references
622
- return expr.path[0]?._artifact === newRootArt ? 0 : -1;
634
+ function findRelativeRoot( expr, origin ) {
635
+ if (!origin._main) // main artifacts can't have outer references
636
+ return expr.path[0]?._artifact === origin ? 0 : -1;
623
637
 
624
638
  const { path } = expr;
625
639
  for (let i = 0; i < path.length; ++i) {
626
640
  const item = path[i];
627
- if (item._artifact === newRootArt)
641
+ if (item._artifact === origin)
628
642
  return i;
629
643
  }
630
644
  return -1;
@@ -738,21 +752,33 @@ function isReturnParam( art ) {
738
752
  }
739
753
 
740
754
  /**
741
- * Returns the target's parent which is expanded and the type from which the elements originate.
755
+ * Gets the artifact (e.g. element) that was expanded. `target` is a sub-artifact of that root and
756
+ * is an expanded element.
757
+ *
758
+ * - expandedRoot: Top-most structure that was expanded.
759
+ * - expandedRootType: The type of expandedRoot.
742
760
  *
743
761
  * @param {XSN.Artifact} target
744
- * @returns {[XSN.Artifact, XSN.Artifact]}
762
+ * @returns { {expandedRoot: XSN.Artifact, expandedRootType: XSN.Artifact}}
745
763
  */
746
764
  function getExpandRoot( target ) {
747
- let viaExpand = target;
748
- while (viaExpand.$inferred === 'expanded')
749
- viaExpand = viaExpand._parent;
750
- const viaExpandType = viaExpand
751
- ? (viaExpand.type?._artifact || viaExpand.items?.type?._artifact)
752
- : null;
753
- const viaInclude = viaExpand?.$inferred === 'include' || false;
754
- viaExpand = !viaInclude && viaExpandType ? viaExpand : false;
755
- return [ viaExpand, viaExpandType ];
765
+ if (target.$inferred !== 'expanded' && target.$inferred !== 'rewrite')
766
+ return { expandedRoot: null, expandedRootType: null };
767
+
768
+ let expandedRoot = target;
769
+
770
+ // 'expanded' for structures, 'rewrite' for foreign keys
771
+ while (expandedRoot.$inferred === 'expanded' || expandedRoot.$inferred === 'rewrite')
772
+ expandedRoot = expandedRoot._parent;
773
+
774
+ // `items` may be inferred via a type, hence why we check `items.type` after `type`
775
+ let expandedRootType = expandedRoot?.type || expandedRoot?.items?.type;
776
+ expandedRootType = (!expandedRootType?.$inferred && expandedRootType?._artifact) || null;
777
+
778
+ const viaInclude = expandedRoot?.$inferred === 'include';
779
+ expandedRoot = !viaInclude && expandedRootType ? expandedRoot : false;
780
+
781
+ return { expandedRoot, expandedRootType };
756
782
  }
757
783
 
758
784
  module.exports = {