@sap/cds-compiler 4.1.2 → 4.2.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 (74) hide show
  1. package/CHANGELOG.md +101 -1
  2. package/bin/cdsc.js +6 -3
  3. package/doc/CHANGELOG_BETA.md +5 -0
  4. package/doc/CHANGELOG_DEPRECATED.md +15 -0
  5. package/lib/api/main.js +2 -2
  6. package/lib/api/options.js +2 -2
  7. package/lib/api/validate.js +24 -24
  8. package/lib/base/message-registry.js +41 -6
  9. package/lib/base/messages.js +7 -0
  10. package/lib/base/model.js +37 -8
  11. package/lib/checks/elements.js +11 -10
  12. package/lib/checks/manyNavigations.js +33 -0
  13. package/lib/checks/onConditions.js +5 -2
  14. package/lib/checks/queryNoDbArtifacts.js +2 -3
  15. package/lib/checks/selectItems.js +4 -55
  16. package/lib/checks/utils.js +3 -2
  17. package/lib/checks/validator.js +3 -1
  18. package/lib/compiler/.eslintrc.json +2 -1
  19. package/lib/compiler/assert-consistency.js +27 -24
  20. package/lib/compiler/base.js +6 -2
  21. package/lib/compiler/builtins.js +34 -34
  22. package/lib/compiler/checks.js +179 -208
  23. package/lib/compiler/classes.js +2 -2
  24. package/lib/compiler/cycle-detector.js +6 -6
  25. package/lib/compiler/define.js +66 -45
  26. package/lib/compiler/extend.js +81 -72
  27. package/lib/compiler/finalize-parse-cdl.js +26 -26
  28. package/lib/compiler/generate.js +61 -45
  29. package/lib/compiler/index.js +47 -49
  30. package/lib/compiler/kick-start.js +8 -7
  31. package/lib/compiler/moduleLayers.js +1 -1
  32. package/lib/compiler/populate.js +42 -35
  33. package/lib/compiler/propagator.js +6 -6
  34. package/lib/compiler/resolve.js +170 -126
  35. package/lib/compiler/shared.js +122 -45
  36. package/lib/compiler/tweak-assocs.js +93 -40
  37. package/lib/compiler/utils.js +15 -12
  38. package/lib/edm/.eslintrc.json +40 -1
  39. package/lib/edm/annotations/genericTranslation.js +721 -707
  40. package/lib/edm/annotations/preprocessAnnotations.js +88 -77
  41. package/lib/edm/csn2edm.js +389 -378
  42. package/lib/edm/edm.js +678 -772
  43. package/lib/edm/edmAnnoPreprocessor.js +132 -146
  44. package/lib/edm/edmInboundChecks.js +29 -27
  45. package/lib/edm/edmPreprocessor.js +686 -646
  46. package/lib/edm/edmUtils.js +277 -296
  47. package/lib/gen/language.checksum +1 -1
  48. package/lib/gen/language.interp +1 -1
  49. package/lib/gen/languageParser.js +1253 -1276
  50. package/lib/json/from-csn.js +34 -4
  51. package/lib/json/to-csn.js +4 -4
  52. package/lib/language/language.g4 +2 -5
  53. package/lib/main.d.ts +61 -1
  54. package/lib/model/csnUtils.js +31 -2
  55. package/lib/model/revealInternalProperties.js +1 -1
  56. package/lib/modelCompare/compare.js +37 -2
  57. package/lib/modelCompare/utils/filter.js +1 -1
  58. package/lib/optionProcessor.js +15 -3
  59. package/lib/render/toCdl.js +30 -4
  60. package/lib/render/toSql.js +5 -9
  61. package/lib/render/utils/common.js +8 -6
  62. package/lib/transform/db/applyTransformations.js +1 -1
  63. package/lib/transform/db/cdsPersistence.js +1 -1
  64. package/lib/transform/db/constraints.js +47 -17
  65. package/lib/transform/db/expansion.js +121 -47
  66. package/lib/transform/db/flattening.js +75 -7
  67. package/lib/transform/forOdata.js +4 -1
  68. package/lib/transform/forRelationalDB.js +80 -62
  69. package/lib/transform/localized.js +91 -54
  70. package/lib/transform/transformUtils.js +9 -10
  71. package/lib/utils/file.js +7 -7
  72. package/lib/utils/moduleResolve.js +210 -121
  73. package/lib/utils/objectUtils.js +1 -1
  74. package/package.json +5 -5
package/CHANGELOG.md CHANGED
@@ -7,11 +7,92 @@
7
7
  Note: `beta` fixes, changes and features are usually not listed in this ChangeLog but [here](doc/CHANGELOG_BETA.md).
8
8
  The compiler behavior concerning `beta` features can change at any time without notice.
9
9
 
