@sap/cds-compiler 6.1.0 → 6.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 (90) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/bin/cdsc.js +17 -6
  3. package/bin/cdsse.js +1 -1
  4. package/bin/cdsv2m.js +1 -1
  5. package/lib/api/main.js +29 -7
  6. package/lib/api/options.js +1 -1
  7. package/lib/base/builtins.js +9 -0
  8. package/lib/base/keywords.js +1 -1
  9. package/lib/base/message-registry.js +41 -10
  10. package/lib/base/messages.js +13 -6
  11. package/lib/base/model.js +1 -1
  12. package/lib/base/optionProcessorHelper.js +7 -2
  13. package/lib/checks/assocOutsideService.js +17 -30
  14. package/lib/checks/checkForTypes.js +0 -18
  15. package/lib/checks/checkPathsInStoredCalcElement.js +2 -1
  16. package/lib/checks/featureFlags.js +4 -1
  17. package/lib/checks/onConditions.js +2 -2
  18. package/lib/checks/queryNoDbArtifacts.js +16 -15
  19. package/lib/checks/types.js +1 -1
  20. package/lib/checks/utils.js +30 -6
  21. package/lib/checks/validator.js +4 -5
  22. package/lib/compiler/assert-consistency.js +3 -1
  23. package/lib/compiler/base.js +1 -1
  24. package/lib/compiler/builtins.js +1 -1
  25. package/lib/compiler/checks.js +85 -39
  26. package/lib/compiler/define.js +24 -5
  27. package/lib/compiler/extend.js +1 -1
  28. package/lib/compiler/finalize-parse-cdl.js +9 -1
  29. package/lib/compiler/generate.js +4 -4
  30. package/lib/compiler/index.js +88 -6
  31. package/lib/compiler/lsp-api.js +2 -0
  32. package/lib/compiler/populate.js +8 -8
  33. package/lib/compiler/propagator.js +1 -1
  34. package/lib/compiler/resolve.js +22 -21
  35. package/lib/compiler/shared.js +6 -6
  36. package/lib/compiler/tweak-assocs.js +53 -31
  37. package/lib/compiler/utils.js +9 -16
  38. package/lib/compiler/xpr-rewrite.js +2 -2
  39. package/lib/gen/BaseParser.js +35 -29
  40. package/lib/gen/CdlGrammar.checksum +1 -1
  41. package/lib/gen/CdlParser.js +1424 -1430
  42. package/lib/gen/Dictionary.json +1 -2
  43. package/lib/gen/cdlKeywords.json +26 -0
  44. package/lib/inspect/inspectPropagation.js +1 -1
  45. package/lib/json/from-csn.js +2 -2
  46. package/lib/json/to-csn.js +1 -1
  47. package/lib/language/multiLineStringParser.js +1 -1
  48. package/lib/model/cloneCsn.js +1 -0
  49. package/lib/model/csnRefs.js +9 -4
  50. package/lib/model/csnUtils.js +67 -2
  51. package/lib/optionProcessor.js +9 -9
  52. package/lib/parsers/AstBuildingParser.js +28 -26
  53. package/lib/parsers/identifiers.js +2 -30
  54. package/lib/render/toCdl.js +73 -13
  55. package/lib/render/toSql.js +127 -108
  56. package/lib/render/utils/common.js +4 -2
  57. package/lib/render/utils/sql.js +67 -0
  58. package/lib/transform/addTenantFields.js +4 -4
  59. package/lib/transform/db/assertUnique.js +2 -1
  60. package/lib/transform/db/associations.js +37 -1
  61. package/lib/transform/db/assocsToQueries/transformExists.js +21 -32
  62. package/lib/transform/db/assocsToQueries/utils.js +1 -1
  63. package/lib/transform/db/cdsPersistence.js +1 -1
  64. package/lib/transform/db/expansion.js +37 -36
  65. package/lib/transform/db/killAnnotations.js +1 -0
  66. package/lib/transform/db/processSqlServices.js +20 -2
  67. package/lib/transform/draft/db.js +20 -20
  68. package/lib/transform/draft/odata.js +38 -40
  69. package/lib/transform/effective/associations.js +1 -1
  70. package/lib/transform/effective/flattening.js +40 -47
  71. package/lib/transform/effective/main.js +6 -4
  72. package/lib/transform/forOdata.js +201 -92
  73. package/lib/transform/forRelationalDB.js +151 -142
  74. package/lib/transform/localized.js +116 -109
  75. package/lib/transform/odata/adaptAnnotationRefs.js +21 -16
  76. package/lib/transform/odata/createForeignKeys.js +73 -70
  77. package/lib/transform/odata/flattening.js +216 -200
  78. package/lib/transform/odata/foreignKeyRefsInXprAnnos.js +47 -45
  79. package/lib/transform/odata/toFinalBaseType.js +40 -39
  80. package/lib/transform/odata/typesExposure.js +151 -133
  81. package/lib/transform/odata/utils.js +7 -6
  82. package/lib/transform/parseExpr.js +165 -162
  83. package/lib/transform/transformUtils.js +184 -551
  84. package/lib/transform/translateAssocsToJoins.js +511 -596
  85. package/lib/transform/tupleExpansion.js +495 -0
  86. package/lib/transform/universalCsn/universalCsnEnricher.js +1 -0
  87. package/lib/utils/moduleResolve.js +1 -1
  88. package/package.json +2 -2
  89. package/lib/base/cleanSymbols.js +0 -17
  90. package/lib/checks/nonexpandableStructured.js +0 -39
