@sap/cds-compiler 2.7.0 → 2.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +167 -0
  2. package/bin/cdsc.js +42 -25
  3. package/bin/cdsse.js +1 -0
  4. package/doc/CHANGELOG_BETA.md +10 -0
  5. package/lib/api/.eslintrc.json +2 -0
  6. package/lib/api/main.js +17 -33
  7. package/lib/api/options.js +25 -13
  8. package/lib/api/validate.js +33 -9
  9. package/lib/backends.js +9 -8
  10. package/lib/base/dictionaries.js +2 -1
  11. package/lib/base/keywords.js +32 -2
  12. package/lib/base/message-registry.js +26 -2
  13. package/lib/base/messages.js +25 -9
  14. package/lib/base/model.js +5 -3
  15. package/lib/base/optionProcessorHelper.js +56 -22
  16. package/lib/checks/onConditions.js +5 -0
  17. package/lib/checks/selectItems.js +4 -0
  18. package/lib/checks/types.js +26 -2
  19. package/lib/checks/unknownMagic.js +41 -0
  20. package/lib/checks/validator.js +7 -2
  21. package/lib/compiler/assert-consistency.js +18 -5
  22. package/lib/compiler/base.js +65 -0
  23. package/lib/compiler/builtins.js +30 -1
  24. package/lib/compiler/checks.js +5 -2
  25. package/lib/compiler/definer.js +145 -120
  26. package/lib/compiler/index.js +16 -4
  27. package/lib/compiler/propagator.js +5 -2
  28. package/lib/compiler/resolver.js +207 -47
  29. package/lib/compiler/shared.js +47 -200
  30. package/lib/compiler/utils.js +173 -0
  31. package/lib/edm/annotations/genericTranslation.js +183 -187
  32. package/lib/edm/csn2edm.js +94 -98
  33. package/lib/edm/edm.js +16 -20
  34. package/lib/edm/edmPreprocessor.js +302 -115
  35. package/lib/edm/edmUtils.js +31 -12
  36. package/lib/gen/language.checksum +1 -1
  37. package/lib/gen/language.interp +28 -1
  38. package/lib/gen/language.tokens +79 -69
  39. package/lib/gen/languageLexer.interp +28 -1
  40. package/lib/gen/languageLexer.js +879 -805
  41. package/lib/gen/languageLexer.tokens +71 -62
  42. package/lib/gen/languageParser.js +5308 -4308
  43. package/lib/json/from-csn.js +59 -30
  44. package/lib/json/to-csn.js +354 -105
  45. package/lib/language/antlrParser.js +11 -0
  46. package/lib/language/errorStrategy.js +1 -0
  47. package/lib/language/genericAntlrParser.js +81 -14
  48. package/lib/language/language.g4 +163 -31
  49. package/lib/main.d.ts +136 -17
  50. package/lib/main.js +7 -1
  51. package/lib/model/api.js +78 -0
  52. package/lib/model/csnRefs.js +115 -32
  53. package/lib/model/csnUtils.js +71 -33
  54. package/lib/model/enrichCsn.js +36 -9
  55. package/lib/model/revealInternalProperties.js +20 -4
  56. package/lib/modelCompare/compare.js +2 -1
  57. package/lib/optionProcessor.js +33 -16
  58. package/lib/render/.eslintrc.json +3 -1
  59. package/lib/render/DuplicateChecker.js +1 -1
  60. package/lib/render/toCdl.js +60 -17
  61. package/lib/render/toHdbcds.js +122 -74
  62. package/lib/render/toSql.js +57 -32
  63. package/lib/render/utils/common.js +6 -10
  64. package/lib/sql-identifier.js +6 -1
  65. package/lib/transform/db/constraints.js +273 -119
  66. package/lib/transform/db/draft.js +9 -6
  67. package/lib/transform/db/expansion.js +19 -7
  68. package/lib/transform/db/flattening.js +31 -7
  69. package/lib/transform/db/transformExists.js +344 -66
  70. package/lib/transform/db/views.js +438 -0
  71. package/lib/transform/forHanaNew.js +65 -436
  72. package/lib/transform/forOdataNew.js +21 -10
  73. package/lib/transform/localized.js +2 -0
  74. package/lib/transform/odata/attachPath.js +19 -4
  75. package/lib/transform/odata/generateForeignKeyElements.js +11 -10
  76. package/lib/transform/odata/referenceFlattener.js +44 -38
  77. package/lib/transform/odata/sortByAssociationDependency.js +2 -2
  78. package/lib/transform/odata/structuralPath.js +72 -0
  79. package/lib/transform/odata/structureFlattener.js +13 -10
  80. package/lib/transform/odata/typesExposure.js +22 -12
  81. package/lib/transform/transformUtilsNew.js +55 -9
  82. package/lib/transform/translateAssocsToJoins.js +11 -17
  83. package/lib/transform/universalCsnEnricher.js +67 -0
  84. package/lib/utils/file.js +5 -3
  85. package/lib/utils/term.js +65 -42
  86. package/lib/utils/timetrace.js +48 -26
  87. package/package.json +1 -1
@@ -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
+ };