@sap/cds-compiler 6.3.6 → 6.4.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 (55) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/LICENSE +32 -0
  3. package/README.md +14 -2
  4. package/bin/cdsse.js +0 -3
  5. package/doc/CHANGELOG_BETA.md +1 -1
  6. package/doc/CHANGELOG_DEPRECATED.md +1 -1
  7. package/lib/base/message-registry.js +7 -0
  8. package/lib/base/messages.js +1 -1
  9. package/lib/base/model.js +2 -0
  10. package/lib/compiler/assert-consistency.js +1 -0
  11. package/lib/compiler/checks.js +37 -26
  12. package/lib/compiler/define.js +1 -1
  13. package/lib/compiler/extend.js +39 -50
  14. package/lib/compiler/finalize-parse-cdl.js +1 -1
  15. package/lib/compiler/lsp-api.js +1 -1
  16. package/lib/compiler/populate.js +2 -2
  17. package/lib/compiler/propagator.js +29 -6
  18. package/lib/compiler/resolve.js +13 -3
  19. package/lib/compiler/shared.js +31 -25
  20. package/lib/compiler/tweak-assocs.js +86 -28
  21. package/lib/compiler/xpr-rewrite.js +70 -38
  22. package/lib/edm/annotations/edmJson.js +206 -37
  23. package/lib/edm/csn2edm.js +13 -0
  24. package/lib/edm/edmUtils.js +2 -2
  25. package/lib/gen/BaseParser.js +106 -72
  26. package/lib/gen/CdlGrammar.checksum +1 -1
  27. package/lib/gen/CdlParser.js +1500 -1509
  28. package/lib/json/to-csn.js +8 -5
  29. package/lib/language/genericAntlrParser.js +0 -0
  30. package/lib/main.js +19 -16
  31. package/lib/model/csnRefs.js +589 -521
  32. package/lib/model/csnUtils.js +8 -5
  33. package/lib/model/enrichCsn.js +1 -0
  34. package/lib/parsers/AstBuildingParser.js +72 -27
  35. package/lib/render/toCdl.js +2 -1
  36. package/lib/render/toHdbcds.js +6 -3
  37. package/lib/render/toSql.js +5 -0
  38. package/lib/transform/db/applyTransformations.js +1 -1
  39. package/lib/transform/db/assertUnique.js +4 -1
  40. package/lib/transform/db/cdsPersistence.js +17 -18
  41. package/lib/transform/db/expansion.js +179 -3
  42. package/lib/transform/db/flattening.js +16 -5
  43. package/lib/transform/db/rewriteCalculatedElements.js +79 -283
  44. package/lib/transform/effective/main.js +8 -1
  45. package/lib/transform/forOdata.js +1 -1
  46. package/lib/transform/forRelationalDB.js +21 -80
  47. package/lib/transform/localized.js +65 -110
  48. package/lib/transform/odata/foreignKeyRefsInXprAnnos.js +89 -63
  49. package/lib/transform/transformUtils.js +23 -21
  50. package/lib/transform/translateAssocsToJoins.js +7 -5
  51. package/lib/transform/tupleExpansion.js +16 -3
  52. package/package.json +1 -1
  53. package/doc/DeprecatedOptions_v2.md +0 -150
  54. package/doc/NameResolution.md +0 -837
  55. package/lib/transform/parseExpr.js +0 -415
@@ -12,6 +12,7 @@ const {
12
12
  forEachDefinition,
13
13
  forEachMember,
14
14
  forEachGeneric,
15
+ isBetaEnabled,
15
16
  } = require( '../base/model');
