@sap/cds-compiler 6.3.4 → 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 +54 -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 +26 -7
  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
@@ -87,6 +87,8 @@ function getUtils( model, universalReady ) {
87
87
  * - merge all queries in case of joins
88
88
  * - follow subqueries
89
89
  *
90
+ * NOTE: Use `csnRefs.queryForElements()` instead! See e.g. expandWildcard()
91
+ *
90
92
  * @param {CSN.Query} query Query to check
91
93
  * @param {boolean} [isSubquery]
92
94
  * @returns {object} Map of sources
@@ -176,12 +178,13 @@ function getUtils( model, universalReady ) {
176
178
  */
177
179
  function mergeElementsIntoMap( existingMap, elements, $location, parent, errorParent ) {
178
180
  for (const elementName in elements) {
179
- const element = elements[elementName];
180
- if (!existingMap[elementName])
181
- existingMap[elementName] = [];
182
-
181
+ existingMap[elementName] ??= [];
183
182
  existingMap[elementName].push({
184
- element, name: elementName, source: $location, parent, errorParent,
183
+ element: elements[elementName],
184
+ name: elementName,
185
+ source: $location,
186
+ parent,
187
+ errorParent,
185
188
  });
186
189
  }
187
190
 
@@ -668,8 +671,7 @@ function forAllQueries( query, queryCallback, path ) {
668
671
  const expr = query[prop];
669
672
  if (expr && typeof expr === 'object') {
670
673
  if (Array.isArray(expr)) {
671
- for (let i = 0; i < expr.length; i++)
672
- forAllQueries(expr[i], queryCallback, [ ...path, prop, i ]);
674
+ traverseStructurizedExpression(expr, [ ...path, prop ]);
673
675
  }
674
676
  else {
675
677
  for (const argName of Object.keys( expr ))
@@ -677,6 +679,23 @@ function forAllQueries( query, queryCallback, path ) {
677
679
  }
678
680
  }
679
681
  }
682
+
683
+ /**
684
+ * Traverse the possibly structured xpr. A structured expression may contain arrays inside arrays
685
+ * to show precedence of operators.
686
+ *
687
+ * @param {any} xpr
688
+ * @param {CSN.Path} csnPath
689
+ */
690
+ function traverseStructurizedExpression( xpr, csnPath ) {
691
+ if (Array.isArray(xpr)) {
692
+ for (let i = 0; i < xpr.length; i++)
693
+ traverseStructurizedExpression(xpr[i], [ ...csnPath, i ]);
694
+ }
695
+ else {
696
+ forAllQueries(xpr, queryCallback, csnPath);
697
+ }
698
+ }
680
699
  }
681
700
 
682
701
  /**
@@ -61,6 +61,7 @@ function enrichCsn( csn, options = {} ) {
61
61
  target: simpleRef,
62
62
  includes: simpleRef,
63
63
  $origin,
64
+ $calc: standard,
64
65
  // TODO: excluding
65
66
  '@': assignment,
66
67
  $: () => { /* ignore properties like $location for performance */ },
@@ -8,6 +8,13 @@ const { functionsWithoutParentheses } = require('./identifiers');
8
8
 
9
9
  const { pathName } = require('../compiler/utils');
10
10
  const { quotedLiteralPatterns, specialFunctions } = require('../compiler/builtins');
11
+
12
+ const { parseMultiLineStringLiteral } = require('../language/multiLineStringParser');
13
+ const { normalizeNewLine, normalizeNumberString } = require('../language/textUtils');
14
+ const { parseDocComment } = require('../language/docCommentParser');
15
+
16
+ const $location = Symbol.for('cds.$location');
17
+
11
18
  const parserTokens = { // TODO: precompile into specialFunction
12
19
  __proto__: null,
13
20
  GenericIntro: 'intro',
@@ -15,11 +22,23 @@ const parserTokens = { // TODO: precompile into specialFunction
15
22
  GenericSeparator: 'separator',
16
23
  };
17
24
 
18
- const { parseMultiLineStringLiteral } = require('../language/multiLineStringParser');
19
- const { normalizeNewLine, normalizeNumberString } = require('../language/textUtils');
20
- const { parseDocComment } = require('../language/docCommentParser');
21
-
22
- const $location = Symbol.for('cds.$location');
25
+ // if the expectedArray contains all the following tokens, we replace them by `Value`
26
+ const valueTokens = {
27
+ Id: true,
28
+ Number: true,
29
+ QuotedLiteral: true,
30
+ String: true,
31
+ ':': true,
32
+ '#': true,
33
+ '+': true,
34
+ '-': true,
35
+ cast: true, // is special function
36
+ false: true,
37
+ new: true, // is too seldomly really useful
38
+ null: true,
39
+ true: true,
40
+ };
41
+ const valueTokensLength = Object.values( valueTokens ).length;
23
42
 
24
43
  const extensionDicts = {
25
44
  elements: true, enum: true, params: true, returns: true,
@@ -82,30 +101,38 @@ class AstBuildingParser extends BaseParser {
82
101
  return this.$messageFunctions.info( id, location?.location || location, args, text );
83
102
  }
84
103
 
85
- expectingArray() {
104
+ expectingArray( raw = false ) {
86
105
  const savedState = this.s;
87
106
  this.s = this.errorState;
88
107
  let array = this.expectingArray_();
89
108
  this.s = savedState;
90
- // compatibility: replace true+false by Boolean - TODO: delete
91
- if (array.includes( 'true' ))
92
- array = [ 'Boolean', ...array.filter( n => n !== 'true' && n !== 'false' ) ];
109
+ // Avoid clutter in messages: replace all value tokens by ‹Value› if all are there:
110
+ const reduced = raw ? array : array.filter( tok => valueTokens[tok] !== true );
111
+ if (reduced.length + valueTokensLength === array.length)
112
+ array = [ 'Value', ...reduced ];
93
113
  return array.map( tok => this.antlrName( tok ) )
94
114
  .sort( (a, b) => (tokenPrecedence(a) < tokenPrecedence(b) ? -1 : 1) );
95
115
  }
96
116
 
97
- reportUnexpectedToken_() {
98
- const token = this.la();
99
- const args = { offending: this.antlrName( token ), expecting: this.expectingArray() };
117
+ reportUnexpectedToken_( msgKind ) {
118
+ const token = (msgKind === 'reuse') ? this.lb() : this.la();
119
+ const args = {
120
+ offending: this.antlrName( token ),
121
+ expecting: (msgKind === 'reuse' ? undefined : this.expectingArray()),
122
+ };
100
123
  const errorMethod = this.conditionTokenIdx === this.tokenIdx &&
101
124
  this[`${ this.conditionName }Error`];
102
125
  let err = errorMethod && errorMethod.call( this, args, token );
103
126
  // TODO: should we set the msg variant always? (→ no nestedExpandError necessary)
104
127
  if (errorMethod && !err)
105
128
  args['#'] ??= this.conditionName;
129
+ if (!errorMethod && msgKind === 'reuse') {
130
+ err = this.error( 'syntax-missing-input', token, { keyword: token.keyword },
131
+ 'Missing input before keyword $(KEYWORD)' );
132
+ }
106
133
  err ||= this.error( 'syntax-unexpected-token', token, args );
107
134
  // No 'unwanted' variant, no 'syntax-missing-token'
108
- err.expectedTokens = args.expecting;
135
+ err.expectedTokens = this.expectingArray( true );
109
136
  }
110
137
 
111
138
  reportReservedWord_() {
@@ -224,29 +251,39 @@ class AstBuildingParser extends BaseParser {
224
251
  }
225
252
 
226
253
  inSelectItem( arg ) { // <prepare=…>
227
- this.dynamic_.inSelectItem = arg ||
228
- (this.tokens[this.tokenIdx - 2].type === '.' ? 'inline' : 'expand');
254
+ if (!Object.hasOwn(this.dynamic_, 'inSelectItem')) {
255
+ // This copies the entries from the lower prototype into the current dynamic_ object.
256
+ this.dynamic_.inSelectItem ??= [];
257
+ this.dynamic_.inSelectItem = [ ...this.dynamic_.inSelectItem ];
258
+ }
259
+ this.dynamic_.inSelectItem.push(arg ||
260
+ (this.tokens[this.tokenIdx - 2].type === '.' ? 'inline' : 'expand'));
229
261
  }
230
262
 
231
263
  /**
232
- * `virtual` and `key` cannot be used inside expand/inline
264
+ * `virtual` cannot be used inside expand/inline.
265
+ * `key` can only be used in inlines, but not expands.
233
266
  * (also inside sub queries in those, which will be rejected later anyway)
234
267
  */
235
268
  modifierRestriction() {
236
- const { inSelectItem } = this.dynamic_;
237
- // TODO: really reject for top-level "inline"?
238
- return inSelectItem === 'expand' || inSelectItem === 'inline';
269
+ const top = this.dynamic_.inSelectItem?.at(-1);
270
+ const insideExpand = this.dynamic_.inSelectItem?.includes('expand');
271
+ const next = this.tokens[this.tokenIdx]?.keyword;
272
+ return insideExpand || (next !== 'key' && top === 'inline');
239
273
  }
240
274
  modifierRestrictionError( args, offending ) {
241
- return this.error( 'syntax-unexpected-modifier', offending, args,
242
- // TODO: we would have text variant for expand or inline,
243
- // but we probably allow `key` in nested top-level inline
244
- 'Unexpected $(OFFENDING) in nested expand/inline, expecting $(EXPECTING)' );
275
+ return this.error( 'syntax-unexpected-modifier', offending, {
276
+ '#': offending.keyword || 'std', ...args,
277
+ }, {
278
+ // TODO: we would have text variant for expand or inline
279
+ std: 'Unexpected $(OFFENDING) in nested expand/inline, expecting $(EXPECTING)',
280
+ key: 'Unexpected $(OFFENDING) in nested expand, expecting $(EXPECTING)',
281
+ } );
245
282
  }
246
283
 
247
284
  isDotForPath() { // see also inSelectItem
248
285
  // TODO: also consider whether we are in the <prefer>ed `valuePath` branch
249
- if (this.dynamic_.inSelectItem == null)
286
+ if (this.dynamic_.inSelectItem == null || this.dynamic_.inSelectItem.length === 0)
250
287
  return false;
251
288
  // TODO: it would be best to set this.dynamic_.inSelectItem to null in filters
252
289
  // (as <prepare>)
@@ -643,9 +680,15 @@ class AstBuildingParser extends BaseParser {
643
680
  if (!art)
644
681
  return art;
645
682
  art.location ??= this.startLocation();
646
- if (this.s == null) // do not set end location if error
647
- return art;
648
- const { location } = this.lb();
683
+ let last = this.lb();
684
+ if (this.s == null) {
685
+ let { tokenIdx } = this;
686
+ if (tokenIdx === this.stack.at(-1).tokenIdx)
687
+ return art;
688
+ while (!last.parsedAs)
689
+ last = this.tokens[--tokenIdx];
690
+ }
691
+ const { location } = last;
649
692
  art.location.endLine = location.endLine;
650
693
  art.location.endCol = location.endCol;
651
694
  return art;
@@ -1526,6 +1569,8 @@ class AstBuildingParser extends BaseParser {
1526
1569
  }
1527
1570
 
1528
1571
  AstBuildingParser.prototype.queryOnLeftSloppyAlias.afterError = true;
1572
+ AstBuildingParser.prototype.isNamedArg.afterError = true;
1573
+ // TODO: can we somehow _add_ '=>' and ':' to expected set if next token fails ?
1529
1574
 
1530
1575
  function addOneForDefinition( count, ext ) {
1531
1576
  return (ext.kind === 'extend') ? count : count + 1;
@@ -2255,9 +2255,10 @@ class CsnToCdl {
2255
2255
  filter = `${ where } ${ filter }`;
2256
2256
  }
2257
2257
 
2258
+ const hasFilter = filter.length > 0; // filter may be empty after trimming
2258
2259
  filter = filter.trim();
2259
2260
 
2260
- if (cardinality || filter) {
2261
+ if (cardinality || hasFilter) {
2261
2262
  if (filter.endsWith(']')) // for cases such as [… ![id] ]
2262
2263
  result += `[ ${ cardinality }${ filter } ]`;
2263
2264
  else
@@ -4,7 +4,7 @@ const {
4
4
  getLastPartOf, getLastPartOfRef,
5
5
  hasValidSkipOrExists, getNormalizedQuery,
6
6
  getRootArtifactName, getResultingName, getNamespace, forEachMember, getVariableReplacement,
7
- pathName, hasPersistenceSkipAnnotation,
7
+ pathName, hasPersistenceSkipAnnotation, implicitAs,
8
8
  } = require('../model/csnUtils');
9
9
  const { isBuiltinType, isMagicVariable } = require('../base/builtins');
10
10
  const keywords = require('../base/keywords');
@@ -735,7 +735,7 @@ function toHdbcdsSource( csn, options, messageFunctions ) {
735
735
  * @returns {string} Rendered column
736
736
  */
737
737
  function renderViewColumn( col, elements, env ) {
738
- const leaf = col.as || col.ref && col.ref[col.ref.length - 1] || col.func;
738
+ const leaf = col.as || col.ref?.[col.ref.length - 1] || col.func;
739
739
  const element = elements[leaf];
740
740
 
741
741
  // Render 'null as <alias>' only for database and if element is virtual
@@ -746,7 +746,6 @@ function toHdbcdsSource( csn, options, messageFunctions ) {
746
746
  }
747
747
  return renderNonVirtualColumn();
748
748
 
749
-
750
749
  function renderNonVirtualColumn() {
751
750
  let result = env.indent;
752
751
  // only if column is virtual, keyword virtual was present in the source text
@@ -756,6 +755,10 @@ function toHdbcdsSource( csn, options, messageFunctions ) {
756
755
  if (col.key && env.skipKeys)
757
756
  error(null, env.path, { keyword: 'key', $reviewed: true }, 'Unexpected $(KEYWORD) in subquery');
758
757
 
758
+ // Since magic variables are replaced, we may need to create an alias if there isn't one.
759
+ if (!col.as && col.ref?.[0] && isMagicVariable(pathId(col.ref?.[0])))
760
+ col.as = implicitAs(col.ref);
761
+
759
762
  const key = (!env.skipKeys && (col.key || element?.key) ? 'key ' : '');
760
763
  result += key + renderExpr(withoutCast(col), env);
761
764
  let alias = col.as || (!col.args && col.func); // func: e.g. CURRENT_TIMESTAMP
@@ -6,6 +6,7 @@ const {
6
6
  hasValidSkipOrExists, getNormalizedQuery,
7
7
  forEachDefinition, getResultingName,
8
8
  getVariableReplacement, pathName,
9
+ implicitAs,
9
10
  } = require('../model/csnUtils');
10
11
  const { isBuiltinType, isMagicVariable } = require('../base/builtins');
11
12
  const { forEach, forEachValue, forEachKey } = require('../utils/objectUtils');
@@ -1178,6 +1179,10 @@ function toSqlDdl( csn, options, messageFunctions ) {
1178
1179
  result += `${ env.indent }NULL AS ${ quoteSqlId(col.as || leaf) }`;
1179
1180
  }
1180
1181
  else {
1182
+ // Since magic variables are replaced, we may need to create an alias if there isn't one.
1183
+ if (!col.as && col.ref?.[0] && isMagicVariable(pathId(col.ref?.[0])))
1184
+ col.as = implicitAs(col.ref);
1185
+
1181
1186
  result = env.indent + renderExpr(withoutCast(col), env);
1182
1187
  if (col.as)
1183
1188
  result += ` AS ${ quoteSqlId(col.as) }`;
@@ -30,7 +30,7 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
30
30
  if (!options.skipStandard)
31
31
  options.skipStandard = { $tableConstraints: true };
32
32
  else if (options.skipStandard.$tableConstraints === undefined)
33
- options.skipStandard = { ...options.skipStandard, ...{ $tableConstraints: true } };
33
+ options.skipStandard = { ...options.skipStandard, $tableConstraints: true };
34
34
 
35
35
  const transformers = {
36
36
  elements: dictionary,
@@ -67,7 +67,10 @@ function processAssertUnique( csn, options, messageFunctions ) {
67
67
  pathObjects = pathObjects.filter(p => p._art);
68
68
  // 7) Flatten correct paths, check and clean again
69
69
  let flattenedPathObjects = [];
70
- pathObjects.forEach(p => flattenedPathObjects.push(...flattenPath(p, true)));
70
+ pathObjects.forEach(p => flattenedPathObjects.push(...flattenPath(p, true).map((f) => {
71
+ delete f.comparisonRef;
72
+ return f;
73
+ })));
71
74
  flattenedPathObjects.forEach(p => check(p, propName));
72
75
  flattenedPathObjects = flattenedPathObjects.filter(p => p._art);
73
76
  // 8) Duplicate path check on final flattened paths to detect structural overlaps
@@ -6,7 +6,7 @@ const {
6
6
  isPersistedOnDatabase,
7
7
  hasPersistenceSkipAnnotation,
8
8
  } = require('../../model/csnUtils');
9
- const transformUtils = require('../transformUtils');
9
+ const { recurseElements } = require('../transformUtils');
10
10
 
11
11
  /**
12
12
  * Return a callback function for forEachDefinition that marks artifacts that are abstract or @cds.persistence.exists/skip
@@ -105,9 +105,6 @@ function getAssocToSkippedIgnorer( csn, options, messageFunctions, csnUtils ) {
105
105
  */
106
106
  function getPersistenceTableProcessor( csn, options, messageFunctions ) {
107
107
  const { error } = messageFunctions;
108
- const {
109
- recurseElements,
110
- } = transformUtils.getTransformers(csn, options, messageFunctions, '_');
111
108
 
112
109
  return handleQueryish;
113
110
 
@@ -117,20 +114,22 @@ function getPersistenceTableProcessor( csn, options, messageFunctions ) {
117
114
  * @param {string} artifactName
118
115
  */
119
116
  function handleQueryish( artifact, artifactName ) {
120
- const stripQueryish = artifact.query && artifact['@cds.persistence.table'];
121
-
122
- if (stripQueryish) {
123
- artifact.kind = 'entity';
124
- delete artifact.query;
125
-
126
- recurseElements(artifact, [ 'definitions', artifactName ], (member, path) => {
127
- // All elements must have a type for this to work
128
- if (!member.$ignore && !member.kind && !member.type && !member.elements) { // .items? Probably resolved at this point
129
- error(null, path, { anno: '@cds.persistence.table' },
130
- 'Expecting element to have a type if view is annotated with $(ANNO)');
131
- }
132
- });
133
- }
117
+ const stripQueryish = (artifact.query || artifact.projection) && artifact['@cds.persistence.table'];
118
+
119
+ if (!stripQueryish)
120
+ return;
121
+
122
+ artifact.kind = 'entity';
123
+ delete artifact.query;
124
+ delete artifact.projection;
125
+
126
+ recurseElements(artifact, [ 'definitions', artifactName ], (member, path) => {
127
+ // All elements must have a type for this to work
128
+ if (!member.$ignore && !member.kind && !member.type && !member.elements) { // .items? Probably resolved at this point
129
+ error(null, path, { anno: '@cds.persistence.table' },
130
+ 'Expecting element to have a type if view is annotated with $(ANNO)');
131
+ }
132
+ });
134
133
  }
135
134
  }
136
135
 
@@ -5,12 +5,14 @@ const {
5
5
  setDependencies,
6
6
  walkCsnPath,
7
7
  getUtils,
8
+ forEachDefinition,
8
9
  } = require('../../model/csnUtils');
9
10
  const { implicitAs, columnAlias, pathId } = require('../../model/csnRefs');
10
11
  const { setProp } = require('../../base/model');
11
12
  const { forEach } = require('../../utils/objectUtils');
12
13
  const { killNonrequiredAnno } = require('./killAnnotations');
13
14
  const { featureFlags } = require('../featureFlags');
15
+ const { applyTransformationsOnNonDictionary } = require('./applyTransformations');
14
16
 
15
17
  /**
16
18
  * For keys, columns, groupBy and orderBy, expand structured things.
@@ -28,6 +30,8 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
28
30
 
29
31
  if (options.transformation === 'odata' || csn.meta?.[featureFlags]?.$expandInline)
30
32
  rewriteExpandInline();
33
+ if (options.transformation === 'odata' || csn.meta?.[featureFlags]?.$calculatedElements)
34
+ rewriteRefsInCalcSubElements();
31
35
 
32
36
  throwWithAnyError();
33
37
 
@@ -66,6 +70,59 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
66
70
 
67
71
  applyTransformations(csn, transformers, [], iterateOptions);
68
72
 
73
+ /**
74
+ * Rewrite references in calculated elements that are in sub-elements.
75
+ * We do so, do absolutify any references.
76
+ *
77
+ * TODO: This should not be necessary if `value` of calculated elements were to be
78
+ * rewritten during flattening, i.e. if we would keep track of parent elements
79
+ * and prepend the parent-hierarchy to all references in the calc element.
80
+ */
81
+ function rewriteRefsInCalcSubElements() {
82
+ forEachDefinition(csn, (def, _defName) => {
83
+ if (def.kind === 'entity' && !def.query && !def.projection)
84
+ rewriteInElements(def.elements, []);
85
+ });
86
+
87
+ /**
88
+ * Rewrite the paths in calc elements.
89
+ * @param {object} elements
90
+ * @param {string[]} parents
91
+ */
92
+ function rewriteInElements(elements, parents ) {
93
+ for (const elemName in elements) {
94
+ const element = elements[elemName];
95
+ if (element.elements)
96
+ rewriteInElements(element.elements, parents.concat(elemName));
97
+ else
98
+ rewriteInElement(elemName, elements[elemName], parents);
99
+ }
100
+ }
101
+
102
+ /**
103
+ *
104
+ * @param {string} elemName
105
+ * @param {object} element
106
+ * @param {string[]} parents
107
+ */
108
+ function rewriteInElement(elemName, element, parents) {
109
+ if (!element.value)
110
+ return;
111
+ if (parents.length === 0)
112
+ return; // don't rewrite in top-level elements
113
+
114
+ applyTransformationsOnNonDictionary(element, 'value', {
115
+ ref: (path, _name) => {
116
+ if (path.$scope === 'parent') {
117
+ path.ref.unshift('$self', ...parents);
118
+ setProp(path, '$scope', '$self');
119
+ delete path._links;
120
+ }
121
+ },
122
+ });
123
+ }
124
+ }
125
+
69
126
  /**
70
127
  * Turn .expand/.inline into normal refs. @cds.persistence.skip .expand with to-many (and all transitive views).
71
128
  * For such skipped things, error for usage of assoc pointing to them and ignore publishing of assoc pointing to them.
@@ -90,8 +147,8 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
90
147
  });
91
148
  const rewritten = rewrite(root, parent.columns, parent.excluding);
92
149
  /*
93
- * Do not remove unexpandable many columns in OData
94
- */
150
+ * Do not remove unexpandable many columns in OData
151
+ */
95
152
  if (rewritten.toMany.length > 0 && !options.toOdata) {
96
153
  markAsToDummify(artifact, path[1]);
97
154
  rewritten.toMany.forEach(({ art }) => {
@@ -719,7 +776,7 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
719
776
  * @param {object} base The raw set of things a * can expand to
720
777
  * @param {Array} subs Things - the .expand/.inline or .columns
721
778
  * @param {string[]} [excluding=[]]
722
- * @param {boolean} [isComplexQuery=false] Wether the query is a single source select or something more complex
779
+ * @param {boolean} [isComplexQuery=false] Whether the query is a single source select or something more complex
723
780
  * @returns {Array} If there was a star, expand it and handle shadowing/excluding, else just return subs
724
781
  */
725
782
  function replaceStar( base, subs, excluding = [], isComplexQuery = false ) {
@@ -777,6 +834,125 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
777
834
  }
778
835
  }
779
836
 
837
+
838
+ /**
839
+ * Expands the asterisks '*' a.k.a. wildcard in a query.
840
+ * Does not expand it inside expands/inline, though.
841
+ *
842
+ * @param {object} query
843
+ * @param {object} csnUtils
844
+ * @param {object} options
845
+ */
846
+ function expandWildcard( query, csnUtils, options ) {
847
+ if (!query.SELECT && !query.projection)
848
+ return; // e.g. query source node inside `from`
849
+
850
+ const SELECT = query.SELECT ?? query.projection;
851
+ SELECT.columns ??= [ '*' ]; // no columns -> implicit wildcard
852
+
853
+ const wildcardIndex = SELECT.columns.indexOf('*');
854
+ if (wildcardIndex === -1)
855
+ return;
856
+
857
+ const allElements = wildcardElements(query, csnUtils);
858
+ const columns = Object.keys(allElements).map( (elementName) => {
859
+ const elem = allElements[elementName];
860
+ if (elem.replacedByColumn) {
861
+ SELECT.columns.splice(SELECT.columns.indexOf(elem.replacedByColumn), 1);
862
+ return elem.replacedByColumn;
863
+ }
864
+
865
+ const column = { ref: [ elementName ] };
866
+ if (elem.alias) {
867
+ // Special case for `for.effective` to reduce diffs and unnecessary table aliases.
868
+ const requiresAlias = options.transformation !== 'effective' ||
869
+ elementName === elem.alias ||
870
+ elementName.charAt(0) === '$' ||
871
+ (!query.projection && !query.SELECT?.from?.ref);
872
+
873
+ if (requiresAlias)
874
+ column.ref.unshift(elem.alias); // use table alias if available and required
875
+ }
876
+ return column;
877
+ });
878
+
879
+ SELECT.columns.splice(SELECT.columns.indexOf('*'), 1, ...columns);
880
+ // since we've expanded '*', the excluding clause has no effect anymore
881
+ delete SELECT.excluding;
882
+ }
883
+
884
+
885
+ /**
886
+ * Get all elements that are expanded by '*'.
887
+ * Respects the query's 'excluding' clause and 'masked' elements.
888
+ * Elements that are replaced by columns have a `replacedByColumn` property.
889
+ *
890
+ * @param {object} query Query with SELECT/projection.
891
+ * @param {object} csnUtils
892
+ * @returns {object} All elements which '*' expands to.
893
+ */
894
+ function wildcardElements( query, csnUtils ) {
895
+ const combined = aliasesToCombined(csnUtils.queryForElements(query));
896
+ const SELECT = query.SELECT ?? query.projection;
897
+ for (const excluded of SELECT.excluding ?? [])
898
+ delete combined[excluded];
899
+
900
+ // Handle deprecated 'masked' keyword.
901
+ for (const name in combined) {
902
+ combined[name] = combined[name].filter(sourceElement => !sourceElement.element?.masked);
903
+ if (combined[name].length === 0)
904
+ delete combined[name];
905
+ else
906
+ combined[name] = combined[name][0]; // flatten
907
+ }
908
+
909
+ let seenStar = false;
910
+ for (const col of SELECT.columns) {
911
+ if (col === '*') {
912
+ seenStar = true;
913
+ continue;
914
+ }
915
+ if (col.inline) {
916
+ // 'inline' does not result in an element, hence skip it. We can do so, because for
917
+ // something like `*, toSelf.{ id }`, '*' would still expand 'id' which results in
918
+ // a 'duplicate' error. It wouldn't happen for `*, toSelf.id`.
919
+ continue;
920
+ }
921
+ const alias = col.as || col.ref && implicitAs(col.ref);
922
+ if (alias && combined[alias]) {
923
+ if (seenStar)
924
+ combined[alias] = { replacedByColumn: col };
925
+ else
926
+ delete combined[alias];
927
+ }
928
+ }
929
+
930
+ return combined;
931
+ }
932
+
933
+ /**
934
+ * Transforms the result of `csnRefs.queryElements()` into a dictionary like form
935
+ * for further processing by `wildcardElements`.
936
+ *
937
+ * @param {object} queryElements Result of `csnRefs.queryElements()`
938
+ * @returns {object}
939
+ */
940
+ function aliasesToCombined(queryElements) {
941
+ const combined = Object.create(null);
942
+ for (const alias in queryElements.$aliases) {
943
+ const elements = queryElements.$aliases[alias]?.elements;
944
+ for (const elementName in elements) {
945
+ combined[elementName] ??= [];
946
+ const entry = { name: elementName, element: elements[elementName] };
947
+ if (alias.charAt(0) !== '$') // don't set alias for e.g. '$_select_N' internal aliases
948
+ entry.alias = alias;
949
+ combined[elementName].push(entry);
950
+ }
951
+ }
952
+ return combined;
953
+ }
954
+
780
955
  module.exports = {
781
956
  expandStructureReferences,
957
+ expandWildcard,
782
958
  };
@@ -210,7 +210,7 @@ function getStructStepsFlattener( csn, options, messageFunctions, resolved, path
210
210
  const { links, art, scope } = inspectRef(path);
211
211
  const resolvedLinkTypes = resolveLinkTypes(links);
212
212
  setProp(parent, '$path', [ ...path ]);
213
- const lastRef = ref[ref.length - 1];
213
+ const lastRef = ref.at(-1);
214
214
  const fn = (suspend = false, suspendPos = 0,
215
215
  refFilter = _parent => true) => {
216
216
  let refChanged = false;
@@ -226,13 +226,14 @@ function getStructStepsFlattener( csn, options, messageFunctions, resolved, path
226
226
  // Explicitly set implicit alias for things that are now flattened - but only in columns
227
227
  // TODO: Can this be done elegantly during expand phase already?
228
228
  if (parent.$implicitAlias) { // an expanded s -> s.a is marked with this - do not add implicit alias "a" there, we want s_a
229
- if (parent.ref[parent.ref.length - 1] === parent.as) // for a simple s that was expanded - for s.substructure this would not apply
229
+ if (parent.ref.at(-1) === parent.as) // for a simple s that was expanded - for s.substructure this would not apply
230
230
  delete parent.as;
231
231
  delete parent.$implicitAlias;
232
232
  }
233
- // To handle explicitly written s.a - add implicit alias a, since after flattening it would otherwise be s_a
234
- else if (parent.ref[parent.ref.length - 1] !== lastRef &&
235
- (insideColumns(scopedPath) || insideKeys(scopedPath)) &&
233
+ // To handle explicitly written s.a - add implicit alias a, since after flattening it would otherwise be s_a,
234
+ // also for 'from' clauses
235
+ else if (parent.ref.at(-1) !== lastRef &&
236
+ (insideColumns(scopedPath) || insideKeys(scopedPath) || isFromRef(scopedPath)) &&
236
237
  !parent.as) {
237
238
  parent.as = lastRef;
238
239
  }
@@ -276,6 +277,16 @@ function getStructStepsFlattener( csn, options, messageFunctions, resolved, path
276
277
  return path.length >= 3 && path[path.length - 2] === 'keys' && typeof path[path.length - 1] === 'number';
277
278
  }
278
279
 
280
+ /**
281
+ * Whether the given path points to a `from` clause.
282
+ *
283
+ * @param {CSN.Path} path
284
+ * @returns {boolean}
285
+ */
286
+ function isFromRef(path) {
287
+ return path.at(-1) === 'from';
288
+ }
289
+
279
290
  return transformer;
280
291
  }
281
292