@sap/cds-compiler 2.4.4 → 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 (106) hide show
  1. package/CHANGELOG.md +241 -1
  2. package/bin/.eslintrc.json +17 -0
  3. package/bin/cds_update_identifiers.js +8 -7
  4. package/bin/cdsc.js +180 -132
  5. package/bin/cdshi.js +18 -11
  6. package/bin/cdsse.js +38 -32
  7. package/bin/cdsv2m.js +8 -7
  8. package/doc/CHANGELOG_BETA.md +36 -1
  9. package/lib/api/main.js +81 -100
  10. package/lib/api/options.js +17 -11
  11. package/lib/api/validate.js +12 -8
  12. package/lib/backends.js +0 -81
  13. package/lib/base/keywords.js +32 -2
  14. package/lib/base/location.js +2 -2
  15. package/lib/base/message-registry.js +66 -4
  16. package/lib/base/messages.js +84 -27
  17. package/lib/base/model.js +2 -61
  18. package/lib/checks/arrayOfs.js +0 -1
  19. package/lib/checks/defaultValues.js +27 -2
  20. package/lib/checks/elements.js +1 -6
  21. package/lib/checks/enricher.js +8 -2
  22. package/lib/checks/foreignKeys.js +0 -6
  23. package/lib/checks/managedWithoutKeys.js +17 -0
  24. package/lib/checks/nonexpandableStructured.js +38 -0
  25. package/lib/checks/onConditions.js +9 -45
  26. package/lib/checks/queryNoDbArtifacts.js +27 -9
  27. package/lib/checks/selectItems.js +25 -2
  28. package/lib/checks/types.js +26 -2
  29. package/lib/checks/unknownMagic.js +38 -0
  30. package/lib/checks/utils.js +61 -0
  31. package/lib/checks/validator.js +66 -13
  32. package/lib/compiler/assert-consistency.js +24 -12
  33. package/lib/compiler/builtins.js +2 -0
  34. package/lib/compiler/checks.js +6 -4
  35. package/lib/compiler/definer.js +101 -39
  36. package/lib/compiler/index.js +88 -59
  37. package/lib/compiler/resolver.js +455 -209
  38. package/lib/compiler/shared.js +57 -33
  39. package/lib/edm/annotations/genericTranslation.js +183 -187
  40. package/lib/edm/csn2edm.js +128 -99
  41. package/lib/edm/edm.js +18 -21
  42. package/lib/edm/edmPreprocessor.js +361 -127
  43. package/lib/edm/edmUtils.js +103 -33
  44. package/lib/gen/Dictionary.json +74 -28
  45. package/lib/gen/language.checksum +1 -1
  46. package/lib/gen/language.interp +18 -4
  47. package/lib/gen/language.tokens +124 -118
  48. package/lib/gen/languageLexer.interp +13 -1
  49. package/lib/gen/languageLexer.js +870 -839
  50. package/lib/gen/languageLexer.tokens +116 -111
  51. package/lib/gen/languageParser.js +5894 -5614
  52. package/lib/json/from-csn.js +152 -67
  53. package/lib/json/to-csn.js +334 -135
  54. package/lib/language/antlrParser.js +4 -3
  55. package/lib/language/errorStrategy.js +1 -0
  56. package/lib/language/genericAntlrParser.js +24 -14
  57. package/lib/language/language.g4 +188 -128
  58. package/lib/main.d.ts +435 -0
  59. package/lib/main.js +31 -7
  60. package/lib/model/api.js +78 -0
  61. package/lib/model/csnRefs.js +463 -187
  62. package/lib/model/csnUtils.js +280 -136
  63. package/lib/model/enrichCsn.js +75 -4
  64. package/lib/model/revealInternalProperties.js +2 -1
  65. package/lib/modelCompare/compare.js +70 -25
  66. package/lib/optionProcessor.js +13 -10
  67. package/lib/render/.eslintrc.json +4 -1
  68. package/lib/render/DuplicateChecker.js +8 -5
  69. package/lib/render/toCdl.js +123 -40
  70. package/lib/render/toHdbcds.js +156 -65
  71. package/lib/render/toSql.js +87 -11
  72. package/lib/render/utils/common.js +55 -9
  73. package/lib/render/utils/sql.js +3 -3
  74. package/lib/sql-identifier.js +6 -1
  75. package/lib/transform/{sql → db}/.eslintrc.json +0 -0
  76. package/lib/transform/{sql → db}/assertUnique.js +7 -8
  77. package/lib/transform/{sql → db}/constraints.js +35 -20
  78. package/lib/transform/db/draft.js +353 -0
  79. package/lib/transform/db/expansion.js +582 -0
  80. package/lib/transform/db/flattening.js +325 -0
  81. package/lib/transform/{sql → db}/groupByOrderBy.js +8 -16
  82. package/lib/transform/{sql → db}/helpers.js +0 -0
  83. package/lib/transform/{sql → db}/transformExists.js +256 -60
  84. package/lib/transform/forHanaNew.js +216 -765
  85. package/lib/transform/forOdataNew.js +60 -56
  86. package/lib/transform/localized.js +48 -26
  87. package/lib/transform/odata/attachPath.js +19 -4
  88. package/lib/transform/odata/expandStructKeysInAssociations.js +2 -2
  89. package/lib/transform/odata/generateForeignKeyElements.js +13 -12
  90. package/lib/transform/odata/referenceFlattener.js +60 -36
  91. package/lib/transform/odata/sortByAssociationDependency.js +4 -4
  92. package/lib/transform/odata/structuralPath.js +76 -0
  93. package/lib/transform/odata/structureFlattener.js +21 -22
  94. package/lib/transform/odata/toFinalBaseType.js +5 -5
  95. package/lib/transform/odata/typesExposure.js +27 -17
  96. package/lib/transform/odata/utils.js +2 -2
  97. package/lib/transform/transformUtilsNew.js +141 -77
  98. package/lib/transform/translateAssocsToJoins.js +17 -14
  99. package/lib/transform/universalCsnEnricher.js +67 -0
  100. package/lib/utils/file.js +0 -11
  101. package/lib/utils/moduleResolve.js +6 -8
  102. package/lib/utils/timetrace.js +6 -1
  103. package/package.json +2 -1
  104. package/lib/base/deepCopy.js +0 -66
  105. package/lib/json/walker.js +0 -26
  106. 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,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
