@sap/cds-compiler 2.10.4 → 2.12.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 (103) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/bin/.eslintrc.json +1 -2
  3. package/bin/cds_update_identifiers.js +10 -8
  4. package/bin/cdsc.js +58 -35
  5. package/bin/cdsse.js +1 -0
  6. package/bin/cdsv2m.js +3 -2
  7. package/doc/CHANGELOG_ARCHIVE.md +1 -1
  8. package/doc/CHANGELOG_BETA.md +16 -0
  9. package/lib/api/.eslintrc.json +2 -0
  10. package/lib/api/main.js +10 -36
  11. package/lib/api/options.js +17 -8
  12. package/lib/api/validate.js +30 -3
  13. package/lib/backends.js +12 -13
  14. package/lib/base/dictionaries.js +2 -1
  15. package/lib/base/keywords.js +3 -2
  16. package/lib/base/message-registry.js +64 -11
  17. package/lib/base/messages.js +38 -18
  18. package/lib/base/model.js +6 -4
  19. package/lib/base/optionProcessorHelper.js +148 -86
  20. package/lib/checks/.eslintrc.json +2 -0
  21. package/lib/checks/actionsFunctions.js +2 -1
  22. package/lib/checks/emptyOrOnlyVirtual.js +2 -2
  23. package/lib/checks/foreignKeys.js +4 -4
  24. package/lib/checks/managedInType.js +4 -4
  25. package/lib/checks/queryNoDbArtifacts.js +1 -3
  26. package/lib/checks/selectItems.js +4 -0
  27. package/lib/checks/sql-snippets.js +93 -0
  28. package/lib/checks/unknownMagic.js +6 -3
  29. package/lib/checks/validator.js +8 -0
  30. package/lib/compiler/assert-consistency.js +14 -5
  31. package/lib/compiler/base.js +64 -0
  32. package/lib/compiler/builtins.js +62 -16
  33. package/lib/compiler/checks.js +34 -10
  34. package/lib/compiler/definer.js +91 -112
  35. package/lib/compiler/index.js +30 -30
  36. package/lib/compiler/propagator.js +8 -4
  37. package/lib/compiler/resolver.js +279 -63
  38. package/lib/compiler/shared.js +65 -230
  39. package/lib/compiler/utils.js +191 -0
  40. package/lib/edm/annotations/genericTranslation.js +35 -18
  41. package/lib/edm/annotations/preprocessAnnotations.js +1 -1
  42. package/lib/edm/csn2edm.js +4 -3
  43. package/lib/edm/edm.js +8 -8
  44. package/lib/edm/edmPreprocessor.js +61 -59
  45. package/lib/edm/edmUtils.js +14 -15
  46. package/lib/gen/Dictionary.json +82 -40
  47. package/lib/gen/language.checksum +1 -1
  48. package/lib/gen/language.interp +19 -1
  49. package/lib/gen/language.tokens +80 -73
  50. package/lib/gen/languageLexer.interp +27 -1
  51. package/lib/gen/languageLexer.js +925 -826
  52. package/lib/gen/languageLexer.tokens +72 -65
  53. package/lib/gen/languageParser.js +4817 -4102
  54. package/lib/json/from-csn.js +57 -26
  55. package/lib/json/to-csn.js +244 -51
  56. package/lib/language/antlrParser.js +12 -1
  57. package/lib/language/docCommentParser.js +1 -1
  58. package/lib/language/errorStrategy.js +26 -8
  59. package/lib/language/genericAntlrParser.js +106 -30
  60. package/lib/language/language.g4 +200 -70
  61. package/lib/language/multiLineStringParser.js +536 -0
  62. package/lib/main.d.ts +220 -21
  63. package/lib/main.js +6 -3
  64. package/lib/model/api.js +2 -2
  65. package/lib/model/csnRefs.js +218 -86
  66. package/lib/model/csnUtils.js +99 -178
  67. package/lib/model/enrichCsn.js +84 -43
  68. package/lib/model/revealInternalProperties.js +25 -8
  69. package/lib/model/sortViews.js +8 -1
  70. package/lib/modelCompare/compare.js +2 -1
  71. package/lib/optionProcessor.js +33 -18
  72. package/lib/render/.eslintrc.json +1 -2
  73. package/lib/render/DuplicateChecker.js +2 -2
  74. package/lib/render/manageConstraints.js +1 -1
  75. package/lib/render/toCdl.js +202 -82
  76. package/lib/render/toHdbcds.js +194 -135
  77. package/lib/render/toRename.js +7 -10
  78. package/lib/render/toSql.js +91 -51
  79. package/lib/render/utils/common.js +24 -5
  80. package/lib/render/utils/sql.js +6 -4
  81. package/lib/transform/braceExpression.js +4 -2
  82. package/lib/transform/db/applyTransformations.js +189 -0
  83. package/lib/transform/db/associations.js +389 -0
  84. package/lib/transform/db/cdsPersistence.js +150 -0
  85. package/lib/transform/db/constraints.js +275 -119
  86. package/lib/transform/db/draft.js +6 -4
  87. package/lib/transform/db/expansion.js +10 -9
  88. package/lib/transform/db/flattening.js +23 -8
  89. package/lib/transform/db/temporal.js +236 -0
  90. package/lib/transform/db/transformExists.js +106 -25
  91. package/lib/transform/db/views.js +485 -0
  92. package/lib/transform/forHanaNew.js +90 -1036
  93. package/lib/transform/forOdataNew.js +11 -3
  94. package/lib/transform/localized.js +5 -14
  95. package/lib/transform/odata/generateForeignKeyElements.js +2 -2
  96. package/lib/transform/transformUtilsNew.js +34 -20
  97. package/lib/transform/translateAssocsToJoins.js +15 -23
  98. package/lib/transform/universalCsnEnricher.js +217 -47
  99. package/lib/utils/file.js +13 -6
  100. package/lib/utils/term.js +65 -42
  101. package/lib/utils/timetrace.js +55 -27
  102. package/package.json +1 -1
  103. package/lib/transform/db/helpers.js +0 -58
