@sap/cds-compiler 4.4.4 → 4.6.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 (82) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/bin/cdsc.js +18 -11
  3. package/bin/cdsv2m.js +7 -5
  4. package/doc/CHANGELOG_BETA.md +22 -0
  5. package/lib/api/main.js +306 -144
  6. package/lib/api/options.js +18 -6
  7. package/lib/api/validate.js +1 -1
  8. package/lib/base/message-registry.js +45 -10
  9. package/lib/base/messages.js +33 -16
  10. package/lib/base/model.js +4 -0
  11. package/lib/base/optionProcessorHelper.js +45 -176
  12. package/lib/checks/annotationsOData.js +49 -0
  13. package/lib/checks/elements.js +32 -34
  14. package/lib/checks/enricher.js +39 -3
  15. package/lib/checks/validator.js +8 -7
  16. package/lib/compiler/assert-consistency.js +40 -17
  17. package/lib/compiler/builtins.js +30 -53
  18. package/lib/compiler/checks.js +46 -14
  19. package/lib/compiler/cycle-detector.js +1 -4
  20. package/lib/compiler/define.js +35 -10
  21. package/lib/compiler/extend.js +21 -7
  22. package/lib/compiler/generate.js +3 -0
  23. package/lib/compiler/populate.js +5 -1
  24. package/lib/compiler/propagator.js +46 -9
  25. package/lib/compiler/resolve.js +94 -35
  26. package/lib/compiler/shared.js +60 -33
  27. package/lib/compiler/tweak-assocs.js +188 -92
  28. package/lib/compiler/utils.js +11 -1
  29. package/lib/edm/annotations/edmJson.js +41 -66
  30. package/lib/edm/annotations/genericTranslation.js +27 -9
  31. package/lib/edm/annotations/preprocessAnnotations.js +2 -3
  32. package/lib/edm/csn2edm.js +28 -11
  33. package/lib/edm/edmInboundChecks.js +58 -15
  34. package/lib/edm/edmPreprocessor.js +12 -16
  35. package/lib/edm/edmUtils.js +5 -2
  36. package/lib/gen/Dictionary.json +10 -0
  37. package/lib/gen/language.checksum +1 -1
  38. package/lib/gen/language.interp +15 -2
  39. package/lib/gen/language.tokens +1 -0
  40. package/lib/gen/languageParser.js +6557 -5618
  41. package/lib/json/from-csn.js +4 -5
  42. package/lib/json/to-csn.js +29 -4
  43. package/lib/language/antlrParser.js +19 -1
  44. package/lib/language/errorStrategy.js +28 -7
  45. package/lib/language/genericAntlrParser.js +118 -24
  46. package/lib/language/textUtils.js +16 -0
  47. package/lib/main.d.ts +28 -3
  48. package/lib/main.js +3 -0
  49. package/lib/model/csnRefs.js +4 -1
  50. package/lib/model/csnUtils.js +20 -14
  51. package/lib/model/revealInternalProperties.js +5 -2
  52. package/lib/optionProcessor.js +23 -22
  53. package/lib/render/manageConstraints.js +13 -29
  54. package/lib/render/toCdl.js +47 -26
  55. package/lib/render/toHdbcds.js +63 -42
  56. package/lib/render/toRename.js +6 -10
  57. package/lib/render/toSql.js +71 -117
  58. package/lib/render/utils/common.js +41 -6
  59. package/lib/transform/.eslintrc.json +9 -1
  60. package/lib/transform/addTenantFields.js +228 -0
  61. package/lib/transform/db/applyTransformations.js +57 -4
  62. package/lib/transform/db/assertUnique.js +4 -4
  63. package/lib/transform/db/backlinks.js +13 -1
  64. package/lib/transform/db/cdsPersistence.js +1 -1
  65. package/lib/transform/db/expansion.js +24 -3
  66. package/lib/transform/db/flattening.js +70 -71
  67. package/lib/transform/db/killAnnotations.js +37 -0
  68. package/lib/transform/db/rewriteCalculatedElements.js +46 -6
  69. package/lib/transform/db/temporal.js +1 -1
  70. package/lib/transform/draft/db.js +2 -16
  71. package/lib/transform/draft/odata.js +3 -3
  72. package/lib/transform/effective/associations.js +3 -5
  73. package/lib/transform/effective/main.js +6 -9
  74. package/lib/transform/forOdata.js +26 -55
  75. package/lib/transform/forRelationalDB.js +38 -18
  76. package/lib/transform/odata/toFinalBaseType.js +3 -3
  77. package/lib/transform/odata/typesExposure.js +14 -5
  78. package/lib/transform/transformUtils.js +47 -34
  79. package/lib/transform/translateAssocsToJoins.js +45 -11
  80. package/lib/transform/universalCsn/coreComputed.js +1 -1
  81. package/lib/transform/universalCsn/universalCsnEnricher.js +4 -4
  82. package/package.json +7 -6