16
17
  const {
17
18
  setLink,
@@ -34,7 +35,11 @@ function propagate( model ) {
34
35
  virtual,
35
36
  notNull,
36
37
  targetElement: onlyViaParent, // in foreign keys
37
- value: onlyViaParent, // enum symbol value, calculated element
38
+ value: enumOrCalcValue, // enum symbol value, calculated element
39
+ // `value` is also used for column expression
40
+ // TODO(!): think of having an extra XSN property for calculated elements,
41
+ // replacing `value:…`+`$syntax:'calc'` and `$calc:…
42
+ $calc: enumOrCalcValue,
38
43
  // masked: special = done in definer
39
44
  // key: special = done in resolver
40
45
  // actions: struct includes & primary source = in definer/resolver
@@ -59,7 +64,7 @@ function propagate( model ) {
59
64
  // enum: expensive,
60
65
  // params: expensive, // actually only with parent action
61
66
  // returns,
62
- $enclosed: annotation,
67
+ $enclosed: annotation, // TODO: hm
63
68
  };
64
69
  const ruleToFunction = {
65
70
  __proto__: null,
@@ -71,7 +76,7 @@ function propagate( model ) {
71
76
  for (const rule in propagationRules)
72
77
  props[rule] = ruleToFunction[propagationRules[rule]];
73
78
 
74
- const { rewriteAnnotationsRefs } = xprRewriteFns( model );
79
+ const { rewriteAnnotationsRefs, rewriteRefsInExpression } = xprRewriteFns( model );
75
80
 
76
81
  const { message, throwWithError } = model.$messageFunctions;
77
82
 
@@ -165,7 +170,9 @@ function propagate( model ) {
165
170
  // console.log('PROPS:',ref(source),'->',ref(target),keys.join('+'))
166
171
  for (const prop of keys) {
167
172
  // TODO: warning with competing props from multi-includes, but not in propagator.js
168
- if (target[prop] !== undefined || source[prop] === undefined)
173
+ if (target[prop] !== undefined &&
174
+ (prop !== 'value' || !source.$calcDepElement || !target._main?.query) ||
175
+ source[prop] === undefined)
169
176
  continue;
170
177
  const transformer = props[prop] || props[prop.charAt(0)];
171
178
  if (transformer)
@@ -282,6 +289,22 @@ function propagate( model ) {
282
289
  }
283
290
  }
284
291
 
292
+ function enumOrCalcValue( prop, destination, origin ) {
293
+ // Remark: with include, the calc expression has been copied early
294
+ if (prop === 'value' && !origin.$calcDepElement) {
295
+ onlyViaParent( prop, destination, origin ); // enum value
296
+ }
297
+ else if (destination.kind === 'element' &&
298
+ destination._main?.query && // query element
299
+ !destination.$calc && origin.$calc !== true &&
300
+ isBetaEnabled( model.options, '$calcForDraft' )) {
301
+ destination.$calc
302
+ = Object.assign( copyExpr( origin[prop] ), { $inferred: 'prop' } );
303
+ if (rewriteRefsInExpression( destination, origin, '$calc' ))
304
+ destination.$calc = true; // TODO: or { val: true }?
305
+ }
306
+ }
307
+
285
308
  function notWithExpand( prop, target, source ) {
286
309
  if (!target.expand || prop === 'type' && source.elements)
287
310
  always( prop, target, source );
@@ -296,13 +319,13 @@ function propagate( model ) {
296
319
  function annotation( prop, target, source ) {
297
320
  const anno = source[prop];
298
321
  if (anno.val !== null)
299
- withKind( prop, target, source );
322
+ withKind( prop, target, source ); // TODO: unfold
300
323
  }
301
324
 
302
325
  function docComment( prop, target, source ) {
303
326
  if (model.options.propagateDocComments)
304
327
  annotation( prop, target, source );
305
- else // TODO: Probably just "never"
328
+ else // TODO: or just "never"
306
329
  onlyViaParent( prop, target, source );
307
330
  }
308
331
 
@@ -1583,12 +1583,19 @@ function resolve( model ) {
1583
1583
  // (for code completion)
1584
1584
  const last = expr.path[expr.path.length - 1];
1585
1585
  if (!last || !(last.args || last.where || last.cardinality) ||
1586
- expr.$expected === 'approved-exists' ||
1587
1586
  user.expand || user.inline ||
1588
1587
  expWithFilter.includes( expected ) || // `from`, …
1589
1588
  last._navigation?.kind === '$tableAlias') // error already reported
1590
1589
  return ref;
1591
1590
 
1591
+ if (expr.$expected === 'approved-exists') {
1592
+ if (last.where?.args?.length === 0) {
1593
+ // at the moment, empty filter is not allowed on last path step
1594
+ reportUnexpectedArgsAndFilter( last, expected, user, null, 'last-empty-filter' );
1595
+ }
1596
+ return ref;
1597
+ }
1598
+
1592
1599
  const type = effectiveType( last._artifact );
1593
1600
  const art = type && (type.kind === 'entity' ? type : type.target?._artifact);
1594
1601
  if (!art)
@@ -1596,7 +1603,8 @@ function resolve( model ) {
1596
1603
  if (last.args || last.where || last.cardinality) {
1597
1604
  const unexpectedFilter = (expected !== 'annotation' && expected !== 'column' &&
1598
1605
  expected !== 'calc' && 'std') ||
1599
- isQuasiVirtualAssociation( type ) && 'model-only';
1606
+ isQuasiVirtualAssociation( type ) && 'model-only' ||
1607
+ last.where?.args?.length === 0 && 'last-empty-filter';
1600
1608
  reportUnexpectedArgsAndFilter( last, expected, user, art, unexpectedFilter );
1601
1609
  }
1602
1610
  // TODO: we should have different message-ids for the "last" stuff: adding
@@ -1696,7 +1704,9 @@ function resolve( model ) {
1696
1704
  error( 'expr-unexpected-argument', loc, { '#': variant } );
1697
1705
  }
1698
1706
  if ((step.where || step.cardinality) && variant) {
1699
- const location = combinedLocation( step.where, step.cardinality );
1707
+ const location = step.where?.location || step.cardinality?.location
1708
+ ? combinedLocation( step.where, step.cardinality )
1709
+ : step.location;
1700
1710
  // XSN TODO: filter$location including […]
1701
1711
  error( 'expr-unexpected-filter', [ location, user ], { '#': variant } );
1702
1712
  }
@@ -40,6 +40,7 @@ function fns( model ) {
40
40
  } = model.$messageFunctions;
41
41
  const Functions = model.$functions;
42
42
 
43
+ // Map `exprCtx` (is a param of traversal functions) to reference semantics
43
44
  const referenceSemantics = {
44
45
  // global: ------------------------------------------------------------------
45
46
  using: { // only used to produce error message
@@ -60,10 +61,10 @@ function fns( model ) {
60
61
  isMainRef: 'no-generated',
61
62
  lexical: userBlock,
62
63
  dynamic: modelDefinitions,
63
- notFound: undefinedDefinition,
64
+ notFound: undefinedForExtend,
64
65
  accept: extendableArtifact,
65
66
  },
66
- _extensions: {
67
+ _uncheckedExtension: { // to be used only with resolveUncheckedPath()
67
68
  isMainRef: 'all',
68
69
  lexical: userBlock,
69
70
  dynamic: modelDefinitions,
@@ -76,12 +77,6 @@ function fns( model ) {
76
77
  notFound: undefinedDefinition,
77
78
  accept: acceptStructOrBare,
78
79
  },
79
- _include: { // cyclic include: no accept
80
- isMainRef: 'no-generated',
81
- lexical: userBlock,
82
- dynamic: modelBuiltinsOrDefinitions,
83
- notFound: undefinedDefinition,
84
- },
85
80
  target: {
86
81
  isMainRef: 'no-autoexposed',
87
82
  lexical: userBlock,
@@ -315,6 +310,7 @@ function fns( model ) {
315
310
  rewriteProjectionToSelf: true,
316
311
  }),
317
312
  },
313
+ //
318
314
  };
319
315
 
320
316
  Object.assign( model.$functions, {
@@ -853,7 +849,7 @@ function fns( model ) {
853
849
  const { isMainRef } = semantics;
854
850
  if (isMainRef) {
855
851
  artItemsCount = (typeof ref.scope === 'number' && ref.scope) ||
856
- (ref.scope ? 1 : path.length);
852
+ (ref.scope ? 1 : path.length);
857
853
  }
858
854
  let art = null;
859
855
  const elementsEnv = semantics.navigation || environment;
@@ -1287,26 +1283,36 @@ function fns( model ) {
1287
1283
 
1288
1284
  function undefinedForAnnotate( user, item, valid, _dict, prev, path ) {
1289
1285
  // in a CSN source, only one env was tested (valid.length 1):
1290
- const art = (!prev) ? item.id : searchName( prev, item.id, 'absolute' );
1286
+ const name = (prev) ? `${ prev.name.id }.${ item.id }` : item.id;
1291
1287
  if (!user.elements && !user.actions && !user.enum && !user.params &&
1292
- couldBeDraftsEntity( item.id, valid, prev, path ))
1293
- return;
1294
- if (couldBeDraftAdminDataEntity( item ) )
1288
+ endsWithSuffix( name, '.drafts', art => art?._service && art.kind === 'entity' ) ||
1289
+ endsWithSuffix( name, '.DraftAdministrativeData',
1290
+ ( art, prefix ) => (art?.kind === 'service' || prefix === 'DRAFT') ) ||
1291
+ name === 'DRAFT' && path?.length === 2 && path[1].id === 'DraftAdministrativeData' ||
1292
+ name.startsWith( 'localized.' )) // TODO: only if suffix is defined
1295
1293
  return;
1296
1294
  signalNotFound( (valid.length > 1 ? 'ext-undefined-art' : 'ext-undefined-def'),
1297
1295
  // TODO: ext-undefined-xyz
1298
- [ item.location, user ], valid, { art } );
1296
+ [ item.location, user ], valid, { art: name } );
1299
1297
  }
1300
1298
 
1301
- function couldBeDraftsEntity( id, valid, prev, path ) {
1302
- const entity = prev
1303
- ? prev === path[path.length - 2]._artifact && prev // TODO: Should check for '.drafts'?
1304
- : path.length === 1 && id.endsWith( '.drafts' ) && model.definitions[id.slice( 0, -7 )];
1305
- return entity?.kind === 'entity' && !!entity._service;
1299
+ function undefinedForExtend( user, item, valid, _dict, prev ) {
1300
+ // in a CSN source, only one env was tested (valid.length 1):
1301
+ const name = (prev) ? `${ prev.name.id }.${ item.id }` : item.id;
1302
+ if (name.startsWith( 'localized.' )) {
1303
+ error( 'ref-undefined-art', [ user.name.location || user.location, user ],
1304
+ { '#': 'localized', keyword: 'annotate' } );
1305
+ }
1306
+ else {
1307
+ undefinedDefinition( user, item, valid, _dict, prev );
1308
+ }
1306
1309
  }
1307
1310
 
1308
- function couldBeDraftAdminDataEntity( item ) {
1309
- return item.id === 'DraftAdministrativeData' && !item._artifact;
1311
+ function endsWithSuffix( name, suffix, cond ) {
1312
+ if (!name.endsWith( suffix ))
1313
+ return false;
1314
+ const prefix = name.slice( 0, -suffix.length );
1315
+ return cond( model.definitions[prefix], prefix, name );
1310
1316
  }
1311
1317
 
1312
1318
  function undefinedParam( user, head, valid, _dict, _art, _path, semantics ) {
@@ -1537,7 +1543,7 @@ function fns( model ) {
1537
1543
  }
1538
1544
  // TODO: combine $requireElementAccess/$autoElement to $bareRoot ?
1539
1545
  else if (!user.expand && !user.inline && // $self._artifact to main artifact
1540
- !(art._main && art.kind !== 'select') && ref.path[0]._navigation?.kind === '$self') {
1546
+ !(art._main && art.kind !== 'select') && ref.path[0]._navigation?.kind === '$self') {
1541
1547
  // TODO: better ref-invalid-self
1542
1548
  const { location, id } = path[0];
1543
1549
  error( 'ref-unexpected-self', [ location, user ], { id } );
@@ -1655,7 +1661,7 @@ function fns( model ) {
1655
1661
  }
1656
1662
  }
1657
1663
  else if (source.kind !== 'entity' &&
1658
- !acceptEventProjectionSource( source, user )) {
1664
+ !acceptEventProjectionSource( source, user )) {
1659
1665
  signalNotFound( 'ref-invalid-source', [ ref.location, user ], null,
1660
1666
  { '#': user._main.kind } );
1661
1667
  return (source === art) ? art : false; // art to show cyclic issues
@@ -1937,8 +1943,8 @@ function fns( model ) {
1937
1943
  // has to be run after foreign-key rewrite
1938
1944
  const outer = user._columnParent?._origin;
1939
1945
  let assoc = outer?.foreignKeys &&
1940
- pathStartsWithSelf( { path } ) == null && // not $self or CDS var like $now
1941
- outer;
1946
+ pathStartsWithSelf( { path } ) == null && // not $self or CDS var like $now
1947
+ outer;
1942
1948
  for (let index = startIndex; index < path.length; ++index) {
1943
1949
  if (assoc?.target) {
1944
1950
  if (!assoc.foreignKeys) {
@@ -5,6 +5,7 @@
5
5
  const {
6
6
  forEachGeneric,
7
7
  forEachInOrder,
8
+ isBetaEnabled,
8
9
  } = require('../base/model');
9
10
  const { dictLocation, weakLocation, weakRefLocation } = require('../base/location');
10
11
 
@@ -90,7 +91,7 @@ function tweakAssocs( model ) {
90
91
 
91
92
  if (art.query) {
92
93
  traverseQueryPost(art.query, false, (query) => {
93
- forEachGeneric( query, 'elements', rewriteAssociationCheck );
94
+ forEachGeneric( query, 'elements', handleQueryElements );
94
95
  });
95
96
  }
96
97
  }
@@ -116,10 +117,10 @@ function tweakAssocs( model ) {
116
117
  return;
117
118
  const loc = [ elem.target.location, elem ];
118
119
  const main = elem._main || elem;
119
- if (!elem.$inferred && !main.$inferred) {
120
+ if (!elem.$inferred && !main.$inferred && !model.options.$recompile) {
120
121
  info( 'assoc-target-not-in-service', loc,
121
122
  { target, '#': (elem._main.query ? 'select' : 'define') }, {
122
- std: 'Target $(TARGET) of association is outside any service', // not used
123
+ std: 'Association target $(TARGET) is outside any service', // not used
123
124
  define: 'Target $(TARGET) of explicitly defined association is outside any service',
124
125
  select: 'Target $(TARGET) of explicitly selected association is outside any service',
125
126
  } );
@@ -135,6 +136,26 @@ function tweakAssocs( model ) {
135
136
  }
136
137
  }
137
138
 
139
+ function handleQueryElements( column ) {
140
+ rewriteAssociationCheck( column );
141
+ if (!isBetaEnabled( model.options, '$calcForDraft' ))
142
+ return;
143
+ const { value } = column; // `value` = column expression
144
+ if (!value || !value.args && !value.suffix)
145
+ return;
146
+ // TODO: what about non-simple refs (assocs, even with filter/args)?
147
+
148
+ // with “real” expressions, set $calc according to these
149
+ // (with references, $calc might be inherited from the source element)
150
+ column.$calc = copyExpr( column.value, null ); // copy while keeping location
151
+
152
+ if (traverseExpr.STOP ===
153
+ traverseExpr( column.$calc, 'rewrite-on', column,
154
+ ref => rewriteColumnPath( ref, column ) ))
155
+ column.$calc = true;
156
+ }
157
+
158
+ // Check explicit ON / keys with REDIRECTED TO
138
159
  function rewriteAssociationCheck( element ) {
139
160
  const elem = element.items || element; // TODO v6: nested items
140
161
  if (elem.elements)
@@ -732,6 +753,7 @@ function tweakAssocs( model ) {
732
753
  rewritePathForEnv( expr, navEnv, assoc );
733
754
  }
734
755
  else if (assoc._main.query) { // from ON cond of mixin element in query
756
+ // here also $calc
735
757
  const root = expr.path[0]._navigation || expr.path[0]._artifact;
736
758
  if (expr.scope === 'param' || root?.kind === '$parameters') {
737
759
  if (assoc.$errorReported !== 'assoc-unexpected-scope') {
@@ -743,7 +765,8 @@ function tweakAssocs( model ) {
743
765
  }
744
766
  return;
745
767
  }
746
- if (expr.path[0]._navigation) { // rewrite src elem, mixin, $self[.elem]
768
+
769
+ if (expr.path[0]._navigation) { // rewrite: src elem, mixin, $self[.elem]
747
770
  const nav = pathNavigation( expr );
748
771
  const elem = (assoc._origin === root) ? assoc : navProjection( nav.navigation, assoc );
749
772
  // TODO: Use rewritePathForEnv(); make it handle mixins
@@ -823,8 +846,11 @@ function tweakAssocs( model ) {
823
846
 
824
847
  for (let i = startIndex; i < ref.path.length; ++i) {
825
848
  if (i > startIndex && art.target) {
826
- // if the current artifact is an association, we need to respect the redirection
827
- // chain from original target to new one.
849
+ // TODO: Can we combine this with the code from xpr-rewrite.js
850
+ // If the current artifact is an association, we need to respect the redirection
851
+ // chain from original target to new one. We need to use '_originalArtifact' due
852
+ // to secondary associations and their redirection chains. See comment in
853
+ // test3/Redirections/SecondaryAssocs/RedirectedPathRewriteOne.cds
828
854
  // FIXME: Won't work with associations in projected structures.
829
855
  const origTarget = ref.path[i - 1]?._originalArtifact?.target?._artifact;
830
856
  const chain = cachedRedirectionChain( art, origTarget );
@@ -914,6 +940,33 @@ function tweakAssocs( model ) {
914
940
  }
915
941
  }
916
942
 
943
+ /**
944
+ * Rewrite the reference `ref` with first elem/mixin ref item `item` for user
945
+ * `assoc` (the query element), `elem` is the first (or preferred) query element
946
+ * for item.
947
+ */
948
+ function rewriteColumnPath( ref, column ) {
949
+ if (!ref._artifact)
950
+ return null;
951
+ const root = ref.path?.[0];
952
+ const nav = pathNavigation( ref );
953
+ if (nav.navigation) { // TabAlias.elem, elem, mixin
954
+ const elem = navProjection( nav.navigation, null );
955
+ // TODO?: Use rewritePathForEnv(); make it handle mixins?
956
+ if (rewritePath( ref, nav.item, column, elem, null ))
957
+ return traverseExpr.STOP;
958
+ }
959
+ else if (ref.scope === 'param' || root?.kind === '$parameters') {
960
+ return traverseExpr.STOP;
961
+ }
962
+ return null;
963
+ }
964
+
965
+ /**
966
+ * Rewrite the reference `ref` with first elem/mixin ref item `item` for user
967
+ * `assoc` (the query element), `elem` is the first (or preferred) query element
968
+ * for item.
969
+ */
917
970
  function rewritePath( ref, item, assoc, elem, location ) {
918
971
  const { path } = ref;
919
972
  const root = path[0];
@@ -928,9 +981,9 @@ function tweakAssocs( model ) {
928
981
  delete root._navigation;
929
982
  setArtifactLink( root, elem );
930
983
  setArtifactLink( ref, elem );
931
- return;
984
+ return true; // ERROR
932
985
  }
933
- if (item !== root) {
986
+ if (item !== root) { // TableAlias.item, $self.item
934
987
  // e.g. mixin ON-condition: Base.foo -> $self.foo or multi-path projection,
935
988
  // $projection -> $self
936
989
  root.id = '$self';
@@ -948,7 +1001,7 @@ function tweakAssocs( model ) {
948
1001
  setLink( root, '_navigation', elem );
949
1002
  }
950
1003
  if (!elem.name) // nothing to do for own $projection, $projection.elem
951
- return; // (except having it renamed to $self)
1004
+ return false; // (except having it renamed to $self)
952
1005
  item.id = elem.name.id;
953
1006
  let state = null;
954
1007
  for (const i of path) {
@@ -964,6 +1017,7 @@ function tweakAssocs( model ) {
964
1017
  }
965
1018
  if (state !== true)
966
1019
  setArtifactLink( ref, state );
1020
+ return false;
967
1021
  }
968
1022
 
969
1023
  function prependSelfToPath( path, elem ) {
@@ -1049,24 +1103,6 @@ function tweakAssocs( model ) {
1049
1103
  }
1050
1104
  }
1051
1105
 
1052
- function navProjection( navigation, preferred ) {
1053
- // TODO: Info if more than one possibility?
1054
- if (!navigation)
1055
- return {};
1056
-
1057
- if (!navigation._projections && !navigation._complexProjections)
1058
- return null;
1059
-
1060
- // _complexProjections contains projections that are not "simple",
1061
- // i.e. contain a filter or arguments. Only used if it contains our
1062
- // preferred association.
1063
- if (preferred && ( navigation._complexProjections?.includes( preferred ) ||
1064
- navigation._projections?.includes( preferred )))
1065
- return preferred;
1066
-
1067
- return navigation._projections?.[0] || null;
1068
- }
1069
-
1070
1106
  function findRewriteTarget( expr, index, env, user ) {
1071
1107
  if (env.kind === '$navElement' || env.kind === '$tableAlias') {
1072
1108
  const r = firstProjectionForPath( expr.path, index, env, user );
@@ -1216,7 +1252,7 @@ function followNavigationPath( path, nav ) {
1216
1252
  * - $projection.elem -> also $self.item -> { item: path[1], tableAlias: $self }
1217
1253
  * - $self -> { item: undefined, tableAlias: $self }
1218
1254
  * - $parameters.P, :P -> {}
1219
- * - $now, current_date -> {}
1255
+ * - $now -> {}
1220
1256
  * - undef, redef -> {}
1221
1257
  * With 'navigation': store that navigation._artifact is projected
1222
1258
  * With 'navigation': rewrite its ON condition
@@ -1245,4 +1281,26 @@ function pathNavigation( ref ) {
1245
1281
  return { navigation: root.elements[item.id], item, tableAlias: root };
1246
1282
  }
1247
1283
 
1284
+ /**
1285
+ * Return the first (or preferred) query elements which projections the navigation
1286
+ * element `navigation` (i.e. source element belonging to a specific table alias).
1287
+ */
1288
+ function navProjection( navigation, preferred ) {
1289
+ // TODO: Info if more than one possibility?
1290
+ if (!navigation)
1291
+ return {};
1292
+
1293
+ if (!navigation._projections && !navigation._complexProjections)
1294
+ return null;
1295
+
1296
+ // _complexProjections contains projections that are not "simple",
1297
+ // i.e. contain a filter or arguments. Only used if it contains our
1298
+ // preferred association.
1299
+ if (preferred && ( navigation._complexProjections?.includes( preferred ) ||
1300
+ navigation._projections?.includes( preferred )))
1301
+ return preferred;
1302
+
1303
+ return navigation._projections?.[0] || null;
1304
+ }
1305
+
1248
1306
  module.exports = tweakAssocs;