@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.
- package/CHANGELOG.md +58 -0
- package/bin/cdsc.js +5 -0
- package/bin/cdsv2m.js +7 -5
- package/doc/CHANGELOG_BETA.md +16 -0
- package/lib/api/main.js +68 -47
- package/lib/api/options.js +10 -6
- package/lib/api/validate.js +1 -1
- package/lib/base/message-registry.js +28 -6
- package/lib/base/messages.js +18 -13
- package/lib/base/model.js +3 -0
- package/lib/checks/annotationsOData.js +49 -0
- package/lib/checks/validator.js +6 -4
- package/lib/compiler/assert-consistency.js +38 -16
- package/lib/compiler/builtins.js +10 -49
- package/lib/compiler/checks.js +16 -8
- package/lib/compiler/cycle-detector.js +1 -4
- package/lib/compiler/define.js +4 -1
- package/lib/compiler/extend.js +21 -7
- package/lib/compiler/generate.js +3 -0
- package/lib/compiler/populate.js +5 -1
- package/lib/compiler/propagator.js +46 -9
- package/lib/compiler/resolve.js +68 -14
- package/lib/compiler/shared.js +44 -27
- package/lib/compiler/tweak-assocs.js +158 -37
- package/lib/compiler/utils.js +9 -0
- package/lib/edm/annotations/edmJson.js +35 -61
- package/lib/edm/annotations/genericTranslation.js +13 -5
- package/lib/edm/annotations/preprocessAnnotations.js +2 -3
- package/lib/edm/csn2edm.js +4 -1
- package/lib/edm/edmInboundChecks.js +59 -15
- package/lib/edm/edmPreprocessor.js +1 -7
- package/lib/gen/Dictionary.json +8 -0
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +12 -2
- package/lib/gen/languageParser.js +6095 -5195
- package/lib/json/from-csn.js +4 -5
- package/lib/json/to-csn.js +22 -3
- package/lib/language/errorStrategy.js +7 -3
- package/lib/language/genericAntlrParser.js +120 -24
- package/lib/language/textUtils.js +16 -0
- package/lib/model/csnUtils.js +9 -8
- package/lib/model/revealInternalProperties.js +5 -2
- package/lib/modelCompare/compare.js +10 -4
- package/lib/optionProcessor.js +2 -3
- package/lib/render/toCdl.js +31 -13
- package/lib/render/toHdbcds.js +20 -30
- package/lib/render/toSql.js +33 -54
- package/lib/render/utils/common.js +24 -6
- package/lib/transform/db/applyTransformations.js +59 -2
- package/lib/transform/db/backlinks.js +13 -1
- package/lib/transform/db/expansion.js +24 -3
- package/lib/transform/db/flattening.js +2 -2
- package/lib/transform/db/killAnnotations.js +37 -0
- package/lib/transform/db/rewriteCalculatedElements.js +46 -6
- package/lib/transform/forOdata.js +13 -46
- package/lib/transform/forRelationalDB.js +2 -1
- package/lib/transform/translateAssocsToJoins.js +13 -4
- package/lib/transform/universalCsn/coreComputed.js +1 -1
- package/lib/transform/universalCsn/universalCsnEnricher.js +4 -4
- package/package.json +7 -6
package/lib/json/from-csn.js
CHANGED
|
@@ -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', '
|
|
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
|
-
|
|
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,
|
|
1344
|
+
? { val, location: location() }
|
|
1346
1345
|
: natnum( val, spec );
|
|
1347
1346
|
}
|
|
1348
1347
|
|
package/lib/json/to-csn.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
530
|
-
|
|
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
|
-
|
|
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
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
-
|
|
967
|
-
|
|
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
|
-
|
|
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
|
|
1177
|
-
|
|
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
|
-
|
|
1181
|
-
|
|
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
|
};
|
package/lib/model/csnUtils.js
CHANGED
|
@@ -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
|
-
|
|
1122
|
-
|
|
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) {
|
|
325
|
-
outer = (node.
|
|
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
|
|
package/lib/optionProcessor.js
CHANGED
|
@@ -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')
|
package/lib/render/toCdl.js
CHANGED
|
@@ -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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 `${
|
|
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',
|