@sap/cds-compiler 4.2.4 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/bin/cdsc.js +8 -0
- package/bin/cdshi.js +3 -3
- package/doc/CHANGELOG_BETA.md +7 -0
- package/lib/api/main.js +19 -0
- package/lib/base/location.js +16 -0
- package/lib/base/message-registry.js +47 -16
- package/lib/base/messages.js +49 -38
- package/lib/base/model.js +1 -1
- package/lib/checks/checkPathsInStoredCalcElement.js +83 -0
- package/lib/checks/existsExpressionsOnlyForeignKeys.js +71 -0
- package/lib/checks/existsMustEndInAssoc.js +27 -0
- package/lib/checks/onConditions.js +47 -1
- package/lib/checks/validator.js +10 -1
- package/lib/compiler/assert-consistency.js +23 -15
- package/lib/compiler/base.js +31 -14
- package/lib/compiler/builtins.js +21 -20
- package/lib/compiler/checks.js +36 -49
- package/lib/compiler/define.js +71 -91
- package/lib/compiler/extend.js +27 -25
- package/lib/compiler/finalize-parse-cdl.js +1 -1
- package/lib/compiler/generate.js +67 -87
- package/lib/compiler/kick-start.js +7 -5
- package/lib/compiler/populate.js +32 -30
- package/lib/compiler/propagator.js +2 -0
- package/lib/compiler/resolve.js +29 -25
- package/lib/compiler/shared.js +57 -31
- package/lib/compiler/tweak-assocs.js +203 -22
- package/lib/compiler/utils.js +0 -18
- package/lib/gen/Dictionary.json +10 -4
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/languageParser.js +3 -3
- package/lib/inspect/inspectPropagation.js +2 -1
- package/lib/json/from-csn.js +63 -28
- package/lib/json/to-csn.js +23 -13
- package/lib/language/antlrParser.js +1 -1
- package/lib/language/errorStrategy.js +5 -1
- package/lib/language/genericAntlrParser.js +67 -61
- package/lib/main.d.ts +26 -1
- package/lib/main.js +2 -1
- package/lib/model/csnRefs.js +1 -0
- package/lib/model/csnUtils.js +28 -0
- package/lib/model/revealInternalProperties.js +3 -9
- package/lib/optionProcessor.js +17 -1
- package/lib/render/toCdl.js +1 -1
- package/lib/transform/db/associations.js +3 -4
- package/lib/transform/db/backlinks.js +293 -0
- package/lib/transform/db/expansion.js +9 -7
- package/lib/transform/db/flattening.js +3 -2
- package/lib/transform/db/rewriteCalculatedElements.js +1 -67
- package/lib/transform/db/transformExists.js +3 -58
- package/lib/transform/db/views.js +8 -14
- package/lib/transform/effective/.eslintrc.json +4 -0
- package/lib/transform/effective/associations.js +101 -0
- package/lib/transform/effective/main.js +88 -0
- package/lib/transform/effective/misc.js +61 -0
- package/lib/transform/effective/queries.js +42 -0
- package/lib/transform/effective/types.js +121 -0
- package/lib/transform/forRelationalDB.js +12 -235
- package/lib/transform/localized.js +22 -3
- package/lib/transform/parseExpr.js +7 -3
- package/lib/transform/transformUtils.js +5 -22
- package/lib/transform/translateAssocsToJoins.js +42 -38
- package/lib/transform/universalCsn/universalCsnEnricher.js +17 -1
- package/package.json +1 -2
- package/lib/language/language.g4 +0 -3260
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
cloneCsnNonDict, applyTransformationsOnNonDictionary, isAssociationOperand, isDollarSelfOrProjectionOperand,
|
|
5
|
+
} = require('../../model/csnUtils');
|
|
6
|
+
|
|
7
|
+
const { setProp } = require('../../base/model');
|
|
8
|
+
const { ModelError } = require('../../base/error');
|
|
9
|
+
const { forEach } = require('../../utils/objectUtils');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get a function that transforms $self backlinks
|
|
13
|
+
* @param {object} csnUtils
|
|
14
|
+
* @param {object} messageFunctions
|
|
15
|
+
* @param {CSN.Options} options
|
|
16
|
+
* @param {string} pathDelimiter
|
|
17
|
+
* @param {boolean} doA2J
|
|
18
|
+
* @returns {import('../../model/csnUtils').genericCallback} callback for forEachDefinition
|
|
19
|
+
*/
|
|
20
|
+
function getBacklinkTransformer( csnUtils, messageFunctions, options, pathDelimiter, doA2J = true ) {
|
|
21
|
+
return transformSelfInBacklinks;
|
|
22
|
+
/**
|
|
23
|
+
* @param {CSN.Artifact} artifact
|
|
24
|
+
* @param {string} artifactName
|
|
25
|
+
* @param {any} dummy unused Parameter
|
|
26
|
+
* @param {CSN.Path} path
|
|
27
|
+
*/
|
|
28
|
+
function transformSelfInBacklinks( artifact, artifactName, dummy, path ) {
|
|
29
|
+
// Fixme: For toHana mixins must be transformed, for toSql -d hana
|
|
30
|
+
// mixin elements must be transformed, why can't toSql also use mixins?
|
|
31
|
+
if (artifact.kind === 'entity' || artifact.query || (options.forHana && options.sqlMapping === 'hdbcds' && artifact.kind === 'type'))
|
|
32
|
+
processDict(artifact.elements, path.concat([ 'elements' ]));
|
|
33
|
+
if (artifact.query?.SELECT?.mixin)
|
|
34
|
+
processDict(artifact.query.SELECT.mixin, path.concat([ 'query', 'SELECT', 'mixin' ]));
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Loop over the dict and start the processing.
|
|
38
|
+
*
|
|
39
|
+
* @param {object} dict .elements or .mixin
|
|
40
|
+
* @param {Array} subPath Path into the dict
|
|
41
|
+
*/
|
|
42
|
+
function processDict( dict, subPath ) {
|
|
43
|
+
forEach(dict, (elemName, elem) => {
|
|
44
|
+
if (elem.on && csnUtils.isAssocOrComposition(elem))
|
|
45
|
+
processBacklinkAssoc(elem, elemName, artifact, artifactName, subPath.concat([ elemName, 'on' ]));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* If the association element 'elem' of 'art' is a backlink association, massage its ON-condition
|
|
53
|
+
* (in place) so that it
|
|
54
|
+
* - compares the generated foreign key fields of the corresponding forward
|
|
55
|
+
* association with their respective keys in 'art' (for managed forward associations)
|
|
56
|
+
* - contains the corresponding forward association's ON-condition in "reversed" form,
|
|
57
|
+
* i.e. as seen from 'elem' (for unmanaged associations)
|
|
58
|
+
* Otherwise, do nothing.
|
|
59
|
+
* @param {CSN.Element} elem
|
|
60
|
+
* @param {string} elemName
|
|
61
|
+
* @param {CSN.Artifact} art
|
|
62
|
+
* @param {string} artName
|
|
63
|
+
* @param {CSN.Path} pathToOn
|
|
64
|
+
*/
|
|
65
|
+
function processBacklinkAssoc( elem, elemName, art, artName, pathToOn ) {
|
|
66
|
+
// Don't add braces if it is a single expression (ignoring superfluous braces)
|
|
67
|
+
const multipleExprs = elem.on.filter(x => x !== '(' && x !== ')' ).length > 3;
|
|
68
|
+
elem.on = processExpressionArgs(elem.on, pathToOn);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Process the args
|
|
72
|
+
*
|
|
73
|
+
* @param {Array} xprArgs
|
|
74
|
+
* @param {CSN.Path} path
|
|
75
|
+
* @returns {Array} Array of parsed expression
|
|
76
|
+
*/
|
|
77
|
+
function processExpressionArgs( xprArgs, path ) {
|
|
78
|
+
const result = [];
|
|
79
|
+
let i = 0;
|
|
80
|
+
while (i < xprArgs.length) {
|
|
81
|
+
// Only token tripel `<path>, '=', <path>` are of interest here
|
|
82
|
+
if (i < xprArgs.length - 2 && xprArgs[i + 1] === '=') {
|
|
83
|
+
// Check if one side is $self and the other an association
|
|
84
|
+
// (if so, replace all three tokens with the condition generated from the other side, in parentheses)
|
|
85
|
+
if (isDollarSelfOrProjectionOperand(xprArgs[i]) && isAssociationOperand(xprArgs[i + 2], path.concat([ i + 2 ]), csnUtils.inspectRef)) {
|
|
86
|
+
const assoc = csnUtils.inspectRef(path.concat([ i + 2 ])).art;
|
|
87
|
+
if (multipleExprs)
|
|
88
|
+
result.push('(');
|
|
89
|
+
const backlinkName = xprArgs[i + 2].ref[xprArgs[i + 2].ref.length - 1];
|
|
90
|
+
result.push(...transformDollarSelfComparison(xprArgs[i + 2],
|
|
91
|
+
assoc,
|
|
92
|
+
backlinkName,
|
|
93
|
+
elem, elemName, art, artName, path.concat([ i ])));
|
|
94
|
+
if (multipleExprs)
|
|
95
|
+
result.push(')');
|
|
96
|
+
i += 3;
|
|
97
|
+
attachBacklinkInformation(backlinkName);
|
|
98
|
+
}
|
|
99
|
+
else if (isDollarSelfOrProjectionOperand(xprArgs[i + 2]) && isAssociationOperand(xprArgs[i], path.concat([ i ]), csnUtils.inspectRef)) {
|
|
100
|
+
const assoc = csnUtils.inspectRef(path.concat([ i ])).art;
|
|
101
|
+
if (multipleExprs)
|
|
102
|
+
result.push('(');
|
|
103
|
+
const backlinkName = xprArgs[i].ref[xprArgs[i].ref.length - 1];
|
|
104
|
+
result.push(...transformDollarSelfComparison(xprArgs[i], assoc, backlinkName, elem, elemName, art, artName, path.concat([ i + 2 ])));
|
|
105
|
+
if (multipleExprs)
|
|
106
|
+
result.push(')');
|
|
107
|
+
i += 3;
|
|
108
|
+
attachBacklinkInformation(backlinkName);
|
|
109
|
+
}
|
|
110
|
+
// Otherwise take one (!) token unchanged
|
|
111
|
+
else {
|
|
112
|
+
result.push(xprArgs[i]);
|
|
113
|
+
i++;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Process subexpressions - but keep them as subexpressions
|
|
117
|
+
else if (xprArgs[i].xpr) {
|
|
118
|
+
result.push({ xpr: processExpressionArgs(xprArgs[i].xpr, path.concat([ i, 'xpr' ])) });
|
|
119
|
+
i++;
|
|
120
|
+
}
|
|
121
|
+
// Take all other tokens unchanged
|
|
122
|
+
else {
|
|
123
|
+
result.push(xprArgs[i]);
|
|
124
|
+
i++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The knowledge whether an association was an `<up_>` association in a
|
|
131
|
+
* `$self = <comp>.<up_>` comparison, is important for the foreign key constraints.
|
|
132
|
+
* By the time we generate them, such on-conditions are already transformed
|
|
133
|
+
* --> no more `$self` in the on-conditions, that is why we need to remember it here.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} backlinkName name of `<up_>` in a `$self = <comp>.<up_>` comparison
|
|
136
|
+
*/
|
|
137
|
+
function attachBacklinkInformation( backlinkName ) {
|
|
138
|
+
if (elem.$selfOnCondition) {
|
|
139
|
+
elem.$selfOnCondition.up_.push(backlinkName);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
setProp(elem, '$selfOnCondition', {
|
|
143
|
+
up_: [ backlinkName ],
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Return the condition to replace the comparison `<assocOp> = $self` in the ON-condition
|
|
152
|
+
* of element <elem> of artifact 'art'. If there is anything to complain, use location <loc>
|
|
153
|
+
*
|
|
154
|
+
* @param {any} assocOp
|
|
155
|
+
* @param {CSN.Element} assoc
|
|
156
|
+
* @param {string} assocName
|
|
157
|
+
* @param {CSN.Element} elem
|
|
158
|
+
* @param {string} elemName
|
|
159
|
+
* @param {CSN.Artifact} art
|
|
160
|
+
* @param {string} artifactName
|
|
161
|
+
* @param {CSN.Path} path
|
|
162
|
+
* @returns {Array} New on-condition
|
|
163
|
+
*/
|
|
164
|
+
function transformDollarSelfComparison( assocOp, assoc, assocName, elem, elemName, art, artifactName, path ) {
|
|
165
|
+
// Check: The forward link <assocOp> must point back to this artifact
|
|
166
|
+
// FIXME: Unfortunately, we can currently only check this for non-views (because when a view selects
|
|
167
|
+
// a backlink association element from an entity, the forward link will point to the entity,
|
|
168
|
+
// not to the view).
|
|
169
|
+
// FIXME: This also means that corresponding key fields should be in the select list etc ...
|
|
170
|
+
if (!art.query && !art.projection && assoc.target && assoc.target !== artifactName) {
|
|
171
|
+
messageFunctions.error( null, path, { id: '$self', name: artifactName, target: assoc.target },
|
|
172
|
+
'Expected association using $(ID) to point back to $(NAME) but found $(TARGET)' );
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check: The forward link <assocOp> must not contain '$self' in its own ON-condition
|
|
176
|
+
if (assoc.on) {
|
|
177
|
+
const containsDollarSelf = assoc.on.some(isDollarSelfOrProjectionOperand);
|
|
178
|
+
|
|
179
|
+
if (containsDollarSelf) {
|
|
180
|
+
messageFunctions.error(null, path, { name: '$self' },
|
|
181
|
+
'An association that uses $(NAME) in its ON-condition can\'t be compared to $(NAME)');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Transform comparison of $self to managed association into AND-combined foreign key comparisons
|
|
186
|
+
if (assoc.keys) {
|
|
187
|
+
if (assoc.keys.length)
|
|
188
|
+
return transformDollarSelfComparisonWithManagedAssoc(assocOp, assoc, assocName, elemName);
|
|
189
|
+
|
|
190
|
+
elem.$ignore = true;
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Transform comparison of $self to unmanaged association into "reversed" ON-condition
|
|
195
|
+
else if (assoc.on) {
|
|
196
|
+
return transformDollarSelfComparisonWithUnmanagedAssoc(assocOp, assoc, assocName, elemName);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
throw new ModelError(`Expected either managed or unmanaged association in $self-comparison: ${JSON.stringify(elem.on)}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* For a condition `<elemName>.<assoc> = $self` in the ON-condition of element <elemName>,
|
|
205
|
+
* where <assoc> is a managed association, return a condition comparing the generated
|
|
206
|
+
* foreign key elements <elemName>.<assoc>_<fkey1..n> of <assoc> to the corresponding
|
|
207
|
+
* keys in this artifact.
|
|
208
|
+
* For example, `ON elem.ass = $self` becomes `ON elem.ass_key1 = key1 AND elem.ass_key2 = key2`
|
|
209
|
+
* (assuming that `ass` has the foreign keys `key1` and `key2`)
|
|
210
|
+
* @param {any} assocOp
|
|
211
|
+
* @param {CSN.Element} assoc
|
|
212
|
+
* @param {string} originalAssocName
|
|
213
|
+
* @param {string} elemName
|
|
214
|
+
* @returns {Array} New on-condition
|
|
215
|
+
*/
|
|
216
|
+
function transformDollarSelfComparisonWithManagedAssoc( assocOp, assoc, originalAssocName, elemName ) {
|
|
217
|
+
const conditions = [];
|
|
218
|
+
// if the element was structured then it was flattened => change of the delimiter from '.' to '_'
|
|
219
|
+
// this is done in the flattening, but as we do not alter the onCond itself there should be done here as well
|
|
220
|
+
const assocName = originalAssocName.replace(/\./g, pathDelimiter);
|
|
221
|
+
elemName = elemName.replace(/\./g, pathDelimiter);
|
|
222
|
+
|
|
223
|
+
assoc.keys.forEach((k) => {
|
|
224
|
+
// Depending on naming conventions, the foreign key may two path steps (hdbcds) or be a single path step with a flattened name (plain, quoted)
|
|
225
|
+
// With to.hdbcds in conjunction with hdbcds naming, we need to NOT use the alias - else we get deployment errors
|
|
226
|
+
const keyName = k.as && doA2J ? [ k.as ] : k.ref;
|
|
227
|
+
const fKeyPath = !doA2J ? [ assocName, ...keyName ] : [ `${assocName}${pathDelimiter}${keyName[0]}` ];
|
|
228
|
+
// FIXME: _artifact to the args ???
|
|
229
|
+
const a = [
|
|
230
|
+
{
|
|
231
|
+
ref: [ elemName, ...fKeyPath ],
|
|
232
|
+
},
|
|
233
|
+
{ ref: k.ref },
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
conditions.push([ a[0], '=', a[1] ]);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return conditions.reduce((prev, current) => {
|
|
240
|
+
if (prev.length === 0)
|
|
241
|
+
return [ ...current ];
|
|
242
|
+
|
|
243
|
+
return [ ...prev, 'and', ...current ];
|
|
244
|
+
}, []);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* For a condition `<elemName>.<assoc> = $self` in the ON-condition of element <elemName>,
|
|
249
|
+
* where <assoc> is an unmanaged association, return the ON-condition of <assoc> as it would
|
|
250
|
+
* be written from the perspective of the artifact containing association <elemName>.
|
|
251
|
+
* For example, `ON elem.ass = $self` becomes `ON a = elem.x AND b = elem.y`
|
|
252
|
+
* (assuming that `ass` has the ON-condition `ON ass.a = x AND ass.b = y`)
|
|
253
|
+
*
|
|
254
|
+
* @param {any} assocOp
|
|
255
|
+
* @param {CSN.Element} assoc
|
|
256
|
+
* @param {string} originalAssocName
|
|
257
|
+
* @param {string} elemName
|
|
258
|
+
* @returns {Array} New on-condition
|
|
259
|
+
*/
|
|
260
|
+
function transformDollarSelfComparisonWithUnmanagedAssoc( assocOp, assoc, originalAssocName, elemName ) {
|
|
261
|
+
// if the element was structured then it may have been flattened => change of the delimiter from '.' to '_'
|
|
262
|
+
// this is done in the flattening, but as we do not alter the onCond itself there should be done here as well
|
|
263
|
+
elemName = elemName.replace(/\./g, pathDelimiter);
|
|
264
|
+
const assocName = originalAssocName.replace(/\./g, pathDelimiter);
|
|
265
|
+
// clone the onCond for later use in the path transformation
|
|
266
|
+
const newOnCond = cloneCsnNonDict(assoc.on, options);
|
|
267
|
+
applyTransformationsOnNonDictionary({ on: newOnCond }, 'on', {
|
|
268
|
+
ref: (parent, prop, ref) => {
|
|
269
|
+
// we are in the "path" from the forwarding assoc => need to remove the first part of the path
|
|
270
|
+
if (ref[0] === assocName) {
|
|
271
|
+
ref.shift();
|
|
272
|
+
}
|
|
273
|
+
else if (ref.length > 1 && ref[0] === '$self' && ref[1] === assocName) {
|
|
274
|
+
// We could also have a $self in front of the assoc name - so we would need to shift twice
|
|
275
|
+
ref.shift();
|
|
276
|
+
ref.shift();
|
|
277
|
+
}
|
|
278
|
+
else { // we are in the backlink assoc "path" => need to push at the beginning the association's id
|
|
279
|
+
ref.unshift(elemName);
|
|
280
|
+
// if there was a $self identifier in the forwarding association onCond
|
|
281
|
+
// we do not need it anymore, as we prepended in the previous step the back association's id
|
|
282
|
+
if (ref[1] === '$self')
|
|
283
|
+
ref.splice(1, 1);
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
return newOnCond;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
module.exports = {
|
|
292
|
+
getBacklinkTransformer,
|
|
293
|
+
};
|
|
@@ -42,7 +42,9 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
|
|
|
42
42
|
const isComplexQuery = parent.from.join !== undefined;
|
|
43
43
|
if (!options.toOdata)
|
|
44
44
|
parent.columns = replaceStar(root, columns, parent.excluding, isComplexQuery);
|
|
45
|
-
|
|
45
|
+
// FIXME(v5): Remove argument "isComplexOrNestedQuery"; we use path.length > 4 to check
|
|
46
|
+
// if we're inside the outermost "columns". If so, always prepend a table alias. See #11662
|
|
47
|
+
parent.columns = expand(parent.columns, path.concat('columns'), true, isComplexQuery || path.length > 4);
|
|
46
48
|
}
|
|
47
49
|
},
|
|
48
50
|
groupBy: (parent, name, groupBy, path) => {
|
|
@@ -513,17 +515,17 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
|
|
|
513
515
|
* @param {Array} thing
|
|
514
516
|
* @param {CSN.Path} path
|
|
515
517
|
* @param {boolean} [withAlias=false] Whether to "expand" the (implicit) alias as well.
|
|
516
|
-
* @param {boolean} [
|
|
518
|
+
* @param {boolean} [isComplexOrNestedQuery]
|
|
517
519
|
* @returns {Array} New array - with all structured things expanded
|
|
518
520
|
*/
|
|
519
|
-
function expand( thing, path, withAlias = false,
|
|
521
|
+
function expand( thing, path, withAlias = false, isComplexOrNestedQuery = false ) {
|
|
520
522
|
const newThing = [];
|
|
521
523
|
for (let i = 0; i < thing.length; i++) {
|
|
522
524
|
const col = thing[i];
|
|
523
525
|
if (col.ref && col.$scope !== '$magic') {
|
|
524
526
|
const _art = col._art || csnUtils.inspectRef(path.concat(i)).art;
|
|
525
527
|
if (_art && csnUtils.isStructured(_art))
|
|
526
|
-
newThing.push(...expandRef(_art, col, withAlias,
|
|
528
|
+
newThing.push(...expandRef(_art, col, withAlias, isComplexOrNestedQuery));
|
|
527
529
|
else
|
|
528
530
|
newThing.push(col);
|
|
529
531
|
}
|
|
@@ -598,10 +600,10 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
|
|
|
598
600
|
* @param {CSN.Element} art
|
|
599
601
|
* @param {object} root Column, ref in order by, etc.
|
|
600
602
|
* @param {boolean} withAlias Whether to add an explicit flattened alias to the expanded columns/references.
|
|
601
|
-
* @param {boolean} [
|
|
603
|
+
* @param {boolean} [isComplexOrNestedQuery]
|
|
602
604
|
* @returns {Array}
|
|
603
605
|
*/
|
|
604
|
-
function expandRef( art, root, withAlias,
|
|
606
|
+
function expandRef( art, root, withAlias, isComplexOrNestedQuery ) {
|
|
605
607
|
return _expandStructCol(art, columnAlias(root), root.ref, ( currentRef, currentAlias) => {
|
|
606
608
|
const obj = { ...root, ref: currentRef };
|
|
607
609
|
if (withAlias) {
|
|
@@ -622,7 +624,7 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
|
|
|
622
624
|
// - it is a complex query with possibly multiple available table aliases, or
|
|
623
625
|
// - the transformation is not for OData (which is used by Java), or
|
|
624
626
|
// - the first path step has the same name as the table alias (only one, as otherwise the query would be complex)
|
|
625
|
-
if (typeof root.$env === 'string' && (
|
|
627
|
+
if (typeof root.$env === 'string' && (isComplexOrNestedQuery || options.transformation !== 'odata' || root.$env === pathId(obj.ref[0])))
|
|
626
628
|
obj.ref = [ root.$env, ...obj.ref ];
|
|
627
629
|
|
|
628
630
|
return obj;
|
|
@@ -840,8 +840,9 @@ function createForeignKeys( csnUtils, path, element, prefix, csn, options, pathD
|
|
|
840
840
|
fk[0] = `${prefix}${pathDelimiter}${fk[0]}`;
|
|
841
841
|
// if this is the entry association, decorate the final foreign keys with the association props
|
|
842
842
|
if (lvl === 0) {
|
|
843
|
-
|
|
844
|
-
|
|
843
|
+
if (options.transformation !== 'effective')
|
|
844
|
+
fk[1]['@odata.foreignKey4'] = prefix;
|
|
845
|
+
if (options.transformation === 'odata' || options.transformation === 'effective')
|
|
845
846
|
copyAnnotations(element, fk[1], true);
|
|
846
847
|
|
|
847
848
|
// propagate not null to final foreign key
|
|
@@ -8,11 +8,9 @@ const {
|
|
|
8
8
|
applyTransformationsOnDictionary,
|
|
9
9
|
implicitAs,
|
|
10
10
|
cloneCsnNonDict,
|
|
11
|
-
forEachMemberRecursively,
|
|
12
11
|
} = require('../../model/csnUtils');
|
|
13
12
|
const { getBranches } = require('./flattening');
|
|
14
13
|
const { getColumnMap } = require('./views');
|
|
15
|
-
const { requireForeignKeyAccess } = require('../../checks/onConditions');
|
|
16
14
|
|
|
17
15
|
const cloneCsnOptions = { hiddenPropertiesToClone: [ '_art', '_links', '$env', '$scope' ] };
|
|
18
16
|
|
|
@@ -96,73 +94,9 @@ function rewriteCalculatedElementsInViews( csn, options, csnUtils, pathDelimiter
|
|
|
96
94
|
}, {}, [ 'definitions' ]);
|
|
97
95
|
});
|
|
98
96
|
|
|
99
|
-
/**
|
|
100
|
-
* Check that all paths in calculated elements-on write either access normal fields
|
|
101
|
-
* (structures are already rejected by the compiler) or access the foreign keys of
|
|
102
|
-
* associations. Non-fk fields must be rejected.
|
|
103
|
-
* Filters and parameters are not allowed.
|
|
104
|
-
*
|
|
105
|
-
* Note: This coding is similar to checks/onConditions.js, but does not check $self or other
|
|
106
|
-
* ON-condition related stuff.
|
|
107
|
-
*
|
|
108
|
-
* @param {object} parent
|
|
109
|
-
* @param {(string|object)[]} value
|
|
110
|
-
* @param {CSN.Path} csnPath
|
|
111
|
-
*/
|
|
112
|
-
function checkPathsInStoredCalcElement( parent, value, csnPath ) {
|
|
113
|
-
const { _links } = parent;
|
|
114
|
-
|
|
115
|
-
// If there is only one path step, it's been checked before.
|
|
116
|
-
for (let i = 0; i < value.length - 1; ++i) {
|
|
117
|
-
let hasPathError = false;
|
|
118
|
-
const step = value[i];
|
|
119
|
-
const stepArt = _links[i].art;
|
|
120
|
-
|
|
121
|
-
if (stepArt.target) {
|
|
122
|
-
const id = step.id || step;
|
|
123
|
-
if (stepArt.on) {
|
|
124
|
-
// It's an unmanaged association - traversal is always forbidden
|
|
125
|
-
error('ref-unexpected-navigation', csnPath, { '#': 'calc-unmanaged', id, elemref: parent });
|
|
126
|
-
hasPathError = true;
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
// It's a managed association - access of the foreign keys is allowed
|
|
130
|
-
requireForeignKeyAccess(parent, i, (errorIndex) => {
|
|
131
|
-
error('ref-unexpected-navigation', csnPath, {
|
|
132
|
-
'#': 'calc-non-fk', id, elemref: parent, name: value[errorIndex].id || value[errorIndex],
|
|
133
|
-
});
|
|
134
|
-
hasPathError = true;
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
if (typeof step === 'object') {
|
|
139
|
-
if (step.where) {
|
|
140
|
-
error('ref-unexpected-filter', csnPath, { '#': 'calc', elemref: parent });
|
|
141
|
-
hasPathError = true;
|
|
142
|
-
}
|
|
143
|
-
if (step.args) {
|
|
144
|
-
error('ref-unexpected-args', csnPath, { '#': 'calc', elemref: parent });
|
|
145
|
-
hasPathError = true;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
if (hasPathError)
|
|
149
|
-
break; // avoid too many consequent errors
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
97
|
// Last pass, turn .value in tables into a simple 'val' so we don't need to rewrite/flatten properly - will kill them later
|
|
154
98
|
entities.forEach(({ artifact, artifactName }) => {
|
|
155
99
|
dummifyInEntity(artifact, [ 'definitions', artifactName ]);
|
|
156
|
-
|
|
157
|
-
forEachMemberRecursively({ elements: artifact.elements }, (element, elementName, _prop, path) => {
|
|
158
|
-
if (element.value?.stored) {
|
|
159
|
-
applyTransformationsOnNonDictionary(element, 'value', {
|
|
160
|
-
ref(parent, prop, value, csnPath) {
|
|
161
|
-
checkPathsInStoredCalcElement(parent, value, csnPath);
|
|
162
|
-
},
|
|
163
|
-
}, {}, path);
|
|
164
|
-
}
|
|
165
|
-
}, [ 'definitions', artifactName ]);
|
|
166
100
|
});
|
|
167
101
|
|
|
168
102
|
/**
|
|
@@ -329,7 +263,7 @@ function rewriteCalculatedElementsInViews( csn, options, csnUtils, pathDelimiter
|
|
|
329
263
|
linksBase: current.linksBase,
|
|
330
264
|
});
|
|
331
265
|
},
|
|
332
|
-
});
|
|
266
|
+
}, { skipStandard: { where: true } });
|
|
333
267
|
}
|
|
334
268
|
else if (current.value.ref && current.value._art?.value && !current.value._art?.value.stored) {
|
|
335
269
|
const linksBase = current.value._links;
|
|
@@ -66,6 +66,9 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
|
|
|
66
66
|
if (query.SELECT?.where?.length > 1)
|
|
67
67
|
toProcess.push([ path.slice(0, -1), path.concat('where') ]);
|
|
68
68
|
|
|
69
|
+
if (query.SELECT?.having?.length > 1)
|
|
70
|
+
toProcess.push([ path.slice(0, -1), path.concat('having') ]);
|
|
71
|
+
|
|
69
72
|
if (query.SELECT?.columns)
|
|
70
73
|
toProcess.push([ path.slice(0, -1), path.concat('columns') ]);
|
|
71
74
|
|
|
@@ -73,7 +76,6 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
|
|
|
73
76
|
toProcess.push([ path.slice(0, -1), path.concat([ 'from', 'on' ]) ]);
|
|
74
77
|
|
|
75
78
|
for (const [ , exprPath ] of toProcess) {
|
|
76
|
-
forbidAssocInExists(exprPath);
|
|
77
79
|
const expr = nestExists(exprPath);
|
|
78
80
|
walkCsnPath(csn, exprPath.slice(0, -1))[exprPath[exprPath.length - 1]] = expr;
|
|
79
81
|
}
|
|
@@ -254,63 +256,6 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
|
|
|
254
256
|
return startAssoc;
|
|
255
257
|
}
|
|
256
258
|
|
|
257
|
-
/**
|
|
258
|
-
* Check that associations in filters (in an exists expression) are only fk-accesses. Everything else is forbidden.
|
|
259
|
-
*
|
|
260
|
-
* @param {CSN.Path} exprPath
|
|
261
|
-
* @returns {void}
|
|
262
|
-
*/
|
|
263
|
-
function forbidAssocInExists( exprPath ) {
|
|
264
|
-
const expr = walkCsnPath(csn, exprPath);
|
|
265
|
-
for (let i = 0; i < expr.length; i++) {
|
|
266
|
-
if (i < expr.length - 1 && expr[i] === 'exists' && expr[i + 1].ref) {
|
|
267
|
-
i++;
|
|
268
|
-
const current = expr[i];
|
|
269
|
-
|
|
270
|
-
const { links } = inspectRef(exprPath.concat(i));
|
|
271
|
-
|
|
272
|
-
const assocs = links.filter(link => link.art?.target).map(link => current.ref[link.idx]);
|
|
273
|
-
|
|
274
|
-
checkForInvalidAssoc(assocs);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* @param {object[]} assocs Array of refs of assocs - possibly with a .where to check
|
|
281
|
-
*/
|
|
282
|
-
function checkForInvalidAssoc( assocs ) {
|
|
283
|
-
for (const assoc of assocs) {
|
|
284
|
-
if (assoc.where) {
|
|
285
|
-
for (let i = 0; i < assoc.where.length; i++) {
|
|
286
|
-
const part = assoc.where[i];
|
|
287
|
-
|
|
288
|
-
if (part._links && !(assoc.where[i - 1] && assoc.where[i - 1] === 'exists')) {
|
|
289
|
-
for (const link of part._links) {
|
|
290
|
-
if (link.art && link.art.target) {
|
|
291
|
-
if (link.art.keys) { // managed - allow FK access
|
|
292
|
-
if (part._links[link.idx + 1] !== undefined) { // there is a next path step - check if it is a fk
|
|
293
|
-
if (!(part._links[link.idx + 1] && link.art.keys.some(fk => fk._art === part._links[link.idx + 1].art)))
|
|
294
|
-
error(null, part.$path, { id: assoc.id, name: assoc.where[part.$path[part.$path.length - 1]].ref[link.idx] }, 'Unexpected non foreign key access after managed association $(NAME) in filter expression of $(ID)');
|
|
295
|
-
}
|
|
296
|
-
else { // no traversal, ends on managed
|
|
297
|
-
error(null, part.$path, { id: assoc.id, name: assoc.where[part.$path[part.$path.length - 1]].ref[link.idx] }, 'Unexpected managed association $(NAME) in filter expression of $(ID)');
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
else { // unmanaged - always wrong
|
|
301
|
-
error(null, part.$path, { id: assoc.id, name: assoc.where[part.$path[part.$path.length - 1]].ref[link.idx] }, 'Unexpected unmanaged association $(NAME) in filter expression of $(ID)');
|
|
302
|
-
}
|
|
303
|
-
// Recursively drill down if the assoc-step has a filter
|
|
304
|
-
if (part.ref[link.idx].where)
|
|
305
|
-
checkForInvalidAssoc([ part.ref[link.idx] ]);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
259
|
/**
|
|
315
260
|
* Walk to the expr using the given path and scan it for the "exists" + "ref" pattern.
|
|
316
261
|
* If such a pattern is found, nest association steps therein into filters.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const {
|
|
4
4
|
getUtils, cloneCsnNonDict, applyTransformationsOnNonDictionary, forEachDefinition,
|
|
5
5
|
} = require('../../model/csnUtils');
|
|
6
|
-
const { implicitAs, columnAlias } = require('../../model/csnRefs');
|
|
6
|
+
const { implicitAs, columnAlias, pathId } = require('../../model/csnRefs');
|
|
7
7
|
const { ModelError } = require('../../base/error');
|
|
8
8
|
const { setProp } = require('../../base/model');
|
|
9
9
|
|
|
@@ -16,20 +16,18 @@ const { setProp } = require('../../base/model');
|
|
|
16
16
|
* @returns {object} The mixin association
|
|
17
17
|
*/
|
|
18
18
|
function getMixinAssocOfQueryIfPublished( query, association, associationName ) {
|
|
19
|
-
if (query
|
|
19
|
+
if (query?.SELECT?.mixin) {
|
|
20
20
|
const aliasedColumnsMap = Object.create(null);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
aliasedColumnsMap[column.as] = column;
|
|
25
|
-
}
|
|
21
|
+
for (const column of query.SELECT.columns || []) {
|
|
22
|
+
if (column.as && column.ref?.length === 1)
|
|
23
|
+
aliasedColumnsMap[column.as] = column;
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
for (const elem of Object.keys(query.SELECT.mixin)) {
|
|
29
27
|
const mixinElement = query.SELECT.mixin[elem];
|
|
30
28
|
let originalName = associationName;
|
|
31
29
|
if (aliasedColumnsMap[associationName])
|
|
32
|
-
originalName = aliasedColumnsMap[associationName].ref[0];
|
|
30
|
+
originalName = pathId(aliasedColumnsMap[associationName].ref[0]);
|
|
33
31
|
|
|
34
32
|
if (elem === originalName)
|
|
35
33
|
return { mixinElement, mixinName: originalName };
|
|
@@ -210,11 +208,7 @@ function getViewTransformer( csn, options, messageFunctions, transformCommon ) {
|
|
|
210
208
|
function handleAssociationElement( query, elements, columnMap, publishedMixins, elem, elemName, elementsPath, queryPath ) {
|
|
211
209
|
if (isUnion(queryPath) && options.transformation === 'hdbcds') {
|
|
212
210
|
if (doA2J) {
|
|
213
|
-
|
|
214
|
-
info('query-ignoring-assoc-in-union', queryPath, { name: elemName, '#': 'managed' });
|
|
215
|
-
else
|
|
216
|
-
info('query-ignoring-assoc-in-union', queryPath, { name: elemName, '#': 'std' });
|
|
217
|
-
|
|
211
|
+
info('query-ignoring-assoc-in-union', queryPath, { name: elemName, '#': elem.keys ? 'managed' : 'std' });
|
|
218
212
|
elem.$ignore = true;
|
|
219
213
|
}
|
|
220
214
|
else {
|
|
@@ -448,7 +442,7 @@ function stripLeadingSelf( col ) {
|
|
|
448
442
|
* @returns {boolean}
|
|
449
443
|
*/
|
|
450
444
|
function checkIsNotMixinByItself( query, columnMap, elementName ) {
|
|
451
|
-
if (query
|
|
445
|
+
if (query?.SELECT?.mixin) {
|
|
452
446
|
const col = columnMap[elementName];
|
|
453
447
|
|
|
454
448
|
if (!col.ref) // No ref -> new association, but not a mixin.
|