@sap/cds-compiler 3.6.2 → 3.7.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 (68) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +3 -0
  3. package/bin/cdsc.js +9 -5
  4. package/doc/CHANGELOG_BETA.md +20 -2
  5. package/doc/CHANGELOG_DEPRECATED.md +2 -2
  6. package/lib/api/main.js +2 -1
  7. package/lib/base/dictionaries.js +10 -0
  8. package/lib/base/message-registry.js +56 -12
  9. package/lib/base/messages.js +39 -20
  10. package/lib/base/model.js +1 -0
  11. package/lib/base/shuffle.js +2 -1
  12. package/lib/checks/elements.js +29 -1
  13. package/lib/checks/{emptyOrOnlyVirtual.js → hasPersistedElements.js} +9 -5
  14. package/lib/checks/nonexpandableStructured.js +1 -1
  15. package/lib/checks/onConditions.js +8 -5
  16. package/lib/checks/types.js +6 -1
  17. package/lib/checks/validator.js +7 -3
  18. package/lib/compiler/assert-consistency.js +20 -23
  19. package/lib/compiler/base.js +1 -2
  20. package/lib/compiler/builtins.js +2 -2
  21. package/lib/compiler/checks.js +237 -242
  22. package/lib/compiler/define.js +63 -75
  23. package/lib/compiler/extend.js +325 -22
  24. package/lib/compiler/finalize-parse-cdl.js +1 -55
  25. package/lib/compiler/kick-start.js +6 -7
  26. package/lib/compiler/populate.js +284 -288
  27. package/lib/compiler/propagator.js +15 -13
  28. package/lib/compiler/resolve.js +136 -306
  29. package/lib/compiler/shared.js +42 -44
  30. package/lib/compiler/tweak-assocs.js +29 -27
  31. package/lib/compiler/utils.js +29 -3
  32. package/lib/edm/annotations/genericTranslation.js +7 -13
  33. package/lib/edm/annotations/preprocessAnnotations.js +3 -0
  34. package/lib/edm/csn2edm.js +0 -4
  35. package/lib/edm/edm.js +6 -4
  36. package/lib/edm/edmAnnoPreprocessor.js +1 -0
  37. package/lib/edm/edmPreprocessor.js +1 -5
  38. package/lib/gen/Dictionary.json +34 -2
  39. package/lib/gen/language.checksum +1 -1
  40. package/lib/gen/language.interp +1 -1
  41. package/lib/gen/languageParser.js +2429 -2401
  42. package/lib/inspect/inspectPropagation.js +2 -0
  43. package/lib/json/from-csn.js +87 -41
  44. package/lib/json/to-csn.js +47 -16
  45. package/lib/language/errorStrategy.js +1 -0
  46. package/lib/language/genericAntlrParser.js +109 -28
  47. package/lib/language/language.g4 +20 -4
  48. package/lib/model/csnRefs.js +1 -1
  49. package/lib/model/csnUtils.js +1 -0
  50. package/lib/model/revealInternalProperties.js +1 -2
  51. package/lib/modelCompare/compare.js +2 -1
  52. package/lib/render/manageConstraints.js +5 -2
  53. package/lib/render/toCdl.js +20 -7
  54. package/lib/render/toHdbcds.js +2 -8
  55. package/lib/render/toSql.js +4 -3
  56. package/lib/render/utils/common.js +9 -5
  57. package/lib/transform/db/assertUnique.js +2 -1
  58. package/lib/transform/db/expansion.js +2 -0
  59. package/lib/transform/db/flattening.js +37 -36
  60. package/lib/transform/db/rewriteCalculatedElements.js +559 -0
  61. package/lib/transform/db/transformExists.js +4 -0
  62. package/lib/transform/db/views.js +40 -37
  63. package/lib/transform/forRelationalDB.js +38 -28
  64. package/lib/transform/odata/typesExposure.js +50 -15
  65. package/lib/transform/parseExpr.js +14 -8
  66. package/lib/transform/transformUtilsNew.js +6 -5
  67. package/lib/transform/translateAssocsToJoins.js +49 -33
  68. package/package.json +1 -1