@@ -183,7 +183,7 @@ function compileX( filenames, dir = '', options = {}, fileCache = Object.create(
183
183
  return all.then( () => {
184
184
  options.abortSignal?.throwIfAborted();
185
185
  moduleLayers.setLayers( input.sources );
186
- return compileDoX( model );
186
+ return compileDoX( model ); // also async
187
187
  } );
188
188
 
189
189
  // Read file `filename` and parse its content, return messages
@@ -303,7 +303,7 @@ function compileSyncX( filenames, dir = '', options = {}, fileCache = Object.cre
303
303
  }
304
304
 
305
305
  moduleLayers.setLayers( a.sources );
306
- return compileDoX( model );
306
+ return compileDoXSync( model );
307
307
 
308
308
  // Read file `filename` and parse its content, return messages
309
309
  function readAndParseSync( filename, cb ) {
@@ -423,7 +423,7 @@ function compileSourcesX( sourcesDict, options = {} ) {
423
423
  }
424
424
  moduleLayers.setLayers( sources );
425
425
 
426
- return compileDoX( model );
426
+ return compileDoXSync( model );
427
427
  }
428
428
 
429
429
  /**
@@ -457,8 +457,8 @@ function recompileX( csn, options ) {
457
457
 
458
458
  sources[file] = parseCsn.augment( csn, file, options, model.$messageFunctions );
459
459
  moduleLayers.setLayers( sources );
460
- const compiled = compileDoX( model ); // calls throwWithError()
461
- if (options.messages) // does not help with exception in compileDoX()
460
+ const compiled = compileDoXSync( model ); // calls throwWithError()
461
+ if (options.messages) // does not help with exception in compileDoXSync()
462
462
  deduplicateMessages( options.messages ); // TODO: do better
463
463
  return compiled;
464
464
  }
@@ -467,10 +467,12 @@ function recompileX( csn, options ) {
467
467
  * On the given model (AST like CSN) run the definer, resolver as well as semantic checks.
468
468
  * Creates an augmented CSN (XSN) and returns it.
469
469
  *
470
+ * This is the non-interruptible version of `compileDoX()` and can be used in non-`async` functions.
471
+ *
470
472
  * @param {object} model AST like CSN generated e.g. by `parsers.parseCdl()`
471
473
  * @returns {XSN.Model} Augmented CSN (XSN)
472
474
  */
473
- function compileDoX( model ) {
475
+ function compileDoXSync( model ) {
474
476
  const { options } = model;
475
477
  const { throwWithError } = model.$messageFunctions;
476
478
  if (!options.testMode)
@@ -512,6 +514,61 @@ function compileDoX( model ) {
512
514
  return propagator.propagate( model );
513
515
  }
514
516
 
517
+ /**
518
+ * On the given model (AST like CSN) run the definer, resolver as well as semantic checks.
519
+ * Creates an augmented CSN (XSN) and returns it.
520
+ *
521
+ * @param {object} model AST like CSN generated e.g. by `parsers.parseCdl()`
522
+ * @returns {XSN.Model} Augmented CSN (XSN)
523
+ */
524
+ async function compileDoX( model ) {
525
+ const { options } = model;
526
+ const { throwWithError } = model.$messageFunctions;
527
+ if (!options.testMode)
528
+ model.meta = {}; // provide initial central meta object
529
+
530
+ checkRemovedDeprecatedFlags( options, model.$messageFunctions );
531
+
532
+ if (options.parseOnly) {
533
+ throwWithError();
534
+ return model;
535
+ }
536
+ model.$functions = {};
537
+ fns( model ); // attach (mostly) paths functions
538
+ define( model );
539
+ await checkAsyncAbortFlag( options.abortSignal );
540
+
541
+ // do not run the resolver in parse-cdl mode or we get duplicate annotations, etc.
542
+ // TODO: do not use this function for parseCdl anyway…
543
+ if (options.parseCdl) {
544
+ finalizeParseCdl( model );
545
+ throwWithError();
546
+ return model;
547
+ }
548
+
549
+ for (const phase of [ extend, generate, kickStart, populate ]) {
550
+ phase( model );
551
+ // eslint-disable-next-line no-await-in-loop
552
+ await checkAsyncAbortFlag( options.abortSignal );
553
+ }
554
+
555
+ model.definitions = model.$functions.shuffleDict( model.definitions );
556
+ // Shuffling extensions is more difficult due to intra-file extensions of same artifact
557
+ // TODO: think about making this work
558
+
559
+ for (const phase of [ resolve, tweakAssocs, assertConsistency, check ]) {
560
+ phase( model );
561
+ // eslint-disable-next-line no-await-in-loop
562
+ await checkAsyncAbortFlag( options.abortSignal );
563
+ }
564
+
565
+ throwWithError();
566
+ if (options.lintMode)
567
+ return model;
568
+
569
+ return propagator.propagate( model );
570
+ }
571
+
515
572
  /**
516
573
  * Process an array of `filenames`. Returns an object with properties:
517
574
  * - `sources`: dictionary which has a filename as key (value is irrelevant)
@@ -595,6 +652,31 @@ function createSourcesDict( filenames, filenameMap, dir ) {
595
652
  return { sources, files, repeated };
596
653
  }
597
654
 
655
+ /**
656
+ * An `await`able function to fake a real asynchronous event.
657
+ *
658
+ * @returns {Promise<unknown>}
659
+ */
660
+ async function waitForNextEventLoopIteration() {
661
+ return new Promise( ( r ) => {
662
+ setTimeout( r, 0 );
663
+ });
664
+ }
665
+
666
+ /**
667
+ * An actual async function that uses `setTimeout()` to allow the Node event loop to
668
+ * start its next iteration, but only if the `abortSignal` is defined.
669
+ *
670
+ * @param {AbortSignal?} abortSignal
671
+ * @returns {Promise<void>}
672
+ */
673
+ async function checkAsyncAbortFlag( abortSignal ) {
674
+ if (!abortSignal)
675
+ return;
676
+ await waitForNextEventLoopIteration();
677
+ abortSignal.throwIfAborted();
678
+ }
679
+
598
680
  module.exports = {
599
681
  parseX,
600
682
  compileX,
@@ -397,6 +397,8 @@ function* nameAsReference( ref, hint = null ) {
397
397
  function* definitionNameTokens( name, art ) {
398
398
  if (!art.kind)
399
399
  return null; // e.g. parameter references
400
+ if (!name)
401
+ return null; // e.g. column that couldn't be populated
400
402
  if (art.kind === '$annotation')
401
403
  return null; // annotation name, e.g. in `@anno: (elem)`
402
404
 
@@ -813,7 +813,7 @@ function populate( model ) {
813
813
  // invent a name for code completion in expression, see also #10596
814
814
  col.name = {
815
815
  id: '',
816
- location: col.value && col.value.location || col.location,
816
+ location: col.value?.location || col.location,
817
817
  $inferred: 'none',
818
818
  };
819
819
  return '';
@@ -979,14 +979,14 @@ function populate( model ) {
979
979
  // ID published! Used in stakeholder project; if renamed, add to oldMessageIds
980
980
  info( 'wildcard-excluding-many', [ sibling.name.location, query ],
981
981
  { id, keyword: 'excluding' },
982
- // eslint-disable-next-line @stylistic/js/max-len
982
+ // eslint-disable-next-line @stylistic/max-len
983
983
  'This select item replaces $(ID) from two or more sources. Add $(ID) to $(KEYWORD) to silence this message' );
984
984
  }
985
985
  else {
986
986
  // ID published! Used in stakeholder project; if renamed, add to oldMessageIds
987
987
  info( 'wildcard-excluding-one', [ sibling.name.location, query ],
988
988
  { id, alias: navElem._parent.name.id, keyword: 'excluding' },
989
- // eslint-disable-next-line @stylistic/js/max-len
989
+ // eslint-disable-next-line @stylistic/max-len
990
990
  'This select item replaces $(ID) from table alias $(ALIAS). Add $(ID) to $(KEYWORD) to silence this message' );
991
991
  }
992
992
  }
@@ -1112,9 +1112,9 @@ function populate( model ) {
1112
1112
  // art: definitionScope( target ), - TODO extra debug info in message
1113
1113
  sorted_arts: exposed,
1114
1114
  }, {
1115
- // eslint-disable-next-line @stylistic/js/max-len
1115
+ // eslint-disable-next-line @stylistic/max-len
1116
1116
  std: 'Replace target $(TARGET) by one of $(SORTED_ARTS); can\'t auto-redirect this association if multiple projections exist in this service',
1117
- // eslint-disable-next-line @stylistic/js/max-len
1117
+ // eslint-disable-next-line @stylistic/max-len
1118
1118
  two: 'Replace target $(TARGET) by $(SORTED_ARTS) or $(SECOND); can\'t auto-redirect this association if multiple projections exist in this service',
1119
1119
  } );
1120
1120
  // continuation semantics: no auto-redirection
@@ -1136,11 +1136,11 @@ function populate( model ) {
1136
1136
  anno: 'cds.redirection.target',
1137
1137
  sorted_arts: exposed,
1138
1138
  }, {
1139
- // eslint-disable-next-line @stylistic/js/max-len
1139
+ // eslint-disable-next-line @stylistic/max-len
1140
1140
  std: 'Add $(ANNO) to one of $(SORTED_ARTS) to select the entity as redirection target for $(TARGET) in this service; can\'t auto-redirect $(ART) otherwise',
1141
- // eslint-disable-next-line @stylistic/js/max-len
1141
+ // eslint-disable-next-line @stylistic/max-len
1142
1142
  two: 'Add $(ANNO) to either $(SORTED_ARTS) or $(SECOND) to select the entity as redirection target for $(TARGET) in this service; can\'t auto-redirect $(ART) otherwise',
1143
- // eslint-disable-next-line @stylistic/js/max-len
1143
+ // eslint-disable-next-line @stylistic/max-len
1144
1144
  justOne: 'Remove $(ANNO) from all but one of $(SORTED_ARTS) to have a unique redirection target for $(TARGET) in this service; can\'t auto-redirect $(ART) otherwise',
1145
1145
  } );
1146
1146
  }
@@ -354,7 +354,7 @@ function propagate( model ) {
354
354
  const art = item && item._artifact;
355
355
  if (art?.virtual?.val) {
356
356
  message( 'def-missing-virtual', [ item.location, elem ], { art, keyword: 'virtual' },
357
- // eslint-disable-next-line @stylistic/js/max-len
357
+ // eslint-disable-next-line @stylistic/max-len
358
358
  'Prepend $(KEYWORD) to current select item - containing element $(ART) is virtual' );
359
359
  return;
360
360
  }
@@ -326,7 +326,7 @@ function resolve( model ) {
326
326
  propagateKeys = false;
327
327
  info( 'query-from-many', [ toMany.location, query ], { art: toMany }, {
328
328
  std: 'Key properties are not propagated because a to-many association $(ART) is selected',
329
- // eslint-disable-next-line @stylistic/js/max-len
329
+ // eslint-disable-next-line @stylistic/max-len
330
330
  element: 'Key properties are not propagated because a to-many association $(MEMBER) of $(ART) is selected',
331
331
  } );
332
332
  }
@@ -367,9 +367,9 @@ function resolve( model ) {
367
367
  // ID published! Used in stakeholder project; if renamed, add to oldMessageIds
368
368
  info( 'query-navigate-many', [ art.location, user || query ], { art }, {
369
369
  std: 'Navigating along to-many association $(ART) - key properties are not propagated',
370
- // eslint-disable-next-line @stylistic/js/max-len
370
+ // eslint-disable-next-line @stylistic/max-len
371
371
  element: 'Navigating along to-many association $(MEMBER) of $(ART) - key properties are not propagated',
372
- // eslint-disable-next-line @stylistic/js/max-len
372
+ // eslint-disable-next-line @stylistic/max-len
373
373
  alias: 'Navigating along to-many mixin association $(MEMBER) - key properties are not propagated',
374
374
  } );
375
375
  }
@@ -430,10 +430,7 @@ function resolve( model ) {
430
430
  }
431
431
  else if (!allowedInMain || !isTopLevelElement) {
432
432
  warning( 'def-unsupported-key', [ art.key.location, art ],
433
- { '#': allowedInMain ? 'sub' : 'std', keyword: 'key' }, {
434
- std: '$(KEYWORD) is only supported for elements in an entity or an aspect',
435
- sub: '$(KEYWORD) is only supported for top-level elements',
436
- } );
433
+ { '#': allowedInMain ? 'sub' : 'kind', keyword: 'key' } );
437
434
  }
438
435
  }
439
436
 
@@ -490,9 +487,9 @@ function resolve( model ) {
490
487
  resolveTypeExpr( obj, art );
491
488
  // typeOf unmanaged assoc? TODO: is this the right place to check this?
492
489
  // (probably better in rewriteAssociations)
493
- const elemtype = obj.type._artifact;
494
- if (elemtype && effectiveType( elemtype )) {
495
- const assocType = getAssocSpec( elemtype ) || {};
490
+ const elemType = obj.type._artifact;
491
+ if (elemType && effectiveType( elemType )) {
492
+ const assocType = getAssocSpec( elemType ) || {};
496
493
  if ((assocType.on || assocType.$assocFilter) && !obj.on)
497
494
  obj.on = { $inferred: 'rewrite' }; // TODO: no extra rewrite here
498
495
  if (assocType.targetAspect) {
@@ -506,11 +503,10 @@ function resolve( model ) {
506
503
  }
507
504
 
508
505
  // Check if relational type is missing its target or if it's used directly.
509
- if (elemtype.category === 'relation' &&
510
- !obj.target && !obj.targetAspect) {
506
+ if (elemType.category === 'relation' && !obj.target && !obj.targetAspect) {
511
507
  const isCsn = (obj._block && obj._block.$frontend === 'json');
512
508
  error( 'type-missing-target', [ obj.type.location, obj ],
513
- { '#': isCsn ? 'csn' : 'std', type: elemtype }, {
509
+ { '#': isCsn ? 'csn' : 'std', type: elemType }, {
514
510
  // We don't say "use 'association to <target>" because the type could be used
515
511
  // in action parameters, etc. as well.
516
512
  std: 'The type $(TYPE) can\'t be used directly because it\'s compiler internal',
@@ -906,10 +902,13 @@ function resolve( model ) {
906
902
  }
907
903
  else if (effectiveType( art )?.elements && !art.$inferred) {
908
904
  // For inferred (e.g. included) calc elements, this error is already emitted at the origin.
909
- if (art.type)
905
+ if (art.type) {
910
906
  error( 'type-unexpected-structure', [ art.type.location, art ], { '#': 'calc' } );
911
- else
912
- error( 'ref-unexpected-structured', [ art.value.location, art ], { '#': 'expr' } );
907
+ }
908
+ else {
909
+ error( 'ref-unexpected-structured', [ art.value.location, art ],
910
+ { '#': 'struct-expr', elemref: art.value } );
911
+ }
913
912
  }
914
913
  else if (effectiveType( art )?.items && !art.$inferred) {
915
914
  // For inferred (e.g. included) calc elements, this error is already emitted at the origin.
@@ -920,7 +919,7 @@ function resolve( model ) {
920
919
  } );
921
920
  }
922
921
  else {
923
- const noTruthyAllowed = [ 'localized', 'key', 'virtual' ];
922
+ const noTruthyAllowed = [ 'key', 'virtual' ];
924
923
  for (const prop of noTruthyAllowed) {
925
924
  if (art[prop]?.val) {
926
925
  // probably better than a parse error (which is good for DEFAULT vs calc),
@@ -1274,7 +1273,7 @@ function resolve( model ) {
1274
1273
  const text = (item !== last) ? 'sub' : 'std';
1275
1274
  error( 'duplicate-key-ref', [ item.location, key ], { '#': text, name }, {
1276
1275
  std: 'Foreign key $(NAME) already refers to the same target element',
1277
- // eslint-disable-next-line @stylistic/js/max-len
1276
+ // eslint-disable-next-line @stylistic/max-len
1278
1277
  sub: 'Foreign key $(NAME) already refers to the target element whose sub element is again referred to here',
1279
1278
  // TODO: please add ideas for a better text, e.g. to (closed) PR #11325
1280
1279
  } );
@@ -1594,10 +1593,12 @@ function resolve( model ) {
1594
1593
  const art = type && (type.kind === 'entity' ? type : type.target?._artifact);
1595
1594
  if (!art)
1596
1595
  return ref; // error already reported via resolvePathItem()
1597
- const unexpectedFilter = expected !== 'column' && expected !== 'calc' && 'std' ||
1598
- isQuasiVirtualAssociation( type ) && 'model-only';
1599
- if (last.args || last.where || last.cardinality)
1596
+ if (last.args || last.where || last.cardinality) {
1597
+ const unexpectedFilter = (expected !== 'annotation' && expected !== 'column' &&
1598
+ expected !== 'calc' && 'std') ||
1599
+ isQuasiVirtualAssociation( type ) && 'model-only';
1600
1600
  reportUnexpectedArgsAndFilter( last, expected, user, art, unexpectedFilter );
1601
+ }
1601
1602
  // TODO: we should have different message-ids for the "last" stuff: adding
1602
1603
  // `.item` likely corrects the ref, probably with location at end of ref
1603
1604
  return ref;
@@ -991,7 +991,7 @@ function fns( model ) {
991
991
  }
992
992
  else if ($extended && art.elements) {
993
993
  warning( 'ref-deprecated-in-extend', [ head.location, user ], { id: head.id },
994
- // eslint-disable-next-line @stylistic/js/max-len
994
+ // eslint-disable-next-line @stylistic/max-len
995
995
  'In an added column, do not use the table alias $(ID) to refer to source elements' );
996
996
  }
997
997
  }
@@ -1489,7 +1489,7 @@ function fns( model ) {
1489
1489
  // of invisible table aliases; at least one stakeholder uses this,
1490
1490
  // so it can't be an error (yet).
1491
1491
  message( 'ref-deprecated-self-element', [ ref.path[0].location, user._user ], {},
1492
- // eslint-disable-next-line @stylistic/js/max-len
1492
+ // eslint-disable-next-line @stylistic/max-len
1493
1493
  'Referring to the query\'s own elements here might lead to invalid SQL references; use source elements only' );
1494
1494
  return false;
1495
1495
  default:
@@ -1898,7 +1898,7 @@ function fns( model ) {
1898
1898
  else if (target._artifact && target._artifact !== user._main && user._main.kind === 'entity') {
1899
1899
  const last = path[path.length - 1];
1900
1900
  warning( 'ref-invalid-backlink', [ last.location, user ], { art: target, id: '$self' },
1901
- // eslint-disable-next-line @stylistic/js/max-len
1901
+ // eslint-disable-next-line @stylistic/max-len
1902
1902
  'The target $(ART) of the association is not the current entity represented by $(ID)' );
1903
1903
  }
1904
1904
  }
@@ -1988,11 +1988,11 @@ function fns( model ) {
1988
1988
  std: 'Can follow association $(ART) only to its foreign key references, not to $(NAME)',
1989
1989
  keys: 'Can follow managed association $(ART) only to the keys of its target, not to $(NAME)',
1990
1990
  complete: 'The reference must cover a full foreign key reference of association $(ART)',
1991
- // eslint-disable-next-line @stylistic/js/max-len
1991
+ // eslint-disable-next-line @stylistic/max-len
1992
1992
  'self-std': 'In column ref starting with $(ALIAS), we can follow association $(ART) only to its foreign key references, not to $(NAME)',
1993
- // eslint-disable-next-line @stylistic/js/max-len
1993
+ // eslint-disable-next-line @stylistic/max-len
1994
1994
  'self-keys': 'In column ref starting with $(ALIAS), we can follow managed association $(ART) only to the keys of its target, not to $(NAME)',
1995
- // eslint-disable-next-line @stylistic/js/max-len
1995
+ // eslint-disable-next-line @stylistic/max-len
1996
1996
  'self-complete': 'The column reference starting with $(ALIAS) must cover a full foreign key reference of association $(ART)',
1997
1997
  } );
1998
1998
  // TODO later: mention allowed ones
@@ -129,7 +129,7 @@ function tweakAssocs( model ) {
129
129
  // ID published! Used in stakeholder project; if renamed, add to oldMessageIds
130
130
  info( 'assoc-outside-service', loc, { '#': text, target, service: main._service }, {
131
131
  std: 'Association target $(TARGET) is outside any service',
132
- // eslint-disable-next-line @stylistic/js/max-len
132
+ // eslint-disable-next-line @stylistic/max-len
133
133
  exposed: 'If association is published in service $(SERVICE), its target $(TARGET) is outside any service',
134
134
  } );
135
135
  }
@@ -146,7 +146,7 @@ function tweakAssocs( model ) {
146
146
  if (assoc && assoc.foreignKeys) {
147
147
  error( 'rewrite-key-for-unmanaged', [ elem.on.location, elem ],
148
148
  { keyword: 'on', art: assocWithExplicitSpec( assoc ) },
149
- // eslint-disable-next-line @stylistic/js/max-len
149
+ // eslint-disable-next-line @stylistic/max-len
150
150
  'Do not specify an $(KEYWORD) condition when redirecting the managed association $(ART)' );
151
151
  }
152
152
  checkIgnoredFilter( elem );
@@ -349,42 +349,46 @@ function tweakAssocs( model ) {
349
349
  setLink( fk, '_effectiveType', fk );
350
350
  fk.targetElement = copyExpr( orig.targetElement, location );
351
351
  if (elem._redirected)
352
- rewriteKey( elem, fk.targetElement );
352
+ rewriteKey( elem, fk );
353
353
  } );
354
354
  if (elem.foreignKeys) // Possibly no fk was set
355
355
  elem.foreignKeys[$inferred] = 'rewrite';
356
356
  }
357
357
 
358
- function rewriteKey( elem, targetElement ) {
358
+ function rewriteKey( elem, fk ) {
359
+ const { targetElement } = fk;
359
360
  let projectedKey = null;
360
361
  // rewrite along redirection chain
361
362
  for (const alias of elem._redirected) {
362
- if (alias.kind !== '$tableAlias')
363
- continue;
364
-
365
- projectedKey = firstProjectionForPath( targetElement.path, 0, alias, null );
366
- if (projectedKey.elem) {
367
- const item = targetElement.path[projectedKey.index];
368
- item.id = projectedKey.elem.name.id;
369
- if (projectedKey.index > 0)
370
- targetElement.path.splice(0, projectedKey.index);
371
- }
372
- else {
373
- setArtifactLink( targetElement.path[0], null );
374
- setArtifactLink( targetElement, null );
363
+ if (alias.kind === '$tableAlias') {
364
+ projectedKey = firstProjectionForPath( targetElement.path, 0, alias, null );
365
+ if (projectedKey.elem) {
366
+ const item = targetElement.path[projectedKey.index];
367
+ item.id = projectedKey.elem.name.id;
368
+ if (projectedKey.index > 0)
369
+ targetElement.path.splice(0, projectedKey.index);
370
+ }
371
+ else {
372
+ setArtifactLink( targetElement.path[0], null );
373
+ setArtifactLink( targetElement, null );
375
374
 
376
- const culprit = !elem.target.$inferred && elem.target ||
375
+ const culprit = !elem.target.$inferred && elem.target ||
377
376
  elem.value?.path?.[elem.value.path.length - 1] ||
378
377
  elem;
379
- // TODO: probably better to collect the non-projected foreign keys
380
- // and have one message for all
381
- error('rewrite-undefined-key', [ weakLocation( culprit.location ), elem ], {
382
- '#': 'std',
383
- id: targetElement.path.map(p => p.id).join('.'),
384
- target: alias._main,
385
- name: elem.name.id,
386
- });
387
- return null;
378
+ // TODO: probably better to collect the non-projected foreign keys
379
+ // and have one message for all
380
+ error('rewrite-undefined-key', [ weakLocation( culprit.location ), elem ], {
381
+ '#': 'std',
382
+ id: targetElement.path.map(p => p.id).join('.'),
383
+ target: alias._main,
384
+ name: elem.name.id,
385
+ });
386
+ return null;
387
+ }
388
+ }
389
+ else {
390
+ // e.g. redirection target is entity that includes original target
391
+ projectedKey = { elem: findTargetElement( alias, targetElement ) };
388
392
  }
389
393
  }
390
394
 
@@ -397,6 +401,24 @@ function tweakAssocs( model ) {
397
401
  return null;
398
402
  }
399
403
 
404
+ /**
405
+ * Find the target element in the given redirection target.
406
+ * Used to find the target element in entities that include the original
407
+ * target entity.
408
+ *
409
+ * @param redirected
410
+ * @param targetElement
411
+ * @returns {*|null}
412
+ */
413
+ function findTargetElement( redirected, targetElement ) {
414
+ for (const step of targetElement.path) {
415
+ redirected = redirected.elements?.[step.id];
416
+ if (!redirected)
417
+ return null;
418
+ }
419
+ return redirected;
420
+ }
421
+
400
422
  // TODO: there is no need to rewrite the on condition of non-leading queries,
401
423
  // i.e. we could just have on = {…}
402
424
  // TODO: re-check $self rewrite (with managed composition of aspects),
@@ -714,7 +736,7 @@ function tweakAssocs( model ) {
714
736
  if (assoc.$errorReported !== 'assoc-unexpected-scope') {
715
737
  error( 'assoc-unexpected-scope', [ assoc.value.location, assoc ],
716
738
  { id: assoc.value._artifact.name.id },
717
- // eslint-disable-next-line @stylistic/js/max-len
739
+ // eslint-disable-next-line @stylistic/max-len
718
740
  'Association $(ID) can\'t be projected because its ON-condition refers to a parameter' );
719
741
  assoc.$errorReported = 'assoc-unexpected-scope';
720
742
  }
@@ -747,11 +769,11 @@ function tweakAssocs( model ) {
747
769
  if (!(elem === item._artifact || // redirection for explicit def
748
770
  elem._origin === item._artifact)) {
749
771
  const art = assoc._origin;
750
- // eslint-disable-next-line @stylistic/js/max-len
772
+ // eslint-disable-next-line @stylistic/max-len
751
773
  warning( 'rewrite-shadowed', [ elem.name.location, elem ], { art: art && effectiveType( art ) }, {
752
- // eslint-disable-next-line @stylistic/js/max-len
774
+ // eslint-disable-next-line @stylistic/max-len
753
775
  std: 'This element is not originally referred to in the ON-condition of association $(ART)',
754
- // eslint-disable-next-line @stylistic/js/max-len
776
+ // eslint-disable-next-line @stylistic/max-len
755
777
  element: 'This element is not originally referred to in the ON-condition of association $(MEMBER) of $(ART)',
756
778
  } );
757
779
  }
@@ -150,7 +150,7 @@ function setMemberParent( elem, name, parent, prop ) {
150
150
  p[prop] = Object.create( null );
151
151
  dictAdd( p[prop], name, elem );
152
152
  }
153
- if (parent._outer && parent._outer.items) // TODO: remove for items, too
153
+ if (parent._outer?.items) // TODO: remove for items, too
154
154
  parent = parent._outer;
155
155
  setLink( elem, '_parent', parent );
156
156
  setLink( elem, '_main', parent._main || parent );
@@ -484,11 +484,13 @@ function traverseQueryPost( query, simpleOnly, callback ) {
484
484
  // else: with parse error (`select from <EOF>`, `select distinct from;`)
485
485
  }
486
486
 
487
- // Call callback on all queries in dependency order, i.e. starting with query Q
488
- // 1. sub queries in FROM sources of Q
489
- // 2. Q itself, except if non-referred query, but with right UNION parts
490
- // 3. sub queries in ON in FROM of Q
491
- // 4. sub queries in columns, WHERE, HAVING
487
+ /**
488
+ * Call callback on all queries in dependency order, i.e. starting with query Q
489
+ * 1. sub queries in FROM sources of Q
490
+ * 2. Q itself, ALSO if non-referred query
491
+ * 3. sub queries in ON in FROM of Q
492
+ * 4. sub queries in columns, WHERE, HAVING
493
+ */
492
494
  function traverseQueryExtra( main, callback ) {
493
495
  if (!main.$queries)
494
496
  return;
@@ -501,16 +503,7 @@ function traverseQueryExtra( main, callback ) {
501
503
  if (query._status === 'extra' || query._parent.kind === '$tableAlias')
502
504
  continue; // if parent is alias, query is FROM source -> run by traverseQueryPost
503
505
  // we are now in the top-level (parent is entity) or a non-referred query (parent is query)
504
- setLink( query, '_status', 'extra' ); // do not call callback() in non-referred query
505
- // console.log( 'A:', query.name,query._status)
506
- traverseQueryPost( query, null, (q) => {
507
- if (q._status !== 'extra') {
508
- // console.log( 'T:', q.name)
509
- setLink( q, '_status', 'extra' );
510
- callback( q );
511
- }
512
- // else console.log( 'E:', q.name)
513
- } );
506
+ traverseQueryPost( query, null, callback );
514
507
  }
515
508
  }
516
509
 
@@ -266,7 +266,7 @@ function xprRewriteFns( model ) {
266
266
  config.tokenExpr = expr;
267
267
  return traverseExpr.STOP === traverseExpr(
268
268
  expr, 'annoRewrite', config.target,
269
- // eslint-disable-next-line @stylistic/js/max-len, @stylistic/js/function-paren-newline
269
+ // eslint-disable-next-line @stylistic/max-len, @stylistic/function-paren-newline
270
270
  (e, refCtx) => (rewriteAnnoExpr( e, config, refCtx ) ? traverseExpr.STOP : traverseExpr.SKIP) );
271
271
  }
272
272
  return false;
@@ -323,7 +323,7 @@ function xprRewriteFns( model ) {
323
323
  const filterConfig = { ...config, target: assocTarget, isInFilter: true };
324
324
  if (traverseExpr.STOP === traverseExpr(
325
325
  step.where, 'filter', step,
326
- // eslint-disable-next-line @stylistic/js/max-len
326
+ // eslint-disable-next-line @stylistic/max-len
327
327
  (e, ctx) => expr.path && (rewriteGenericAnnoPath( e, filterConfig, ctx ) ? traverseExpr.STOP : traverseExpr.SKIP)
328
328
  ))
329
329
  return true;