@sap/cds-compiler 2.5.2 → 2.11.0

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 (102) hide show
  1. package/CHANGELOG.md +235 -9
  2. package/bin/cdsc.js +44 -27
  3. package/bin/cdsse.js +1 -0
  4. package/doc/CHANGELOG_BETA.md +37 -3
  5. package/lib/api/.eslintrc.json +2 -0
  6. package/lib/api/main.js +37 -123
  7. package/lib/api/options.js +27 -15
  8. package/lib/api/validate.js +34 -9
  9. package/lib/backends.js +9 -89
  10. package/lib/base/dictionaries.js +2 -1
  11. package/lib/base/keywords.js +32 -2
  12. package/lib/base/message-registry.js +73 -11
  13. package/lib/base/messages.js +86 -30
  14. package/lib/base/model.js +6 -6
  15. package/lib/base/optionProcessorHelper.js +56 -22
  16. package/lib/checks/defaultValues.js +27 -2
  17. package/lib/checks/elements.js +1 -6
  18. package/lib/checks/foreignKeys.js +0 -6
  19. package/lib/checks/managedWithoutKeys.js +17 -0
  20. package/lib/checks/nonexpandableStructured.js +38 -0
  21. package/lib/checks/onConditions.js +9 -45
  22. package/lib/checks/queryNoDbArtifacts.js +25 -7
  23. package/lib/checks/selectItems.js +29 -2
  24. package/lib/checks/types.js +26 -2
  25. package/lib/checks/unknownMagic.js +41 -0
  26. package/lib/checks/utils.js +61 -0
  27. package/lib/checks/validator.js +60 -7
  28. package/lib/compiler/assert-consistency.js +23 -7
  29. package/lib/compiler/base.js +65 -0
  30. package/lib/compiler/builtins.js +30 -1
  31. package/lib/compiler/checks.js +8 -5
  32. package/lib/compiler/definer.js +157 -133
  33. package/lib/compiler/index.js +89 -31
  34. package/lib/compiler/propagator.js +5 -2
  35. package/lib/compiler/resolver.js +375 -185
  36. package/lib/compiler/shared.js +49 -202
  37. package/lib/compiler/utils.js +173 -0
  38. package/lib/edm/annotations/genericTranslation.js +183 -187
  39. package/lib/edm/csn2edm.js +104 -108
  40. package/lib/edm/edm.js +18 -21
  41. package/lib/edm/edmPreprocessor.js +388 -146
  42. package/lib/edm/edmUtils.js +104 -34
  43. package/lib/gen/Dictionary.json +22 -0
  44. package/lib/gen/language.checksum +1 -1
  45. package/lib/gen/language.interp +28 -1
  46. package/lib/gen/language.tokens +79 -69
  47. package/lib/gen/languageLexer.interp +28 -1
  48. package/lib/gen/languageLexer.js +879 -805
  49. package/lib/gen/languageLexer.tokens +71 -62
  50. package/lib/gen/languageParser.js +5330 -4300
  51. package/lib/json/from-csn.js +110 -52
  52. package/lib/json/to-csn.js +434 -120
  53. package/lib/language/antlrParser.js +15 -3
  54. package/lib/language/errorStrategy.js +1 -0
  55. package/lib/language/genericAntlrParser.js +93 -26
  56. package/lib/language/language.g4 +172 -31
  57. package/lib/main.d.ts +216 -19
  58. package/lib/main.js +32 -7
  59. package/lib/model/api.js +78 -0
  60. package/lib/model/csnRefs.js +413 -149
  61. package/lib/model/csnUtils.js +286 -75
  62. package/lib/model/enrichCsn.js +50 -6
  63. package/lib/model/revealInternalProperties.js +22 -5
  64. package/lib/modelCompare/compare.js +39 -21
  65. package/lib/optionProcessor.js +35 -18
  66. package/lib/render/.eslintrc.json +4 -1
  67. package/lib/render/DuplicateChecker.js +9 -6
  68. package/lib/render/toCdl.js +121 -36
  69. package/lib/render/toHdbcds.js +148 -98
  70. package/lib/render/toSql.js +114 -43
  71. package/lib/render/utils/common.js +8 -13
  72. package/lib/render/utils/sql.js +3 -3
  73. package/lib/sql-identifier.js +6 -1
  74. package/lib/transform/db/assertUnique.js +5 -6
  75. package/lib/transform/db/constraints.js +281 -106
  76. package/lib/transform/db/draft.js +11 -8
  77. package/lib/transform/db/expansion.js +584 -0
  78. package/lib/transform/db/flattening.js +341 -0
  79. package/lib/transform/db/groupByOrderBy.js +2 -2
  80. package/lib/transform/db/transformExists.js +345 -65
  81. package/lib/transform/db/views.js +438 -0
  82. package/lib/transform/forHanaNew.js +131 -793
  83. package/lib/transform/forOdataNew.js +30 -24
  84. package/lib/transform/localized.js +39 -10
  85. package/lib/transform/odata/attachPath.js +19 -4
  86. package/lib/transform/odata/generateForeignKeyElements.js +11 -10
  87. package/lib/transform/odata/referenceFlattener.js +60 -39
  88. package/lib/transform/odata/sortByAssociationDependency.js +2 -2
  89. package/lib/transform/odata/structuralPath.js +72 -0
  90. package/lib/transform/odata/structureFlattener.js +19 -18
  91. package/lib/transform/odata/typesExposure.js +22 -12
  92. package/lib/transform/transformUtilsNew.js +144 -78
  93. package/lib/transform/translateAssocsToJoins.js +22 -27
  94. package/lib/transform/universalCsnEnricher.js +67 -0
  95. package/lib/utils/file.js +5 -14
  96. package/lib/utils/moduleResolve.js +6 -8
  97. package/lib/utils/term.js +65 -42
  98. package/lib/utils/timetrace.js +48 -26
  99. package/package.json +1 -1
  100. package/lib/json/walker.js +0 -26
  101. package/lib/transform/sqlite +0 -0
  102. 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,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}
