@sap/cds-compiler 4.4.4 → 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 (59) hide show
  1. package/CHANGELOG.md +52 -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/optionProcessor.js +2 -3
  44. package/lib/render/toCdl.js +31 -13
  45. package/lib/render/toHdbcds.js +20 -30
  46. package/lib/render/toSql.js +33 -54
  47. package/lib/render/utils/common.js +24 -6
  48. package/lib/transform/db/applyTransformations.js +59 -2
  49. package/lib/transform/db/backlinks.js +13 -1
  50. package/lib/transform/db/expansion.js +24 -3
  51. package/lib/transform/db/flattening.js +2 -2
  52. package/lib/transform/db/killAnnotations.js +37 -0
  53. package/lib/transform/db/rewriteCalculatedElements.js +46 -6
  54. package/lib/transform/forOdata.js +13 -46
  55. package/lib/transform/forRelationalDB.js +2 -1
  56. package/lib/transform/translateAssocsToJoins.js +13 -4
  57. package/lib/transform/universalCsn/coreComputed.js +1 -1
  58. package/lib/transform/universalCsn/universalCsnEnricher.js +4 -4
  59. 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)
@@ -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',
@@ -4,12 +4,13 @@ const {
4
4
  getLastPartOf, getLastPartOfRef,
5
5
  hasValidSkipOrExists, isBuiltinType, generatedByCompilerVersion, getNormalizedQuery,
6
6
  getRootArtifactName, getResultingName, getNamespace, forEachMember, getVariableReplacement, hasAnnotationValue,
7
+ pathName,
7
8
  } = require('../model/csnUtils');
8
9
  const keywords = require('../base/keywords');
9
10
  const {
10
11
  renderFunc, createExpressionRenderer, getRealName, addContextMarkers, addIntermediateContexts,
11
12
  hasHanaComment, getHanaComment, funcWithoutParen, getSqlSnippets,
12
- cdsToSqlTypes, cdsToHdbcdsTypes, withoutCast,
13
+ cdsToSqlTypes, cdsToHdbcdsTypes, withoutCast, variableForDialect,
13
14
  } = require('./utils/common');
14
15
  const {
15
16
  renderReferentialConstraint,
@@ -1209,11 +1210,10 @@ function toHdbcdsSource( csn, options ) {
1209
1210
  *
1210
1211
  * @param {string|object} s Path step
1211
1212
  * @param {number} idx Path position
1212
- * @param {any[]} ref
1213
1213
  * @param {HdbcdsRenderEnvironment} env
1214
1214
  * @returns {string} Rendered path step
1215
1215
  */
1216
- function renderPathStep( s, idx, ref, env ) {
1216
+ function renderPathStep( s, idx, env ) {
1217
1217
  // Simple id or absolute name
1218
1218
  if (typeof s === 'string') {
1219
1219
  // HANA-specific extra magic (should actually be in forRelationalDB)
@@ -1227,12 +1227,6 @@ function toHdbcdsSource( csn, options ) {
1227
1227
  return plainNames ? renderAbsoluteNamePlain(env.currentArtifactName, env)
1228
1228
  : renderAbsoluteNameWithQuotes(env.currentArtifactName, env);
1229
1229
  }
1230
- // HANA-specific translation of '$now' and '$user'
1231
- if (s === '$now' && ref.length === 1)
1232
- return 'CURRENT_TIMESTAMP';
1233
-
1234
- // In first path position, do not quote $projection and magic $-variables like CURRENT_DATE, $now etc.
1235
- // FIXME: We should rather explicitly recognize quoting somehow
1236
1230
 
1237
1231
  // TODO: quote $parameters if it doesn't reference a parameter, this requires knowledge about the kind
1238
1232
  // Example: both views are correct in HANA CDS
@@ -1319,39 +1313,35 @@ function toHdbcdsSource( csn, options ) {
1319
1313
  /**
1320
1314
  * @param {object} x Expression with a ref property
1321
1315
  * @returns {string} Rendered expression
1322
- * @todo no extra magic with x.param or x.global
1316
+ * @todo no extra magic with x.param
1323
1317
  */
1324
1318
  function renderExpressionRef( x ) {
1325
- if (!x.param && !x.global) {
1319
+ if (!x.param) {
1326
1320
  const magicReplacement = getVariableReplacement(x.ref, options);
1327
- if (x.ref[0] === '$user') {
1321
+ if (x.ref[0] === '$user' || x.ref[0] === '$tenant') {
1328
1322
  if (magicReplacement !== null)
1329
1323
  return renderStringForHdbcds(magicReplacement);
1330
1324
 
1331
- // Note: The compiler already transforms $user into $user.id.
1332
-
1333
- // FIXME: this is all not enough: we might need an explicit select item alias (?)
1334
- if (x.ref[1] === 'id')
1335
- return 'SESSION_CONTEXT(\'APPLICATIONUSER\')';
1336
-
1337
- else if (x.ref[1] === 'locale')
1338
- return 'SESSION_CONTEXT(\'LOCALE\')';
1339
-
1340
- else if (x.ref[1] === 'tenant')
1341
- return 'SESSION_CONTEXT(\'TENANT\')';
1325
+ const name = pathName(x.ref);
1326
+ const result = variableForDialect(options, name);
1327
+ // Invalid second path step doesn't cause a return
1328
+ if (result)
1329
+ return result;
1342
1330
  }
1343
- else if (x.ref[0] === '$at' || x.ref[0] === '$valid') {
1344
- if (x.ref[1] === 'from')
1345
- return 'TO_TIMESTAMP(SESSION_CONTEXT(\'VALID-FROM\'))';
1346
-
1347
- else if (x.ref[1] === 'to')
1348
- return 'TO_TIMESTAMP(SESSION_CONTEXT(\'VALID-TO\'))';
1331
+ else if (x.ref[0] === '$at' || x.ref[0] === '$valid' || x.ref[0] === '$now') {
1332
+ const name = pathName(x.ref);
1333
+ const result = variableForDialect(options, name);
1334
+ // Invalid second path step doesn't cause a return
1335
+ if (result)
1336
+ return result;
1349
1337
  }
1350
1338
  else if (x.ref[0] === '$session' && magicReplacement !== null) {
1351
1339
  return renderStringForHdbcds(magicReplacement);
1352
1340
  }
1353
1341
  }
1354
- return `${(x.param || x.global) ? ':' : ''}${x.ref.map((step, index) => renderPathStep(step, index, x.ref, this.env.withSubPath([ 'ref', index ]))).join('.')}`;
1342
+ const prefix = x.param ? ':' : '';
1343
+ const ref = x.ref.map((step, index) => renderPathStep(step, index, this.env.withSubPath([ 'ref', index ]))).join('.');
1344
+ return `${prefix}${ref}`;
1355
1345
  }
1356
1346
 
1357
1347
  /**