@sap/cds-compiler 6.2.2 → 6.3.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 (57) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/bin/cdsc.js +11 -4
  3. package/lib/api/options.js +1 -1
  4. package/lib/base/message-registry.js +36 -7
  5. package/lib/base/messages.js +11 -4
  6. package/lib/base/model.js +0 -1
  7. package/lib/checks/assocOutsideService.js +17 -30
  8. package/lib/checks/checkForTypes.js +0 -18
  9. package/lib/checks/checkPathsInStoredCalcElement.js +2 -1
  10. package/lib/checks/onConditions.js +2 -2
  11. package/lib/checks/queryNoDbArtifacts.js +16 -15
  12. package/lib/checks/types.js +1 -1
  13. package/lib/checks/utils.js +30 -6
  14. package/lib/checks/validator.js +4 -5
  15. package/lib/compiler/checks.js +47 -18
  16. package/lib/compiler/index.js +88 -6
  17. package/lib/compiler/resolve.js +7 -7
  18. package/lib/compiler/tweak-assocs.js +47 -25
  19. package/lib/gen/BaseParser.js +1 -1
  20. package/lib/gen/CdlGrammar.checksum +1 -1
  21. package/lib/gen/CdlParser.js +381 -378
  22. package/lib/gen/Dictionary.json +0 -2
  23. package/lib/model/csnRefs.js +9 -4
  24. package/lib/model/csnUtils.js +67 -2
  25. package/lib/optionProcessor.js +2 -3
  26. package/lib/parsers/AstBuildingParser.js +5 -6
  27. package/lib/render/toCdl.js +10 -4
  28. package/lib/render/utils/common.js +4 -2
  29. package/lib/transform/db/assertUnique.js +2 -1
  30. package/lib/transform/db/associations.js +37 -1
  31. package/lib/transform/db/assocsToQueries/transformExists.js +21 -32
  32. package/lib/transform/db/assocsToQueries/utils.js +1 -1
  33. package/lib/transform/db/cdsPersistence.js +1 -1
  34. package/lib/transform/db/expansion.js +37 -36
  35. package/lib/transform/draft/db.js +20 -20
  36. package/lib/transform/draft/odata.js +38 -40
  37. package/lib/transform/effective/associations.js +1 -1
  38. package/lib/transform/effective/flattening.js +40 -47
  39. package/lib/transform/effective/main.js +6 -4
  40. package/lib/transform/forOdata.js +135 -115
  41. package/lib/transform/forRelationalDB.js +151 -142
  42. package/lib/transform/localized.js +116 -109
  43. package/lib/transform/odata/adaptAnnotationRefs.js +21 -16
  44. package/lib/transform/odata/createForeignKeys.js +73 -70
  45. package/lib/transform/odata/flattening.js +216 -200
  46. package/lib/transform/odata/foreignKeyRefsInXprAnnos.js +47 -45
  47. package/lib/transform/odata/toFinalBaseType.js +40 -39
  48. package/lib/transform/odata/typesExposure.js +151 -133
  49. package/lib/transform/odata/utils.js +7 -6
  50. package/lib/transform/parseExpr.js +165 -162
  51. package/lib/transform/transformUtils.js +184 -551
  52. package/lib/transform/translateAssocsToJoins.js +510 -571
  53. package/lib/transform/tupleExpansion.js +495 -0
  54. package/lib/transform/universalCsn/universalCsnEnricher.js +1 -0
  55. package/package.json +1 -1
  56. package/lib/base/cleanSymbols.js +0 -17
  57. package/lib/checks/nonexpandableStructured.js +0 -39
@@ -116,13 +116,21 @@ function check( model ) {
116
116
  return;
117
117
 
118
118
  const isVirtual = parentProps.virtual?.val || elem.virtual?.val;
119
+ const typeName = elem._effectiveType?.name?.id;
119
120
  if (isVirtual) {
120
121
  error( 'def-unexpected-key', [ (parentProps.key || elem.key).location, elem ],
121
- { '#': 'virtual', prop: 'key' } );
122
+ { '#': 'virtual', keyword: 'key' } );
122
123
  }
