@sap/cds-compiler 2.7.0 → 2.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -0
- package/lib/api/main.js +8 -10
- package/lib/api/options.js +13 -9
- package/lib/api/validate.js +11 -8
- package/lib/base/keywords.js +32 -2
- package/lib/base/message-registry.js +16 -0
- package/lib/base/messages.js +2 -0
- package/lib/base/model.js +1 -0
- package/lib/checks/onConditions.js +5 -0
- package/lib/checks/types.js +26 -2
- package/lib/checks/unknownMagic.js +38 -0
- package/lib/checks/validator.js +7 -2
- package/lib/compiler/assert-consistency.js +11 -5
- package/lib/compiler/builtins.js +2 -0
- package/lib/compiler/checks.js +3 -1
- package/lib/compiler/definer.js +87 -29
- package/lib/compiler/resolver.js +75 -16
- package/lib/compiler/shared.js +29 -9
- package/lib/edm/annotations/genericTranslation.js +182 -186
- package/lib/edm/csn2edm.js +93 -98
- package/lib/edm/edm.js +16 -20
- package/lib/edm/edmPreprocessor.js +274 -83
- package/lib/edm/edmUtils.js +29 -10
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +12 -1
- package/lib/gen/language.tokens +57 -53
- package/lib/gen/languageLexer.interp +10 -1
- package/lib/gen/languageLexer.js +770 -744
- package/lib/gen/languageLexer.tokens +49 -46
- package/lib/gen/languageParser.js +4727 -4323
- package/lib/json/from-csn.js +52 -23
- package/lib/json/to-csn.js +185 -71
- package/lib/language/errorStrategy.js +1 -0
- package/lib/language/genericAntlrParser.js +9 -0
- package/lib/language/language.g4 +90 -31
- package/lib/main.js +4 -0
- package/lib/model/api.js +78 -0
- package/lib/model/csnRefs.js +7 -1
- package/lib/model/csnUtils.js +5 -4
- package/lib/optionProcessor.js +7 -1
- package/lib/render/.eslintrc.json +3 -1
- package/lib/render/toCdl.js +45 -9
- package/lib/render/toHdbcds.js +100 -34
- package/lib/render/toSql.js +12 -4
- package/lib/render/utils/common.js +5 -9
- package/lib/sql-identifier.js +6 -1
- package/lib/transform/db/draft.js +6 -4
- package/lib/transform/db/expansion.js +14 -4
- package/lib/transform/db/flattening.js +13 -5
- package/lib/transform/db/transformExists.js +252 -58
- package/lib/transform/forHanaNew.js +7 -1
- package/lib/transform/forOdataNew.js +12 -8
- package/lib/transform/odata/attachPath.js +19 -4
- package/lib/transform/odata/generateForeignKeyElements.js +11 -10
- package/lib/transform/odata/referenceFlattener.js +44 -38
- package/lib/transform/odata/sortByAssociationDependency.js +2 -2
- package/lib/transform/odata/structuralPath.js +76 -0
- package/lib/transform/odata/structureFlattener.js +13 -10
- package/lib/transform/odata/typesExposure.js +22 -12
- package/lib/transform/transformUtilsNew.js +33 -1
- package/lib/transform/translateAssocsToJoins.js +6 -4
- package/lib/transform/universalCsnEnricher.js +67 -0
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { forAllQueries, forEachDefinition } = require('../../model/csnUtils');
|
|
3
|
+
const { forAllQueries, forEachDefinition, walkCsnPath } = require('../../model/csnUtils');
|
|
4
4
|
const { setProp } = require('../../base/model');
|
|
5
5
|
const { getRealName } = require('../../render/utils/common');
|
|
6
|
+
const { csnRefs } = require('../../model/csnRefs');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Turn a `exists assoc[filter = 100]` into a `exists (select 1 as dummy from assoc.target where <assoc on condition> and assoc.target.filter = 100)`.
|
|
@@ -43,23 +44,40 @@ const { getRealName } = require('../../render/utils/common');
|
|
|
43
44
|
* The final subselect looks like (select 1 as dummy from E where F.backToE.id = E.id and filter = 100).
|
|
44
45
|
*
|
|
45
46
|
* @param {CSN.Model} csn
|
|
47
|
+
* @param {CSN.Options} options
|
|
46
48
|
* @param {Function} error
|
|
47
49
|
*/
|
|
48
|
-
function handleExists(csn, error) {
|
|
50
|
+
function handleExists(csn, options, error) {
|
|
51
|
+
const { inspectRef } = csnRefs(csn);
|
|
49
52
|
forEachDefinition(csn, (artifact, artifactName) => {
|
|
50
53
|
if (artifact.query) {
|
|
51
|
-
forAllQueries(artifact.query, (query) => {
|
|
54
|
+
forAllQueries(artifact.query, (query, path) => {
|
|
52
55
|
if (!query.$generatedExists) {
|
|
56
|
+
const toProcess = []; // Collect all expressions we need to process here
|
|
53
57
|
if (query.SELECT && query.SELECT.where && query.SELECT.where.length > 1)
|
|
54
|
-
|
|
58
|
+
toProcess.push([ path.slice(0, -1), path.concat('where') ]);
|
|
55
59
|
|
|
56
60
|
|
|
57
61
|
if (query.SELECT && query.SELECT.columns)
|
|
58
|
-
|
|
62
|
+
toProcess.push([ path.slice(0, -1), path.concat('columns') ]);
|
|
59
63
|
|
|
60
64
|
|
|
61
65
|
if (query.SELECT && query.SELECT.from.on )
|
|
62
|
-
|
|
66
|
+
toProcess.push([ path.slice(0, -1), path.concat([ 'from', 'on' ]) ]);
|
|
67
|
+
|
|
68
|
+
for (const [ , exprPath ] of toProcess) {
|
|
69
|
+
const expr = nestExists(exprPath);
|
|
70
|
+
walkCsnPath(csn, exprPath.slice(0, -1))[exprPath[exprPath.length - 1]] = expr;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
while (toProcess.length > 0) {
|
|
74
|
+
const [ queryPath, exprPath ] = toProcess.pop();
|
|
75
|
+
// leftovers can happen with nested exists - we then need to drill down into the created SELECT
|
|
76
|
+
// to check for further exists
|
|
77
|
+
const { result, leftovers } = processExists(queryPath, exprPath);
|
|
78
|
+
walkCsnPath(csn, exprPath.slice(0, -1))[exprPath[exprPath.length - 1]] = result;
|
|
79
|
+
toProcess.push(...leftovers.reverse()); // any leftovers - schedule for further processing
|
|
80
|
+
}
|
|
63
81
|
}
|
|
64
82
|
}, [ 'definitions', artifactName, 'query' ]);
|
|
65
83
|
}
|
|
@@ -108,14 +126,149 @@ function handleExists(csn, error) {
|
|
|
108
126
|
}
|
|
109
127
|
|
|
110
128
|
/**
|
|
111
|
-
*
|
|
129
|
+
* Get the index of the first association that is found - starting the
|
|
130
|
+
* search at the given startIndex.
|
|
112
131
|
*
|
|
113
|
-
* @param {
|
|
114
|
-
* @param {
|
|
115
|
-
* @returns {
|
|
132
|
+
* @param {number} startIndex Where to start searching
|
|
133
|
+
* @param {object[]} links links for a ref, produced by inspectRef
|
|
134
|
+
* @returns {number|null} Null if no association was found
|
|
135
|
+
*/
|
|
136
|
+
function getFirstAssocIndex(startIndex, links) {
|
|
137
|
+
for (let i = startIndex; i < links.length; i++) {
|
|
138
|
+
if (links[i] && links[i].art && links[i].art.target)
|
|
139
|
+
return i;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* For a given ref-array, this function is called for the first assoc-ref in the array.
|
|
147
|
+
*
|
|
148
|
+
* It then runs over the rest of the array and puts all other steps in the first assocs filter.
|
|
149
|
+
* If the rest contains another assoc, we put all following things into that assocs filter and
|
|
150
|
+
* add the sub-assoc to the previous assoc filter.
|
|
151
|
+
*
|
|
152
|
+
* Or in other words:
|
|
153
|
+
* - exists toF[1=1].toG[1=1].toH[1=1] is found
|
|
154
|
+
* - we get called with toF[1=1].toG[1=1].toH[1=1]
|
|
155
|
+
* - we return toF[1=1 and exists toG[1=1 and exists toH[1=1]]]
|
|
156
|
+
*
|
|
157
|
+
* @param {number} startIndex The index of the thing AFTER _main in the ref-array
|
|
158
|
+
* @param {string|object} startAssoc The path step that is the first assoc
|
|
159
|
+
* @param {Array} startRest Any path steps after startAssoc
|
|
160
|
+
* @param {CSN.Path} path to the overall ref where _main is contained
|
|
161
|
+
* @returns {Array} Return the now-nested ref-array
|
|
162
|
+
*/
|
|
163
|
+
function nestFilters(startIndex, startAssoc, startRest, path) {
|
|
164
|
+
let revert;
|
|
165
|
+
if (!startAssoc.where) { // initialize first filter if not present
|
|
166
|
+
if (typeof startAssoc === 'string') {
|
|
167
|
+
startAssoc = {
|
|
168
|
+
id: startAssoc,
|
|
169
|
+
where: [],
|
|
170
|
+
};
|
|
171
|
+
revert = () => {
|
|
172
|
+
startAssoc = startAssoc.id;
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
startAssoc.where = [];
|
|
177
|
+
revert = () => {
|
|
178
|
+
delete startAssoc.where;
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const stack = [ [ null, startAssoc, startRest, startIndex ] ];
|
|
183
|
+
const { links } = inspectRef(path);
|
|
184
|
+
while (stack.length > 0) {
|
|
185
|
+
// previous: to nest "up" if the previous assoc did not originaly have a filter
|
|
186
|
+
// assoc: the assoc path step
|
|
187
|
+
// rest: path steps after assoc
|
|
188
|
+
// index: index of after-assoc in the overall ref-array - so we know where to start looking for the next assoc
|
|
189
|
+
const workPackage = stack.pop();
|
|
190
|
+
const [ previous, , rest, index ] = workPackage;
|
|
191
|
+
let [ , assoc, , ] = workPackage;
|
|
192
|
+
|
|
193
|
+
const firstAssocIndex = getFirstAssocIndex(index, links);
|
|
194
|
+
|
|
195
|
+
const head = rest.slice(0, firstAssocIndex - index);
|
|
196
|
+
const nextAssoc = rest[firstAssocIndex - index];
|
|
197
|
+
const tail = rest.slice(firstAssocIndex - index + 1);
|
|
198
|
+
|
|
199
|
+
const hasAssoc = nextAssoc !== undefined;
|
|
200
|
+
|
|
201
|
+
if (!assoc.where && hasAssoc) { // no existing filter - and there is stuff we need to nest afterwards
|
|
202
|
+
if (typeof assoc === 'string') {
|
|
203
|
+
assoc = {
|
|
204
|
+
id: assoc,
|
|
205
|
+
where: [],
|
|
206
|
+
};
|
|
207
|
+
// We need to "hook" this into the previous filter.
|
|
208
|
+
// Since we create a new object, we don't have a handy reference we can just manipulate
|
|
209
|
+
if (previous)
|
|
210
|
+
previous.where[previous.where.length - 1] = { ref: [ assoc ] };
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
assoc.where = [];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else if (assoc.where && assoc.where.length > 0 && (hasAssoc || rest.length > 0)) {
|
|
217
|
+
assoc.where.push('and');
|
|
218
|
+
} // merge with existing filter
|
|
219
|
+
|
|
220
|
+
if (hasAssoc)
|
|
221
|
+
assoc.where.push('exists', { ref: [ ...head, nextAssoc ] });
|
|
222
|
+
else if (rest.length > 0)
|
|
223
|
+
assoc.where.push({ ref: rest });
|
|
224
|
+
|
|
225
|
+
if (hasAssoc)
|
|
226
|
+
stack.push([ assoc, nextAssoc, tail, firstAssocIndex ]);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Seems like we did not have anything to nest into the filter - then kill it
|
|
230
|
+
if (startAssoc.where.length === 0 && revert !== undefined)
|
|
231
|
+
revert();
|
|
232
|
+
|
|
233
|
+
return startAssoc;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Walk to the expr using the given path and scan it for the "exists" + "ref" pattern.
|
|
238
|
+
* If such a pattern is found, nest association steps therein into filters.
|
|
239
|
+
*
|
|
240
|
+
* @param {CSN.Path} exprPath
|
|
241
|
+
* @returns {Array}
|
|
116
242
|
*/
|
|
117
|
-
function
|
|
243
|
+
function nestExists(exprPath) {
|
|
244
|
+
const expr = walkCsnPath(csn, exprPath);
|
|
245
|
+
for (let i = 0; i < expr.length; i++) {
|
|
246
|
+
if (i < expr.length - 1 && expr[i] === 'exists' && expr[i + 1].ref) {
|
|
247
|
+
i++;
|
|
248
|
+
const current = expr[i];
|
|
249
|
+
const {
|
|
250
|
+
ref, head, tail,
|
|
251
|
+
} = getFirstAssoc(current, exprPath.concat(i));
|
|
252
|
+
const newThing = [ ...head, nestFilters(head.length + 1, ref, tail, exprPath.concat([ i ])) ];
|
|
253
|
+
expr[i].ref = newThing;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return expr;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Process the given expr of the given query and translate a `EXISTS assoc` into a `EXISTS (subquery)`. Also, return paths to things we need to process in a second step.
|
|
262
|
+
*
|
|
263
|
+
* @param {CSN.Path} queryPath Path to the query-object
|
|
264
|
+
* @param {CSN.Path} exprPath Path to the expression-array to process
|
|
265
|
+
* @returns {{result: TokenStream, leftovers: Array[]}} result: A new token stream expression - the same as expr, but with the expanded EXISTS, leftovers: path-tuples to further subqueries to process.
|
|
266
|
+
*/
|
|
267
|
+
function processExists(queryPath, exprPath) {
|
|
268
|
+
const toContinue = [];
|
|
118
269
|
const newExpr = [];
|
|
270
|
+
const query = walkCsnPath(csn, queryPath);
|
|
271
|
+
const expr = walkCsnPath(csn, exprPath);
|
|
119
272
|
const queryBase = query.SELECT.from.ref ? (query.SELECT.from.as || query.SELECT.from.ref[0]) : null;
|
|
120
273
|
const sources = getQuerySources(query.SELECT);
|
|
121
274
|
|
|
@@ -123,9 +276,9 @@ function handleExists(csn, error) {
|
|
|
123
276
|
if (i < expr.length - 1 && expr[i] === 'exists' && expr[i + 1].ref) {
|
|
124
277
|
i++;
|
|
125
278
|
const current = expr[i];
|
|
126
|
-
const isPrefixedWithTableAlias = firstLinkIsEntityOrQuerySource(
|
|
127
|
-
const base = getBase(queryBase, isPrefixedWithTableAlias, current);
|
|
128
|
-
const { root, ref, tail } = getFirstAssoc(current);
|
|
279
|
+
const isPrefixedWithTableAlias = firstLinkIsEntityOrQuerySource(exprPath.concat(i));
|
|
280
|
+
const base = getBase(queryBase, isPrefixedWithTableAlias, current, exprPath.concat(i));
|
|
281
|
+
const { root, ref, tail } = getFirstAssoc(current, exprPath.concat(i));
|
|
129
282
|
|
|
130
283
|
if (tail.length > 0) {
|
|
131
284
|
error(null, current.$path, { id: tail[0].id ? tail[0].id : tail[0], name: ref.id ? ref.id : ref }, 'Unexpected path step $(ID) after association $(NAME) in "EXISTS"');
|
|
@@ -134,7 +287,7 @@ function handleExists(csn, error) {
|
|
|
134
287
|
|
|
135
288
|
if (!root.target) {
|
|
136
289
|
error(null, current.$path, { type: root.type }, '“EXISTS” can only be used with associations/compositions, found $(TYPE)');
|
|
137
|
-
return [];
|
|
290
|
+
return { result: [], leftovers: [] };
|
|
138
291
|
}
|
|
139
292
|
|
|
140
293
|
const subselect = getSubselect(root.target, ref, sources);
|
|
@@ -152,18 +305,24 @@ function handleExists(csn, error) {
|
|
|
152
305
|
subselect.SELECT.where.push(...[ 'and', ...remapExistingWhere(target, ref.where) ]);
|
|
153
306
|
|
|
154
307
|
newExpr.push(subselect);
|
|
308
|
+
toContinue.push([ exprPath.concat(newExpr.length - 1), exprPath.concat([ newExpr.length - 1, 'SELECT', 'where' ]) ]);
|
|
155
309
|
}
|
|
156
310
|
else { // Drill down into other places that might contain a `EXISTS <assoc>`
|
|
157
|
-
if (expr[i].xpr)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
311
|
+
if (expr[i].xpr) {
|
|
312
|
+
const { result, leftovers } = processExists(queryPath, exprPath.concat([ i, 'xpr' ]));
|
|
313
|
+
expr[i].xpr = result;
|
|
314
|
+
toContinue.push(...leftovers);
|
|
315
|
+
}
|
|
316
|
+
if (expr[i].args && Array.isArray(expr[i].args)) {
|
|
317
|
+
const { result, leftovers } = processExists(queryPath, exprPath.concat([ i, 'args' ]));
|
|
318
|
+
expr[i].args = result;
|
|
319
|
+
toContinue.push(...leftovers);
|
|
320
|
+
}
|
|
162
321
|
newExpr.push(expr[i]);
|
|
163
322
|
}
|
|
164
323
|
}
|
|
165
324
|
|
|
166
|
-
return newExpr;
|
|
325
|
+
return { result: newExpr, leftovers: toContinue };
|
|
167
326
|
}
|
|
168
327
|
|
|
169
328
|
/**
|
|
@@ -229,23 +388,35 @@ function handleExists(csn, error) {
|
|
|
229
388
|
function translateUnmanagedAssocToWhere(root, target, subselect, isPrefixedWithTableAlias, base, current) {
|
|
230
389
|
for (let j = 0; j < root.on.length; j++) {
|
|
231
390
|
const part = root.on[j];
|
|
391
|
+
|
|
392
|
+
// we can only resolve stuff on refs - skip literals like =
|
|
393
|
+
// but also keep along stuff like null and undefined, so compiler
|
|
394
|
+
// can have a chance to complain/ we can fail later nicely maybe
|
|
395
|
+
if (!(part && part.ref)) {
|
|
396
|
+
subselect.SELECT.where.push(part);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// root.$path should be safe - we can only reference things in exists that exist when we enrich
|
|
401
|
+
// so all of them should have a $path.
|
|
402
|
+
const { art, links } = inspectRef(root.$path.concat([ 'on', j ]));
|
|
232
403
|
// Dollar Self Backlink
|
|
233
|
-
if (isValidDollarSelf(root.on[j], root.on[j + 1], root.on[j + 2])) {
|
|
404
|
+
if (isValidDollarSelf(root.on[j], root.$path.concat([ 'on', j ]), root.on[j + 1], root.on[j + 2], root.$path.concat([ 'on', j + 2 ]))) {
|
|
234
405
|
if (root.on[j].ref[0] === '$self' && root.on[j].ref.length === 1)
|
|
235
|
-
subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j + 2]));
|
|
406
|
+
subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j + 2], root.$path.concat([ 'on', j + 2 ])));
|
|
236
407
|
else
|
|
237
|
-
subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j]));
|
|
408
|
+
subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j], root.$path.concat([ 'on', j ])));
|
|
238
409
|
|
|
239
410
|
j += 2;
|
|
240
411
|
}
|
|
241
|
-
else if (
|
|
412
|
+
else if (links && links[0].art === root) { // target side
|
|
242
413
|
subselect.SELECT.where.push({ ref: [ target, ...part.ref.slice(1) ] });
|
|
243
414
|
}
|
|
244
415
|
else if (part.$scope === '$self') { // source side - "absolute" scope
|
|
245
416
|
// cut off the $self, as we prefix the entity name now
|
|
246
417
|
subselect.SELECT.where.push({ ref: [ base, ...part.ref.slice(1) ] });
|
|
247
418
|
}
|
|
248
|
-
else if (
|
|
419
|
+
else if (art) { // source side - with local scope
|
|
249
420
|
if (isPrefixedWithTableAlias)
|
|
250
421
|
subselect.SELECT.where.push({ ref: [ ...current.ref.slice(0, -1), ...part.ref ] });
|
|
251
422
|
else
|
|
@@ -260,43 +431,61 @@ function handleExists(csn, error) {
|
|
|
260
431
|
* Check that an expression triple is a valid $self
|
|
261
432
|
*
|
|
262
433
|
* @param {Token} leftSide
|
|
434
|
+
* @param {CSN.Path} pathLeft
|
|
263
435
|
* @param {Token} middle
|
|
264
436
|
* @param {Token} rightSide
|
|
437
|
+
* @param {CSN.Path} pathRight
|
|
265
438
|
* @returns {boolean}
|
|
266
439
|
*/
|
|
267
|
-
function isValidDollarSelf(leftSide, middle, rightSide) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
440
|
+
function isValidDollarSelf(leftSide, pathLeft, middle, rightSide, pathRight) {
|
|
441
|
+
if (leftSide && leftSide.ref && rightSide && rightSide.ref && middle === '=') {
|
|
442
|
+
const right = inspectRef(pathRight);
|
|
443
|
+
const left = inspectRef(pathLeft);
|
|
444
|
+
|
|
445
|
+
if (!right || !left)
|
|
446
|
+
return false;
|
|
447
|
+
|
|
448
|
+
const rightSideArt = right.art;
|
|
449
|
+
const leftSideArt = left.art;
|
|
450
|
+
|
|
451
|
+
return leftSide.ref[0] === '$self' && leftSide.ref.length === 1 && rightSideArt && rightSideArt.target ||
|
|
452
|
+
rightSide.ref[0] === '$self' && rightSide.ref.length === 1 && leftSideArt && leftSideArt.target;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return false;
|
|
275
456
|
}
|
|
276
457
|
}
|
|
277
458
|
|
|
278
459
|
/**
|
|
279
|
-
* From the given expression (having
|
|
460
|
+
* From the given expression (having inspectRef -> links), find the first association.
|
|
280
461
|
*
|
|
281
462
|
* @param {object} xprPart
|
|
282
|
-
* @
|
|
463
|
+
* @param {CSN.Path} path
|
|
464
|
+
* @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).
|
|
283
465
|
*/
|
|
284
|
-
function getFirstAssoc(xprPart) {
|
|
466
|
+
function getFirstAssoc(xprPart, path) {
|
|
467
|
+
const { links, art } = inspectRef(path);
|
|
285
468
|
for (let i = 0; i < xprPart.ref.length - 1; i++) {
|
|
286
|
-
if (
|
|
287
|
-
return {
|
|
469
|
+
if (links[i].art && links[i].art.target) {
|
|
470
|
+
return {
|
|
471
|
+
head: (i === 0 ? [] : xprPart.ref.slice(0, i)), root: links[i].art, ref: xprPart.ref[i], tail: xprPart.ref.slice(i + 1),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
288
474
|
}
|
|
289
|
-
return {
|
|
475
|
+
return {
|
|
476
|
+
head: (xprPart.ref.length === 1 ? [] : xprPart.ref.slice(0, xprPart.ref.length - 1)), root: art, ref: xprPart.ref[xprPart.ref.length - 1], tail: [],
|
|
477
|
+
};
|
|
290
478
|
}
|
|
291
479
|
|
|
292
480
|
/**
|
|
293
|
-
* Check (using
|
|
481
|
+
* Check (using inspectRef -> links), wether the first path step is an entity or query source
|
|
294
482
|
*
|
|
295
|
-
* @param {
|
|
483
|
+
* @param {CSN.Path} path
|
|
296
484
|
* @returns {boolean}
|
|
297
485
|
*/
|
|
298
|
-
function firstLinkIsEntityOrQuerySource(
|
|
299
|
-
|
|
486
|
+
function firstLinkIsEntityOrQuerySource(path) {
|
|
487
|
+
const { links } = inspectRef(path);
|
|
488
|
+
return links && (links[0].art.kind === 'entity' || links[0].art.query || links[0].art.from);
|
|
300
489
|
}
|
|
301
490
|
|
|
302
491
|
/**
|
|
@@ -316,13 +505,14 @@ function handleExists(csn, error) {
|
|
|
316
505
|
* we can be sure that resolving the ref requires $env information.
|
|
317
506
|
*
|
|
318
507
|
* @param {object} xpr
|
|
508
|
+
* @param {CSN.Path} path
|
|
319
509
|
* @returns {string|undefined} undefined in case of errors
|
|
320
510
|
* @throws {Error} Throws if xpr.ref but no xpr.$env
|
|
321
511
|
* @todo $env is going to be removed from CSN, but csnRefs will provide it
|
|
322
512
|
*/
|
|
323
513
|
// eslint-disable-next-line consistent-return
|
|
324
|
-
function getParent(xpr) {
|
|
325
|
-
if (firstLinkIsEntityOrQuerySource(
|
|
514
|
+
function getParent(xpr, path) {
|
|
515
|
+
if (firstLinkIsEntityOrQuerySource(path)) {
|
|
326
516
|
return xpr.ref[0];
|
|
327
517
|
}
|
|
328
518
|
else if (xpr.$env) {
|
|
@@ -384,14 +574,15 @@ function handleExists(csn, error) {
|
|
|
384
574
|
* @param {string|null} queryBase
|
|
385
575
|
* @param {boolean} isPrefixedWithTableAlias
|
|
386
576
|
* @param {CSN.Column} current
|
|
577
|
+
* @param {CSN.Path} path
|
|
387
578
|
* @returns {string}
|
|
388
579
|
*/
|
|
389
|
-
function getBase(queryBase, isPrefixedWithTableAlias, current) {
|
|
580
|
+
function getBase(queryBase, isPrefixedWithTableAlias, current, path) {
|
|
390
581
|
if (queryBase)
|
|
391
582
|
return getRealName(csn, queryBase);
|
|
392
583
|
else if (isPrefixedWithTableAlias)
|
|
393
584
|
return current.ref[0];
|
|
394
|
-
return getParent(current);
|
|
585
|
+
return getParent(current, path);
|
|
395
586
|
}
|
|
396
587
|
|
|
397
588
|
|
|
@@ -423,31 +614,34 @@ function handleExists(csn, error) {
|
|
|
423
614
|
* @param {string} base The source entity/query source name
|
|
424
615
|
* @param {string} target The target entity/query source name
|
|
425
616
|
* @param {CSN.Element} assoc The association element - the "not-$self" side of the comparison
|
|
617
|
+
* @param {CSN.Path} path
|
|
426
618
|
* @returns {TokenStream} The WHERE representing the $self comparison
|
|
427
619
|
*/
|
|
428
|
-
function translateDollarSelfToWhere(base, target, assoc) {
|
|
620
|
+
function translateDollarSelfToWhere(base, target, assoc, path) {
|
|
429
621
|
const where = [];
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
622
|
+
const { art } = inspectRef(path);
|
|
623
|
+
if (art.keys) {
|
|
624
|
+
for (let i = 0; i < art.keys.length; i++) {
|
|
625
|
+
const lop = { ref: [ target, ...assoc.ref.slice(1), ...art.keys[i].ref ] }; // target side
|
|
626
|
+
const rop = { ref: [ base, ...art.keys[i].ref ] }; // source side
|
|
434
627
|
if (i > 0)
|
|
435
628
|
where.push('and');
|
|
436
629
|
|
|
437
630
|
where.push(...[ lop, '=', rop ]);
|
|
438
631
|
}
|
|
439
632
|
}
|
|
440
|
-
else if (
|
|
441
|
-
for (let i = 0; i <
|
|
442
|
-
const part =
|
|
443
|
-
|
|
633
|
+
else if (art.on) {
|
|
634
|
+
for (let i = 0; i < art.on.length; i++) {
|
|
635
|
+
const part = art.on[i];
|
|
636
|
+
const partInspect = inspectRef(art.$path.concat([ 'on', i ]));
|
|
637
|
+
if (partInspect.links && partInspect.links[0].art === art) { // target side
|
|
444
638
|
where.push({ ref: [ base, ...part.ref.slice(1) ] });
|
|
445
639
|
}
|
|
446
640
|
else if (part.$scope === '$self') { // source side - "absolute" scope
|
|
447
641
|
// Same message as in forHanaNew/transformDollarSelfComparisonWithUnmanagedAssoc
|
|
448
642
|
error(null, part.$path, 'An association that uses "$self" in its ON-condition can\'t be compared to "$self"');
|
|
449
643
|
}
|
|
450
|
-
else if (
|
|
644
|
+
else if (partInspect.art) { // source side - with local scope
|
|
451
645
|
where.push({ ref: [ target, ...assoc.ref.slice(1, -1), ...part.ref ] });
|
|
452
646
|
}
|
|
453
647
|
else { // operator - or any other leftover
|
|
@@ -26,6 +26,7 @@ const flattening = require('./db/flattening');
|
|
|
26
26
|
const expansion = require('./db/expansion');
|
|
27
27
|
const assertUnique = require('./db/assertUnique');
|
|
28
28
|
const generateDrafts = require('./db/draft');
|
|
29
|
+
const enrichUniversalCsn = require('./universalCsnEnricher');
|
|
29
30
|
|
|
30
31
|
// By default: Do not process non-entities/views
|
|
31
32
|
function forEachDefinition(csn, cb) {
|
|
@@ -121,6 +122,11 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
121
122
|
bindCsnReference();
|
|
122
123
|
|
|
123
124
|
throwWithError(); // reclassify and throw in case of non-configurable errors
|
|
125
|
+
|
|
126
|
+
if (options.csnFlavor === 'universal' && isBetaEnabled(options, 'enableUniversalCsn')) {
|
|
127
|
+
enrichUniversalCsn(csn, options);
|
|
128
|
+
bindCsnReference();
|
|
129
|
+
}
|
|
124
130
|
|
|
125
131
|
const dialect = options.forHana && options.forHana.dialect || options.toSql && options.toSql.dialect;
|
|
126
132
|
const doA2J = !(options.transformation === 'hdbcds' && options.sqlMapping === 'hdbcds');
|
|
@@ -144,7 +150,7 @@ function transformForHanaWithCsn(inputModel, options, moduleName) {
|
|
|
144
150
|
// FIXME: This does something very similar to cloneWithTransformations -> refactor?
|
|
145
151
|
const transformCsn = transformUtils.transformModel;
|
|
146
152
|
|
|
147
|
-
handleExists(csn, error);
|
|
153
|
+
handleExists(csn, options, error);
|
|
148
154
|
|
|
149
155
|
// (001) Add a temporal where condition to views where applicable before assoc2join
|
|
150
156
|
// assoc2join eventually rewrites the table aliases
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { makeMessageFunction } = require('../base/messages');
|
|
4
|
-
const { isDeprecatedEnabled } = require('../base/model');
|
|
4
|
+
const { isDeprecatedEnabled, isBetaEnabled } = require('../base/model');
|
|
5
5
|
const transformUtils = require('./transformUtilsNew');
|
|
6
6
|
const { getUtils,
|
|
7
7
|
cloneCsn,
|
|
@@ -24,6 +24,7 @@ const expandStructKeysInAssociations = require('./odata/expandStructKeysInAssoci
|
|
|
24
24
|
const expandToFinalBaseType = require('./odata/toFinalBaseType');
|
|
25
25
|
const timetrace = require('../utils/timetrace');
|
|
26
26
|
const { attachPath } = require('./odata/attachPath');
|
|
27
|
+
const enrichUniversalCsn = require('./universalCsnEnricher');
|
|
27
28
|
|
|
28
29
|
const { addLocalizationViews } = require('./localized');
|
|
29
30
|
|
|
@@ -89,7 +90,7 @@ function transform4odataWithCsn(inputModel, options) {
|
|
|
89
90
|
addElement, createAction, assignAction,
|
|
90
91
|
extractValidFromToKeyElement,
|
|
91
92
|
checkAssignment, checkMultipleAssignments,
|
|
92
|
-
recurseElements, setAnnotation, renameAnnotation,
|
|
93
|
+
recurseElements, setAnnotation, resetAnnotation, renameAnnotation,
|
|
93
94
|
expandStructsInExpression
|
|
94
95
|
} = transformers;
|
|
95
96
|
|
|
@@ -119,6 +120,9 @@ function transform4odataWithCsn(inputModel, options) {
|
|
|
119
120
|
// @ts-ignore
|
|
120
121
|
const isExternalServiceMember = (_art, name) => externalServices.includes(getServiceName(name));
|
|
121
122
|
|
|
123
|
+
if (options.csnFlavor === 'universal' && isBetaEnabled(options, 'enableUniversalCsn'))
|
|
124
|
+
enrichUniversalCsn(csn, options);
|
|
125
|
+
|
|
122
126
|
const keepLocalizedViews = isDeprecatedEnabled(options, 'createLocalizedViews');
|
|
123
127
|
|
|
124
128
|
function acceptLocalizedView(_name, parent) {
|
|
@@ -326,7 +330,7 @@ function transform4odataWithCsn(inputModel, options) {
|
|
|
326
330
|
renameAnnotation(node, name, '@UI.Importance');
|
|
327
331
|
let annotation = node['@UI.Importance'];
|
|
328
332
|
if (annotation !== null)
|
|
329
|
-
node['@UI.Importance'] = { '#': annotation ? 'High' : 'Low' }
|
|
333
|
+
node['@UI.Importance'] = { '#': annotation ? 'High' : 'Low' };
|
|
330
334
|
}
|
|
331
335
|
|
|
332
336
|
// Special case: '@readonly' becomes a triplet of capability restrictions for entities,
|
|
@@ -457,11 +461,11 @@ function transform4odataWithCsn(inputModel, options) {
|
|
|
457
461
|
}
|
|
458
462
|
// Generate the annotations describing the draft actions (only draft roots can be activated/edited)
|
|
459
463
|
if (artifact == rootArtifact) {
|
|
460
|
-
artifact
|
|
461
|
-
artifact
|
|
462
|
-
artifact
|
|
464
|
+
resetAnnotation(artifact, '@Common.DraftRoot.ActivationAction', 'draftActivate', info, ['definitions', draftAdminDataProjectionName]);
|
|
465
|
+
resetAnnotation(artifact, '@Common.DraftRoot.EditAction', 'draftEdit', info, ['definitions', draftAdminDataProjectionName]);
|
|
466
|
+
resetAnnotation(artifact, '@Common.DraftRoot.PreparationAction', 'draftPrepare', info, ['definitions', draftAdminDataProjectionName]);
|
|
463
467
|
} else {
|
|
464
|
-
artifact
|
|
468
|
+
resetAnnotation(artifact, '@Common.DraftNode.PreparationAction', 'draftPrepare', info, ['definitions', draftAdminDataProjectionName]);
|
|
465
469
|
}
|
|
466
470
|
|
|
467
471
|
artifact.elements && Object.values(artifact.elements).forEach( elem => {
|
|
@@ -529,7 +533,7 @@ function transform4odataWithCsn(inputModel, options) {
|
|
|
529
533
|
|
|
530
534
|
// Ignore if that is our own draft root
|
|
531
535
|
if (draftNode != rootArtifact) {
|
|
532
|
-
//
|
|
536
|
+
// Report error when the draft node has @odata.draft.enabled itself
|
|
533
537
|
if (hasAnnotationValue(draftNode, '@odata.draft.enabled', true)) {
|
|
534
538
|
error(null, ['definitions', artifactName, 'elements', elemName], 'Composition in draft-enabled entity can\'t lead to another entity with “@odata.draft.enabled”');
|
|
535
539
|
}
|
|
@@ -24,6 +24,8 @@ const structuralNodeHandlers = {
|
|
|
24
24
|
groupBy: traverseArray,
|
|
25
25
|
having: traverseArray,
|
|
26
26
|
xpr: traverseArray,
|
|
27
|
+
expand: traverseArray,
|
|
28
|
+
inline: traverseArray,
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
function attachPath(csn) {
|
|
@@ -38,6 +40,7 @@ function attachPathOnPartialCSN(csnPart, pathPrefix) {
|
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
function traverseRef(obj, path) {
|
|
43
|
+
if(!obj) return;
|
|
41
44
|
setPath(obj, path);
|
|
42
45
|
traverseArray(obj, path);
|
|
43
46
|
}
|
|
@@ -48,7 +51,7 @@ function traverseArray(obj, path) {
|
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
function traverseDict(obj, path) {
|
|
51
|
-
if(typeof obj !== 'object') return;
|
|
54
|
+
if(!obj || typeof obj !== 'object') return;
|
|
52
55
|
forAllEnumerableProperties(obj, name => {
|
|
53
56
|
const ipath = path.concat(name);
|
|
54
57
|
setPath(obj[name], ipath);
|
|
@@ -56,17 +59,29 @@ function traverseDict(obj, path) {
|
|
|
56
59
|
})
|
|
57
60
|
}
|
|
58
61
|
|
|
62
|
+
function traverseDictArray(obj, path) {
|
|
63
|
+
if(!obj || typeof obj !== 'object') return;
|
|
64
|
+
forAllEnumerableProperties(obj, name => {
|
|
65
|
+
const ipath = path.concat(name);
|
|
66
|
+
setPath(obj[name], ipath);
|
|
67
|
+
traverseArray(obj[name], ipath);
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
function traverseTyped(obj, path) {
|
|
60
|
-
if(!obj) return;
|
|
72
|
+
if(!obj || typeof obj !== 'object') return;
|
|
61
73
|
forAllEnumerableProperties(obj, name => {
|
|
62
74
|
if(name[0]==='@') return; // skip annotations
|
|
63
75
|
const func = structuralNodeHandlers[name];
|
|
64
|
-
if(func)
|
|
76
|
+
if(func)
|
|
77
|
+
func(obj[name], path.concat(name));
|
|
78
|
+
else if(path[path.length-2] === 'columns')
|
|
79
|
+
traverseDictArray(obj[name], path.concat(name)); // for columns
|
|
65
80
|
})
|
|
66
81
|
}
|
|
67
82
|
|
|
68
83
|
function setPath(obj, path) {
|
|
69
|
-
if(typeof obj !== 'object') return;
|
|
84
|
+
if(!obj || typeof obj !== 'object') return;
|
|
70
85
|
if(path.length>0)
|
|
71
86
|
Object.defineProperty( obj, '$path', { value: path, configurable: true, writable: true, enumerable: false } );
|
|
72
87
|
}
|