10
+ ## Version 4.2.2 - 2023-08-31
11
+
12
+ ### Fixed
13
+
14
+ - to.sql|hdi.migration: Fix bug that caused a migration to be rendered for the HANA CDS types that were removed from the CSN
15
+
16
+ ## Version 4.2.0 - 2023-08-29
17
+
18
+ ### Added
19
+
20
+ - Compiler:
21
+ + Option `moduleLookupDirectories: [ 'strings' ]` can be used to specify additional module lookup
22
+ directories, similar to `node_modules/`.
23
+ + LIMIT and OFFSET clauses can now contain expressions, including parameter references.
24
+ - to.edm(x):
25
+ + Detect spec violation `scale` > `precision`.
26
+ - to.sql/for.odata:
27
+ + With the new option `fewerLocalizedViews: true|false`, an entity/view will not get a localized convenience
28
+ view, if it only has associations to localized entities/views. Only if there is actually a localized
29
+ element (e.g. new localized element or reference to one), will it get a convenience view.
30
+ - to.sql
31
+ + In a localized scenario, create foreign key constraints for the generated `.texts` entities.
32
+ + Casting `null` to a structure such as `null as field : StructT` is now supported. For each leaf element,
33
+ an additional `null as field_name` column is added.
34
+
35
+ ### Changed
36
+
37
+ - Compiler:
38
+ + Selecting fields of structures or associations (without filters) are now candidates for ON-condition
39
+ rewrites: It is no longer necessary to select the struct/association directly.
40
+ + Consistently handle the case when type elements are defined to be a `key`:
41
+ the `key` property is not only preserved with `includes`, but also in other cases.
42
+ Use option `deprecated.noKeyPropagationWithExpansions` to switch to the v3 behavior.
43
+ + When including aspects or entities into structured type definitions,
44
+ do not add actions to the type.
45
+ + An `annotate` statement in the `extensions` section of a CSN now consistently uses the
46
+ `elements` property even if the `annotate` is intended to be used for an enum symbol.
47
+ Before v4.2, the compiler has used the `enum` property in a CSN of flavor `xtended`
48
+ (`gensrc`) if it was certain that it was to be applied to an enum symbol.
49
+ - to.cdl: If a definition has an `actions` property, an `actions {…}` block is now always rendered,
50
+ and is not ignored if empty.
51
+ - to.sql:
52
+ + For SQL dialect "sqlite", `cds.DateTime` is now rendered as `DATETIME_TEXT` instead of `TIMESTAMP_TEXT`.
53
+ + Casting a literal (except `null`) to a structure now yields a proper error.
54
+ + `.texts` entities annotated with `@cds.persistence.skip` (without their original entity having that annotation)
55
+ lead to deployment issues later. It is now an error.
56
+
57
+ ### Fixed
58
+
59
+ - Compiler:
60
+ + Reject invalid reference in the `on` of `join`s already while compiling,
61
+ not just when calling the (SQL) backend.
62
+ + Correct the calculation of annotation assignments on the return structure of actions
63
+ when both `annotate … with {…}` and `annotate … with returns {…}` had been used
64
+ on the same structure element. Ensure that it works when non-applied, too.
65
+ + Do not remove or invent `actions` properties with zero actions or functions in it.
66
+ + Correct auto-redirection of direct cycle associations: if the source and target of a
67
+ model association are the same entity, and the main artifact of the service association
68
+ based on the model association is a suitable auto-redirection target, then use it
69
+ as new target, independently from the value of `@cds.redirection.target`.
70
+ - to.cdl: Indirectly structured types (`type T: Struct;`) with `includes` (`extend T with T2;`), are now properly rendered.
71
+ - to.sql/hdi/hdbcds:
72
+ + Views on views with parameters did not have localized convenience views based on
73
+ other localized views (missing `localized.` prefix in FROM clause)
74
+ + Run less checks on entities marked with `@cds.persistence.exists`
75
+ + Correctly render SELECT items where the column name conflicts with a table alias
76
+ - to.sql: Casting expressions to a structured type yields a proper error instead of strange compiler error.
77
+ - to.edm(x):
78
+ + Don't expand `@mandatory` if element has an annotation with prefix `@Common.FieldControl.`.
79
+ + Fix a bug when referencing nested many structures, especially referring to a managed association via
80
+ `$self` comparison.
81
+ + Improve handling of non-collection annotation values for collection-like vocabulary types.
82
+ + Don't render `Scale: variable` for `cds.Decimal(scale:0)`.
83
+ - to.sql|hdi.migration:
84
+ + Fixed a bug that caused rendering of `ALTER` statements to abort early and not render some statements.
85
+ + CSN output now only contains real `cds` builtins, no early remapping to HANA CDS types or similar.
86
+ - to.sql.migration: Don't drop-create views marked with `@cds.persistence.exists`
87
+
88
+ ### Removed
89
+
10
90
  ## Version 4.1.2 - 2023-07-31
11
91
 
12
92
  ### Fixed
13
93
 
14
- - to.hdi.migration: Changes in constraints are not rendered as part of the .hdbmigrationtable file, as they belong in other HDI artifacts
94
+ - to.hdi.migration: Changes in constraints are not rendered as part of the .hdbmigrationtable file,
95
+ as they belong in other HDI artifacts
15
96
 
16
97
  ## Version 4.1.0 - 2023-07-28
17
98
 
