@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
package/CHANGELOG.md CHANGED
@@ -8,6 +8,41 @@ Note: `beta` fixes, changes and features are usually not listed in this ChangeLo
8
8
  but in [doc/CHANGELOG_BETA.md](doc/CHANGELOG_BETA.md).
9
9
  The compiler behavior concerning `beta` features can change at any time without notice.
10
10
 
11
+ ## Version 6.3.0 - 2025-08-28
12
+
13
+ ### Added
14
+
15
+ - compiler: Column casts can now use more modifiers such as `default` directly.
16
+ - for.odata/to.edm(x):
17
+ + New option `draftUserDescription` is now available. It adds the fields `CreatedByUserDescription`,
18
+ `LastChangedByUserDescription`, `InProcessByUserDescription` to the `DraftAdministrativeData` entity.
19
+ - to.sql:
20
+ + Structures with only one element can now be compared to scalar values.
21
+ This also applies to associations with only one foreign key.
22
+ + `cds.UInt8` can now be used in SQL dialects "h2" and "postgres".
23
+ + Managed associations can now be used in comparisons, e.g. `assoc = struct`.
24
+ + Structures and managed associations with only one element can be compared with scalars, e.g. `struct = 1`.
25
+ + In the draft use case, the `DRAFT.DraftAdministrativeData` entity now includes the following fields by default:
26
+ `CreatedByUserDescription`, `LastChangedByUserDescription`, `InProcessByUserDescription`, and `DraftMessages`.
27
+
28
+ ### Changed
29
+
30
+ - Update OData vocabularies: Common
31
+ - cdsc: EDMX output uses XML comments as service separators instead of `//`.
32
+ If there is only one service, no header is printed, allowing piping the output to a file.
33
+ - to.sql: path expressions which end in a foreign key are now always optimized to use the element of the source side.
34
+
35
+ ### Fixed
36
+
37
+ - compiler: Redirecting associations to non-query entities was fixed.
38
+ - to.sql/to.edm(x): References to associations can now be compared to other associations and structures.
39
+ - to.sql: Referencing a foreign key of an `@cds.persistence.skip` entity previously caused an
40
+ error in queries. Now the foreign key in the source entity is resolved and rendered.
41
+
42
+ ### Removed
43
+
44
+ - for.odata/to.edm(x): The `addAnnotationAddressViaNavigationPath` option has been removed. Its functionality is included in the `draftMessages` option.
45
+
11
46
  ## Version 6.2.2 - 2025-07-28
12
47
 
13
48
  ### Fixed
package/bin/cdsc.js CHANGED
@@ -462,13 +462,15 @@ async function executeCommandLine( command, options, args ) {
462
462
  }
463
463
  else if (options.json) {
464
464
  const result = main.to.edm.all(csn, options);
465
+ const omitHeadline = Object.keys(result).length === 1;
465
466
  for (const serviceName in result)
466
- writeToFileOrDisplay(options.out, `${ serviceName }.json`, result[serviceName]);
467
+ writeToFileOrDisplay(options.out, `${ serviceName }.json`, result[serviceName], omitHeadline);
467
468
  }
468
469
  else {
469
470
  const result = main.to.edmx.all(csn, options);
471
+ const omitHeadline = Object.keys(result).length === 1;
470
472
  for (const serviceName in result)
471
- writeToFileOrDisplay(options.out, `${ serviceName }.xml`, result[serviceName]);
473
+ writeToFileOrDisplay(options.out, `${ serviceName }.xml`, result[serviceName], omitHeadline);
472
474
  }
473
475
  return model;
474
476
  }
@@ -710,8 +712,13 @@ async function executeCommandLine( command, options, args ) {
710
712
  hdbview: true,
711
713
  hdbprojectionview: true,
712
714
  };
