@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
@@ -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,42 @@ 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);
52
+ const generatedExists = new WeakMap();
49
53
  forEachDefinition(csn, (artifact, artifactName) => {
50
54
  if (artifact.query) {
51
- forAllQueries(artifact.query, (query) => {
52
- if (!query.$generatedExists) {
55
+ forAllQueries(artifact.query, (query, path) => {
56
+ if (!generatedExists.has(query)) {
57
+ const toProcess = []; // Collect all expressions we need to process here
53
58
  if (query.SELECT && query.SELECT.where && query.SELECT.where.length > 1)
54
- query.SELECT.where = processExists(query, query.SELECT.where);
59
+ toProcess.push([ path.slice(0, -1), path.concat('where') ]);
55
60
 
56
61
 
57
62
  if (query.SELECT && query.SELECT.columns)
58
- query.SELECT.columns = processExists(query, query.SELECT.columns);
63
+ toProcess.push([ path.slice(0, -1), path.concat('columns') ]);
59
64
 
60
65
 
61
66
  if (query.SELECT && query.SELECT.from.on )
62
- query.SELECT.from.on = processExists(query, query.SELECT.from.on);
67
+ toProcess.push([ path.slice(0, -1), path.concat([ 'from', 'on' ]) ]);
68
+
69
+ for (const [ , exprPath ] of toProcess) {
70
+ forbidAssocInExists(exprPath);
71
+ const expr = nestExists(exprPath);
72
+ walkCsnPath(csn, exprPath.slice(0, -1))[exprPath[exprPath.length - 1]] = expr;
73
+ }
74
+
75
+ while (toProcess.length > 0) {
76
+ const [ queryPath, exprPath ] = toProcess.pop();
77
+ // leftovers can happen with nested exists - we then need to drill down into the created SELECT
78
+ // to check for further exists
79
+ const { result, leftovers } = processExists(queryPath, exprPath);
80
+ walkCsnPath(csn, exprPath.slice(0, -1))[exprPath[exprPath.length - 1]] = result;
81
+ toProcess.push(...leftovers.reverse()); // any leftovers - schedule for further processing
82
+ }
63
83
  }
64
84
  }, [ 'definitions', artifactName, 'query' ]);
65
85
  }
@@ -108,14 +128,215 @@ function handleExists(csn, error) {
108
128
  }
109
129
 
110
130
  /**
111
- * Process the given expr of the given query and translate a `EXISTS assoc` into a `EXISTS (subquery)`.
131
+ * Get the index of the first association that is found - starting the
132
+ * search at the given startIndex.
112
133
  *
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
134
+ * @param {number} startIndex Where to start searching
135
+ * @param {object[]} links links for a ref, produced by inspectRef
136
+ * @returns {number|null} Null if no association was found
137
+ */
138
+ function getFirstAssocIndex(startIndex, links) {
139
+ for (let i = startIndex; i < links.length; i++) {
140
+ if (links[i] && links[i].art && links[i].art.target)
141
+ return i;
142
+ }
143
+
144
+ return null;
145
+ }
146
+
147
+ /**
148
+ * For a given ref-array, this function is called for the first assoc-ref in the array.
149
+ *
150
+ * It then runs over the rest of the array and puts all other steps in the first assocs filter.
151
+ * If the rest contains another assoc, we put all following things into that assocs filter and
152
+ * add the sub-assoc to the previous assoc filter.
153
+ *
154
+ * Or in other words:
155
+ * - exists toF[1=1].toG[1=1].toH[1=1] is found
156
+ * - we get called with toF[1=1].toG[1=1].toH[1=1]
157
+ * - we return toF[1=1 and exists toG[1=1 and exists toH[1=1]]]
158
+ *
159
+ * @param {number} startIndex The index of the thing AFTER _main in the ref-array
160
+ * @param {string|object} startAssoc The path step that is the first assoc
161
+ * @param {Array} startRest Any path steps after startAssoc
162
+ * @param {CSN.Path} path to the overall ref where _main is contained
163
+ * @returns {Array} Return the now-nested ref-array
164
+ */
165
+ function nestFilters(startIndex, startAssoc, startRest, path) {
166
+ let revert;
167
+ if (!startAssoc.where) { // initialize first filter if not present
168
+ if (typeof startAssoc === 'string') {
169
+ startAssoc = {
170
+ id: startAssoc,
171
+ where: [],
172
+ };
173
+ revert = () => {
174
+ startAssoc = startAssoc.id;
175
+ };
176
+ }
177
+ else {
178
+ startAssoc.where = [];
179
+ revert = () => {
180
+ delete startAssoc.where;
181
+ };
182
+ }
183
+ }
184
+ const stack = [ [ null, startAssoc, startRest, startIndex ] ];
185
+ const { links } = inspectRef(path);
186
+ while (stack.length > 0) {
187
+ // previous: to nest "up" if the previous assoc did not originaly have a filter
188
+ // assoc: the assoc path step
189
+ // rest: path steps after assoc
190
+ // index: index of after-assoc in the overall ref-array - so we know where to start looking for the next assoc
191
+ const workPackage = stack.pop();
192
+ const [ previous, , rest, index ] = workPackage;
193
+ let [ , assoc, , ] = workPackage;
194
+
195
+ const firstAssocIndex = getFirstAssocIndex(index, links);
196
+
197
+ const head = rest.slice(0, firstAssocIndex - index);
198
+ const nextAssoc = rest[firstAssocIndex - index];
199
+ const tail = rest.slice(firstAssocIndex - index + 1);
200
+
201
+ const hasAssoc = nextAssoc !== undefined;
202
+
203
+ if (!assoc.where && hasAssoc) { // no existing filter - and there is stuff we need to nest afterwards
204
+ if (typeof assoc === 'string') {
205
+ assoc = {
206
+ id: assoc,
207
+ where: [],
208
+ };
209
+ // We need to "hook" this into the previous filter.
210
+ // Since we create a new object, we don't have a handy reference we can just manipulate
211
+ if (previous)
212
+ previous.where[previous.where.length - 1] = { ref: [ assoc ] };
213
+ }
214
+ else {
215
+ assoc.where = [];
216
+ }
217
+ }
218
+ else if (assoc.where && assoc.where.length > 0 && (hasAssoc || rest.length > 0)) {
219
+ assoc.where.push('and');
220
+ } // merge with existing filter
221
+
222
+ if (hasAssoc)
223
+ assoc.where.push('exists', { ref: [ ...head, nextAssoc ] });
224
+ else if (rest.length > 0)
225
+ assoc.where.push({ ref: rest });
226
+
227
+ if (hasAssoc)
228
+ stack.push([ assoc, nextAssoc, tail, firstAssocIndex ]);
229
+ }
230
+
231
+ // Seems like we did not have anything to nest into the filter - then kill it
232
+ if (startAssoc.where.length === 0 && revert !== undefined)
233
+ revert();
234
+
235
+ return startAssoc;
236
+ }
237
+
238
+ /**
239
+ * Check that associations in filters (in an exists expression) are only fk-accesses. Everything else is forbidden.
240
+ *
241
+ * @param {CSN.Path} exprPath
242
+ * @returns {void}
243
+ */
244
+ function forbidAssocInExists(exprPath) {
245
+ const expr = walkCsnPath(csn, exprPath);
246
+ for (let i = 0; i < expr.length; i++) {
247
+ if (i < expr.length - 1 && expr[i] === 'exists' && expr[i + 1].ref) {
248
+ i++;
249
+ const current = expr[i];
250
+
251
+ const { links } = inspectRef(exprPath.concat(i));
252
+
253
+ const assocs = links.filter(link => link.art && link.art.target).map(link => current.ref[link.idx]);
254
+
255
+ checkForInvalidAssoc(assocs);
256
+ }
257
+ }
258
+ }
259
+
260
+ /**
261
+ * @param {object[]} assocs Array of refs of assocs - possibly with a .where to check
262
+ */
263
+ function checkForInvalidAssoc(assocs) {
264
+ for (const assoc of assocs) {
265
+ if (assoc.where) {
266
+ for (let i = 0; i < assoc.where.length; i++) {
267
+ const part = assoc.where[i];
268
+
269
+ if (part._links && !(assoc.where[i - 1] && assoc.where[i - 1] === 'exists')) {
270
+ for (const link of part._links) {
271
+ if (link.art && link.art.target) {
272
+ if (link.art.keys) { // managed - allow FK access
273
+ if (part._links[link.idx + 1] !== undefined) { // there is a next path step - check if it is a fk
274
+ if (!(part._links[link.idx + 1] && link.art.keys.some(fk => fk._art === part._links[link.idx + 1].art)))
275
+ error(null, part.$path, { id: assoc.id, name: assoc.where[part.$path[part.$path.length - 1]].ref[link.idx] }, 'Unexpected non foreign key access after managed association $(NAME) in filter expression of $(ID)');
276
+ }
277
+ else { // no traversal, ends on managed
278
+ error(null, part.$path, { id: assoc.id, name: assoc.where[part.$path[part.$path.length - 1]].ref[link.idx] }, 'Unexpected managed association $(NAME) in filter expression of $(ID)');
279
+ }
280
+ }
281
+ else { // unmanaged - always wrong
282
+ error(null, part.$path, { id: assoc.id, name: assoc.where[part.$path[part.$path.length - 1]].ref[link.idx] }, 'Unexpected unmanaged association $(NAME) in filter expression of $(ID)');
283
+ }
284
+ // Recursively drill down if the assoc-step has a filter
285
+ if (part.ref[link.idx].where)
286
+ checkForInvalidAssoc([ part.ref[link.idx] ]);
287
+ }
288
+ }
289
+ }
290
+ }
291
+ }
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Walk to the expr using the given path and scan it for the "exists" + "ref" pattern.
297
+ * If such a pattern is found, nest association steps therein into filters.
298
+ *
299
+ * @param {CSN.Path} exprPath
300
+ * @returns {Array}
116
301
  */
117
- function processExists(query, expr) {
302
+ function nestExists(exprPath) {
303
+ const expr = walkCsnPath(csn, exprPath);
304
+ for (let i = 0; i < expr.length; i++) {
305
+ if (i < expr.length - 1 && expr[i] === 'exists' && expr[i + 1].ref) {
306
+ i++;
307
+ const current = expr[i];
308
+ const {
309
+ ref, head, tail,
310
+ } = getFirstAssoc(current, exprPath.concat(i));
311
+
312
+ const lastAssoc = getLastAssoc(current, exprPath.concat(i));
313
+ // toE.toF.id -> we must not end on a non-assoc - this will also be caught downstream by
314
+ // '“EXISTS” can only be used with associations/compositions, found $(TYPE)'
315
+ // But the error might not be clear, since it could be because of our rewritten stuff. The later check
316
+ // checks for exists id -> our rewrite turns toE.toF.id into toE[exists toF[exists id]], leading to the same error
317
+ if (lastAssoc.tail.length > 0)
318
+ 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"');
319
+
320
+ const newThing = [ ...head, nestFilters(head.length + 1, ref, tail, exprPath.concat([ i ])) ];
321
+ expr[i].ref = newThing;
322
+ }
323
+ }
324
+
325
+ return expr;
326
+ }
327
+
328
+ /**
329
+ * 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.
330
+ *
331
+ * @param {CSN.Path} queryPath Path to the query-object
332
+ * @param {CSN.Path} exprPath Path to the expression-array to process
333
+ * @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.
334
+ */
335
+ function processExists(queryPath, exprPath) {
336
+ const toContinue = [];
118
337
  const newExpr = [];
338
+ const query = walkCsnPath(csn, queryPath);
339
+ const expr = walkCsnPath(csn, exprPath);
119
340
  const queryBase = query.SELECT.from.ref ? (query.SELECT.from.as || query.SELECT.from.ref[0]) : null;
120
341
  const sources = getQuerySources(query.SELECT);
121
342
 
@@ -123,18 +344,13 @@ function handleExists(csn, error) {
123
344
  if (i < expr.length - 1 && expr[i] === 'exists' && expr[i + 1].ref) {
124
345
  i++;
125
346
  const current = expr[i];
126
- const isPrefixedWithTableAlias = firstLinkIsEntityOrQuerySource(current);
127
- const base = getBase(queryBase, isPrefixedWithTableAlias, current);
128
- const { root, ref, tail } = getFirstAssoc(current);
129
-
130
- if (tail.length > 0) {
131
- 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
- continue;
133
- }
347
+ const isPrefixedWithTableAlias = firstLinkIsEntityOrQuerySource(exprPath.concat(i));
348
+ const base = getBase(queryBase, isPrefixedWithTableAlias, current, exprPath.concat(i));
349
+ const { root, ref } = getFirstAssoc(current, exprPath.concat(i));
134
350
 
135
351
  if (!root.target) {
136
- error(null, current.$path, { type: root.type }, '“EXISTS” can only be used with associations/compositions, found $(TYPE)');
137
- return [];
352
+ error(null, exprPath.concat(i), { type: root.type }, '“EXISTS” can only be used with associations/compositions, found $(TYPE)');
353
+ return { result: [], leftovers: [] };
138
354
  }
139
355
 
140
356
  const subselect = getSubselect(root.target, ref, sources);
@@ -152,18 +368,24 @@ function handleExists(csn, error) {
152
368
  subselect.SELECT.where.push(...[ 'and', ...remapExistingWhere(target, ref.where) ]);
153
369
 
154
370
  newExpr.push(subselect);
371
+ toContinue.push([ exprPath.concat(newExpr.length - 1), exprPath.concat([ newExpr.length - 1, 'SELECT', 'where' ]) ]);
155
372
  }
156
373
  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
-
374
+ if (expr[i].xpr) {
375
+ const { result, leftovers } = processExists(queryPath, exprPath.concat([ i, 'xpr' ]));
376
+ expr[i].xpr = result;
377
+ toContinue.push(...leftovers);
378
+ }
379
+ if (expr[i].args && Array.isArray(expr[i].args)) {
380
+ const { result, leftovers } = processExists(queryPath, exprPath.concat([ i, 'args' ]));
381
+ expr[i].args = result;
382
+ toContinue.push(...leftovers);
383
+ }
162
384
  newExpr.push(expr[i]);
163
385
  }
164
386
  }
165
387
 
166
- return newExpr;
388
+ return { result: newExpr, leftovers: toContinue };
167
389
  }
168
390
 
169
391
  /**
@@ -229,23 +451,35 @@ function handleExists(csn, error) {
229
451
  function translateUnmanagedAssocToWhere(root, target, subselect, isPrefixedWithTableAlias, base, current) {
230
452
  for (let j = 0; j < root.on.length; j++) {
231
453
  const part = root.on[j];
454
+
455
+ // we can only resolve stuff on refs - skip literals like =
456
+ // but also keep along stuff like null and undefined, so compiler
457
+ // can have a chance to complain/ we can fail later nicely maybe
458
+ if (!(part && part.ref)) {
459
+ subselect.SELECT.where.push(part);
460
+ continue;
461
+ }
462
+
463
+ // root.$path should be safe - we can only reference things in exists that exist when we enrich
464
+ // so all of them should have a $path.
465
+ const { art, links } = inspectRef(root.$path.concat([ 'on', j ]));
232
466
  // Dollar Self Backlink
233
- if (isValidDollarSelf(root.on[j], root.on[j + 1], root.on[j + 2])) {
467
+ 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
468
  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]));
469
+ subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j + 2], root.$path.concat([ 'on', j + 2 ])));
236
470
  else
237
- subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j]));
471
+ subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j], root.$path.concat([ 'on', j ])));
238
472
 
239
473
  j += 2;
240
474
  }
241
- else if (part._links && part._links[0].art === root) { // target side
475
+ else if (links && links[0].art === root) { // target side
242
476
  subselect.SELECT.where.push({ ref: [ target, ...part.ref.slice(1) ] });
243
477
  }
244
478
  else if (part.$scope === '$self') { // source side - "absolute" scope
245
479
  // cut off the $self, as we prefix the entity name now
246
480
  subselect.SELECT.where.push({ ref: [ base, ...part.ref.slice(1) ] });
247
481
  }
248
- else if (part._art) { // source side - with local scope
482
+ else if (art) { // source side - with local scope
249
483
  if (isPrefixedWithTableAlias)
250
484
  subselect.SELECT.where.push({ ref: [ ...current.ref.slice(0, -1), ...part.ref ] });
251
485
  else
@@ -260,43 +494,82 @@ function handleExists(csn, error) {
260
494
  * Check that an expression triple is a valid $self
261
495
  *
262
496
  * @param {Token} leftSide
497
+ * @param {CSN.Path} pathLeft
263
498
  * @param {Token} middle
264
499
  * @param {Token} rightSide
500
+ * @param {CSN.Path} pathRight
265
501
  * @returns {boolean}
266
502
  */
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
- );
503
+ function isValidDollarSelf(leftSide, pathLeft, middle, rightSide, pathRight) {
504
+ if (leftSide && leftSide.ref && rightSide && rightSide.ref && middle === '=') {
505
+ const right = inspectRef(pathRight);
506
+ const left = inspectRef(pathLeft);
507
+
508
+ if (!right || !left)
509
+ return false;
510
+
511
+ const rightSideArt = right.art;
512
+ const leftSideArt = left.art;
513
+
514
+ return leftSide.ref[0] === '$self' && leftSide.ref.length === 1 && rightSideArt && rightSideArt.target ||
515
+ rightSide.ref[0] === '$self' && rightSide.ref.length === 1 && leftSideArt && leftSideArt.target;
516
+ }
517
+
518
+ return false;
275
519
  }
276
520
  }
277
521
 
278
522
  /**
279
- * From the given expression (having _links), find the first association.
523
+ * From the given expression (having inspectRef -> links), find the first association.
280
524
  *
281
525
  * @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).
526
+ * @param {CSN.Path} path
527
+ * @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
528
  */
284
- function getFirstAssoc(xprPart) {
529
+ function getFirstAssoc(xprPart, path) {
530
+ const { links, art } = inspectRef(path);
285
531
  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) };
532
+ if (links[i].art && links[i].art.target) {
533
+ return {
534
+ head: (i === 0 ? [] : xprPart.ref.slice(0, i)), root: links[i].art, ref: xprPart.ref[i], tail: xprPart.ref.slice(i + 1),
535
+ };
536
+ }
288
537
  }
289
- return { root: xprPart._art, ref: xprPart.ref[xprPart.ref.length - 1], tail: [] };
538
+ return {
539
+ head: (xprPart.ref.length === 1 ? [] : xprPart.ref.slice(0, xprPart.ref.length - 1)), root: art, ref: xprPart.ref[xprPart.ref.length - 1], tail: [],
540
+ };
541
+ }
542
+
543
+ /**
544
+ * Get the last association from the expression part - similar to getFirstAssoc
545
+ *
546
+ * @param {object} xprPart
547
+ * @param {CSN.Path} path
548
+ * @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).
549
+ */
550
+ function getLastAssoc(xprPart, path) {
551
+ const { links, art } = inspectRef(path);
552
+ for (let i = xprPart.ref.length - 1; i > -1; i--) {
553
+ if (links[i].art && links[i].art.target) {
554
+ return {
555
+ head: (i === 0 ? [] : xprPart.ref.slice(0, i)), root: links[i].art, ref: xprPart.ref[i], tail: xprPart.ref.slice(i + 1),
556
+ };
557
+ }
558
+ }
559
+ return {
560
+ head: (xprPart.ref.length === 1 ? [] : xprPart.ref.slice(0, xprPart.ref.length - 1)), root: art, ref: xprPart.ref[xprPart.ref.length - 1], tail: [],
561
+ };
290
562
  }
291
563
 
292
564
  /**
293
- * Check (using _links), wether the first path step is an entity or query source
565
+ * Check (using inspectRef -> links), wether the first path step is an entity or query source
294
566
  *
295
- * @param {object} o
567
+ * @param {CSN.Path} path
296
568
  * @returns {boolean}
297
569
  */
298
- function firstLinkIsEntityOrQuerySource(o) {
299
- return o._links && (o._links[0].art.kind === 'entity' || o._links[0].art.query || o._links[0].art.from);
570
+ function firstLinkIsEntityOrQuerySource(path) {
571
+ const { links } = inspectRef(path);
572
+ return links && (links[0].art.kind === 'entity' || links[0].art.query || links[0].art.from);
300
573
  }
301
574
 
302
575
  /**
@@ -316,13 +589,14 @@ function handleExists(csn, error) {
316
589
  * we can be sure that resolving the ref requires $env information.
317
590
  *
318
591
  * @param {object} xpr
592
+ * @param {CSN.Path} path
319
593
  * @returns {string|undefined} undefined in case of errors
320
594
  * @throws {Error} Throws if xpr.ref but no xpr.$env
321
595
  * @todo $env is going to be removed from CSN, but csnRefs will provide it
322
596
  */
323
597
  // eslint-disable-next-line consistent-return
324
- function getParent(xpr) {
325
- if (firstLinkIsEntityOrQuerySource(xpr)) {
598
+ function getParent(xpr, path) {
599
+ if (firstLinkIsEntityOrQuerySource(path)) {
326
600
  return xpr.ref[0];
327
601
  }
328
602
  else if (xpr.$env) {
@@ -366,7 +640,7 @@ function handleExists(csn, error) {
366
640
  };
367
641
  // Because the generated things don't have _links, _art etc. set
368
642
  // We could also make getParent more robust to calculate the links JIT if they are missing
369
- setProp(subselect, '$generatedExists', true);
643
+ generatedExists.set(subselect, true);
370
644
 
371
645
  const nonEnumElements = Object.create(null);
372
646
  nonEnumElements.dummy = {
@@ -384,14 +658,15 @@ function handleExists(csn, error) {
384
658
  * @param {string|null} queryBase
385
659
  * @param {boolean} isPrefixedWithTableAlias
386
660
  * @param {CSN.Column} current
661
+ * @param {CSN.Path} path
387
662
  * @returns {string}
388
663
  */
389
- function getBase(queryBase, isPrefixedWithTableAlias, current) {
664
+ function getBase(queryBase, isPrefixedWithTableAlias, current, path) {
390
665
  if (queryBase)
391
666
  return getRealName(csn, queryBase);
392
667
  else if (isPrefixedWithTableAlias)
393
668
  return current.ref[0];
394
- return getParent(current);
669
+ return getParent(current, path);
395
670
  }
396
671
 
397
672
 
@@ -423,31 +698,34 @@ function handleExists(csn, error) {
423
698
  * @param {string} base The source entity/query source name
424
699
  * @param {string} target The target entity/query source name
425
700
  * @param {CSN.Element} assoc The association element - the "not-$self" side of the comparison
701
+ * @param {CSN.Path} path
426
702
  * @returns {TokenStream} The WHERE representing the $self comparison
427
703
  */
428
- function translateDollarSelfToWhere(base, target, assoc) {
704
+ function translateDollarSelfToWhere(base, target, assoc, path) {
429
705
  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
706
+ const { art } = inspectRef(path);
707
+ if (art.keys) {
708
+ for (let i = 0; i < art.keys.length; i++) {
709
+ const lop = { ref: [ target, ...assoc.ref.slice(1), ...art.keys[i].ref ] }; // target side
710
+ const rop = { ref: [ base, ...art.keys[i].ref ] }; // source side
434
711
  if (i > 0)
435
712
  where.push('and');
436
713
 
437
714
  where.push(...[ lop, '=', rop ]);
438
715
  }
439
716
  }
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
717
+ else if (art.on) {
718
+ for (let i = 0; i < art.on.length; i++) {
719
+ const part = art.on[i];
720
+ const partInspect = inspectRef(art.$path.concat([ 'on', i ]));
721
+ if (partInspect.links && partInspect.links[0].art === art) { // target side
444
722
  where.push({ ref: [ base, ...part.ref.slice(1) ] });
445
723
  }
446
724
  else if (part.$scope === '$self') { // source side - "absolute" scope
447
725
  // Same message as in forHanaNew/transformDollarSelfComparisonWithUnmanagedAssoc
448
726
  error(null, part.$path, 'An association that uses "$self" in its ON-condition can\'t be compared to "$self"');
449
727
  }
450
- else if (part._art) { // source side - with local scope
728
+ else if (partInspect.art) { // source side - with local scope
451
729
  where.push({ ref: [ target, ...assoc.ref.slice(1, -1), ...part.ref ] });
452
730
  }
453
731
  else { // operator - or any other leftover