@@ -18,6 +18,7 @@ const { timetrace } = require('../utils/timetrace');
18
18
  const { createReferentialConstraints, assertConstraintIdentifierUniqueness } = require('./db/constraints');
19
19
  const { createDict, forEach } = require('../utils/objectUtils');
20
20
  const handleExists = require('./db/transformExists');
21
+ const { rewriteCalculatedElementsInViews, processCalculatedElementsInEntities } = require('./db/rewriteCalculatedElements');
21
22
  const replaceAssociationsInGroupByOrderBy = require('./db/groupByOrderBy');
22
23
  const _forEachDefinition = require('../model/csnUtils').forEachDefinition;
23
24
  const flattening = require('./db/flattening');
@@ -178,14 +179,15 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
178
179
  const transformCsn = transformUtils.transformModel;
179
180
 
180
181
 
181
- timetrace.start('temporal');
182
- // (001) Add a temporal where condition to views where applicable before assoc2join
183
- // assoc2join eventually rewrites the table aliases
184
- forEachDefinition(csn, temporal.getViewDecorator(csn, {info}, csnUtils));
185
- timetrace.stop('temporal');
182
+ forEachDefinition(csn, [
183
+ // (001) Add a temporal where condition to views where applicable before assoc2join
184
+ // assoc2join eventually rewrites the table aliases
185
+ temporal.getViewDecorator(csn, {info}, csnUtils),
186
+ // check unique constraints - further processing is done in rewriteUniqueConstraints
187
+ assertUnique.prepare(csn, options, error, info)
188
+ ]);
186
189
 
187
- // check unique constraints - further processing is done in rewriteUniqueConstraints
188
- assertUnique.prepare(csn, options, error, info);
190
+ rewriteCalculatedElementsInViews(csn, options, pathDelimiter, error);
189
191
 
190
192
  if(doA2J) {
191
193
  // Expand a structured thing in: keys, columns, order by, group by
@@ -199,6 +201,11 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
199
201
  bindCsnReferenceOnly();
200
202
 
201
203
 
204
+ // TODO: Instead of 3 separate applyTransformations, we could have each of them just return the "listeners", merge them into
205
+ // one big listener that then gets passed into one single applyTransformations. Each listener would then have to return an array of callbacks to call.
206
+ // With that, we could still ensure the processing order (assuming we don't run into problems with scoping).
207
+ // To analyze: Increased memory vs. saved cycles
208
+ // Looked at it with AFC: This is only a small part of the overall processing time, enrich step of validator is just as expensive
202
209
  if(doA2J) {
203
210
  const resolved = new WeakMap();
204
211
  // No refs with struct-steps exist anymore
@@ -240,6 +247,8 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
240
247
  }
241
248
  });
242
249
 
250
+ processCalculatedElementsInEntities(csn, options);
251
+
243
252
  timetrace.start('Transform CSN')
244
253
 
245
254
  // (000) Rename primitive types, make UUID a String
@@ -255,15 +264,15 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
255
264
  },
256
265
  }, true);
257
266
 
258
- // (040) Ignore entities and views that are abstract or implemented
259
- // or carry the annotation cds.persistence.skip/exists
260
- // These entities are not removed from the csn, but flagged as "to be ignored"
261
- forEachDefinition(csn, cdsPersistence.getAnnoProcessor());
262
-
263
-
264
- // (050) Check @cds.valid.from/to only on entity
265
- // Views are checked in (001), unbalanced valid.from/to's or mismatching origins
266
- forEachDefinition(csn, temporal.getAnnotationHandler(csn, options, pathDelimiter, {error}));
267
+ forEachDefinition(csn, [
268
+ // (040) Ignore entities and views that are abstract or implemented
269
+ // or carry the annotation cds.persistence.skip/exists
270
+ // These entities are not removed from the csn, but flagged as "to be ignored"
271
+ cdsPersistence.getAnnoProcessor(),
272
+ // (050) Check @cds.valid.from/to only on entity
273
+ // Views are checked in (001), unbalanced valid.from/to's or mismatching origins
274
+ temporal.getAnnotationHandler(csn, options, pathDelimiter, {error})
275
+ ]);
267
276
 