713
- const commentStarter = fileName.split('.').pop() in sqlTypes ? '--$' : '//';
714
- process.stdout.write(`${ commentStarter } ------------------- ${ fileName } -------------------\n`);
715
+ if (fileName.endsWith('.xml')) {
716
+ process.stdout.write(`<!-- ------------------- ${ fileName.replaceAll('-->', '-- >') } ------------------- -->\n`);
717
+ }
718
+ else {
719
+ const commentStarter = fileName.split('.').pop() in sqlTypes ? '--$' : '//';
720
+ process.stdout.write(`${ commentStarter } ------------------- ${ fileName } -------------------\n`);
721
+ }
715
722
  }
716
723
 
717
724
  process.stdout.write(`${ content }\n`);
@@ -36,7 +36,6 @@ const publicOptionsNewAPI = [
36
36
  'booleanEquality',
37
37
  'dollarNowAsTimestamp',
38
38
  // ODATA
39
- 'addAnnotationAddressViaNavigationPath',
40
39
  'odataOpenapiHints',
41
40
  'edm4OpenAPI',
42
41
  'odataVersion',
@@ -50,6 +49,7 @@ const publicOptionsNewAPI = [
50
49
  'odataVocabularies',
51
50
  'odataNoCreator',
52
51
  'draftMessages',
52
+ 'draftUserDescription',
53
53
  'service',
54
54
  'serviceNames',
55
55
  // to.cdl
@@ -178,7 +178,7 @@ const centralMessages = {
178
178
  'service-nested-context': { severity: 'Error', configurableFor: true }, // does not hurt compile, TODO
179
179
  'service-nested-service': { severity: 'Error' }, // not supported yet; TODO: configurableFor:'test'?
180
180
 
181
- 'expr-unexpected-operator': { severity: 'Error', configurableFor: true },
181
+ 'expr-unexpected-operator': { severity: 'Error' },
182
182
 
183
183
  // Published! Used in @sap/cds-lsp; if renamed, add to oldMessageIds and contact colleagues
184
184
  // Also used by other projects that rely on double-quotes for delimited identifiers.
@@ -196,7 +196,7 @@ const centralMessages = {
196
196
  'syntax-missing-as': { severity: 'Error', configurableFor: true },
197
197
  'syntax-missing-proj-semicolon': { severity: 'Warning' },
198
198
  'syntax-unexpected-after': { severity: 'Error' },
199
- 'syntax-unexpected-filter': { severity: 'Error', configurableFor: true },
199
+ 'syntax-unexpected-filter': { severity: 'Error', configurableFor: 'v7' },
200
200
  'syntax-unexpected-many-one': { severity: 'Error' },
201
201
  'syntax-deprecated-ref-virtual': { severity: 'Error' },
202
202
  'syntax-unexpected-reserved-word': { severity: 'Error', configurableFor: true },
@@ -786,8 +786,11 @@ const centralMessageTexts = {
786
786
  calc: 'Calculated elements can\'t use parameter references',
787
787
  },
788
788
  'ref-unexpected-structured': {
789
- std: 'Unexpected usage of structured type $(ELEMREF)',
790
- expr: 'Structured elements can\'t be used in expressions',
789
+ std: 'Unexpected usage of structured element $(ELEMREF)',
790
+ assoc: 'Unexpected usage of managed association $(ELEMREF)',
791
+ 'struct-expr': 'Structured element $(ELEMREF) can\'t be used in expressions with scalars; only possible for structures with one leaf-element',
792
+ 'assoc-expr': 'Associations $(ELEMREF) can\'t be used in expressions with scalars; only possible for association with one foreign key',
793
+ complexExpr: 'Unexpected reference to a structured element $(ELEMREF) in expression $(VALUE)',
791
794
  },
792
795
  'ref-unexpected-virtual': {
793
796
  std: 'Unexpected reference to virtual element $(NAME)', // "std" currently unused
@@ -801,8 +804,14 @@ const centralMessageTexts = {
801
804
  'with-filter': 'Unexpected reference to an association with filter',
802
805
  'self-with-filter': 'Unexpected column reference starting with $(ALIAS) to an association with filter',
803
806
  self: 'A reference to an unmanaged association is only valid when compared via $(CODE)',
807
+
804
808
  expr: 'Associations can\'t be used as values in expressions',
805
809
  'expr-comp': 'Compositions can\'t be used as values in expressions',
810
+ 'anno-expr': 'Associations can\'t be used in expressions for annotation values',
811
+ 'anno-expr-comp': 'Compositions can\'t be used in expressions for annotation values',
812
+ 'query-expr': 'Unmanaged associations can\'t be used in expressions in queries',
813
+ 'query-expr-comp': 'Unmanaged compositions can\'t be used in expressions in queries',
814
+
806
815
  'assoc-stored': 'Associations and compositions can\'t be used as values in stored calculated elements',
807
816
 
808
817
  'managed-filter': 'Unexpected managed association $(NAME) in filter expression of $(ID)',
@@ -831,6 +840,7 @@ const centralMessageTexts = {
831
840
  unmanagedleaf: 'Unexpected unmanaged association as final path step of $(ELEMREF) in an ON-condition',
832
841
  'calc-non-fk': 'Can\'t follow association $(ID) in path $(ELEMREF) in a stored calculated element; only foreign keys can be referred to, but not $(NAME)',
833
842
  'calc-unmanaged': 'Can\'t follow association $(ID) in path $(ELEMREF) in a stored calculated element',
843
+ 'calc-missing': 'Missing foreign key access for association $(ID) in path $(ELEMREF) in a stored calculated element',
834
844
  },
835
845
  'ref-unexpected-filter': {
836
846
  std: 'Unexpected filter in path $(ELEMREF)', // unused
@@ -983,10 +993,16 @@ const centralMessageTexts = {
983
993
  },
984
994
  'def-unexpected-key': {
985
995
  std: '$(ART) can\'t have additional keys',
986
- virtual: 'Unexpected $(PROP) for virtual element',
996
+ virtual: 'Unexpected $(KEYWORD) for virtual element',
987
997
  // TODO: Better message?
988
998
  include: '$(ART) can\'t have additional keys (through include)',
989
- invalidType: 'Unexpected $(PROP) for element of type $(TYPE)',
999
+ invalidType: 'Unexpected $(KEYWORD) for element of type $(TYPE)',
1000
+ },
1001
+ 'def-unsupported-key': {
1002
+ std: '$(KEYWORD) is not supported here', // unused variant
1003
+ kind: '$(KEYWORD) is only supported for elements in an entity or an aspect',
1004
+ sub: '$(KEYWORD) is only supported for top-level elements',
1005
+ type: '$(KEYWORD) is not supported for elements of type $(TYPE)',
990
1006
  },
991
1007
  'def-unexpected-localized': {
992
1008
  std: 'Unexpected $(KEYWORD)',
@@ -1046,6 +1062,9 @@ const centralMessageTexts = {
1046
1062
  'include-elements': 'Duplicate element $(NAME) through multiple includes $(SORTED_ARTS)',
1047
1063
  'include-actions': 'Duplicate action or function $(NAME) through multiple includes $(SORTED_ARTS)',
1048
1064
  },
1065
+ 'ref-invalid-assoc-navigation': {
1066
+ std: 'Invalid navigation along association $(ID) in path $(ELEMREF) to target $(NAME) having annotation $(ANNO)',
1067
+ },
1049
1068
  'ref-invalid-element': {
1050
1069
  std: 'Invalid element reference',
1051
1070
  $tableAlias: 'Can\'t refer to source elements of table alias $(ID)',
@@ -1254,7 +1273,17 @@ const centralMessageTexts = {
1254
1273
  publishingFilter: 'Can\'t publish managed association $(ID) with filter, as it must have at least one foreign key',
1255
1274
  },
1256
1275
 
1257
- // tenenat isolation via discriminator column:
1276
+ 'expr-invalid-expansion': {
1277
+ std: 'Path $(NAME) in expression $(VALUE) can\'t be expanded',
1278
+ 'path-mismatch': 'Missing sub path $(NAME) in $(ALIAS) for tuple expansion of $(VALUE); both sides must expand to the same sub paths',
1279
+ 'non-scalar': 'Path $(NAME) in expression $(VALUE) can\'t be expanded as it does not contain any scalar element',
1280
+ },
1281
+ 'expr-unsupported-expansion': {
1282
+ std: 'Unsupported $(ELEMREF) in structural expression $(VALUE)',
1283
+ scalarRef: 'Unsupported scalar reference $(ELEMREF) in structural expression $(VALUE)',
1284
+ },
1285
+
1286
+ // tenant isolation via discriminator column:
1258
1287
  'tenant-invalid-alias-name': {
1259
1288
  std: 'Can\'t have a table alias named $(NAME) in a tenant-dependent entity',
1260
1289
  implicit: 'Provide an explicit table alias name; do not use $(NAME)',
@@ -911,14 +911,20 @@ function transformElementRef( arg ) {
911
911
  if (!ref)
912
912
  return quoted( arg );
913
913
  // Can be used by CSN backends or compiler to create a simple path such as E:elem
914
- return quoted(
915
- ((arg.scope === 'param' || arg.param) ? ':' : '') +
914
+ return quoted( pathToMessageString( arg ) );
915
+ }
916
+
917
+ function pathToMessageString( arg ) {
918
+ const ref = arg?.ref || arg?.path || arg; // support CSN and XSN
919
+ if (!ref)
920
+ return null;
921
+
922
+ return ((arg.scope === 'param' || arg.param) ? ':' : '') +
916
923
  ref.map(
917
924
  item => (typeof item !== 'string'
918
925
  ? `${ item.id }${ item.args ? '(…)' : '' }${ item.where ? '[…]' : '' }`
919
926
  : item)
920
- ).join('.')
921
- );
927
+ ).join('.');
922
928
  }
923
929
 
924
930
  function transformArg( arg, r, args, texts ) {
@@ -1958,4 +1964,5 @@ module.exports = {
1958
1964
  // for tests only
1959
1965
  constructSemanticLocationFromCsnPath,
1960
1966
  homeName,
1967
+ pathToMessageString,
1961
1968
  };
package/lib/base/model.js CHANGED
@@ -26,7 +26,6 @@ const availableBetaFlags = {
26
26
  calcAssoc: true,
27
27
  temporalRawProjection: true,
28
28
  v7preview: true,
29
- draftMessages: true,
30
29
  rewriteAnnotationExpressionsViaType: true,
31
30
  sqlServiceDummies: true,
32
31
  projectionViews: true,
@@ -1,42 +1,29 @@
1
1
  'use strict';
2
2
 
3
+ // Only to be used with validator.js - a correct this value needs to be provided!
4
+ const { forEachMemberRecursively } = require('../model/csnUtils');
5
+
3
6
  /**
4
7
  * Asserts that there is no association usage outside of the specified service.
5
8
  * We do not check in type-ofs - we resolve them, so they are not a problem.
6
9
  *
7
- * @param {object} parent - The parent object in the CSN (Core Schema Notation).
8
- * @param {string} prop - The property name of the parent object.
9
- * @param {object} ref - The reference object.
10
- * @param {Array} path - The path array indicating the location in the CSN.
11
- * @param {object} grandparent - The grandparent object in the CSN.
12
- * @param {string} parentProp - The property name of the grandparent object.
10
+ * @param {CSN.Artifact} artifact Artifact to validate
11
+ * @param {string} artifactName Name of the artifact
13
12
  */
14
- function assertNoAssocUsageOutsideOfService( parent, prop, ref, path, grandparent, parentProp ) {
15
- const artifactName = path[1];
16
- if (parentProp === 'type')
17
- return;
18
-
19
- if (this.csn.definitions[this.options.effectiveServiceName]?.kind !== 'service' ||
20
- !artifactName.startsWith(`${ this.options.effectiveServiceName }.`))
21
- return;
22
-
23
- const { _links } = parent;
24
- // session variables can't have assoc steps, _links of 1 can't have assoc steps
25
- // TODO: (typeof parentProp === 'number' && path[path.length - 2] === 'on') - ignore on-conditions, as they are cut off anyway
26
- if (parent.$scope === '$magic' || _links?.length <= 1 )
13
+ function assertNoAssocUsageOutsideOfService( artifact, artifactName ) {
14
+ if (artifact.kind !== 'entity' || this.csn.definitions[this.options.effectiveServiceName]?.kind !== 'service' ||
15
+ !artifactName.startsWith(`${ this.options.effectiveServiceName }.`))
27
16
  return;
28
17
 
29
- for (let i = 0; i < _links.length - 1; i++) {
30
- const { art } = _links[i];
31
- if (art.target && !art.target.startsWith(`${ this.options.effectiveServiceName }.`)) {
32
- this.error('assoc-invalid-outside-service', path.concat('ref', i),
33
- { name: this.options.effectiveServiceName, id: ref[i].id || ref[i] },
34
- 'Association $(ID) pointing outside of service $(NAME) must not be used');
35
- return;
36
- }
18
+ if (artifact.kind === 'entity' && (artifact.query || artifact.projection)) {
19
+ forEachMemberRecursively(artifact, (element, elementName, prop, path) => {
20
+ if (element && element.target && !element.target.startsWith(`${ this.options.effectiveServiceName }.`)) {
21
+ this.error('assoc-invalid-outside-service', path,
22
+ { name: this.options.effectiveServiceName, id: elementName },
23
+ 'Association $(ID) pointing outside of service $(NAME) must not be published');
24
+ }
25
+ }, [ 'definitions', artifactName ], true, { elementsOnly: true });
37
26
  }
38
27
  }
39
28
 
40
- module.exports = {
41
- ref: assertNoAssocUsageOutsideOfService,
42
- };
29
+ module.exports = assertNoAssocUsageOutsideOfService;
@@ -18,22 +18,6 @@ function checkForHanaTypes( parent, name, type, path ) {
18
18
  }
19
19
  }
20
20
 
21
- /**
22
- * Check that `cds.UInt8` is not used - we don't have a clear idea how to represent it on postgres and h2
23
- *
24
- * @param {object} parent Object with a type
25
- * @param {string} name Name of the type property on parent
26
- * @param {Array} type type to check
27
- * @param {CSN.Path} path
28
- */
29
- function CheckForUInt8( parent, name, type, path ) {
30
- const artifact = this.csn.definitions[path[1]];
31
- if (artifact.kind === 'entity' && isPersistedOnDatabase(artifact) && parent.type === 'cds.UInt8') {
32
- this.error('ref-unexpected-type', [ ...path, 'type' ], { type: 'cds.UInt8', value: this.options.sqlDialect },
33
- 'Type $(TYPE) can\'t be used with sqlDialect $(VALUE)');
34
- }
35
- }
36
-
37
21
  /**
38
22
  * Check types - specifically for postgres and h2
39
23
  *
@@ -44,8 +28,6 @@ function CheckForUInt8( parent, name, type, path ) {
44
28
  */
45
29
  function checkTypes( parent, name, type, path ) {
46
30
  checkForHanaTypes.bind(this)(parent, name, type, path);
47
- if (this.options.sqlDialect === 'postgres' || this.options.sqlDialect === 'h2')
48
- CheckForUInt8.bind(this)(parent, name, type, path);
49
31
  }
50
32
 
51
33
  module.exports = {
@@ -56,8 +56,9 @@ function _checkPathsInStoredCalcElement( parent, value, csnPath ) {
56
56
  else {
57
57
  // It's a managed association - access of the foreign keys is allowed
58
58
  requireForeignKeyAccess(parent, i, (errorIndex) => {
59
+ const variant = errorIndex >= value.length ? 'calc-missing' : 'calc-non-fk';
59
60
  this.error('ref-unexpected-navigation', csnPath, {
60
- '#': 'calc-non-fk', id, elemref: parent, name: value[errorIndex].id || value[errorIndex],
61
+ '#': variant, id, elemref: parent, name: value[errorIndex]?.id || value[errorIndex],
61
62
  });
62
63
  hasPathError = true;
63
64
  });
@@ -135,7 +135,7 @@ function validateOnCondition( member, memberName, property, path ) {
135
135
  ((type.target && type.keys || type.elements) && validStructuredElement ||
136
136
  (type.target && validDollarSelf)) && !type.virtual
137
137
  ) {
138
- // Do nothing - handled by lib/checks/nonexpandableStructured.js
138
+ // Do nothing - handled by tuple expansion
139
139
  }
140
140
  else if (type.items && !type.virtual) {
141
141
  this.error(null, onPath, { elemref: { ref } },
@@ -183,7 +183,7 @@ function requireForeignKeyAccess( parent, refIndex, noForeignKeyCallback ) {
183
183
  ref.splice(refIndex + 1, 1, ...resolved);
184
184
  }
185
185
 
186
- const next = pathId(ref[refIndex + 1]);
186
+ const next = ref[refIndex + 1] && pathId(ref[refIndex + 1]);
187
187
  let possibleKeys = next && assoc.keys.filter(r => r.ref[0] === next);
188
188
  if (!possibleKeys || possibleKeys.length === 0) {
189
189
  noForeignKeyCallback(refIndex + 1);
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { isPersistedOnDatabase, hasPersistenceSkipAnnotation } = require('../model/csnUtils');
3
+ const { isPersistedOnDatabase, applyTransformationsOnNonDictionary } = require('../model/csnUtils');
4
4
  const { isBuiltinType } = require('../base/builtins');
5
5
  const { requireForeignKeyAccess } = require('./onConditions');
6
6
  const { pathId } = require('../model/csnRefs');
@@ -26,8 +26,13 @@ function checkQueryForNoDBArtifacts( query ) {
26
26
  for (const prop of generalQueryProperties) {
27
27
  const queryPart = (query.SELECT || query.SET)[prop];
28
28
  if (Array.isArray(queryPart)) {
29
- for (const part of queryPart)
30
- checkQueryRef.call(this, part, prop === 'columns');
29
+ const that = this;
30
+ applyTransformationsOnNonDictionary((query.SELECT || query.SET), prop, {
31
+ ref: (parent, _name, val, csnPath, tokenStream, refIndex) => {
32
+ const danglingAssocAllowed = tokenStream[refIndex - 1] !== 'exists';
33
+ checkQueryRef.call(that, parent, danglingAssocAllowed);
34
+ },
35
+ }, { skipStandard: { on: true } });
31
36
  }
32
37
  else if (typeof queryPart === 'object') {
33
38
  checkQueryRef.call(this, queryPart, prop === 'columns');
@@ -119,9 +124,10 @@ function _checkExpandInline( obj, previousRefs = [], previousLinks = [] ) {
119
124
  * @param {CSN.Path} ref
120
125
  * @param {object[]} _links
121
126
  * @param {CSN.Path} $path
122
- * @param {boolean} inColumns
127
+ * @param {boolean} danglingAssocAllowed usually optimised to foreign key hence allowed even if target is skipped,
128
+ * except in from or after exists
123
129
  */
124
- function _checkRef( ref, _links, $path, inColumns ) {
130
+ function _checkRef( ref, _links, $path, danglingAssocAllowed ) {
125
131
  if (!ref || !_links )
126
132
  return;
127
133
 
@@ -129,7 +135,7 @@ function _checkRef( ref, _links, $path, inColumns ) {
129
135
  const isPublishedAssoc = this.csnUtils.isAssocOrComposition(_links[_links.length - 1].art);
130
136
 
131
137
  // Don't check the last element - to allow association publishing in columns
132
- for (let i = 0; i < (inColumns ? _links.length - 1 : _links.length); i++) {
138
+ for (let i = 0; i < (danglingAssocAllowed ? _links.length - 1 : _links.length); i++) {
133
139
  const link = _links[i];
134
140
  if (!link)
135
141
  continue;
@@ -153,8 +159,8 @@ function _checkRef( ref, _links, $path, inColumns ) {
153
159
  if (nonPersistedTarget) {
154
160
  let isJoinRelevant = isPublishedAssoc || // publishing associations is always join relevant
155
161
  isLast || // e.g. FROM targets are always join relevant.
156
- isUnmanagedOrNoKeys; // unmanaged associations are always join relevant -> no FKs
157
-
162
+ isUnmanagedOrNoKeys || // unmanaged associations are always join relevant -> no FKs
163
+ ref.slice(i).some(s => s.where || s.args); // function calls or filters are always join relevant
158
164
  if (!isJoinRelevant) {
159
165
  // for managed, published associations with more than one $path-step, only FK
160
166
  // access is allowed.
@@ -164,17 +170,12 @@ function _checkRef( ref, _links, $path, inColumns ) {
164
170
  }
165
171
 
166
172
  if (isJoinRelevant) {
167
- const cdsPersistenceSkipped = hasPersistenceSkipAnnotation(targetArt);
168
- this.error( null, $path, {
169
- '#': cdsPersistenceSkipped ? 'std' : 'abstract',
173
+ this.error('ref-invalid-assoc-navigation', $path, {
170
174
  anno: '@cds.persistence.skip',
171
175
  id: nonPersistedTarget.pathStep,
172
176
  elemref: { ref },
173
177
  name: nonPersistedTarget.name,
174
- }, {
175
- std: 'Unexpected $(ANNO) annotation on association target $(NAME) of $(ID) in path $(ELEMREF)',
176
- abstract: 'Unexpected abstract association target $(NAME) of $(ID) in path $(ELEMREF)',
177
- } );
178
+ });
178
179
  break; // only one error per path
179
180
  }
180
181
  }
@@ -152,7 +152,7 @@ function checkTypeOfHasProperType( artOrElement, name, model, error, path, deriv
152
152
 
153
153
 
154
154
  /**
155
- * Can happen in CSN, e.g. `{ a: { kind: "type" } }` but should not happen in CDL.
155
+ * Can happen in CSN, e.g. `{ a: { kind: "type" } }` or via `elem;` in CDL.
156
156
  *
157
157
  * @param {Function} error the error function
158
158
  * @param {CSN.Path} path the path to the element or the artifact
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { isBuiltinType } = require('../base/builtins');
4
- const { RelationalOperators } = require('../transform/transformUtils');
4
+ const { RelationalOperators } = require('../transform/tupleExpansion');
5
5
  /**
6
6
  * Prepare the ref steps so that they are loggable
7
7
  *
@@ -17,7 +17,9 @@ function logReady( refStep ) {
17
17
  * structured that can be used for tuple expansion. This can either be a
18
18
  * real 'elements' thing or a managed association/composition with foreign keys.
19
19
  *
20
- * The RHS may be 'null' or any value
20
+ * @TODO: This function also allows `is null` on the right-hand-side.
21
+ * We should move these checks to the actual tuple expansion, because
22
+ * if we're missing cases here, it currently results in incorrect expansion.
21
23
  *
22
24
  * @param {Array} on the on condition which to check
23
25
  * @param {number} startIndex the index of the relational term in the on condition array
@@ -26,8 +28,7 @@ function logReady( refStep ) {
26
28
  function otherSideIsExpandableStructure( on, startIndex ) {
27
29
  if (on[startIndex - 1] && RelationalOperators.includes(on[startIndex - 1])) {
28
30
  const lhs = on[startIndex - 2];
29
- // if ever lhs is allowed to be a value uncomment this
30
- return /* lhs?.val !== undefined || */ isOk(resolveArtifactType.call(this, lhs?._art));
31
+ return isOk(resolveArtifactType.call(this, lhs?._art));
31
32
  }
32
33
  else if (on[startIndex + 1] && RelationalOperators.includes(on[startIndex + 1])) {
33
34
  const op = on[startIndex + 1];
@@ -35,8 +36,7 @@ function otherSideIsExpandableStructure( on, startIndex ) {
35
36
  if (op === 'is')
36
37
  // check for unary operator 'is [not] null' as token stream
37
38
  return rhs === 'null' || (rhs === 'not' && on[startIndex + 3] === 'null');
38
- // if ever rhs is allowed to be a value uncomment this
39
- return /* rhs?.val !== undefined || */ isOk(resolveArtifactType.call(this, rhs?._art));
39
+ return isOk(resolveArtifactType.call(this, rhs?._art));
40
40
  }
41
41
  return false;
42
42
 
@@ -51,6 +51,29 @@ function otherSideIsExpandableStructure( on, startIndex ) {
51
51
  }
52
52
  }
53
53
 
54
+ /**
55
+ * Check that the opposite operand to a relational term is s value or "is null".
56
+ *
57
+ * @param {Array} expr the expression which to check
58
+ * @param {number} startIndex the index of the relational term in the expression array
59
+ * @returns {boolean} indicates whether the other side of a relational term is scalar
60
+ */
61
+ function otherSideIsValue( expr, startIndex ) {
62
+ if (expr[startIndex - 1] && RelationalOperators.includes(expr[startIndex - 1]))
63
+ return expr[startIndex - 2]?.val !== undefined;
64
+
65
+ if (expr[startIndex + 1] && RelationalOperators.includes(expr[startIndex + 1])) {
66
+ const op = expr[startIndex + 1];
67
+ const rhs = expr[startIndex + 2];
68
+ if (op === 'is')
69
+ // check for unary operator 'is [not] null' as token stream
70
+ return rhs === 'null' || (rhs === 'not' && expr[startIndex + 3] === 'null');
71
+ return rhs?.val !== undefined;
72
+ }
73
+
74
+ return false;
75
+ }
76
+
54
77
  /**
55
78
  * Get the real type of an artifact
56
79
  *
@@ -68,5 +91,6 @@ function resolveArtifactType( art ) {
68
91
  module.exports = {
69
92
  logReady,
70
93
  otherSideIsExpandableStructure,
94
+ otherSideIsValue,
71
95
  resolveArtifactType,
72
96
  };
@@ -42,7 +42,6 @@ const checkForInvalidTarget = require('./invalidTarget');
42
42
  const { validateAssociationsInItems } = require('./arrayOfs');
43
43
  const checkQueryForNoDBArtifacts = require('./queryNoDbArtifacts');
44
44
  const checkExplicitlyNullableKeys = require('./nullableKeys');
45
- const nonexpandableStructuredInExpression = require('./nonexpandableStructured');
46
45
  const existsMustEndInAssoc = require('./existsMustEndInAssoc');
47
46
  const forbidAssocInExists = require('./existsExpressionsOnlyForeignKeys');
48
47
  const checkPathsInStoredCalcElement = require('./checkPathsInStoredCalcElement');
@@ -87,7 +86,6 @@ const forRelationalDBCsnValidators = [
87
86
  checkCdsMap,
88
87
  existsMustEndInAssoc,
89
88
  forbidAssocInExists,
90
- nonexpandableStructuredInExpression,
91
89
  navigationIntoMany,
92
90
  checkPathsInStoredCalcElement,
93
91
  featureFlags,
@@ -124,7 +122,7 @@ const forOdataArtifactValidators
124
122
  checkReadOnlyAndInsertOnly,
125
123
  ];
126
124
 
127
- const forOdataCsnValidators = [ checkCdsMap, nonexpandableStructuredInExpression ];
125
+ const forOdataCsnValidators = [ checkCdsMap ];
128
126
 
129
127
  const forOdataQueryValidators = [];
130
128
 
@@ -202,8 +200,6 @@ function getDBCsnValidators( options ) {
202
200
  validations.push(checkForParams.csnValidator);
203
201
  if (options.sqlDialect === 'h2' || options.sqlDialect === 'postgres')
204
202
  validations.push(checkForHanaTypes);
205
- if (options.transformation === 'effective' && options.effectiveServiceName)
206
- validations.push(assertNoAssocUsageOutsideOfService);
207
203
 
208
204
  return validations;
209
205
  }
@@ -232,6 +228,9 @@ function forRelationalDB( csn, that ) {
232
228
  },
233
229
  (artifact, artifactName) => {
234
230
  if (that.options.transformation === 'effective') {
231
+ if (that.options.effectiveServiceName)
232
+ assertNoAssocUsageOutsideOfService.bind(that)(artifact, artifactName);
233
+
235
234
  forEachMemberRecursively(artifact, checkAnnotationExpression.bind(that), [ 'definitions', artifactName ], false, {
236
235
  skipArtifact: a => a.returns || (a.params && !a.query),
237
236
  });