@sap/cds-compiler 6.0.14 → 6.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 (61) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/bin/cdsc.js +6 -2
  3. package/bin/cdsse.js +1 -1
  4. package/bin/cdsv2m.js +1 -1
  5. package/lib/api/main.js +29 -7
  6. package/lib/api/options.js +2 -0
  7. package/lib/base/builtins.js +9 -0
  8. package/lib/base/keywords.js +1 -1
  9. package/lib/base/message-registry.js +5 -3
  10. package/lib/base/messages.js +3 -3
  11. package/lib/base/model.js +1 -0
  12. package/lib/base/node-helpers.js +10 -2
  13. package/lib/base/optionProcessorHelper.js +7 -2
  14. package/lib/checks/assocOutsideService.js +3 -1
  15. package/lib/checks/featureFlags.js +4 -1
  16. package/lib/compiler/assert-consistency.js +3 -1
  17. package/lib/compiler/base.js +1 -1
  18. package/lib/compiler/builtins.js +1 -1
  19. package/lib/compiler/checks.js +38 -21
  20. package/lib/compiler/define.js +24 -5
  21. package/lib/compiler/extend.js +1 -1
  22. package/lib/compiler/finalize-parse-cdl.js +9 -1
  23. package/lib/compiler/generate.js +4 -4
  24. package/lib/compiler/index.js +10 -1
  25. package/lib/compiler/lsp-api.js +2 -0
  26. package/lib/compiler/populate.js +8 -8
  27. package/lib/compiler/propagator.js +1 -1
  28. package/lib/compiler/resolve.js +15 -14
  29. package/lib/compiler/shared.js +6 -7
  30. package/lib/compiler/tweak-assocs.js +6 -6
  31. package/lib/compiler/utils.js +9 -16
  32. package/lib/compiler/xpr-rewrite.js +2 -2
  33. package/lib/gen/BaseParser.js +43 -37
  34. package/lib/gen/CdlGrammar.checksum +1 -1
  35. package/lib/gen/CdlParser.js +1424 -1433
  36. package/lib/gen/Dictionary.json +1 -7
  37. package/lib/gen/cdlKeywords.json +26 -0
  38. package/lib/inspect/inspectPropagation.js +1 -1
  39. package/lib/json/from-csn.js +2 -2
  40. package/lib/json/to-csn.js +9 -5
  41. package/lib/language/multiLineStringParser.js +1 -1
  42. package/lib/main.d.ts +10 -2
  43. package/lib/model/cloneCsn.js +1 -0
  44. package/lib/optionProcessor.js +13 -7
  45. package/lib/parsers/AstBuildingParser.js +24 -21
  46. package/lib/parsers/identifiers.js +2 -30
  47. package/lib/render/toCdl.js +63 -9
  48. package/lib/render/toSql.js +127 -108
  49. package/lib/render/utils/sql.js +67 -0
  50. package/lib/transform/addTenantFields.js +4 -4
  51. package/lib/transform/db/killAnnotations.js +1 -0
  52. package/lib/transform/db/processSqlServices.js +20 -2
  53. package/lib/transform/draft/db.js +1 -1
  54. package/lib/transform/draft/odata.js +14 -4
  55. package/lib/transform/forOdata.js +91 -2
  56. package/lib/transform/forRelationalDB.js +1 -1
  57. package/lib/transform/odata/flattening.js +1 -1
  58. package/lib/transform/transformUtils.js +2 -2
  59. package/lib/transform/translateAssocsToJoins.js +2 -26
  60. package/lib/utils/moduleResolve.js +1 -1
  61. package/package.json +2 -2
@@ -3231,13 +3231,6 @@
3231
3231
  "ValidationFunction": "Common.QualifiedName"
3232
3232
  }
3233
3233
  },
