@sap/cds-compiler 2.5.0 → 2.10.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 (92) hide show
  1. package/CHANGELOG.md +191 -9
  2. package/bin/cdsc.js +2 -2
  3. package/doc/CHANGELOG_BETA.md +33 -3
  4. package/lib/api/main.js +29 -101
  5. package/lib/api/options.js +15 -11
  6. package/lib/api/validate.js +12 -8
  7. package/lib/backends.js +0 -81
  8. package/lib/base/keywords.js +32 -2
  9. package/lib/base/message-registry.js +63 -9
  10. package/lib/base/messages.js +63 -21
  11. package/lib/base/model.js +2 -3
  12. package/lib/checks/defaultValues.js +27 -2
  13. package/lib/checks/elements.js +1 -6
  14. package/lib/checks/foreignKeys.js +0 -6
  15. package/lib/checks/managedWithoutKeys.js +17 -0
  16. package/lib/checks/nonexpandableStructured.js +38 -0
  17. package/lib/checks/onConditions.js +9 -45
  18. package/lib/checks/queryNoDbArtifacts.js +25 -7
  19. package/lib/checks/selectItems.js +25 -2
  20. package/lib/checks/types.js +26 -2
  21. package/lib/checks/unknownMagic.js +38 -0
  22. package/lib/checks/utils.js +61 -0
  23. package/lib/checks/validator.js +60 -7
  24. package/lib/compiler/assert-consistency.js +16 -7
  25. package/lib/compiler/builtins.js +2 -0
  26. package/lib/compiler/checks.js +6 -4
  27. package/lib/compiler/definer.js +99 -42
  28. package/lib/compiler/index.js +73 -27
  29. package/lib/compiler/resolver.js +288 -157
  30. package/lib/compiler/shared.js +31 -11
  31. package/lib/edm/annotations/genericTranslation.js +182 -186
  32. package/lib/edm/csn2edm.js +103 -108
  33. package/lib/edm/edm.js +18 -21
  34. package/lib/edm/edmPreprocessor.js +361 -114
  35. package/lib/edm/edmUtils.js +103 -33
  36. package/lib/gen/Dictionary.json +22 -0
  37. package/lib/gen/language.checksum +1 -1
  38. package/lib/gen/language.interp +12 -1
  39. package/lib/gen/language.tokens +57 -53
  40. package/lib/gen/languageLexer.interp +10 -1
  41. package/lib/gen/languageLexer.js +770 -744
  42. package/lib/gen/languageLexer.tokens +49 -46
  43. package/lib/gen/languageParser.js +4713 -4279
  44. package/lib/json/from-csn.js +103 -45
  45. package/lib/json/to-csn.js +296 -117
  46. package/lib/language/antlrParser.js +4 -3
  47. package/lib/language/errorStrategy.js +1 -0
  48. package/lib/language/genericAntlrParser.js +21 -12
  49. package/lib/language/language.g4 +99 -31
  50. package/lib/main.d.ts +81 -3
  51. package/lib/main.js +30 -7
  52. package/lib/model/api.js +78 -0
  53. package/lib/model/csnRefs.js +329 -142
  54. package/lib/model/csnUtils.js +235 -58
  55. package/lib/model/enrichCsn.js +18 -1
  56. package/lib/model/revealInternalProperties.js +2 -1
  57. package/lib/modelCompare/compare.js +37 -20
  58. package/lib/optionProcessor.js +9 -3
  59. package/lib/render/.eslintrc.json +4 -1
  60. package/lib/render/DuplicateChecker.js +8 -5
  61. package/lib/render/toCdl.js +112 -33
  62. package/lib/render/toHdbcds.js +134 -64
  63. package/lib/render/toSql.js +91 -38
  64. package/lib/render/utils/common.js +8 -13
  65. package/lib/render/utils/sql.js +3 -3
  66. package/lib/sql-identifier.js +6 -1
  67. package/lib/transform/db/assertUnique.js +5 -6
  68. package/lib/transform/db/constraints.js +29 -13
  69. package/lib/transform/db/draft.js +8 -6
  70. package/lib/transform/db/expansion.js +582 -0
  71. package/lib/transform/db/flattening.js +325 -0
  72. package/lib/transform/db/groupByOrderBy.js +2 -2
  73. package/lib/transform/db/transformExists.js +284 -63
  74. package/lib/transform/forHanaNew.js +98 -381
  75. package/lib/transform/forOdataNew.js +21 -22
  76. package/lib/transform/localized.js +37 -10
  77. package/lib/transform/odata/attachPath.js +19 -4
  78. package/lib/transform/odata/generateForeignKeyElements.js +11 -10
  79. package/lib/transform/odata/referenceFlattener.js +60 -39
  80. package/lib/transform/odata/sortByAssociationDependency.js +2 -2
  81. package/lib/transform/odata/structuralPath.js +72 -0
  82. package/lib/transform/odata/structureFlattener.js +19 -18
  83. package/lib/transform/odata/typesExposure.js +22 -12
  84. package/lib/transform/transformUtilsNew.js +134 -78
  85. package/lib/transform/translateAssocsToJoins.js +17 -14
  86. package/lib/transform/universalCsnEnricher.js +67 -0
  87. package/lib/utils/file.js +0 -11
  88. package/lib/utils/moduleResolve.js +6 -8
  89. package/package.json +1 -1
  90. package/lib/json/walker.js +0 -26
  91. package/lib/transform/sqlite +0 -0
  92. package/lib/utils/string.js +0 -17
