@sap/cds-compiler 6.9.0 → 6.9.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.
package/CHANGELOG.md CHANGED
@@ -13,6 +13,29 @@ we might not list every change in its behavior here.
13
13
  Productive code should never require a `beta` flag to be set, and
14
14
  might use a deprecated flag only for a limited period of time.
15
15
 
16
+ ## Version 6.9.2 - 2026-05-08
17
+
18
+ ### Bug Fixes
19
+
20
+ - **api:** when the environment variable `CDSC_TRACE_API` is set,
21
+ the compiler writes a trace for calls of API functions;
22
+ it now has more information, and also traces the exit of the API function.
23
+
24
+
25
+
26
+ ## Version 6.9.1 - 2026-05-05
27
+
28
+ ### Bug Fixes
29
+
30
+ - **compiler:**
31
+ + make an element added via `extend` correctly shadow an element from an include
32
+ + do not issue a warning for a correct use of `$projection`
33
+ - **odata:**
34
+ + do not generate wrong ReferentialConstraints for unmanaged Composition without a partner
35
+ + render Partner attribute on forward association correctly
36
+
37
+
38
+
16
39
  ## Version 6.9.0 - 2026-04-21
17
40
 
18
41
  ### Features
package/lib/api/main.js CHANGED
@@ -1169,7 +1169,7 @@ function publishCsnProcessor( processor, _name ) {
1169
1169
  * @returns {any} What ever the processor returns
1170
1170
  */
1171
1171
  function api( csn, options = {}, ...args ) {
1172
- trace.traceApi(_name, options);
1172
+ trace.call(_name, options, csn);
1173
1173
  const originalMessageLength = options.messages?.length;
1174
1174
  try {
1175
1175
  const messageFunctions = messages.makeMessageFunction(csn, options, _name);
@@ -1184,6 +1184,7 @@ function publishCsnProcessor( processor, _name ) {
1184
1184
  timetrace.timetrace.start(_name);
1185
1185
  const result = processor( csn, options, messageFunctions, ...args );
1186
1186
  timetrace.timetrace.stop(_name);
1187
+ trace.exit(_name, result);
1187
1188
  return result;
1188
1189
  }
1189
1190
  catch (err) {
@@ -1200,6 +1201,7 @@ function publishCsnProcessor( processor, _name ) {
1200
1201
  if (originalMessageLength !== undefined)
1201
1202
  options.messages.length = originalMessageLength;
1202
1203
 
1204
+ trace.log( 'recompile CSN and retry backend' );
1203
1205
  const messageFunctions = messages.makeMessageFunction( csn, options, _name );
1204
1206
  const recompileMsg = messageFunctions.info( 'api-recompiled-csn', location.emptyLocation('csn.json'), {},
1205
1207
  'CSN input had to be recompiled' );
@@ -1211,7 +1213,10 @@ function publishCsnProcessor( processor, _name ) {
1211
1213
  const xsn = compiler.recompileX(csn, options);
1212
1214
  const recompiledCsn = toCsn.compactModel(xsn);
1213
1215
  messageFunctions.setModel(recompiledCsn);
1214
- return processor( recompiledCsn, options, messageFunctions, ...args );
1216
+ const result = processor( recompiledCsn, options, messageFunctions, ...args );
1217
+ timetrace.timetrace.stop(_name);
1218
+ trace.exit(_name, result);
1219
+ return result;
1215
1220
  }
1216
1221
  }
1217
1222
  }
@@ -1138,7 +1138,6 @@ const centralMessageTexts = {
1138
1138
  'old-not-target': 'Expected element $(NAME) not to be an association, because it overrides the included element from $(ART)',
1139
1139
  },
1140
1140
 
1141
- 'ref-expecting-$self': 'Use $(NEWCODE) instead of $(CODE) here or remove $(CODE) altogether if possible; the compiler has rewritten it to $(NEWCODE) in CSN',
1142
1141
  'ref-expecting-assoc': {
1143
1142
  std: 'Expecting path $(ELEMREF) following “EXISTS” predicate to end with association/composition',
1144
1143
  'with-type': 'Expecting path $(ELEMREF) following “EXISTS” predicate to end with association/composition, found $(TYPE)',
@@ -16,6 +16,7 @@ const {
16
16
  const _messageIdsWithExplanation = require('../../share/messages/message-explanations.json').messages;
17
17
  const { analyseCsnPath, traverseQuery } = require('../base/csnRefs');
18
18
  const { CompilerAssertion } = require('./error');
19
+ const trace = require('./trace');
19
20
  const { getArtifactName } = require('../compiler/base');
20
21
  const { cdlNewLineRegEx } = require('../language/textUtils');
21
22
  const meta = require('./meta');
@@ -567,8 +568,10 @@ function makeMessageFunction( model, options, _moduleName = null ) {
567
568
  }
568
569
 
569
570
  function throwWithError() {
570
- if (hasNewError)
571
+ if (hasNewError) {
572
+ trace.log( `stop compilation with ${ messages.length } messages` );
571
573
  throw new CompilationError(messages, options.attachValidNames && model);
574
+ }
572
575
  }
573
576
 
574
577
  /**
@@ -583,8 +586,10 @@ function makeMessageFunction( model, options, _moduleName = null ) {
583
586
  if (!messages || !messages.length)
584
587
  return;
585
588
  const hasError = options.testMode ? hasNonDowngradableErrors : hasErrors;
586
- if (hasError( messages, moduleName, options ))
589
+ if (hasError( messages, moduleName, options )) {
590
+ trace.log( `stop compilation with ${ messages.length } messages` );
587
591
  throw new CompilationError(messages, options.attachValidNames && model);
592
+ }
588
593
  }
589
594
 
590
595
  /**
package/lib/base/trace.js CHANGED
@@ -5,33 +5,87 @@ const shouldTraceApi = process?.env?.CDSC_TRACE_API;
5
5
 
6
6
  /**
7
7
  * Placeholder for disabled tracing (no-op).
8
- *
9
- * @param {string} apiName API name
10
- * @param {object} options Options passed to the API.
11
- * @param {...any} [args] Arguments to be logged to stderr
12
8
  */
13
- // eslint-disable-next-line no-unused-vars
14
- function noOp( apiName, options, ...args ) {
9
+ function noOp() {
15
10
  // no-op
16
11
  }
17
12
 
18
13
  /**
19
- * Print args to stderr if CDSC_TRACE_API is set
20
- *
21
- * @param {string} apiName API name
22
- * @param {object} options Options passed to the API.
23
- * @param {...any} [args] Arguments to be logged to stderr
14
+ * Print trace info to stderr when calling an API function
24
15
  */
25
- function traceApi( apiName, options, ...args ) {
26
- const optStr = typeof options === 'object' ? JSON.stringify(options, null, 2) : options;
27
- const argsStr = args.map(val => JSON.stringify(val)).join(', ');
28
- const rest = args.length > 0 ? ` | ${ argsStr }` : '';
29
- // Local require: Only load on-demand, not when tracing is disabled.
16
+ function call( apiName, options, csn, files ) {
30
17
  const { version } = require('../../package.json');
18
+ const now = (new Date( Date.now() )).toISOString();
19
+ const args = (files || csn)
20
+ ? `${ optionsString( options ) } on ${ filesInfo( files ) || csnInfo( csn ) }`
21
+ : optionsString( options );
31
22
  // eslint-disable-next-line no-console
32
- console.error( `CDSC_TRACE_API | ${ version } | ${ apiName }() | options: ${ optStr }${ rest }`);
23
+ console.error( 'CDSC_TRACE_API: at %s, call %s() of v%s with options %s',
24
+ now, apiName, version, args );
33
25
  }
34
26
 
35
- module.exports = {
36
- traceApi: shouldTraceApi ? traceApi : noOp,
37
- };
27
+ /**
28
+ * Print trace info to stderr when exiting an API function
29
+ */
30
+ function exit( apiName, result ) {
31
+ const now = (new Date( Date.now() )).toISOString();
32
+ const info = (result?.definitions || result?.extensions)
33
+ ? csnInfo( result )
34
+ : `a result of type ${ typeof result }`;
35
+ // eslint-disable-next-line no-console
36
+ console.error( 'CDSC_TRACE_API: at %s, exit %s() and return %s',
37
+ now, apiName, info );
38
+ }
39
+
40
+ /**
41
+ * Print trace info to stderr for miscellaneous use cases
42
+ */
43
+ function log( info ) {
44
+ const now = (new Date( Date.now() )).toISOString();
45
+ // eslint-disable-next-line no-console
46
+ console.error( 'CDSC_TRACE_API: at %s, %s', now, info );
47
+ }
48
+
49
+ function optionsString( obj ) {
50
+ if (!obj || typeof obj !== 'object')
51
+ return obj.toString();
52
+ try {
53
+ if (Array.isArray( obj.messages ) && obj.messages.length) {
54
+ const messages = {};
55
+ for (const msg of obj.messages)
56
+ messages[msg.severity] = (messages[msg.severity] || 0) + 1;
57
+ obj = { ...obj, messages };
58
+ }
59
+ return JSON.stringify( obj, null, 2 );
60
+ }
61
+ catch (err) {
62
+ return err.toString();
63
+ }
64
+ }
65
+
66
+ function filesInfo( files ) {
67
+ if (!files)
68
+ return files;
69
+ if (!Array.isArray( files ))
70
+ files = Object.keys( files );
71
+ return files.length ? [ 'files', ...files ].join( '\n ') : 'no files';
72
+ }
73
+
74
+ function csnInfo( csn ) {
75
+ if (!csn || typeof csn !== 'object' || Array.isArray( csn ))
76
+ return `some value of type ${ typeof csn }`;
77
+ try {
78
+ JSON.stringify( csn );
79
+ const defs = csn.definitions ? Object.keys( csn.definitions ).length : 'no';
80
+ const exts = csn.extensions ? csn.extensions.length : 'no';
81
+ const flavor = csn.meta?.flavor || csn.meta?.compilerCsnFlavor || 'unknown';
82
+ return `a CSN of flavor '${ flavor }' with ${ defs } definitions and ${ exts } extensions`;
83
+ }
84
+ catch (err) {
85
+ return `a CORRUPTED CSN (${ err.toString() })`;
86
+ }
87
+ }
88
+
89
+ module.exports = (shouldTraceApi)
90
+ ? { call, exit, log }
91
+ : { call: noOp, exit: noOp, log: noOp };
@@ -1173,7 +1173,8 @@ function extend( model ) {
1173
1173
  }
1174
1174
  if (art._extensions?.$add)
1175
1175
  extendArtifact( art._extensions.$add, art );
1176
- // TODO: for proper shadowing: first collect defined element & action names
1176
+ checkRedefinitionThroughIncludes( art, 'elements' );
1177
+ checkRedefinitionThroughIncludes( art, 'actions' );
1177
1178
  }
1178
1179
 
1179
1180
  /**
@@ -1497,6 +1498,9 @@ function extend( model ) {
1497
1498
  }
1498
1499
 
1499
1500
  const existing = parent[prop]?.[name];
1501
+ const shadowsIncludeMember = construct !== parent &&
1502
+ elem.$inferred !== 'include' &&
1503
+ existing?.$inferred === 'include';
1500
1504
  const add = construct !== parent && (!existing || elem.$inferred !== 'include');
1501
1505
  if (!add && existing?.$inferred === 'include' && elem.$inferred === 'include') {
1502
1506
  includeCollisions.push( {
@@ -1507,9 +1511,17 @@ function extend( model ) {
1507
1511
  const { $duplicates } = elem;
1508
1512
  if ($duplicates === true && add)
1509
1513
  elem.$duplicates = null;
1510
- setMemberParent( elem, name, parent, add && prop );
1511
- if (!$duplicates) // not already reported
1512
- checkRedefinition( elem );
1514
+ if (add && elem.$inferred === 'include' && Array.isArray( $duplicates ))
1515
+ elem.$duplicates = null;
1516
+ if (shadowsIncludeMember) {
1517
+ parent[prop][name] = elem;
1518
+ setMemberParent( elem, name, parent );
1519
+ }
1520
+ else {
1521
+ setMemberParent( elem, name, parent, add && prop );
1522
+ if (!$duplicates) // not already reported
1523
+ checkRedefinition( elem );
1524
+ }
1513
1525
  initMembers( elem, elem, elem._block );
1514
1526
  if (elem.kind === 'action' || elem.kind === 'function')
1515
1527
  initBoundSelfParam( elem.params, elem._main );
@@ -1740,9 +1752,6 @@ function extend( model ) {
1740
1752
  } );
1741
1753
  }
1742
1754
  }
1743
-
1744
- checkRedefinitionThroughIncludes( parent, prop );
1745
-
1746
1755
  if (!hasNewElement && members) {
1747
1756
  ext[prop] = members;
1748
1757
  }
@@ -1788,7 +1797,8 @@ function extend( model ) {
1788
1797
 
1789
1798
  /**
1790
1799
  * Report duplicates in parent[prop] that happen due to multiple includes having the
1791
- * same member. Covers `entity G : E, G {};` but not `entity G : E {}; extend G with F;`.
1800
+ * same member. Run after includes and extends so shadowing members have already
1801
+ * replaced any include-derived survivors in-place.
1792
1802
  */
1793
1803
  function checkRedefinitionThroughIncludes( parent, prop ) {
1794
1804
  if (!parent[prop])
@@ -984,8 +984,6 @@ function fns( model ) {
984
984
  art.kind === '$self' && path[0].id === '$projection') {
985
985
  // Rewrite $projection to $self
986
986
  path[0].id = '$self';
987
- warning( 'ref-expecting-$self', [ path[0].location, user ],
988
- { code: '$projection', newcode: '$self' });
989
987
  }
990
988
  return art.name?.$inferred !== '$internal'; // not a compiler-generated internal alias
991
989
  }
@@ -472,8 +472,10 @@ function csn2edmAll( _csn, _options, serviceNames, messageFunctions ) {
472
472
 
473
473
  navigationProperties.forEach((np) => {
474
474
  if (options.isV4()) {
475
- // V4: No referential constraints for Containment Relationships
476
- if ((!np.isContainment() || (options.renderForeignKeys)) && !np.isToMany())
475
+ // V4: No referential constraints for Containment Relationships or
476
+ // unmanaged Compositions without a partner
477
+ const isUnmanagedCompositionWithoutPartner = np._csn.type === 'cds.Composition' && np._csn.on && !np._csn._constraints._partnerCsn;
478
+ if ((!np.isContainment() || (options.renderForeignKeys)) && !np.isToMany() && !isUnmanagedCompositionWithoutPartner)
477
479
  np.addReferentialConstraintNodes();
478
480
  }
479
481
  else {
package/lib/edm/edm.js CHANGED
@@ -1053,11 +1053,14 @@ function getEdm( options ) {
1053
1053
  delete this._edmAttributes.Nullable;
1054
1054
  }
1055
1055
  // we have exactly one selfReference or the default partner
1056
-
1057
- if ( !csn.$noPartner) {
1056
+ // $noPartner can be set when a second projection creates an ambiguous backlink, but if exactly
1057
+ // one self-reference targets the same entity as this association, that unambiguous partner still applies.
1058
+ const selfRefsToTarget = csn._selfReferences.filter(ref => ref.$abspath[0] === csn._target?.name);
1059
+ const isPartner = selfRefsToTarget.length === 1 && selfRefsToTarget[0]._constraints?._partnerCsn === csn;
1060
+ if (!csn.$noPartner || isPartner) {
1058
1061
  const partner = csn._selfReferences.length === 1
1059
1062
  ? csn._selfReferences[0]
1060
- : csn._constraints._partnerCsn;
1063
+ : selfRefsToTarget[0] || csn._constraints._partnerCsn;
1061
1064
  if (partner && partner['@odata.navigable'] !== false && this._csn._edmParentCsn.kind !== 'type') {
1062
1065
  // $abspath[0] is main entity
1063
1066
  this._edmAttributes.Partner = partner.$abspath.slice(1).join('/');
package/lib/main.js CHANGED
@@ -16,7 +16,7 @@
16
16
 
17
17
  const lazyload = require('./utils/lazyload')( module );
18
18
 
19
- const { traceApi } = require('./base/trace');
19
+ const trace = require('./base/trace');
20
20
 
21
21
  const snapi = lazyload('./api/main');
22
22
  const csnUtils = lazyload('./model/csnUtils');
@@ -40,6 +40,7 @@ const meta = lazyload('./base/meta');
40
40
  const toCsn = lazyload('./json/to-csn');
41
41
 
42
42
  function parseCdl( cdlSource, filename, options = {} ) {
43
+ trace.call( 'parse.cdl', options );
43
44
  options = Object.assign( {}, options, { parseCdl: true } );
44
45
  const sources = Object.create(null);
45
46
  /** @type {XSN.Model} */
@@ -56,7 +57,7 @@ function parseCdl( cdlSource, filename, options = {} ) {
56
57
  define( model );
57
58
  finalizeParseCdl( model );
58
59
  messageFunctions.throwWithError();
59
- return toCsn.compactModel( model );
60
+ return compactAndTrace( model, 'parse.cdl' );
60
61
  }
61
62
 
62
63
  function parseCql( cdlSource, filename = '<query>.cds', options = {} ) {
@@ -75,6 +76,12 @@ function parseExpr( cdlSource, filename = '<expr>.cds', options = {} ) {
75
76
  return toCsn.compactExpr( xsn );
76
77
  }
77
78
 
79
+ function compactAndTrace( xsn, apiName = 'compile' ) {
80
+ const csn = toCsn.compactModel( xsn );
81
+ trace.exit( apiName, csn );
82
+ return csn;
83
+ }
84
+
78
85
  // FIXME: The implementation of those functions that delegate to 'backends'
79
86
  // should probably move here
80
87
  // ATTENTION: Keep in sync with main.d.ts!
@@ -82,16 +89,19 @@ module.exports = {
82
89
  // Compiler
83
90
  version: () => meta.version(),
84
91
  compile: (filenames, dir, options, fileCache) => { // main function
85
- traceApi( 'compile', options );
86
- return compiler.compileX(filenames, dir, options, fileCache).then(toCsn.compactModel);
92
+ trace.call( 'compile', options, null, filenames );
93
+ return compiler.compileX( filenames, dir, options, fileCache )
94
+ .then( compactAndTrace );
87
95
  },
88
96
  compileSync: (filenames, dir, options, fileCache) => { // main function
89
- traceApi('compileSync', options);
90
- return toCsn.compactModel(compiler.compileSyncX(filenames, dir, options, fileCache));
97
+ trace.call( 'compileSync', options, null, filenames );
98
+ const xsn = compiler.compileSyncX( filenames, dir, options, fileCache );
99
+ return compactAndTrace( xsn, 'compileSync' );
91
100
  },
92
101
  compileSources: (sourcesDict, options) => { // main function
93
- traceApi('compileSources', options);
94
- return toCsn.compactModel(compiler.compileSourcesX(sourcesDict, options));
102
+ trace.call( 'compileSources', options, null, sourcesDict );
103
+ const xsn = compiler.compileSourcesX( sourcesDict, options );
104
+ return compactAndTrace( xsn, 'compileSources' );
95
105
  },
96
106
  compactModel: csn => csn, // for easy v2 migration
97
107
  get CompilationError() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds-compiler",
3
- "version": "6.9.0",
3
+ "version": "6.9.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)",