@sap/cds-compiler 4.1.2 → 4.2.4

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 (74) hide show
  1. package/CHANGELOG.md +107 -1
  2. package/bin/cdsc.js +6 -3
  3. package/doc/CHANGELOG_BETA.md +5 -0
  4. package/doc/CHANGELOG_DEPRECATED.md +15 -0
  5. package/lib/api/main.js +2 -2
  6. package/lib/api/options.js +2 -2
  7. package/lib/api/validate.js +24 -24
  8. package/lib/base/message-registry.js +41 -6
  9. package/lib/base/messages.js +7 -0
  10. package/lib/base/model.js +38 -8
  11. package/lib/checks/elements.js +11 -10
  12. package/lib/checks/manyNavigations.js +33 -0
  13. package/lib/checks/onConditions.js +5 -2
  14. package/lib/checks/queryNoDbArtifacts.js +2 -3
  15. package/lib/checks/selectItems.js +4 -55
  16. package/lib/checks/utils.js +3 -2
  17. package/lib/checks/validator.js +3 -1
  18. package/lib/compiler/.eslintrc.json +2 -1
  19. package/lib/compiler/assert-consistency.js +27 -24
  20. package/lib/compiler/base.js +6 -2
  21. package/lib/compiler/builtins.js +34 -34
  22. package/lib/compiler/checks.js +179 -208
  23. package/lib/compiler/classes.js +2 -2
  24. package/lib/compiler/cycle-detector.js +6 -6
  25. package/lib/compiler/define.js +66 -45
  26. package/lib/compiler/extend.js +81 -72
  27. package/lib/compiler/finalize-parse-cdl.js +26 -26
  28. package/lib/compiler/generate.js +61 -45
  29. package/lib/compiler/index.js +47 -49
  30. package/lib/compiler/kick-start.js +8 -7
  31. package/lib/compiler/moduleLayers.js +1 -1
  32. package/lib/compiler/populate.js +42 -35
  33. package/lib/compiler/propagator.js +6 -6
  34. package/lib/compiler/resolve.js +170 -126
  35. package/lib/compiler/shared.js +122 -45
  36. package/lib/compiler/tweak-assocs.js +93 -40
  37. package/lib/compiler/utils.js +15 -12
  38. package/lib/edm/.eslintrc.json +40 -1
  39. package/lib/edm/annotations/genericTranslation.js +721 -707
  40. package/lib/edm/annotations/preprocessAnnotations.js +88 -77
  41. package/lib/edm/csn2edm.js +389 -378
  42. package/lib/edm/edm.js +678 -772
  43. package/lib/edm/edmAnnoPreprocessor.js +132 -146
  44. package/lib/edm/edmInboundChecks.js +29 -27
  45. package/lib/edm/edmPreprocessor.js +686 -646
  46. package/lib/edm/edmUtils.js +277 -296
  47. package/lib/gen/language.checksum +1 -1
  48. package/lib/gen/language.interp +1 -1
  49. package/lib/gen/languageParser.js +1253 -1276
  50. package/lib/json/from-csn.js +34 -4
  51. package/lib/json/to-csn.js +4 -4
  52. package/lib/language/language.g4 +2 -5
  53. package/lib/main.d.ts +61 -1
  54. package/lib/model/csnUtils.js +31 -2
  55. package/lib/model/revealInternalProperties.js +1 -1
  56. package/lib/modelCompare/compare.js +37 -2
  57. package/lib/modelCompare/utils/filter.js +1 -1
  58. package/lib/optionProcessor.js +15 -3
  59. package/lib/render/toCdl.js +30 -4
  60. package/lib/render/toSql.js +5 -9
  61. package/lib/render/utils/common.js +8 -6
  62. package/lib/transform/db/applyTransformations.js +1 -1
  63. package/lib/transform/db/cdsPersistence.js +1 -1
  64. package/lib/transform/db/constraints.js +47 -17
  65. package/lib/transform/db/expansion.js +133 -50
  66. package/lib/transform/db/flattening.js +75 -7
  67. package/lib/transform/forOdata.js +4 -1
  68. package/lib/transform/forRelationalDB.js +80 -62
  69. package/lib/transform/localized.js +91 -54
  70. package/lib/transform/transformUtils.js +9 -10
  71. package/lib/utils/file.js +7 -7
  72. package/lib/utils/moduleResolve.js +210 -121
  73. package/lib/utils/objectUtils.js +1 -1
  74. package/package.json +5 -5
@@ -56,6 +56,25 @@ function createReferentialConstraints( csn, options ) {
56
56
  },
57
57
  });
58
58
  }
