@sap/cds-compiler 4.4.2 → 4.5.0

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 (60) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/bin/cdsc.js +5 -0
  3. package/bin/cdsv2m.js +7 -5
  4. package/doc/CHANGELOG_BETA.md +16 -0
  5. package/lib/api/main.js +68 -47
  6. package/lib/api/options.js +10 -6
  7. package/lib/api/validate.js +1 -1
  8. package/lib/base/message-registry.js +28 -6
  9. package/lib/base/messages.js +18 -13
  10. package/lib/base/model.js +3 -0
  11. package/lib/checks/annotationsOData.js +49 -0
  12. package/lib/checks/validator.js +6 -4
  13. package/lib/compiler/assert-consistency.js +38 -16
  14. package/lib/compiler/builtins.js +10 -49
  15. package/lib/compiler/checks.js +16 -8
  16. package/lib/compiler/cycle-detector.js +1 -4
  17. package/lib/compiler/define.js +4 -1
  18. package/lib/compiler/extend.js +21 -7
  19. package/lib/compiler/generate.js +3 -0
  20. package/lib/compiler/populate.js +5 -1
  21. package/lib/compiler/propagator.js +46 -9
  22. package/lib/compiler/resolve.js +68 -14
  23. package/lib/compiler/shared.js +44 -27
  24. package/lib/compiler/tweak-assocs.js +158 -37
  25. package/lib/compiler/utils.js +9 -0
  26. package/lib/edm/annotations/edmJson.js +35 -61
  27. package/lib/edm/annotations/genericTranslation.js +13 -5
  28. package/lib/edm/annotations/preprocessAnnotations.js +2 -3
  29. package/lib/edm/csn2edm.js +4 -1
  30. package/lib/edm/edmInboundChecks.js +59 -15
  31. package/lib/edm/edmPreprocessor.js +1 -7
  32. package/lib/gen/Dictionary.json +8 -0
  33. package/lib/gen/language.checksum +1 -1
  34. package/lib/gen/language.interp +12 -2
  35. package/lib/gen/languageParser.js +6095 -5195
  36. package/lib/json/from-csn.js +4 -5
  37. package/lib/json/to-csn.js +22 -3
  38. package/lib/language/errorStrategy.js +7 -3
  39. package/lib/language/genericAntlrParser.js +120 -24
  40. package/lib/language/textUtils.js +16 -0
  41. package/lib/model/csnUtils.js +9 -8
  42. package/lib/model/revealInternalProperties.js +5 -2
  43. package/lib/modelCompare/compare.js +10 -4
  44. package/lib/optionProcessor.js +2 -3
  45. package/lib/render/toCdl.js +31 -13
  46. package/lib/render/toHdbcds.js +20 -30
  47. package/lib/render/toSql.js +33 -54
  48. package/lib/render/utils/common.js +24 -6
  49. package/lib/transform/db/applyTransformations.js +59 -2
  50. package/lib/transform/db/backlinks.js +13 -1
  51. package/lib/transform/db/expansion.js +24 -3
  52. package/lib/transform/db/flattening.js +2 -2
  53. package/lib/transform/db/killAnnotations.js +37 -0
  54. package/lib/transform/db/rewriteCalculatedElements.js +46 -6
  55. package/lib/transform/forOdata.js +13 -46
  56. package/lib/transform/forRelationalDB.js +2 -1
  57. package/lib/transform/translateAssocsToJoins.js +13 -4
  58. package/lib/transform/universalCsn/coreComputed.js +1 -1
  59. package/lib/transform/universalCsn/universalCsnEnricher.js +4 -4
  60. package/package.json +7 -6