@@ -196,6 +277,25 @@ The compiler behavior concerning `beta` features can change at any time without
196
277
  are removed, because since - `Entity.myElement` could also be a definition,
197
278
  creating ambiguities. This did not work always, anyway.
198
279
 
280
+ ## Version 3.9.10 - 2023-08-25
281
+
282
+ ### Fixed
283
+
284
+ - to.edm(x): Error reporting for incorrect handling of "Collection()" has been improved.
285
+ - to.sql/hdi/hdbcds: Views on views with parameters did not have localized convenience views based on
286
+ other localized views (missing `localized.` prefix in FROM clause)
287
+ - to.sql: Casting expressions to a structured type yields a proper error instead of strange compiler error.
288
+ - to.sql.migration: Don't drop-create views marked with `@cds.persistence.exists` or `@cds.persistence.skip`
289
+
290
+ ## Version 3.9.8 - 2023-08-03
291
+
292
+ ### Fixed
293
+
294
+ - to.edm(x):
295
+ + Don't expand `@mandatory` if element has an annotation with prefix `@Common.FieldControl.`.
296
+ + Fix a bug when referencing nested many structures, especially referring to a managed association via
297
+ `$self` comparison.
298
+ - to.sql/hdi/hdbcds: Detect navigation into arrayed structures and raise helpful errors instead of running into internal errors.
199
299
 
200
300
  ## Version 3.9.6 - 2023-07-27
201
301
 
