@sap/cds-compiler 3.6.0 → 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 (70) hide show
  1. package/CHANGELOG.md +58 -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/api/options.js +3 -2
  8. package/lib/base/dictionaries.js +10 -0
  9. package/lib/base/message-registry.js +56 -12
  10. package/lib/base/messages.js +39 -20
  11. package/lib/base/model.js +1 -0
  12. package/lib/base/shuffle.js +2 -1
  13. package/lib/checks/elements.js +29 -1
  14. package/lib/checks/{emptyOrOnlyVirtual.js → hasPersistedElements.js} +9 -5
  15. package/lib/checks/nonexpandableStructured.js +1 -1
  16. package/lib/checks/onConditions.js +8 -5
  17. package/lib/checks/types.js +6 -1
  18. package/lib/checks/validator.js +7 -3
  19. package/lib/compiler/assert-consistency.js +20 -23
  20. package/lib/compiler/base.js +1 -2
  21. package/lib/compiler/builtins.js +2 -2
  22. package/lib/compiler/checks.js +237 -242
  23. package/lib/compiler/define.js +63 -75
  24. package/lib/compiler/extend.js +325 -22
  25. package/lib/compiler/finalize-parse-cdl.js +1 -55
  26. package/lib/compiler/kick-start.js +6 -7
  27. package/lib/compiler/populate.js +284 -288
  28. package/lib/compiler/propagator.js +15 -13
  29. package/lib/compiler/resolve.js +136 -306
  30. package/lib/compiler/shared.js +42 -44
  31. package/lib/compiler/tweak-assocs.js +29 -27
  32. package/lib/compiler/utils.js +29 -3
  33. package/lib/edm/annotations/genericTranslation.js +7 -13
  34. package/lib/edm/annotations/preprocessAnnotations.js +3 -0
  35. package/lib/edm/csn2edm.js +0 -4
  36. package/lib/edm/edm.js +6 -4
  37. package/lib/edm/edmAnnoPreprocessor.js +1 -0
  38. package/lib/edm/edmPreprocessor.js +1 -5
  39. package/lib/gen/Dictionary.json +34 -2
  40. package/lib/gen/language.checksum +1 -1
  41. package/lib/gen/language.interp +1 -1
  42. package/lib/gen/languageParser.js +2429 -2401
  43. package/lib/inspect/inspectPropagation.js +2 -0
  44. package/lib/json/from-csn.js +87 -41
  45. package/lib/json/to-csn.js +47 -16
  46. package/lib/language/errorStrategy.js +1 -0
  47. package/lib/language/genericAntlrParser.js +109 -28
  48. package/lib/language/language.g4 +20 -4
  49. package/lib/model/csnRefs.js +30 -2
  50. package/lib/model/csnUtils.js +1 -0
  51. package/lib/model/revealInternalProperties.js +1 -2
  52. package/lib/modelCompare/compare.js +2 -1
  53. package/lib/optionProcessor.js +3 -0
  54. package/lib/render/manageConstraints.js +5 -2
  55. package/lib/render/toCdl.js +20 -7
  56. package/lib/render/toHdbcds.js +2 -8
  57. package/lib/render/toSql.js +6 -5
  58. package/lib/render/utils/common.js +9 -5
  59. package/lib/transform/db/assertUnique.js +2 -1
  60. package/lib/transform/db/expansion.js +2 -0
  61. package/lib/transform/db/flattening.js +37 -36
  62. package/lib/transform/db/rewriteCalculatedElements.js +559 -0
  63. package/lib/transform/db/transformExists.js +15 -6
  64. package/lib/transform/db/views.js +40 -37
  65. package/lib/transform/forRelationalDB.js +44 -30
  66. package/lib/transform/odata/typesExposure.js +50 -15
  67. package/lib/transform/parseExpr.js +14 -8
  68. package/lib/transform/transformUtilsNew.js +6 -5
  69. package/lib/transform/translateAssocsToJoins.js +49 -33
  70. package/package.json +1 -1