116
162
  */
117
- function processExists(query, expr) {
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
+ 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,17 +276,19 @@ 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
- error(null, current.$path, { id: tail[0], name: ref.id ? ref.id : ref }, 'Unexpected path step $(ID) after association $(NAME) in "EXISTS"');
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"');
132
285
  continue;
133
286
  }
134
287
 
135
- if (!root.target)
136
- return error(null, current.$path, { type: root.type }, '"EXISTS" can only be used with associations/compositions, found $(TYPE)');
288
+ if (!root.target) {
289
+ error(null, current.$path, { type: root.type }, 'EXISTS can only be used with associations/compositions, found $(TYPE)');
290
+ return { result: [], leftovers: [] };
291
+ }
137
292
 
138
293
  const subselect = getSubselect(root.target, ref, sources);
139
294
 
@@ -150,18 +305,24 @@ function handleExists(csn, error) {
150
305
  subselect.SELECT.where.push(...[ 'and', ...remapExistingWhere(target, ref.where) ]);
151
306
 
152
307
  newExpr.push(subselect);
308
+ toContinue.push([ exprPath.concat(newExpr.length - 1), exprPath.concat([ newExpr.length - 1, 'SELECT', 'where' ]) ]);
153
309
  }
154
310
  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
-
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
+ }
160
321
  newExpr.push(expr[i]);
161
322
  }
162
323
  }
163
324
 
164
- return newExpr;
325
+ return { result: newExpr, leftovers: toContinue };
165
326
  }
166
327
 
167
328
  /**
@@ -227,23 +388,35 @@ function handleExists(csn, error) {
227
388
  function translateUnmanagedAssocToWhere(root, target, subselect, isPrefixedWithTableAlias, base, current) {
228
389
  for (let j = 0; j < root.on.length; j++) {
229
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 ]));
230
403
  // Dollar Self Backlink
231
- 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 ]))) {
232
405
  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]));
406
+ subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j + 2], root.$path.concat([ 'on', j + 2 ])));
234
407
  else
235
- 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 ])));
236
409
 
237
410
  j += 2;
238
411
  }
239
- else if (part._links && part._links[0].art === root) { // target side
412
+ else if (links && links[0].art === root) { // target side
240
413
  subselect.SELECT.where.push({ ref: [ target, ...part.ref.slice(1) ] });
241
414
  }
242
415
  else if (part.$scope === '$self') { // source side - "absolute" scope
243
416
  // cut off the $self, as we prefix the entity name now
244
417
  subselect.SELECT.where.push({ ref: [ base, ...part.ref.slice(1) ] });
245
418
  }
246
- else if (part._art) { // source side - with local scope
419
+ else if (art) { // source side - with local scope
247
420
  if (isPrefixedWithTableAlias)
248
421
  subselect.SELECT.where.push({ ref: [ ...current.ref.slice(0, -1), ...part.ref ] });
249
422
  else
@@ -258,43 +431,61 @@ function handleExists(csn, error) {
258
431
  * Check that an expression triple is a valid $self
259
432
  *
260
433
  * @param {Token} leftSide
434
+ * @param {CSN.Path} pathLeft
261
435
  * @param {Token} middle
262
436
  * @param {Token} rightSide
437
+ * @param {CSN.Path} pathRight
263
438
  * @returns {boolean}
264
439
  */
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
- );
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;
273
456
  }
