@sap/cds-compiler 6.3.6 → 6.4.6
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.
- package/CHANGELOG.md +101 -3
- package/LICENSE +32 -0
- package/README.md +14 -2
- package/bin/cdsse.js +0 -3
- package/doc/CHANGELOG_BETA.md +1 -1
- package/doc/CHANGELOG_DEPRECATED.md +1 -1
- package/lib/base/message-registry.js +9 -2
- package/lib/base/messages.js +1 -1
- package/lib/base/model.js +2 -0
- package/lib/checks/existsExpressionsOnlyForeignKeys.js +16 -10
- package/lib/checks/existsMustEndInAssoc.js +1 -1
- package/lib/checks/existsMustNotStartWithDollarSelf.js +31 -0
- package/lib/checks/validator.js +4 -2
- package/lib/compiler/assert-consistency.js +3 -2
- package/lib/compiler/builtins.js +5 -6
- package/lib/compiler/checks.js +37 -26
- package/lib/compiler/define.js +1 -1
- package/lib/compiler/extend.js +39 -50
- package/lib/compiler/finalize-parse-cdl.js +1 -1
- package/lib/compiler/lsp-api.js +1 -1
- package/lib/compiler/populate.js +2 -2
- package/lib/compiler/propagator.js +29 -6
- package/lib/compiler/resolve.js +13 -3
- package/lib/compiler/shared.js +157 -133
- package/lib/compiler/tweak-assocs.js +87 -29
- package/lib/compiler/xpr-rewrite.js +164 -160
- package/lib/edm/annotations/edmJson.js +206 -37
- package/lib/edm/csn2edm.js +13 -0
- package/lib/edm/edmUtils.js +2 -2
- package/lib/gen/BaseParser.js +106 -72
- package/lib/gen/CdlGrammar.checksum +1 -1
- package/lib/gen/CdlParser.js +1501 -1509
- package/lib/json/to-csn.js +8 -5
- package/lib/language/genericAntlrParser.js +0 -0
- package/lib/main.js +19 -16
- package/lib/model/csnRefs.js +589 -521
- package/lib/model/csnUtils.js +8 -5
- package/lib/model/enrichCsn.js +1 -0
- package/lib/parsers/AstBuildingParser.js +73 -28
- package/lib/render/toCdl.js +2 -1
- package/lib/render/toHdbcds.js +6 -3
- package/lib/render/toSql.js +5 -0
- package/lib/transform/db/applyTransformations.js +1 -1
- package/lib/transform/db/assertUnique.js +4 -1
- package/lib/transform/db/assocsToQueries/transformExists.js +3 -10
- package/lib/transform/db/assocsToQueries/utils.js +0 -5
- package/lib/transform/db/cdsPersistence.js +17 -18
- package/lib/transform/db/expansion.js +179 -3
- package/lib/transform/db/flattening.js +16 -5
- package/lib/transform/db/rewriteCalculatedElements.js +79 -283
- package/lib/transform/effective/main.js +8 -1
- package/lib/transform/forOdata.js +1 -1
- package/lib/transform/forRelationalDB.js +21 -80
- package/lib/transform/localized.js +75 -127
- package/lib/transform/odata/foreignKeyRefsInXprAnnos.js +89 -63
- package/lib/transform/transformUtils.js +23 -21
- package/lib/transform/translateAssocsToJoins.js +7 -5
- package/lib/transform/tupleExpansion.js +16 -3
- package/package.json +3 -3
- package/doc/DeprecatedOptions_v2.md +0 -150
- package/doc/NameResolution.md +0 -837
- package/lib/transform/parseExpr.js +0 -415
package/lib/model/csnUtils.js
CHANGED
|
@@ -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
|
-
|
|
180
|
-
if (!existingMap[elementName])
|
|
181
|
-
existingMap[elementName] = [];
|
|
182
|
-
|
|
181
|
+
existingMap[elementName] ??= [];
|
|
183
182
|
existingMap[elementName].push({
|
|
184
|
-
element
|
|
183
|
+
element: elements[elementName],
|
|
184
|
+
name: elementName,
|
|
185
|
+
source: $location,
|
|
186
|
+
parent,
|
|
187
|
+
errorParent,
|
|
185
188
|
});
|
|
186
189
|
}
|
|
187
190
|
|
package/lib/model/enrichCsn.js
CHANGED
|
@@ -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
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
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 = {
|
|
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 =
|
|
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_
|
|
228
|
-
|
|
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`
|
|
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
|
|
237
|
-
|
|
238
|
-
|
|
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,
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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;
|
|
@@ -653,7 +696,7 @@ class AstBuildingParser extends BaseParser {
|
|
|
653
696
|
|
|
654
697
|
ruleTokensText() {
|
|
655
698
|
let tokenIdx = this.stack.at(-1).tokenIdx + 1;
|
|
656
|
-
const stop = this.tokenIdx
|
|
699
|
+
const stop = this.tokenIdx;
|
|
657
700
|
|
|
658
701
|
let { text: result, location: prev } = this.tokens[tokenIdx];
|
|
659
702
|
while (++tokenIdx < stop) {
|
|
@@ -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;
|
package/lib/render/toCdl.js
CHANGED
|
@@ -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 ||
|
|
2261
|
+
if (cardinality || hasFilter) {
|
|
2261
2262
|
if (filter.endsWith(']')) // for cases such as [… ![id] ]
|
|
2262
2263
|
result += `[ ${ cardinality }${ filter } ]`;
|
|
2263
2264
|
else
|
package/lib/render/toHdbcds.js
CHANGED
|
@@ -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
|
|
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
|
package/lib/render/toSql.js
CHANGED
|
@@ -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,
|
|
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
|
|
@@ -283,7 +283,7 @@ function handleExists( csn, options, messageFunctions, csnUtils ) {
|
|
|
283
283
|
|
|
284
284
|
newExpr.push('exists');
|
|
285
285
|
if (ref?.where) {
|
|
286
|
-
const remappedWhere = remapExistingWhere(target, ref.where
|
|
286
|
+
const remappedWhere = remapExistingWhere( target, ref.where );
|
|
287
287
|
subselect.SELECT.where.push('and');
|
|
288
288
|
if (remappedWhere.length > 3)
|
|
289
289
|
subselect.SELECT.where.push( { xpr: remappedWhere } );
|
|
@@ -361,21 +361,14 @@ function handleExists( csn, options, messageFunctions, csnUtils ) {
|
|
|
361
361
|
*
|
|
362
362
|
* This function does this by adding the assoc target before all the refs so that the refs are resolvable in the WHERE.
|
|
363
363
|
*
|
|
364
|
-
* This function also rejects $self paths in filter conditions.
|
|
365
|
-
*
|
|
366
364
|
* @param {string} target
|
|
367
365
|
* @param {TokenStream} where
|
|
368
|
-
* @param {CSN.Path} path path to the part, used if error needs to be thrown
|
|
369
|
-
* @param {CSN.Artifact} parent the host of the `where`, used if error needs to be thrown
|
|
370
366
|
*
|
|
371
367
|
* @returns {TokenStream} where The input-where with the refs transformed to absolute ones
|
|
372
368
|
*/
|
|
373
|
-
function remapExistingWhere( target, where
|
|
369
|
+
function remapExistingWhere( target, where ) {
|
|
374
370
|
return where.map((part) => {
|
|
375
|
-
if (part.$scope
|
|
376
|
-
error('ref-unexpected-self', path, { '#': 'exists-filter', elemref: parent, id: part.ref[0] });
|
|
377
|
-
}
|
|
378
|
-
else if (part.ref && part.$scope !== '$magic') {
|
|
371
|
+
if (part.ref && part.$scope !== '$magic') {
|
|
379
372
|
part.ref = [ target, ...part.ref ];
|
|
380
373
|
return part;
|
|
381
374
|
}
|
|
@@ -145,11 +145,6 @@ function getHelpers( csn, inspectRef, error ) {
|
|
|
145
145
|
* @returns {object[]} The stuff to add to the where
|
|
146
146
|
*/
|
|
147
147
|
function translateManagedAssocToWhere( root, target, isPrefixedWithTableAlias, base, current ) {
|
|
148
|
-
if (current.$scope === '$self') {
|
|
149
|
-
error('ref-unexpected-self', current.$path, { '#': 'exists', id: current.ref[0], name: 'exists' });
|
|
150
|
-
return [];
|
|
151
|
-
}
|
|
152
|
-
|
|
153
148
|
const whereExtension = [];
|
|
154
149
|
for (let j = 0; j < root.keys.length; j++) {
|
|
155
150
|
const lop = { ref: [ target, ...root.keys[j].ref ] }; // target side
|
|
@@ -6,7 +6,7 @@ const {
|
|
|
6
6
|
isPersistedOnDatabase,
|
|
7
7
|
hasPersistenceSkipAnnotation,
|
|
8
8
|
} = require('../../model/csnUtils');
|
|
9
|
-
const
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
};
|