@sap/cds-compiler 4.1.2 → 4.2.2

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 +101 -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 +37 -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 +121 -47
  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 } = require('../../model/csnRefs');
10
11
  const { setProp } = require('../../base/model');
11
12
  const { forEach } = require('../../utils/objectUtils');
12
13
 
@@ -18,34 +19,28 @@ 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
47
42
  if (!options.toOdata)
48
- parent.columns = replaceStar(root, columns, parent.excluding);
43
+ parent.columns = replaceStar(root, columns, parent.excluding, parent.from.join !== undefined);
49
44
  parent.columns = expand(parent.columns, path.concat('columns'), true);
50
45
  }
51
46
  },
@@ -74,7 +69,7 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
74
69
  // get$combined expects a SET/SELECT - so we wrap the parent
75
70
  // (which is the thing inside SET/SELECT)
76
71
  // We can directly use SELECT here, as only projections and SELECT can have .columns
77
- const root = get$combined({ SELECT: parent });
72
+ const root = csnUtils.get$combined({ SELECT: parent });
78
73
  if (!hasAnnotationValue(artifact, '@cds.persistence.table')) {
79
74
  // Make root look like normal .elements - we never cared about conflict afaik anyway
80
75
  Object.keys(root).forEach((key) => {
@@ -102,7 +97,7 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
102
97
 
103
98
  cleanup.forEach(fn => fn());
104
99
 
105
- ({ effectiveType, inspectRef } = csnRefs(csn));
100
+ csnUtils = getUtils(csn);
106
101
 
107
102
  const publishing = [];
108
103
  // OData must allow navigations to @cds.persistence.skip targets
@@ -144,7 +139,7 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
144
139
  if (!(obj && obj.ref) || obj.$scope === 'alias')
145
140
  continue;
146
141
 
147
- const links = obj._links || inspectRef(path.concat([ name, i ])).links;
142
+ const links = obj._links || csnUtils.inspectRef(path.concat([ name, i ])).links;
148
143
 
149
144
  if (!links)
150
145
  continue;
@@ -244,7 +239,7 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
244
239
  */
245
240
  function nextBase( parent, base ) {
246
241
  if (parent.ref) {
247
- const finalBaseType = getFinalTypeInfo(parent._art.type);
242
+ const finalBaseType = csnUtils.getFinalTypeInfo(parent._art.type);
248
243
  const art = parent._art;
249
244
 
250
245
  if (finalBaseType && (finalBaseType.type === 'cds.Association' || finalBaseType.type === 'cds.Composition'))
@@ -297,7 +292,7 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
297
292
  function isToMany( obj ) {
298
293
  if (!obj._art)
299
294
  return false;
300
- const eType = effectiveType(obj._art);
295
+ const eType = csnUtils.effectiveType(obj._art);
301
296
  return (eType.type === 'cds.Association' || eType.type === 'cds.Composition') && eType.cardinality && eType.cardinality.max !== 1;
302
297
  }
303
298
 
@@ -516,7 +511,7 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
516
511
  *
517
512
  * @param {Array} thing
518
513
  * @param {CSN.Path} path
519
- * @param {boolean} [withAlias=false] Whether to "expand" the (implicit) alias aswell.
514
+ * @param {boolean} [withAlias=false] Whether to "expand" the (implicit) alias as well.
520
515
  * @returns {Array} New array - with all structured things expanded
521
516
  */
522
517
  function expand( thing, path, withAlias = false ) {
@@ -524,10 +519,9 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
524
519
  for (let i = 0; i < thing.length; i++) {
525
520
  const col = thing[i];
526
521
  if (col.ref && col.$scope !== '$magic') {
527
- const _art = col._art || inspectRef(path.concat(i)).art;
528
- if (_art && isStructured(_art))
522
+ const _art = col._art || csnUtils.inspectRef(path.concat(i)).art;
523
+ if (_art && csnUtils.isStructured(_art))
529
524
  newThing.push(...expandRef(_art, col, withAlias));
530
-
531
525
  else
532
526
  newThing.push(col);
533
527
  }
@@ -535,6 +529,23 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
535
529
  col.as = implicitAs(col.ref);
536
530
  newThing.push(col);
537
531
  }
532
+ else if (col.cast?.type) {
533
+ const _art = col.cast._type || csnUtils.inspectRef(path.concat(i, 'cast', 'type')).art;
534
+ if (_art && csnUtils.isStructured(_art)) {
535
+ // special case for `null as name : Struct`
536
+ if (col.val === null) {
537
+ newThing.push(...expandValAsStructure(_art, col, withAlias));
538
+ }
539
+ else {
540
+ error('type-invalid-cast', path.concat(i, 'cast', 'type'), {
541
+ '#': col.val !== undefined ? 'val-to-structure' : 'expr-to-structure', value: col.val,
542
+ });
543
+ }
544
+ }
545
+ else {
546
+ newThing.push(col);
547
+ }
548
+ }
538
549
  else {
539
550
  newThing.push(col);
540
551
  }
@@ -544,44 +555,102 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
544
555
  }
545
556
 
546
557
  /**
547
- * Expand the ref and - if requested - expand the alias with it.
548
- *
549
- * Iterative, to not run into stack overflow.
558
+ * Expands a column, and calls leafCallback() when a leaf node is reached.
550
559
  *
551
560
  * @param {CSN.Element} art
552
- * @param {object} root Column, ref in order by, etc.
553
- * @param {boolean} withAlias
554
- * @returns {Array}
561
+ * Structured Artifact which is used for expansion (and names, etc.). For a ref, it's the
562
+ * underlying type or a cast-type, for a value, it's always the cast-type.
563
+ * @param {string} colName
564
+ * Name of the column, that is used as the first name segment, e.g. a column `a` may end up in
565
+ * leafs `a_b` and `a_c`, if `art` has elements `b` and `c`.
566
+ * @param {string[]} colTypeRef
567
+ * Expanded type for the column. Basically the path to the to-be-expanded `art`.
568
+ * @param {(currentRef: any[], currentAlias: string[]) => object} leafCallback
569
+ * Callback when leaf nodes are reached. currentRef is the type reference for the expanded
570
+ * column. currentAlias is the columns calculated alias.
571
+ * @returns {object[]}
555
572
  */
556
- function expandRef( art, root, withAlias ) {
573
+ function _expandStructCol( art, colName, colTypeRef, leafCallback ) {
557
574
  const expanded = [];
558
- /** @type {Array<[CSN.Element, any[], any[]]>} */
559
- const stack = [ [ art, root.ref, [ root.as || implicitAs(root.ref) ] ] ];
575
+ /** @type {Array<[CSN.Element, any[], string[]]>} */
576
+ const stack = [ [ art, colTypeRef, [ colName ] ] ];
560
577
  while (stack.length > 0) {
561
578
  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) ]);
579
+ if (csnUtils.isStructured(current)) {
580
+ const elements = Object.entries(current.elements || csnUtils.effectiveType(current).elements).reverse();
581
+ for (const [ name, elem ] of elements)
582
+ stack.push([ elem, currentRef.concat(name), currentAlias.concat(name) ]);
565
583
  }
566
584
  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);
585
+ const newCol = leafCallback(currentRef, currentAlias);
586
+ expanded.push(newCol);
579
587
  }
580
588
  }
581
589
 
582
590
  return expanded;
583
591
  }
584
592
 
593
+ /**
594
+ * Expand the ref and - if requested - expand/set the alias with it.
595
+ *
596
+ * @param {CSN.Element} art
597
+ * @param {object} root Column, ref in order by, etc.
598
+ * @param {boolean} withAlias Whether to add an explicit flattened alias to the expanded columns/references.
599
+ * @returns {Array}
600
+ */
601
+ function expandRef( art, root, withAlias ) {
602
+ return _expandStructCol(art, columnAlias(root), root.ref, ( currentRef, currentAlias) => {
603
+ const obj = { ...root, ref: currentRef };
604
+ if (withAlias) {
605
+ // TODO: Remove this line in case foreign key annotations should
606
+ // be adressed via full path into target instead of using alias
607
+ // names. See flattening.js::flattenAllStructStepsInRefs()
608
+ // apply transformations on `ref` counterpart comment.
609
+ setProp(obj, '$structRef', currentAlias);
610
+ obj.as = currentAlias.join(pathDelimiter);
611
+ // alias was implicit - to later distinguish expanded s -> s.a from explicitly written s.a
612
+ if (root.as === undefined)
613
+ setProp(obj, '$implicitAlias', true);
614
+ }
615
+
616
+ if (typeof root.$env === 'string')
617
+ obj.ref = [ root.$env, ...obj.ref ];
618
+
619
+ return obj;
620
+ });
621
+ }
622
+
623
+ /**
624
+ * Expand `null` columns which were cast to a structure, that is: `null as name : Struct`.
625
+ * Requires that `col` has an alias.
626
+ *
627
+ * @param {CSN.Element} art
628
+ * @param {object} col
629
+ * @param {boolean} withAlias Whether to add an explicit flattened alias to the expanded columns/references.
630
+ * @returns {Array}
631
+ */
632
+ function expandValAsStructure( art, col, withAlias ) {
633
+ const colName = col.as || '';
634
+ // Expression-columns may have an internal name such as `$_column_N`. If the name is internal,
635
+ // we should not publish names based upon the internal name.
636
+ const isInternal = !col.as || !Object.prototype.propertyIsEnumerable.call(col, 'as');
637
+
638
+ return _expandStructCol(art, colName, col.cast.type?.ref || [ col.cast.type ], ( currentRef, currentAlias) => {
639
+ const newCol = {
640
+ ...col,
641
+ val: col.val,
642
+ cast: { type: { ref: currentRef } },
643
+ };
644
+ if (withAlias) {
645
+ if (!isInternal)
646
+ newCol.as = currentAlias.join(pathDelimiter);
647
+ else
648
+ setProp(newCol, 'as', currentAlias.join(pathDelimiter));
649
+ }
650
+ return newCol;
651
+ });
652
+ }
653
+
585
654
  /**
586
655
  * Get the effective name produced by the object
587
656
  *
@@ -604,9 +673,10 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
604
673
  * @param {object} base The raw set of things a * can expand to
605
674
  * @param {Array} subs Things - the .expand/.inline or .columns
606
675
  * @param {string[]} [excluding=[]]
676
+ * @param {boolean} [isComplexQuery=false] Wether the query is a single source select or something more complex
607
677
  * @returns {Array} If there was a star, expand it and handle shadowing/excluding, else just return subs
608
678
  */
609
- function replaceStar( base, subs, excluding = [] ) {
679
+ function replaceStar( base, subs, excluding = [], isComplexQuery = false ) {
610
680
  const stars = [];
611
681
  const names = Object.create(null);
612
682
  for (let i = 0; i < subs.length; i++) {
@@ -638,7 +708,11 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
638
708
  }
639
709
  }
640
710
  else { // the thing is not shadowed - use the name from the base
641
- star.push({ ref: [ part ] });
711
+ const col = { ref: [ part ] };
712
+ if (isComplexQuery) // $env: tableAlias
713
+ setProp(col, '$env', base[part][0].parent);
714
+
715
+ star.push(col);
642
716
  }
643
717
  }
644
718
  }
@@ -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 &&