@@ -239,15 +239,14 @@ function expandStructureReferences(csn, options, pathDelimiter, { error, info, t
239
239
  * @param {CSN.Artifact} root All elements visible fromt he query source ($combined)
240
240
  * @param {CSN.Column[]} columns
241
241
  * @param {string[]} excluding
242
- * @returns {{columns: Array, toManys: Array}} Object with rewritten columns (.expand/.inline) and with any .expand + to-many
242
+ * @returns {{columns: Array, toMany: Array}} Object with rewritten columns (.expand/.inline) and with any .expand + to-many
243
243
  */
244
244
  function rewrite(root, columns, excluding) {
245
245
  const allToMany = [];
246
246
  const newThing = [];
247
247
  // Replace stars - needs to happen here since the .expand/.inline first path step affects the root *
248
248
  columns = replaceStar(root, columns, excluding);
249
- for (let i = 0; i < columns.length; i++) {
250
- const col = columns[i];
249
+ for (const col of columns) {
251
250
  if (col.expand) {
252
251
  // TODO: Can col.ref be empty without an as? Assumption is it cannot - if it has, it's an error, we throw, compiler checks.
253
252
  const { expanded, toManys } = expandInline(root, col, col.ref || [], [ dbName(col) ]);
@@ -451,7 +450,7 @@ function expandStructureReferences(csn, options, pathDelimiter, { error, info, t
451
450
  else
452
451
  newThing.push(col);
453
452
  }
454
- else if (col.ref && col.$scope === '$magic' && col.ref[0] === '$user' && !col.as) {
453
+ else if (col.ref && col.$scope === '$magic' && ( col.ref[0] === '$user' || col.ref[0] === '$session' ) && !col.as) {
455
454
  col.as = implicitAs(col.ref);
456
455
  newThing.push(col);
457
456
  }
@@ -477,6 +476,7 @@ function expandStructureReferences(csn, options, pathDelimiter, { error, info, t
477
476
  */
478
477
  function expandRef(art, ref, alias, isKey, withAlias) {
479
478
  const expanded = [];
479
+ /** @type {Array<[CSN.Element, any[], any[]]>} */
480
480
  const stack = [ [ art, ref, [ alias || ref[ref.length - 1] ] ] ];
481
481
  while (stack.length > 0) {
482
482
  const [ current, currentRef, currentAlias ] = stack.pop();
@@ -552,9 +552,11 @@ function expandStructureReferences(csn, options, pathDelimiter, { error, info, t
552
552
  for (const part of Object.keys(base)) {
553
553
  if (excluding.indexOf(part) === -1) {
554
554
  // The thing is shadowed - ignore names present because of .inline, as those "disappear"
555
- if (names[part] !== undefined && !subs[names[part]].inline) {
556
- replaced[part] = true;
557
- star.push(subs[names[part]]);
555
+ if (names[part] !== undefined && !subs[names[part]].inline) { // Only works for a single * - but a second is forbidden anyway
556
+ if (names[part] > stars[0]) { // explicit definitions BEFORE the star should stay "infront" of the star
557
+ replaced[part] = true;
558
+ star.push(subs[names[part]]);
559
+ }
558
560
  }
559
561
  else { // the thing is not shadowed - use the name from the base
560
562
  star.push({ ref: [ part ] });
@@ -562,8 +564,7 @@ function expandStructureReferences(csn, options, pathDelimiter, { error, info, t
562
564
  }
563
565
  }
564
566
  // Finally: Replace the stars and leave out the shadowed things
565
- for (let i = 0; i < subs.length; i++) {
566
- const sub = subs[i];
567
+ for (const sub of subs) {
567
568
  if (sub !== '*' && !replaced[dbName(sub)])
568
569
  final.push(sub);
569
570
  else if (sub === '*')
@@ -14,7 +14,7 @@ const { setProp } = require('../../base/model');
14
14
  * @param {CSN.Model} csn
15
15
  */
16
16
  function removeLeadingSelf(csn) {
17
- const magicVars = [ '$now' ];
17
+ const magicVars = [ '$now', '$self', '$projection', '$user', '$session', '$at' ];
18
18
  forEachDefinition(csn, (artifact, artifactName) => {
19
19
  if (artifact.kind === 'entity' || artifact.kind === 'view') {
20
20
  forAllElements(artifact, artifactName, (parent, elements) => {
@@ -79,6 +79,22 @@ function resolveTypeReferences(csn, options, resolved, pathDelimiter) {
79
79
  if (!isBuiltinType(type)) {
80
80
  const directLocalized = parent.localized || false;
81
81
  toFinalBaseType(parent, resolved);
82
+ // structured types might not have the child-types replaced.
83
+ // Drill down to ensure this.
84
+ if (parent.elements) {
85
+ const stack = [ parent.elements ];
86
+ while (stack.length > 0) {
87
+ const elements = stack.pop();
88
+ for (const e of Object.values(elements)) {
89
+ if (e.type && !isBuiltinType(e.type))
90
+ toFinalBaseType(e, resolved);
91
+
92
+ if (e.elements)
93
+ stack.push(e.elements);
94
+ }
95
+ }
96
+ }
97
+
82
98
  if (!directLocalized)
83
99
  removeLocalized(parent);
84
100
  }
@@ -233,10 +249,9 @@ function flattenElements(csn, options, pathDelimiter, error) {
233
249
 
234
250
  if (flatElement.type && isAssocOrComposition(flatElement.type) && flatElement.on) {
235
251
  // Make refs resolvable by fixing the first ref step
236
- for (let i = 0; i < flatElement.on.length; i++) {
237
- const onPart = flatElement.on[i];
252
+ for (const onPart of flatElement.on) {
238
253
  if (onPart.ref) {
239
- const firstRef = flatElement.on[i].ref[0];
254
+ const firstRef = onPart.ref[0];
240
255
 
241
256
  /*
242
257
  when element is defined in the current name resolution scope, like
@@ -253,7 +268,7 @@ function flattenElements(csn, options, pathDelimiter, error) {
253
268
  const possibleFlatName = prefix + pathDelimiter + firstRef;
254
269
 
255
270
  if (flatElems[possibleFlatName])
256
- flatElement.on[i].ref[0] = possibleFlatName;
271
+ onPart.ref[0] = possibleFlatName;
257
272
  }
258
273
  }
259
274
  }
@@ -271,7 +286,7 @@ function flattenElements(csn, options, pathDelimiter, error) {
271
286
  previous[name] = element;
272
287
  return previous;
273
288
  }, Object.create(null));
274
- });
289
+ }, true);
275
290
  }
276
291
 
277
292
  /**
@@ -289,11 +304,11 @@ function flattenElements(csn, options, pathDelimiter, error) {
289
304
  /**
290
305
  * Walk the element chain
291
306
  *
292
- * @param {CSN.Element} e
307
+ * @param {object} e
293
308
  * @param {string} name
294
309
  */
295
310
  function walkElements(e, name) {
296
- if (isBuiltinType(e)) {
311
+ if (isBuiltinType(e.type)) {
297
312
  branches[subbranchNames.concat(name).join(pathDelimiter)] = subbranchElements.concat(e);
298
313
  }
299
314
  else {
@@ -0,0 +1,236 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ getUtils, getNormalizedQuery, hasAnnotationValue, forEachMember,
5
+ } = require('../../model/csnUtils');
6
+ const { implicitAs } = require('../../model/csnRefs');
7
+ const { setProp } = require('../../base/model');
8
+ const { getTransformers } = require('../transformUtilsNew');
9
+
10
+ const validToString = '@cds.valid.to';
11
+ const validFromString = '@cds.valid.from';
12
+ /**
13
+ * Get the forEachDefinition callback function that adds a where condition to views that
14
+ * - are annotated with @cds.valid.from and @cds.valid.to,
15
+ * - have only one @cds.valid.from and @cds.valid.to,
16
+ * - and both annotations come from the same entity
17
+ *
18
+ * If the view has one of the annotations but the other conditions are not met, an error will be raised.
19
+ *
20
+ * @param {CSN.Model} csn
21
+ * @param {object} messageFunctions
22
+ * @param {Function} messageFunctions.info
23
+ * @returns {(artifact: CSN.Artifact, artifactName: string) => void} Callback for forEachDefinition applying the where-condition to views.
24
+ */
25
+ function getViewDecorator(csn, messageFunctions) {
26
+ const { info } = messageFunctions;
27
+ const { get$combined } = getUtils(csn);
28
+ return addTemporalWhereConditionToView;
29
+ /**
30
+ * Add a where condition to views that
31
+ * - are annotated with @cds.valid.from and @cds.valid.to,
32
+ * - have only one @cds.valid.from and @cds.valid.to,
33
+ * - and both annotations come from the same entity
34
+ *
35
+ * If the view has one of the annotations but the other conditions are not met, an error will be raised.
36
+ *
37
+ * @param {CSN.Artifact} artifact
38
+ * @param {string} artifactName
39
+ */
40
+ function addTemporalWhereConditionToView(artifact, artifactName) {
41
+ const normalizedQuery = getNormalizedQuery(artifact);
42
+ if (normalizedQuery && normalizedQuery.query && normalizedQuery.query.SELECT) {
43
+ // BLOCKER: We need information to handle $combined
44
+ // What we are trying to achieve by this:
45
+ // Forbid joining/selecting from two or more temporal entities
46
+ // Idea: Follow the query-tree and check each from
47
+ // Collect all source-entities and compute our own $combined
48
+ const $combined = get$combined(normalizedQuery.query);
49
+ const [ from, to ] = getFromToElements($combined);
50
+ // exactly one validFrom & validTo
51
+ if (from.length === 1 && to.length === 1) {
52
+ // and both are from the same origin
53
+ if (from[0].source === to[0].source && from[0].parent === to[0].parent) {
54
+ if (!hasFalsyTemporalAnnotations(normalizedQuery.query.SELECT, artifact.elements, from[0], to[0])) {
55
+ const fromPath = {
56
+ ref: [
57
+ from[0].parent,
58
+ from[0].name,
59
+ ],
60
+ };
61
+
62
+ const toPath = {
63
+ ref: [
64
+ to[0].parent,
65
+ to[0].name,
66
+ ],
67
+ };
68
+
69
+
70
+ const atFrom = { ref: [ '$at', 'from' ] };
71
+ const atTo = { ref: [ '$at', 'to' ] };
72
+
73
+ const cond = [ '(', fromPath, '<', atTo, 'and', toPath, '>', atFrom, ')' ];
74
+
75
+ if (normalizedQuery.query.SELECT.where) { // if there is an existing where-clause, extend it by adding 'and (temporal clause)'
76
+ normalizedQuery.query.SELECT.where = [ '(', ...normalizedQuery.query.SELECT.where, ')', 'and', ...cond ];
77
+ }
78
+ else {
79
+ normalizedQuery.query.SELECT.where = cond;
80
+ }
81
+ }
82
+ }
83
+ else {
84
+ info(null, [ 'definitions', artifactName ], `No temporal WHERE clause added as "${from[0].error_parent}"."${from[0].name}" and "${to[0].error_parent}"."${to[0].name}" are not of same origin`);
85
+ }
86
+ }
87
+ else if (from.length > 0 || to.length > 0) {
88
+ const missingAnnotation = from.length > to.length ? validToString : validFromString;
89
+ info(null, [ 'definitions', artifactName ],
90
+ { anno: missingAnnotation },
91
+ 'No temporal WHERE clause added because $(ANNO) is missing');
92
+ }
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Get all elements tagged with @cds.valid.from/to from the union of all entities of the from-clause.
98
+ *
99
+ * @param {any} combined union of all entities of the from-clause
100
+ * @returns {Array[]} Array where first field is array of elements with @cds.valid.from, second field is array of elements with @cds.valid.to.
101
+ */
102
+ function getFromToElements(combined) {
103
+ const from = [];
104
+ const to = [];
105
+ for (const name in combined) {
106
+ let elt = combined[name];
107
+ if (!Array.isArray(elt))
108
+ elt = [ elt ];
109
+ elt.forEach((e) => {
110
+ if (hasAnnotationValue(e.element, validFromString))
111
+ from.push(e);
112
+
113
+ if (hasAnnotationValue(e.element, validToString))
114
+ to.push(e);
115
+ });
116
+ }
117
+
118
+ return [ from, to ];
119
+ }
120
+
121
+ /**
122
+ * Check if the given SELECT has a falsy @cds.valid.from and a falsy @cds.valid.to
123
+ *
124
+ * @param {CSN.QuerySelect} SELECT
125
+ * @param {CSN.Elements} elements
126
+ * @param {object} from
127
+ * @param {object} to
128
+ * @returns {boolean} True if both are present and false.
129
+ */
130
+ function hasFalsyTemporalAnnotations(SELECT, elements, from, to) {
131
+ let fromElement = elements[from.name];
132
+ let toElement = elements[to.name];
133
+
134
+ if (SELECT.columns) {
135
+ for (const col of SELECT.columns) {
136
+ if (col.ref) {
137
+ const implicitAlias = implicitAs(col.ref);
138
+ if (implicitAlias === from.name)
139
+ fromElement = elements[col.as || implicitAlias];
140
+ else if (implicitAlias === to.name)
141
+ toElement = elements[col.as || implicitAlias];
142
+ }
143
+ }
144
+ }
145
+ return fromElement && toElement &&
146
+ hasAnnotationValue(fromElement, validFromString, false) &&
147
+ hasAnnotationValue(toElement, validToString, false);
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Get the forEachDefinition callback function that collects all usages of @cds.valid.from/to/key and checks that
153
+ * - the assignment is on a valid element
154
+ * - the annotation is only assigned once
155
+ * - key is only used in conjunction with from and to
156
+ *
157
+ * Furthermore, @cds.valid.from and @cds.valid.key is processed - @cds.valid.from is marked as key or marked as unique if @cds.valid.key is used.
158
+ * If @cds.valid.key is used, the real key-elements have their key-property removed (set non-enumerable as $key) and instead the @cds.valid.key-marked elements have it added.
159
+ *
160
+ * @param {CSN.Model} csn
161
+ * @param {CSN.Options} options
162
+ * @param {string} pathDelimiter
163
+ * @param {object} messageFunctions
164
+ * @param {Function} messageFunctions.error
165
+ * @returns {(artifact: CSN.Artifact, artifactName: string) => void} Callback for forEachDefinition processing the annotations.
166
+ */
167
+ function getAnnotationHandler(csn, options, pathDelimiter, messageFunctions) {
168
+ const { error } = messageFunctions;
169
+ const {
170
+ extractValidFromToKeyElement, checkAssignment, checkMultipleAssignments, recurseElements,
171
+ } = getTransformers(csn, options, pathDelimiter);
172
+
173
+ return handleTemporalAnnotations;
174
+ /**
175
+ * @param {CSN.Artifact} artifact
176
+ * @param {string} artifactName
177
+ */
178
+ function handleTemporalAnnotations(artifact, artifactName) {
179
+ const validFrom = [];
180
+ const validTo = [];
181
+ const validKey = [];
182
+
183
+ recurseElements(artifact, [ 'definitions', artifactName ], (member, path) => {
184
+ const [ f, t, k ] = extractValidFromToKeyElement(member, path);
185
+ validFrom.push(...f);
186
+ validTo.push(...t);
187
+ validKey.push(...k);
188
+ });
189
+
190
+ if (artifact.kind === 'entity' && !artifact.query) {
191
+ validFrom.forEach(obj => checkAssignment(validFromString, obj.element, obj.path, artifact));
192
+ validTo.forEach(obj => checkAssignment(validToString, obj.element, obj.path, artifact));
193
+ validKey.forEach(obj => checkAssignment('@cds.valid.key', obj.element, obj.path, artifact));
194
+ checkMultipleAssignments(validFrom, validFromString, artifact, artifactName);
195
+ checkMultipleAssignments(validTo, validToString, artifact, artifactName, true);
196
+ checkMultipleAssignments(validKey, '@cds.valid.key', artifact, artifactName);
197
+ }
198
+
199
+ // if there is an cds.valid.key, make this the only primary key
200
+ // otherwise add all cds.valid.from to primary key tuple
201
+ if (validKey.length) {
202
+ if (!validFrom.length || !validTo.length) {
203
+ error(null, [ 'definitions', artifactName ],
204
+ 'Expecting “@cds.valid.from” and “@cds.valid.to” if “@cds.valid.key” is used');
205
+ }
206
+
207
+ forEachMember(artifact, (member) => {
208
+ if (member.key) {
209
+ member.unique = true;
210
+ delete member.key;
211
+ // Remember that this element was a key in the original artifact.
212
+ // This is needed for localized convenience view generation.
213
+ setProp(member, '$key', true);
214
+ }
215
+ });
216
+ validKey.forEach((member) => {
217
+ member.element.key = true;
218
+ });
219
+
220
+ validFrom.forEach((member) => {
221
+ member.element.unique = true;
222
+ });
223
+ }
224
+ else {
225
+ validFrom.forEach((member) => {
226
+ member.element.key = true;
227
+ });
228
+ }
229
+ }
230
+ }
231
+
232
+
233
+ module.exports = {
234
+ getViewDecorator,
235
+ getAnnotationHandler,
236
+ };
@@ -49,10 +49,14 @@ const { csnRefs } = require('../../model/csnRefs');
49
49
  */
50
50
  function handleExists(csn, options, error) {
51
51
  const { inspectRef } = csnRefs(csn);
52
+ const generatedExists = new WeakMap();
52
53
  forEachDefinition(csn, (artifact, artifactName) => {
54
+ if (artifact.projection) // do the same hack we do for the other stuff...
55
+ artifact.query = { SELECT: artifact.projection };
56
+
53
57
  if (artifact.query) {
54
58
  forAllQueries(artifact.query, (query, path) => {
55
- if (!query.$generatedExists) {
59
+ if (!generatedExists.has(query)) {
56
60
  const toProcess = []; // Collect all expressions we need to process here
57
61
  if (query.SELECT && query.SELECT.where && query.SELECT.where.length > 1)
58
62
  toProcess.push([ path.slice(0, -1), path.concat('where') ]);
@@ -66,6 +70,7 @@ function handleExists(csn, options, error) {
66
70
  toProcess.push([ path.slice(0, -1), path.concat([ 'from', 'on' ]) ]);
67
71
 
68
72
  for (const [ , exprPath ] of toProcess) {
73
+ forbidAssocInExists(exprPath);
69
74
  const expr = nestExists(exprPath);
70
75
  walkCsnPath(csn, exprPath.slice(0, -1))[exprPath[exprPath.length - 1]] = expr;
71
76
  }
@@ -81,6 +86,12 @@ function handleExists(csn, options, error) {
81
86
  }
82
87
  }, [ 'definitions', artifactName, 'query' ]);
83
88
  }
89
+
90
+ if (artifact.projection) { // undo our hack
91
+ artifact.projection = artifact.query.SELECT;
92
+
93
+ delete artifact.query;
94
+ }
84
95
  });
85
96
 
86
97
  /**
@@ -233,6 +244,63 @@ function handleExists(csn, options, error) {
233
244
  return startAssoc;
234
245
  }
235
246
 
247
+ /**
248
+ * Check that associations in filters (in an exists expression) are only fk-accesses. Everything else is forbidden.
249
+ *
250
+ * @param {CSN.Path} exprPath
251
+ * @returns {void}
252
+ */
253
+ function forbidAssocInExists(exprPath) {
254
+ const expr = walkCsnPath(csn, exprPath);
255
+ for (let i = 0; i < expr.length; i++) {
256
+ if (i < expr.length - 1 && expr[i] === 'exists' && expr[i + 1].ref) {
257
+ i++;
258
+ const current = expr[i];
259
+
260
+ const { links } = inspectRef(exprPath.concat(i));
261
+
262
+ const assocs = links.filter(link => link.art && link.art.target).map(link => current.ref[link.idx]);
263
+
264
+ checkForInvalidAssoc(assocs);
265
+ }
266
+ }
267
+ }
268
+
269
+ /**
270
+ * @param {object[]} assocs Array of refs of assocs - possibly with a .where to check
271
+ */
272
+ function checkForInvalidAssoc(assocs) {
273
+ for (const assoc of assocs) {
274
+ if (assoc.where) {
275
+ for (let i = 0; i < assoc.where.length; i++) {
276
+ const part = assoc.where[i];
277
+
278
+ if (part._links && !(assoc.where[i - 1] && assoc.where[i - 1] === 'exists')) {
279
+ for (const link of part._links) {
280
+ if (link.art && link.art.target) {
281
+ if (link.art.keys) { // managed - allow FK access
282
+ if (part._links[link.idx + 1] !== undefined) { // there is a next path step - check if it is a fk
283
+ if (!(part._links[link.idx + 1] && link.art.keys.some(fk => fk._art === part._links[link.idx + 1].art)))
284
+ 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)');
285
+ }
286
+ else { // no traversal, ends on managed
287
+ 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)');
288
+ }
289
+ }
290
+ else { // unmanaged - always wrong
291
+ 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)');
292
+ }
293
+ // Recursively drill down if the assoc-step has a filter
294
+ if (part.ref[link.idx].where)
295
+ checkForInvalidAssoc([ part.ref[link.idx] ]);
296
+ }
297
+ }
298
+ }
299
+ }
300
+ }
301
+ }
302
+ }
303
+
236
304
  /**
237
305
  * Walk to the expr using the given path and scan it for the "exists" + "ref" pattern.
238
306
  * If such a pattern is found, nest association steps therein into filters.
@@ -297,16 +365,23 @@ function handleExists(csn, options, error) {
297
365
  const subselect = getSubselect(root.target, ref, sources);
298
366
 
299
367
  const target = subselect.SELECT.from.as; // use subquery alias as target - prevent shadowing
300
- if (root.keys) { // managed assoc
301
- translateManagedAssocToWhere(root, target, subselect, isPrefixedWithTableAlias, base, current);
302
- }
303
- else { // unmanaged assoc
304
- translateUnmanagedAssocToWhere(root, target, subselect, isPrefixedWithTableAlias, base, current);
305
- }
368
+ const extension = root.keys ? translateManagedAssocToWhere(root, target, isPrefixedWithTableAlias, base, current) : translateUnmanagedAssocToWhere(root, target, isPrefixedWithTableAlias, base, current);
369
+ if (extension.length > 3)
370
+ subselect.SELECT.where.push('('); // add braces around the on-condition part to ensure precedence is kept
371
+
372
+ subselect.SELECT.where.push(...extension);
373
+
374
+ if (extension.length > 3)
375
+ subselect.SELECT.where.push(')');
306
376
 
307
377
  newExpr.push('exists');
308
- if (ref && ref.where)
309
- subselect.SELECT.where.push(...[ 'and', ...remapExistingWhere(target, ref.where) ]);
378
+ if (ref && ref.where) {
379
+ const remappedWhere = remapExistingWhere(target, ref.where);
380
+ if (remappedWhere.length > 3)
381
+ subselect.SELECT.where.push(...[ 'and', '(', ...remappedWhere, ')' ]);
382
+ else
383
+ subselect.SELECT.where.push(...[ 'and', ...remappedWhere ]);
384
+ }
310
385
 
311
386
  newExpr.push(subselect);
312
387
  toContinue.push([ exprPath.concat(newExpr.length - 1), exprPath.concat([ newExpr.length - 1, 'SELECT', 'where' ]) ]);
@@ -344,21 +419,24 @@ function handleExists(csn, options, error) {
344
419
  *
345
420
  * @param {CSN.Element} root
346
421
  * @param {string} target
347
- * @param {CSN.Query} subselect This subselect will in the end replace <assoc> in EXISTS <assoc>
348
422
  * @param {boolean} isPrefixedWithTableAlias
349
423
  * @param {string} base
350
424
  * @param {Token} current
425
+ * @returns {object[]} The stuff to add to the where
351
426
  */
352
- function translateManagedAssocToWhere(root, target, subselect, isPrefixedWithTableAlias, base, current) {
427
+ function translateManagedAssocToWhere(root, target, isPrefixedWithTableAlias, base, current) {
428
+ const whereExtension = [];
353
429
  for (let j = 0; j < root.keys.length; j++) {
354
430
  const lop = { ref: [ target, ...root.keys[j].ref ] }; // target side
355
431
  const rop = { ref: (isPrefixedWithTableAlias ? [] : [ base ]).concat([ ...toRawRef(current.ref), ...root.keys[j].ref ]) }; // source side
356
432
 
357
433
  if (j > 0)
358
- subselect.SELECT.where.push('and');
434
+ whereExtension.push('and');
359
435
 
360
- subselect.SELECT.where.push(...[ lop, '=', rop ]);
436
+ whereExtension.push(...[ lop, '=', rop ]);
361
437
  }
438
+
439
+ return whereExtension;
362
440
  }
363
441
 
364
442
  /**
@@ -384,12 +462,13 @@ function handleExists(csn, options, error) {
384
462
  *
385
463
  * @param {CSN.Element} root
386
464
  * @param {string} target
387
- * @param {CSN.Query} subselect This subselect will in the end replace <assoc> in EXISTS <assoc>
388
465
  * @param {boolean} isPrefixedWithTableAlias
389
466
  * @param {string} base
390
467
  * @param {Token} current
468
+ * @returns {object[]} The stuff to add to the where
391
469
  */
392
- function translateUnmanagedAssocToWhere(root, target, subselect, isPrefixedWithTableAlias, base, current) {
470
+ function translateUnmanagedAssocToWhere(root, target, isPrefixedWithTableAlias, base, current) {
471
+ const whereExtension = [];
393
472
  for (let j = 0; j < root.on.length; j++) {
394
473
  const part = root.on[j];
395
474
 
@@ -397,7 +476,7 @@ function handleExists(csn, options, error) {
397
476
  // but also keep along stuff like null and undefined, so compiler
398
477
  // can have a chance to complain/ we can fail later nicely maybe
399
478
  if (!(part && part.ref)) {
400
- subselect.SELECT.where.push(part);
479
+ whereExtension.push(part);
401
480
  continue;
402
481
  }
403
482
 
@@ -407,30 +486,32 @@ function handleExists(csn, options, error) {
407
486
  // Dollar Self Backlink
408
487
  if (isValidDollarSelf(root.on[j], root.$path.concat([ 'on', j ]), root.on[j + 1], root.on[j + 2], root.$path.concat([ 'on', j + 2 ]))) {
409
488
  if (root.on[j].ref[0] === '$self' && root.on[j].ref.length === 1)
410
- subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j + 2], root.$path.concat([ 'on', j + 2 ])));
489
+ whereExtension.push(...translateDollarSelfToWhere(base, target, root.on[j + 2], root.$path.concat([ 'on', j + 2 ])));
411
490
  else
412
- subselect.SELECT.where.push(...translateDollarSelfToWhere(base, target, root.on[j], root.$path.concat([ 'on', j ])));
491
+ whereExtension.push(...translateDollarSelfToWhere(base, target, root.on[j], root.$path.concat([ 'on', j ])));
413
492
 
414
493
  j += 2;
415
494
  }
416
495
  else if (links && links[0].art === root) { // target side
417
- subselect.SELECT.where.push({ ref: [ target, ...part.ref.slice(1) ] });
496
+ whereExtension.push({ ref: [ target, ...part.ref.slice(1) ] });
418
497
  }
419
498
  else if (part.$scope === '$self') { // source side - "absolute" scope
420
499
  // cut off the $self, as we prefix the entity name now
421
- subselect.SELECT.where.push({ ref: [ base, ...part.ref.slice(1) ] });
500
+ whereExtension.push({ ref: [ base, ...part.ref.slice(1) ] });
422
501
  }
423
502
  else if (art) { // source side - with local scope
424
503
  if (isPrefixedWithTableAlias)
425
- subselect.SELECT.where.push({ ref: [ ...current.ref.slice(0, -1), ...part.ref ] });
504
+ whereExtension.push({ ref: [ ...current.ref.slice(0, -1), ...part.ref ] });
426
505
  else
427
- subselect.SELECT.where.push({ ref: [ base, ...current.ref.slice(0, -1), ...part.ref ] });
506
+ whereExtension.push({ ref: [ base, ...current.ref.slice(0, -1), ...part.ref ] });
428
507
  }
429
508
  else { // operator - or any other leftover
430
- subselect.SELECT.where.push(part);
509
+ whereExtension.push(part);
431
510
  }
432
511
  }
433
512
 
513
+ return whereExtension;
514
+
434
515
  /**
435
516
  * Check that an expression triple is a valid $self
436
517
  *
@@ -581,7 +662,7 @@ function handleExists(csn, options, error) {
581
662
  };
582
663
  // Because the generated things don't have _links, _art etc. set
583
664
  // We could also make getParent more robust to calculate the links JIT if they are missing
584
- setProp(subselect, '$generatedExists', true);
665
+ generatedExists.set(subselect, true);
585
666
 
586
667
  const nonEnumElements = Object.create(null);
587
668
  nonEnumElements.dummy = {
@@ -638,7 +719,7 @@ function handleExists(csn, options, error) {
638
719
  *
639
720
  * @param {string} base The source entity/query source name
640
721
  * @param {string} target The target entity/query source name
641
- * @param {CSN.Element} assoc The association element - the "not-$self" side of the comparison
722
+ * @param {object} assoc The association element - the "not-$self" side of the comparison
642
723
  * @param {CSN.Path} path
643
724
  * @returns {TokenStream} The WHERE representing the $self comparison
644
725
  */