@sap/cds-compiler 2.10.2 → 2.11.4
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 +90 -5
- package/bin/.eslintrc.json +1 -2
- package/bin/cds_update_identifiers.js +3 -1
- package/bin/cdsc.js +49 -25
- package/bin/cdsse.js +1 -0
- package/bin/cdsv2m.js +3 -2
- package/doc/CHANGELOG_BETA.md +10 -0
- package/lib/api/.eslintrc.json +2 -0
- package/lib/api/main.js +8 -36
- package/lib/api/options.js +15 -6
- package/lib/api/validate.js +30 -3
- package/lib/backends.js +12 -13
- package/lib/base/dictionaries.js +2 -1
- package/lib/base/keywords.js +3 -2
- package/lib/base/message-registry.js +34 -10
- package/lib/base/messages.js +38 -18
- package/lib/base/model.js +5 -4
- package/lib/base/optionProcessorHelper.js +57 -23
- package/lib/checks/emptyOrOnlyVirtual.js +2 -2
- package/lib/checks/selectItems.js +4 -0
- package/lib/checks/unknownMagic.js +6 -3
- package/lib/compiler/assert-consistency.js +9 -2
- package/lib/compiler/base.js +65 -0
- package/lib/compiler/builtins.js +62 -16
- package/lib/compiler/checks.js +2 -1
- package/lib/compiler/definer.js +66 -108
- package/lib/compiler/index.js +29 -29
- package/lib/compiler/propagator.js +5 -2
- package/lib/compiler/resolver.js +225 -58
- package/lib/compiler/shared.js +53 -229
- package/lib/compiler/utils.js +184 -0
- package/lib/edm/annotations/genericTranslation.js +1 -1
- package/lib/edm/csn2edm.js +3 -2
- package/lib/edm/edmPreprocessor.js +34 -38
- package/lib/edm/edmUtils.js +3 -3
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +17 -1
- package/lib/gen/language.tokens +79 -73
- package/lib/gen/languageLexer.interp +19 -1
- package/lib/gen/languageLexer.js +779 -731
- package/lib/gen/languageLexer.tokens +71 -65
- package/lib/gen/languageParser.js +4668 -4072
- package/lib/json/from-csn.js +10 -10
- package/lib/json/to-csn.js +228 -47
- package/lib/language/antlrParser.js +11 -0
- package/lib/language/errorStrategy.js +26 -8
- package/lib/language/genericAntlrParser.js +73 -14
- package/lib/language/language.g4 +79 -3
- package/lib/main.d.ts +215 -18
- package/lib/main.js +3 -1
- package/lib/model/api.js +2 -2
- package/lib/model/csnRefs.js +117 -33
- package/lib/model/csnUtils.js +65 -133
- package/lib/model/enrichCsn.js +62 -37
- package/lib/model/revealInternalProperties.js +25 -8
- package/lib/model/sortViews.js +8 -1
- package/lib/modelCompare/compare.js +2 -1
- package/lib/optionProcessor.js +33 -18
- package/lib/render/.eslintrc.json +1 -2
- package/lib/render/DuplicateChecker.js +1 -1
- package/lib/render/toCdl.js +15 -8
- package/lib/render/toHdbcds.js +26 -49
- package/lib/render/toSql.js +61 -39
- package/lib/render/utils/common.js +1 -1
- package/lib/transform/db/applyTransformations.js +189 -0
- package/lib/transform/db/constraints.js +273 -119
- package/lib/transform/db/draft.js +3 -2
- package/lib/transform/db/expansion.js +6 -4
- package/lib/transform/db/flattening.js +19 -3
- package/lib/transform/db/transformExists.js +102 -9
- package/lib/transform/db/views.js +485 -0
- package/lib/transform/forHanaNew.js +93 -448
- package/lib/transform/forOdataNew.js +9 -2
- package/lib/transform/localized.js +2 -0
- package/lib/transform/odata/structuralPath.js +1 -5
- package/lib/transform/transformUtilsNew.js +22 -8
- package/lib/transform/translateAssocsToJoins.js +7 -15
- package/lib/utils/file.js +11 -5
- package/lib/utils/term.js +65 -42
- package/lib/utils/timetrace.js +48 -26
- package/package.json +1 -1
- package/lib/transform/db/helpers.js +0 -58
|
@@ -49,10 +49,14 @@ const { csnRefs } = require('../../model/csnRefs');
|
|
|
49
49
|
*/
|
|
50
50
|
function handleExists(csn, options, error) {
|
|
51
51
|
const { inspectRef } = csnRefs(csn);
|
|
52
|
+
const generatedExists = new WeakMap();
|
|
52
53
|
forEachDefinition(csn, (artifact, artifactName) => {
|
|
54
|
+
if (artifact.projection) // do the same hack we do for the other stuff...
|
|
55
|
+
artifact.query = { SELECT: artifact.projection };
|
|
56
|
+
|
|
53
57
|
if (artifact.query) {
|
|
54
58
|
forAllQueries(artifact.query, (query, path) => {
|
|
55
|
-
if (!query
|
|
59
|
+
if (!generatedExists.has(query)) {
|
|
56
60
|
const toProcess = []; // Collect all expressions we need to process here
|
|
57
61
|
if (query.SELECT && query.SELECT.where && query.SELECT.where.length > 1)
|
|
58
62
|
toProcess.push([ path.slice(0, -1), path.concat('where') ]);
|
|
@@ -66,6 +70,7 @@ function handleExists(csn, options, error) {
|
|
|
66
70
|
toProcess.push([ path.slice(0, -1), path.concat([ 'from', 'on' ]) ]);
|
|
67
71
|
|
|
68
72
|
for (const [ , exprPath ] of toProcess) {
|
|
73
|
+
forbidAssocInExists(exprPath);
|
|
69
74
|
const expr = nestExists(exprPath);
|
|
70
75
|
walkCsnPath(csn, exprPath.slice(0, -1))[exprPath[exprPath.length - 1]] = expr;
|
|
71
76
|
}
|
|
@@ -81,6 +86,12 @@ function handleExists(csn, options, error) {
|
|
|
81
86
|
}
|
|
82
87
|
}, [ 'definitions', artifactName, 'query' ]);
|
|
83
88
|
}
|
|
89
|
+
|
|
90
|
+
if (artifact.projection) { // undo our hack
|
|
91
|
+
artifact.projection = artifact.query.SELECT;
|
|
92
|
+
|
|
93
|
+
delete artifact.query;
|
|
94
|
+
}
|
|
84
95
|
});
|
|
85
96
|
|
|
86
97
|
/**
|
|
@@ -233,6 +244,63 @@ function handleExists(csn, options, error) {
|
|
|
233
244
|
return startAssoc;
|
|
234
245
|
}
|
|
235
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Check that associations in filters (in an exists expression) are only fk-accesses. Everything else is forbidden.
|
|
249
|
+
*
|
|
250
|
+
* @param {CSN.Path} exprPath
|
|
251
|
+
* @returns {void}
|
|
252
|
+
*/
|
|
253
|
+
function forbidAssocInExists(exprPath) {
|
|
254
|
+
const expr = walkCsnPath(csn, exprPath);
|
|
255
|
+
for (let i = 0; i < expr.length; i++) {
|
|
256
|
+
if (i < expr.length - 1 && expr[i] === 'exists' && expr[i + 1].ref) {
|
|
257
|
+
i++;
|
|
258
|
+
const current = expr[i];
|
|
259
|
+
|
|
260
|
+
const { links } = inspectRef(exprPath.concat(i));
|
|
261
|
+
|
|
262
|
+
const assocs = links.filter(link => link.art && link.art.target).map(link => current.ref[link.idx]);
|
|
263
|
+
|
|
264
|
+
checkForInvalidAssoc(assocs);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* @param {object[]} assocs Array of refs of assocs - possibly with a .where to check
|
|
271
|
+
*/
|
|
272
|
+
function checkForInvalidAssoc(assocs) {
|
|
273
|
+
for (const assoc of assocs) {
|
|
274
|
+
if (assoc.where) {
|
|
275
|
+
for (let i = 0; i < assoc.where.length; i++) {
|
|
276
|
+
const part = assoc.where[i];
|
|
277
|
+
|
|
278
|
+
if (part._links && !(assoc.where[i - 1] && assoc.where[i - 1] === 'exists')) {
|
|
279
|
+
for (const link of part._links) {
|
|
280
|
+
if (link.art && link.art.target) {
|
|
281
|
+
if (link.art.keys) { // managed - allow FK access
|
|
282
|
+
if (part._links[link.idx + 1] !== undefined) { // there is a next path step - check if it is a fk
|
|
283
|
+
if (!(part._links[link.idx + 1] && link.art.keys.some(fk => fk._art === part._links[link.idx + 1].art)))
|
|
284
|
+
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)');
|
|
285
|
+
}
|
|
286
|
+
else { // no traversal, ends on managed
|
|
287
|
+
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)');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else { // unmanaged - always wrong
|
|
291
|
+
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)');
|
|
292
|
+
}
|
|
293
|
+
// Recursively drill down if the assoc-step has a filter
|
|
294
|
+
if (part.ref[link.idx].where)
|
|
295
|
+
checkForInvalidAssoc([ part.ref[link.idx] ]);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
236
304
|
/**
|
|
237
305
|
* Walk to the expr using the given path and scan it for the "exists" + "ref" pattern.
|
|
238
306
|
* If such a pattern is found, nest association steps therein into filters.
|
|
@@ -249,6 +317,15 @@ function handleExists(csn, options, error) {
|
|
|
249
317
|
const {
|
|
250
318
|
ref, head, tail,
|
|
251
319
|
} = getFirstAssoc(current, exprPath.concat(i));
|
|
320
|
+
|
|
321
|
+
const lastAssoc = getLastAssoc(current, exprPath.concat(i));
|
|
322
|
+
// toE.toF.id -> we must not end on a non-assoc - this will also be caught downstream by
|
|
323
|
+
// '“EXISTS” can only be used with associations/compositions, found $(TYPE)'
|
|
324
|
+
// But the error might not be clear, since it could be because of our rewritten stuff. The later check
|
|
325
|
+
// checks for exists id -> our rewrite turns toE.toF.id into toE[exists toF[exists id]], leading to the same error
|
|
326
|
+
if (lastAssoc.tail.length > 0)
|
|
327
|
+
error(null, current.$path, { id: lastAssoc.tail[0].id ? lastAssoc.tail[0].id : lastAssoc.tail[0], name: lastAssoc.ref.id ? lastAssoc.ref.id : lastAssoc.ref }, 'Unexpected path step $(ID) after association $(NAME) in "EXISTS"');
|
|
328
|
+
|
|
252
329
|
const newThing = [ ...head, nestFilters(head.length + 1, ref, tail, exprPath.concat([ i ])) ];
|
|
253
330
|
expr[i].ref = newThing;
|
|
254
331
|
}
|
|
@@ -278,15 +355,10 @@ function handleExists(csn, options, error) {
|
|
|
278
355
|
const current = expr[i];
|
|
279
356
|
const isPrefixedWithTableAlias = firstLinkIsEntityOrQuerySource(exprPath.concat(i));
|
|
280
357
|
const base = getBase(queryBase, isPrefixedWithTableAlias, current, exprPath.concat(i));
|
|
281
|
-
const { root, ref
|
|
282
|
-
|
|
283
|
-
if (tail.length > 0) {
|
|
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"');
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
358
|
+
const { root, ref } = getFirstAssoc(current, exprPath.concat(i));
|
|
287
359
|
|
|
288
360
|
if (!root.target) {
|
|
289
|
-
error(null,
|
|
361
|
+
error(null, exprPath.concat(i), { type: root.type }, '“EXISTS” can only be used with associations/compositions, found $(TYPE)');
|
|
290
362
|
return { result: [], leftovers: [] };
|
|
291
363
|
}
|
|
292
364
|
|
|
@@ -477,6 +549,27 @@ function handleExists(csn, options, error) {
|
|
|
477
549
|
};
|
|
478
550
|
}
|
|
479
551
|
|
|
552
|
+
/**
|
|
553
|
+
* Get the last association from the expression part - similar to getFirstAssoc
|
|
554
|
+
*
|
|
555
|
+
* @param {object} xprPart
|
|
556
|
+
* @param {CSN.Path} path
|
|
557
|
+
* @returns {{head: Array, root: CSN.Element, ref: string|object, tail: Array}} The last assoc (root), the corresponding ref (ref), anything before the ref (head) and the rest of the ref (tail).
|
|
558
|
+
*/
|
|
559
|
+
function getLastAssoc(xprPart, path) {
|
|
560
|
+
const { links, art } = inspectRef(path);
|
|
561
|
+
for (let i = xprPart.ref.length - 1; i > -1; i--) {
|
|
562
|
+
if (links[i].art && links[i].art.target) {
|
|
563
|
+
return {
|
|
564
|
+
head: (i === 0 ? [] : xprPart.ref.slice(0, i)), root: links[i].art, ref: xprPart.ref[i], tail: xprPart.ref.slice(i + 1),
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return {
|
|
569
|
+
head: (xprPart.ref.length === 1 ? [] : xprPart.ref.slice(0, xprPart.ref.length - 1)), root: art, ref: xprPart.ref[xprPart.ref.length - 1], tail: [],
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
480
573
|
/**
|
|
481
574
|
* Check (using inspectRef -> links), wether the first path step is an entity or query source
|
|
482
575
|
*
|
|
@@ -556,7 +649,7 @@ function handleExists(csn, options, error) {
|
|
|
556
649
|
};
|
|
557
650
|
// Because the generated things don't have _links, _art etc. set
|
|
558
651
|
// We could also make getParent more robust to calculate the links JIT if they are missing
|
|
559
|
-
|
|
652
|
+
generatedExists.set(subselect, true);
|
|
560
653
|
|
|
561
654
|
const nonEnumElements = Object.create(null);
|
|
562
655
|
nonEnumElements.dummy = {
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
getUtils, cloneCsn, applyTransformationsOnNonDictionary,
|
|
5
|
+
} = require('../../model/csnUtils');
|
|
6
|
+
const { implicitAs, csnRefs } = require('../../model/csnRefs');
|
|
7
|
+
const { isBetaEnabled } = require('../../base/model');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* If a mixin association is published, return the mixin association.
|
|
11
|
+
*
|
|
12
|
+
* @param {CSN.Query} query Query of the artifact to check
|
|
13
|
+
* @param {object} association Association (Element) published by the view
|
|
14
|
+
* @param {string} associationName
|
|
15
|
+
* @returns {object} The mixin association
|
|
16
|
+
*/
|
|
17
|
+
function getMixinAssocOfQueryIfPublished(query, association, associationName) {
|
|
18
|
+
if (query && query.SELECT && query.SELECT.mixin) {
|
|
19
|
+
const aliasedColumnsMap = Object.create(null);
|
|
20
|
+
if (query.SELECT.columns) {
|
|
21
|
+
for (const column of query.SELECT.columns) {
|
|
22
|
+
if (column.as && column.ref && column.ref.length === 1)
|
|
23
|
+
aliasedColumnsMap[column.as] = column;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const elem of Object.keys(query.SELECT.mixin)) {
|
|
28
|
+
const mixinElement = query.SELECT.mixin[elem];
|
|
29
|
+
let originalName = associationName;
|
|
30
|
+
if (aliasedColumnsMap[associationName])
|
|
31
|
+
originalName = aliasedColumnsMap[associationName].ref[0];
|
|
32
|
+
|
|
33
|
+
if (elem === originalName)
|
|
34
|
+
return { mixinElement, mixinName: originalName };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check wether the given artifact uses the given mixin association.
|
|
42
|
+
*
|
|
43
|
+
* We can rely on the fact that there can be no usage starting with $self/$projection,
|
|
44
|
+
* since lib/checks/selectItems.js forbids that.
|
|
45
|
+
*
|
|
46
|
+
* @param {CSN.Query} query Query of the artifact to check
|
|
47
|
+
* @param {object} association Mixin association (Element) to check for
|
|
48
|
+
* @param {string} associationName
|
|
49
|
+
* @returns {boolean} True if used
|
|
50
|
+
*/
|
|
51
|
+
function usesMixinAssociation(query, association, associationName) {
|
|
52
|
+
if (query && query.SELECT && query.SELECT.columns) {
|
|
53
|
+
for (const column of query.SELECT.columns) {
|
|
54
|
+
if (typeof column === 'object' && column.ref && column.ref.length > 1 && (column.ref[0] === associationName || column.ref[0].id === associationName))
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {CSN.Model} csn
|
|
63
|
+
* @param {CSN.Options} options
|
|
64
|
+
* @param {{error: Function, info: Function}} messageFunctions
|
|
65
|
+
* @param {Function} transformCommon For the time being: Pass from outside
|
|
66
|
+
* @returns {(query: CSN.Query, artifact: CSN.Artifact, artName: string, path: CSN.Path) => void} Transformer function for views
|
|
67
|
+
*/
|
|
68
|
+
function getViewTransformer(csn, options, messageFunctions, transformCommon) {
|
|
69
|
+
const {
|
|
70
|
+
get$combined, isAssocOrComposition,
|
|
71
|
+
} = getUtils(csn);
|
|
72
|
+
const { inspectRef, queryOrMain } = csnRefs(csn);
|
|
73
|
+
const pathDelimiter = (options.forHana.names === 'hdbcds') ? '.' : '_';
|
|
74
|
+
const { error, info } = messageFunctions;
|
|
75
|
+
const doA2J = !(options.transformation === 'hdbcds' && options.sqlMapping === 'hdbcds');
|
|
76
|
+
|
|
77
|
+
return transformViewOrEntity;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
*
|
|
81
|
+
* check all queries/subqueries for mixin publishing inside of unions -> forbidden in hdbcds
|
|
82
|
+
*
|
|
83
|
+
* @param {CSN.Query} query
|
|
84
|
+
* @param {CSN.Elements} elements
|
|
85
|
+
* @param {CSN.Path} path
|
|
86
|
+
*/
|
|
87
|
+
function checkForMixinPublishing(query, elements, path) {
|
|
88
|
+
for (const elementName in elements) {
|
|
89
|
+
const element = elements[elementName];
|
|
90
|
+
if (element.target) {
|
|
91
|
+
let colLocation;
|
|
92
|
+
for (let i = 0; i < query.SELECT.columns.length; i++) {
|
|
93
|
+
const col = query.SELECT.columns[i];
|
|
94
|
+
if (col.ref && col.ref.length === 1) {
|
|
95
|
+
if (!colLocation && col.ref[0] === elementName)
|
|
96
|
+
colLocation = i;
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if (col.as === elementName)
|
|
100
|
+
colLocation = i;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (colLocation) {
|
|
104
|
+
const matchingCol = query.SELECT.columns[colLocation];
|
|
105
|
+
const possibleMixinName = matchingCol.ref[0];
|
|
106
|
+
const isMixin = query.SELECT.mixin[possibleMixinName] !== undefined;
|
|
107
|
+
if (element.target && isMixin) {
|
|
108
|
+
error(null, path.concat([ 'columns', colLocation ]), { id: elementName, name: possibleMixinName, '#': possibleMixinName === elementName ? 'std' : 'renamed' }, {
|
|
109
|
+
std: 'Element $(ID) is a mixin association and can\'t be published in a UNION',
|
|
110
|
+
renamed: 'Element $(ID) is a mixin association ($(NAME))and can\'t be published in a UNION',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Build a map of the resulting names (i.e. the element name of the column) and references to the respective columns
|
|
119
|
+
*
|
|
120
|
+
* This can later be used to match from elements to columns.
|
|
121
|
+
*
|
|
122
|
+
* @param {CSN.Query} query
|
|
123
|
+
* @returns {object}
|
|
124
|
+
*/
|
|
125
|
+
function getColumnMap(query) {
|
|
126
|
+
const map = Object.create(null);
|
|
127
|
+
if (query && query.SELECT && query.SELECT.columns) {
|
|
128
|
+
query.SELECT.columns.forEach((col) => {
|
|
129
|
+
if (col === '*') {
|
|
130
|
+
// do nothing
|
|
131
|
+
}
|
|
132
|
+
else if (col.as) {
|
|
133
|
+
if (!map[col.as])
|
|
134
|
+
map[col.as] = col;
|
|
135
|
+
}
|
|
136
|
+
else if (col.ref) {
|
|
137
|
+
// .id on last path step can happen with hdbcds.hdbcds and malicious CSN input - maybe also with params?
|
|
138
|
+
// We made things right in the end with the second add of missing stuff, but why not do it
|
|
139
|
+
// right from the getgo
|
|
140
|
+
const last = getLastRefStepString(col.ref);
|
|
141
|
+
if (!map[last])
|
|
142
|
+
map[last] = col;
|
|
143
|
+
}
|
|
144
|
+
else if (col.func) {
|
|
145
|
+
map[col.func] = col;
|
|
146
|
+
}
|
|
147
|
+
else if (!map[col]) {
|
|
148
|
+
map[col] = col;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return map;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* For things that are not explicitly found in the columns but still present in the elements, add them to the columnMap.
|
|
157
|
+
*
|
|
158
|
+
* This can happen for:
|
|
159
|
+
* - projections, as we might not have .columns at all
|
|
160
|
+
* - *, as we don't resolve it for hdbcds with hdbcds-naming
|
|
161
|
+
*
|
|
162
|
+
* We ensure that we attach a table alias before each column
|
|
163
|
+
*
|
|
164
|
+
* @param {CSN.Query} query
|
|
165
|
+
* @param {boolean} isProjection
|
|
166
|
+
* @param {boolean} isSelectStar
|
|
167
|
+
* @param {object} $combined
|
|
168
|
+
* @param {object} columnMap
|
|
169
|
+
* @param {string} elemName
|
|
170
|
+
*/
|
|
171
|
+
function addProjectionOrStarElement(query, isProjection, isSelectStar, $combined, columnMap, elemName) {
|
|
172
|
+
// Prepend an alias if present
|
|
173
|
+
let alias = (isProjection || isSelectStar) &&
|
|
174
|
+
(query.SELECT.from.as || (query.SELECT.from.ref && implicitAs(query.SELECT.from.ref)));
|
|
175
|
+
// In case of * and no explicit alias
|
|
176
|
+
// find the source of the col by looking at $combined and prepend it
|
|
177
|
+
if (isSelectStar && !alias && !isProjection) {
|
|
178
|
+
if (!$combined)
|
|
179
|
+
$combined = get$combined(query);
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
const matchingCombined = $combined[elemName];
|
|
183
|
+
// Internal errors - this should never happen!
|
|
184
|
+
if (matchingCombined.length > 1) { // should already be caught by compiler
|
|
185
|
+
throw new Error(`Ambiguous name - can't be resolved: ${elemName}. Found in: ${matchingCombined.map(o => o.parent)}`);
|
|
186
|
+
}
|
|
187
|
+
else if (matchingCombined.length === 0) { // no clue how this could happen? Invalid CSN?
|
|
188
|
+
throw new Error(`No matching entry found in UNION of all elements for: ${elemName}`);
|
|
189
|
+
}
|
|
190
|
+
alias = matchingCombined[0].parent;
|
|
191
|
+
}
|
|
192
|
+
if (alias)
|
|
193
|
+
columnMap[elemName] = { ref: [ alias, elemName ] };
|
|
194
|
+
else
|
|
195
|
+
columnMap[elemName] = { ref: [ elemName ] };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* So far, we only added foreign keys to elements - we also need to create corresponding columns
|
|
200
|
+
* and respect aliasing etc.
|
|
201
|
+
*
|
|
202
|
+
* @todo Maybe this can be done earlier, during flattening/expansion already?
|
|
203
|
+
* @param {object} columnMap
|
|
204
|
+
* @param {CSN.Element} elem
|
|
205
|
+
* @param {string} elemName
|
|
206
|
+
*/
|
|
207
|
+
function addForeignKeysToColumns(columnMap, elem, elemName) {
|
|
208
|
+
const assocCol = columnMap[elemName];
|
|
209
|
+
if (assocCol && assocCol.ref) {
|
|
210
|
+
elem.keys.forEach((foreignKey) => {
|
|
211
|
+
const ref = cloneCsn(assocCol.ref, options);
|
|
212
|
+
ref[ref.length - 1] = [ getLastRefStepString(ref) ].concat(foreignKey.as).join(pathDelimiter);
|
|
213
|
+
const result = {
|
|
214
|
+
ref,
|
|
215
|
+
};
|
|
216
|
+
if (assocCol.as) {
|
|
217
|
+
const columnName = `${assocCol.as}${pathDelimiter}${foreignKey.as}`;
|
|
218
|
+
result.as = columnName;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (assocCol.key)
|
|
222
|
+
result.key = true;
|
|
223
|
+
|
|
224
|
+
const colName = result.as || getLastRefStepString(ref);
|
|
225
|
+
columnMap[colName] = result;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Check for invalid association publishing (in Union or in Subquery) (for hdbcds) and
|
|
233
|
+
* create the __clone for publishing stuff.
|
|
234
|
+
*
|
|
235
|
+
* @todo Factor out the checks
|
|
236
|
+
* @param {CSN.Query} query
|
|
237
|
+
* @param {object} elements
|
|
238
|
+
* @param {object} columnMap
|
|
239
|
+
* @param {WeakMap} publishedMixins Map to collect the published mixins
|
|
240
|
+
* @param {CSN.Element} elem
|
|
241
|
+
* @param {string} elemName
|
|
242
|
+
* @param {CSN.Path} elementsPath Path pointing to elements
|
|
243
|
+
* @param {CSN.Path} queryPath Path pointing to the query
|
|
244
|
+
*/
|
|
245
|
+
function handleAssociationElement(query, elements, columnMap, publishedMixins, elem, elemName, elementsPath, queryPath) {
|
|
246
|
+
if (isUnion(queryPath) && options.transformation === 'hdbcds') {
|
|
247
|
+
if (isBetaEnabled(options, 'ignoreAssocPublishingInUnion') && doA2J) {
|
|
248
|
+
if (elem.keys)
|
|
249
|
+
info(null, queryPath, `Managed association "${elemName}", published in a UNION, will be ignored`);
|
|
250
|
+
else
|
|
251
|
+
info(null, queryPath, `Association "${elemName}", published in a UNION, will be ignored`);
|
|
252
|
+
|
|
253
|
+
elem._ignore = true;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
error(null, queryPath, `Association "${elemName}" can't be published in a SAP HANA CDS UNION`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else if (queryPath.length > 4 && options.transformation === 'hdbcds') { // path.length > 4 -> is a subquery
|
|
260
|
+
error(null, queryPath, { name: elemName },
|
|
261
|
+
'Association $(NAME) can\'t be published in a subquery');
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
const isNotMixinByItself = checkIsNotMixinByItself(query, columnMap, elemName);
|
|
265
|
+
const { mixinElement, mixinName } = getMixinAssocOfQueryIfPublished(query, elem, elemName);
|
|
266
|
+
if (isNotMixinByItself || mixinElement !== undefined) {
|
|
267
|
+
// If the mixin is only published and not used, only display the __ clone. Kill the "original".
|
|
268
|
+
if (mixinElement !== undefined && !usesMixinAssociation(query, elem, elemName))
|
|
269
|
+
delete query.SELECT.mixin[mixinName];
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
// Create an unused alias name for the MIXIN - use 3 _ to avoid collision with usings
|
|
273
|
+
let mixinElemName = `___${mixinName || elemName}`;
|
|
274
|
+
while (elements[mixinElemName])
|
|
275
|
+
mixinElemName = `_${mixinElemName}`;
|
|
276
|
+
|
|
277
|
+
// Copy the association element to the MIXIN clause under its alias name
|
|
278
|
+
// Needs to be a deep copy, as we transform the on-condition
|
|
279
|
+
const mixinElem = cloneCsn(elem, options);
|
|
280
|
+
// Perform common transformations on the newly generated MIXIN element (won't be reached otherwise)
|
|
281
|
+
transformCommon(mixinElem, mixinElemName);
|
|
282
|
+
|
|
283
|
+
if (query.SELECT && !query.SELECT.mixin)
|
|
284
|
+
query.SELECT.mixin = Object.create(null);
|
|
285
|
+
|
|
286
|
+
// Clone 'on'-condition, pre-pending '$projection' to paths where appropriate,
|
|
287
|
+
// and fixing the association alias just created
|
|
288
|
+
|
|
289
|
+
if (mixinElem.on) {
|
|
290
|
+
mixinElem.on = applyTransformationsOnNonDictionary(mixinElem, 'on', {
|
|
291
|
+
ref: (parent, prop, ref, refpath) => {
|
|
292
|
+
if (ref[0] === elemName) {
|
|
293
|
+
ref[0] = mixinElemName;
|
|
294
|
+
}
|
|
295
|
+
else if (!(ref[0] && ref[0].startsWith('$'))) {
|
|
296
|
+
ref.unshift('$projection');
|
|
297
|
+
}
|
|
298
|
+
else if (ref[0] && ref[0].startsWith('$')) {
|
|
299
|
+
// TODO: I think this is non-sense. Stuff with $ is either magic or must start with $self, right?
|
|
300
|
+
const { scope } = inspectRef(refpath);
|
|
301
|
+
if (scope !== '$magic' && scope !== '$self')
|
|
302
|
+
ref.unshift('$projection');
|
|
303
|
+
}
|
|
304
|
+
parent.ref = ref;
|
|
305
|
+
return ref;
|
|
306
|
+
},
|
|
307
|
+
}, elementsPath.concat(elemName));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!mixinElem._ignore)
|
|
311
|
+
columnMap[elemName] = { ref: [ mixinElemName ], as: elemName };
|
|
312
|
+
|
|
313
|
+
if (query.SELECT) {
|
|
314
|
+
query.SELECT.mixin[mixinElemName] = mixinElem;
|
|
315
|
+
|
|
316
|
+
publishedMixins.set(mixinElem, true);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* If following an association, explicitly set the implicit alias
|
|
324
|
+
* due to an issue with HANA - only for hdbcds-hdbcds, I assume flattening
|
|
325
|
+
* takes care of this for the other cases already
|
|
326
|
+
*
|
|
327
|
+
* @param {CSN.Query} query
|
|
328
|
+
* @param {CSN.Path} path
|
|
329
|
+
*/
|
|
330
|
+
function addImplicitAliasWithAssoc(query, path) {
|
|
331
|
+
for (let i = 0; i < query.SELECT.columns.length; i++) {
|
|
332
|
+
const col = query.SELECT.columns[i];
|
|
333
|
+
if (!col.as && col.ref && col.ref.length > 1) {
|
|
334
|
+
const { links } = inspectRef(path.concat([ 'columns', i ]));
|
|
335
|
+
if (links && links.slice(0, -1).some(({ art }) => isAssocOrComposition(art && art.type || '')))
|
|
336
|
+
col.as = getLastRefStepString(col.ref);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* @param {CSN.Query} query
|
|
343
|
+
* @param {CSN.Artifact} artifact
|
|
344
|
+
* @param {string} artName
|
|
345
|
+
* @param {CSN.Path} path
|
|
346
|
+
*/
|
|
347
|
+
// eslint-disable-next-line complexity
|
|
348
|
+
function transformViewOrEntity(query, artifact, artName, path) {
|
|
349
|
+
const { elements } = queryOrMain(query, artifact);
|
|
350
|
+
// We use the elements from the leading query/main artifact - adapt the path
|
|
351
|
+
const elementsPath = elements === artifact.elements ? path.slice(0, 2).concat('elements') : path.concat('elements');
|
|
352
|
+
const queryPath = path;
|
|
353
|
+
|
|
354
|
+
let hasNonAssocElements = false;
|
|
355
|
+
const isSelect = query && query.SELECT;
|
|
356
|
+
const isProjection = !!artifact.projection || query && query.SELECT && !query.SELECT.columns;
|
|
357
|
+
const columnMap = getColumnMap(query);
|
|
358
|
+
const isSelectStar = query && query.SELECT && query.SELECT.columns && query.SELECT.columns.indexOf('*') !== -1;
|
|
359
|
+
|
|
360
|
+
// check all queries/subqueries for mixin publishing inside of unions -> forbidden in hdbcds
|
|
361
|
+
if (query && options.transformation === 'hdbcds' && query.SELECT && query.SELECT.mixin && path.indexOf('SET') !== -1)
|
|
362
|
+
checkForMixinPublishing(query, elements, path);
|
|
363
|
+
|
|
364
|
+
// Second walk through the entity elements: Deal with associations (might also result in new elements)
|
|
365
|
+
// Will be initialized JIT inside the elements-loop
|
|
366
|
+
let $combined;
|
|
367
|
+
|
|
368
|
+
const publishedMixins = new WeakMap();
|
|
369
|
+
|
|
370
|
+
for (const elemName in elements) {
|
|
371
|
+
const elem = elements[elemName];
|
|
372
|
+
if (isSelect) {
|
|
373
|
+
if (!columnMap[elemName])
|
|
374
|
+
addProjectionOrStarElement(query, isProjection, isSelectStar, $combined, columnMap, elemName);
|
|
375
|
+
|
|
376
|
+
// For associations - make sure that the foreign keys have the same "style"
|
|
377
|
+
// If A.assoc => A.assoc_id, else if assoc => assoc_id or assoc as Assoc => Assoc_id
|
|
378
|
+
if (elem.keys && doA2J)
|
|
379
|
+
addForeignKeysToColumns(columnMap, elem, elemName);
|
|
380
|
+
}
|
|
381
|
+
// Views must have at least one element that is not an unmanaged assoc
|
|
382
|
+
if (!elem.on && !elem._ignore)
|
|
383
|
+
hasNonAssocElements = true;
|
|
384
|
+
|
|
385
|
+
// (180 b) Create MIXINs for association elements in projections or views (those that are not mixins by themselves)
|
|
386
|
+
// CDXCORE-585: Allow mixin associations to be used and published in parallel
|
|
387
|
+
if (query !== undefined && elem.target)
|
|
388
|
+
handleAssociationElement(query, elements, columnMap, publishedMixins, elem, elemName, elementsPath, queryPath);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (query && !hasNonAssocElements) {
|
|
392
|
+
// Complain if there are no elements other than unmanaged associations
|
|
393
|
+
// Allow with plain
|
|
394
|
+
error(null, [ 'definitions', artName ], { $reviewed: true },
|
|
395
|
+
'Expecting view or projection to have at least one element that is not an unmanaged association');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (isSelect) {
|
|
399
|
+
// Build new columns from the column map - bring elements and columns back in sync basically
|
|
400
|
+
query.SELECT.columns = Object.keys(elements).filter(elem => !elements[elem]._ignore).map(key => stripLeadingSelf(columnMap[key]));
|
|
401
|
+
// If following an association, explicitly set the implicit alias
|
|
402
|
+
// due to an issue with HANA - this seems to only have an effect on ref files with hdbcds-hdbcds, so only run then
|
|
403
|
+
if (options.transformation === 'hdbcds' && options.sqlMapping === 'hdbcds')
|
|
404
|
+
addImplicitAliasWithAssoc(query, path);
|
|
405
|
+
|
|
406
|
+
delete query.SELECT.excluding; // just to make the output of the new transformer the same as the old
|
|
407
|
+
|
|
408
|
+
// A2J turned usages into JOINs, we must now remove all non-published mixins (i.e. only keep the clones)
|
|
409
|
+
if (query.SELECT.mixin && doA2J) {
|
|
410
|
+
for (const [ name, mixin ] of Object.entries(query.SELECT.mixin)) {
|
|
411
|
+
if (!publishedMixins.has(mixin))
|
|
412
|
+
delete query.SELECT.mixin[name];
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Walk the given path and check if we are in a UNION.
|
|
421
|
+
* This will return true when it is called on the subquery inside of a SET.args property.
|
|
422
|
+
*
|
|
423
|
+
* @param {CSN.Path} path
|
|
424
|
+
* @returns {boolean}
|
|
425
|
+
*/
|
|
426
|
+
function isUnion(path) {
|
|
427
|
+
const subquery = path[path.length - 1];
|
|
428
|
+
const queryIndex = path[path.length - 2];
|
|
429
|
+
const args = path[path.length - 3];
|
|
430
|
+
const unionOperator = path[path.length - 4];
|
|
431
|
+
return path.length > 3 && (subquery === 'SET' || subquery === 'SELECT') && typeof queryIndex === 'number' && queryIndex >= 0 && args === 'args' && unionOperator === 'SET';
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Strip of leading $self of the ref
|
|
436
|
+
*
|
|
437
|
+
* @param {object} col A column
|
|
438
|
+
* @returns {object}
|
|
439
|
+
*/
|
|
440
|
+
function stripLeadingSelf(col) {
|
|
441
|
+
if (col.ref && col.ref.length > 1 && col.ref[0] === '$self')
|
|
442
|
+
col.ref = col.ref.slice(1);
|
|
443
|
+
|
|
444
|
+
return col;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Check that the given element is not a simple mixin-publishing
|
|
449
|
+
*
|
|
450
|
+
* @param {CSN.Query} query
|
|
451
|
+
* @param {object} columnMap
|
|
452
|
+
* @param {string} elementName
|
|
453
|
+
* @returns {boolean}
|
|
454
|
+
*/
|
|
455
|
+
function checkIsNotMixinByItself(query, columnMap, elementName) {
|
|
456
|
+
if (query && query.SELECT && query.SELECT.mixin) {
|
|
457
|
+
const col = columnMap[elementName];
|
|
458
|
+
|
|
459
|
+
// Use getLastRefStepString - with hdbcds.hdbcds and malicious CSN input we might have .id
|
|
460
|
+
const realName = getLastRefStepString(col.ref);
|
|
461
|
+
// If the element is not part of the mixin => True
|
|
462
|
+
return query.SELECT.mixin[realName] === undefined;
|
|
463
|
+
}
|
|
464
|
+
// the artifact does not define any mixins, the element cannot be a mixin
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Return the string value of the last ref step - so either the .id or the last step.
|
|
470
|
+
*
|
|
471
|
+
* We cannot use implicitAs, as this causes problems for structured things with hdi-hdbcds naming
|
|
472
|
+
*
|
|
473
|
+
* @param {Array} ref
|
|
474
|
+
* @returns {string}
|
|
475
|
+
*/
|
|
476
|
+
function getLastRefStepString(ref) {
|
|
477
|
+
const last = ref[ref.length - 1];
|
|
478
|
+
if (last.id)
|
|
479
|
+
return last.id;
|
|
480
|
+
return last;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
module.exports = {
|
|
484
|
+
getViewTransformer,
|
|
485
|
+
};
|