@sap/cds-compiler 5.4.4 → 5.5.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.
- package/CHANGELOG.md +17 -1
- package/bin/cds_remove_invalid_whitespace.js +4 -4
- package/bin/cds_update_annotations.js +3 -3
- package/bin/cds_update_identifiers.js +3 -3
- package/lib/api/main.js +18 -30
- package/lib/api/validate.js +6 -1
- package/lib/base/lazyload.js +28 -0
- package/lib/base/location.js +1 -0
- package/lib/base/message-registry.js +47 -11
- package/lib/base/messages.js +17 -3
- package/lib/checks/{dbFeatureFlags.js → featureFlags.js} +1 -1
- package/lib/checks/parameters.js +61 -4
- package/lib/checks/validator.js +14 -6
- package/lib/compiler/index.js +7 -7
- package/lib/gen/BaseParser.js +345 -235
- package/lib/gen/CdlParser.js +4434 -4492
- package/lib/gen/Dictionary.json +2 -2
- package/lib/language/antlrParser.js +2 -111
- package/lib/main.js +16 -37
- package/lib/modelCompare/utils/filter.js +47 -21
- package/lib/parsers/AstBuildingParser.js +59 -49
- package/lib/parsers/CdlGrammar.g4 +91 -130
- package/lib/parsers/index.js +123 -0
- package/lib/render/toSql.js +8 -2
- package/lib/render/utils/delta.js +33 -1
- package/lib/transform/db/{transformExists.js → assocsToQueries/transformExists.js} +12 -407
- package/lib/transform/db/assocsToQueries/utils.js +440 -0
- package/lib/transform/db/expansion.js +2 -2
- package/lib/transform/draft/db.js +14 -3
- package/lib/transform/effective/annotations.js +3 -3
- package/lib/transform/effective/main.js +5 -7
- package/lib/transform/featureFlags.js +5 -0
- package/lib/transform/forRelationalDB.js +125 -192
- package/lib/transform/transformUtils.js +0 -51
- package/package.json +2 -2
- package/lib/transform/db/featureFlags.js +0 -5
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getRealName } = require('../../../render/utils/common');
|
|
4
|
+
const { ModelError } = require('../../../base/error');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Some shared transformation utilities between exists rewriting and general assoc to subselect rewriting
|
|
8
|
+
*
|
|
9
|
+
* @param {CSN.Model} csn
|
|
10
|
+
* @param {Function} inspectRef
|
|
11
|
+
* @param {Function} error
|
|
12
|
+
* @returns {object}
|
|
13
|
+
*/
|
|
14
|
+
function getHelpers( csn, inspectRef, error ) {
|
|
15
|
+
return {
|
|
16
|
+
getBase,
|
|
17
|
+
firstLinkIsEntityOrQuerySource,
|
|
18
|
+
getFirstAssoc,
|
|
19
|
+
translateManagedAssocToWhere,
|
|
20
|
+
getQuerySources,
|
|
21
|
+
translateUnmanagedAssocToWhere,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the name of the source-side query source
|
|
26
|
+
*
|
|
27
|
+
* @param {string | Array | null} queryBase
|
|
28
|
+
* @param {boolean} isPrefixedWithTableAlias
|
|
29
|
+
* @param {CSN.Column} current
|
|
30
|
+
* @param {CSN.Path} path
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
function getBase( queryBase, isPrefixedWithTableAlias, current, path ) {
|
|
34
|
+
if (typeof queryBase === 'string') // alias
|
|
35
|
+
return queryBase;
|
|
36
|
+
else if (queryBase) // ref
|
|
37
|
+
return queryBase.length > 1 ? queryBase[queryBase.length - 1] : getRealName(csn, queryBase[0]);
|
|
38
|
+
else if (isPrefixedWithTableAlias)
|
|
39
|
+
return current.ref[0];
|
|
40
|
+
return getParent(current, path);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* For a given xpr, check in which entity/query source the ref "is".
|
|
45
|
+
*
|
|
46
|
+
* If the ref already starts with an entity/query source, simply return the first ref step.
|
|
47
|
+
* Otherwise, use $env to figure it out:
|
|
48
|
+
* - $env=<string> -> the string is the source
|
|
49
|
+
* - $env=<number> && $scope='mixin' -> the current query is the source
|
|
50
|
+
* - $env=<number> && $scope!=='mixin' -> such refs start with entity/query source, are already handled
|
|
51
|
+
* - $env=true -> does not apply for "EXISTS" handling, only happens in ORDER BY or explicit on-cond redirection
|
|
52
|
+
*
|
|
53
|
+
* If we have a ref but no $env, throw to trigger recompile - but such cases should have already led to a recompile with
|
|
54
|
+
* the validator/enricher.
|
|
55
|
+
*
|
|
56
|
+
* Since we only call this function when it is not just a simple SELECT FROM X,
|
|
57
|
+
* we can be sure that resolving the ref requires $env information.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} xpr
|
|
60
|
+
* @param {CSN.Path} path
|
|
61
|
+
* @returns {string|undefined} undefined in case of errors
|
|
62
|
+
* @throws {Error} Throws if xpr.ref but no xpr.$env
|
|
63
|
+
* @todo $env is going to be removed from CSN, but csnRefs will provide it
|
|
64
|
+
*/
|
|
65
|
+
// eslint-disable-next-line consistent-return
|
|
66
|
+
function getParent( xpr, path ) {
|
|
67
|
+
if (firstLinkIsEntityOrQuerySource(xpr, path)) {
|
|
68
|
+
return xpr.ref[0];
|
|
69
|
+
}
|
|
70
|
+
else if (xpr.$env) {
|
|
71
|
+
if (typeof xpr.$env === 'string') {
|
|
72
|
+
return xpr.$env;
|
|
73
|
+
}
|
|
74
|
+
else if (typeof xpr.$env === 'number') {
|
|
75
|
+
if (xpr.$scope === 'mixin')
|
|
76
|
+
return '';
|
|
77
|
+
return error(null, xpr.$path, '$env with number is not handled yet - report this error!');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return error(null, xpr.$path, 'Boolean $env is not handled yet - report this error!');
|
|
81
|
+
}
|
|
82
|
+
else if (xpr.ref) {
|
|
83
|
+
throw new ModelError('Missing $env and missing leading artifact ref - throwing to trigger recompilation!');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check (using inspectRef -> links), whether the first path step is an entity or query source
|
|
89
|
+
*
|
|
90
|
+
* @param {object} obj
|
|
91
|
+
* @param {CSN.Path} objPath
|
|
92
|
+
* @returns {boolean}
|
|
93
|
+
*/
|
|
94
|
+
function firstLinkIsEntityOrQuerySource( obj, objPath ) {
|
|
95
|
+
const { links } = getLinksAndArt(obj, objPath);
|
|
96
|
+
return links && (links[0].art.kind === 'entity' || links[0].art.query || links[0].art.from);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* From the given expression (having inspectRef -> links), find the first association.
|
|
101
|
+
*
|
|
102
|
+
* @param {object} xprPart
|
|
103
|
+
* @param {CSN.Path} path
|
|
104
|
+
* @returns {{head: Array, root: CSN.Element, ref: string|object, tail: Array}} The first assoc (root), the corresponding ref (ref), anything before the ref (head) and the rest of the ref (tail).
|
|
105
|
+
*/
|
|
106
|
+
function getFirstAssoc( xprPart, path ) {
|
|
107
|
+
const { links, art } = getLinksAndArt({}, path);
|
|
108
|
+
for (let i = 0; i < xprPart.ref.length - 1; i++) {
|
|
109
|
+
if (links[i].art && links[i].art.target) {
|
|
110
|
+
return {
|
|
111
|
+
head: (i === 0 ? [] : xprPart.ref.slice(0, i)), root: links[i].art, ref: xprPart.ref[i], tail: xprPart.ref.slice(i + 1),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
head: (xprPart.ref.length === 1 ? [] : xprPart.ref.slice(0, xprPart.ref.length - 1)), root: art, ref: xprPart.ref[xprPart.ref.length - 1], tail: [],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Translate an `EXISTS <managed assoc>` into a part of a WHERE condition.
|
|
122
|
+
*
|
|
123
|
+
* For each of the foreign keys, do:
|
|
124
|
+
* + build the target side by prefixing `target` in front of the ref
|
|
125
|
+
* + build the source side by prefixing `base` (if not already part of `current`)
|
|
126
|
+
* and the assoc name itself (current) in front of the ref
|
|
127
|
+
* + Compare source and target with `=`
|
|
128
|
+
*
|
|
129
|
+
* If there is more than one foreign key, join with `and`.
|
|
130
|
+
*
|
|
131
|
+
* The new tokens are immediately added to the WHERE of the subselect
|
|
132
|
+
*
|
|
133
|
+
* @param {CSN.Element} root
|
|
134
|
+
* @param {string} target
|
|
135
|
+
* @param {boolean} isPrefixedWithTableAlias
|
|
136
|
+
* @param {string} base
|
|
137
|
+
* @param {Token} current
|
|
138
|
+
* @returns {object[]} The stuff to add to the where
|
|
139
|
+
*/
|
|
140
|
+
function translateManagedAssocToWhere( root, target, isPrefixedWithTableAlias, base, current ) {
|
|
141
|
+
if (current.$scope === '$self') {
|
|
142
|
+
error('ref-unexpected-self', current.$path, { '#': 'exists', id: current.ref[0], name: 'exists' });
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const whereExtension = [];
|
|
147
|
+
for (let j = 0; j < root.keys.length; j++) {
|
|
148
|
+
const lop = { ref: [ target, ...root.keys[j].ref ] }; // target side
|
|
149
|
+
const rop = { ref: (isPrefixedWithTableAlias ? [] : [ base ]).concat([ ...toRawRef(current.ref), ...root.keys[j].ref ]) }; // source side
|
|
150
|
+
|
|
151
|
+
if (j > 0)
|
|
152
|
+
whereExtension.push('and');
|
|
153
|
+
|
|
154
|
+
whereExtension.push(...[ lop, '=', rop ]);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return whereExtension;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Translate an `EXISTS <unmanaged assoc>` into a part of a WHERE condition.
|
|
162
|
+
*
|
|
163
|
+
* A valid $self-backlink is handled in translateDollarSelfToWhere.
|
|
164
|
+
*
|
|
165
|
+
* For an ordinary unmanaged association, we do the following for each part of the on-condition:
|
|
166
|
+
* - target side: We prefix the real target and cut off the assoc-name from the ref
|
|
167
|
+
* - source side w/ leading $self: We remove the $self and add the source side entity/query source
|
|
168
|
+
* - source side w/o leading $self: We simply add the source side entity/query source in front of the ref
|
|
169
|
+
* - all other: Leave intact, usually operators
|
|
170
|
+
*
|
|
171
|
+
* @param {CSN.Element} root
|
|
172
|
+
* @param {string} target
|
|
173
|
+
* @param {boolean} isPrefixedWithTableAlias
|
|
174
|
+
* @param {string} base
|
|
175
|
+
* @param {Token} current
|
|
176
|
+
* @returns {object[]} The stuff to add to the where
|
|
177
|
+
*/
|
|
178
|
+
function translateUnmanagedAssocToWhere( root, target, isPrefixedWithTableAlias, base, current ) {
|
|
179
|
+
const whereExtension = [];
|
|
180
|
+
|
|
181
|
+
for (let j = 0; j < root.on.length; j++)
|
|
182
|
+
j = processExpressionPart(root.on, root.$path.concat('on'), j, whereExtension);
|
|
183
|
+
|
|
184
|
+
return whereExtension;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Process the given expression and apply the steps described above.
|
|
188
|
+
*
|
|
189
|
+
* @param {Array} expression Expression we are processing
|
|
190
|
+
* @param {CSN.Path} path Path to the expression
|
|
191
|
+
* @param {number} expressionIndex Index in the current expression, imporant for paths and stuff
|
|
192
|
+
* @param {Array} collector Array to collect the processed expressionparts into
|
|
193
|
+
* @returns {number} How far along expression we have processed - so the main loop can jump ahead
|
|
194
|
+
*/
|
|
195
|
+
function processExpressionPart(expression, path, expressionIndex, collector) {
|
|
196
|
+
const part = expression[expressionIndex];
|
|
197
|
+
|
|
198
|
+
if (part?.xpr) {
|
|
199
|
+
const xpr = { xpr: [] };
|
|
200
|
+
for (let i = 0; i < part.xpr.length; i++)
|
|
201
|
+
i = processExpressionPart(part.xpr, path.concat(expressionIndex, 'xpr'), i, xpr.xpr);
|
|
202
|
+
|
|
203
|
+
collector.push(xpr);
|
|
204
|
+
return expressionIndex;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// we can only resolve stuff on refs - skip literals like =
|
|
208
|
+
// but also keep along stuff like null and undefined, so compiler
|
|
209
|
+
// can have a chance to complain/ we can fail later nicely maybe
|
|
210
|
+
if (!(part && part.ref)) {
|
|
211
|
+
collector.push(part);
|
|
212
|
+
return expressionIndex;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// root.$path should be safe - we can only reference things in exists that exist when we enrich
|
|
216
|
+
// so all of them should have a $path.
|
|
217
|
+
const { art, links } = getLinksAndArt(part, path.concat(expressionIndex));
|
|
218
|
+
// Dollar Self Backlink
|
|
219
|
+
if (isValidDollarSelf(expression[expressionIndex], path.concat(expressionIndex), expression[expressionIndex + 1], expression[expressionIndex + 2], path.concat(expressionIndex + 2 ))) {
|
|
220
|
+
if (expression[expressionIndex].ref[0] === '$self' && expression[expressionIndex].ref.length === 1)
|
|
221
|
+
collector.push(...translateDollarSelfToWhere(base, target, expression[expressionIndex + 2], path.concat(expressionIndex + 2 )));
|
|
222
|
+
else
|
|
223
|
+
collector.push(...translateDollarSelfToWhere(base, target, expression[expressionIndex], path.concat(expressionIndex)));
|
|
224
|
+
|
|
225
|
+
return expressionIndex + 2;
|
|
226
|
+
}
|
|
227
|
+
else if (links && links[0].art === root) { // target side
|
|
228
|
+
collector.push({ ref: [ target, ...part.ref.slice(1) ] });
|
|
229
|
+
}
|
|
230
|
+
else if (part.$scope === '$self') { // source side - "absolute" scope
|
|
231
|
+
const column = part._art._column;
|
|
232
|
+
if (column && column.as) { // Replace with the "original" expression (the .ref, .xpr etc.)
|
|
233
|
+
collector.push(translateToSourceSide(column));
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
collector.push(assignAndDeleteAsAndKey({}, part, { ref: [ base, ...part.ref.slice(1) ] }));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else if (art) { // source side - with local scope
|
|
240
|
+
if (isPrefixedWithTableAlias || part.$scope === 'alias')
|
|
241
|
+
collector.push({ ref: [ ...current.ref.slice(0, -1), ...part.ref ] });
|
|
242
|
+
else
|
|
243
|
+
collector.push({ ref: [ base, ...current.ref.slice(0, -1), ...part.ref ] });
|
|
244
|
+
}
|
|
245
|
+
else { // operator - or any other leftover
|
|
246
|
+
collector.push(part);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return expressionIndex;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Run Object.assign on all of the passed in parameters and delete a .as and .key at the end
|
|
255
|
+
*
|
|
256
|
+
* @param {...any} args
|
|
257
|
+
* @returns {object} The merged args without an .as and .key property
|
|
258
|
+
*/
|
|
259
|
+
function assignAndDeleteAsAndKey( ...args ) {
|
|
260
|
+
const obj = Object.assign.apply(null, args);
|
|
261
|
+
delete obj.as;
|
|
262
|
+
delete obj.key;
|
|
263
|
+
return obj;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Translate the given obj (a column-like thing) into an expression that we can use in the WHERE.
|
|
267
|
+
* - Strip off $self/$projection and correctly replace with source expression
|
|
268
|
+
* - Drill further down into .xpr
|
|
269
|
+
* - Correctly set table alias in front of ref
|
|
270
|
+
*
|
|
271
|
+
* @param {object} obj
|
|
272
|
+
* @returns {object}
|
|
273
|
+
*/
|
|
274
|
+
function translateToSourceSide( obj ) {
|
|
275
|
+
if (obj.ref) {
|
|
276
|
+
if (obj.$scope === '$self') { // TODO: Check with this way down, do we keep the links?
|
|
277
|
+
const column = obj._art._column;
|
|
278
|
+
if (column && column.as)
|
|
279
|
+
return translateToSourceSide(column);
|
|
280
|
+
return assignAndDeleteAsAndKey({}, obj, { ref: [ base, ...obj.ref.slice(1) ] });
|
|
281
|
+
}
|
|
282
|
+
else if (typeof obj.$env === 'string') {
|
|
283
|
+
return assignAndDeleteAsAndKey({}, obj, { ref: [ obj.$env, ...obj.ref ] });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return assignAndDeleteAsAndKey({}, obj, { ref: [ ...obj.ref ] });
|
|
287
|
+
}
|
|
288
|
+
else if (obj.xpr) { // we need to drill further down into .xpr
|
|
289
|
+
return assignAndDeleteAsAndKey({}, obj, { xpr: obj.xpr.map(translateToSourceSide) });
|
|
290
|
+
}
|
|
291
|
+
else if (obj.args) {
|
|
292
|
+
return assignAndDeleteAsAndKey({}, obj, { args: obj.args.map(translateToSourceSide) });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return obj;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Check that an expression triple is a valid $self
|
|
300
|
+
*
|
|
301
|
+
* @param {Token} leftSide
|
|
302
|
+
* @param {CSN.Path} pathLeft
|
|
303
|
+
* @param {Token} middle
|
|
304
|
+
* @param {Token} rightSide
|
|
305
|
+
* @param {CSN.Path} pathRight
|
|
306
|
+
* @returns {boolean}
|
|
307
|
+
*/
|
|
308
|
+
function isValidDollarSelf( leftSide, pathLeft, middle, rightSide, pathRight ) {
|
|
309
|
+
if (leftSide && leftSide.ref && rightSide && rightSide.ref && middle === '=') {
|
|
310
|
+
const right = inspectRef(pathRight);
|
|
311
|
+
const left = inspectRef(pathLeft);
|
|
312
|
+
|
|
313
|
+
if (!right || !left)
|
|
314
|
+
return false;
|
|
315
|
+
|
|
316
|
+
const rightSideArt = right.art;
|
|
317
|
+
const leftSideArt = left.art;
|
|
318
|
+
|
|
319
|
+
return leftSide.ref[0] === '$self' && leftSide.ref.length === 1 && rightSideArt && rightSideArt.target ||
|
|
320
|
+
rightSide.ref[0] === '$self' && rightSide.ref.length === 1 && leftSideArt && leftSideArt.target;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Turn the would-be on-condition of a $self backlink into a WHERE condition.
|
|
329
|
+
*
|
|
330
|
+
* Prefix the target/source side base accordingly and build the source = target comparisons.
|
|
331
|
+
*
|
|
332
|
+
* @param {string} base The source entity/query source name
|
|
333
|
+
* @param {string} target The target entity/query source name
|
|
334
|
+
* @param {object} assoc The association element - the "not-$self" side of the comparison
|
|
335
|
+
* @param {CSN.Path} path
|
|
336
|
+
* @returns {TokenStream} The WHERE representing the $self comparison
|
|
337
|
+
*/
|
|
338
|
+
function translateDollarSelfToWhere( base, target, assoc, path ) {
|
|
339
|
+
const where = [];
|
|
340
|
+
const { art } = getLinksAndArt(assoc, path);
|
|
341
|
+
if (art.keys) {
|
|
342
|
+
for (let i = 0; i < art.keys.length; i++) {
|
|
343
|
+
const lop = { ref: [ target, ...assoc.ref.slice(1), ...art.keys[i].ref ] }; // target side
|
|
344
|
+
const rop = { ref: [ base, ...art.keys[i].ref ] }; // source side
|
|
345
|
+
if (i > 0)
|
|
346
|
+
where.push('and');
|
|
347
|
+
|
|
348
|
+
where.push(...[ lop, '=', rop ]);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
else if (art.on) {
|
|
352
|
+
for (let i = 0; i < art.on.length; i++) {
|
|
353
|
+
const part = art.on[i];
|
|
354
|
+
const partInspect = getLinksAndArt(part, art.$path.concat([ 'on', i ]));
|
|
355
|
+
if (partInspect.links && partInspect.links[0].art === art) { // target side
|
|
356
|
+
where.push({ ref: [ base, ...part.ref.slice(1) ] });
|
|
357
|
+
}
|
|
358
|
+
else if (part.$scope === '$self') { // source side - "absolute" scope
|
|
359
|
+
// Same message as in forRelationalDB/transformDollarSelfComparisonWithUnmanagedAssoc
|
|
360
|
+
error(null, part.$path, { name: '$self' },
|
|
361
|
+
'An association that uses $(NAME) in its ON-condition can\'t be compared to "$self"');
|
|
362
|
+
}
|
|
363
|
+
else if (partInspect.art) { // source side - with local scope
|
|
364
|
+
where.push({ ref: [ target, ...assoc.ref.slice(1, -1), ...part.ref ] });
|
|
365
|
+
}
|
|
366
|
+
else { // operator - or any other leftover
|
|
367
|
+
where.push(part);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return where;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Turn a ref-array into an array of strings.
|
|
376
|
+
*
|
|
377
|
+
* @param {Array} ref Array of strings or objects with `id`
|
|
378
|
+
* @returns {string[]}
|
|
379
|
+
*/
|
|
380
|
+
function toRawRef( ref ) {
|
|
381
|
+
return ref.map(r => (r.id ? r.id : r));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Get the source aliases from a query - drill down somewhat into joins (is that correct?)
|
|
386
|
+
*
|
|
387
|
+
* @param {CSN.Query} query
|
|
388
|
+
* @returns {object}
|
|
389
|
+
*/
|
|
390
|
+
function getQuerySources( query ) {
|
|
391
|
+
const sources = Object.create(null);
|
|
392
|
+
if (query.from.as)
|
|
393
|
+
sources[query.from.as] = query.from.as;
|
|
394
|
+
else if (query.from.args)
|
|
395
|
+
return Object.assign(sources, getJoinSources(query.from.args));
|
|
396
|
+
else if (query.from.ref)
|
|
397
|
+
sources[query.from.ref[query.from.ref.length - 1]] = query.from.ref[query.from.ref.length - 1];
|
|
398
|
+
|
|
399
|
+
return sources;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Get the source aliases from a join
|
|
404
|
+
*
|
|
405
|
+
* @param {Array} args Join args
|
|
406
|
+
* @returns {object}
|
|
407
|
+
*/
|
|
408
|
+
function getJoinSources( args ) {
|
|
409
|
+
let sources = Object.create(null);
|
|
410
|
+
for (const join of args) {
|
|
411
|
+
if (join.as) {
|
|
412
|
+
sources[join.as] = join.as;
|
|
413
|
+
}
|
|
414
|
+
else if (join.args) {
|
|
415
|
+
const subSources = getJoinSources(join.args);
|
|
416
|
+
sources = Object.assign(sources, subSources);
|
|
417
|
+
}
|
|
418
|
+
else if (join.ref) {
|
|
419
|
+
sources[join.ref[join.ref.length - 1]] = join.ref[join.ref.length - 1];
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return sources;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Use cacjed _links and _art or calculate via inspectRef
|
|
428
|
+
* @param {object} obj
|
|
429
|
+
* @param {CSN.Path} objPath
|
|
430
|
+
* @returns {object}
|
|
431
|
+
*/
|
|
432
|
+
function getLinksAndArt(obj, objPath) {
|
|
433
|
+
if (obj._links)
|
|
434
|
+
return { links: obj._links, art: obj._art };
|
|
435
|
+
return inspectRef(objPath);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
module.exports = { getHelpers };
|
|
@@ -11,7 +11,7 @@ const { implicitAs, columnAlias, pathId } = require('../../model/csnRefs');
|
|
|
11
11
|
const { setProp } = require('../../base/model');
|
|
12
12
|
const { forEach } = require('../../utils/objectUtils');
|
|
13
13
|
const { killNonrequiredAnno } = require('./killAnnotations');
|
|
14
|
-
const { featureFlags } = require('
|
|
14
|
+
const { featureFlags } = require('../featureFlags');
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* For keys, columns, groupBy and orderBy, expand structured things.
|
|
@@ -27,7 +27,7 @@ const { featureFlags } = require('./featureFlags');
|
|
|
27
27
|
function expandStructureReferences( csn, options, pathDelimiter, messageFunctions, csnUtils, iterateOptions = {} ) {
|
|
28
28
|
const { error, info, throwWithAnyError } = messageFunctions;
|
|
29
29
|
|
|
30
|
-
if (options.transformation === 'odata' ||
|
|
30
|
+
if (options.transformation === 'odata' || csn.meta?.[featureFlags]?.$expandInline)
|
|
31
31
|
rewriteExpandInline();
|
|
32
32
|
|
|
33
33
|
throwWithAnyError();
|
|
@@ -7,6 +7,7 @@ const {
|
|
|
7
7
|
const { setProp, isDeprecatedEnabled } = require('../../base/model');
|
|
8
8
|
const { getTransformers } = require('../transformUtils');
|
|
9
9
|
const { ModelError } = require('../../base/error');
|
|
10
|
+
const { forEach } = require('../../utils/objectUtils');
|
|
10
11
|
const draftAnnotation = '@odata.draft.enabled';
|
|
11
12
|
const booleanBuiltin = 'cds.Boolean';
|
|
12
13
|
|
|
@@ -24,7 +25,7 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
|
|
|
24
25
|
const allServices = getServiceNames(csn);
|
|
25
26
|
const draftRoots = new WeakMap();
|
|
26
27
|
const {
|
|
27
|
-
|
|
28
|
+
createAndAddDraftAdminDataProjection, createScalarElement, createAssociationElement,
|
|
28
29
|
addElement, copyAndAddElement, createAssociationPathComparison, csnUtils,
|
|
29
30
|
} = getTransformers(csn, options, messageFunctions, pathDelimiter);
|
|
30
31
|
const { getCsnDef, isComposition } = csnUtils;
|
|
@@ -268,8 +269,18 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
|
|
|
268
269
|
|
|
269
270
|
const draftUUIDKey = getDraftUUIDKey(draftAdministrativeData.DraftAdministrativeData);
|
|
270
271
|
if (!(options.transformation === 'hdbcds' && options.sqlMapping === 'hdbcds') && draftUUIDKey) {
|
|
271
|
-
const
|
|
272
|
-
|
|
272
|
+
const source = csn.definitions[draftAdministrativeData.DraftAdministrativeData.target];
|
|
273
|
+
const sourceElement = source.elements[draftUUIDKey.ref[0]];
|
|
274
|
+
const targetElement = {};
|
|
275
|
+
forEach(sourceElement, (key, value) => {
|
|
276
|
+
if(!key.startsWith('@') && key !== 'key')
|
|
277
|
+
targetElement[key] = value;
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
if(sourceElement.key) targetElement.notNull = true;
|
|
281
|
+
|
|
282
|
+
draftsArtifact.elements['DraftAdministrativeData' + (options.sqlMapping === 'hdbcds' ? '.' : '_') + draftUUIDKey.ref[0]] = targetElement;
|
|
283
|
+
|
|
273
284
|
draftAdministrativeData.DraftAdministrativeData.on = createAssociationPathComparison('DraftAdministrativeData',
|
|
274
285
|
getNameForRef(draftUUIDKey),
|
|
275
286
|
'=',
|
|
@@ -194,11 +194,10 @@ function remapODataAnnotations( csn ) {
|
|
|
194
194
|
/**
|
|
195
195
|
* Do the .texts anno magic if we can be reasonably sure that we are actually dealing with a .texts entity.
|
|
196
196
|
*
|
|
197
|
-
* @param {CSN.Model} csn
|
|
198
197
|
* @param {string} artifactName
|
|
199
198
|
* @param {CSN.Artifact} artifact
|
|
200
199
|
*/
|
|
201
|
-
function sealAnnoMagicForTexts(
|
|
200
|
+
function sealAnnoMagicForTexts(artifactName, artifact) {
|
|
202
201
|
if (artifactName.endsWith('.texts') && artifact.elements?.locale) {
|
|
203
202
|
const firstNonKey = getFirstNonKeyElement(artifact);
|
|
204
203
|
if (firstNonKey && firstNonKey.type === 'cds.String') {
|
|
@@ -269,6 +268,8 @@ function sealAnnoMagic(csn) {
|
|
|
269
268
|
forEach(parent.elements, (_elementName, element) => {
|
|
270
269
|
if (element['@ObjectModel.text.element'] && parent.elements[element['@ObjectModel.text.element']['=']] && parent.elements[element['@ObjectModel.text.element']['=']]['@Semantics.text'] === undefined)
|
|
271
270
|
parent.elements[element['@ObjectModel.text.element']['=']]['@Semantics.text'] = true;
|
|
271
|
+
if (element.target && element.target.endsWith('.texts') && csn.definitions[element.target].elements?.locale)
|
|
272
|
+
sealAnnoMagicForTexts(element.target, csn.definitions[element.target]);
|
|
272
273
|
});
|
|
273
274
|
}
|
|
274
275
|
|
|
@@ -338,5 +339,4 @@ function getOnConditionAsComparisonTuples(on, assocName) {
|
|
|
338
339
|
module.exports = {
|
|
339
340
|
remapODataAnnotations,
|
|
340
341
|
sealAnnoMagic,
|
|
341
|
-
sealAnnoMagicForTexts,
|
|
342
342
|
};
|
|
@@ -12,11 +12,12 @@ const validate = require('../../checks/validator');
|
|
|
12
12
|
const expansion = require('../db/expansion');
|
|
13
13
|
const queries = require('./queries');
|
|
14
14
|
const associations = require('./associations');
|
|
15
|
-
const handleExists = require('../db/transformExists');
|
|
15
|
+
const handleExists = require('../db/assocsToQueries/transformExists');
|
|
16
16
|
const misc = require('./misc');
|
|
17
17
|
const annotations = require('./annotations');
|
|
18
18
|
const { rewriteCalculatedElementsInViews, processCalculatedElementsInEntities } = require('../db/rewriteCalculatedElements');
|
|
19
19
|
const { cloneFullCsn } = require('../../model/cloneCsn');
|
|
20
|
+
const { featureFlags } = require('../featureFlags');
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* This is just a PoC for now!
|
|
@@ -45,7 +46,8 @@ function effectiveCsn( model, options, messageFunctions ) {
|
|
|
45
46
|
...messageFunctions, csnUtils, ...csnUtils, csn, options, isAspect,
|
|
46
47
|
});
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
if (csn.meta?.[featureFlags]?.$calculatedElements)
|
|
50
|
+
rewriteCalculatedElementsInViews(csn, options, csnUtils, '_', messageFunctions);
|
|
49
51
|
|
|
50
52
|
// Needs to happen before tuple expansion, so the newly generated WHERE-conditions have it applied
|
|
51
53
|
handleExists(csn, options, messageFunctions.error, csnUtils.inspectRef, csnUtils.initDefinition, csnUtils.dropDefinitionCache);
|
|
@@ -85,11 +87,7 @@ function effectiveCsn( model, options, messageFunctions ) {
|
|
|
85
87
|
options.deriveAnalyticalAnnotations ? annotations.sealAnnoMagic(csn) : {},
|
|
86
88
|
], null);
|
|
87
89
|
|
|
88
|
-
|
|
89
|
-
if (options.deriveAnalyticalAnnotations)
|
|
90
|
-
artifactTransformers.push(annotations.sealAnnoMagicForTexts);
|
|
91
|
-
|
|
92
|
-
applyTransformations(csn, transformers, artifactTransformers, { skipIgnore: false, processAnnotations: true });
|
|
90
|
+
applyTransformations(csn, transformers, [], { skipIgnore: false, processAnnotations: true });
|
|
93
91
|
|
|
94
92
|
if (!options.resolveProjections)
|
|
95
93
|
redoProjections.forEach(fn => fn());
|