116
243
  */
117
- function processExists(query, expr) {
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}
301
+ */
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,15 @@ 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);
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));
129
350
 
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;
351
+ if (!root.target) {
352
+ error(null, exprPath.concat(i), { type: root.type }, '“EXISTS” can only be used with associations/compositions, found $(TYPE)');
353
+ return { result: [], leftovers: [] };
133
354
  }
134
355
 
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
356
  const subselect = getSubselect(root.target, ref, sources);
139
357
 
140
358
  const target = subselect.SELECT.from.as; // use subquery alias as target - prevent shadowing
@@ -150,18 +368,24 @@ function handleExists(csn, error) {
150
368
  subselect.SELECT.where.push(...[ 'and', ...remapExistingWhere(target, ref.where) ]);
151
369
 
152
370
  newExpr.push(subselect);
371
+ toContinue.push([ exprPath.concat(newExpr.length - 1), exprPath.concat([ newExpr.length - 1, 'SELECT', 'where' ]) ]);
153
372
  }
154
373
  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
-
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
+ }
160
384
  newExpr.push(expr[i]);
161
385
  }
162
386
  }
163
387
 
164
- return newExpr;
388
+ return { result: newExpr, leftovers: toContinue };
165
389
  }
166
390
 
167
391
  /**
@@ -227,23 +451,35 @@ function handleExists(csn, error) {
227
451
  function translateUnmanagedAssocToWhere(root, target, subselect, isPrefixedWithTableAlias, base, current) {
228
452
  for (let j = 0; j < root.on.length; j++) {
229
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 ]));
230
466
  // Dollar Self Backlink
231
- 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 ]))) {
232
468
  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]));
469
+ subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j + 2], root.$path.concat([ 'on', j + 2 ])));
234
470
  else
235
- 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 ])));
236
472
 
237
473
  j += 2;
238
474
  }
239
- else if (part._links && part._links[0].art === root) { // target side
475
+ else if (links && links[0].art === root) { // target side
240
476
  subselect.SELECT.where.push({ ref: [ target, ...part.ref.slice(1) ] });
241
477
  }
242
478
  else if (part.$scope === '$self') { // source side - "absolute" scope
243
479
  // cut off the $self, as we prefix the entity name now
244
480
  subselect.SELECT.where.push({ ref: [ base, ...part.ref.slice(1) ] });
245
481
  }
246
- else if (part._art) { // source side - with local scope
482
+ else if (art) { // source side - with local scope
247
483
  if (isPrefixedWithTableAlias)
248
484
  subselect.SELECT.where.push({ ref: [ ...current.ref.slice(0, -1), ...part.ref ] });
249
485
  else
@@ -258,43 +494,82 @@ function handleExists(csn, error) {
258
494
  * Check that an expression triple is a valid $self
259
495
  *
260
496
  * @param {Token} leftSide
497
+ * @param {CSN.Path} pathLeft
261
498
  * @param {Token} middle
262
499
  * @param {Token} rightSide
500
+ * @param {CSN.Path} pathRight
263
501
  * @returns {boolean}
264
502
  */
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
- );
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;
273
519
  }
