@sap/cds-compiler 5.7.2 → 5.8.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 (73) hide show
  1. package/CHANGELOG.md +62 -2
  2. package/bin/cdsse.js +13 -1
  3. package/doc/CHANGELOG_BETA.md +7 -0
  4. package/lib/api/options.js +2 -1
  5. package/lib/api/validate.js +9 -0
  6. package/lib/base/location.js +1 -1
  7. package/lib/base/message-registry.js +55 -20
  8. package/lib/base/messages.js +5 -2
  9. package/lib/base/model.js +8 -6
  10. package/lib/checks/assocOutsideService.js +40 -0
  11. package/lib/checks/featureFlags.js +4 -1
  12. package/lib/checks/types.js +7 -4
  13. package/lib/checks/validator.js +3 -0
  14. package/lib/compiler/assert-consistency.js +11 -5
  15. package/lib/compiler/checks.js +79 -17
  16. package/lib/compiler/define.js +60 -3
  17. package/lib/compiler/extend.js +1 -2
  18. package/lib/compiler/generate.js +1 -1
  19. package/lib/compiler/populate.js +17 -6
  20. package/lib/compiler/propagator.js +1 -1
  21. package/lib/compiler/resolve.js +181 -150
  22. package/lib/compiler/shared.js +276 -22
  23. package/lib/compiler/tweak-assocs.js +15 -4
  24. package/lib/compiler/xpr-rewrite.js +76 -50
  25. package/lib/edm/annotations/edmJson.js +1 -1
  26. package/lib/edm/annotations/genericTranslation.js +2 -2
  27. package/lib/edm/csn2edm.js +2 -2
  28. package/lib/edm/edmPreprocessor.js +15 -9
  29. package/lib/edm/edmUtils.js +12 -5
  30. package/lib/gen/CdlGrammar.checksum +1 -0
  31. package/lib/gen/CdlParser.js +2239 -2229
  32. package/lib/gen/Dictionary.json +55 -8
  33. package/lib/json/from-csn.js +37 -17
  34. package/lib/json/to-csn.js +4 -0
  35. package/lib/language/genericAntlrParser.js +7 -0
  36. package/lib/main.d.ts +5 -1
  37. package/lib/model/cloneCsn.js +1 -0
  38. package/lib/model/csnRefs.js +1 -0
  39. package/lib/model/csnUtils.js +0 -5
  40. package/lib/modelCompare/utils/filter.js +2 -2
  41. package/lib/optionProcessor.js +2 -0
  42. package/lib/parsers/AstBuildingParser.js +72 -34
  43. package/lib/parsers/CdlGrammar.g4 +20 -19
  44. package/lib/parsers/XprTree.js +206 -0
  45. package/lib/parsers/index.js +1 -1
  46. package/lib/render/toCdl.js +61 -89
  47. package/lib/render/toSql.js +59 -29
  48. package/lib/render/utils/standardDatabaseFunctions.js +252 -15
  49. package/lib/transform/addTenantFields.js +9 -3
  50. package/lib/transform/db/assocsToQueries/transformExists.js +3 -0
  51. package/lib/transform/db/assocsToQueries/utils.js +10 -3
  52. package/lib/transform/db/expansion.js +3 -1
  53. package/lib/transform/db/flattening.js +7 -3
  54. package/lib/transform/db/killAnnotations.js +1 -0
  55. package/lib/transform/db/processSqlServices.js +70 -17
  56. package/lib/transform/draft/db.js +8 -3
  57. package/lib/transform/draft/odata.js +27 -4
  58. package/lib/transform/effective/main.js +37 -10
  59. package/lib/transform/effective/misc.js +4 -9
  60. package/lib/transform/effective/service.js +34 -0
  61. package/lib/transform/effective/types.js +28 -17
  62. package/lib/transform/forOdata.js +36 -10
  63. package/lib/transform/forRelationalDB.js +30 -18
  64. package/lib/transform/odata/adaptAnnotationRefs.js +37 -21
  65. package/lib/transform/odata/createForeignKeys.js +120 -116
  66. package/lib/transform/odata/flattening.js +10 -8
  67. package/lib/transform/transformUtils.js +58 -25
  68. package/lib/transform/translateAssocsToJoins.js +10 -6
  69. package/lib/transform/universalCsn/coreComputed.js +5 -1
  70. package/package.json +1 -1
  71. package/share/messages/message-explanations.json +1 -0
  72. package/share/messages/rewrite-not-supported.md +5 -0
  73. package/share/messages/rewrite-undefined-key.md +94 -0