package/bin/cdsc.js CHANGED
@@ -162,7 +162,7 @@ function cdsc_main() {
162
162
  validateDirectBackendOption(cmdLine.command, cmdLine.options, cmdLine.args);
163
163
 
164
164
  // If set through CLI (and not options file), `beta` is a string and needs processing.
165
- if (cmdLine.options.beta && typeof cmdLine.options.beta === 'string') {
165
+ if (typeof cmdLine.options.beta === 'string') {
166
166
  const features = cmdLine.options.beta.split(',');
167
167
  cmdLine.options.beta = {};
168
168
  features.forEach((val) => {
@@ -191,14 +191,17 @@ function cdsc_main() {
191
191
  console.error( `Running ‘${cmdLine.command}’ with test-mode shuffle ${cmdLine.options.testMode} …` );
192
192
  }
193
193
 
194
- // If set through CLI (and not options file), `deprecated` is a string and needs processing.
195
- if (cmdLine.options.deprecated && typeof cmdLine.options.deprecated === 'string') {
194
+ // If set through CLI (and not options file), `deprecated` and `moduleLookupDirectories`
195
+ // are strings and needs processing.
196
+ if (typeof cmdLine.options.deprecated === 'string') {
196
197
  const features = cmdLine.options.deprecated.split(',');
197
198
  cmdLine.options.deprecated = {};
198
199
  features.forEach((val) => {
199
200
  cmdLine.options.deprecated[val] = true;
200
201
  });
201
202
  }
203
+ if (typeof cmdLine.options.moduleLookupDirectories === 'string')
204
+ cmdLine.options.moduleLookupDirectories = cmdLine.options.moduleLookupDirectories.split(',');
202
205
 
203
206
  parseSeverityOptions(cmdLine);
204
207
 
@@ -73,6 +73,11 @@ Now enabled per default.
73
73
 
74
74
  ## Version 3.7.0 - 2023-02-22
75
75
 
76
+ ### Added `annotationExpressions`
77
+
78
+ This option allows to use expressions as annotation values, e.g.
79
+ `@anno: (1+2)`.
80
+
76
81
  ### Added `calculatedElements`
77
82
 
78
83
  Allows to define calculated elements in entities and aspects. When used in views, they
@@ -11,6 +11,21 @@ Note: `deprecated` features are listed in this ChangeLog just for information.
11
11
  **When the `deprecated` option is set, the `beta` option is ignored,
12
12
  and several new features are not available.**
13
13
 
14
+ ## Version 4.2.0 - 2023-08-29
15
+
16
+ ### Added `noKeyPropagationWithExpansions`
17
+
18
+ When this option is set, element `id` in types `Orig` and `I` are keys,
19
+ but `id` in `D` is not.
20
+
21
+ ```cds
22
+ type Orig { key id: Integer };
23
+ type I: Orig {};
24
+ type D: Orig;
25
+ ```
26
+
27
+ When this option is not set, element `id` in all three types are keys.
28
+
14
29
  ## Version 4.0.0 - 2023-06-06
15
30
 
16
31
  ### Added `downgradableErrors`
package/lib/api/main.js CHANGED
@@ -400,8 +400,7 @@ function sqlMigration( csn, options, beforeImage ) {
400
400
  const beforeArtifact = beforeImage.definitions[artifactName];
401
401
  const diffArtifact = diff.definitions[artifactName];
402
402
  // TODO: exists, abstract? isPersistedOnDb?
403
- if (diffArtifact && diffArtifact['@cds.persistence.name'] && !diffArtifact['@cds.persistence.skip'] &&
404
- (diffArtifact.query || diffArtifact.projection) &&
403
+ if (diffArtifact && diffArtifact['@cds.persistence.name'] && csnUtils.isPersistedAsView(diffArtifact) &&
405
404
  (diffArtifact[modelCompare.isChanged] === true || // we know it changed because we compared two views
406
405
  diffArtifact[modelCompare.isChanged] === undefined)) { // if it was removed in the after, then we don't have the flag
407
406
  drops.creates[artifactName] = `DROP VIEW ${ identifierUtils.renderArtifactName(artifactName) };`;
@@ -409,6 +408,7 @@ function sqlMigration( csn, options, beforeImage ) {
409
408
  else if (diffArtifact &&
410
409
  diffArtifact['@cds.persistence.skip'] !== true &&
411
410
  diffArtifact.kind === beforeArtifact.kind && // detect action -> entity
411
+ csnUtils.isPersistedAsTable(diffArtifact) === csnUtils.isPersistedAsTable(beforeArtifact) && // detect removal of @cds.persistence.exists
412
412
  csnUtils.isPersistedAsView(diffArtifact) === csnUtils.isPersistedAsView(beforeArtifact) // detect view -> entity
413
413
  ) { // don't render again, but need info for primary key extension
414
414
  diffArtifact['@cds.persistence.skip'] = true;
@@ -30,6 +30,7 @@ const publicOptionsNewAPI = [
30
30
  'pre2134ReferentialConstraintNames',
31
31
  'generatedByComment',
32
32
  'betterSqliteSessionVariables',
33
+ 'fewerLocalizedViews',
33
34
  // ODATA
34
35
  'odataOpenapiHints',
35
36
  'odataVersion',
@@ -52,7 +53,6 @@ const privateOptions = [
52
53
  // Not callable via cdsc, keep private for now until we are sure that we want this
53
54
  'filterCsn',
54
55
  'lintMode',
55
- 'fuzzyCsnError',
56
56
  'traceFs',
57
57
  'traceParser',
58
58
  'traceParserAmb',
@@ -66,7 +66,7 @@ const privateOptions = [
66
66
  'noRecompile',
67
67
  'internalMsg',
68
68
  'disableHanaComments', // in case of issues with hana comment rendering
69
- 'localizedWithoutCoalesce', // deprecated version of 'localizedLanguageFallback', TODO(v4): Remove option
69
+ 'localizedWithoutCoalesce', // deprecated version of 'localizedLanguageFallback', TODO(v5): Remove option
70
70
  ];
71
71
 
72
72
  const overallOptions = publicOptionsNewAPI.concat(privateOptions);
@@ -130,26 +130,28 @@ const validators = {
130
130
  assertIntegrityType: generateStringValidator([ 'DB', 'RT' ]),
131
131
  };
132
132
 
133
+ // Note: if `validate()` returns true, it means the option is _invalid_!
133
134
  const allCombinationValidators = {
134
- 'valid-structured': {
135
- validate: options => options.odataVersion === 'v2' && options.odataFormat === 'structured',
136
- severity: 'error',
137
- getParameters: () => {},
138
- getMessage: () => 'Structured OData is only supported with OData version v4',
139
- },
140
- 'sql-dialect-and-naming': {
141
- validate: options => options.sqlDialect && options.sqlMapping && ![ 'hana' ].includes(options.sqlDialect) && [ 'quoted', 'hdbcds' ].includes(options.sqlMapping),
142
- severity: 'error',
143
- getParameters: options => ({ name: options.sqlDialect, prop: options.sqlMapping }),
144
- getMessage: () => 'sqlDialect $(NAME) can\'t be combined with sqlMapping $(PROP)',
145
- },
146
- 'beta-no-test': {
147
- validate: options => options.beta && !options.testMode,
148
- severity: 'warning',
149
- getParameters: () => {},
150
- getMessage: () => 'Option "beta" was used. This option should not be used in productive scenarios!',
135
+ 'valid-structured': (options, message) => {
136
+ if (options.odataVersion === 'v2' && options.odataFormat === 'structured')
137
+ message.error('api-invalid-combination', null, { '#': 'valid-structured' });
138
+ },
139
+ 'sql-dialect-and-naming': (options, message) => {
140
+ if (options.sqlDialect && options.sqlMapping && options.sqlDialect !== 'hana' && [ 'quoted', 'hdbcds' ].includes(options.sqlMapping))
141
+ message.error('api-invalid-combination', null, { '#': 'sql-dialect-and-naming', name: options.sqlDialect, prop: options.sqlMapping });
142
+ },
143
+ 'sql-dialect-and-localized': (options, message) => {
144
+ if (options.fewerLocalizedViews && options.sqlDialect === 'hana')
145
+ message.error('api-invalid-combination', null, { '#': 'sql-dialect-and-localized', option: 'fewerLocalizedViews', value: 'hana' });
146
+ },
147
+ 'beta-no-test': (options, message) => {
148
+ if (options.beta && !options.testMode)
149
+ message.warning('api-unexpected-combination', null, { '#': 'beta-no-test', option: 'beta' });
151
150
  },
152
151
  };
152
+
153
+ const alwaysRunValidators = [ 'beta-no-test', 'sql-dialect-and-localized' ];
154
+
153
155
  /* eslint-disable jsdoc/no-undefined-types */
154
156
  /**
155
157
  * Run the validations for each option.
@@ -172,11 +174,12 @@ function validate( options, moduleName, customValidators = {}, combinationValida
172
174
  const validator = customValidators[optionName] || validators[optionName] || booleanValidator;
173
175
 
174
176
  if (!validator.validate(optionValue)) {
175
- error('invalid-option', null, {
177
+ error('api-invalid-option', null, {
178
+ '#': 'value',
176
179
  prop: optionName,
177
180
  value: validator.expected(optionValue),
178
181
  othervalue: validator.found(optionValue),
179
- }, 'Expected option $(PROP) to have $(VALUE). Found: $(OTHERVALUE)');
182
+ });
180
183
  }
181
184
  });
182
185
  throwWithAnyError();
@@ -184,11 +187,8 @@ function validate( options, moduleName, customValidators = {}, combinationValida
184
187
 
185
188
  const message = makeMessageFunction(null, options, moduleName);
186
189
 
187
- for (const combinationValidatorName of combinationValidators.concat([ 'beta-no-test' ])) {
188
- const combinationValidator = allCombinationValidators[combinationValidatorName];
189
- if (combinationValidator.validate(options))
190
- message[combinationValidator.severity]('invalid-option-combination', null, combinationValidator.getParameters(options), combinationValidator.getMessage(options));
191
- }
190
+ for (const combinationValidatorName of combinationValidators.concat(alwaysRunValidators))
191
+ allCombinationValidators[combinationValidatorName](options, message);
192
192
 
193
193
  message.throwWithAnyError();
194
194
  }
@@ -63,6 +63,7 @@ const centralMessages = {
63
63
  'anno-undefined-element': { severity: 'Warning' },
64
64
  'anno-undefined-param': { severity: 'Warning' },
65
65
  'anno-unexpected-ellipsis': { severity: 'Error', configurableFor: 'deprecated' },
66
+ 'anno-unexpected-localized-skip': { severity: 'Error', configurableFor: true },
66
67
 
67
68
  'name-invalid-dollar-alias': { severity: 'Error', configurableFor: true },
68
69
 
@@ -114,6 +115,7 @@ const centralMessages = {
114
115
  'type-ambiguous-target': { severity: 'Warning' },
115
116
 
116
117
  'ref-unexpected-autoexposed': { severity: 'Error' },
118
+ 'ref-unexpected-many-navigation': { severity: 'Error' },
117
119
  // Published! Used in @sap/cds-lsp; if renamed, add to oldMessageIds and contact colleagues
118
120
  'ref-undefined-art': { severity: 'Error' },
119
121
  'ref-undefined-def': { severity: 'Error' },
@@ -230,6 +232,8 @@ const centralMessageTexts = {
230
232
  std: 'Invalid option $(NAME)!',
231
233
  deprecated: 'Option $(NAME) is no longer supported! Use latest API options instead',
232
234
  magicVars: 'Option $(PROP) is no longer supported! Use $(OTHERPROP) instead. See <https://cap.cloud.sap/docs/guides/databases#configuring-variables> for details',
235
+ value: 'Expected option $(PROP) to have $(VALUE). Found: $(OTHERVALUE)',
236
+ type: 'Expected option $(OPTION) to be of type $(VALUE). Found: $(OTHERVALUE)',
233
237
  },
234
238
 
235
239
  'api-invalid-variable-replacement': {
@@ -239,6 +243,22 @@ const centralMessageTexts = {
239
243
  'noDollar': 'Option $(OPTION) does not know $(NAME). Did you forget a leading $(CODE)?'
240
244
  },
241
245
 
246
+ 'api-invalid-combination': {
247
+ std: 'Invalid option combination found: $(OPTION) and $(PROP)', // unused
248
+ 'valid-structured': 'Structured OData is only supported with OData version v4',
249
+ 'sql-dialect-and-naming': 'sqlDialect $(NAME) can\'t be combined with sqlMapping $(PROP)',
250
+ 'sql-dialect-and-localized': 'Option $(OPTION) can\'t be combined with SQL dialect $(VALUE) or the to.hdi()/to.hdbcds() backend',
251
+ },
252
+ 'api-unexpected-combination': {
253
+ std: 'Unexpected option combination: $(OPTION) and $(PROP)', // unused
254
+ 'beta-no-test':'Option $(OPTION) was used. This option should not be used in productive scenarios!',
255
+ },
256
+ 'api-invalid-lookup-dir': {
257
+ std: '',
258
+ slash: 'Expected directory $(VALUE) in option $(OPTION) to end with $(OTHERVALUE)',
259
+ relative: 'Expected directory $(VALUE) in option $(OPTION) to not start with $(OTHERVALUE)',
260
+ },
261
+
242
262
  'anno-duplicate': {
243
263
  std: 'Duplicate assignment with $(ANNO)',
244
264
  doc: 'Duplicate assignment with a doc comment',
@@ -546,6 +566,9 @@ const centralMessageTexts = {
546
566
  std: '$(ART) can\'t be extended because it originates from an include',
547
567
  elements: '$(ART) can\'t be extended by elements/enums because it originates from an include',
548
568
  },
569
+ 'ref-unexpected-many-navigation': {
570
+ std: 'Unexpected navigation into arrayed structure',
571
+ },
549
572
  'ref-unexpected-scope': {
550
573
  std: 'Unexpected parameter reference',
551
574
  calc: 'Calculated elements can\'t use parameter references',
@@ -737,6 +760,12 @@ const centralMessageTexts = {
737
760
  elements: 'Duplicate element $(NAME) through multiple includes $(SORTED_ARTS)',
738
761
  actions: 'Duplicate action or function $(NAME) through multiple includes $(SORTED_ARTS)',
739
762
  },
763
+ 'ref-invalid-element': {
764
+ std: 'Invalid element reference',
765
+ $tableAlias: 'Can\'t refer to source elements of table alias $(ID)',
766
+ mixin: 'Can\'t refer to the query\'s own mixin $(ID)',
767
+ $self: 'Can\'t refer to the query\'s own elements',
768
+ },
740
769
  'ref-invalid-override': {
741
770
  std: 'Overridden element of include must not change element structure', // unused
742
771
  'new-not-structured': 'Expected element $(NAME) to be structured, because it overrides the included element from $(ART)',
@@ -848,14 +877,17 @@ const centralMessageTexts = {
848
877
  changed: 'Changed element $(ID) is a primary key change and will not work with dialect $(NAME)'
849
878
  },
850
879
 
880
+ 'type-invalid-cast': {
881
+ std: 'Invalid cast to $(TYPE)', // unused
882
+ 'to-structure': 'Can\'t cast to a structured type',
883
+ 'from-structure': 'Structured elements can\'t be cast to a different type',
884
+ 'expr-to-structure': 'Can\'t cast an expression to a structured type',
885
+ 'val-to-structure': 'Can\'t cast $(VALUE) to a structured type'
886
+ },
887
+
851
888
  // -----------------------------------------------------------------------------------
852
889
  // Expressions
853
890
  // -----------------------------------------------------------------------------------
854
- 'expr-invalid-operator': 'Comparing $(ID) is only allowed with $(OP)',
855
- 'expr-missing-comparison': {
856
- std: 'Expected a comparison with $(OP) when using $(ID) in an ON-condition',
857
- },
858
-
859
891
  'type-invalid-cardinality': {
860
892
  std: 'Invalid value $(VALUE) for cardinality', // unused variant
861
893
  sourceMax: 'Invalid value $(PROP) for maximum source cardinality, expecting a positive number or $(OTHERPROP)',
@@ -868,6 +900,8 @@ const centralMessageTexts = {
868
900
 
869
901
  'i18n-different-value': 'Different translation for key $(PROP) of language $(OTHERPROP) in unrelated layers',
870
902
 
903
+ 'expr-missing-foreign-key': 'Path step $(ID) of $(ELEMREF) has no valid foreign keys',
904
+
871
905
  // OData version dependent messages
872
906
  'odata-spec-violation-array': 'Unexpected array type for OData $(VERSION)',
873
907
  'odata-spec-violation-param' : 'Expected parameter to be typed with either scalar or structured type for OData $(VERSION)',
@@ -900,6 +934,7 @@ const centralMessageTexts = {
900
934
  incompatible: 'Unexpected EDM Type $(TYPE) for OData $(VERSION)',
901
935
  facet: 'Unexpected EDM Type facet $(NAME) of type $(TYPE) for OData $(VERSION)',
902
936
  external: 'Referenced type $(TYPE) marked as $(ANNO) can\'t be rendered as $(CODE) in service $(NAME) for OData $(VERSION)',
937
+ scale: 'Expected scale $(NUMBER) to be less than or equal to precision $(RAWVALUE)'
903
938
  },
904
939
  'odata-spec-violation-property-name': 'Expected element name to be different from declaring $(META)',
905
940
  'odata-spec-violation-namespace': {
@@ -965,7 +1000,7 @@ const centralMessageTexts = {
965
1000
  // -----------------------------------------------------------------------------------
966
1001
  'enum': 'Value $(VALUE) is not one out of $(RAWVALUES) for $(ANNO) of type $(TYPE)',
967
1002
  'std': 'Unexpected value $(VALUE) for $(ANNO) of type $(TYPE)',
968
- 'struct': 'Unexpected $(STR) value for $(ANNO) of type $(TYPE)',
1003
+ 'incompval': 'Unexpected $(STR) value for $(ANNO) of type $(TYPE)',
969
1004
  'nestedcollection': 'Nested collections are not supported for $(ANNO)',
970
1005
  'enumincollection': 'Enum inside collection is not supported for $(ANNO)',
971
1006
  'multexpr': 'EDM JSON code contains more than one dynamic expression: $(RAWVALUES) for $(ANNO)',
@@ -1300,6 +1300,13 @@ function deduplicateMessages( messages ) {
1300
1300
 
1301
1301
  function shortArtName( art ) {
1302
1302
  const name = getArtifactName( art );
1303
+ if (!name) {
1304
+ const loc = art.location ? ` at ${ locationString( art.location ) }` : '';
1305
+ throw new CompilerAssertion(
1306
+ art.path
1307
+ ? `No artifact for ${ art.path.map( i => i.id ).join( '.' )}${ loc }`
1308
+ : `No name found in ${ Object.keys( art ).join( '+' )}${ loc }` );
1309
+ }
1303
1310
  if ([ 'select', 'action', 'alias', 'param' ].every( n => name[n] == null || name[n] === 1 ) &&
1304
1311
  !name.absolute.includes(':'))
1305
1312
  return quote.double( name.element ? `${ name.absolute }:${ name.element }` : name.absolute );
package/lib/base/model.js CHANGED
@@ -34,6 +34,7 @@ const availableBetaFlags = {
34
34
  odataTerms: true,
35
35
  optionalActionFunctionParameters: true, // not supported by runtime, yet.
36
36
  associationDefault: true,
37
+ annotateForeignKeys: true,
37
38
  // disabled by --beta-mode
38
39
  nestedServices: false,
39
40
  };
@@ -44,6 +45,7 @@ const availableDeprecatedFlags = {
44
45
  downgradableErrors: true,
45
46
  includesNonShadowedFirst: true,
46
47
  eagerPersistenceForGeneratedEntities: true,
48
+ noKeyPropagationWithExpansions: true,
47
49
  }
48
50
 
49
51
  const oldDeprecatedFlags_v2 = [
@@ -130,18 +132,26 @@ function checkRemovedDeprecatedFlags( options, { error } ) {
130
132
  });
131
133
  }
132
134
 
133
- // Apply function `callback` to all artifacts in dictionary
134
- // `model.definitions`. See function `forEachGeneric` for details.
135
- // TODO: should we skip "namespaces" already here?
135
+ /**
136
+ * Apply function `callback` to all artifacts in dictionary
137
+ * `model.definitions`. See function `forEachGeneric` for details.
138
+ * TODO: should we skip "namespaces" already here?
139
+ */
136
140
  function forEachDefinition( model, callback ) {
137
141
  forEachGeneric( model, 'definitions', callback );
138
142
  }
139
143
 
140
- // Apply function `callback` to all members of object `obj` (main artifact or
141
- // parent member). Members are considered those in dictionaries `elements`,
142
- // `enum`, `actions` and `params` of `obj`, `elements` and `enums` are also
143
- // searched inside property `items` (array of). See function `forEachGeneric`
144
- // for details.
144
+ /**
145
+ * Apply function `callback` to all members of object `obj` (main artifact or
146
+ * parent member). Members are considered those in dictionaries `elements`,
147
+ * `enum`, `actions` and `params` of `obj`, `elements` and `enums` are also
148
+ * searched inside property `items` (array of). See function `forEachGeneric`
149
+ * for details.
150
+ *
151
+ * @param {XSN.Artifact} construct
152
+ * @param {(member: XSN.Artifact, memberName: string, prop: string) => void} callback
153
+ * @param {XSN.Artifact} [target]
154
+ */
145
155
  function forEachMember( construct, callback, target ) {
146
156
  let obj = construct;
147
157
  while (obj.items)
@@ -155,6 +165,24 @@ function forEachMember( construct, callback, target ) {
155
165
  callback( construct.returns, '', 'params' );
156
166
  }
157
167
 
168
+ /**
169
+ * Same as forEachMember, but inside each member, calls itself recursively, i.e.
170
+ * sub members are traversed as well.
171
+ *
172
+ * @param {XSN.Artifact} construct
173
+ * @param {(member: XSN.Artifact, memberName: string, prop: string) => void} callback
174
+ * @param {XSN.Artifact} [target]
175
+ */
176
+ function forEachMemberRecursively( construct, callback, target ) {
177
+ forEachMember( construct, ( member, memberName, prop ) => {
178
+ callback( member, memberName, prop );
179
+ if (Array.isArray(member.$duplicates)) // redefinitions
180
+ member.$duplicates.forEach( o => callback( o, memberName, prop ) );
181
+ // Descend into nested members, too
182
+ forEachMemberRecursively( member, callback );
183
+ }, target);
184
+ }
185
+
158
186
  // Apply function `callback` to all objects in dictionary `dict`, including all
159
187
  // duplicates (found under the same name). Function `callback` is called with
160
188
  // the following arguments: the object, the name, and -if it is a duplicate-
@@ -197,6 +225,7 @@ module.exports = {
197
225
  queryOps,
198
226
  forEachDefinition,
199
227
  forEachMember,
228
+ forEachMemberRecursively,
200
229
  forEachGeneric,
201
230
  forEachInOrder,
202
231
  setProp,
@@ -119,18 +119,19 @@ function checkManagedAssoc( art ) {
119
119
  forEachMemberRecursively(art, (member) => {
120
120
  if (this.csnUtils.isAssocOrComposition(member) &&
121
121
  !isManagedComposition.bind(this)(member)) {
122
+ // Implementation note: Imported services (i.e. external ones) may contain to-many associations
123
+ // with an empty foreign key list. If the user (in this case importer) explicitly sets an empty
124
+ // foreign key array, we won't emit a warning to avoid spamming the user.
122
125
  const max = member.cardinality?.max ? member.cardinality.max : 1;
123
- if (max !== 1 && !member.on) {
126
+ if (max !== 1 && !member.on && (!member.keys || member.keys.length > 0)) {
124
127
  const isNoDb = art['@cds.persistence.skip'] || art['@cds.persistence.exists'];
125
- this.warning(isNoDb ? 'to-many-no-on-noDB' : 'to-many-no-on', member.cardinality ? member.cardinality.$path : member.$path,
126
- {
127
- value: cardinality2str(member, false),
128
- '#': this.csnUtils.isComposition(member) ? 'comp' : 'std',
129
- },
130
- {
131
- std: 'Expected association with target cardinality $(VALUE) to have an ON-condition',
132
- comp: 'Expected composition with target cardinality $(VALUE) to have an ON-condition',
133
- });
128
+ this.warning(isNoDb ? 'to-many-no-on-noDB' : 'to-many-no-on', member.cardinality ? member.cardinality.$path : member.$path, {
129
+ value: cardinality2str(member, false),
130
+ '#': this.csnUtils.isComposition(member) ? 'comp' : 'std',
131
+ }, {
132
+ std: 'Expected association with target cardinality $(VALUE) to have an ON-condition',
133
+ comp: 'Expected composition with target cardinality $(VALUE) to have an ON-condition',
134
+ });
134
135
  }
135
136
  }
136
137
  });
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ const { applyTransformationsOnNonDictionary } = require('../model/csnUtils');
4
+
5
+ /**
6
+ * Check all refs in the given parent for the traversal of paths
7
+ * into `.items`
8
+ *
9
+ * @param {object} parent Object with the expression as a property
10
+ * @param {string} propOnParent Name of the expression property on parent
11
+ * @param {Array} e Expression to check - see module.exports
12
+ * @param {CSN.Path} path
13
+ */
14
+ function navigationIntoMany( parent, propOnParent, e, path ) {
15
+ applyTransformationsOnNonDictionary(parent, propOnParent, {
16
+ ref: (_parent, _prop, ref, _path) => {
17
+ const itemNavigationIndex = _parent._links?.findIndex(l => l.art.items);
18
+ if (itemNavigationIndex !== -1 && _parent.ref.length > itemNavigationIndex + 1)
19
+ this.message('ref-unexpected-many-navigation', _path);
20
+ },
21
+ }, { skipStandard: { type: true } }, path);
22
+ }
23
+
24
+ module.exports = {
25
+ columns: navigationIntoMany,
26
+ from: navigationIntoMany,
27
+ on: navigationIntoMany,
28
+ having: navigationIntoMany,
29
+ groupBy: navigationIntoMany,
30
+ orderBy: navigationIntoMany,
31
+ where: navigationIntoMany,
32
+ xpr: navigationIntoMany,
33
+ };
@@ -182,10 +182,13 @@ function requireForeignKeyAccess( parent, refIndex, noForeignKeyCallback ) {
182
182
  }
183
183
  else {
184
184
  // For cases where `Association to T { struct.one, struct.two };` is used instead of `{ struct }`.
185
- // Note: We know that `{ struct, struct.one }` is not possible, so no prefix check required.
185
+ // We know that `{ struct, struct.one }` is not possible, so no prefix check required.
186
+ // If `ref.length` does not cover any full foreign key, then we call noForeignKeyCallback()
187
+ // as well. This could happen for `struct.one` as foreign key, and `assoc.struct = …`
188
+ // in ON-condition.
186
189
  let fkIndex = 0;
187
190
  let success = false;
188
- while (!success && possibleKeys.length > 0) {
191
+ while (!success && possibleKeys.length > 0 && refIndex + fkIndex + 1 < ref.length) {
189
192
  const pathStep = ref[refIndex + fkIndex + 1].id || ref[refIndex + fkIndex + 1];
190
193
 
191
194
  // Function is immediately executed, before next iteration of loop. Access is fine.
@@ -76,7 +76,7 @@ function _hasForeignKeyOrElements( def ) {
76
76
  * @param {boolean} inColumns True if the ref is part of a from
77
77
  */
78
78
  function checkQueryRef( obj, inColumns ) {
79
- if (!obj || obj.$scope === 'alias')
79
+ if (!obj)
80
80
  return;
81
81
 
82
82
  if (obj.expand || obj.inline)
@@ -179,8 +179,7 @@ function _checkRef( ref, _links, $path, inColumns ) {
179
179
 
180
180
  // check managed association to have foreign keys array filled
181
181
  if (art.keys && !hasForeignKeys.call(this, art)) {
182
- this.error(null, $path, { id: pathStep, elemref: { ref } },
183
- 'Path step $(ID) of $(ELEMREF) has no foreign keys');
182
+ this.error('expr-missing-foreign-key', $path, { id: pathStep, elemref: { ref } } );
184
183
  break; // only one error per path
185
184
  }
186
185
  else if (art.on) {