@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +90 -5
  2. package/bin/.eslintrc.json +1 -2
  3. package/bin/cds_update_identifiers.js +3 -1
  4. package/bin/cdsc.js +49 -25
  5. package/bin/cdsse.js +1 -0
  6. package/bin/cdsv2m.js +3 -2
  7. package/doc/CHANGELOG_BETA.md +10 -0
  8. package/lib/api/.eslintrc.json +2 -0
  9. package/lib/api/main.js +8 -36
  10. package/lib/api/options.js +15 -6
  11. package/lib/api/validate.js +30 -3
  12. package/lib/backends.js +12 -13
  13. package/lib/base/dictionaries.js +2 -1
  14. package/lib/base/keywords.js +3 -2
  15. package/lib/base/message-registry.js +34 -10
  16. package/lib/base/messages.js +38 -18
  17. package/lib/base/model.js +5 -4
  18. package/lib/base/optionProcessorHelper.js +57 -23
  19. package/lib/checks/emptyOrOnlyVirtual.js +2 -2
  20. package/lib/checks/selectItems.js +4 -0
  21. package/lib/checks/unknownMagic.js +6 -3
  22. package/lib/compiler/assert-consistency.js +9 -2
  23. package/lib/compiler/base.js +65 -0
  24. package/lib/compiler/builtins.js +62 -16
  25. package/lib/compiler/checks.js +2 -1
  26. package/lib/compiler/definer.js +66 -108
  27. package/lib/compiler/index.js +29 -29
  28. package/lib/compiler/propagator.js +5 -2
  29. package/lib/compiler/resolver.js +225 -58
  30. package/lib/compiler/shared.js +53 -229
  31. package/lib/compiler/utils.js +184 -0
  32. package/lib/edm/annotations/genericTranslation.js +1 -1
  33. package/lib/edm/csn2edm.js +3 -2
  34. package/lib/edm/edmPreprocessor.js +34 -38
  35. package/lib/edm/edmUtils.js +3 -3
  36. package/lib/gen/language.checksum +1 -1
  37. package/lib/gen/language.interp +17 -1
  38. package/lib/gen/language.tokens +79 -73
  39. package/lib/gen/languageLexer.interp +19 -1
  40. package/lib/gen/languageLexer.js +779 -731
  41. package/lib/gen/languageLexer.tokens +71 -65
  42. package/lib/gen/languageParser.js +4668 -4072
  43. package/lib/json/from-csn.js +10 -10
  44. package/lib/json/to-csn.js +228 -47
  45. package/lib/language/antlrParser.js +11 -0
  46. package/lib/language/errorStrategy.js +26 -8
  47. package/lib/language/genericAntlrParser.js +73 -14
  48. package/lib/language/language.g4 +79 -3
  49. package/lib/main.d.ts +215 -18
  50. package/lib/main.js +3 -1
  51. package/lib/model/api.js +2 -2
  52. package/lib/model/csnRefs.js +117 -33
  53. package/lib/model/csnUtils.js +65 -133
  54. package/lib/model/enrichCsn.js +62 -37
  55. package/lib/model/revealInternalProperties.js +25 -8
  56. package/lib/model/sortViews.js +8 -1
  57. package/lib/modelCompare/compare.js +2 -1
  58. package/lib/optionProcessor.js +33 -18
  59. package/lib/render/.eslintrc.json +1 -2
  60. package/lib/render/DuplicateChecker.js +1 -1
  61. package/lib/render/toCdl.js +15 -8
  62. package/lib/render/toHdbcds.js +26 -49
  63. package/lib/render/toSql.js +61 -39
  64. package/lib/render/utils/common.js +1 -1
  65. package/lib/transform/db/applyTransformations.js +189 -0
  66. package/lib/transform/db/constraints.js +273 -119
  67. package/lib/transform/db/draft.js +3 -2
  68. package/lib/transform/db/expansion.js +6 -4
  69. package/lib/transform/db/flattening.js +19 -3
  70. package/lib/transform/db/transformExists.js +102 -9
  71. package/lib/transform/db/views.js +485 -0
  72. package/lib/transform/forHanaNew.js +93 -448
  73. package/lib/transform/forOdataNew.js +9 -2
  74. package/lib/transform/localized.js +2 -0
  75. package/lib/transform/odata/structuralPath.js +1 -5
  76. package/lib/transform/transformUtilsNew.js +22 -8
  77. package/lib/transform/translateAssocsToJoins.js +7 -15
  78. package/lib/utils/file.js +11 -5
  79. package/lib/utils/term.js +65 -42
  80. package/lib/utils/timetrace.js +48 -26
  81. package/package.json +1 -1
  82. 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.$generatedExists) {
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, tail } = getFirstAssoc(current, exprPath.concat(i));
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, current.$path, { type: root.type }, '“EXISTS” can only be used with associations/compositions, found $(TYPE)');
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
- setProp(subselect, '$generatedExists', true);
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
+ };