268
277
  // eliminate the doA2J in the functions 'handleManagedAssociationFKs' and 'createForeignKeyElements'
269
278
  doA2J && flattening.handleManagedAssociationsAndCreateForeignKeys(csn, options, error, pathDelimiter, true, csnUtils, { skipDict: { actions: true }, allowArtifact: artifact => (artifact.kind === 'entity') });
@@ -300,18 +309,19 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
300
309
  }
301
310
  });
302
311
 
303
- // For generating DB stuff:
304
- // - table-entity with parameters: not allowed
305
- // - view with parameters: ok on HANA, not allowed otherwise
306
- // (don't complain about action/function with parameters)
307
- forEachDefinition(csn, handleChecksForWithParameters);
308
-
309
- // Remove .masked
310
- // Check that keys are not explicitly nullable
311
- // Check that Associations are not used in entities/views with parameters
312
- // (150 b) Strip inheritance
313
- // Note that this should happen after implicit redirection, because includes are required for that
314
- forEachDefinition(csn, handleDBChecks);
312
+ forEachDefinition(csn, [
313
+ // For generating DB stuff:
314
+ // - table-entity with parameters: not allowed
315
+ // - view with parameters: ok on HANA, not allowed otherwise
316
+ // (don't complain about action/function with parameters)
317
+ handleChecksForWithParameters,
318
+ // Remove .masked
319
+ // Check that keys are not explicitly nullable
320
+ // Check that Associations are not used in entities/views with parameters
321
+ // (150 b) Strip inheritance
322
+ // Note that this should happen after implicit redirection, because includes are required for that
323
+ handleDBChecks,
324
+ ]);
315
325
 
316
326
  // (170) Transform '$self' in backlink associations to appropriate key comparisons
317
327
  // Must happen before draft processing because the artificial ON-conditions in generated
@@ -13,21 +13,66 @@ const { copyAnnotations, getNamespace } = require('../../model/csnUtils');
13
13
  const { isBetaEnabled } = require('../../base/model.js');
14
14
  const { CompilerAssertion } = require('../../base/error');
15
15
 
