@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.
- package/CHANGELOG.md +52 -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/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/base/messages.js
CHANGED
|
@@ -797,15 +797,16 @@ function tokenSymbol( token ) {
|
|
|
797
797
|
* Transform an element reference (/path), e.g. on-condition path.
|
|
798
798
|
*/
|
|
799
799
|
function transformElementRef( arg ) {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
return quoted(arg
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
800
|
+
const ref = arg.ref || arg.path;
|
|
801
|
+
if (!ref)
|
|
802
|
+
return quoted( arg );
|
|
803
|
+
// Can be used by CSN backends or compiler to create a simple path such as E:elem
|
|
804
|
+
return quoted(
|
|
805
|
+
ref.map(
|
|
806
|
+
item => (typeof item !== 'string'
|
|
807
|
+
? `${ item.id }${item.args ? '(…)' : ''}${item.where ? '[…]' : ''}`
|
|
808
|
+
: item) )
|
|
809
|
+
.join('.') );
|
|
809
810
|
}
|
|
810
811
|
|
|
811
812
|
function transformArg( arg, r, args, texts ) {
|
|
@@ -1299,7 +1300,7 @@ function deduplicateMessages( messages ) {
|
|
|
1299
1300
|
}
|
|
1300
1301
|
|
|
1301
1302
|
function shortArtName( art ) {
|
|
1302
|
-
if (!art.name)
|
|
1303
|
+
if (!art.name || art.kind === '$annotation')
|
|
1303
1304
|
return artName( art );
|
|
1304
1305
|
const name = getArtifactName( art );
|
|
1305
1306
|
if ([ 'select', 'action', 'alias', 'param' ].every( n => name[n] == null || name[n] === 1 ) &&
|
|
@@ -1310,7 +1311,7 @@ function shortArtName( art ) {
|
|
|
1310
1311
|
|
|
1311
1312
|
function artName( art, omit ) {
|
|
1312
1313
|
let suffix = 0;
|
|
1313
|
-
while (!art.name && art._outer) {
|
|
1314
|
+
while (!art.name && art._outer && art.kind !== '$annotation') {
|
|
1314
1315
|
++suffix;
|
|
1315
1316
|
art = art._outer;
|
|
1316
1317
|
}
|
|
@@ -1367,8 +1368,12 @@ function homeName( art, absoluteOnly ) {
|
|
|
1367
1368
|
return art;
|
|
1368
1369
|
if (art._user) // when providing a path item with filter as “user”
|
|
1369
1370
|
return homeName( art._user, absoluteOnly );
|
|
1370
|
-
if (art._outer)
|
|
1371
|
-
|
|
1371
|
+
if (art._outer) { // in items property, or annotation with path
|
|
1372
|
+
const outer = homeName( art._outer, absoluteOnly );
|
|
1373
|
+
return (art.kind === '$annotation')
|
|
1374
|
+
? `${ outer }/${ quoted( '@' + art.name.id ) }`
|
|
1375
|
+
: outer;
|
|
1376
|
+
}
|
|
1372
1377
|
else if (art.kind === 'source' || !art.name) // error reported in parser or on source level
|
|
1373
1378
|
return null;
|
|
1374
1379
|
else if (art.kind === 'using')
|
package/lib/base/model.js
CHANGED
|
@@ -27,6 +27,7 @@ const queryOps = {
|
|
|
27
27
|
const availableBetaFlags = {
|
|
28
28
|
// enabled by --beta-mode
|
|
29
29
|
annotationExpressions: true,
|
|
30
|
+
odataAnnotationExpressions: true,
|
|
30
31
|
assocsWithParams: true, // beta, because runtimes don't support it, yet.
|
|
31
32
|
hanaAssocRealCardinality: true,
|
|
32
33
|
mapAssocToJoinCardinality: true, // only SAP HANA HEX engine supports it
|
|
@@ -35,6 +36,8 @@ const availableBetaFlags = {
|
|
|
35
36
|
optionalActionFunctionParameters: true, // not supported by runtime, yet.
|
|
36
37
|
annotateForeignKeys: true,
|
|
37
38
|
effectiveCsn: true,
|
|
39
|
+
tenantVariable: true,
|
|
40
|
+
calcAssoc: true,
|
|
38
41
|
// disabled by --beta-mode
|
|
39
42
|
nestedServices: false,
|
|
40
43
|
};
|
|
@@ -66,9 +66,58 @@ function checkReadOnlyAndInsertOnly( artifact, artifactName ) {
|
|
|
66
66
|
this.warning(null, artifact.$path, {}, 'Annotations “@readonly” and “@insertonly” can\'t be assigned in combination');
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Check temporal annotations @cds.valid.from, @cds.valid.to, @cds.valid.key
|
|
71
|
+
* assignment for the given artifact. This consists of the following:
|
|
72
|
+
* - @cds.valid.from/to/key annotation is assigned only once in the scope of the definition
|
|
73
|
+
* - annotation is assigned only to allowed element types. Not allowed on association/composition,
|
|
74
|
+
* structured elements, leaf element of a structure
|
|
75
|
+
* - when @cds.valid.key is used, it requires also @cds.valid.from and @cds.valid.to to be defined
|
|
76
|
+
* @param {CSN.Artifact} artifact
|
|
77
|
+
* @param {string} artifactName
|
|
78
|
+
*/
|
|
79
|
+
function checkTemporalAnnotationsAssignment( artifact, artifactName ) {
|
|
80
|
+
const valid = { from: [], to: [], key: [] };
|
|
81
|
+
|
|
82
|
+
// collect annotation assignments throughout the elements of the definition
|
|
83
|
+
this.recurseElements( artifact, artifact.$path || [ 'definitions', artifactName ], (member, path) => {
|
|
84
|
+
checkForAnnoAssignmentAndApplicability.bind(this)('from', member, path);
|
|
85
|
+
checkForAnnoAssignmentAndApplicability.bind(this)('to', member, path);
|
|
86
|
+
checkForAnnoAssignmentAndApplicability.bind(this)('key', member, path);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// check if the annotations are assigned more than once in the scope of the current artifact
|
|
90
|
+
this.checkMultipleAssignments(valid.from, '@cds.valid.from', artifact, artifactName);
|
|
91
|
+
this.checkMultipleAssignments(valid.to, '@cds.valid.to', artifact, artifactName);
|
|
92
|
+
this.checkMultipleAssignments(valid.key, '@cds.valid.key', artifact, artifactName);
|
|
93
|
+
|
|
94
|
+
// if @cds.valid.key is defined, check whether @cds.valid.from and @cds.valid.to are also there
|
|
95
|
+
if (valid.key.length && !(valid.from.length && valid.to.length))
|
|
96
|
+
this.error(null, [ 'definitions', artifactName ], 'Annotation “@cds.valid.key” was used but “@cds.valid.from” and “@cds.valid.to” are missing');
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if the given annotation is assigned to the current member and collect the path if so.
|
|
100
|
+
* Also determine whether the annotation is applicable for the member type. @cds.valid.from/to.key annotations
|
|
101
|
+
* are NOT allowed for elements which are: association/composition, structured or leaf element of a structure
|
|
102
|
+
*
|
|
103
|
+
* @param {string} annoIdentifier
|
|
104
|
+
* @param {CSN.Element} member
|
|
105
|
+
* @param {CSN.Path} path
|
|
106
|
+
*/
|
|
107
|
+
function checkForAnnoAssignmentAndApplicability( annoIdentifier, member, path ) {
|
|
108
|
+
if (this.csnUtils.hasAnnotationValue(member, `@cds.valid.${ annoIdentifier }`)) {
|
|
109
|
+
valid[annoIdentifier].push(path);
|
|
110
|
+
// check whether annotation is not assigned to not allowed element type, these are: association, structured elements, leaf element of a structure
|
|
111
|
+
if (this.csnUtils.isAssocOrComposition(member) || this.csnUtils.isStructured(member) || path.length > 5)
|
|
112
|
+
this.error(null, member.$path, { anno: `@cds.valid.${ annoIdentifier }` }, 'Element can\'t be annotated with $(ANNO)');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
69
117
|
module.exports = {
|
|
70
118
|
checkCoreMediaTypeAllowance,
|
|
71
119
|
checkAnalytics,
|
|
72
120
|
checkAtSapAnnotations,
|
|
73
121
|
checkReadOnlyAndInsertOnly,
|
|
122
|
+
checkTemporalAnnotationsAssignment,
|
|
74
123
|
};
|
package/lib/checks/validator.js
CHANGED
|
@@ -22,6 +22,7 @@ const { checkActionOrFunction } = require('./actionsFunctions');
|
|
|
22
22
|
const {
|
|
23
23
|
checkCoreMediaTypeAllowance, checkAnalytics,
|
|
24
24
|
checkAtSapAnnotations, checkReadOnlyAndInsertOnly,
|
|
25
|
+
checkTemporalAnnotationsAssignment,
|
|
25
26
|
} = require('./annotationsOData');
|
|
26
27
|
// both
|
|
27
28
|
const { validateOnCondition, validateMixinOnCondition } = require('./onConditions');
|
|
@@ -136,10 +137,10 @@ const commonQueryValidators = [ validateMixinOnCondition ];
|
|
|
136
137
|
*
|
|
137
138
|
* @param {CSN.Model} csn CSN to check
|
|
138
139
|
* @param {object} that Will be provided to the validators via "this"
|
|
139
|
-
* @param {object[]} [csnValidators
|
|
140
|
-
* @param {Function[]} [memberValidators
|
|
141
|
-
* @param {Function[]} [artifactValidators
|
|
142
|
-
* @param {Function[]} [queryValidators
|
|
140
|
+
* @param {object[]} [csnValidators] Validations on whole CSN using applyTransformations
|
|
141
|
+
* @param {Function[]} [memberValidators] Validations on member-level
|
|
142
|
+
* @param {Function[]} [artifactValidators] Validations on artifact-level
|
|
143
|
+
* @param {Function[]} [queryValidators] Validations on query-level
|
|
143
144
|
* @param {object} iterateOptions can be used to skip certain kinds from being iterated e.g. 'action' and 'function' for hana
|
|
144
145
|
* @returns {Function} Function taking no parameters, that cleans up the attached helpers
|
|
145
146
|
*/
|
|
@@ -267,6 +268,7 @@ function forOdata( csn, that ) {
|
|
|
267
268
|
checkAtSapAnnotations.bind(that),
|
|
268
269
|
]);
|
|
269
270
|
}
|
|
271
|
+
checkTemporalAnnotationsAssignment.bind(that)(artifact, artifactName);
|
|
270
272
|
}
|
|
271
273
|
),
|
|
272
274
|
// eslint-disable-next-line sonarjs/no-empty-collection
|
|
@@ -126,6 +126,7 @@ function assertConsistency( model, stage ) {
|
|
|
126
126
|
'@sql_mapping', // TODO: it is time that a 'header' attribute replaces 'version'
|
|
127
127
|
'$withLocalized',
|
|
128
128
|
'$sources',
|
|
129
|
+
'tokenStream',
|
|
129
130
|
],
|
|
130
131
|
instanceOf: XsnSource,
|
|
131
132
|
},
|
|
@@ -196,13 +197,14 @@ function assertConsistency( model, stage ) {
|
|
|
196
197
|
schema: {
|
|
197
198
|
kind: { test: isString, enum: [ '$magicVariables' ] },
|
|
198
199
|
elements: {
|
|
199
|
-
// Do not use "normal" definitions spec because
|
|
200
|
+
// Do not use "normal" definitions spec because these artifacts
|
|
200
201
|
// are missing the location property
|
|
201
202
|
test: isDictionary( definition ),
|
|
202
203
|
requires: [ 'kind', 'name' ],
|
|
203
204
|
optional: [
|
|
204
205
|
'elements', '$autoElement', '$uncheckedElements', '_origin', '_extensions',
|
|
205
|
-
'$requireElementAccess', '_effectiveType', '$effectiveSeqNo', '_deps',
|
|
206
|
+
'$requireElementAccess', '_effectiveType', '$effectiveSeqNo', '_deps',
|
|
207
|
+
'$calcDepElement', '$filtered', '_parent',
|
|
206
208
|
],
|
|
207
209
|
schema: {
|
|
208
210
|
kind: { test: isString, enum: [ 'builtin' ] },
|
|
@@ -249,10 +251,13 @@ function assertConsistency( model, stage ) {
|
|
|
249
251
|
elements$: { kind: true, enumerable: false, test: TODO },
|
|
250
252
|
enum$: { kind: true, enumerable: false, test: TODO },
|
|
251
253
|
typeProps$: { kind: true, enumerable: false, test: TODO },
|
|
254
|
+
// helper property for faster processing:
|
|
255
|
+
$contains: { kind: true, test: TODO },
|
|
252
256
|
actions: { kind: true, inherits: 'definitions' },
|
|
253
257
|
enum: { kind: true, inherits: 'definitions' },
|
|
254
258
|
foreignKeys: { kind: true, inherits: 'definitions', instanceOf: 'ignore' },
|
|
255
259
|
$keysNavigation: { kind: true, test: TODO },
|
|
260
|
+
$filtered: { kind: true, inherits: 'value' }, // for assoc+filter
|
|
256
261
|
params: { kind: true, inherits: 'definitions' },
|
|
257
262
|
_extendType: { kind: true, test: TODO },
|
|
258
263
|
mixin: { inherits: 'definitions' },
|
|
@@ -264,16 +269,18 @@ function assertConsistency( model, stage ) {
|
|
|
264
269
|
requires: [ 'op', 'location', 'args' ],
|
|
265
270
|
optional: [
|
|
266
271
|
'quantifier', 'orderBy', 'limit', 'name', '$parens', 'kind',
|
|
267
|
-
'_origin',
|
|
272
|
+
'_origin', '$contains', // TODO tmp, see TODO in getOriginRaw()
|
|
268
273
|
'_parent', '_main', '_leadingQuery', '_effectiveType', '$effectiveSeqNo', // in FROM
|
|
274
|
+
'_$next', // parsing error: tableTerm with UNION on rhs.
|
|
269
275
|
],
|
|
270
276
|
},
|
|
271
277
|
select: { // sub query
|
|
272
278
|
requires: [ 'op', 'location', 'from' ],
|
|
273
279
|
optional: [
|
|
274
280
|
'name', '$parens', 'quantifier', 'mixin', 'excludingDict', 'columns', 'elements', '_deps',
|
|
281
|
+
'$calcDepElement',
|
|
275
282
|
'where', 'groupBy', 'having', 'orderBy', '$orderBy', 'limit', '$limit',
|
|
276
|
-
'_origin', '_block',
|
|
283
|
+
'_origin', '_block', '$contains',
|
|
277
284
|
'_projections', '_parent', '_main', '_effectiveType', '$effectiveSeqNo', '$expand',
|
|
278
285
|
'$tableAliases', 'kind', '_$next', '_combined', '$inlines', '_status',
|
|
279
286
|
],
|
|
@@ -396,7 +403,7 @@ function assertConsistency( model, stage ) {
|
|
|
396
403
|
'annotate', 'extend', '$column',
|
|
397
404
|
'select', '$join', 'mixin',
|
|
398
405
|
'source', 'namespace', 'using',
|
|
399
|
-
'$tableAlias', '$navElement',
|
|
406
|
+
'$tableAlias', '$navElement', '$calculation', '$annotation',
|
|
400
407
|
'builtin', // magic variables
|
|
401
408
|
],
|
|
402
409
|
},
|
|
@@ -505,7 +512,8 @@ function assertConsistency( model, stage ) {
|
|
|
505
512
|
optional: [
|
|
506
513
|
'name', '_block', '$priority', '$inferred', '$duplicates', '$errorReported',
|
|
507
514
|
// annotation values
|
|
508
|
-
'$tokenTexts',
|
|
515
|
+
'$tokenTexts', 'kind', '_outer',
|
|
516
|
+
'_effectiveType', '$effectiveSeqNo', '_origin', '_deps',
|
|
509
517
|
// CSN parser may let these properties slip through to XSN, even if input is invalid.
|
|
510
518
|
'args', 'op', 'func', 'suffix',
|
|
511
519
|
],
|
|
@@ -554,6 +562,7 @@ function assertConsistency( model, stage ) {
|
|
|
554
562
|
'elements', 'cardinality', 'target', 'on', 'foreignKeys', 'items',
|
|
555
563
|
'_outer', '_effectiveType', '$effectiveSeqNo', 'notNull', '_parent',
|
|
556
564
|
'_origin', '_block', '$inferred', '$expand', '$inCycle', '_deps',
|
|
565
|
+
'$calcDepElement',
|
|
557
566
|
'$syntax', '_extensions',
|
|
558
567
|
'_status', '_redirected',
|
|
559
568
|
...typeProperties,
|
|
@@ -563,20 +572,20 @@ function assertConsistency( model, stage ) {
|
|
|
563
572
|
artifacts: { kind: true, inherits: 'definitions', test: isDictionary( inDefinitions ) },
|
|
564
573
|
_subArtifacts: { kind: true, inherits: 'definitions', test: isDictionary( inDefinitions ) },
|
|
565
574
|
blocks: { kind: true, test: TODO }, // TODO: make it $blocks ?
|
|
566
|
-
length: { kind: true,
|
|
567
|
-
precision: { kind: true,
|
|
568
|
-
scale: { kind: true,
|
|
569
|
-
srid: { kind: true,
|
|
575
|
+
length: { kind: true, test: isNumberVal }, // for number is to be checked in resolver
|
|
576
|
+
precision: { kind: true, test: isNumberVal },
|
|
577
|
+
scale: { kind: true, test: isNumberVal, also: [ 'floating', 'variable' ] },
|
|
578
|
+
srid: { kind: true, test: isNumberVal },
|
|
570
579
|
localized: { kind: true, test: locationVal() },
|
|
571
580
|
cardinality: {
|
|
572
581
|
kind: true,
|
|
573
582
|
requires: [ 'location' ],
|
|
574
583
|
optional: [ 'sourceMin', 'sourceMax', 'targetMin', 'targetMax' ],
|
|
575
584
|
},
|
|
576
|
-
sourceMin: { test:
|
|
577
|
-
sourceMax: { test:
|
|
578
|
-
targetMin: { test:
|
|
579
|
-
targetMax: { test:
|
|
585
|
+
sourceMin: { test: isNumberVal },
|
|
586
|
+
sourceMax: { test: isNumberVal, also: [ '*' ] },
|
|
587
|
+
targetMin: { test: isNumberVal },
|
|
588
|
+
targetMax: { test: isNumberVal, also: [ '*' ] },
|
|
580
589
|
default: { kind: true, inherits: 'value' },
|
|
581
590
|
$typeArgs: { parser: true, kind: true, test: TODO },
|
|
582
591
|
$tableAliases: { kind: true, test: TODO }, // containing $self outside queries
|
|
@@ -585,13 +594,18 @@ function assertConsistency( model, stage ) {
|
|
|
585
594
|
_service: { kind: true, test: TODO },
|
|
586
595
|
_main: { kind: true, test: TODO },
|
|
587
596
|
_user: { kind: true, test: TODO },
|
|
597
|
+
// - on a path item with a filter condition to the user of the ref (not nested)
|
|
598
|
+
// - on a JOIN node to the query (TODO: _outer?)
|
|
588
599
|
_artifact: { test: TODO },
|
|
589
600
|
_navigation: { test: TODO },
|
|
590
601
|
_effectiveType: { kind: true, test: TODO },
|
|
591
602
|
$effectiveSeqNo: { kind: true, test: isNumber },
|
|
592
603
|
_joinParent: { test: TODO },
|
|
593
604
|
$joinArgsIndex: { test: isNumber },
|
|
594
|
-
_outer: { test: TODO }, // for
|
|
605
|
+
_outer: { test: TODO }, // for items
|
|
606
|
+
// - on an array item to the array elem/type/item (nested)
|
|
607
|
+
// - on an anonymous aspect to the composition element
|
|
608
|
+
// - on an annotation assignment to the annotatee
|
|
595
609
|
$queries: {
|
|
596
610
|
kind: [ 'entity', 'event' ],
|
|
597
611
|
test: isArray(),
|
|
@@ -605,7 +619,7 @@ function assertConsistency( model, stage ) {
|
|
|
605
619
|
],
|
|
606
620
|
optional: [
|
|
607
621
|
'_effectiveType', '$effectiveSeqNo', '$parens',
|
|
608
|
-
'_deps', '$expand',
|
|
622
|
+
'_deps', '$calcDepElement', '$expand', '$contains',
|
|
609
623
|
// query specific
|
|
610
624
|
'where', 'columns', 'mixin', 'quantifier', 'offset',
|
|
611
625
|
'orderBy', '$orderBy', 'groupBy', 'excludingDict', 'having',
|
|
@@ -628,6 +642,9 @@ function assertConsistency( model, stage ) {
|
|
|
628
642
|
_annotate: { kind: true, test: TODO }, // for collecting extend/annotate on artifact
|
|
629
643
|
_extension: { kind: true, test: TODO }, // on artifact to its "super extend/annotate" statement
|
|
630
644
|
_deps: { kind: true, test: TODO }, // for cyclic calculation
|
|
645
|
+
// a fake element for cyclic dependency detection: e.g. dependencies to target entities.
|
|
646
|
+
// dependants don't only depend on the calc element, but on this element as well.
|
|
647
|
+
$calcDepElement: { kind: true, test: TODO },
|
|
631
648
|
_scc: { kind: true, test: TODO }, // for cyclic calculation
|
|
632
649
|
_sccCaller: { kind: true, test: TODO }, // for cyclic calculation
|
|
633
650
|
_status: { kind: true, test: TODO }, // TODO: $status
|
|
@@ -703,6 +720,7 @@ function assertConsistency( model, stage ) {
|
|
|
703
720
|
$extra: { parser: true, test: TODO }, // for unexpected properties in CSN
|
|
704
721
|
$withLocalized: { test: isBoolean },
|
|
705
722
|
$sources: { parser: true, test: isArray( isString ) },
|
|
723
|
+
tokenStream: { parser: true, test: TODO },
|
|
706
724
|
$expected: { parser: true, test: isOneOf( [ 'approved-exists', 'exists' ] ) },
|
|
707
725
|
$messageFunctions: { test: TODO },
|
|
708
726
|
$functions: { test: TODO },
|
|
@@ -958,6 +976,10 @@ function assertConsistency( model, stage ) {
|
|
|
958
976
|
throw new InternalConsistencyError( `Expected boolean or null${ at( [ node, parent ], prop ) }` );
|
|
959
977
|
}
|
|
960
978
|
|
|
979
|
+
function isNumberVal() {
|
|
980
|
+
return locationVal( isNumber );
|
|
981
|
+
}
|
|
982
|
+
|
|
961
983
|
function isNumber( node, parent, prop, spec ) {
|
|
962
984
|
if (spec.also && spec.also.includes( node ))
|
|
963
985
|
return;
|
package/lib/compiler/builtins.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
const { builtinLocation } = require('../base/location');
|
|
11
11
|
const { setLink: setProp } = require('./utils');
|
|
12
|
+
const { isBetaEnabled } = require('../base/model');
|
|
12
13
|
|
|
13
14
|
// TODO: make type parameters a dict
|
|
14
15
|
const core = {
|
|
@@ -190,14 +191,14 @@ function compileArg( src ) {
|
|
|
190
191
|
*/
|
|
191
192
|
const magicVariables = {
|
|
192
193
|
$user: {
|
|
193
|
-
//
|
|
194
|
-
elements: { id: {}, locale: {}
|
|
194
|
+
// always available
|
|
195
|
+
elements: { id: {}, locale: {} },
|
|
195
196
|
// Allow $user.<any>
|
|
196
197
|
$uncheckedElements: true,
|
|
197
198
|
// Allow shortcut in CDL: `$user` becomes `$user.id` in CSN.
|
|
198
199
|
$autoElement: 'id',
|
|
199
200
|
},
|
|
200
|
-
$at: {
|
|
201
|
+
$at: {
|
|
201
202
|
elements: {
|
|
202
203
|
from: {}, to: {},
|
|
203
204
|
},
|
|
@@ -211,7 +212,8 @@ const magicVariables = {
|
|
|
211
212
|
// Require that elements are accessed, i.e. no $valid, only $valid.<element>.
|
|
212
213
|
$requireElementAccess: true,
|
|
213
214
|
},
|
|
214
|
-
$now: {},
|
|
215
|
+
$now: {},
|
|
216
|
+
$tenant: { $requiresBetaFlag: 'tenantVariable' },
|
|
215
217
|
$session: {
|
|
216
218
|
// In ABAP CDS session variables are accessed in a generic way via
|
|
217
219
|
// the pseudo variable $session.
|
|
@@ -229,7 +231,7 @@ const timeRegEx = /^T?(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?(?:Z|[+-]\d{2}(?::\d{2})
|
|
|
229
231
|
// eslint-disable-next-line max-len
|
|
230
232
|
const timestampRegEx = /^(-?\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2})(\.\d{1,7})?)?(?:Z|[+-]\d{2}(?::\d{2})?)?$/;
|
|
231
233
|
// YYYY - MM - dd T HH : mm : ss . fraction TZD
|
|
232
|
-
const numberRegEx = /^[ \t]*[-+]?(\d+(\.\d*)?|\.\d+)(e[-+]
|
|
234
|
+
const numberRegEx = /^[ \t]*[-+]?(\d+(\.\d*)?|\.\d+)(e[-+]?\d+)?[ \t]*$/i;
|
|
233
235
|
|
|
234
236
|
/**
|
|
235
237
|
* Patterns for literal token tests and creation. The value is a map from the
|
|
@@ -364,46 +366,10 @@ Object.keys( coreHana ).forEach( (type) => {
|
|
|
364
366
|
typeCategories[coreHana[type].category].push( `cds.hana.${ type }` );
|
|
365
367
|
} );
|
|
366
368
|
|
|
367
|
-
/** @param {string} typeName */
|
|
368
|
-
function isIntegerTypeName( typeName ) {
|
|
369
|
-
return typeCategories.integer.includes( typeName );
|
|
370
|
-
}
|
|
371
|
-
/** @param {string} typeName */
|
|
372
|
-
function isDecimalTypeName( typeName ) {
|
|
373
|
-
return typeCategories.decimal.includes( typeName );
|
|
374
|
-
}
|
|
375
|
-
/** @param {string} typeName */
|
|
376
|
-
function isNumericTypeName( typeName ) {
|
|
377
|
-
return isIntegerTypeName( typeName ) || isDecimalTypeName( typeName );
|
|
378
|
-
}
|
|
379
|
-
/** @param {string} typeName */
|
|
380
|
-
function isStringTypeName( typeName ) {
|
|
381
|
-
return typeCategories.string.includes( typeName );
|
|
382
|
-
}
|
|
383
|
-
/** @param {string} typeName */
|
|
384
|
-
function isDateOrTimeTypeName( typeName ) {
|
|
385
|
-
return typeCategories.dateTime.includes( typeName );
|
|
386
|
-
}
|
|
387
|
-
/** @param {string} typeName */
|
|
388
|
-
function isBooleanTypeName( typeName ) {
|
|
389
|
-
return typeCategories.boolean.includes( typeName );
|
|
390
|
-
}
|
|
391
|
-
/** @param {string} typeName */
|
|
392
|
-
function isBinaryTypeName( typeName ) {
|
|
393
|
-
return typeCategories.binary.includes( typeName );
|
|
394
|
-
}
|
|
395
369
|
/** @param {string} typeName */
|
|
396
370
|
function isGeoTypeName( typeName ) {
|
|
397
371
|
return typeCategories.geo.includes( typeName );
|
|
398
372
|
}
|
|
399
|
-
/**
|
|
400
|
-
* Whether the given type name is a relation, i.e. an association or composition.
|
|
401
|
-
*
|
|
402
|
-
* @param {string} typeName
|
|
403
|
-
*/
|
|
404
|
-
function isRelationTypeName( typeName ) {
|
|
405
|
-
return typeCategories.relation.includes( typeName );
|
|
406
|
-
}
|
|
407
373
|
|
|
408
374
|
/**
|
|
409
375
|
* Checks whether the given absolute path is inside a reserved namespace.
|
|
@@ -507,6 +473,9 @@ function initBuiltins( model ) {
|
|
|
507
473
|
model.$magicVariables = { kind: '$magicVariables', elements };
|
|
508
474
|
for (const id in builtins) {
|
|
509
475
|
const magic = builtins[id];
|
|
476
|
+
if (magic.$requiresBetaFlag && !isBetaEnabled( options, magic.$requiresBetaFlag ))
|
|
477
|
+
continue;
|
|
478
|
+
|
|
510
479
|
// TODO: rename to $builtinFunction
|
|
511
480
|
const art = {
|
|
512
481
|
kind: 'builtin', // TODO: $var
|
|
@@ -565,13 +534,5 @@ module.exports = {
|
|
|
565
534
|
isAnnotationExpression,
|
|
566
535
|
isInReservedNamespace,
|
|
567
536
|
isBuiltinType,
|
|
568
|
-
isIntegerTypeName,
|
|
569
|
-
isDecimalTypeName,
|
|
570
|
-
isNumericTypeName,
|
|
571
|
-
isStringTypeName,
|
|
572
|
-
isDateOrTimeTypeName,
|
|
573
|
-
isBooleanTypeName,
|
|
574
|
-
isBinaryTypeName,
|
|
575
537
|
isGeoTypeName,
|
|
576
|
-
isRelationTypeName,
|
|
577
538
|
};
|
package/lib/compiler/checks.js
CHANGED
|
@@ -182,12 +182,14 @@ function check( model ) {
|
|
|
182
182
|
} );
|
|
183
183
|
break; // Avoid spam: Only emit the first error.
|
|
184
184
|
}
|
|
185
|
-
else if (!typeParameters.expectedLiteralsFor[param].includes( art[param].
|
|
185
|
+
else if (!typeParameters.expectedLiteralsFor[param].includes( typeof art[param].val )) {
|
|
186
|
+
// TODO: this could be probably better done via syntax check (already for CSN input)
|
|
186
187
|
error( 'type-unexpected-argument', [ art[param].location, user ], {
|
|
187
188
|
'#': 'incorrect-type',
|
|
188
189
|
prop: param,
|
|
189
|
-
code: art[param].
|
|
190
|
+
code: typeof art[param].val,
|
|
190
191
|
names: typeParameters.expectedLiteralsFor[param],
|
|
192
|
+
// TODO: no double quote via $(NAMES), but see TODO above
|
|
191
193
|
} );
|
|
192
194
|
break; // Avoid spam: Only emit the first error.
|
|
193
195
|
}
|
|
@@ -422,8 +424,8 @@ function check( model ) {
|
|
|
422
424
|
// Max cardinalities must be a positive number or '*'
|
|
423
425
|
for (const prop of [ 'sourceMax', 'targetMax' ]) {
|
|
424
426
|
if (art.cardinality[prop]) {
|
|
425
|
-
const {
|
|
426
|
-
if (
|
|
427
|
+
const { val, location } = art.cardinality[prop];
|
|
428
|
+
if (val !== '*' && val <= 0) {
|
|
427
429
|
error( 'type-invalid-cardinality', [ location, art ],
|
|
428
430
|
{ '#': prop, prop: val, otherprop: '*' } );
|
|
429
431
|
}
|
|
@@ -630,24 +632,30 @@ function check( model ) {
|
|
|
630
632
|
}
|
|
631
633
|
|
|
632
634
|
function checkCalculatedElementValue( elem ) {
|
|
635
|
+
const isStored = elem.value.stored?.val;
|
|
633
636
|
visitExpression( elem.value, elem, (xpr, user) => {
|
|
634
637
|
// We only need to check artifact references. To avoid false positives and conflicts
|
|
635
638
|
// with $self comparison-checks, ignore bare $self.
|
|
636
639
|
const isArtRef = xpr._artifact && !(xpr.path?.length === 1 &&
|
|
637
640
|
xpr.path[0]._navigation?.kind === '$self');
|
|
638
641
|
if (isArtRef) {
|
|
639
|
-
const
|
|
642
|
+
const lastStep = xpr.path?.[xpr.path.length - 1];
|
|
643
|
+
const sourceLoc = lastStep.location || xpr.location;
|
|
640
644
|
checkExpressionNotVirtual( xpr, user );
|
|
641
645
|
// For inferred (e.g. included) calc elements, this error is already emitted at the origin.
|
|
642
646
|
// And users can't change structured to non-structured elements.
|
|
643
647
|
if (!elem.$inferred && xpr._artifact._effectiveType?.elements) {
|
|
644
648
|
error( 'ref-unexpected-structured', [ sourceLoc, elem ], { '#': 'expr' } );
|
|
645
649
|
}
|
|
646
|
-
else if (xpr._artifact.target !== undefined) {
|
|
647
|
-
|
|
650
|
+
else if (xpr._artifact.target !== undefined && (!lastStep.where || isStored)) {
|
|
651
|
+
// Allow using an association _with filter_, but only for on-read calculated elements.
|
|
652
|
+
// TODO: Also allow bare unmanaged association references and remove beta.
|
|
653
|
+
const variant = (isStored && lastStep.where && 'assoc-stored') ||
|
|
654
|
+
(isComposition( model, xpr._artifact ) && 'expr-comp') ||
|
|
655
|
+
'expr';
|
|
648
656
|
error( 'ref-unexpected-assoc', [ sourceLoc, elem ], { '#': variant } );
|
|
649
657
|
}
|
|
650
|
-
else if (xpr._artifact.localized?.val &&
|
|
658
|
+
else if (xpr._artifact.localized?.val && isStored) {
|
|
651
659
|
error( 'ref-unexpected-localized', [ sourceLoc, elem ], { '#': 'calc' } );
|
|
652
660
|
}
|
|
653
661
|
}
|
|
@@ -30,10 +30,7 @@ function detectCycles( definitions, reportCycle, cbScc ) {
|
|
|
30
30
|
|
|
31
31
|
for (const name in definitions) {
|
|
32
32
|
const a = definitions[name];
|
|
33
|
-
|
|
34
|
-
a.forEach( strongConnectRec );
|
|
35
|
-
else
|
|
36
|
-
strongConnectRec( a );
|
|
33
|
+
strongConnectRec( a );
|
|
37
34
|
}
|
|
38
35
|
// now the cleanup
|
|
39
36
|
let nodes = Object.getOwnPropertyNames( definitions ).map( n => definitions[n] );
|
package/lib/compiler/define.js
CHANGED
|
@@ -133,6 +133,7 @@ const { kindProperties, dictKinds } = require('./base');
|
|
|
133
133
|
const {
|
|
134
134
|
setLink,
|
|
135
135
|
setMemberParent,
|
|
136
|
+
createAndLinkCalcDepElement,
|
|
136
137
|
storeExtension,
|
|
137
138
|
dependsOnSilent,
|
|
138
139
|
pathName,
|
|
@@ -974,7 +975,7 @@ function define( model ) {
|
|
|
974
975
|
// Drill down
|
|
975
976
|
if (exprOrPathElement.args)
|
|
976
977
|
exprOrPathElement.args.forEach( elem => approveExistsInChildren( elem ) );
|
|
977
|
-
else if (exprOrPathElement.where
|
|
978
|
+
else if (exprOrPathElement.where?.args)
|
|
978
979
|
exprOrPathElement.where.args.forEach( elem => approveExistsInChildren( elem ) );
|
|
979
980
|
else if (exprOrPathElement.path)
|
|
980
981
|
exprOrPathElement.path.forEach( elem => approveExistsInChildren( elem ) );
|
|
@@ -1169,6 +1170,8 @@ function define( model ) {
|
|
|
1169
1170
|
elem.type = { ...elem.value.type, $inferred: 'cast' };
|
|
1170
1171
|
}
|
|
1171
1172
|
elem.$syntax = 'calc';
|
|
1173
|
+
// TODO: it is not just "syntax" - maybe better test for `$calcDepElement`?
|
|
1174
|
+
createAndLinkCalcDepElement( elem );
|
|
1172
1175
|
}
|
|
1173
1176
|
}
|
|
1174
1177
|
|
package/lib/compiler/extend.js
CHANGED
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
copyExpr,
|
|
19
19
|
setExpandStatusAnnotate,
|
|
20
20
|
linkToOrigin,
|
|
21
|
+
createAndLinkCalcDepElement,
|
|
21
22
|
dependsOnSilent,
|
|
22
23
|
pathName,
|
|
23
24
|
annotationHasEllipsis,
|
|
@@ -390,7 +391,8 @@ function extend( model ) {
|
|
|
390
391
|
col.$extended = true;
|
|
391
392
|
|
|
392
393
|
if (!query?.from?.path) {
|
|
393
|
-
|
|
394
|
+
const variant = (query?.from || query)?.op?.val || 'std';
|
|
395
|
+
error( 'extend-columns', [ ext.columns[$location], ext ], { '#': variant, art } );
|
|
394
396
|
return;
|
|
395
397
|
}
|
|
396
398
|
if (!query.columns)
|
|
@@ -485,12 +487,13 @@ function extend( model ) {
|
|
|
485
487
|
if ('val' in upToSpec) {
|
|
486
488
|
if (previousItem.val === upToSpec.val) // enum, struct and ref have no val
|
|
487
489
|
return true;
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
490
|
+
// TODO v5: delete the speciao UP TO comparison?
|
|
491
|
+
const upToVal = upToSpec.val;
|
|
492
|
+
const prevVal = previousItem.val;
|
|
493
|
+
// eslint-disable-next-line eqeqeq
|
|
494
|
+
return prevVal == upToVal &&
|
|
495
|
+
( typeof upToVal === 'number' && stringCouldHaveBeenCdlNumber( prevVal ) ||
|
|
496
|
+
typeof prevVal === 'number' && stringCouldHaveBeenCdlNumber( upToVal ) );
|
|
494
497
|
}
|
|
495
498
|
else if (upToSpec.path) {
|
|
496
499
|
return previousItem.path && normalizeRef( previousItem ) === normalizeRef( upToSpec );
|
|
@@ -505,6 +508,16 @@ function extend( model ) {
|
|
|
505
508
|
return false;
|
|
506
509
|
}
|
|
507
510
|
|
|
511
|
+
// We only compare a string by number if the string is not empty, and could have
|
|
512
|
+
// been produced for a CDL number by (a previous version of) the compiler,
|
|
513
|
+
// i.e. having used a decimal dot, or using the scientific notation:
|
|
514
|
+
function stringCouldHaveBeenCdlNumber( val ) { // also consider previous compiler versions
|
|
515
|
+
return val && typeof val === 'string' && /[.eE]/.test( val );
|
|
516
|
+
// We do not use `!Number.isSafeInteger( Number.parseFloat( text||'0' )`
|
|
517
|
+
// because it is unlikely that people have written a non-integer like this,
|
|
518
|
+
// more likely is meant a digit-sequence as string
|
|
519
|
+
}
|
|
520
|
+
|
|
508
521
|
function normalizeRef( node ) { // see to-csn.js
|
|
509
522
|
const ref = pathName( node.path );
|
|
510
523
|
// TODO: get rid of name.variant (induces a wrong structure anyway)
|
|
@@ -1206,6 +1219,7 @@ function extend( model ) {
|
|
|
1206
1219
|
// TODO: Unify with coding in extend.js
|
|
1207
1220
|
elem.value = Object.assign( { $inferred: 'include' }, copyExpr( origin.value ));
|
|
1208
1221
|
elem.$syntax = 'calc';
|
|
1222
|
+
createAndLinkCalcDepElement( elem );
|
|
1209
1223
|
setLink( elem, '_calcOrigin', origin._calcOrigin || origin );
|
|
1210
1224
|
}
|
|
1211
1225
|
// TODO: also complain if elem is just defined in art
|
package/lib/compiler/generate.js
CHANGED
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
setAnnotation,
|
|
14
14
|
linkToOrigin,
|
|
15
15
|
setMemberParent,
|
|
16
|
+
createAndLinkCalcDepElement,
|
|
16
17
|
augmentPath,
|
|
17
18
|
isDirectComposition,
|
|
18
19
|
copyExpr,
|
|
@@ -770,6 +771,8 @@ function generate( model ) {
|
|
|
770
771
|
// TODO: Unify with coding in extend.js
|
|
771
772
|
proxy.value = Object.assign( { $inferred: 'include' }, copyExpr( origin.value ));
|
|
772
773
|
proxy.$syntax = 'calc';
|
|
774
|
+
createAndLinkCalcDepElement( proxy );
|
|
775
|
+
// TODO: re-check _calcOrigin
|
|
773
776
|
setLink( proxy, '_calcOrigin', origin._calcOrigin || origin );
|
|
774
777
|
}
|
|
775
778
|
if (anno)
|