274
457
  }
275
458
 
276
459
  /**
277
- * From the given expression (having _links), find the first association.
460
+ * From the given expression (having inspectRef -> links), find the first association.
278
461
  *
279
462
  * @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).
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).
281
465
  */
282
- function getFirstAssoc(xprPart) {
466
+ function getFirstAssoc(xprPart, path) {
467
+ const { links, art } = inspectRef(path);
283
468
  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) };
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
+ }
286
474
  }
287
- 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
+ };
288
478
  }
289
479
 
290
480
  /**
291
- * 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
292
482
  *
293
- * @param {object} o
483
+ * @param {CSN.Path} path
294
484
  * @returns {boolean}
295
485
  */
296
- function firstLinkIsEntityOrQuerySource(o) {
297
- 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);
298
489
  }
299
490
 
300
491
  /**
@@ -314,13 +505,14 @@ function handleExists(csn, error) {
314
505
  * we can be sure that resolving the ref requires $env information.
315
506
  *
316
507
  * @param {object} xpr
508
+ * @param {CSN.Path} path
317
509
  * @returns {string|undefined} undefined in case of errors
318
510
  * @throws {Error} Throws if xpr.ref but no xpr.$env
319
511
  * @todo $env is going to be removed from CSN, but csnRefs will provide it
320
512
  */
321
513
  // eslint-disable-next-line consistent-return
322
- function getParent(xpr) {
323
- if (firstLinkIsEntityOrQuerySource(xpr)) {
514
+ function getParent(xpr, path) {
515
+ if (firstLinkIsEntityOrQuerySource(path)) {
324
516
  return xpr.ref[0];
325
517
  }
326
518
  else if (xpr.$env) {
@@ -382,14 +574,15 @@ function handleExists(csn, error) {
382
574
  * @param {string|null} queryBase
383
575
  * @param {boolean} isPrefixedWithTableAlias
384
576
  * @param {CSN.Column} current
577
+ * @param {CSN.Path} path
385
578
  * @returns {string}
386
579
  */
387
- function getBase(queryBase, isPrefixedWithTableAlias, current) {
580
+ function getBase(queryBase, isPrefixedWithTableAlias, current, path) {
388
581
  if (queryBase)
389
582
  return getRealName(csn, queryBase);
390
583
  else if (isPrefixedWithTableAlias)
391
584
  return current.ref[0];
392
- return getParent(current);
585
+ return getParent(current, path);
393
586
  }
394
587
 
395
588
 
@@ -421,31 +614,34 @@ function handleExists(csn, error) {
421
614
  * @param {string} base The source entity/query source name
422
615
  * @param {string} target The target entity/query source name
423
616
  * @param {CSN.Element} assoc The association element - the "not-$self" side of the comparison
617
+ * @param {CSN.Path} path
424
618
  * @returns {TokenStream} The WHERE representing the $self comparison
425
619
  */
426
- function translateDollarSelfToWhere(base, target, assoc) {
620
+ function translateDollarSelfToWhere(base, target, assoc, path) {
427
621
  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
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
432
627
  if (i > 0)
433
628
  where.push('and');
434
629
 
435
630
  where.push(...[ lop, '=', rop ]);
436
631
  }
437
632
  }
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
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
442
638
  where.push({ ref: [ base, ...part.ref.slice(1) ] });
443
639
  }
444
640
  else if (part.$scope === '$self') { // source side - "absolute" scope
445
641
  // Same message as in forHanaNew/transformDollarSelfComparisonWithUnmanagedAssoc
446
642
  error(null, part.$path, 'An association that uses "$self" in its ON-condition can\'t be compared to "$self"');
447
643
  }
448
- else if (part._art) { // source side - with local scope
644
+ else if (partInspect.art) { // source side - with local scope
449
645
  where.push({ ref: [ target, ...assoc.ref.slice(1, -1), ...part.ref ] });
450
646
  }
451
647
  else { // operator - or any other leftover