123
- else if (elem._effectiveType?.name?.id === 'cds.Map') {
124
+ else if (typeName === 'cds.Map') {
124
125
  error( 'def-unexpected-key', [ elem.type?.location || elem.location, elem ],
125
- { '#': 'invalidType', prop: 'key', type: 'cds.Map' } );
126
+ { '#': 'invalidType', keyword: 'key', type: typeName } );
127
+ }
128
+ else if (typeName === 'cds.LargeString' ||
129
+ typeName === 'cds.Vector' ||
130
+ typeName === 'cds.hana.CLOB' ||
131
+ typeName === 'cds.LargeBinary') {
132
+ warning( 'def-unsupported-key', [ elem.type?.location || elem.location, elem ],
133
+ { '#': 'type', keyword: 'key', type: typeName } );
126
134
  }
127
135
  }
128
136
 
@@ -704,12 +712,15 @@ function check( model ) {
704
712
  *
705
713
  * @param {any} xpr The expression to check
706
714
  * @param {XSN.Artifact} user User for semantic location
715
+ * @param {any} _parentExpr
707
716
  * @param {string} [context] where the expression is used, e.g. 'anno'
708
717
  */
709
- function checkGenericExpression( xpr, user, context ) {
718
+ function checkGenericExpression( xpr, user, _parentExpr, context ) {
710
719
  if (context !== 'anno')
711
720
  checkExpressionNotVirtual( xpr, user );
712
- checkExpressionAssociationUsage( xpr, user, false );
721
+ checkExpressionAssociationUsage( xpr, user, {
722
+ context, rejectManaged: context === 'anno', rejectUnmanaged: true,
723
+ } );
713
724
  if (xpr.op?.val === 'cast') {
714
725
  requireExplicitTypeInSqlCast( xpr, user );
715
726
  checkTypeCast( xpr, user );
@@ -730,7 +741,7 @@ function check( model ) {
730
741
 
731
742
  visitExpression( elem.on, elem, (xpr, user) => {
732
743
  checkExpressionNotVirtual( xpr, user );
733
- checkExpressionAssociationUsage( xpr, user, true );
744
+ checkExpressionAssociationUsage( xpr, user, null );
734
745
 
735
746
  if (xpr._artifact?._effectiveType?.name.id === 'cds.Map') {
736
747
  error( 'ref-unexpected-map', [ xpr.location, user ], { '#': 'onCond', type: 'cds.Map' } );
@@ -744,7 +755,9 @@ function check( model ) {
744
755
  }
745
756
 
746
757
  function checkSelectItemValue( elem ) {
747
- checkExpressionAssociationUsage( elem.value, elem, false );
758
+ checkExpressionAssociationUsage( elem.value, elem, {
759
+ context: 'query', rejectManaged: false, rejectUnmanaged: true,
760
+ } );
748
761
  checkVirtualSelectItemChangeForV6( elem );
749
762
  // To avoid duplicate messages, only run this check if the type wasn't inferred from
750
763
  // the cast, as otherwise we will check it twice (once here, once via element).
@@ -753,8 +766,8 @@ function check( model ) {
753
766
  checkTypeCast( elem.value, elem );
754
767
  checkTypeArguments( elem.value, elem );
755
768
  }
756
- visitSubExpression( elem.value, elem, (xpr) => {
757
- checkGenericExpression( xpr, elem );
769
+ visitSubExpression( elem.value, elem, (xpr, user, parentExpr) => {
770
+ checkGenericExpression( xpr, elem, parentExpr, 'query' );
758
771
  } );
759
772
  }
760
773
 
@@ -811,7 +824,8 @@ function check( model ) {
811
824
  // For inferred (e.g. included) calc elements, this error is already emitted at the origin.
812
825
  // And users can't change structured to non-structured elements.
813
826
  if (!elem.$inferred && xpr._artifact._effectiveType?.elements) {
814
- error( 'ref-unexpected-structured', [ sourceLoc, elem ], { '#': 'expr' } );
827
+ error( 'ref-unexpected-structured', [ sourceLoc, elem ],
828
+ { '#': 'struct-expr', elemref: xpr } );
815
829
  }
816
830
  else if (xpr._artifact.target !== undefined && (!lastStep.where || isStored)) {
817
831
  // Allow using an association _with filter_, but only for on-read calculated elements.
@@ -884,10 +898,11 @@ function check( model ) {
884
898
  *
885
899
  * @param {any} xpr The expression to check
886
900
  * @param {XSN.Artifact} user
887
- * @param {boolean} allowAssocTail
901
+ * @param {{context: string, rejectUnmanaged, rejectManaged}|null} [rejectAssocTail]
902
+ * Context where association tails are not allowed.
888
903
  * @returns {void}
889
904
  */
890
- function checkExpressionAssociationUsage( xpr, user, allowAssocTail ) {
905
+ function checkExpressionAssociationUsage( xpr, user, rejectAssocTail = null ) {
891
906
  if (!xpr.args)
892
907
  return;
893
908
 
@@ -902,12 +917,18 @@ function check( model ) {
902
917
  const op = getBinaryOp( xpr );
903
918
  for (const arg of args) {
904
919
  if (arg && !(op?.val !== '=' && isDollarSelfOrProjectionOperand( arg )))
905
- checkExpressionIsNotAssocOrSelf( arg, user, allowAssocTail );
920
+ checkExpressionIsNotAssocOrSelf( arg, user, rejectAssocTail );
906
921
  }
907
922
  }
908
923
  }
909
924
 
910
- function checkExpressionIsNotAssocOrSelf( arg, user, allowAssocTail ) {
925
+ /**
926
+ * @param arg
927
+ * @param {XSN.Artifact} user
928
+ * @param {{context: string, rejectUnmanaged, rejectManaged}|null} [rejectAssocTail]
929
+ * Context where association tails are not allowed.
930
+ */
931
+ function checkExpressionIsNotAssocOrSelf( arg, user, rejectAssocTail ) {
911
932
  // Arg must not be an association and not $self
912
933
  // Only if path is not approved exists path (that is non-query position)
913
934
  if (arg.path && arg.$expected !== undefined) { // not 'approved-exists'
@@ -916,9 +937,17 @@ function check( model ) {
916
937
  error( 'ref-unexpected-assoc', [ arg.location, user ], { '#': variant } );
917
938
  }
918
939
  }
919
- else if (!allowAssocTail && isAssociationOperand( arg )) {
920
- const variant = isComposition( model, arg._artifact ) ? 'expr-comp' : 'expr';
921
- error( 'ref-unexpected-assoc', [ arg.location, user ], { '#': variant } );
940
+ else if (rejectAssocTail && isAssociationOperand( arg )) {
941
+ if (rejectAssocTail.rejectManaged && rejectAssocTail.rejectUnmanaged ||
942
+ rejectAssocTail.rejectManaged && arg._artifact.keys ||
943
+ rejectAssocTail.rejectUnmanaged && arg._artifact.on) {
944
+ // only a few contexts have special message
945
+ const context = rejectAssocTail.context === 'query' && 'query-' ||
946
+ rejectAssocTail.context === 'anno' && 'anno-' ||
947
+ '';
948
+ const variant = isComposition( model, arg._artifact ) ? 'expr-comp' : 'expr';
949
+ error( 'ref-unexpected-assoc', [ arg.location, user ], { '#': `${ context }${ variant }` } );
950
+ }
922
951
  }
923
952
  }
924
953
 
@@ -1105,7 +1134,7 @@ function check( model ) {
1105
1134
  */
1106
1135
  function checkAnnotationExpressions( anno, art ) {
1107
1136
  if (anno.$tokenTexts) {
1108
- checkGenericExpression( anno, art, 'anno' );
1137
+ checkGenericExpression( anno, art, null, 'anno' );
1109
1138
  }
1110
1139
  else if (anno.literal === 'array') {
1111
1140
  anno.val.forEach( val => checkAnnotationExpressions( val, art ) );
@@ -183,7 +183,7 @@ function compileX( filenames, dir = '', options = {}, fileCache = Object.create(
183
183
  return all.then( () => {
184
184
  options.abortSignal?.throwIfAborted();
185
185
  moduleLayers.setLayers( input.sources );
186
- return compileDoX( model );
186
+ return compileDoX( model ); // also async
187
187
  } );
188
188
 
189
189
  // Read file `filename` and parse its content, return messages
@@ -303,7 +303,7 @@ function compileSyncX( filenames, dir = '', options = {}, fileCache = Object.cre
303
303
  }
304
304
 
305
305
  moduleLayers.setLayers( a.sources );
306
- return compileDoX( model );
306
+ return compileDoXSync( model );
307
307
 
308
308
  // Read file `filename` and parse its content, return messages
309
309
  function readAndParseSync( filename, cb ) {
@@ -423,7 +423,7 @@ function compileSourcesX( sourcesDict, options = {} ) {
423
423
  }
424
424
  moduleLayers.setLayers( sources );
425
425
 
426
- return compileDoX( model );
426
+ return compileDoXSync( model );
427
427
  }
428
428
 
429
429
  /**
@@ -457,8 +457,8 @@ function recompileX( csn, options ) {
457
457
 
458
458
  sources[file] = parseCsn.augment( csn, file, options, model.$messageFunctions );
459
459
  moduleLayers.setLayers( sources );
460
- const compiled = compileDoX( model ); // calls throwWithError()
461
- if (options.messages) // does not help with exception in compileDoX()
460
+ const compiled = compileDoXSync( model ); // calls throwWithError()
461
+ if (options.messages) // does not help with exception in compileDoXSync()
462
462
  deduplicateMessages( options.messages ); // TODO: do better
463
463
  return compiled;
464
464
  }
@@ -467,10 +467,12 @@ function recompileX( csn, options ) {
467
467
  * On the given model (AST like CSN) run the definer, resolver as well as semantic checks.
468
468
  * Creates an augmented CSN (XSN) and returns it.
469
469
  *
470
+ * This is the non-interruptible version of `compileDoX()` and can be used in non-`async` functions.
471
+ *
470
472
  * @param {object} model AST like CSN generated e.g. by `parsers.parseCdl()`
471
473
  * @returns {XSN.Model} Augmented CSN (XSN)
472
474
  */
473
- function compileDoX( model ) {
475
+ function compileDoXSync( model ) {
474
476
  const { options } = model;
475
477
  const { throwWithError } = model.$messageFunctions;
476
478
  if (!options.testMode)
@@ -512,6 +514,61 @@ function compileDoX( model ) {
512
514
  return propagator.propagate( model );
513
515
  }
514
516
 
517
+ /**
518
+ * On the given model (AST like CSN) run the definer, resolver as well as semantic checks.
519
+ * Creates an augmented CSN (XSN) and returns it.
520
+ *
521
+ * @param {object} model AST like CSN generated e.g. by `parsers.parseCdl()`
522
+ * @returns {XSN.Model} Augmented CSN (XSN)
523
+ */
524
+ async function compileDoX( model ) {
525
+ const { options } = model;
526
+ const { throwWithError } = model.$messageFunctions;
527
+ if (!options.testMode)
528
+ model.meta = {}; // provide initial central meta object
529
+
530
+ checkRemovedDeprecatedFlags( options, model.$messageFunctions );
531
+
532
+ if (options.parseOnly) {
533
+ throwWithError();
534
+ return model;
535
+ }
536
+ model.$functions = {};
537
+ fns( model ); // attach (mostly) paths functions
538
+ define( model );
539
+ await checkAsyncAbortFlag( options.abortSignal );
540
+
541
+ // do not run the resolver in parse-cdl mode or we get duplicate annotations, etc.
542
+ // TODO: do not use this function for parseCdl anyway…
543
+ if (options.parseCdl) {
544
+ finalizeParseCdl( model );
545
+ throwWithError();
546
+ return model;
547
+ }
548
+
549
+ for (const phase of [ extend, generate, kickStart, populate ]) {
550
+ phase( model );
551
+ // eslint-disable-next-line no-await-in-loop
552
+ await checkAsyncAbortFlag( options.abortSignal );
553
+ }
554
+
555
+ model.definitions = model.$functions.shuffleDict( model.definitions );
556
+ // Shuffling extensions is more difficult due to intra-file extensions of same artifact
557
+ // TODO: think about making this work
558
+
559
+ for (const phase of [ resolve, tweakAssocs, assertConsistency, check ]) {
560
+ phase( model );
561
+ // eslint-disable-next-line no-await-in-loop
562
+ await checkAsyncAbortFlag( options.abortSignal );
563
+ }
564
+
565
+ throwWithError();
566
+ if (options.lintMode)
567
+ return model;
568
+
569
+ return propagator.propagate( model );
570
+ }
571
+
515
572
  /**
516
573
  * Process an array of `filenames`. Returns an object with properties:
517
574
  * - `sources`: dictionary which has a filename as key (value is irrelevant)
@@ -595,6 +652,31 @@ function createSourcesDict( filenames, filenameMap, dir ) {
595
652
  return { sources, files, repeated };
596
653
  }
597
654
 
655
+ /**
656
+ * An `await`able function to fake a real asynchronous event.
657
+ *
658
+ * @returns {Promise<unknown>}
659
+ */
660
+ async function waitForNextEventLoopIteration() {
661
+ return new Promise( ( r ) => {
662
+ setTimeout( r, 0 );
663
+ });
664
+ }
665
+
666
+ /**
667
+ * An actual async function that uses `setTimeout()` to allow the Node event loop to
668
+ * start its next iteration, but only if the `abortSignal` is defined.
669
+ *
670
+ * @param {AbortSignal?} abortSignal
671
+ * @returns {Promise<void>}
672
+ */
673
+ async function checkAsyncAbortFlag( abortSignal ) {
674
+ if (!abortSignal)
675
+ return;
676
+ await waitForNextEventLoopIteration();
677
+ abortSignal.throwIfAborted();
678
+ }
679
+
598
680
  module.exports = {
599
681
  parseX,
600
682
  compileX,
@@ -430,10 +430,7 @@ function resolve( model ) {
430
430
  }
431
431
  else if (!allowedInMain || !isTopLevelElement) {
432
432
  warning( 'def-unsupported-key', [ art.key.location, art ],
433
- { '#': allowedInMain ? 'sub' : 'std', keyword: 'key' }, {
434
- std: '$(KEYWORD) is only supported for elements in an entity or an aspect',
435
- sub: '$(KEYWORD) is only supported for top-level elements',
436
- } );
433
+ { '#': allowedInMain ? 'sub' : 'kind', keyword: 'key' } );
437
434
  }
438
435
  }
439
436
 
@@ -905,10 +902,13 @@ function resolve( model ) {
905
902
  }
906
903
  else if (effectiveType( art )?.elements && !art.$inferred) {
907
904
  // For inferred (e.g. included) calc elements, this error is already emitted at the origin.
908
- if (art.type)
905
+ if (art.type) {
909
906
  error( 'type-unexpected-structure', [ art.type.location, art ], { '#': 'calc' } );
910
- else
911
- error( 'ref-unexpected-structured', [ art.value.location, art ], { '#': 'expr' } );
907
+ }
908
+ else {
909
+ error( 'ref-unexpected-structured', [ art.value.location, art ],
910
+ { '#': 'struct-expr', elemref: art.value } );
911
+ }
912
912
  }
913
913
  else if (effectiveType( art )?.items && !art.$inferred) {
914
914
  // For inferred (e.g. included) calc elements, this error is already emitted at the origin.
@@ -349,42 +349,46 @@ function tweakAssocs( model ) {
349
349
  setLink( fk, '_effectiveType', fk );
350
350
  fk.targetElement = copyExpr( orig.targetElement, location );
351
351
  if (elem._redirected)
352
- rewriteKey( elem, fk.targetElement );
352
+ rewriteKey( elem, fk );
353
353
  } );
354
354
  if (elem.foreignKeys) // Possibly no fk was set
355
355
  elem.foreignKeys[$inferred] = 'rewrite';
356
356
  }
357
357
 
358
- function rewriteKey( elem, targetElement ) {
358
+ function rewriteKey( elem, fk ) {
359
+ const { targetElement } = fk;
359
360
  let projectedKey = null;
360
361
  // rewrite along redirection chain
361
362
  for (const alias of elem._redirected) {
362
- if (alias.kind !== '$tableAlias')
363
- continue;
364
-
365
- projectedKey = firstProjectionForPath( targetElement.path, 0, alias, null );
366
- if (projectedKey.elem) {
367
- const item = targetElement.path[projectedKey.index];
368
- item.id = projectedKey.elem.name.id;
369
- if (projectedKey.index > 0)
370
- targetElement.path.splice(0, projectedKey.index);
371
- }
372
- else {
373
- setArtifactLink( targetElement.path[0], null );
374
- setArtifactLink( targetElement, null );
363
+ if (alias.kind === '$tableAlias') {
364
+ projectedKey = firstProjectionForPath( targetElement.path, 0, alias, null );
365
+ if (projectedKey.elem) {
366
+ const item = targetElement.path[projectedKey.index];
367
+ item.id = projectedKey.elem.name.id;
368
+ if (projectedKey.index > 0)
369
+ targetElement.path.splice(0, projectedKey.index);
370
+ }
371
+ else {
372
+ setArtifactLink( targetElement.path[0], null );
373
+ setArtifactLink( targetElement, null );
375
374
 
376
- const culprit = !elem.target.$inferred && elem.target ||
375
+ const culprit = !elem.target.$inferred && elem.target ||
377
376
  elem.value?.path?.[elem.value.path.length - 1] ||
378
377
  elem;
379
- // TODO: probably better to collect the non-projected foreign keys
380
- // and have one message for all
381
- error('rewrite-undefined-key', [ weakLocation( culprit.location ), elem ], {
382
- '#': 'std',
383
- id: targetElement.path.map(p => p.id).join('.'),
384
- target: alias._main,
385
- name: elem.name.id,
386
- });
387
- return null;
378
+ // TODO: probably better to collect the non-projected foreign keys
379
+ // and have one message for all
380
+ error('rewrite-undefined-key', [ weakLocation( culprit.location ), elem ], {
381
+ '#': 'std',
382
+ id: targetElement.path.map(p => p.id).join('.'),
383
+ target: alias._main,
384
+ name: elem.name.id,
385
+ });
386
+ return null;
387
+ }
388
+ }
389
+ else {
390
+ // e.g. redirection target is entity that includes original target
391
+ projectedKey = { elem: findTargetElement( alias, targetElement ) };
388
392
  }
389
393
  }
390
394
 
@@ -397,6 +401,24 @@ function tweakAssocs( model ) {
397
401
  return null;
398
402
  }
399
403
 
404
+ /**
405
+ * Find the target element in the given redirection target.
406
+ * Used to find the target element in entities that include the original
407
+ * target entity.
408
+ *
409
+ * @param redirected
410
+ * @param targetElement
411
+ * @returns {*|null}
412
+ */
413
+ function findTargetElement( redirected, targetElement ) {
414
+ for (const step of targetElement.path) {
415
+ redirected = redirected.elements?.[step.id];
416
+ if (!redirected)
417
+ return null;
418
+ }
419
+ return redirected;
420
+ }
421
+
400
422
  // TODO: there is no need to rewrite the on condition of non-leading queries,
401
423
  // i.e. we could just have on = {…}
402
424
  // TODO: re-check $self rewrite (with managed composition of aspects),
@@ -1,4 +1,4 @@
1
- // Base class for generated parser, for redepage v0.2.7
1
+ // Base class for generated parser, for redepage v0.3.0
2
2
 
3
3
  'use strict';
4
4
 
@@ -1 +1 @@
1
- 7489417512fa82b33fb6799686ce2917
1
+ 1f3bba2acb882f5120a55a01eb9a7bdc