@@ -115,44 +115,7 @@ function getViewTransformer( csn, options, messageFunctions, transformCommon ) {
115
115
  }
116
116
  }
117
117
  }
118
- /**
119
- * Build a map of the resulting names (i.e. the element name of the column) and references to the respective columns
120
- *
121
- * This can later be used to match from elements to columns.
122
- *
123
- * @param {CSN.Query} query
124
- * @returns {object}
125
- */
126
- function getColumnMap( query ) {
127
- const map = Object.create(null);
128
- if (query && query.SELECT && query.SELECT.columns) {
129
- query.SELECT.columns.forEach((col) => {
130
- if (col === '*') {
131
- // do nothing
132
- }
133
- else if (col.as) {
134
- if (!map[col.as])
135
- map[col.as] = col;
136
- }
137
- else if (col.ref) {
138
- // .id on last path step can happen with hdbcds.hdbcds and malicious CSN input - maybe also with params?
139
- // We made things right in the end with the second add of missing stuff, but why not do it
140
- // right from the getgo
141
- const last = getLastRefStepString(col.ref);
142
- if (!map[last])
143
- map[last] = col;
144
- }
145
- else if (col.func) {
146
- map[col.func] = col;
147
- }
148
- else if (!map[col]) {
149
- map[col] = col;
150
- }
151
- });
152
- }
153
118
 
154
- return map;
155
- }
156
119
  /**
157
120
  * For things that are not explicitly found in the columns but still present in the elements, add them to the columnMap.
158
121
  *
@@ -513,6 +476,46 @@ function getLastRefStepString( ref ) {
513
476
  return last;
514
477
  }
515
478
 
479
+ /**
480
+ * Build a map of the resulting names (i.e. the element name of the column) and references to the respective columns
481
+ *
482
+ * This can later be used to match from elements to columns.
483
+ *
484
+ * @param {CSN.Query} query
485
+ * @returns {object}
486
+ */
487
+ function getColumnMap( query ) {
488
+ const map = Object.create(null);
489
+ if (query && query.SELECT && query.SELECT.columns) {
490
+ query.SELECT.columns.forEach((col) => {
491
+ if (col === '*') {
492
+ // do nothing
493
+ }
494
+ else if (col.as) {
495
+ if (!map[col.as])
496
+ map[col.as] = col;
497
+ }
498
+ else if (col.ref) {
499
+ // .id on last path step can happen with hdbcds.hdbcds and malicious CSN input - maybe also with params?
500
+ // We made things right in the end with the second add of missing stuff, but why not do it
501
+ // right from the getgo
502
+ const last = getLastRefStepString(col.ref);
503
+ if (!map[last])
504
+ map[last] = col;
505
+ }
506
+ else if (col.func) {
507
+ map[col.func] = col;
508
+ }
509
+ else if (!map[col]) {
510
+ map[col] = col;
511
+ }
512
+ });
513
+ }
514
+
515
+ return map;
516
+ }
517
+
516
518
  module.exports = {
517
519
  getViewTransformer,
520
+ getColumnMap,
518
521
  };
@@ -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');
@@ -121,6 +122,8 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
121
122
  let artifactRef,
122
123
  inspectRef,
123
124
  effectiveType,
125
+ initDefinition,
126
+ dropDefinitionCache,
124
127
  get$combined,
125
128
  getCsnDef,
126
129
  isAssocOrComposition,
@@ -159,7 +162,7 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
159
162
  timetrace.stop('Validate');
160
163
 
161
164
  // Needs to happen before tuple expansion, so the newly generated WHERE-conditions have it applied
162
- handleExists(csn, options, error);
165
+ handleExists(csn, options, error, inspectRef, initDefinition, dropDefinitionCache);
163
166
 
164
167
  // Check if structured elements and managed associations are compared in an expression
165
168
  // and expand these structured elements. This tuple expansion allows all other
