@sap/cds-compiler 2.7.0 → 2.10.2

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