@@ -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
  },
@@ -473,7 +480,8 @@ function assertConsistency( model, stage ) {
473
480
  optional: [
474
481
  'literal', 'val', 'sym', 'struct', 'variant', 'path', 'name', '$duplicates', 'upTo',
475
482
  // expressions as annotation values
476
- '$tokenTexts', 'op', 'args', 'func', '_artifact', 'type',
483
+ '$tokenTexts', 'op', 'args', 'func', '_artifact', 'type', '$typeArgs',
484
+ 'scale', 'srid', 'length', 'precision',
477
485
  ],
478
486
  // TODO: restrict path to #simplePath
479
487
  },
@@ -505,7 +513,8 @@ function assertConsistency( model, stage ) {
505
513
  optional: [
506
514
  'name', '_block', '$priority', '$inferred', '$duplicates', '$errorReported',
507
515
  // annotation values
508
- '$tokenTexts',
516
+ '$tokenTexts', 'kind', '_outer',
517
+ '_effectiveType', '$effectiveSeqNo', '_origin', '_deps',
509
518
  // CSN parser may let these properties slip through to XSN, even if input is invalid.
510
519
  'args', 'op', 'func', 'suffix',
511
520
  ],
@@ -554,6 +563,7 @@ function assertConsistency( model, stage ) {
554
563
  'elements', 'cardinality', 'target', 'on', 'foreignKeys', 'items',
555
564
  '_outer', '_effectiveType', '$effectiveSeqNo', 'notNull', '_parent',
556
565
  '_origin', '_block', '$inferred', '$expand', '$inCycle', '_deps',
566
+ '$calcDepElement',
557
567
  '$syntax', '_extensions',
558
568
  '_status', '_redirected',
559
569
  ...typeProperties,
@@ -563,20 +573,20 @@ function assertConsistency( model, stage ) {
563
573
  artifacts: { kind: true, inherits: 'definitions', test: isDictionary( inDefinitions ) },
564
574
  _subArtifacts: { kind: true, inherits: 'definitions', test: isDictionary( inDefinitions ) },
565
575
  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' },
576
+ length: { kind: true, test: isNumberVal }, // for number is to be checked in resolver
577
+ precision: { kind: true, test: isNumberVal },
578
+ scale: { kind: true, test: isNumberVal, also: [ 'floating', 'variable' ] },
579
+ srid: { kind: true, test: isNumberVal },
570
580
  localized: { kind: true, test: locationVal() },
571
581
  cardinality: {
572
582
  kind: true,
573
583
  requires: [ 'location' ],
574
584
  optional: [ 'sourceMin', 'sourceMax', 'targetMin', 'targetMax' ],
575
585
  },
576
- sourceMin: { test: locationVal( isNumber ) },
577
- sourceMax: { test: locationVal( isNumber ), also: [ '*' ] },
578
- targetMin: { test: locationVal( isNumber ) },
579
- targetMax: { test: locationVal( isNumber ), also: [ '*' ] },
586
+ sourceMin: { test: isNumberVal },
587
+ sourceMax: { test: isNumberVal, also: [ '*' ] },
588
+ targetMin: { test: isNumberVal },
589
+ targetMax: { test: isNumberVal, also: [ '*' ] },
580
590
  default: { kind: true, inherits: 'value' },
581
591
  $typeArgs: { parser: true, kind: true, test: TODO },
582
592
  $tableAliases: { kind: true, test: TODO }, // containing $self outside queries
@@ -585,13 +595,18 @@ function assertConsistency( model, stage ) {
585
595
  _service: { kind: true, test: TODO },
586
596
  _main: { kind: true, test: TODO },
587
597
  _user: { kind: true, test: TODO },
598
+ // - on a path item with a filter condition to the user of the ref (not nested)
599
+ // - on a JOIN node to the query (TODO: _outer?)
588
600
  _artifact: { test: TODO },
589
601
  _navigation: { test: TODO },
590
602
  _effectiveType: { kind: true, test: TODO },
591
603
  $effectiveSeqNo: { kind: true, test: isNumber },
592
604
  _joinParent: { test: TODO },
593
605
  $joinArgsIndex: { test: isNumber },
594
- _outer: { test: TODO }, // for returns/items
606
+ _outer: { test: TODO }, // for items
607
+ // - on an array item to the array elem/type/item (nested)
608
+ // - on an anonymous aspect to the composition element
609
+ // - on an annotation assignment to the annotatee
595
610
  $queries: {
596
611
  kind: [ 'entity', 'event' ],
597
612
  test: isArray(),
@@ -605,7 +620,7 @@ function assertConsistency( model, stage ) {
605
620
  ],
606
621
  optional: [
607
622
  '_effectiveType', '$effectiveSeqNo', '$parens',
608
- '_deps', '$expand',
623
+ '_deps', '$calcDepElement', '$expand', '$contains',
609
624
  // query specific
610
625
  'where', 'columns', 'mixin', 'quantifier', 'offset',
611
626
  'orderBy', '$orderBy', 'groupBy', 'excludingDict', 'having',
@@ -628,6 +643,9 @@ function assertConsistency( model, stage ) {
628
643
  _annotate: { kind: true, test: TODO }, // for collecting extend/annotate on artifact
629
644
  _extension: { kind: true, test: TODO }, // on artifact to its "super extend/annotate" statement
630
645
  _deps: { kind: true, test: TODO }, // for cyclic calculation
646
+ // a fake element for cyclic dependency detection: e.g. dependencies to target entities.
647
+ // dependants don't only depend on the calc element, but on this element as well.
648
+ $calcDepElement: { kind: true, test: TODO },
631
649
  _scc: { kind: true, test: TODO }, // for cyclic calculation
632
650
  _sccCaller: { kind: true, test: TODO }, // for cyclic calculation
633
651
  _status: { kind: true, test: TODO }, // TODO: $status
@@ -703,6 +721,7 @@ function assertConsistency( model, stage ) {
703
721
  $extra: { parser: true, test: TODO }, // for unexpected properties in CSN
704
722
  $withLocalized: { test: isBoolean },
705
723
  $sources: { parser: true, test: isArray( isString ) },
724
+ tokenStream: { parser: true, test: TODO },
706
725
  $expected: { parser: true, test: isOneOf( [ 'approved-exists', 'exists' ] ) },
707
726
  $messageFunctions: { test: TODO },
708
727
  $functions: { test: TODO },
@@ -958,6 +977,10 @@ function assertConsistency( model, stage ) {
958
977
  throw new InternalConsistencyError( `Expected boolean or null${ at( [ node, parent ], prop ) }` );
959
978
  }
960
979
 
980
+ function isNumberVal() {
981
+ return locationVal( isNumber );
982
+ }
983
+
961
984
  function isNumber( node, parent, prop, spec ) {
962
985
  if (spec.also && spec.also.includes( node ))
963
986
  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 = {
@@ -31,6 +32,7 @@ const core = {
31
32
  Timestamp: { category: 'dateTime' },
32
33
  Boolean: { category: 'boolean' },
33
34
  UUID: { category: 'string' },
35
+ Vector: { parameters: [ 'length' /* , 'type' */ ], category: 'vector' },
34
36
  Association: { internal: true, category: 'relation' },
35
37
  Composition: { internal: true, category: 'relation' },
36
38
  };
@@ -190,14 +192,14 @@ function compileArg( src ) {
190
192
  */
191
193
  const magicVariables = {
192
194
  $user: {
193
- // id and locale are always available
194
- elements: { id: {}, locale: {}, tenant: {} },
195
+ // always available
196
+ elements: { id: {}, locale: {} },
195
197
  // Allow $user.<any>
196
198
  $uncheckedElements: true,
197
199
  // Allow shortcut in CDL: `$user` becomes `$user.id` in CSN.
198
200
  $autoElement: 'id',
199
201
  },
200
- $at: { // CDS-specific, not part of SQL
202
+ $at: {
201
203
  elements: {
202
204
  from: {}, to: {},
203
205
  },
@@ -211,7 +213,8 @@ const magicVariables = {
211
213
  // Require that elements are accessed, i.e. no $valid, only $valid.<element>.
212
214
  $requireElementAccess: true,
213
215
  },
214
- $now: {}, // Dito
216
+ $now: {},
217
+ $tenant: { $requiresBetaFlag: 'tenantVariable' },
215
218
  $session: {
216
219
  // In ABAP CDS session variables are accessed in a generic way via
217
220
  // the pseudo variable $session.
@@ -229,7 +232,7 @@ const timeRegEx = /^T?(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?(?:Z|[+-]\d{2}(?::\d{2})
229
232
  // eslint-disable-next-line max-len
230
233
  const timestampRegEx = /^(-?\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2})(\.\d{1,7})?)?(?:Z|[+-]\d{2}(?::\d{2})?)?$/;
231
234
  // YYYY - MM - dd T HH : mm : ss . fraction TZD
232
- const numberRegEx = /^[ \t]*[-+]?(\d+(\.\d*)?|\.\d+)(e[-+]\d+)?[ \t]*$/i;
235
+ const numberRegEx = /^[ \t]*[-+]?(\d+(\.\d*)?|\.\d+)(e[-+]?\d+)?[ \t]*$/i;
233
236
 
234
237
  /**
235
238
  * Patterns for literal token tests and creation. The value is a map from the
@@ -318,9 +321,7 @@ function checkDate( year, month, day ) {
318
321
  * Return whether JSON object `val` is a representation for an annotation expression
319
322
  */
320
323
  function isAnnotationExpression( val ) {
321
- // TODO: we might allow `'=': true`, not just a string, for expressions, to be
322
- // decided → just check truthy at the moment
323
- return val['='] && xprInAnnoProperties.some( prop => val[prop] !== undefined );
324
+ return val?.['='] !== undefined && xprInAnnoProperties.some( prop => val[prop] !== undefined );
324
325
  }
325
326
 
326
327
  /**
@@ -352,6 +353,7 @@ const typeCategories = {
352
353
  boolean: [],
353
354
  relation: [],
354
355
  geo: [],
356
+ vector: [],
355
357
  };
356
358
  // Fill type categories with `cds.*` types
357
359
  Object.keys( core ).forEach( (type) => {
@@ -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.
@@ -433,6 +399,16 @@ function isBuiltinType( type ) {
433
399
  return typeof type === 'string' && isInReservedNamespace( type );
434
400
  }
435
401
 
402
+ /**
403
+ * Tell if a name is a magic variable
404
+ *
405
+ * @param {string} name
406
+ * @returns {boolean}
407
+ */
408
+ function isMagicVariable( name ) {
409
+ return typeof name === 'string' && Object.prototype.hasOwnProperty.call(magicVariables, name);
410
+ }
411
+
436
412
  /**
437
413
  * Add CDS builtins like the `cds` namespace with types like `cds.Integer` to
438
414
  * `definitions` of the XSN model as well as to `$builtins`.
@@ -445,9 +421,14 @@ function initBuiltins( model ) {
445
421
  // namespace:"cds" stores the builtins ---
446
422
  const cds = createNamespace( 'cds', 'reserved' );
447
423
  model.definitions.cds = cds;
424
+
448
425
  // Also add the core artifacts to model.definitions`
449
- model.$builtins = env( core, 'cds.', cds );
426
+ const c = { ...core };
427
+ if (!isBetaEnabled( model.options, 'vectorType' ))
428
+ delete c.Vector;
429
+ model.$builtins = env( c, 'cds.', cds );
450
430
  model.$builtins.cds = cds;
431
+
451
432
  // namespace:"cds.hana" stores HANA-specific builtins ---
452
433
  const hana = createNamespace( 'cds.hana', 'reserved' );
453
434
  model.definitions['cds.hana'] = hana;
@@ -507,6 +488,9 @@ function initBuiltins( model ) {
507
488
  model.$magicVariables = { kind: '$magicVariables', elements };
508
489
  for (const id in builtins) {
509
490
  const magic = builtins[id];
491
+ if (magic.$requiresBetaFlag && !isBetaEnabled( options, magic.$requiresBetaFlag ))
492
+ continue;
493
+
510
494
  // TODO: rename to $builtinFunction
511
495
  const art = {
512
496
  kind: 'builtin', // TODO: $var
@@ -565,13 +549,6 @@ module.exports = {
565
549
  isAnnotationExpression,
566
550
  isInReservedNamespace,
567
551
  isBuiltinType,
568
- isIntegerTypeName,
569
- isDecimalTypeName,
570
- isNumericTypeName,
571
- isStringTypeName,
572
- isDateOrTimeTypeName,
573
- isBooleanTypeName,
574
- isBinaryTypeName,
552
+ isMagicVariable,
575
553
  isGeoTypeName,
576
- isRelationTypeName,
577
554
  };
@@ -15,6 +15,7 @@ const {
15
15
  forEachDefinition,
16
16
  forEachMember,
17
17
  forEachMemberRecursively,
18
+ isDeprecatedEnabled,
18
19
  } = require('../base/model');
19
20
  const { CompilerAssertion } = require('../base/error');
20
21
  const { typeParameters } = require('./builtins');
@@ -153,13 +154,18 @@ function check( model ) {
153
154
  while (effectiveType?.enum)
154
155
  effectiveType = (effectiveType._origin || effectiveType.type?._artifact)?._effectiveType;
155
156
 
156
- if (!effectiveType) {
157
- return; // e.g. illegal definition references, cycles, ...
157
+ if (!effectiveType || (effectiveType.type && !effectiveType.type._artifact)) {
158
+ return; // e.g. illegal definition references, cycles, unknown artifacts, …
158
159
  }
159
160
  else if (!art.type && !effectiveType.type && !effectiveType?.builtin) {
160
- error( 'type-missing-type', [ art.location, user ],
161
- { otherprop: 'type', prop: actualParams[0] },
162
- 'Missing $(OTHERPROP) property next to $(PROP)' );
161
+ // Special case for deprecated flag "ignore specified elements": The `type` property
162
+ // is lost in columns, but `length`,… are kept -> mismatch. This behavior is the
163
+ // same as in cds-compiler v3. See #12169 for details.
164
+ if (!isDeprecatedEnabled( model.options, 'ignoreSpecifiedQueryElements' )) {
165
+ error( 'type-missing-type', [ art.location, user ],
166
+ { otherprop: 'type', prop: actualParams[0] },
167
+ 'Missing $(OTHERPROP) property next to $(PROP)' );
168
+ }
163
169
  return;
164
170
  }
165
171
 
@@ -182,12 +188,14 @@ function check( model ) {
182
188
  } );
183
189
  break; // Avoid spam: Only emit the first error.
184
190
  }
185
- else if (!typeParameters.expectedLiteralsFor[param].includes( art[param].literal )) {
191
+ else if (!typeParameters.expectedLiteralsFor[param].includes( typeof art[param].val )) {
192
+ // TODO: this could be probably better done via syntax check (already for CSN input)
186
193
  error( 'type-unexpected-argument', [ art[param].location, user ], {
187
194
  '#': 'incorrect-type',
188
195
  prop: param,
189
- code: art[param].literal,
196
+ code: typeof art[param].val,
190
197
  names: typeParameters.expectedLiteralsFor[param],
198
+ // TODO: no double quote via $(NAMES), but see TODO above
191
199
  } );
192
200
  break; // Avoid spam: Only emit the first error.
193
201
  }
@@ -422,8 +430,8 @@ function check( model ) {
422
430
  // Max cardinalities must be a positive number or '*'
423
431
  for (const prop of [ 'sourceMax', 'targetMax' ]) {
424
432
  if (art.cardinality[prop]) {
425
- const { literal, val, location } = art.cardinality[prop];
426
- if (!(literal === 'number' && val > 0 || literal === 'string' && val === '*')) {
433
+ const { val, location } = art.cardinality[prop];
434
+ if (val !== '*' && val <= 0) {
427
435
  error( 'type-invalid-cardinality', [ location, art ],
428
436
  { '#': prop, prop: val, otherprop: '*' } );
429
437
  }
@@ -630,24 +638,30 @@ function check( model ) {
630
638
  }
631
639
 
632
640
  function checkCalculatedElementValue( elem ) {
641
+ const isStored = elem.value.stored?.val;
633
642
  visitExpression( elem.value, elem, (xpr, user) => {
634
643
  // We only need to check artifact references. To avoid false positives and conflicts
635
644
  // with $self comparison-checks, ignore bare $self.
636
645
  const isArtRef = xpr._artifact && !(xpr.path?.length === 1 &&
637
646
  xpr.path[0]._navigation?.kind === '$self');
638
647
  if (isArtRef) {
639
- const sourceLoc = xpr.path?.[xpr.path.length - 1].location || xpr.location;
648
+ const lastStep = xpr.path?.[xpr.path.length - 1];
649
+ const sourceLoc = lastStep.location || xpr.location;
640
650
  checkExpressionNotVirtual( xpr, user );
641
651
  // For inferred (e.g. included) calc elements, this error is already emitted at the origin.
642
652
  // And users can't change structured to non-structured elements.
643
653
  if (!elem.$inferred && xpr._artifact._effectiveType?.elements) {
644
654
  error( 'ref-unexpected-structured', [ sourceLoc, elem ], { '#': 'expr' } );
645
655
  }
646
- else if (xpr._artifact.target !== undefined) {
647
- const variant = isComposition( model, xpr._artifact ) ? 'expr-comp' : 'expr';
656
+ else if (xpr._artifact.target !== undefined && (!lastStep.where || isStored)) {
657
+ // Allow using an association _with filter_, but only for on-read calculated elements.
658
+ // TODO: Also allow bare unmanaged association references and remove beta.
659
+ const variant = (isStored && lastStep.where && 'assoc-stored') ||
660
+ (isComposition( model, xpr._artifact ) && 'expr-comp') ||
661
+ 'expr';
648
662
  error( 'ref-unexpected-assoc', [ sourceLoc, elem ], { '#': variant } );
649
663
  }
650
- else if (xpr._artifact.localized?.val && elem.value.stored?.val) {
664
+ else if (xpr._artifact.localized?.val && isStored) {
651
665
  error( 'ref-unexpected-localized', [ sourceLoc, elem ], { '#': 'calc' } );
652
666
  }
653
667
  }
@@ -807,6 +821,9 @@ function check( model ) {
807
821
  // Has been slightly adapted for model.vocabularies but comments need to be
808
822
  // adapted, etc.
809
823
  function checkAnnotationAssignment1( art, anno ) {
824
+ if (art.$contains?.$annotation)
825
+ checkAnnotationExpressions( anno, art );
826
+
810
827
  // Sanity checks (ignore broken assignments)
811
828
  if (!anno.name?.id)
812
829
  return;
@@ -874,7 +891,6 @@ function check( model ) {
874
891
  warning( 'anno-expecting-value', [ anno.location || anno.name.location, art, anno ],
875
892
  { '#': 'std', anno: anno.name.id } );
876
893
  }
877
-
878
894
  return;
879
895
  }
880
896
 
@@ -882,6 +898,22 @@ function check( model ) {
882
898
  checkValueAssignableTo( anno, anno, elementDecl, art );
883
899
  }
884
900
 
901
+ /**
902
+ * Check the expressions inside annotations.
903
+ */
904
+ function checkAnnotationExpressions( anno, art ) {
905
+ if (anno.$tokenTexts) {
906
+ checkGenericExpression( anno, art );
907
+ }
908
+ else if (anno.literal === 'array') {
909
+ anno.val.forEach( val => checkAnnotationExpressions( val, art ) );
910
+ }
911
+ else if (anno.literal === 'struct') {
912
+ const struct = Object.values(anno.struct);
913
+ struct.forEach(val => checkAnnotationExpressions( val, art ));
914
+ }
915
+ }
916
+
885
917
  // Check that annotation assignment 'value' (having 'path or 'literal' and
886
918
  // 'val') is potentially assignable to element 'element'. Complain on 'loc'
887
919
  // if not
@@ -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,
@@ -939,6 +940,8 @@ function define( model ) {
939
940
  initExprForQuery( col.value, parent );
940
941
  initSelectItems( col, null, user ); // TODO: use col as user (i.e. remove param)
941
942
  }
943
+
944
+ initItemsLinks( col, parent._block );
942
945
  }
943
946
 
944
947
  if (hasItems && !wildcard && parent.excludingDict && !options.$recompile) {
@@ -974,7 +977,7 @@ function define( model ) {
974
977
  // Drill down
975
978
  if (exprOrPathElement.args)
976
979
  exprOrPathElement.args.forEach( elem => approveExistsInChildren( elem ) );
977
- else if (exprOrPathElement.where && exprOrPathElement.where.args)
980
+ else if (exprOrPathElement.where?.args)
978
981
  exprOrPathElement.where.args.forEach( elem => approveExistsInChildren( elem ) );
979
982
  else if (exprOrPathElement.path)
980
983
  exprOrPathElement.path.forEach( elem => approveExistsInChildren( elem ) );
@@ -998,15 +1001,7 @@ function define( model ) {
998
1001
  // TODO: split extend from init
999
1002
  const main = parent._main || parent;
1000
1003
  const isQueryExtension = construct.kind === 'extend' && main.query;
1001
- let obj = construct;
1002
- let { items } = obj;
1003
- while (items) {
1004
- setLink( items, '_outer', obj );
1005
- setLink( items, '_parent', obj._parent );
1006
- setLink( items, '_block', block );
1007
- obj = items;
1008
- items = obj.items;
1009
- }
1004
+ let obj = initItemsLinks( construct, block );
1010
1005
  if (obj.target && targetIsTargetAspect( obj )) {
1011
1006
  obj.targetAspect = obj.target;
1012
1007
  delete obj.target;
@@ -1169,6 +1164,16 @@ function define( model ) {
1169
1164
  elem.type = { ...elem.value.type, $inferred: 'cast' };
1170
1165
  }
1171
1166
  elem.$syntax = 'calc';
1167
+ // TODO: it is not just "syntax" - maybe better test for `$calcDepElement`?
1168
+ createAndLinkCalcDepElement( elem );
1169
+
1170
+ // Special case (hack) for calculated elements that use associations+filter:
1171
+ // See "Notes on `$filtered`" in `ExposingAssocWithFilter.md` for details.
1172
+ if (elem.target && elem.value.path?.[elem.value.path.length - 1]?.where) {
1173
+ delete elem.type;
1174
+ delete elem.on;
1175
+ delete elem.target;
1176
+ }
1172
1177
  }
1173
1178
  }
1174
1179
 
@@ -1196,6 +1201,26 @@ function define( model ) {
1196
1201
  }
1197
1202
  }
1198
1203
 
1204
+ /**
1205
+ * Initialize artifact links inside `obj.items` (for nested ones as well).
1206
+ * Does nothing, it `obj.items` does not exist.
1207
+ *
1208
+ * @param {XSN.Artifact} obj
1209
+ * @param {object} block
1210
+ * @return {XSN.Artifact}
1211
+ */
1212
+ function initItemsLinks( obj, block ) {
1213
+ let { items } = obj;
1214
+ while (items) {
1215
+ setLink( items, '_outer', obj );
1216
+ setLink( items, '_parent', obj._parent );
1217
+ setLink( items, '_block', block );
1218
+ obj = items;
1219
+ items = obj.items;
1220
+ }
1221
+ return obj;
1222
+ }
1223
+
1199
1224
  // To be reworked -------------------------------------------------------------
1200
1225
 
1201
1226
  // TODO: is only necessary for extensions - make special for extend/annotate
@@ -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)