@@ -176,14 +179,15 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
176
179
  const transformCsn = transformUtils.transformModel;
177
180
 
178
181
 
179
- timetrace.start('temporal');
180
- // (001) Add a temporal where condition to views where applicable before assoc2join
181
- // assoc2join eventually rewrites the table aliases
182
- forEachDefinition(csn, temporal.getViewDecorator(csn, {info}, csnUtils));
183
- 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
+ ]);
184
189
 
185
- // check unique constraints - further processing is done in rewriteUniqueConstraints
186
- assertUnique.prepare(csn, options, error, info);
190
+ rewriteCalculatedElementsInViews(csn, options, pathDelimiter, error);
187
191
 
188
192
  if(doA2J) {
189
193
  // Expand a structured thing in: keys, columns, order by, group by
@@ -197,6 +201,11 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
197
201
  bindCsnReferenceOnly();
198
202
 
199
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
200
209
  if(doA2J) {
201
210
  const resolved = new WeakMap();
202
211
  // No refs with struct-steps exist anymore
@@ -238,6 +247,8 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
238
247
  }
239
248
  });
240
249
 
250
+ processCalculatedElementsInEntities(csn, options);
251
+
241
252
  timetrace.start('Transform CSN')
242
253
 
243
254
  // (000) Rename primitive types, make UUID a String
@@ -253,15 +264,15 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
253
264
  },
254
265
  }, true);
255
266
 
256
- // (040) Ignore entities and views that are abstract or implemented
257
- // or carry the annotation cds.persistence.skip/exists
258
- // These entities are not removed from the csn, but flagged as "to be ignored"
259
- forEachDefinition(csn, cdsPersistence.getAnnoProcessor());
260
-
261
-
262
- // (050) Check @cds.valid.from/to only on entity
263
- // Views are checked in (001), unbalanced valid.from/to's or mismatching origins
264
- 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
+ ]);
265
276
 
266
277
  // eliminate the doA2J in the functions 'handleManagedAssociationFKs' and 'createForeignKeyElements'
267
278
  doA2J && flattening.handleManagedAssociationsAndCreateForeignKeys(csn, options, error, pathDelimiter, true, csnUtils, { skipDict: { actions: true }, allowArtifact: artifact => (artifact.kind === 'entity') });
@@ -298,18 +309,19 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
298
309
  }
299
310
  });
300
311
 
301
- // For generating DB stuff:
302
- // - table-entity with parameters: not allowed
303
- // - view with parameters: ok on HANA, not allowed otherwise
304
- // (don't complain about action/function with parameters)
305
- forEachDefinition(csn, handleChecksForWithParameters);
306
-
307
- // Remove .masked
308
- // Check that keys are not explicitly nullable
309
- // Check that Associations are not used in entities/views with parameters
310
- // (150 b) Strip inheritance
311
- // Note that this should happen after implicit redirection, because includes are required for that
312
- 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
+ ]);
313
325
 
314
326
  // (170) Transform '$self' in backlink associations to appropriate key comparisons
315
327
  // Must happen before draft processing because the artificial ON-conditions in generated
@@ -441,6 +453,8 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
441
453
  ({ artifactRef,
442
454
  inspectRef,
443
455
  effectiveType,
456
+ initDefinition,
457
+ dropDefinitionCache,
444
458
  get$combined,
445
459
  getCsnDef,
446
460
  isAssocOrComposition,
@@ -451,7 +465,7 @@ function transformForRelationalDBWithCsn(inputModel, options, moduleName) {
451
465
 
452
466
  function bindCsnReferenceOnly(){
453
467
  // invalidate caches for CSN ref API
454
- ({ artifactRef, inspectRef, effectiveType } = csnRefs(csn));
468
+ ({ artifactRef, inspectRef, effectiveType, initDefinition, dropDefinitionCache } = csnRefs(csn));
455
469
  }
456
470
 
457
471
  function handleMixinOnConditions(artifact, artifactName) {
@@ -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.0",
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)",