59
+ // for `texts` compositions, we may generate foreign key constraints even w/o `up_`
60
+ else if (elementName === 'texts' && element.target === `${path[path.length - 1]}.texts`) {
61
+ const { on } = element;
62
+ const target = csn.definitions[element.target];
63
+ // `texts` entities have a key named "locale"
64
+ const targetSideHasLocaleKey = target.elements.locale?.key;
65
+ if (targetSideHasLocaleKey && !skipConstraintGeneration(parent, target, { /* there is no assoc */ })) {
66
+ const sourceElements = Array.from(elementsOfSourceSide(on, elements));
67
+ const targetElements = Array.from(elementsOfTargetSide(on, target.elements));
68
+ // `texts` entities have all the keys the original entity has
69
+ const allElementsAreKeysAndHaveTheSameName = targetElements.length &&
70
+ targetElements.every(
71
+ ([ targetKey, e ]) => e.key &&
72
+ sourceElements.some(([ sourceKey, sourceElement ]) => sourceElement.key && targetKey === sourceKey )
73
+ );
74
+ if (allElementsAreKeysAndHaveTheSameName)
75
+ attachConstraintsToDependentKeys(targetElements, sourceElements, path[path.length - 1], 'texts', { texts: true });
76
+ }
77
+ }
59
78
  }
60
79
  },
61
80
  }, [], { skipIgnore: false, skipArtifact: a => a.query || a.kind !== 'entity' });