@@ -376,7 +376,7 @@ const schema = compileSchema( {
376
376
  type: object,
377
377
  optional: [ 'src', 'min', 'max' ],
378
378
  inKind: [ 'element', 'type', 'mixin' ],
379
- onlyWith: [ 'target', 'targetAspect', 'id' ], // also in 'ref[]'
379
+ onlyWith: [ 'target', 'targetAspect', 'where' ], // also in 'ref[]'
380
380
  },
381
381
  items: {
382
382
  type: object,
@@ -1325,14 +1325,13 @@ function stringValOrNull( val, spec ) {
1325
1325
 
1326
1326
  function scalenum( val, spec ) {
1327
1327
  if ([ 'floating', 'variable' ].includes(val))
1328
- return { val, literal: 'string', location: location() };
1328
+ return { val, literal: 'string', location: location() }; // XSN TODO: remove `literal`
1329
1329
  return natnum(val, spec );
1330
1330
  }
1331
1331
 
1332
1332
  function natnum( val, spec ) {
1333
1333
  if (typeof val === 'number' && val >= 0 && Number.isSafeInteger( val ))
1334
- // XSN TODO: do not require literal
1335
- return { val, literal: 'number', location: location() };
1334
+ return { val, location: location() };
1336
1335
  const loc = location(true);
1337
1336
  error( 'syntax-expecting-unsigned-int', loc,
1338
1337
  { '#': spec.msgVariant || 'csn', prop: spec.msgProp, op: '*' } );
@@ -1342,7 +1341,7 @@ function natnum( val, spec ) {
1342
1341
  // Use with spec.msgVariant !
1343
1342
  function natnumOrStar( val, spec ) {
1344
1343
  return (val === '*')
1345
- ? { val, literal: 'string', location: location() }
1344
+ ? { val, location: location() }
1346
1345
  : natnum( val, spec );
1347
1346
  }
1348
1347
 
@@ -14,7 +14,7 @@
14
14
  const { locationString } = require('../base/messages');
15
15
  const { isBetaEnabled, isDeprecatedEnabled } = require('../base/model');
16
16
  const { pathName } = require('../compiler/utils');
17
- const { CompilerAssertion } = require('../base/error');
17
+ const { CompilerAssertion, ModelError } = require('../base/error');
18
18
 
19
19
  const compilerVersion = require('../../package.json').version;
20
20
  const creator = `CDS Compiler v${ compilerVersion }`;
@@ -93,6 +93,7 @@ const transformers = {
93
93
  cardinality, // also in pathItem: after 'id', before 'where'
94
94
  targetAspect,
95
95
  target,
96
+ $filtered: value, // assoc+filter
96
97
  foreignKeys,
97
98
  enum: enumDict,
98
99
  items,
@@ -305,8 +306,14 @@ function csnDictionary( csn, sort, cloneOptions = false ) {
305
306
  ? proto
306
307
  : (proto) ? Object.prototype : null;
307
308
  const r = Object.create( dictProto );
308
- for (const n of (sort) ? Object.keys(csn).sort() : Object.keys(csn))
309
- r[n] = sortCsn( csn[n], cloneOptions );
309
+ for (const n of (sort) ? Object.keys(csn).sort() : Object.keys(csn)) {
310
+ // CSN does not allow any dictionary that are not objects.
311
+ // The compiler handles it, but a pre-transformed OData CSN won't trigger recompilation.
312
+ if (csn[n] && typeof csn[n] === 'object')
313
+ r[n] = sortCsn(csn[n], cloneOptions);
314
+ else
315
+ throw new ModelError(`Found non-object dictionary entry: "${ n }" of type "${ typeof csn[n] }"`);
316
+ }
310
317
 
311
318
  return r;
312
319
  }
@@ -1176,6 +1183,18 @@ function args( node ) {
1176
1183
  }
1177
1184
 
1178
1185
  function anno( node ) {
1186
+ if (!node)
1187
+ return true; // `@aBool` short for `@aBool: true`
1188
+ if (universalCsn && node.$inferred) {
1189
+ // TODO: return undefined for all values of node.$inferred (except 'NULL')?
1190
+ if (node.$inferred === 'prop' || node.$inferred === '$generated' || // via propagator.js
1191
+ node.$inferred === 'parent-origin')
1192
+ return undefined;
1193
+ else if (node.$inferred === 'NULL')
1194
+ return null;
1195
+ }
1196
+ if (node.$inferred && gensrcFlavor)
1197
+ return undefined;
1179
1198
  if (node.$tokenTexts) // expressions in annotation values
1180
1199
  return Object.assign({ '=': node.$tokenTexts }, expression( node ));
1181
1200
  return value(node);
@@ -524,10 +524,14 @@ function getTokenDisplay( token, recognizer ) {
524
524
  if (!token)
525
525
  return '<EOF>';
526
526
  const t = token.type;
527
- if (t === antlr4.Token.EOF || t === antlr4.Token.EPSILON )
527
+ if (t === antlr4.Token.EOF || t === antlr4.Token.EPSILON ) {
528
528
  return '<EOF>';
529
- else if (token.text === '.') // also for DOTbeforeBRACE
530
- return "'.'";
529
+ }
530
+ else if (t === recognizer.constructor.DOTbeforeBRACE) {
531
+ if (recognizer.getTokenStream().LT(2).text === '{')
532
+ return "'.{'";
533
+ return "'.*'";
534
+ }
531
535
  return recognizer.literalNames[t] || recognizer.symbolicNames[t];
532
536
  }
533
537
 
@@ -8,6 +8,7 @@
8
8
 
9
9
  const antlr4 = require('antlr4');
10
10
  const { ATNState } = require('antlr4/src/antlr4/atn/ATNState');
11
+ const { DEFAULT: CommonTokenFactory } = require('antlr4/src/antlr4/CommonTokenFactory');
11
12
  const { dictAdd, dictAddArray } = require('../base/dictionaries');
12
13
  const locUtils = require('../base/location');
13
14
  const { parseDocComment } = require('./docCommentParser');
@@ -23,7 +24,7 @@ const {
23
24
  } = require('../compiler/classes');
24
25
  const { isBetaEnabled } = require('../base/model');
25
26
  const { weakLocation } = require('../base/location');
26
- const { normalizeNewLine } = require('./textUtils');
27
+ const { normalizeNewLine, normalizeNumberString } = require('./textUtils');
27
28
 
28
29
  const $location = Symbol.for('cds.$location');
29
30
 
@@ -107,6 +108,8 @@ Object.assign(GenericAntlrParser.prototype, {
107
108
  fixNewKeywordPlacement,
108
109
  signedExpression,
109
110
  numberLiteral,
111
+ unsignedIntegerLiteral,
112
+ assignAnnotationValue,
110
113
  quotedLiteral,
111
114
  pathName,
112
115
  docComment,
@@ -117,13 +120,14 @@ Object.assign(GenericAntlrParser.prototype, {
117
120
  createDict,
118
121
  createArray,
119
122
  finalizeDictOrArray,
120
- createPrefixOp,
123
+ insertSemicolon,
121
124
  setMaxCardinality,
122
125
  setNullability,
123
126
  reportDuplicateClause,
124
127
  reportUnexpectedExtension,
125
128
  reportUnexpectedSpace,
126
129
  pushIdent,
130
+ pushItem,
127
131
  handleComposition,
128
132
  associationInSelectItem,
129
133
  reportExpandInline,
@@ -911,10 +915,6 @@ function expressionAsAnnotationValue( assignment, cond ) {
911
915
  return;
912
916
  Object.assign(assignment, cond.cond);
913
917
  assignment.$tokenTexts = this.tokensToStringRepresentation(cond);
914
- if (!this.isBetaEnabled(this.options, 'annotationExpressions')) {
915
- this.error( 'syntax-unsupported-expression', [ cond.cond.location ], {},
916
- 'Expressions in annotation values are not supported' );
917
- }
918
918
  }
919
919
 
920
920
  // If a '-' is directly before an unsigned number, consider it part of the number;
@@ -941,12 +941,27 @@ function signedExpression( args, expr ) {
941
941
  }
942
942
  }
943
943
 
944
- // Return AST for number token `token` with optional token `sign`. Represent
945
- // the number as number in property `val` if the number can safely be
946
- // represented as an integer. Otherwise, represent the number by a string, the
947
- // token lexeme.
948
- function numberLiteral( token, sign, text = token.text ) {
944
+ /**
945
+ * Return number literal (XSN) for number token `token` with optional token `sign`.
946
+ * Represent the number as a JS number in property `val` if the number can safely be
947
+ * represented as one. Represent the number by a string, the token lexeme, if the
948
+ * stringified version of the number does not match the token lexeme.
949
+ *
950
+ * TODO: Always use text !== `${ num }`
951
+ */
952
+ function numberLiteral( sign, text = this._input.LT(-1).text ) {
953
+ const token = this._input.LT(-1);
949
954
  let location = this.tokenLocation( token );
955
+ const nextToken = this._input.LT(1);
956
+ if (token.type === this.constructor.Number &&
957
+ token.stop + 1 === nextToken.start &&
958
+ (nextToken.type === this.constructor.Identifier ||
959
+ nextToken.type < this.constructor.Identifier && /^[a-z]+$/i.test( nextToken.text ))) {
960
+ // TODO: Make it an error in v5
961
+ this.warning('syntax-expecting-space', nextToken, {},
962
+ 'Expecting a space between a number and a keyword/identifier');
963
+ }
964
+
950
965
  if (sign) {
951
966
  const { endLine, endCol } = location;
952
967
  location = this.startLocation( sign );
@@ -957,17 +972,65 @@ function numberLiteral( token, sign, text = token.text ) {
957
972
  }
958
973
 
959
974
  const num = Number.parseFloat( text || '0' ); // not Number.parseInt() !
975
+ const normalized = normalizeNumberString(text);
976
+ if (normalized !== `${ num }` && normalized !== `${ sign.text }${ num }`)
977
+ return { literal: 'number', val: normalized, location };
978
+ return { literal: 'number', val: num, location };
979
+ }
980
+
981
+ /**
982
+ * Given `token`, return a number literal (XSN). If the number is not an unsigned integer
983
+ * or it can't be represented in JS, emit an error.
984
+ */
985
+ function unsignedIntegerLiteral() {
986
+ const token = this._input.LT(-1);
987
+ const location = this.tokenLocation( token );
988
+ const text = token.text || '0';
989
+ const num = Number.parseFloat( text ); // not Number.parseInt() !
960
990
  if (!Number.isSafeInteger(num)) {
961
- if (sign == null) {
962
- this.error( 'syntax-expecting-unsigned-int', token,
963
- { '#': !text.match(/^\d*$/) ? 'normal' : 'unsafe' } );
964
- }
991
+ this.error( 'syntax-expecting-unsigned-int', token,
992
+ { '#': !text.match(/^\d*$/) ? 'normal' : 'unsafe' } );
993
+ }
994
+ else if (text.match(/^\d+[.]\d+$/)) {
995
+ // More restrictive check: 10.0 emits a message, because we don't expect
996
+ // any decimal places.
997
+ const dotLoc = { ...location };
998
+ dotLoc.col += text.indexOf('.');
999
+ dotLoc.endCol = dotLoc.col + 1;
1000
+ this.info( 'syntax-ignoring-decimal', dotLoc );
1001
+ }
1002
+ return { literal: 'number', val: num, location };
1003
+ }
965
1004
 
966
- else if (text !== `${ num }`) {
967
- return { literal: 'number', val: text, location };
1005
+ // Make the annotation `anno` have `value` as value. This function is basically
1006
+ // just `Object.assign`, but we really try to represent the provided CDL number as
1007
+ // JSON number. We give a warning if this is not possible or leads to a precision
1008
+ // loss.
1009
+ function assignAnnotationValue( anno, value ) {
1010
+ const { val } = value;
1011
+ if (value.literal === 'number' && typeof val !== 'number') {
1012
+ // a number in CDL, but stored as string in `val` - due to rounding or scientific notation
1013
+ let num = Number.parseFloat( val || '0' );
1014
+ const inf = !Number.isFinite( num );
1015
+ if (inf)
1016
+ num = val;
1017
+ if (inf || relevantDigits( val ) !== relevantDigits( num.toString() )) {
1018
+ this.warning( 'syntax-invalid-anno-number', value.location,
1019
+ { '#': (inf ? 'infinite' : 'rounded' ), rawvalue: val, value: num },
1020
+ {
1021
+ std: 'Annotation number $(RAWVALUE) is put as $(VALUE) into the CSN',
1022
+ rounded: 'Annotation number $(RAWVALUE) is rounded to $(VALUE)',
1023
+ // eslint-disable-next-line max-len
1024
+ infinite: 'Annotation value $(RAWVALUE) is infinite as number and put as string into the CSN',
1025
+ } );
968
1026
  }
1027
+ value.val = num;
969
1028
  }
970
- return { literal: 'number', val: num, location };
1029
+ Object.assign( anno, value );
1030
+ }
1031
+
1032
+ function relevantDigits( val ) {
1033
+ return val.replace( /0*(e.+)?$/i, '' ).replace( /\./, '' ).replace( /^[-+0]+/, '' );
971
1034
  }
972
1035
 
973
1036
  // Create AST node for quoted literals like string and e.g. date'2017-02-22'.
@@ -1036,6 +1099,16 @@ function pushIdent( path, ident, prefix ) {
1036
1099
  }
1037
1100
  }
1038
1101
 
1102
+ function pushItem( array, val ) {
1103
+ if (!array)
1104
+ return;
1105
+
1106
+ if (val != null)
1107
+ array.push(val);
1108
+ else
1109
+ array.broken = true;
1110
+ }
1111
+
1039
1112
  // For :param, #variant, #symbol, @(…) and @Begin and `@` inside annotation paths
1040
1113
  function reportUnexpectedSpace( prefix = this._input.LT(-1),
1041
1114
  location = this.tokenLocation( this._input.LT(1) ) ) {
@@ -1173,14 +1246,37 @@ function finalizeDictOrArray( dict ) {
1173
1246
  loc.endCol = stop.stop - stop.start + stop.column + 2;
1174
1247
  }
1175
1248
 
1176
- function createSource() {
1177
- return new XsnSource();
1249
+ function insertSemicolon() {
1250
+ // hand-selected keyword list: first() set of `artifactDefOrExtend`
1251
+ const allowedKeywords = [ this.constructor.ACTION, this.constructor.ABSTRACT,
1252
+ this.constructor.ANNOTATE, this.constructor.ANNOTATION, this.constructor.ASPECT,
1253
+ this.constructor.CONTEXT, this.constructor.DEFINE, this.constructor.ENTITY,
1254
+ this.constructor.EOF, this.constructor.EVENT, this.constructor.EXTEND,
1255
+ this.constructor.FUNCTION, this.constructor.SERVICE,
1256
+ this.constructor.TYPE, this.constructor.VIEW, this.literalNames.indexOf( "'@'" ) ];
1257
+
1258
+ const currentToken = this._input.tokens[this._input.index];
1259
+
1260
+ if (allowedKeywords.includes(currentToken.type)) {
1261
+ const prev = this._input.LT(-1);
1262
+ const t = CommonTokenFactory.create(
1263
+ currentToken.source, this.literalNames.indexOf( "';'" ),
1264
+ '', currentToken.channel,
1265
+ prev.stop, prev.stop,
1266
+ prev.line, prev.column
1267
+ );
1268
+ t.tokenIndex = prev.tokenIndex + 1;
1269
+ this._input.tokens.splice(t.tokenIndex, 0, t);
1270
+
1271
+ // Update tokenIndex: There could have been comments between two non-hidden tokens.
1272
+ for (let tokenIndex = t.tokenIndex + 1; tokenIndex < this._input.tokens.length; tokenIndex++)
1273
+ this._input.tokens[tokenIndex].tokenIndex += 1;
1274
+ this._input.index = t.tokenIndex;
1275
+ }
1178
1276
  }
1179
1277
 
1180
- // Create AST node for prefix operator `op` and arguments `args`
1181
- function createPrefixOp( token, args ) {
1182
- const op = this.valueWithTokenLocation( token.text.toLowerCase(), token );
1183
- return { op, args, location: this.combinedLocation( op, args[args.length - 1] ) };
1278
+ function createSource() {
1279
+ return new XsnSource();
1184
1280
  }
1185
1281
 
1186
1282
  // Create AST node for binary operator `op` and arguments `args`
@@ -49,9 +49,25 @@ function normalizeNewLine( str ) {
49
49
  return str.replace(new RegExp(cdlNewLineRegEx, 'ug'), '\n');
50
50
  }
51
51
 
52
+ /**
53
+ * Normalizes the given number (as a string):
54
+ *
55
+ * - removes leading zeroes (`0`) to avoid accidental octal-conversion
56
+ *
57
+ * @param {string} str
58
+ * @return {string}
59
+ */
60
+ function normalizeNumberString( str ) {
61
+ const num = str.replace(/^([+-]?)0+(\d)/, '$1$2');
62
+ if (!num.includes('.'))
63
+ return num;
64
+ return num.replace(/([.]\d)0+$/, '$1');
65
+ }
66
+
52
67
  module.exports = {
53
68
  isWhitespaceOrNewLineOnly,
54
69
  isWhitespaceCharacterNoNewline,
55
70
  cdlNewLineRegEx,
56
71
  normalizeNewLine,
72
+ normalizeNumberString,
57
73
  };
@@ -1098,7 +1098,7 @@ function copyAnnotations( fromNode, toNode, overwrite = false, excludes = {} ) {
1098
1098
 
1099
1099
  for (const anno of annotations) {
1100
1100
  if ((toNode[anno] === undefined || overwrite) && !excludes[anno])
1101
- toNode[anno] = fromNode[anno];
1101
+ toNode[anno] = cloneAnnotationValue(fromNode[anno]);
1102
1102
  }
1103
1103
  }
1104
1104
 
@@ -1118,13 +1118,9 @@ function copyAnnotationsAndDoc( fromNode, toNode, overwrite = false ) {
1118
1118
  if (!toNode)
1119
1119
  return;
1120
1120
 
1121
- const annotations = Object.keys(fromNode)
1122
- .filter(key => key.startsWith('@') || key === 'doc');
1123
-
1124
- for (const anno of annotations) {
1125
- if (toNode[anno] === undefined || overwrite)
1126
- toNode[anno] = fromNode[anno];
1127
- }
1121
+ copyAnnotations(fromNode, toNode, overwrite);
1122
+ if (toNode.doc === undefined || overwrite)
1123
+ toNode.doc = fromNode.doc;
1128
1124
  }
1129
1125
 
1130
1126
  /**
@@ -1449,6 +1445,10 @@ function isAssociationOperand( arg, path, inspectRef ) {
1449
1445
  return art && art.target !== undefined;
1450
1446
  }
1451
1447
 
1448
+ function pathName( ref ) {
1449
+ return ref ? ref.map( id => id?.id || id ).join( '.' ) : '';
1450
+ }
1451
+
1452
1452
  module.exports = {
1453
1453
  getUtils,
1454
1454
  cloneCsn: cloneCsnNonDict, // Umbrella relies on this name
@@ -1496,4 +1496,5 @@ module.exports = {
1496
1496
  cardinality2str,
1497
1497
  isAssociationOperand,
1498
1498
  isDollarSelfOrProjectionOperand,
1499
+ pathName,
1499
1500
  };
@@ -321,8 +321,11 @@ function artifactIdentifier( node, parent ) {
321
321
  Object.defineProperty( node, '__unique_id__', { value: uniqueId-- } );
322
322
  const outerNum = node.$effectiveSeqNo || node.__unique_id__;
323
323
  let outer = outerNum != null ? `##${ outerNum }` : '';
324
- if (node._outer) { // anon aspect in targetAspect | items
325
- outer = (node._outer.targetAspect === node) ? `/target${ outer }` : `/items${ outer }`;
324
+ if (node._outer) { // anon aspect in targetAspect | items | $calcDepElement
325
+ outer = (node.kind === '$annotation')
326
+ // eslint-disable-next-line prefer-template
327
+ ? `/${ quoted( '@' + node.name.id ) }`
328
+ : `/${ node.kind || 'items' }${ outer }`;
326
329
  node = node._outer;
327
330
  }
328
331
  if (node === parent)
@@ -190,7 +190,7 @@ function getExtensionAndMigrations(beforeModel, options, { extensions, migration
190
190
  migration.change = changedElements;
191
191
  if(!hasPrimaryKeyChange)
192
192
  forEach(changedElements, (_name, change) => {
193
- if((change.old.key || change.new.key) && !change.new.target && !change.old.target) {
193
+ if(!change.onlyDoc && (change.old.key || change.new.key) && !change.new.target && !change.old.target) {
194
194
  // For to.hdi.migration: Just drop-create (commented out), for to.sql.migration: Handle case where we add/remove "key" keyword, no drop-create otherwise
195
195
  if(options.sqlDialect === 'hana' && options.src === 'hdi' || (!change.old.key || !change.new.key)) {
196
196
  hasPrimaryKeyChange = true;
@@ -276,6 +276,8 @@ function getElementComparator(otherArtifact, addedElementsDict = null, changedEl
276
276
  element.$notNull = false; // Explicitly set notNull to the implicit default so we render the correct ALTER
277
277
  }
278
278
  changedElementsDict[name] = changedElement(element, otherElement);
279
+ } else if(docCommentChanged(element, otherElement)) {
280
+ changedElementsDict[name] = { ...changedElement(element, otherElement), onlyDoc: true };
279
281
  }
280
282
 
281
283
  return;
@@ -356,6 +358,9 @@ function deepEqual(a, b, include = () => true, depth = 0) {
356
358
  : a === b;
357
359
  }
358
360
 
361
+ function docCommentChanged(element, otherElement) {
362
+ return element.doc && !otherElement.doc || otherElement.doc && !element.doc || element.doc && element.doc !== otherElement.doc;
363
+ }
359
364
 
360
365
  const relevantProperties = {
361
366
  'doc': true,
@@ -364,7 +369,8 @@ const relevantProperties = {
364
369
  };
365
370
 
366
371
  /**
367
- * Returns whether any type parameters differ between two given elements. Ignores whether types themselves differ (`type` property).
372
+ * Returns whether any type parameters differ between two given elements. Ignores whether types themselves differ (`type` property) and ignores
373
+ * diff in doc comments.
368
374
  * @param element {object} an element
369
375
  * @param otherElement {object} another element
370
376
  * @returns {boolean}
@@ -373,7 +379,7 @@ function typeParametersChanged(element, otherElement) {
373
379
  const checked = new Set();
374
380
  for (const key in element) {
375
381
  if (Object.prototype.hasOwnProperty.call(element, key))
376
- if((!key.startsWith('@') || relevantProperties[key]) && key !== 'type') {
382
+ if((!key.startsWith('@') || relevantProperties[key]) && key !== 'type' && key !== 'doc') {
377
383
  checked.add(key);
378
384
  if(!deepEqual(element[key], otherElement[key]))
379
385
  return true;
@@ -382,7 +388,7 @@ function typeParametersChanged(element, otherElement) {
382
388
 
383
389
  for (const key in otherElement) {
384
390
  if (Object.prototype.hasOwnProperty.call(otherElement, key))
385
- if((!key.startsWith('@') || relevantProperties[key]) && key !== 'type' && !checked.has(key))
391
+ if((!key.startsWith('@') || relevantProperties[key]) && key !== 'type' && key !== 'doc' && !checked.has(key))
386
392
  return true;
387
393
  }
388
394
 
@@ -227,7 +227,6 @@ optionProcessor.command('O, toOdata')
227
227
  .option(' --odata-foreign-keys')
228
228
  .option(' --odata-v2-partial-constr')
229
229
  .option(' --odata-vocabularies <list>')
230
- .option(' --odata-open-type')
231
230
  .option('-c, --csn')
232
231
  .option('-f, --odata-format <format>', ['flat', 'structured'])
233
232
  .option('-n, --sql-mapping <style>', ['plain', 'quoted', 'hdbcds'], { aliases: [ '--names' ] })
@@ -260,7 +259,6 @@ optionProcessor.command('O, toOdata')
260
259
  (Not spec compliant and V2 only)
261
260
  --odata-vocabularies <list> JSON array of adhoc vocabulary definitions
262
261
  { prefix: { alias, ns, uri }, ... }
263
- --odata-open-type Renders all structured types as OpenType=true, if not annotated otherwise.
264
262
  -n, --sql-mapping <style> Annotate artifacts and elements with "@cds.persistence.name", which is
265
263
  the corresponding database name (see "--sql-mapping" for "toHana or "toSql")
266
264
  plain : (default) Names in uppercase and flattened with underscores
@@ -305,6 +303,7 @@ optionProcessor.command('Q, toSql')
305
303
  .option(' --generated-by-comment')
306
304
  .option(' --better-sqlite-session-variables')
307
305
  .option(' --fewer-localized-views')
306
+ .option(' --without-hana-associations')
308
307
  .help(`
309
308
  Usage: cdsc toSql [options] <files...>
310
309
 
@@ -360,7 +359,7 @@ optionProcessor.command('Q, toSql')
360
359
  active if sqlDialect is \`sqlite\`
361
360
  --fewer-localized-views If set, the backends will not create localized convenience views for
362
361
  those views, that only have an association to a localized entity/view.
363
-
362
+ --without-hana-associations If set, the backend will not render a "WITH ASSOCIATIONS" for sqlDialect 'hana'
364
363
  `);
365
364
 
366
365
  optionProcessor.command('toRename')
@@ -11,7 +11,7 @@ const enrichUniversalCsn = require('../transform/universalCsn/universalCsnEnrich
11
11
  const { isBetaEnabled } = require('../base/model');
12
12
  const { ModelError } = require('../base/error');
13
13
  const { makeMessageFunction } = require('../base/messages.js');
14
- const { typeParameters, specialFunctions } = require('../compiler/builtins');
14
+ const { typeParameters, specialFunctions, xprInAnnoProperties } = require('../compiler/builtins');
15
15
  const { forEach } = require('../utils/objectUtils');
16
16
  const {
17
17
  isBuiltinType,
@@ -589,11 +589,11 @@ function csnToCdl( csn, options ) {
589
589
  * @param {CdlRenderEnvironment} env
590
590
  */
591
591
  function renderElement( elementName, element, env ) {
592
+ const isCalcElement = (element.value !== undefined);
592
593
  let result = renderAnnotationAssignmentsAndDocComment(element, env);
593
594
  result += env.indent;
594
595
  result += element.virtual ? 'virtual ' : '';
595
596
  result += element.key ? 'key ' : '';
596
- // TODO(v5): Remove once deprecated flag for `masked` is removed.
597
597
  result += element.masked ? 'masked ' : '';
598
598
  result += quoteNonIdentifierOrKeyword(elementName, env);
599
599
  if (element.val !== undefined) { // enum value
@@ -602,13 +602,16 @@ function csnToCdl( csn, options ) {
602
602
  else if (element['#'] !== undefined) { // enum symbol reference
603
603
  result += ` = #${element['#']}`;
604
604
  }
605
- else {
605
+ else if (!isCalcElement || !isDirectAssocOrComp(element.type)) {
606
+ // If the element is a calculated element _and_ a direct association or
607
+ // composition, we'd render `Association to F on (cond) = calcValue;` which
608
+ // would alter the ON-condition.
606
609
  const props = renderTypeReferenceAndProps(element, env);
607
610
  if (props !== '')
608
611
  result += ` : ${props}`;
609
612
  }
610
613
 
611
- if (element.value !== undefined) { // calculated element // @ts-ignore
614
+ if (isCalcElement) { // calculated element // @ts-ignore
612
615
  result += ' = ';
613
616
  if (element.value.xpr && xprContainsCondition(element.value.xpr))
614
617
  result += exprRenderer.renderSubExpr(element.value, env.withSubPath([ 'value' ]));
@@ -873,7 +876,7 @@ function csnToCdl( csn, options ) {
873
876
  // of an `annotate` statement. That may change in the future.
874
877
  result += renderDocComment(element, env);
875
878
  }
876
- // Note: parentheses are a workaround for #9015
879
+ // Note: parentheses are a workaround for #9015; TODO: Check & Update
877
880
  result += renderAnnotationAssignmentsAndDocComment(col, env, { parentheses: true });
878
881
  result += env.indent;
879
882
 
@@ -1316,7 +1319,7 @@ function csnToCdl( csn, options ) {
1316
1319
  }
1317
1320
 
1318
1321
  // Association type
1319
- if (type === 'cds.Association' || type === 'cds.Composition') {
1322
+ if (isDirectAssocOrComp(type)) {
1320
1323
  const isComp = type === 'cds.Composition';
1321
1324
  // Type, cardinality and target; CAPire uses CamelCase
1322
1325
  result += isComp ? 'Composition' : 'Association';
@@ -1460,10 +1463,7 @@ function csnToCdl( csn, options ) {
1460
1463
  * @param {CdlRenderEnvironment} env
1461
1464
  */
1462
1465
  function renderAnnotationValue( annoValue, env ) {
1463
- // TODO: There must be at least one known expression property, otherwise
1464
- // it could be `type: 'unchecked'`.
1465
- const isXpr = annoValue?.['='] !== undefined && (Object.keys(annoValue).length > 1) &&
1466
- isBetaEnabled(options, 'annotationExpressions');
1466
+ const isXpr = annoValue?.['='] !== undefined && xprInAnnoProperties.some(xProp => annoValue[xProp] !== undefined);
1467
1467
  if (isXpr) {
1468
1468
  // Once inside an expression, we stay there.
1469
1469
  const xpr = exprRenderer.renderExpr(annoValue, env);
@@ -1852,7 +1852,7 @@ function csnToCdl( csn, options ) {
1852
1852
  name = name.substring(1);
1853
1853
  // Take the annotation assignment apart into <nameBeforeVariant>#<variantAndRest>
1854
1854
  const parts = name.split('#');
1855
- const nameBeforeVariant = parts[0];
1855
+ let nameBeforeVariant = parts[0];
1856
1856
  const variant = parts.length > 1 ? parts.slice(1).join('#') : undefined;
1857
1857
  const { parentheses } = config;
1858
1858
 
@@ -1860,8 +1860,13 @@ function csnToCdl( csn, options ) {
1860
1860
  if (parentheses)
1861
1861
  result += '(';
1862
1862
 
1863
+ // if the variant is empty, render '#' as part of the name, e.g `variant !== undefined`.
1864
+ if (variant === '')
1865
+ nameBeforeVariant += '#';
1866
+
1863
1867
  result += quoteAnnotationPathIfRequired(nameBeforeVariant, env);
1864
- if (variant !== undefined) {
1868
+
1869
+ if (variant !== undefined && variant !== '') {
1865
1870
  // Unfortunately, the compiler does not allow `.@` after the first variant identifier,
1866
1871
  // nor multiple `#`, so we're back at simple paths that are possibly quoted.
1867
1872
  result += `#${quotePathIfRequired(variant, env)}`;
@@ -1939,7 +1944,7 @@ function csnToCdl( csn, options ) {
1939
1944
  aliasOnly: x => x.as,
1940
1945
  enum: x => `#${x['#']}`,
1941
1946
  ref(x) {
1942
- return `${(x.param || x.global) ? ':' : ''}${x.ref.map((step, index) => renderPathStep(step, index, this.env.withSubPath([ 'ref', index ]))).join('.')}`;
1947
+ return `${x.param ? ':' : ''}${x.ref.map((step, index) => renderPathStep(step, index, this.env.withSubPath([ 'ref', index ]))).join('.')}`;
1943
1948
  },
1944
1949
  windowFunction(x) {
1945
1950
  const funcDef = this.func(x);
@@ -2182,6 +2187,19 @@ function requiresQuotingForCdl( id, additionalKeywords ) {
2182
2187
  additionalKeywords.includes(id.toUpperCase());
2183
2188
  }
2184
2189
 
2190
+ /**
2191
+ * Returns true if the given type is an association or composition,
2192
+ * without type indirection.
2193
+ *
2194
+ * @param {string|string[]} type
2195
+ * @return {boolean}
2196
+ */
2197
+
2198
+ function isDirectAssocOrComp( type ) {
2199
+ type = normalizeTypeRef(type);
2200
+ return (type === 'cds.Association' || type === 'cds.Composition');
2201
+ }
2202
+
2185
2203
  const conditionOperators = [
2186
2204
  // Antlr rule 'condition', 'conditionAnd'
2187
2205
  'AND', 'OR',