274
520
  }
275
521
 
276
522
  /**
277
- * From the given expression (having _links), find the first association.
523
+ * From the given expression (having inspectRef -> links), find the first association.
278
524
  *
279
525
  * @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).
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).
281
528
  */
282
- function getFirstAssoc(xprPart) {
529
+ function getFirstAssoc(xprPart, path) {
530
+ const { links, art } = inspectRef(path);
283
531
  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) };
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
+ }
286
537
  }
287
- 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
+ };
288
562
  }
289
563
 
290
564
  /**
291
- * 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
292
566
  *
293
- * @param {object} o
567
+ * @param {CSN.Path} path
294
568
  * @returns {boolean}
295
569
  */
296
- function firstLinkIsEntityOrQuerySource(o) {
297
- 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);
298
573
  }
299
574
 
300
575
  /**
@@ -314,13 +589,14 @@ function handleExists(csn, error) {
314
589
  * we can be sure that resolving the ref requires $env information.
315
590
  *
316
591
  * @param {object} xpr
592
+ * @param {CSN.Path} path
317
593
  * @returns {string|undefined} undefined in case of errors
318
594
  * @throws {Error} Throws if xpr.ref but no xpr.$env
319
595
  * @todo $env is going to be removed from CSN, but csnRefs will provide it
320
596
  */
321
597
  // eslint-disable-next-line consistent-return
322
- function getParent(xpr) {
323
- if (firstLinkIsEntityOrQuerySource(xpr)) {
598
+ function getParent(xpr, path) {
599
+ if (firstLinkIsEntityOrQuerySource(path)) {
324
600
  return xpr.ref[0];
325
601
  }
326
602
  else if (xpr.$env) {
@@ -364,7 +640,7 @@ function handleExists(csn, error) {
364
640
  };
365
641
  // Because the generated things don't have _links, _art etc. set
366
642
  // We could also make getParent more robust to calculate the links JIT if they are missing
367
- setProp(subselect, '$generatedExists', true);
643
+ generatedExists.set(subselect, true);
368
644
 
369
645
  const nonEnumElements = Object.create(null);
370
646
  nonEnumElements.dummy = {
@@ -382,14 +658,15 @@ function handleExists(csn, error) {
382
658
  * @param {string|null} queryBase
383
659
  * @param {boolean} isPrefixedWithTableAlias
384
660
  * @param {CSN.Column} current
661
+ * @param {CSN.Path} path
385
662
  * @returns {string}
386
663
  */
387
- function getBase(queryBase, isPrefixedWithTableAlias, current) {
664
+ function getBase(queryBase, isPrefixedWithTableAlias, current, path) {
388
665
  if (queryBase)
389
666
  return getRealName(csn, queryBase);
390
667
  else if (isPrefixedWithTableAlias)
391
668
  return current.ref[0];
392
- return getParent(current);
669
+ return getParent(current, path);
393
670
  }
394
671
 
395
672
 
@@ -421,31 +698,34 @@ function handleExists(csn, error) {
421
698
  * @param {string} base The source entity/query source name
422
699
  * @param {string} target The target entity/query source name
423
700
  * @param {CSN.Element} assoc The association element - the "not-$self" side of the comparison
701
+ * @param {CSN.Path} path
424
702
  * @returns {TokenStream} The WHERE representing the $self comparison
425
703
  */
426
- function translateDollarSelfToWhere(base, target, assoc) {
704
+ function translateDollarSelfToWhere(base, target, assoc, path) {
427
705
  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
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
432
711
  if (i > 0)
433
712
  where.push('and');
434
713
 
435
714
  where.push(...[ lop, '=', rop ]);
436
715
  }
437
716
  }
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
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
442
722
  where.push({ ref: [ base, ...part.ref.slice(1) ] });
443
723
  }
444
724
  else if (part.$scope === '$self') { // source side - "absolute" scope
445
725
  // Same message as in forHanaNew/transformDollarSelfComparisonWithUnmanagedAssoc
446
726
  error(null, part.$path, 'An association that uses "$self" in its ON-condition can\'t be compared to "$self"');
447
727
  }
448
- else if (part._art) { // source side - with local scope
728
+ else if (partInspect.art) { // source side - with local scope
449
729
  where.push({ ref: [ target, ...assoc.ref.slice(1, -1), ...part.ref ] });
450
730
  }
451
731
  else { // operator - or any other leftover