@@ -16,9 +16,11 @@ const {
16
16
  forEachMember,
17
17
  forEachMemberRecursively,
18
18
  isDeprecatedEnabled,
19
+ isBetaEnabled,
19
20
  } = require('../base/model');
20
21
  const { typeParameters } = require('./builtins');
21
22
  const { propagationRules } = require('../base/builtins');
23
+ const { annotationVal } = require('./utils');
22
24
 
23
25
  const $location = Symbol.for( 'cds.$location' );
24
26
 
@@ -32,7 +34,10 @@ function check( model ) {
32
34
  error, warning, info, message,
33
35
  } = model.$messageFunctions;
34
36
 
35
- const { getOrigin } = model.$functions;
37
+ const {
38
+ getOrigin,
39
+ getInheritedProp,
40
+ } = model.$functions;
36
41
 
37
42
  checkSapCommonLocale( model );
38
43
  checkSapCommonTextsAspects( model );
@@ -55,7 +60,8 @@ function check( model ) {
55
60
  function checkEvent( def ) {
56
61
  // Ensure that events are structured. Up to compiler v4, we allowed non-structured events,
57
62
  // because when we introduced them, it was not fully specified what they are.
58
- if (def.kind === 'event' && !def._effectiveType?.elements && !def._effectiveType?.projection)
63
+ if (def.kind === 'event' && def._effectiveType &&
64
+ !def._effectiveType.elements && !def._effectiveType.projection)
59
65
  message( 'def-expected-structured', [ (def.type || def.name).location, def ] );
60
66
  }
61
67
 
@@ -268,11 +274,10 @@ function check( model ) {
268
274
  function checkLocalizedElement( elem ) {
269
275
  if (elem.localized?.val) {
270
276
  const type = elem._effectiveType;
271
- // TODO(v6): Also for ` || type?.elements`; (#13154)
272
- if (type?.category === 'map') {
273
- error( 'ref-unexpected-localized-map', [ elem.type?.location, elem ],
274
- { keyword: 'localized' },
275
- 'Map types can\'t be used with $(KEYWORD)' );
277
+ if (type?.category === 'map' ||
278
+ (type?.elements && isBetaEnabled( model.options, 'v6preview' ))) {
279
+ error( 'def-unexpected-localized', [ elem.localized.location, elem ],
280
+ { keyword: 'localized', '#': type.category === 'map' ? 'map' : 'struct' } );
276
281
  }
277
282
  else if (!type || !type.builtin || type.category !== 'string') {
278
283
  // See discussion issue #6520: should we allow all scalar types?
@@ -584,14 +589,36 @@ function check( model ) {
584
589
  }
585
590
 
586
591
  function checkDefaultValue( art ) {
587
- if (!art.default || !art._effectiveType)
592
+ if (!art._effectiveType)
588
593
  return;
589
- // TODO(v6): Also reject default for structures (#13154)
590
- const isStructured = art._effectiveType?.name.id === 'cds.Map';
591
- if (!isStructured)
594
+ if (art.kind !== 'element' && art.kind !== 'type' && art.kind !== 'param')
595
+ return;
596
+
597
+ const defaultValue = getInheritedProp( art, 'default' );
598
+ if (defaultValue?.val === undefined)
592
599
  return;
593
- if (art.default?.val !== undefined) {
594
- error( 'type-unexpected-default', [ art.default.location, art ], {
600
+
601
+ // Check that "not null" artifacts don't have `null` default values.
602
+ // At least one property must be written explicitly to avoid reporting on inferred elements.
603
+ if (isBetaEnabled( model.options, 'v6preview' ) &&
604
+ (art.default?.val === null || art.notNull?.val)) {
605
+ const notNullValue = getInheritedProp( art, 'notNull' );
606
+ if (notNullValue?.val && defaultValue?.val === null) {
607
+ const loc = (art.default || art.notNull)?.location || art.location;
608
+ const variant = art.kind + (!art.default && art.notNull ? 'NotNull' : 'DefaultNull');
609
+ message( 'type-unexpected-null', [ loc, art ], {
610
+ '#': variant,
611
+ art,
612
+ keyword: 'not null',
613
+ value: 'null',
614
+ } );
615
+ }
616
+ }
617
+
618
+ // TODO(v6): Also reject default for structures (#13154), i.e. add `|| !!art.….elements`
619
+ const isStructured = art._effectiveType?.name.id === 'cds.Map';
620
+ if (isStructured) {
621
+ error( 'type-unexpected-default', [ defaultValue.location, art ], {
595
622
  '#': 'map', keyword: 'default', type: 'cds.Map',
596
623
  } );
597
624
  }
@@ -956,21 +983,32 @@ function check( model ) {
956
983
  }
957
984
 
958
985
  function checkAnnotationAssignment1( art, anno ) {
986
+ const name = anno.name?.id;
959
987
  if (art.$contains?.$annotation && anno.kind === '$annotation') {
960
988
  if (checkAnnotationAcceptsExpressions( anno, art ))
961
989
  checkAnnotationExpressions( anno, art );
962
990
  }
963
991
 
964
- if (!model.vocabularies)
965
- return;
966
-
967
992
  // Has been slightly adapted for model.vocabularies but comments need to be
968
993
  // adapted, etc.
969
994
  // TODO: rework completely!
970
995
  // TODO: if we have such a check, consider #variant, anno.@anno, anno@anno
971
996
  // Sanity checks (ignore broken assignments)
972
- if (!anno.name?.id)
997
+ if (!name)
998
+ return;
999
+
1000
+ // Compiler specific annotation validation.
1001
+ const annotationChecks = {
1002
+ __proto__: null,
1003
+ '@cds.redirection.target': checkAnnoRedirectionTarget,
1004
+ };
1005
+
1006
+ const annoName = `@${ name }`;
1007
+ annotationChecks[annoName]?.(anno, art);
1008
+
1009
+ if (!model.vocabularies)
973
1010
  return;
1011
+
974
1012
  // Just a little workaround to adapt to changed `name`s, not nice coding:
975
1013
  const hashIndex = anno.name.id.indexOf( '#' );
976
1014
  const path = (hashIndex > 0 ? anno.name.id.substring( 0, hashIndex ) : anno.name.id)
@@ -1000,6 +1038,23 @@ function check( model ) {
1000
1038
  checkAnnotationAssignment( anno, artifact, endOfPath, art );
1001
1039
  }
1002
1040
 
1041
+ function checkAnnoRedirectionTarget( anno, art ) {
1042
+ if (anno.$inferred)
1043
+ return;
1044
+ if (!annotationVal( anno ))
1045
+ return; // ignore falsey values, including 'null'
1046
+
1047
+ // Non-entities can't have this annotation, nor can non-service entities,
1048
+ // nor can complex queries such as joins, unions, or selecting from an association.
1049
+
1050
+ const isIgnored = isComplexView( art ) || (art.kind !== 'entity') || !art._service;
1051
+ if (isIgnored) {
1052
+ const loc = anno.val ? anno.location : anno.name.location;
1053
+ info( 'anno-ignoring-redirection-target', [ loc, art ], { anno: anno.name.id },
1054
+ '$(ANNO) has no effect here; use it on simple projections inside services' );
1055
+ }
1056
+ }
1057
+
1003
1058
  // Perform checks for annotation assignment 'anno', using corresponding annotation declaration,
1004
1059
  // made of 'annoDecl' (artifact or undefined) and 'elementDecl' (annotation or element
1005
1060
  // or undefined). Report errors on 'options.messages.
@@ -1316,4 +1371,11 @@ function isComposition( model, elem ) {
1316
1371
  return false;
1317
1372
  }
1318
1373
 
1374
+ function isComplexView( art ) {
1375
+ if (!art?.query) // non-query
1376
+ return false;
1377
+ // Either UNION, JOIN, SUB-SELECT, or target is an association
1378
+ return (!art.query.from?._artifact || art.query.from._artifact.kind === 'element');
1379
+ }
1380
+
1319
1381
  module.exports = check;
@@ -572,9 +572,57 @@ function define( model ) {
572
572
  initMembers( art, art, block );
573
573
  if (art.params)
574
574
  initDollarParameters( art ); // $parameters
575
+ if (art.query) {
576
+ initArtifactQuery( art );
577
+ restrictToSimpleProjection( art );
578
+ }
579
+ }
575
580
 
576
- if (!art.query)
581
+ /**
582
+ * Restrict the query of `art` to only simple projections, i.e. those without 'group by', etc.
583
+ *
584
+ * @param {XSN.Artifact} art
585
+ */
586
+ function restrictToSimpleProjection( art ) {
587
+ const { query } = art;
588
+
589
+ if (art.kind !== 'type')
590
+ return; // TODO(v6): Also for event
591
+
592
+ if (!query.from?.path)
593
+ return; // union, sub-select, etc. should already be rejected
594
+
595
+ const check = (prop, keyword) => (query[prop] && { prop: query[prop], keyword });
596
+ const unexpectedQueryProp = check( 'where', 'where' ) ||
597
+ check( 'groupBy', 'group by' ) ||
598
+ check( 'limit', 'limit' ) ||
599
+ check( 'having', 'having' ) ||
600
+ check( 'orderBy', 'order by' ) ||
601
+ null;
602
+
603
+ if (unexpectedQueryProp) {
604
+ error( 'query-unexpected-prop', [ unexpectedQueryProp.prop.location, query ], {
605
+ '#': art.kind,
606
+ keyword: unexpectedQueryProp.keyword,
607
+ }, {
608
+ std: 'Unexpected $(KEYWORD) for projection clause used as type expression',
609
+ type: 'Unexpected $(KEYWORD) for type definition',
610
+ event: 'Unexpected $(KEYWORD) for event definition',
611
+ } );
577
612
  return;
613
+ }
614
+
615
+ const firstCondition = query.from.path.find(step => step.where)?.where;
616
+ if (firstCondition) {
617
+ error( 'query-unexpected-filter', [ firstCondition.location, query ], { '#': art.kind }, {
618
+ std: 'Unexpected filter in query source for projection clause used as type expression',
619
+ type: 'Unexpected filter in projection clause of type definition',
620
+ event: 'Unexpected filter in projection clause of event definition',
621
+ } );
622
+ }
623
+ }
624
+
625
+ function initArtifactQuery( art ) {
578
626
  art.$queries = [];
579
627
  setLink( art, '_from', [] ); // for sequence of resolve steps - TODO: remove
580
628
  if (!setLink( art, '_leadingQuery', initQueryExpression( art.query, art ) ) )
@@ -593,6 +641,12 @@ function define( model ) {
593
641
  checkRedefinition( art );
594
642
  const block = art._block;
595
643
  initMembers( art, art, block );
644
+
645
+ if (art.query) {
646
+ initArtifactQuery( art );
647
+ error( 'def-unsupported-projection', [ art.location, art ], null,
648
+ 'Projections for annotation definitions are not supported' );
649
+ }
596
650
  }
597
651
 
598
652
  function initArtifactParentLink( art, definitions, path, pathIndex ) {
@@ -665,7 +719,7 @@ function define( model ) {
665
719
  // Init queries: --------------------------------------------------------------
666
720
 
667
721
  // art is:
668
- // - entity for top-level queries (including UNION args)
722
+ // - entity/event/type for top-level queries (including UNION args)
669
723
  // - $tableAlias for sub query in FROM - TODO: what about UNION there?
670
724
  // - $query for real sub query (in columns, WHERE, ...), again: what about UNION there?
671
725
  function initQueryExpression( query, art ) {
@@ -988,7 +1042,7 @@ function define( model ) {
988
1042
  if (exprOrPathElement.$expected === 'exists')
989
1043
  exprOrPathElement.$expected = 'approved-exists';
990
1044
  // Drill down
991
- if (exprOrPathElement.args)
1045
+ if (Array.isArray(exprOrPathElement.args))
992
1046
  exprOrPathElement.args.forEach( elem => approveExistsInChildren( elem ) );
993
1047
  else if (exprOrPathElement.where?.args)
994
1048
  exprOrPathElement.where.args.forEach( elem => approveExistsInChildren( elem ) );
@@ -1155,6 +1209,9 @@ function define( model ) {
1155
1209
  setLink( elem, '_block', bl );
1156
1210
  const existing = parent[prop]?.[name];
1157
1211
  const add = construct !== parent && (!existing || elem.$inferred !== 'include');
1212
+ // don't dump with `entity T {}; extend T with { extend e {}; e {}; e {} };`:
1213
+ if (elem.$duplicates === true && add)
1214
+ elem.$duplicates = null;
1158
1215
  setMemberParent( elem, name, parent, add && prop );
1159
1216
  // console.log(message( null, elem.location, elem, {}, 'Info', 'INIT').toString())
1160
1217
  checkRedefinition( elem );
@@ -200,8 +200,7 @@ function extend( model ) {
200
200
  */
201
201
  function extendForeignKeys( art ) {
202
202
  // See extendArtifactAfter() for targetAspect/items handling.
203
- const sub = art.items || art.targetAspect?.elements && art.targetAspect;
204
- if (!art._extensions || sub)
203
+ if (!art._extensions || art.items || art.targetAspect?.elements)
205
204
  return;
206
205
 
207
206
  // push down foreign keys
@@ -141,7 +141,7 @@ function generate( model ) {
141
141
  // Not supported anyway, but important for recompilation (which fails correctly).
142
142
  const loc = elem.localized?.location || elem.location;
143
143
  error( 'def-unexpected-localized', [ loc, elem ],
144
- { '#': !include ? 'std' : 'include', art: textsAspect } );
144
+ { '#': !include ? 'elements' : 'include', art: textsAspect } );
145
145
  hasError = true;
146
146
  }
147
147
  else if (elem.targetAspect) {
@@ -740,6 +740,7 @@ function populate( model ) {
740
740
  query._main.elements = elemsParent.elements;
741
741
  }
742
742
 
743
+ const isExpand = (query.expand === columns);
743
744
  if (!columns)
744
745
  columns = [ { val: '*' } ];
745
746
 
@@ -768,7 +769,7 @@ function populate( model ) {
768
769
  initFromColumns( query, col.inline, col );
769
770
  }
770
771
  else if (!col.$replacement) {
771
- const id = ensureColumnName( col, i, query );
772
+ const id = ensureColumnName( col, i, query, isExpand );
772
773
  col.kind = 'element';
773
774
  dictAdd( elemsParent.elements, id, col, ( name, location ) => {
774
775
  error( 'duplicate-definition', [ location, query ], { name, '#': 'element' } );
@@ -780,8 +781,17 @@ function populate( model ) {
780
781
  return true;
781
782
  }
782
783
 
783
- // TODO: probably do this already in definer.js
784
- function ensureColumnName( col, colIndex, query ) {
784
+ /**
785
+ * TODO: probably do this already in definer.js
786
+ *
787
+ * @param col
788
+ * @param {number} colIndex
789
+ * @param query
790
+ * @param {boolean} insideExpand
791
+ * Whether the column is inside 'expand'.
792
+ * Anonymous 'expands' don't have a column parent, hence why we need to know this explicitly.
793
+ */
794
+ function ensureColumnName( col, colIndex, query, insideExpand ) {
785
795
  if (col.name)
786
796
  return col.name.id;
787
797
  if (col.inline || col.val === '*')
@@ -795,8 +805,9 @@ function populate( model ) {
795
805
  return col.name.id;
796
806
  }
797
807
  }
798
- else if (col.expand || col.value && (col._columnParent || query._parent.kind !== 'select')) {
799
- // _columnParent => inline/expand; _parent -> only allowed in sub-selects
808
+ else if (insideExpand || col.expand ||
809
+ col.value && (col._columnParent || query._parent.kind !== 'select')) {
810
+ // _columnParent => inline/expand with path head; _parent -> only allowed in sub-selects
800
811
  error( 'query-req-name', [ col.value?.location || col.location, query ], {},
801
812
  'Alias name is required for this select item' );
802
813
  }
@@ -851,7 +862,7 @@ function populate( model ) {
851
862
  let seenWildcard = null;
852
863
  let colIndex = 0;
853
864
  for (const col of columns) {
854
- const id = ensureColumnName( col, colIndex, query );
865
+ const id = ensureColumnName( col, colIndex, query, false );
855
866
  if (id) {
856
867
  col.$replacement = !seenWildcard;
857
868
  siblings[id] = !(id in siblings) && col;
@@ -44,7 +44,7 @@ function propagate( model ) {
44
44
  precision: always,
45
45
  scale: always,
46
46
  srid: always,
47
- localized: always,
47
+ localized: withKind,
48
48
  target: notWithExpand,
49
49
  targetAspect,
50
50
  cardinality: notWithExpand,