3234
- "Common.DraftUserAccessType": {
3235
- "$kind": "ComplexType",
3236
- "Properties": {
3237
- "UserAccessRole": "Edm.String",
3238
- "UserID": "Edm.String"
3239
- }
3240
- },
3241
3234
  "Common.EffectType": {
3242
3235
  "$deprecated": true,
3243
3236
  "$deprecationText": "All side effects are essentially value changes, differentiation not needed.",
@@ -3513,6 +3506,7 @@
3513
3506
  "Label": "Edm.String",
3514
3507
  "Parameters": "Collection(Common.ValueListParameter)",
3515
3508
  "PresentationVariantQualifier": "Core.SimpleIdentifier",
3509
+ "RelativeCollectionPath": "Edm.NavigationPropertyPath",
3516
3510
  "SearchSupported": "Edm.Boolean",
3517
3511
  "SelectionVariantQualifier": "Core.SimpleIdentifier"
3518
3512
  }
@@ -0,0 +1,26 @@
1
+ {
2
+ "reserved": [
3
+ "ALL",
4
+ "ANY",
5
+ "AS",
6
+ "BY",
7
+ "CASE",
8
+ "CAST",
9
+ "DISTINCT",
10
+ "EXISTS",
11
+ "FALSE",
12
+ "FROM",
13
+ "IN",
14
+ "KEY",
15
+ "NOT",
16
+ "NULL",
17
+ "OF",
18
+ "ON",
19
+ "SELECT",
20
+ "SOME",
21
+ "TRUE",
22
+ "WHEN",
23
+ "WHERE",
24
+ "WITH"
25
+ ]
26
+ }
@@ -36,7 +36,7 @@ function inspectPropagation( xsn, options, artifactName ) {
36
36
 
37
37
  if (!artifactXsn) {
38
38
  error(null, null, { name: artifactName },
39
- // eslint-disable-next-line @stylistic/js/max-len
39
+ // eslint-disable-next-line @stylistic/max-len
40
40
  'Artifact $(NAME) not found, only top-level artifacts and their elements are supported for now');
41
41
  return null;
42
42
  }
@@ -137,7 +137,7 @@ const typeProperties = [
137
137
  // do not include CSN v0.1.0 properties here:
138
138
  'target', 'elements', 'enum', 'items',
139
139
  'cardinality', // for association publishing in views
140
- 'type', 'length', 'precision', 'scale', 'srid', 'localized', 'notNull',
140
+ 'type', 'length', 'precision', 'scale', 'srid', 'localized', 'notNull', 'default',
141
141
  'keys', 'on', // only with 'target'
142
142
  ];
143
143
  const exprProperties = [
@@ -1142,7 +1142,7 @@ function elementsDict( def, spec, xsn ) {
1142
1142
  return elements;
1143
1143
  warning( 'syntax-expecting-returns', elements[$location],
1144
1144
  { prop: 'elements', parentprop: 'returns' },
1145
- // eslint-disable-next-line @stylistic/js/max-len
1145
+ // eslint-disable-next-line @stylistic/max-len
1146
1146
  'Expecting property $(PROP) to be put into an object for property $(PARENTPROP) when annotating action return structures' );
1147
1147
  xsn.returns = { kind: 'annotate', elements, location: elements[$location] };
1148
1148
  return undefined;
@@ -201,7 +201,7 @@ const propertyOrder = (function orderPositions() {
201
201
  const typeProperties = [
202
202
  'target', 'elements', 'enum', 'items',
203
203
  'cardinality', // for association publishing in views
204
- 'type', 'length', 'precision', 'scale', 'srid', 'localized', 'notNull',
204
+ 'type', 'length', 'precision', 'scale', 'srid', 'localized', 'notNull', 'default',
205
205
  'foreignKeys', 'on', // for explicit ON/keys with REDIRECTED
206
206
  '$typeArgs', // for unresolved type arguments, e.g. through parseCql
207
207
  ];
@@ -1319,11 +1319,15 @@ function flattenInternalXpr( array, xprOp ) {
1319
1319
  function ternaryOperator( node ) {
1320
1320
  const rargs = [
1321
1321
  'case',
1322
- 'when', exprInternal(node.args[0]),
1323
- 'then', exprInternal(node.args[2]),
1324
- 'else', exprInternal(node.args[4]),
1325
- 'end',
1322
+ 'when', exprInternal( node.args[0] ),
1323
+ 'then', exprInternal( node.args[2] ),
1326
1324
  ];
1325
+ let right = node.args[4];
1326
+ for (; right.op?.val === '?:' && !right.$parens?.length; right = right.args[4]) {
1327
+ rargs.push( 'when', exprInternal( right.args[0] ),
1328
+ 'then', exprInternal( right.args[2] ) );
1329
+ }
1330
+ rargs.push( 'else', exprInternal( right ), 'end' );
1327
1331
 
1328
1332
  if (node.$parens?.length)
1329
1333
  return { xpr: flattenInternalXpr( rargs, 'xpr' ) };
@@ -77,7 +77,7 @@ class MultiLineStringParser {
77
77
  this.str = token.text; // Copy because .text is a getter
78
78
 
79
79
  if (this.str[0] !== '`' || this.str[this.str.length - 1] !== '`')
80
- // eslint-disable-next-line @stylistic/js/max-len
80
+ // eslint-disable-next-line @stylistic/max-len
81
81
  throw new CompilerAssertion('Invalid multi-line string sequence: Require string to be surrounded by back-ticks!');
82
82
 
83
83
  this.output = [];
package/lib/main.d.ts CHANGED
@@ -158,6 +158,16 @@ declare namespace compiler {
158
158
  * @since v4.2.0
159
159
  */
160
160
  moduleLookupDirectories?: string[]
161
+ /**
162
+ * An [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)
163
+ * that can be used to abort the compilation.
164
+ * Used for any `async` task, i.e. at the moment for reading/parsing files.
165
+ *
166
+ * Note that this flag has no effect on _synchronous_ compilation functions.
167
+ *
168
+ * @since v6.1
169
+ */
170
+ abortSignal?: AbortSignal
161
171
  /**
162
172
  * Option for {@link compileSources}. If set, all objects inside the
163
173
  * provided sources dictionary are interpreted as XSN structures instead
@@ -1558,8 +1568,6 @@ declare namespace compiler {
1558
1568
  constructor(location: Location, msg: string, severity?: MessageSeverity, id?: string | null, home?: string | null, moduleName?: string | null);
1559
1569
  /**
1560
1570
  * Optional ID of the message. Can be used to reclassify messages.
1561
- *
1562
- * @note This property is non-enumerable as message IDs are not finalized, yet.
1563
1571
  */
1564
1572
  messageId?: string
1565
1573
 
@@ -39,6 +39,7 @@ const internalCsnProps = {
39
39
  $notNull: shallowCopy, // used for HANA CSN migrations
40
40
  $sqlService: shallowCopy,
41
41
  $dummyService: shallowCopy,
42
+ $dataProductService: shallowCopy,
42
43
  };
43
44
  const internalEnumerableCsnProps = {
44
45
  __proto__: null,
@@ -1,6 +1,6 @@
1
1
  // Compiler options
2
2
 
3
- /* eslint @stylistic/js/max-len: 0 */
3
+ /* eslint @stylistic/max-len: 0 */
4
4
 
5
5
  // Remarks:
6
6
  // - The specification is client-tool centric (bin/cdsc.js):
@@ -169,7 +169,8 @@ optionProcessor
169
169
  Q, toSql [options] <files...> Generate SQL DDL statements
170
170
  J, forJava [options] <files...> Generate CSN for the Java Runtime
171
171
  toCsn [options] <files...> (default) Generate original model as CSN
172
- parseCdl [options] <file> Generate a CSN that is close to the CDL source.
172
+ parse [options] <file> Parse the input file. For CDL input, generate a CSN that is
173
+ close to the CDL source.
173
174
  explain <message-id> Explain a compiler message.
174
175
  parseOnly [options] <files...> (internal) Stop compilation after parsing, write messages to <stderr>,
175
176
  per default no output.
@@ -263,6 +264,8 @@ optionProcessor.command('O, toOdata')
263
264
  .option(' --odata-v2-partial-constr')
264
265
  .option(' --odata-vocabularies <list>')
265
266
  .option(' --odata-no-creator')
267
+ .option(' --draft-messages')
268
+ .option(' --add-annotation-AddressViaNavigationPath')
266
269
  .option('-c, --csn')
267
270
  .option('-f, --odata-format <format>', { valid: [ 'flat', 'structured' ] })
268
271
  .option('-n, --sql-mapping <style>', { valid: [ 'plain', 'quoted', 'hdbcds' ], aliases: [ '--names' ] })
@@ -297,7 +300,10 @@ optionProcessor.command('O, toOdata')
297
300
  --odata-vocabularies <list> JSON array of adhoc vocabulary definitions
298
301
  { prefix: { alias, ns, uri }, ... }
299
302
  --odata-no-creator Omit creator identification in API
300
- -n, --sql-mapping <style> Annotate artifacts and elements with "@cds.persistence.name", which is
303
+ --draft-messages Add draft messages as part of the draft creation
304
+ --add-annotation-AddressViaNavigationPath Add annotation "@Common.AddressViaNavigationPath" to the services
305
+ containing draft enabled entitties
306
+ -n, --sql-mapping <style> Annotate artifacts and elements with "@cds.persistence.name", which is
301
307
  the corresponding database name (see "--sql-mapping" for "toSql")
302
308
  plain : (default) Names in uppercase and flattened with underscores
303
309
  quoted : Names in original case as in CDL. Entity names with dots,
@@ -529,15 +535,15 @@ optionProcessor.command('toCsn')
529
535
  --with-localized Add localized convenience views to the CSN output.
530
536
  `);
531
537
 
532
- optionProcessor.command('parseCdl')
538
+ optionProcessor.command('parse', { aliases: [ 'parseCdl' ] })
533
539
  .option('-h, --help')
534
540
  .positionalArgument('<file>')
535
541
  .option(' --with-locations')
536
542
  .help(`
537
- Usage: cdsc parseCdl [options] <file>
543
+ Usage: cdsc parse [options] <file>
538
544
 
539
- Only parse the CDL and output a CSN that is close to the source. Does not
540
- resolve imports, apply extensions or expand any queries.
545
+ Only parse the input file. For CDL input, output a CSN that is close to the source.
546
+ Does not resolve imports, apply extensions or expand any queries.
541
547
 
542
548
  Options
543
549
  --with-locations Add $location to CSN artifacts.
@@ -47,7 +47,11 @@ const extensionsCode = {
47
47
  const PRECEDENCE_OF_EQUAL = 10;
48
48
 
49
49
  class AstBuildingParser extends BaseParser {
50
- leanConditions = { afterBrace: true, atRightParen: true, fail: true };
50
+ leanConditions = {
51
+ afterBrace: true,
52
+ atRightParen: true,
53
+ fail: true,
54
+ };
51
55
 
52
56
  constructor( lexer, keywords, table, options, messageFunctions ) {
53
57
  super( lexer, keywords, table ); // lexer has file
@@ -115,13 +119,16 @@ class AstBuildingParser extends BaseParser {
115
119
 
116
120
  // Guards, Prepare Commands and Lookahead Methods -----------------------------
117
121
 
118
- tableWithoutAs() { // not used in <guard=…>, only called by other guard
119
- // TODO TOOL: if the tool properly creates `default: this.giR()`, this
120
- // condition method is most likely not necessary
121
- const { keyword } = this.la();
122
- // TODO: if necessary, we could allow some keywords, and just make sure that
123
- // all JOIN variants are still possible
124
- return keyword && this.keywords[keyword] != null;
122
+ queryOnLeftSloppyAlias( _arg, mode ) {
123
+ if (mode === 'M' || this.isNoKeywordInRuleFollow( _arg, mode ))
124
+ return true;
125
+ // TODO TOOL: have a base parser method for the test
126
+ if (this.conditionTokenIdx === this.tokenIdx && // tested on same
127
+ this.conditionStackLength == null) // after error recovery
128
+ return false;
129
+ if (this.constructor.tracingParser)
130
+ this._traceSubPush( 'queryOnLeft' );
131
+ return this.queryOnLeft( 'table', mode );
125
132
  }
126
133
 
127
134
  /**
@@ -134,15 +141,9 @@ class AstBuildingParser extends BaseParser {
134
141
  * - <guard=queryOnLeft> tests whether the expression on the left is a query
135
142
  * - <guard=queryOnLeft, arg=‹SomeVal›>: tests whether the expression on the
136
143
  * left is a query, then make the current context to be not a query anymore
137
- * - <guard=queryOnLeft, arg=tableWithoutAs>: …after having checked
138
- * whether the next token is no (reserved or unreserved) keyword
139
144
  */
140
145
  queryOnLeft( arg, test ) {
141
- if (arg === 'tableWithoutAs') {
142
- if (this.tableWithoutAs())
143
- return true;
144
- }
145
- else if (!arg && !test) {
146
+ if (!arg && !test) {
146
147
  // provide new dynamic parentheses context, except with direct
147
148
  // recursive call:
148
149
  if (this.inSameRule_( this.s, this.stack.at( -1 ).followState ))
@@ -570,7 +571,7 @@ class AstBuildingParser extends BaseParser {
570
571
  if (this.l() === ':') {
571
572
  this.warning( 'syntax-missing-parens', name,
572
573
  { code: '@‹anno›', op: ':', newcode: '@(‹anno›…)' },
573
- // eslint-disable-next-line @stylistic/js/max-len
574
+ // eslint-disable-next-line @stylistic/max-len
574
575
  'When $(CODE) is followed by $(OP), use $(NEWCODE) for annotation assignments at this position' );
575
576
  }
576
577
  }
@@ -582,7 +583,7 @@ class AstBuildingParser extends BaseParser {
582
583
  // do not report error if the '@' is not correct:
583
584
  this.s !== null && this.tokenIdx > this.recoverTokenIdx) {
584
585
  this.warning( 'syntax-missing-semicolon', next, { code: ';' },
585
- // eslint-disable-next-line @stylistic/js/max-len
586
+ // eslint-disable-next-line @stylistic/max-len
586
587
  'Add a $(CODE) and/or newline before the annotation assignment to indicate that it belongs to the next statement' );
587
588
  }
588
589
  }
@@ -737,7 +738,7 @@ class AstBuildingParser extends BaseParser {
737
738
  }
738
739
  else if (text.charAt(0) !== '!') {
739
740
  this.message( 'syntax-deprecated-ident', location, { delimited: id },
740
- // eslint-disable-next-line @stylistic/js/max-len
741
+ // eslint-disable-next-line @stylistic/max-len
741
742
  'Deprecated delimited identifier syntax, use $(DELIMITED) - strings are delimited by single quotes' );
742
743
  }
743
744
  }
@@ -948,7 +949,7 @@ class AstBuildingParser extends BaseParser {
948
949
  {
949
950
  std: 'Annotation number $(RAWVALUE) is put as $(VALUE) into the CSN',
950
951
  rounded: 'Annotation number $(RAWVALUE) is rounded to $(VALUE)',
951
- // eslint-disable-next-line @stylistic/js/max-len
952
+ // eslint-disable-next-line @stylistic/max-len
952
953
  infinite: 'Annotation value $(RAWVALUE) is infinite as number and put as string into the CSN',
953
954
  } );
954
955
  }
@@ -1000,7 +1001,7 @@ class AstBuildingParser extends BaseParser {
1000
1001
 
1001
1002
  // TODO: can we remove `;`/EOF from the expected-set for `annotate Foo with ⎀`?
1002
1003
  checkWith( keyword ) {
1003
- if (this.lb() !== keyword)
1004
+ if (this.lb() !== keyword || ![ ';', '}', 'EOF' ].includes( this.l() ))
1004
1005
  return;
1005
1006
  const tok = this.la();
1006
1007
  const docTokenIndex = this.docCommentIndex &&
@@ -1012,7 +1013,7 @@ class AstBuildingParser extends BaseParser {
1012
1013
  const expecting = this.expectingArray().filter( t => t !== '<EOF>' && t !== '\'}\'' );
1013
1014
  const msg = this.warning( 'syntax-unexpected-semicolon', tok,
1014
1015
  { offending: this.antlrName( tok ), expecting, keyword: 'with' },
1015
- // eslint-disable-next-line @stylistic/js/max-len
1016
+ // eslint-disable-next-line @stylistic/max-len
1016
1017
  'Unexpected $(OFFENDING), expecting $(EXPECTING) - ignored previous $(KEYWORD)' );
1017
1018
  msg.expectedTokens = expecting;
1018
1019
  }
@@ -1523,6 +1524,8 @@ class AstBuildingParser extends BaseParser {
1523
1524
  }
1524
1525
  }
1525
1526
 
1527
+ AstBuildingParser.prototype.queryOnLeftSloppyAlias.afterError = true;
1528
+
1526
1529
  function addOneForDefinition( count, ext ) {
1527
1530
  return (ext.kind === 'extend') ? count : count + 1;
1528
1531
  }
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const cdlKeywords = require('../gen/cdlKeywords.json').reserved;
4
+
3
5
  /** RegEx identifying undelimited identifiers in CDL */
4
6
  const undelimitedIdentifierRegex = /^[$_\p{ID_Start}][$\p{ID_Continue}\u200C\u200D]*$/u;
5
7
 
@@ -13,36 +15,6 @@ const functionsWithoutParentheses = [
13
15
  'CURRENT_USER', 'SESSION_USER', 'SYSTEM_USER',
14
16
  ];
15
17
 
16
- // CDL reserved keywords, used for automatic quoting in 'toCdl' renderer
17
- // TODO: Use `parser.keywords` from our generated CdlParser.js (#13856)
18
- const cdlKeywords = [
19
- 'ALL',
20
- 'ANY',
21
- 'AS',
22
- 'BY',
23
- 'CASE',
24
- 'CAST',
25
- 'DISTINCT',
26
- 'EXISTS',
27
- 'EXTRACT',
28
- 'FALSE', // boolean
29
- 'FROM',
30
- 'IN',
31
- 'KEY',
32
- 'NEW',
33
- 'NOT',
34
- 'NULL',
35
- 'OF',
36
- 'ON',
37
- 'SELECT',
38
- 'SOME',
39
- 'TRIM',
40
- 'TRUE', // boolean
41
- 'WHEN',
42
- 'WHERE',
43
- 'WITH',
44
- ];
45
-
46
18
  function isSimpleCdlIdentifier( id ) {
47
19
  if (undelimitedIdentifierRegex.test(id))
48
20
  return true;
@@ -1439,7 +1439,7 @@ class CsnToCdl {
1439
1439
  if (col.cast.target && !col.cast.type)
1440
1440
  result += ` : ${ this.renderRedirectedTo(col.cast, env) }`;
1441
1441
  else
1442
- result += ` : ${ this.renderTypeReferenceAndProps(col.cast, env, { typeRefOnly: true, noAnnoCollect: true }) }`;
1442
+ result += ` : ${ this.renderTypeReferenceAndProps(col.cast, env, { noAnnoCollect: true }) }`;
1443
1443
  env.path.length -= 1;
1444
1444
  }
1445
1445
  return result;
@@ -1869,9 +1869,12 @@ class CsnToCdl {
1869
1869
  if (artifact.on)
1870
1870
  result += ` on ${ this.exprRenderer.renderExpr(artifact.on, env.withSubPath([ 'on' ])) }`;
1871
1871
 
1872
- // Foreign keys (if any, unless we also have an ON_condition (which means we have been transformed from managed to unmanaged)
1873
- if (artifact.keys && !artifact.on)
1874
- result += ` ${ this.renderForeignKeys(artifact, env) }`;
1872
+ // Foreign keys (if any, unless we also have an ON-condition (which means we have been transformed from managed to unmanaged)
1873
+ if (artifact.keys && !artifact.on) {
1874
+ const keys = this.renderForeignKeys(artifact, env, false);
1875
+ if (keys)
1876
+ result += ` ${ keys }`;
1877
+ }
1875
1878
 
1876
1879
  if (!artifact.on) {
1877
1880
  // unmanaged associations can't be followed by "not null" or "default"
@@ -1923,10 +1926,14 @@ class CsnToCdl {
1923
1926
  */
1924
1927
  renderRedirectedTo( art, env ) {
1925
1928
  let result = `redirected to ${ this.renderDefinitionReference(art.target, env) }`;
1926
- if (art.on)
1929
+ if (art.on) {
1927
1930
  result += ` on ${ this.exprRenderer.renderExpr(art.on, env.withSubPath([ 'on' ])) }`;
1928
- else if (art.keys)
1929
- result += ` ${ this.renderForeignKeys(art, env) }`;
1931
+ }
1932
+ else if (art.keys) {
1933
+ const keys = this.renderForeignKeys(art, env, true);
1934
+ if (keys)
1935
+ result += ` ${ keys }`;
1936
+ }
1930
1937
  return result;
1931
1938
  }
1932
1939
 
@@ -2274,13 +2281,16 @@ class CsnToCdl {
2274
2281
  }
2275
2282
 
2276
2283
  /**
2277
- * Render foreign keys.
2284
+ * Render foreign keys. We only render foreign keys if necessary or if we can't say for sure
2285
+ * that the foreign keys would match the implicitly generated ones.
2278
2286
  *
2279
2287
  * @param {object} art
2280
2288
  * @param {CdlRenderEnvironment} env
2289
+ * @param {boolean} alwaysRenderForeignKeys
2290
+ * If false, only render foreign keys if necessary (i.e. can't be inferred by compiler again).
2281
2291
  * @return {string}
2282
2292
  */
2283
- renderForeignKeys( art, env ) {
2293
+ renderForeignKeys( art, env, alwaysRenderForeignKeys = false ) {
2284
2294
  const renderedKeys = [];
2285
2295
  let hasAnnotations = false;
2286
2296
  env = env.withSubPath([ 'keys', -1 ]);
@@ -2301,18 +2311,62 @@ class CsnToCdl {
2301
2311
  renderedKeys.push(`${ key }${ alias },`);
2302
2312
  }
2303
2313
 
2314
+ // Annotations on foreign keys always require rendering of explicit keys.
2315
+ // Otherwise, we'd have to use annotate statements here.
2304
2316
  if (hasAnnotations) {
2305
2317
  const sep = `\n${ env.indent }`;
2306
2318
  env.decreaseIndent();
2307
2319
  return `{${ sep }${ renderedKeys.join(sep) }\n${ env.indent }}`;
2308
2320
  }
2309
2321
 
2322
+ if (!alwaysRenderForeignKeys && this.foreignKeysMatchImplicitOnes( art ))
2323
+ return '';
2324
+
2310
2325
  let result = renderedKeys.join(' ');
2311
2326
  if (result[result.length - 1] === ',') // remove trailing comma
2312
2327
  result = result.slice(0, -1);
2313
2328
  return `{ ${ result } }`;
2314
2329
  }
2315
2330
 
2331
+ /**
2332
+ * Returns true, if `to.cdl()` could leave out explicit foreign keys in the rendered output
2333
+ * without changing recompilation. We do so, because explicit foreign keys are not promoted
2334
+ * on CAPire.
2335
+ *
2336
+ * @param {CSN.Artifact} art
2337
+ * @returns {boolean}
2338
+ */
2339
+ foreignKeysMatchImplicitOnes( art ) {
2340
+ if (art.cardinality && art.cardinality.max !== 1)
2341
+ return false; // e.g. to-many assocs
2342
+
2343
+ const target = this.csn.definitions[art.target];
2344
+ if (!art.keys?.length || !art.target || !target?.elements)
2345
+ return false;
2346
+
2347
+ // We could improve to.cdl() to properly check if keys match, but then we'd have to look
2348
+ // at sub-elements, structures, etc.; for now, we only check “simple” foreign keys that
2349
+ // don't have any alias.
2350
+ const hasComplexKeys = art.keys.some(key => key.ref?.length !== 1 || key.as !== undefined);
2351
+ if (hasComplexKeys)
2352
+ return false;
2353
+
2354
+ const targetKeys = Object.keys(target.elements).filter(name => target.elements[name]?.key);
2355
+ if (targetKeys.length === 0)
2356
+ return false; // always require explicit keys if there are no target side keys
2357
+
2358
+ const foreignKeys = art.keys.map(key => key.ref[0]);
2359
+ if (foreignKeys.length !== targetKeys.length)
2360
+ return false;
2361
+
2362
+ // We require the _same_ order! Otherwise, recompilation would change the generated foreign key order!
2363
+ for (let i = 0; i < foreignKeys.length; ++i) {
2364
+ if (foreignKeys[i] !== targetKeys[i])
2365
+ return false;
2366
+ }
2367
+ return true;
2368
+ }
2369
+
2316
2370
  /**
2317
2371
  * Render an explicit alias, e.g. for columns.
2318
2372
  *