@@ -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,158 @@ 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}
242
+ */
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
+
253
+ const lastAssoc = getLastAssoc(current, exprPath.concat(i));
254
+ // toE.toF.id -> we must not end on a non-assoc - this will also be caught downstream by
255
+ // '“EXISTS” can only be used with associations/compositions, found $(TYPE)'
256
+ // But the error might not be clear, since it could be because of our rewritten stuff. The later check
257
+ // checks for exists id -> our rewrite turns toE.toF.id into toE[exists toF[exists id]], leading to the same error
258
+ if (lastAssoc.tail.length > 0)
259
+ 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"');
260
+
261
+ const newThing = [ ...head, nestFilters(head.length + 1, ref, tail, exprPath.concat([ i ])) ];
262
+ expr[i].ref = newThing;
263
+ }
264
+ }
265
+
266
+ return expr;
267
+ }
268
+
269
+ /**
270
+ * 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.
271
+ *
272
+ * @param {CSN.Path} queryPath Path to the query-object
273
+ * @param {CSN.Path} exprPath Path to the expression-array to process
274
+ * @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.
116
275
  */
117
- function processExists(query, expr) {
276
+ function processExists(queryPath, exprPath) {
277
+ const toContinue = [];
118
278
  const newExpr = [];
279
+ const query = walkCsnPath(csn, queryPath);
280
+ const expr = walkCsnPath(csn, exprPath);
119
281
  const queryBase = query.SELECT.from.ref ? (query.SELECT.from.as || query.SELECT.from.ref[0]) : null;
120
282
  const sources = getQuerySources(query.SELECT);
121
283
 
@@ -123,18 +285,15 @@ function handleExists(csn, error) {
123
285
  if (i < expr.length - 1 && expr[i] === 'exists' && expr[i + 1].ref) {
124
286
  i++;
125
287
  const current = expr[i];
126
- const isPrefixedWithTableAlias = firstLinkIsEntityOrQuerySource(current);
127
- const base = getBase(queryBase, isPrefixedWithTableAlias, current);
128
- const { root, ref, tail } = getFirstAssoc(current);
288
+ const isPrefixedWithTableAlias = firstLinkIsEntityOrQuerySource(exprPath.concat(i));
289
+ const base = getBase(queryBase, isPrefixedWithTableAlias, current, exprPath.concat(i));
290
+ const { root, ref } = getFirstAssoc(current, exprPath.concat(i));
129
291
 
130
- if (tail.length > 0) {
131
- error(null, current.$path, { id: tail[0], name: ref.id ? ref.id : ref }, 'Unexpected path step $(ID) after association $(NAME) in "EXISTS"');
132
- continue;
292
+ if (!root.target) {
293
+ error(null, exprPath.concat(i), { type: root.type }, '“EXISTS” can only be used with associations/compositions, found $(TYPE)');
294
+ return { result: [], leftovers: [] };
133
295
  }
134
296
 
135
- if (!root.target)
136
- return error(null, current.$path, { type: root.type }, '"EXISTS" can only be used with associations/compositions, found $(TYPE)');
137
-
138
297
  const subselect = getSubselect(root.target, ref, sources);
139
298
 
140
299
  const target = subselect.SELECT.from.as; // use subquery alias as target - prevent shadowing
@@ -150,18 +309,24 @@ function handleExists(csn, error) {
150
309
  subselect.SELECT.where.push(...[ 'and', ...remapExistingWhere(target, ref.where) ]);
151
310
 
152
311
  newExpr.push(subselect);
312
+ toContinue.push([ exprPath.concat(newExpr.length - 1), exprPath.concat([ newExpr.length - 1, 'SELECT', 'where' ]) ]);
153
313
  }
154
314
  else { // Drill down into other places that might contain a `EXISTS <assoc>`
155
- if (expr[i].xpr)
156
- expr[i].xpr = processExists(query, expr[i].xpr);
157
- if (expr[i].args && Array.isArray(expr[i].args))
158
- expr[i].args = processExists(query, expr[i].args);
159
-
315
+ if (expr[i].xpr) {
316
+ const { result, leftovers } = processExists(queryPath, exprPath.concat([ i, 'xpr' ]));
317
+ expr[i].xpr = result;
318
+ toContinue.push(...leftovers);
319
+ }
320
+ if (expr[i].args && Array.isArray(expr[i].args)) {
321
+ const { result, leftovers } = processExists(queryPath, exprPath.concat([ i, 'args' ]));
322
+ expr[i].args = result;
323
+ toContinue.push(...leftovers);
324
+ }
160
325
  newExpr.push(expr[i]);
161
326
  }
162
327
  }
163
328
 
164
- return newExpr;
329
+ return { result: newExpr, leftovers: toContinue };
165
330
  }
166
331
 
167
332
  /**
@@ -227,23 +392,35 @@ function handleExists(csn, error) {
227
392
  function translateUnmanagedAssocToWhere(root, target, subselect, isPrefixedWithTableAlias, base, current) {
228
393
  for (let j = 0; j < root.on.length; j++) {
229
394
  const part = root.on[j];
395
+
396
+ // we can only resolve stuff on refs - skip literals like =
397
+ // but also keep along stuff like null and undefined, so compiler
398
+ // can have a chance to complain/ we can fail later nicely maybe
399
+ if (!(part && part.ref)) {
400
+ subselect.SELECT.where.push(part);
401
+ continue;
402
+ }
403
+
404
+ // root.$path should be safe - we can only reference things in exists that exist when we enrich
405
+ // so all of them should have a $path.
406
+ const { art, links } = inspectRef(root.$path.concat([ 'on', j ]));
230
407
  // Dollar Self Backlink
231
- if (isValidDollarSelf(root.on[j], root.on[j + 1], root.on[j + 2])) {
408
+ if (isValidDollarSelf(root.on[j], root.$path.concat([ 'on', j ]), root.on[j + 1], root.on[j + 2], root.$path.concat([ 'on', j + 2 ]))) {
232
409
  if (root.on[j].ref[0] === '$self' && root.on[j].ref.length === 1)
233
- subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j + 2]));
410
+ subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j + 2], root.$path.concat([ 'on', j + 2 ])));
234
411
  else
235
- subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j]));
412
+ subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j], root.$path.concat([ 'on', j ])));
236
413
 
237
414
  j += 2;
238
415
  }
239
- else if (part._links && part._links[0].art === root) { // target side
416
+ else if (links && links[0].art === root) { // target side
240
417
  subselect.SELECT.where.push({ ref: [ target, ...part.ref.slice(1) ] });
241
418
  }
242
419
  else if (part.$scope === '$self') { // source side - "absolute" scope
243
420
  // cut off the $self, as we prefix the entity name now
244
421
  subselect.SELECT.where.push({ ref: [ base, ...part.ref.slice(1) ] });
245
422
  }
246
- else if (part._art) { // source side - with local scope
423
+ else if (art) { // source side - with local scope
247
424
  if (isPrefixedWithTableAlias)
248
425
  subselect.SELECT.where.push({ ref: [ ...current.ref.slice(0, -1), ...part.ref ] });
249
426
  else
@@ -258,43 +435,82 @@ function handleExists(csn, error) {
258
435
  * Check that an expression triple is a valid $self
259
436
  *
260
437
  * @param {Token} leftSide
438
+ * @param {CSN.Path} pathLeft
261
439
  * @param {Token} middle
262
440
  * @param {Token} rightSide
441
+ * @param {CSN.Path} pathRight
263
442
  * @returns {boolean}
264
443
  */
265
- function isValidDollarSelf(leftSide, middle, rightSide) {
266
- return leftSide && leftSide.ref &&
267
- rightSide && rightSide.ref &&
268
- middle === '=' &&
269
- (
270
- leftSide.ref[0] === '$self' && leftSide.ref.length === 1 && rightSide._art && rightSide._art.target ||
271
- rightSide.ref[0] === '$self' && rightSide.ref.length === 1 && leftSide._art && leftSide._art.target
272
- );
444
+ function isValidDollarSelf(leftSide, pathLeft, middle, rightSide, pathRight) {
445
+ if (leftSide && leftSide.ref && rightSide && rightSide.ref && middle === '=') {
446
+ const right = inspectRef(pathRight);
447
+ const left = inspectRef(pathLeft);
448
+
449
+ if (!right || !left)
450
+ return false;
451
+
452
+ const rightSideArt = right.art;
453
+ const leftSideArt = left.art;
454
+
455
+ return leftSide.ref[0] === '$self' && leftSide.ref.length === 1 && rightSideArt && rightSideArt.target ||
456
+ rightSide.ref[0] === '$self' && rightSide.ref.length === 1 && leftSideArt && leftSideArt.target;
457
+ }
458
+
459
+ return false;
273
460
  }
274
461
  }
275
462
 
276
463
  /**
277
- * From the given expression (having _links), find the first association.
464
+ * From the given expression (having inspectRef -> links), find the first association.
278
465
  *
279
466
  * @param {object} xprPart
280
- * @returns {{root: CSN.Element, ref: string|object, tail: Array}} The first assoc (root), the corresponding ref (ref) and the rest of the ref (tail).
467
+ * @param {CSN.Path} path
468
+ * @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).
281
469
  */
282
- function getFirstAssoc(xprPart) {
470
+ function getFirstAssoc(xprPart, path) {
471
+ const { links, art } = inspectRef(path);
283
472
  for (let i = 0; i < xprPart.ref.length - 1; i++) {
284
- if (xprPart._links[i].art && xprPart._links[i].art.target)
285
- return { root: xprPart._links[i].art, ref: xprPart.ref[i], tail: xprPart.ref.slice(i + 1) };
473
+ if (links[i].art && links[i].art.target) {
474
+ return {
475
+ head: (i === 0 ? [] : xprPart.ref.slice(0, i)), root: links[i].art, ref: xprPart.ref[i], tail: xprPart.ref.slice(i + 1),
476
+ };
477
+ }
286
478
  }
287
- return { root: xprPart._art, ref: xprPart.ref[xprPart.ref.length - 1], tail: [] };
479
+ return {
480
+ head: (xprPart.ref.length === 1 ? [] : xprPart.ref.slice(0, xprPart.ref.length - 1)), root: art, ref: xprPart.ref[xprPart.ref.length - 1], tail: [],
481
+ };
482
+ }
483
+
484
+ /**
485
+ * Get the last association from the expression part - similar to getFirstAssoc
486
+ *
487
+ * @param {object} xprPart
488
+ * @param {CSN.Path} path
489
+ * @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).
490
+ */
491
+ function getLastAssoc(xprPart, path) {
492
+ const { links, art } = inspectRef(path);
493
+ for (let i = xprPart.ref.length - 1; i > -1; i--) {
494
+ if (links[i].art && links[i].art.target) {
495
+ return {
496
+ head: (i === 0 ? [] : xprPart.ref.slice(0, i)), root: links[i].art, ref: xprPart.ref[i], tail: xprPart.ref.slice(i + 1),
497
+ };
498
+ }
499
+ }
500
+ return {
501
+ head: (xprPart.ref.length === 1 ? [] : xprPart.ref.slice(0, xprPart.ref.length - 1)), root: art, ref: xprPart.ref[xprPart.ref.length - 1], tail: [],
502
+ };
288
503
  }
289
504
 
290
505
  /**
291
- * Check (using _links), wether the first path step is an entity or query source
506
+ * Check (using inspectRef -> links), wether the first path step is an entity or query source
292
507
  *
293
- * @param {object} o
508
+ * @param {CSN.Path} path
294
509
  * @returns {boolean}
295
510
  */
296
- function firstLinkIsEntityOrQuerySource(o) {
297
- return o._links && (o._links[0].art.kind === 'entity' || o._links[0].art.query || o._links[0].art.from);
511
+ function firstLinkIsEntityOrQuerySource(path) {
512
+ const { links } = inspectRef(path);
513
+ return links && (links[0].art.kind === 'entity' || links[0].art.query || links[0].art.from);
298
514
  }
299
515
 
300
516
  /**
@@ -314,13 +530,14 @@ function handleExists(csn, error) {
314
530
  * we can be sure that resolving the ref requires $env information.
315
531
  *
316
532
  * @param {object} xpr
533
+ * @param {CSN.Path} path
317
534
  * @returns {string|undefined} undefined in case of errors
318
535
  * @throws {Error} Throws if xpr.ref but no xpr.$env
319
536
  * @todo $env is going to be removed from CSN, but csnRefs will provide it
320
537
  */
321
538
  // eslint-disable-next-line consistent-return
322
- function getParent(xpr) {
323
- if (firstLinkIsEntityOrQuerySource(xpr)) {
539
+ function getParent(xpr, path) {
540
+ if (firstLinkIsEntityOrQuerySource(path)) {
324
541
  return xpr.ref[0];
325
542
  }
326
543
  else if (xpr.$env) {
@@ -382,14 +599,15 @@ function handleExists(csn, error) {
382
599
  * @param {string|null} queryBase
383
600
  * @param {boolean} isPrefixedWithTableAlias
384
601
  * @param {CSN.Column} current
602
+ * @param {CSN.Path} path
385
603
  * @returns {string}
386
604
  */
387
- function getBase(queryBase, isPrefixedWithTableAlias, current) {
605
+ function getBase(queryBase, isPrefixedWithTableAlias, current, path) {
388
606
  if (queryBase)
389
607
  return getRealName(csn, queryBase);
390
608
  else if (isPrefixedWithTableAlias)
391
609
  return current.ref[0];
392
- return getParent(current);
610
+ return getParent(current, path);
393
611
  }
394
612
 
395
613
 
@@ -421,31 +639,34 @@ function handleExists(csn, error) {
421
639
  * @param {string} base The source entity/query source name
422
640
  * @param {string} target The target entity/query source name
423
641
  * @param {CSN.Element} assoc The association element - the "not-$self" side of the comparison
642
+ * @param {CSN.Path} path
424
643
  * @returns {TokenStream} The WHERE representing the $self comparison
425
644
  */
426
- function translateDollarSelfToWhere(base, target, assoc) {
645
+ function translateDollarSelfToWhere(base, target, assoc, path) {
427
646
  const where = [];
428
- if (assoc._art.keys) {
429
- for (let i = 0; i < assoc._art.keys.length; i++) {
430
- const lop = { ref: [ target, ...assoc.ref.slice(1), ...assoc._art.keys[i].ref ] }; // target side
431
- const rop = { ref: [ base, ...assoc._art.keys[i].ref ] }; // source side
647
+ const { art } = inspectRef(path);
648
+ if (art.keys) {
649
+ for (let i = 0; i < art.keys.length; i++) {
650
+ const lop = { ref: [ target, ...assoc.ref.slice(1), ...art.keys[i].ref ] }; // target side
651
+ const rop = { ref: [ base, ...art.keys[i].ref ] }; // source side
432
652
  if (i > 0)
433
653
  where.push('and');
434
654
 
435
655
  where.push(...[ lop, '=', rop ]);
436
656
  }
437
657
  }
438
- else if (assoc._art.on) {
439
- for (let i = 0; i < assoc._art.on.length; i++) {
440
- const part = assoc._art.on[i];
441
- if (part._links && part._links[0].art === assoc._art) { // target side
658
+ else if (art.on) {
659
+ for (let i = 0; i < art.on.length; i++) {
660
+ const part = art.on[i];
661
+ const partInspect = inspectRef(art.$path.concat([ 'on', i ]));
662
+ if (partInspect.links && partInspect.links[0].art === art) { // target side
442
663
  where.push({ ref: [ base, ...part.ref.slice(1) ] });
443
664
  }
444
665
  else if (part.$scope === '$self') { // source side - "absolute" scope
445
666
  // Same message as in forHanaNew/transformDollarSelfComparisonWithUnmanagedAssoc
446
667
  error(null, part.$path, 'An association that uses "$self" in its ON-condition can\'t be compared to "$self"');
447
668
  }
448
- else if (part._art) { // source side - with local scope
669
+ else if (partInspect.art) { // source side - with local scope
449
670
  where.push({ ref: [ target, ...assoc.ref.slice(1, -1), ...part.ref ] });
450
671
  }
451
672
  else { // operator - or any other leftover