@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
@@ -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
- if (arg.ref) {
801
- // Can be used by CSN backends to create a simple path such as E:elem
802
- return quoted(arg.ref.map((ref) => {
803
- if (ref.id)
804
- return `${ ref.id }${ref.args ? '(…)' : ''}${ref.where ? '[…]' : ''}`;
805
- return ref;
806
- }).join('.'));
807
- }
808
- return quoted(arg);
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) // in items property
1371
- return homeName( art._outer, absoluteOnly );
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
  };
@@ -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=[]] Validations on whole CSN using applyTransformations
140
- * @param {Function[]} [memberValidators=[]] Validations on member-level
141
- * @param {Function[]} [artifactValidators=[]] Validations on artifact-level
142
- * @param {Function[]} [queryValidators=[]] Validations on query-level
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 of these artifacts
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', '_parent',
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', // TODO tmp, see TODO in getOriginRaw()
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, inherits: 'value' }, // for number is to be checked in resolver
567
- precision: { kind: true, inherits: 'value' },
568
- scale: { kind: true, inherits: 'value' },
569
- srid: { kind: true, inherits: 'value' },
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: locationVal( isNumber ) },
577
- sourceMax: { test: locationVal( isNumber ), also: [ '*' ] },
578
- targetMin: { test: locationVal( isNumber ) },
579
- targetMax: { test: locationVal( isNumber ), also: [ '*' ] },
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 returns/items
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;
@@ -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
- // id and locale are always available
194
- elements: { id: {}, locale: {}, tenant: {} },
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: { // CDS-specific, not part of SQL
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: {}, // Dito
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[-+]\d+)?[ \t]*$/i;
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
  };
@@ -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].literal )) {
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].literal,
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 { literal, val, location } = art.cardinality[prop];
426
- if (!(literal === 'number' && val > 0 || literal === 'string' && val === '*')) {
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 sourceLoc = xpr.path?.[xpr.path.length - 1].location || xpr.location;
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
- const variant = isComposition( model, xpr._artifact ) ? 'expr-comp' : 'expr';
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 && elem.value.stored?.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
- if (Array.isArray( a ))
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] );
@@ -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 && exprOrPathElement.where.args)
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
 
@@ -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
- error( 'extend-columns', [ ext.columns[$location], ext ], { art } );
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
- const typeUpTo = typeof upToSpec.val;
489
- const typePrev = typeof previousItem.val;
490
- if (typeUpTo === 'number')
491
- return typePrev === 'string' && previousItem.val === upToSpec.val.toString();
492
- if (typePrev === 'number')
493
- return typeUpTo === 'string' && upToSpec.val === previousItem.val.toString();
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
@@ -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)