@@ -134,7 +153,7 @@ function createReferentialConstraints( csn, options ) {
134
153
  * @param {Array} parentKeys array holding parent keys in the format [['key1', 'value1'], [...], ...]
135
154
  * @param {CSN.PathSegment} parentTable the sql-table where the foreign key constraints will be pointing to
136
155
  * @param {CSN.PathSegment} sourceAssociation the name of the association from which the constraint originates
137
- * @param {CSN.PathSegment} upLinkFor the name of the composition which used this association in a `$self = <comp>.<up_>` comparison
156
+ * @param {CSN.PathSegment | object} upLinkFor the name of the composition which used this association in a `$self = <comp>.<up_>` comparison
138
157
  * it is used for a comment in the constraint, which is only printed out in test-mode
139
158
  */
140
159
  function attachConstraintsToDependentKeys( dependentKeys, parentKeys, parentTable, sourceAssociation, upLinkFor = null ) {
@@ -440,13 +459,16 @@ function createReferentialConstraints( csn, options ) {
440
459
  */
441
460
  function elementsOfTargetSide( on, targetElements ) {
442
461
  const elements = new Map();
443
- on.filter(element => typeof element === 'object' &&
444
- element.ref.length > 1 &&
445
- targetElements[element.ref[element.ref.length - 1]])
446
- .forEach((element) => {
447
- elements.set(element.ref[element.ref.length - 1], targetElements[element.ref[element.ref.length - 1]]);
448
- });
449
-
462
+ const findElements = (tokenStream) => {
463
+ tokenStream
464
+ .forEach((element) => {
465
+ if (typeof element === 'object' && element.ref?.length > 1 && targetElements[element.ref[element.ref.length - 1]])
466
+ elements.set(element.ref[element.ref.length - 1], targetElements[element.ref[element.ref.length - 1]]);
467
+ else if (element.xpr)
468
+ findElements(element.xpr);
469
+ });
470
+ };
471
+ findElements(on);
450
472
  return elements;
451
473
  }
452
474
 
@@ -459,12 +481,16 @@ function createReferentialConstraints( csn, options ) {
459
481
  */
460
482
  function elementsOfSourceSide( on, sourceElements ) {
461
483
  const elements = new Map();
462
- on.filter(element => typeof element === 'object' &&
463
- element.ref.length === 1 &&
464
- sourceElements[element.ref[0]])
465
- .forEach((element) => {
466
- elements.set(element.ref[0], sourceElements[element.ref[0]]);
467
- });
484
+ const findElements = (tokenStream) => {
485
+ tokenStream
486
+ .forEach((element) => {
487
+ if (typeof element === 'object' && element.ref?.length === 1 && sourceElements[element.ref[0]])
488
+ elements.set(element.ref[0], sourceElements[element.ref[0]]);
489
+ else if (element.xpr)
490
+ findElements(element.xpr);
491
+ });
492
+ };
493
+ findElements(on);
468
494
  return elements;
469
495
  }
470
496
 
@@ -507,12 +533,16 @@ function createReferentialConstraints( csn, options ) {
507
533
  dependentKey.push(foreignKeyName);
508
534
  onDeleteRules.add($foreignKeyConstraintCopy.onDelete);
509
535
  });
510
- // onDelete Rule is the "weakest" rule applicable. Precedence: RESTRICT > SET NULL > CASCADE
536
+ // onDelete Rule is the "weakest" rule applicable. Precedence: RESTRICT > CASCADE
511
537
  const onDelete = onDeleteRules.has('RESTRICT') ? 'RESTRICT' : 'CASCADE';
512
538
  let onDeleteRemark = null;
513
539
  // comments in sqlite files are causing the JDBC driver to throw an error on deployment
514
- if (options.testMode && onDelete === 'CASCADE')
515
- onDeleteRemark = `Up_ link for Composition "${$foreignKeyConstraint.upLinkFor}" implies existential dependency`;
540
+ if (options.testMode && onDelete === 'CASCADE') {
541
+ if ($foreignKeyConstraint.upLinkFor?.texts)
542
+ onDeleteRemark = `Constraint originates from localized composition ”${$foreignKeyConstraint.parentTable}:texts“`;
543
+ else
544
+ onDeleteRemark = `Up_ link for Composition "${$foreignKeyConstraint.upLinkFor}" implies existential dependency`;
545
+ }
516
546
  // constraint identifier usually start with `c__` to avoid name clashes
517
547
  let identifier = options.pre2134ReferentialConstraintNames ? '' : 'c__';
518
548
  identifier += `${getResultingName(csn, options.sqlMapping, artifactName)}_${$foreignKeyConstraint.sourceAssociation}`;
@@ -5,8 +5,9 @@ const {
5
5
  applyTransformations,
6
6
  setDependencies,
7
7
  walkCsnPath,
8
+ getUtils,
8
9
  } = require('../../model/csnUtils');
9
- const { csnRefs, implicitAs } = require('../../model/csnRefs');
10
+ const { implicitAs, columnAlias, pathId } = require('../../model/csnRefs');
10
11
  const { setProp } = require('../../base/model');
11
12
  const { forEach } = require('../../utils/objectUtils');
12
13
 
@@ -18,35 +19,30 @@ const { forEach } = require('../../utils/objectUtils');
18
19
  * @param {CSN.Options} options
19
20
  * @param {string} pathDelimiter
20
21
  * @param {object} messageFunctions
21
- * @param {Function} messageFunctions.error
22
- * @param {Function} messageFunctions.info
23
- * @param {Function} messageFunctions.throwWithAnyError
24
22
  * @param {object} csnUtils
25
23
  * @param {object} [iterateOptions]
26
24
  */
27
- function expandStructureReferences( csn, options, pathDelimiter, { error, info, throwWithAnyError }, csnUtils, iterateOptions = {} ) {
28
- const {
29
- isStructured, get$combined, getFinalTypeInfo,
30
- } = csnUtils;
31
- let { effectiveType, inspectRef } = csnUtils;
25
+ function expandStructureReferences( csn, options, pathDelimiter, messageFunctions, csnUtils, iterateOptions = {} ) {
26
+ const { error, info, throwWithAnyError } = messageFunctions;
32
27
 
33
28
  rewriteExpandInline();
34
29
 
35
-
36
30
  applyTransformations(csn, {
37
31
  keys: (parent, name, keys, path) => {
38
32
  parent.keys = expand(keys, path.concat('keys'), true);
39
33
  },
40
34
  columns: (parent, name, columns, path) => {
41
35
  const artifact = csn.definitions[path[1]];
36
+ csnUtils.initDefinition(artifact); // potentially no initialized, yet
42
37
  if (!hasAnnotationValue(artifact, '@cds.persistence.table')) {
43
- const root = get$combined({ SELECT: parent });
38
+ const root = csnUtils.get$combined({ SELECT: parent });
44
39
  // TODO: replace with the correct options.transformation?
45
40
  // Do not expand the * in OData for a moment, not to introduce changes
46
41
  // while the OData CSN is still official
42
+ const isComplexQuery = parent.from.join !== undefined;
47
43
  if (!options.toOdata)
48
- parent.columns = replaceStar(root, columns, parent.excluding);
49
- parent.columns = expand(parent.columns, path.concat('columns'), true);
44
+ parent.columns = replaceStar(root, columns, parent.excluding, isComplexQuery);
45
+ parent.columns = expand(parent.columns, path.concat('columns'), true, isComplexQuery);
50
46
  }
51
47
  },
52
48
  groupBy: (parent, name, groupBy, path) => {
@@ -74,7 +70,7 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
74
70
  // get$combined expects a SET/SELECT - so we wrap the parent
75
71
  // (which is the thing inside SET/SELECT)
76
72
  // We can directly use SELECT here, as only projections and SELECT can have .columns
77
- const root = get$combined({ SELECT: parent });
73
+ const root = csnUtils.get$combined({ SELECT: parent });
78
74
  if (!hasAnnotationValue(artifact, '@cds.persistence.table')) {
79
75
  // Make root look like normal .elements - we never cared about conflict afaik anyway
80
76
  Object.keys(root).forEach((key) => {
@@ -102,7 +98,7 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
102
98
 
103
99
  cleanup.forEach(fn => fn());
104
100
 
105
- ({ effectiveType, inspectRef } = csnRefs(csn));
101
+ csnUtils = getUtils(csn);
106
102
 
107
103
  const publishing = [];
108
104
  // OData must allow navigations to @cds.persistence.skip targets
@@ -144,7 +140,7 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
144
140
  if (!(obj && obj.ref) || obj.$scope === 'alias')
145
141
  continue;
146
142
 
147
- const links = obj._links || inspectRef(path.concat([ name, i ])).links;
143
+ const links = obj._links || csnUtils.inspectRef(path.concat([ name, i ])).links;
148
144
 
149
145
  if (!links)
150
146
  continue;
@@ -244,7 +240,7 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
244
240
  */
245
241
  function nextBase( parent, base ) {
246
242
  if (parent.ref) {
247
- const finalBaseType = getFinalTypeInfo(parent._art.type);
243
+ const finalBaseType = csnUtils.getFinalTypeInfo(parent._art.type);
248
244
  const art = parent._art;
249
245
 
250
246
  if (finalBaseType && (finalBaseType.type === 'cds.Association' || finalBaseType.type === 'cds.Composition'))
@@ -297,7 +293,7 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
297
293
  function isToMany( obj ) {
298
294
  if (!obj._art)
299
295
  return false;
300
- const eType = effectiveType(obj._art);
296
+ const eType = csnUtils.effectiveType(obj._art);
301
297
  return (eType.type === 'cds.Association' || eType.type === 'cds.Composition') && eType.cardinality && eType.cardinality.max !== 1;
302
298
  }
303
299
 
@@ -516,18 +512,18 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
516
512
  *
517
513
  * @param {Array} thing
518
514
  * @param {CSN.Path} path
519
- * @param {boolean} [withAlias=false] Whether to "expand" the (implicit) alias aswell.
515
+ * @param {boolean} [withAlias=false] Whether to "expand" the (implicit) alias as well.
516
+ * @param {boolean} [isComplexQuery]
520
517
  * @returns {Array} New array - with all structured things expanded
521
518
  */
522
- function expand( thing, path, withAlias = false ) {
519
+ function expand( thing, path, withAlias = false, isComplexQuery = false ) {
523
520
  const newThing = [];
524
521
  for (let i = 0; i < thing.length; i++) {
525
522
  const col = thing[i];
526
523
  if (col.ref && col.$scope !== '$magic') {
527
- const _art = col._art || inspectRef(path.concat(i)).art;
528
- if (_art && isStructured(_art))
529
- newThing.push(...expandRef(_art, col, withAlias));
530
-
524
+ const _art = col._art || csnUtils.inspectRef(path.concat(i)).art;
525
+ if (_art && csnUtils.isStructured(_art))
526
+ newThing.push(...expandRef(_art, col, withAlias, isComplexQuery));
531
527
  else
532
528
  newThing.push(col);
533
529
  }
@@ -535,6 +531,23 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
535
531
  col.as = implicitAs(col.ref);
536
532
  newThing.push(col);
537
533
  }
534
+ else if (col.cast?.type) {
535
+ const _art = col.cast._type || csnUtils.inspectRef(path.concat(i, 'cast', 'type')).art;
536
+ if (_art && csnUtils.isStructured(_art)) {
537
+ // special case for `null as name : Struct`
538
+ if (col.val === null) {
539
+ newThing.push(...expandValAsStructure(_art, col, withAlias));
540
+ }
541
+ else {
542
+ error('type-invalid-cast', path.concat(i, 'cast', 'type'), {
543
+ '#': col.val !== undefined ? 'val-to-structure' : 'expr-to-structure', value: col.val,
544
+ });
545
+ }
546
+ }
547
+ else {
548
+ newThing.push(col);
549
+ }
550
+ }
538
551
  else {
539
552
  newThing.push(col);
540
553
  }
@@ -544,44 +557,109 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
544
557
  }
545
558
 
546
559
  /**
547
- * Expand the ref and - if requested - expand the alias with it.
548
- *
549
- * Iterative, to not run into stack overflow.
560
+ * Expands a column, and calls leafCallback() when a leaf node is reached.
550
561
  *
551
562
  * @param {CSN.Element} art
552
- * @param {object} root Column, ref in order by, etc.
553
- * @param {boolean} withAlias
554
- * @returns {Array}
563
+ * Structured Artifact which is used for expansion (and names, etc.). For a ref, it's the
564
+ * underlying type or a cast-type, for a value, it's always the cast-type.
565
+ * @param {string} colName
566
+ * Name of the column, that is used as the first name segment, e.g. a column `a` may end up in
567
+ * leafs `a_b` and `a_c`, if `art` has elements `b` and `c`.
568
+ * @param {string[]} colTypeRef
569
+ * Expanded type for the column. Basically the path to the to-be-expanded `art`.
570
+ * @param {(currentRef: any[], currentAlias: string[]) => object} leafCallback
571
+ * Callback when leaf nodes are reached. currentRef is the type reference for the expanded
572
+ * column. currentAlias is the columns calculated alias.
573
+ * @returns {object[]}
555
574
  */
556
- function expandRef( art, root, withAlias ) {
575
+ function _expandStructCol( art, colName, colTypeRef, leafCallback ) {
557
576
  const expanded = [];
558
- /** @type {Array<[CSN.Element, any[], any[]]>} */
559
- const stack = [ [ art, root.ref, [ root.as || implicitAs(root.ref) ] ] ];
577
+ /** @type {Array<[CSN.Element, any[], string[]]>} */
578
+ const stack = [ [ art, colTypeRef, [ colName ] ] ];
560
579
  while (stack.length > 0) {
561
580
  const [ current, currentRef, currentAlias ] = stack.pop();
562
- if (isStructured(current)) {
563
- for (const [ n, e ] of Object.entries(current.elements || effectiveType(current).elements).reverse())
564
- stack.push([ e, currentRef.concat(n), currentAlias.concat(n) ]);
581
+ if (csnUtils.isStructured(current)) {
582
+ const elements = Object.entries(current.elements || csnUtils.effectiveType(current).elements).reverse();
583
+ for (const [ name, elem ] of elements)
584
+ stack.push([ elem, currentRef.concat(name), currentAlias.concat(name) ]);
565
585
  }
566
586
  else {
567
- const obj = { ...root, ...{ ref: currentRef } };
568
- if (withAlias) {
569
- const newAlias = currentAlias.join(pathDelimiter);
570
- // if (alias !== undefined) // explicit alias
571
- obj.as = newAlias;
572
- // alias was implicit - to later distinguish expanded s -> s.a from explicitly written s.a
573
- if (root.as === undefined)
574
- setProp(obj, '$implicitAlias', true);
575
- }
576
- if (root.key)
577
- obj.key = true;
578
- expanded.push(obj);
587
+ const newCol = leafCallback(currentRef, currentAlias);
588
+ expanded.push(newCol);
579
589
  }
580
590
  }
581
591
 
582
592
  return expanded;
583
593
  }
584
594
 
595
+ /**
596
+ * Expand the ref and - if requested - expand/set the alias with it.
597
+ *
598
+ * @param {CSN.Element} art
599
+ * @param {object} root Column, ref in order by, etc.
600
+ * @param {boolean} withAlias Whether to add an explicit flattened alias to the expanded columns/references.
601
+ * @param {boolean} [isComplexQuery]
602
+ * @returns {Array}
603
+ */
604
+ function expandRef( art, root, withAlias, isComplexQuery ) {
605
+ return _expandStructCol(art, columnAlias(root), root.ref, ( currentRef, currentAlias) => {
606
+ const obj = { ...root, ref: currentRef };
607
+ if (withAlias) {
608
+ // TODO: Remove this line in case foreign key annotations should
609
+ // be adressed via full path into target instead of using alias
610
+ // names. See flattening.js::flattenAllStructStepsInRefs()
611
+ // apply transformations on `ref` counterpart comment.
612
+ setProp(obj, '$structRef', currentAlias);
613
+ obj.as = currentAlias.join(pathDelimiter);
614
+ // alias was implicit - to later distinguish expanded s -> s.a from explicitly written s.a
615
+ if (root.as === undefined)
616
+ setProp(obj, '$implicitAlias', true);
617
+ }
618
+
619
+ // The Java runtime, as of 2023-09-13, assumes that for _simple projections_, all references
620
+ // are relative to the query source. To avoid breaking that assumption unless necessary,
621
+ // we only add the table alias if:
622
+ // - it is a complex query with possibly multiple available table aliases, or
623
+ // - the transformation is not for OData (which is used by Java), or
624
+ // - the first path step has the same name as the table alias (only one, as otherwise the query would be complex)
625
+ if (typeof root.$env === 'string' && (isComplexQuery || options.transformation !== 'odata' || root.$env === pathId(obj.ref[0])))
626
+ obj.ref = [ root.$env, ...obj.ref ];
627
+
628
+ return obj;
629
+ });
630
+ }
631
+
632
+ /**
633
+ * Expand `null` columns which were cast to a structure, that is: `null as name : Struct`.
634
+ * Requires that `col` has an alias.
635
+ *
636
+ * @param {CSN.Element} art
637
+ * @param {object} col
638
+ * @param {boolean} withAlias Whether to add an explicit flattened alias to the expanded columns/references.
639
+ * @returns {Array}
640
+ */
641
+ function expandValAsStructure( art, col, withAlias ) {
642
+ const colName = col.as || '';
643
+ // Expression-columns may have an internal name such as `$_column_N`. If the name is internal,
644
+ // we should not publish names based upon the internal name.
645
+ const isInternal = !col.as || !Object.prototype.propertyIsEnumerable.call(col, 'as');
646
+
647
+ return _expandStructCol(art, colName, col.cast.type?.ref || [ col.cast.type ], ( currentRef, currentAlias) => {
648
+ const newCol = {
649
+ ...col,
650
+ val: col.val,
651
+ cast: { type: { ref: currentRef } },
652
+ };
653
+ if (withAlias) {
654
+ if (!isInternal)
655
+ newCol.as = currentAlias.join(pathDelimiter);
656
+ else
657
+ setProp(newCol, 'as', currentAlias.join(pathDelimiter));
658
+ }
659
+ return newCol;
660
+ });
661
+ }
662
+
585
663
  /**
586
664
  * Get the effective name produced by the object
587
665
  *
@@ -604,9 +682,10 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
604
682
  * @param {object} base The raw set of things a * can expand to
605
683
  * @param {Array} subs Things - the .expand/.inline or .columns
606
684
  * @param {string[]} [excluding=[]]
685
+ * @param {boolean} [isComplexQuery=false] Wether the query is a single source select or something more complex
607
686
  * @returns {Array} If there was a star, expand it and handle shadowing/excluding, else just return subs
608
687
  */
609
- function replaceStar( base, subs, excluding = [] ) {
688
+ function replaceStar( base, subs, excluding = [], isComplexQuery = false ) {
610
689
  const stars = [];
611
690
  const names = Object.create(null);
612
691
  for (let i = 0; i < subs.length; i++) {
@@ -638,7 +717,11 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
638
717
  }
639
718
  }
640
719
  else { // the thing is not shadowed - use the name from the base
641
- star.push({ ref: [ part ] });
720
+ const col = { ref: [ part ] };
721
+ if (isComplexQuery) // $env: tableAlias
722
+ setProp(col, '$env', base[part][0].parent);
723
+
724
+ star.push(col);
642
725
  }
643
726
  }
644
727
  }
@@ -7,7 +7,7 @@ const {
7
7
  } = require('../../model/csnUtils');
8
8
  const transformUtils = require('../transformUtils');
9
9
  const { csnRefs } = require('../../model/csnRefs');
10
- const { setProp } = require('../../base/model');
10
+ const { setProp, isBetaEnabled } = require('../../base/model');
11
11
  const { forEach } = require('../../utils/objectUtils');
12
12
  const { cardinality2str } = require('../../model/csnUtils');
13
13
 
@@ -208,7 +208,10 @@ function flattenAllStructStepsInRefs( csn, options, resolved, pathDelimiter, ite
208
208
  const lastRef = ref[ref.length - 1];
209
209
  const fn = () => {
210
210
  const scopedPath = [ ...parent.$path ];
211
-
211
+ // TODO: If foreign key annotations should be assigned via
212
+ // full path into target, uncomment this line and
213
+ // comment/remove setProp in expansion.js
214
+ // setProp(parent, '$structRef', parent.ref);
212
215
  parent.ref = flattenStructStepsInRef(ref, scopedPath, links, scope, resolvedLinkTypes);
213
216
  resolved.set(parent, { links, art, scope });
214
217
  // Explicitly set implicit alias for things that are now flattened - but only in columns
@@ -219,7 +222,9 @@ function flattenAllStructStepsInRefs( csn, options, resolved, pathDelimiter, ite
219
222
  delete parent.$implicitAlias;
220
223
  }
221
224
  // To handle explicitly written s.a - add implicit alias a, since after flattening it would otherwise be s_a
222
- else if (parent.ref[parent.ref.length - 1] !== lastRef && (insideColumns(scopedPath) || insideKeys(scopedPath)) && !parent.as) {
225
+ else if (parent.ref[parent.ref.length - 1] !== lastRef &&
226
+ (insideColumns(scopedPath) || insideKeys(scopedPath)) &&
227
+ !parent.as) {
223
228
  parent.as = lastRef;
224
229
  }
225
230
  };
@@ -396,6 +401,36 @@ function getBranches( element, elementName, effectiveType, pathDelimiter ) {
396
401
  return branches;
397
402
  }
398
403
 
404
+ /**
405
+ * Link annotate extensions to managed associations as a preparational step
406
+ * for later annotation assignment on the final foreignkeys
407
+ * This function must be applied on an unmodified, structured CSN in order to
408
+ * traverse both the extensions and dictionary trees in corresponding order.
409
+ *
410
+ * @param {CSN.Model} csn
411
+ * @param {object} options
412
+ */
413
+ function linkForeignKeyAnnotationExtensionsToAssociation( csn, options ) {
414
+ if (isBetaEnabled(options, 'annotateForeignKeys')) {
415
+ csn.extensions?.forEach(( ext ) => {
416
+ const defName = ext.annotate;
417
+
418
+ const traverseExtensions = (env, enode) => {
419
+ if (env?.target && env?.keys) {
420
+ setProp(env, '$fkExtensions', enode);
421
+ }
422
+ else {
423
+ const elements = env?.items?.elements || env?.elements;
424
+ if (enode?.elements && elements)
425
+ Object.keys(enode.elements).forEach(en => traverseExtensions(elements[en], enode.elements[en]));
426
+ }
427
+ };
428
+ if (ext.annotate)
429
+ traverseExtensions(csn.definitions[defName], ext);
430
+ });
431
+ }
432
+ }
433
+
399
434
  /**
400
435
  * @param {CSN.Model} csn
401
436
  * @param {CSN.Options} options
@@ -530,7 +565,7 @@ function handleManagedAssociationsAndCreateForeignKeys( csn, options, error, war
530
565
  * @returns {object} The clone of base
531
566
  */
532
567
  function cloneAndExtendRef( key, base, ref ) {
533
- const clone = cloneCsnNonDict(base, options);
568
+ const clone = cloneCsnNonDict(base, { ...options, hiddenPropertiesToClone: [ '$structRef', '$fkExtensions' ] } );
534
569
  if (key.ref) {
535
570
  // We build a ref that contains the aliased fk - that element will be created later on, so this ref is not resolvable yet
536
571
  // Therefore we keep it as $ref - ref is the non-aliased, resolvable "clone"
@@ -546,6 +581,8 @@ function handleManagedAssociationsAndCreateForeignKeys( csn, options, error, war
546
581
  }
547
582
  setProp(clone, '$ref', $ref);
548
583
  clone.ref = clone.ref.concat(key.ref);
584
+ if (clone.$structRef && key.$structRef)
585
+ clone.$structRef = clone.$structRef.concat(key.$structRef);
549
586
  }
550
587
 
551
588
  if (!clone.as && clone.ref && clone.ref.length > 0) {
@@ -646,6 +683,34 @@ function handleManagedAssociationsAndCreateForeignKeys( csn, options, error, war
646
683
  }
647
684
  }
648
685
  }
686
+ // assign annotations from fkExtension tree to foreign keys
687
+ if (isBetaEnabled(options, 'annotateForeignKeys')) {
688
+ const extCollector = {};
689
+ fks.forEach(([ _fkn, fk ]) => {
690
+ let ext = element.$fkExtensions;
691
+ let extKey = elementName;
692
+ for (const step of fk.$extensionPath) {
693
+ extKey += `.${step}`;
694
+ ext = ext?.elements?.[step];
695
+ if (!ext)
696
+ break;
697
+ // collect annotations, lowest wins
698
+ // eslint-disable-next-line no-loop-func
699
+ Object.entries(ext).forEach(([ k, v ]) => {
700
+ if (k[0] === '@') {
701
+ fk[k] = v;
702
+ extCollector[extKey] = ext;
703
+ }
704
+ });
705
+ }
706
+ });
707
+
708
+ // remove consumed annotations after applying the annotation hierarchy to each fk!
709
+ Object.values(extCollector).forEach(ext => Object.keys(ext).forEach((k) => {
710
+ if (k[0] === '@')
711
+ delete ext[k];
712
+ }));
713
+ }
649
714
  orderedElements.push(...fks);
650
715
  });
651
716
 
@@ -669,10 +734,11 @@ function handleManagedAssociationsAndCreateForeignKeys( csn, options, error, war
669
734
  * @param {CSN.Model} csn
670
735
  * @param {object} options
671
736
  * @param {string} pathDelimiter
737
+ * @param {object} extensionPath
672
738
  * @param {number} lvl
673
739
  * @returns {Array[]} First element of every sub-array is the foreign key name, second is the foreign key definition
674
740
  */
675
- function createForeignKeys( csnUtils, path, element, prefix, csn, options, pathDelimiter, lvl = 0 ) {
741
+ function createForeignKeys( csnUtils, path, element, prefix, csn, options, pathDelimiter, extensionPath = [], lvl = 0 ) {
676
742
  const special$self = !csn?.definitions?.$self && '$self';
677
743
  const isInspectRefResult = !Array.isArray(path);
678
744
 
@@ -718,7 +784,7 @@ function createForeignKeys( csnUtils, path, element, prefix, csn, options, pathD
718
784
  const continuePath = getContinuePath([ 'keys', keyIndex ]);
719
785
  const alias = key.as || implicitAs(key.ref);
720
786
  const result = csnUtils.inspectRef(continuePath);
721
- fks = fks.concat(createForeignKeys(csnUtils, result, result.art, alias, csn, options, pathDelimiter, lvl + 1));
787
+ fks = fks.concat(createForeignKeys(csnUtils, result, result.art, alias, csn, options, pathDelimiter, extensionPath.concat(key.$structRef), lvl + 1));
722
788
  });
723
789
  if (!hasKeys)
724
790
  delete finalElement.keys;
@@ -732,13 +798,14 @@ function createForeignKeys( csnUtils, path, element, prefix, csn, options, pathD
732
798
  // Skip already produced foreign keys
733
799
  if (!elem['@odata.foreignKey4']) {
734
800
  const continuePath = getContinuePath([ 'elements', elemName ]);
735
- fks = fks.concat(createForeignKeys(csnUtils, continuePath, elem, elemName, csn, options, pathDelimiter, lvl + 1));
801
+ fks = fks.concat(createForeignKeys(csnUtils, continuePath, elem, elemName, csn, options, pathDelimiter, extensionPath.concat(elemName), lvl + 1));
736
802
  }
737
803
  });
738
804
  }
739
805
  // we have reached a leaf element, create a foreign key
740
806
  else if (finalElement.type == null || isBuiltinType(finalElement.type)) {
741
807
  const newFk = Object.create(null);
808
+ setProp(newFk, '$extensionPath', extensionPath);
742
809
  for (const prop of [ 'type', 'length', 'scale', 'precision', 'srid', 'default', '@odata.Type' ]) {
743
810
  // copy props from original element to preserve derived types!
744
811
  if (element[prop] !== undefined)
@@ -794,6 +861,7 @@ module.exports = {
794
861
  flattenAllStructStepsInRefs,
795
862
  flattenElements,
796
863
  removeLeadingSelf,
864
+ linkForeignKeyAnnotationExtensionsToAssociation,
797
865
  handleManagedAssociationsAndCreateForeignKeys,
798
866
  getBranches,
799
867
  };
@@ -152,6 +152,8 @@ function transform4odataWithCsn(inputModel, options) {
152
152
  { skipArtifact: isExternalServiceMember }
153
153
  );
154
154
 
155
+ flattening.linkForeignKeyAnnotationExtensionsToAssociation(csn, options);
156
+
155
157
  // All type refs must be resolved, including external APIs.
156
158
  // OData has no 'type of' so 'real' imported OData APIs marked @cds.external are safe.
157
159
  // If in the future 'other' APIs that might support type refs are imported, these refs must be
@@ -169,6 +171,7 @@ function transform4odataWithCsn(inputModel, options) {
169
171
  expansion.expandStructureReferences(csn, options, '_', { error, info, throwWithAnyError }, csnUtils, { skipArtifact: isExternalServiceMember });
170
172
  const resolved = new WeakMap();
171
173
  // No refs with struct-steps exist anymore
174
+
172
175
  flattening.flattenAllStructStepsInRefs(csn, options, resolved, '_', { skipArtifact: isExternalServiceMember });
173
176
  // No type references exist anymore
174
177
  // Needs to happen exactly between flattenAllStructStepsInRefs and flattenElements to keep model resolvable.
@@ -389,7 +392,7 @@ function transform4odataWithCsn(inputModel, options) {
389
392
 
390
393
  // Only on element level
391
394
  if(node.kind == null) {
392
- if (node['@mandatory']&& node['@Common.FieldControl'] === undefined) {
395
+ if (node['@mandatory'] && !Object.entries(node).some(([k,v]) => k === '@Common.FieldControl' || k.startsWith('@Common.FieldControl.') && v != null)) {
393
396
  setAnnotation(node, '@Common.FieldControl', { '#': 'Mandatory' });
394
397
  }
395
398
  if (node['@assert.range'] != null &&