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