16
+ /**
17
+ * A given CDS model is a set of n definitions D = {v_1, ..., v_n } spanning a type dependency
18
+ * graph T(D) with vertices v_r, v_d (representing the referrer (using) and defining node of a type)
19
+ * and edges e(v_r, v_d).
20
+ *
21
+ * S may be a proper subset of D and is defined by v_s. Up to n S_i may exist.
22
+ * v is element of S_i, if name(v) starts with name(v_s). Therefore any v can only be member
23
+ * of exactly one S_i and v_s is always member of its own S_i.
24
+ *
25
+ * A complete service type dependency graph Tc(S) is defined as a set of vertices v_r, v_d
26
+ * and edges e(v_r, v_d) such that all v_r, v_d are elements of S (v_rs, v_ds) and with that
27
+ * all edges are { e(v_rs, v_ds) }.
28
+ *
29
+ * The input CSN may contain edges e(v_rs, v_dns) with v_r element of S_i (v_rs) and v_d not element
30
+ * of S_i (v_dns).
31
+ *
32
+ * The aim of this algorithm is to produce Tc's for all requested S_i by 'filling' up the missing
33
+ * vertices v_ds and rewriting all e(v_rs, v_dns) to e(v_rs, v_ds).
34
+ *
35
+ * This can be done pretty easily by (recursively) iterating over all requested v_rs and
36
+ * follow e(v_r, v_d) until a v_dns is found. v_dns is cloned and added to S via
37
+ * name(v_ds) = name(v_s) + '.' + name(v_dns). If v_dns is an anonymous definition, an
38
+ * artificial name representing the path to that node is being used.
39
+ *
40
+ * The algorithm has a beneficial side effect: it creates new (sub) schemas on the fly
41
+ * which are required for the construction of the EDM intermediate representation later on.
42
+ *
43
+ * An OData service contains at least one schema. Only (OData) schemas may contain definitions.
44
+ * By default, the CDS service v_s represents the default (OData) schema.
45
+ * However, there are situations where { v_dns } must be partitioned into (sub) schemas in order to
46
+ * maintain their original name prefixes and to be compatible with later service definitions.
47
+ *
48
+ * If name(v_dns) is made up of segments separated by a dot '.', the first n-1 segments represent
49
+ * the sub schema: name(schema) = name(v_s) + '.' + concat(1,n-1, segments(name(v_dns)), '.')
50
+ *
51
+ * If name(v_dns) has no prefix segments, the fallback schema name is prepended instead:
52
+ * name(v_ds) = name(v_s) + '.' + fallbackschema + name(v_dns);
53
+ *
54
+ * @param {CSN.Model} csn
55
+ * @param {function} whatsMyServiceName
56
+ * @param {string[]} requestedServiceNames
57
+ * @param {String} fallBackSchemaName
58
+ * @param {Object} options
59
+ * @param {Object} csnUtils
60
+ * @param {Object} message
61
+ * @returns {Object} schemas dictionary of (sub) schemas for all requested services
62
+ */
16
63
  function typesExposure(csn, whatsMyServiceName, requestedServiceNames, fallBackSchemaName, options, csnUtils, message) {
17
64
  const { error } = message;
18
65
  const special$self = !csn?.definitions?.$self && '$self';
19
66
  // are we working with OData proxies or cross-service refs
20
67
  const isMultiSchema = options.odataVersion === 'v4' && (options.odataProxies || options.odataXServiceRefs);
21
- // collect in this variable all the newly exposed types
68
+ // service sub schemas as return value
22
69
  const schemas = Object.create(null);
70
+ // exposed types register
23
71
  const exposedTypes = Object.create(null);
24
- // walk through the definitions of the given CSN and expose types where needed
25
72
  forEachDefinition(csn, (def, defName, propertyName, path) => {
26
- // we do expose types only for definition from inside services
27
73
  const serviceName = whatsMyServiceName(defName, false);
28
74
  // run type exposure only on requested services if not in multi schema mode
29
75
  // multi schema mode requires a proper type exposure for all services as a prerequisite
30
- // for the proxy exposure
31
76
  if (serviceName && requestedServiceNames.includes(serviceName)) {
32
77
  if (def.kind === 'type' || def.kind === 'entity') {
33
78
  forEachMember(def, (element, elementName, propertyName, path) => {
@@ -38,13 +83,9 @@ function typesExposure(csn, whatsMyServiceName, requestedServiceNames, fallBackS
38
83
  }, path);
39
84
  }
40
85
 
41
- // For exposed actions and functions that use non-exposed or anonymous structured types, create
42
- // artificial exposing types.
43
- // unbound actions
44
86
  if (def.kind === 'action' || def.kind === 'function') {
45
87
  exposeTypesOfAction(def, defName, defName, serviceName, path);
46
88
  }
47
- // bound actions
48
89
  def.actions && Object.entries(def.actions).forEach(([actionName, action]) => {
49
90
  exposeTypesOfAction(action, `${defName}_${actionName}`, defName, serviceName, path.concat(['actions', actionName]));
50
91
  });
@@ -53,19 +94,13 @@ function typesExposure(csn, whatsMyServiceName, requestedServiceNames, fallBackS
53
94
 
54
95
  if(isBetaEnabled(options, 'odataTerms')) {
55
96
  forEachGeneric(csn, 'vocabularies', (def, defName, _propertyName, path) => {
56
- // we do expose types only for definition from inside services
57
97
  const serviceName = whatsMyServiceName(defName, false);
58
- // run type exposure only on requested services if not in multi schema mode
59
- // multi schema mode requires a proper type exposure for all services as a prerequisite
60
- // for the proxy exposure
61
98
  if (serviceName && requestedServiceNames.includes(serviceName)) {
62
99
  if(csn.definitions[defName]) {
63
- // error, duplicate definitions not allowed!
64
- // TODO: Use path as error location as soon as refs outside definitions are supported
65
100
  error('odata-definition-exists', ['vocabularies', defName], { anno: defName, '#':'anno' });
66
101
  }
67
102
  else {
68
- // link def into definitions for later use
103
+ // link def into definitions for later use
69
104
  def.kind = 'annotation';
70
105
  csn.definitions[defName] = def;
71
106
  const artificialName = `term_${defName.replace(/\./g, '_')}`;//_${paramName}`;
@@ -19,7 +19,7 @@
19
19
  * Unary: 'is [not] null', 'not'
20
20
  * Conditional: 'case [when then]+ [else]? end', 'and', 'or'
21
21
  *
22
- * Not yet implemented: 'new'
22
+ * stand-alone token: 'new'
23
23
  *
24
24
  * This is not an optimized LL(1) parser but a token 'sniffer'. A stream is
25
25
  * cracked up in sub streams and passed down to the next higher function.
@@ -33,8 +33,8 @@
33
33
  * This parser intentionally does no error handling. If a clause is malformed, it is accepted as is.
34
34
  *
35
35
  * @param {any} xpr A JSON object.
36
- * @param {Object} state Objet
37
- * anno: Don't eliminate arrays with single entry in statetations as they are collections
36
+ * @param {Object} state Object
37
+ * anno: Don't eliminate arrays with single entry in statetations (TODO?) as they are collections
38
38
  * array: Bias AST representation.
39
39
  * nary: return n-ary or binary tree
40
40
  */
@@ -42,7 +42,7 @@
42
42
  function parseExpr(xpr, state = { anno: 0, array: true, nary: false }) {
43
43
  // Notes:
44
44
  // - Variables `s` and `e` are used as index variables into `xpr`s for start and end.
45
- // - xpr's are our CSN expressions, see <https://pages.github.tools.sap/cap/docs/cds/cxn>
45
+ // - xpr's are our CSN expressions, see <https://cap.cloud.sap/docs/cds/cxn>
46
46
 
47
47
  return parseExprInt(xpr, state);
48
48
 
@@ -82,7 +82,7 @@ function parseExpr(xpr, state = { anno: 0, array: true, nary: false }) {
82
82
  */
83
83
  function rewriteCaseBlock(casePos, endPos) {
84
84
  const caseTree = state.array ? [ 'case' ] : { 'case': [] };
85
-
85
+
86
86
  let elsePos = endPos;
87
87
  let whenPos = casePos;
88
88
 
@@ -144,7 +144,7 @@ function parseExpr(xpr, state = { anno: 0, array: true, nary: false }) {
144
144
  function conditionOR(xpr, s, e, state) {
145
145
  return binaryExpr(xpr, ['or'], conditionAnd, s, e, state);
146
146
  }
147
-
147
+
148
148
  function conditionAnd(xpr, s, e, state) {
149
149
  return binaryExpr(xpr, (xpr, s, e) => {
150
150
  let a = s-1;
@@ -283,7 +283,8 @@ function parseExpr(xpr, state = { anno: 0, array: true, nary: false }) {
283
283
  // return xpr;
284
284
  for(let n in xpr) {
285
285
  const x = xpr[n];
286
- if(n[0] === '@')
286
+ const isAnno = n[0] === '@' && isSimpleAnnoValue(x);
287
+ if(isAnno)
287
288
  state.anno++;
288
289
  if(Array.isArray(x)) {
289
290
  if(csnarray.includes(n) || state.anno !== 0)
@@ -295,7 +296,7 @@ function parseExpr(xpr, state = { anno: 0, array: true, nary: false }) {
295
296
  }
296
297
  else
297
298
  xpr[n] = parseExprInt(x, state);
298
- if(n[0] === '@')
299
+ if(isAnno)
299
300
  state.anno--;
300
301
  }
301
302
  }
@@ -343,6 +344,11 @@ function parseExpr(xpr, state = { anno: 0, array: true, nary: false }) {
343
344
 
344
345
  }
345
346
 
347
+ function isSimpleAnnoValue(val) {
348
+ // Expressions as annotation values always have a `=` and another property.
349
+ return !val?.['='] || Object.keys(val) < 2;
350
+ }
351
+
346
352
  module.exports = {
347
353
  parseExpr,
348
354
  };
@@ -321,9 +321,7 @@ function getTransformers(model, options, pathDelimiter = '_') {
321
321
  */
322
322
  function flattenStructStepsInRef(ref, path, links, scope, resolvedLinkTypes=new WeakMap()) {
323
323
  // Refs of length 1 cannot contain steps - no need to check
324
- if (ref.length < 2) {
325
- return ref;
326
- } else if(scope === '$self' && ref.length === 2) {
324
+ if (ref.length < 2 || (scope === '$self' && ref.length === 2)) {
327
325
  return ref;
328
326
  }
329
327
 
@@ -1136,7 +1134,7 @@ function getTransformers(model, options, pathDelimiter = '_') {
1136
1134
  const rhsIsVal = (rhs === 'null' /*|| rhs.val !== undefined*/);
1137
1135
 
1138
1136
  // lhs & rhs must be expandable types (structures or managed associations)
1139
- // if ever lhs should be alowed to be a value uncomment this
1137
+ // if ever lhs should be allowed to be a value uncomment this
1140
1138
  if(!(lhsIsVal /*&& rhsIsVal*/) &&
1141
1139
  !(isDollarSelfOrProjectionOperand(lhs) || isDollarSelfOrProjectionOperand(rhs)) &&
1142
1140
  RelationalOperators.includes(op) &&
@@ -1189,6 +1187,7 @@ function getTransformers(model, options, pathDelimiter = '_') {
1189
1187
  // lhs && rhs are present, consistency checks that affect both ends
1190
1188
  else {
1191
1189
  // is lhs scalar?
1190
+ // eslint-disable-next-line sonarjs/no-gratuitous-expressions
1192
1191
  if(!lhsIsVal && x.lhs && !isScalarOrNoType(x.lhs._art)) {
1193
1192
  error(null, location, { prefix, name: `${pathName(x.lhs.ref)}${(xn.length ? '.' + xn : '')}` },
1194
1193
  '$(PREFIX): Path $(NAME) must end on a scalar type')
@@ -1201,6 +1200,7 @@ function getTransformers(model, options, pathDelimiter = '_') {
1201
1200
  cont = false;
1202
1201
  }
1203
1202
  // info about type incompatibility if no other errors occurred
1203
+ // eslint-disable-next-line sonarjs/no-gratuitous-expressions
1204
1204
  if(!(lhsIsVal || rhsIsVal) && x.lhs && x.rhs && xn && cont) {
1205
1205
  const lhst = getType(x.lhs._art);
1206
1206
  const rhst = getType(x.rhs._art);
@@ -1215,6 +1215,7 @@ function getTransformers(model, options, pathDelimiter = '_') {
1215
1215
  return expr;
1216
1216
 
1217
1217
  // if lhs and rhs are refs set operator from 'like' to '='
1218
+ // eslint-disable-next-line sonarjs/no-gratuitous-expressions
1218
1219
  if(op === 'like' && !(lhsIsVal || rhsIsVal)) {
1219
1220
  op = '=';
1220
1221
  }
@@ -1419,7 +1420,7 @@ function rewriteBuiltinTypeRef(csn) {
1419
1420
  const special$self = !csn?.definitions?.$self && '$self';
1420
1421
  applyTransformations(csn, {
1421
1422
  type: (parent, _prop, type) => {
1422
- if(type.ref && (
1423
+ if(type?.ref && (
1423
1424
  isBuiltinType(type.ref[0]) ||
1424
1425
  type.ref[0] === special$self)
1425
1426
  ) {
@@ -17,6 +17,7 @@ function translateAssocsToJoinsCSN(csn, options){
17
17
  // Do not re-complain about localized
18
18
  const compileOptions = { ...options, $skipNameCheck: true };
19
19
  delete compileOptions.csnFlavor;
20
+ // console.log('CSN passed by A2J to compiler:',JSON.stringify(csn,null,2))
20
21
  const model = recompileX(csn, compileOptions);
21
22
  timetrace.stop('A2J: Recompiling model');
22
23
  timetrace.start('A2J: Translating associations to joins');
@@ -251,22 +252,24 @@ function translateAssocsToJoins(model, inputOptions = {})
251
252
  */
252
253
  function substituteDollarSelf(pathNode)
253
254
  {
254
- let [head, ...tail] = pathNode.path;
255
- if(['$projection', '$self'].includes(head.id) && tail.length) {
255
+ let pathValue = pathNode;
256
+ let [head, ...tail] = pathValue.path;
257
+ while(tail.length && head._navigation?.kind === '$self') {
256
258
  const self = head;
257
- if(self._navigation && self._navigation.kind === '$self') {
258
- [head, ...tail] = tail;
259
- if(head) {
260
- let pathValue = self._navigation._origin.elements[head.id].value;
261
- // core compiler has already caught $self.<assoc>.<postfix> and
262
- // non-path $self expressions with postfix path
263
- if(pathValue.path && tail.length) {
259
+ [head, ...tail] = tail;
260
+ if(head) {
261
+ pathValue = self._navigation._origin.elements[head.id].value;
262
+ // core compiler has already caught $self.<assoc>.<postfix> and
263
+ // non-path $self expressions with postfix path
264
+ if(pathValue.path) {
265
+ if(tail.length)
264
266
  pathValue = constructPathNode([...pathValue.path, ...tail], pathValue.alias, false);
265
- }
266
- replaceNodeContent(pathNode, pathValue);
267
+ [head, ...tail] = pathValue.path;
267
268
  }
268
269
  }
269
270
  }
271
+ if(head)
272
+ replaceNodeContent(pathNode, pathValue);
270
273
  }
271
274
 
272
275
  /*
@@ -636,7 +639,7 @@ function translateAssocsToJoins(model, inputOptions = {})
636
639
  }
637
640
 
638
641
  env.assocStack.push(assoc);
639
- let onCond = cloneOnCondition(assoc.on);
642
+ const onCond = cloneOnCondition(assoc.on);
640
643
  env.assocStack.pop();
641
644
  return onCond;
642
645
  }
@@ -655,8 +658,8 @@ function translateAssocsToJoins(model, inputOptions = {})
655
658
  }
656
659
 
657
660
  function cloneOnCondExprStream(expr) {
658
- let args = expr.args;
659
- let result = { op: { val: expr.op.val }, args: [ ] };
661
+ const args = expr.args;
662
+ const result = { op: { val: expr.op.val }, args: [ ] };
660
663
  for(let i = 0; i < args.length; i++)
661
664
  {
662
665
  if(args[i].op && args[i].op.val === 'xpr')
@@ -668,7 +671,7 @@ function translateAssocsToJoins(model, inputOptions = {})
668
671
  else if(i < args.length-2 && args[i].path &&
669
672
  args[i+1]?.literal === 'token' && args[i+1]?.val === '=' && args[i+2].path)
670
673
  {
671
- let fwdAssoc = getForwardAssociation(args[i].path, args[i+2].path);
674
+ const fwdAssoc = getForwardAssociation(args[i].path, args[i+2].path);
672
675
  if(fwdAssoc)
673
676
  {
674
677
  //env.assocStack.includes(fwdAssoc) => recursion
@@ -702,7 +705,7 @@ function translateAssocsToJoins(model, inputOptions = {})
702
705
 
703
706
  // If this is a backlink condition, produce the
704
707
  // ON cond of the forward assoc with swapped src/tgt aliases
705
- let fwdAssoc = getForwardAssociationExpr(expr);
708
+ const fwdAssoc = getForwardAssociationExpr(expr);
706
709
  if(fwdAssoc) {
707
710
  if(env.assocStack.length === 2) {
708
711
  // reuse (ugly) error message from forHana
@@ -729,25 +732,38 @@ function translateAssocsToJoins(model, inputOptions = {})
729
732
  }
730
733
 
731
734
  // The src/tgtAliases need to be swapped for ON Condition of the forward assoc.
732
- // The correct table alias is the QA of the original target. If this target
733
- // has been redirected, use the QA of the redirected target.
734
- // As last resort use the source alias information.
735
- // TODO Discuss: Huh, why do you need to care about redirections?
736
- // Probably only with to-be-rewritten ON conditions (should be error in v2).
735
+ // If the QAT assoc is a mixin and forward assoc was propagated, the original
736
+ // forward definition must have a target in the query source otherwise the ON cond
737
+ // is not resolvable (exception propagated mixins, as these are defined against the
738
+ // view signature and not a query source). If the target is not part of the query source,
739
+ // raise an error. Swap source and target otherwise.
737
740
  function swapTableAliasesForFwdAssoc(fwdAssoc, srcAlias, tgtAlias) {
738
- let newSrcAlias = tgtAlias;
741
+ const newSrcAlias = tgtAlias;
739
742
  let newTgtAlias = {};
740
- // first try to identify table alias for complex views or redirected associations
741
- if(fwdAssoc._redirected && fwdAssoc._redirected.length &&
742
- // redirected target must have a $QA
743
- fwdAssoc._redirected[fwdAssoc._redirected.length-1].$QA &&
744
- // $QA's artifact must either be same srcAlias artifact
745
- (fwdAssoc._redirected[fwdAssoc._redirected.length-1].$QA._artifact === srcAlias._artifact ||
746
- // OR original assoc is a mixin (then just use the $QA)
747
- assoc.kind === 'mixin')) {
748
- newTgtAlias.id = fwdAssoc._redirected[fwdAssoc._redirected.length-1].$QA.name.id;
749
- newTgtAlias._artifact = fwdAssoc._redirected[fwdAssoc._redirected.length-1]._effectiveType;
750
- newTgtAlias._navigation = fwdAssoc._redirected[fwdAssoc._redirected.length-1].$QA.path[0]._navigation;
743
+
744
+ let i = 0;
745
+ let fwdOrigin = fwdAssoc;
746
+ while(fwdOrigin._origin) {
747
+ fwdOrigin = fwdOrigin._origin;
748
+ i++;
749
+ }
750
+ // If fwdAssoc was propagated and the origin is not a mixin itself (which always
751
+ // points to the signature of the current view and ensures that the ON cond is
752
+ // resolvable) make sure that the original assoc target is contained in the local
753
+ // query source
754
+ if(assoc.kind === 'mixin' && i > 0 && fwdOrigin.kind !== 'mixin') {
755
+ const tas = Object.values(env.lead.$tableAliases);
756
+ const i = tas.findIndex(ta => ta._artifact === fwdOrigin.target._artifact);
757
+ if(i >= 0 && tas[i].$QA) {
758
+ newTgtAlias.id = tas[i].$QA.name.id;
759
+ newTgtAlias._artifact = tas[i]._effectiveType;
760
+ newTgtAlias._navigation = tas[i].$QA.path[0]._navigation;
761
+ }
762
+ else {
763
+ error(null, [ assocQAT._origin.location, assocQAT._origin ], { name: fwdOrigin.target._artifact.name.id, art: assoc.name.id },
764
+ 'Expected association target $(NAME) of association $(ART) to be a query source');
765
+ newTgtAlias = Object.assign(newTgtAlias, srcAlias);
766
+ }
751
767
  }
752
768
  else {
753
769
  newTgtAlias = Object.assign(newTgtAlias, srcAlias);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds-compiler",
3
- "version": "3.6.2",
3
+ "version": "3.7.2",
4
4
  "description": "CDS (Core Data Services) compiler and backends",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "author": "SAP SE (https://www.sap.com)",