@sap/cds-compiler 5.7.4 → 5.8.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 (71) hide show
  1. package/CHANGELOG.md +60 -2
  2. package/bin/cdsse.js +13 -1
  3. package/doc/CHANGELOG_BETA.md +7 -0
  4. package/lib/api/options.js +2 -1
  5. package/lib/api/validate.js +9 -0
  6. package/lib/base/message-registry.js +55 -20
  7. package/lib/base/messages.js +5 -2
  8. package/lib/base/model.js +4 -1
  9. package/lib/checks/assocOutsideService.js +40 -0
  10. package/lib/checks/featureFlags.js +4 -1
  11. package/lib/checks/types.js +7 -4
  12. package/lib/checks/validator.js +3 -0
  13. package/lib/compiler/assert-consistency.js +11 -5
  14. package/lib/compiler/checks.js +79 -17
  15. package/lib/compiler/define.js +57 -3
  16. package/lib/compiler/extend.js +1 -2
  17. package/lib/compiler/generate.js +1 -1
  18. package/lib/compiler/populate.js +17 -6
  19. package/lib/compiler/propagator.js +1 -1
  20. package/lib/compiler/resolve.js +181 -150
  21. package/lib/compiler/shared.js +276 -22
  22. package/lib/compiler/tweak-assocs.js +15 -4
  23. package/lib/compiler/xpr-rewrite.js +76 -50
  24. package/lib/edm/annotations/edmJson.js +1 -1
  25. package/lib/edm/annotations/genericTranslation.js +2 -2
  26. package/lib/edm/csn2edm.js +2 -2
  27. package/lib/edm/edmPreprocessor.js +15 -9
  28. package/lib/edm/edmUtils.js +12 -5
  29. package/lib/gen/CdlGrammar.checksum +1 -0
  30. package/lib/gen/CdlParser.js +2234 -2233
  31. package/lib/gen/Dictionary.json +55 -8
  32. package/lib/json/from-csn.js +37 -17
  33. package/lib/json/to-csn.js +4 -0
  34. package/lib/language/genericAntlrParser.js +7 -0
  35. package/lib/main.d.ts +5 -0
  36. package/lib/model/cloneCsn.js +1 -0
  37. package/lib/model/csnRefs.js +1 -0
  38. package/lib/model/csnUtils.js +0 -5
  39. package/lib/modelCompare/utils/filter.js +2 -2
  40. package/lib/optionProcessor.js +2 -0
  41. package/lib/parsers/AstBuildingParser.js +47 -17
  42. package/lib/parsers/CdlGrammar.g4 +10 -12
  43. package/lib/parsers/XprTree.js +206 -0
  44. package/lib/render/toCdl.js +61 -89
  45. package/lib/render/toSql.js +59 -29
  46. package/lib/render/utils/standardDatabaseFunctions.js +252 -15
  47. package/lib/transform/addTenantFields.js +9 -3
  48. package/lib/transform/db/assocsToQueries/transformExists.js +3 -0
  49. package/lib/transform/db/assocsToQueries/utils.js +10 -3
  50. package/lib/transform/db/expansion.js +3 -1
  51. package/lib/transform/db/flattening.js +7 -3
  52. package/lib/transform/db/killAnnotations.js +1 -0
  53. package/lib/transform/db/processSqlServices.js +70 -17
  54. package/lib/transform/draft/db.js +8 -3
  55. package/lib/transform/draft/odata.js +27 -4
  56. package/lib/transform/effective/main.js +37 -10
  57. package/lib/transform/effective/misc.js +4 -9
  58. package/lib/transform/effective/service.js +34 -0
  59. package/lib/transform/effective/types.js +28 -17
  60. package/lib/transform/forOdata.js +36 -10
  61. package/lib/transform/forRelationalDB.js +30 -18
  62. package/lib/transform/odata/adaptAnnotationRefs.js +37 -21
  63. package/lib/transform/odata/createForeignKeys.js +121 -117
  64. package/lib/transform/odata/flattening.js +12 -9
  65. package/lib/transform/transformUtils.js +58 -25
  66. package/lib/transform/translateAssocsToJoins.js +10 -6
  67. package/lib/transform/universalCsn/coreComputed.js +5 -1
  68. package/package.json +1 -1
  69. package/share/messages/message-explanations.json +1 -0
  70. package/share/messages/rewrite-not-supported.md +5 -0
  71. package/share/messages/rewrite-undefined-key.md +94 -0
@@ -99,7 +99,7 @@ const oDataFunctions = {
99
99
  const { args } = signature;
100
100
  checkArgs.call(this, 'date', args, 1);
101
101
  const x = this.renderArgs({ ...signature, args: [ args[0] ] });
102
- return `date(${x}) `;
102
+ return `date(${x})`;
103
103
  },
104
104
  // this could also be a negative number
105
105
  // also, parts of the EDM.duration are optional which complicates
@@ -199,19 +199,19 @@ const oDataFunctions = {
199
199
  const { args } = signature;
200
200
  checkArgs.call(this, 'fractionalseconds', args, 1);
201
201
  const x = this.renderArgs({ ...signature, args: [ args[0] ] });
202
- return `CAST(date_part('second', ${x}) - floor(date_part('second', ${x})) AS DECIMAL(3,3))`;
202
+ return `cast(date_part('second', ${x}) - floor(date_part('second', ${x})) AS DECIMAL(3,3))`;
203
203
  },
204
204
  time(signature) {
205
205
  const { args } = signature;
206
206
  checkArgs.call(this, 'time', args, 1);
207
207
  const x = this.renderArgs({ ...signature, args: [ args[0] ] });
208
- return `to_char(${x}, 'HH24:MI:SS')`;
208
+ return `to_char(${x}, 'HH24:MI:SS')::TIME`;
209
209
  },
210
210
  date(signature) {
211
211
  const { args } = signature;
212
212
  checkArgs.call(this, 'date', args, 1);
213
213
  const x = this.renderArgs({ ...signature, args: [ args[0] ] });
214
- return `to_char(${x}, 'YYYY-MM-DD')`;
214
+ return `${x}::DATE`;
215
215
  },
216
216
  },
217
217
  // https://help.sap.com/docs/HANA_SERVICE_CF/7c78579ce9b14a669c1f3295b0d8ca16/f12b86a6284c4aeeb449e57eb5dd3ebd.html?locale=en-US
@@ -309,13 +309,13 @@ const oDataFunctions = {
309
309
  const { args } = signature;
310
310
  checkArgs.call(this, 'time', args, 1);
311
311
  const x = this.renderArgs({ ...signature, args: [ args[0] ] });
312
- return `cast(to_time(${x}) AS NVARCHAR)`;
312
+ return `to_time(${x})`;
313
313
  },
314
314
  date(signature) {
315
315
  const { args } = signature;
316
316
  checkArgs.call(this, 'date', args, 1);
317
317
  const x = this.renderArgs({ ...signature, args: [ args[0] ] });
318
- return `cast(to_date(${x}) AS NVARCHAR)`;
318
+ return `to_date(${x})`;
319
319
  },
320
320
  },
321
321
  // https://www.h2database.com/html/functions.html
@@ -429,13 +429,13 @@ const oDataFunctions = {
429
429
  const { args } = signature;
430
430
  checkArgs.call(this, 'time', args, 1);
431
431
  const x = this.renderArgs({ ...signature, args: [ args[0] ] });
432
- return `cast(cast(${x} AS TIME) AS VARCHAR)`;
432
+ return `cast(${x} AS TIME)`;
433
433
  },
434
434
  date(signature) {
435
435
  const { args } = signature;
436
436
  checkArgs.call(this, 'date', args, 1);
437
437
  const x = this.renderArgs({ ...signature, args: [ args[0] ] });
438
- return `cast(cast(${x} AS DATE) AS VARCHAR)`;
438
+ return `cast(${x} AS DATE)`;
439
439
  },
440
440
  },
441
441
  common: {
@@ -544,18 +544,255 @@ const oDataFunctions = {
544
544
  },
545
545
  };
546
546
 
547
- // TODO: add support for the common SAP HANA Functions
548
547
  const hanaFunctions = {
549
- sqlite: {},
550
- postgres: {},
551
- hana: { /* no-op */ },
552
- h2: {},
548
+ sqlite: {
549
+ /**
550
+ * SQLite relies on floating-point arithmetic for date/time calculations, which can introduce
551
+ * slight imprecisions due to the use of the `julianday` function. The `julianday` function
552
+ * computes the difference between two timestamps as a floating-point value in days, which
553
+ * is then scaled to nano100 units (0.1 microseconds). While this approach is efficient,
554
+ * the inherent precision limits of floating-point arithmetic can result in small deviations
555
+ * (e.g., off by a few nano100 units).
556
+ *
557
+ * @param {Object} signature - The function signature containing arguments.
558
+ * @returns {string} - SQL expression to calculate the nano100 difference in SQLite.
559
+ */
560
+ nano100_between(signature) {
561
+ const { args } = signature;
562
+ checkArgs.call(this, 'nano100_between', args, 2);
563
+ const x = this.renderArgs({ ...signature, args: [ args[0] ] });
564
+ const y = this.renderArgs({ ...signature, args: [ args[1] ] });
565
+ // 1 day = 24h*60m*60s*10'000'000 = 864'000'000'000 nano100
566
+ return `CAST(((julianday(${y}) - julianday(${x})) * 864000000000) as INTEGER)`;
567
+ },
568
+ seconds_between(signature) {
569
+ const { args } = signature;
570
+ checkArgs.call(this, 'seconds_between', args, 2);
571
+ const x = this.renderArgs({ ...signature, args: [ args[0] ] });
572
+ const y = this.renderArgs({ ...signature, args: [ args[1] ] });
573
+
574
+ return `CAST(strftime('%s', ${y}) - strftime('%s', ${x}) AS INTEGER)`;
575
+ },
576
+ days_between(signature) {
577
+ const { args } = signature;
578
+ checkArgs.call(this, 'days_between', args, 2);
579
+ const x = this.renderArgs({ ...signature, args: [ args[0] ] });
580
+ const y = this.renderArgs({ ...signature, args: [ args[1] ] });
581
+
582
+ return `(CASE WHEN (strftime('%s', ${y}) - strftime('%s', ${x})) < 86400 AND (strftime('%s', ${y}) - strftime('%s', ${x})) > -86400 THEN 0 ELSE CAST((strftime('%s', ${y}) - strftime('%s', ${x})) / 86400 AS INTEGER) END)`;
583
+ },
584
+ /**
585
+ * Calculates the difference in months between two dates, `x` and `y`, with a correction for partial months.
586
+ *
587
+ * The computation consists of:
588
+ *
589
+ * 1. Year/Month Difference:
590
+ * - Extracts the year and month parts from both dates and computes a raw difference:
591
+ * (year(y) - year(x)) * 12 + (month(y) - month(x)).
592
+ *
593
+ * 2. Partial-Month Correction:
594
+ * - Generates a composite value of day and time components from each date using:
595
+ * strftime('%d%H%M%S%f0000', date)
596
+ * This zero-padded composite includes day, hour, minute, second, and fractional seconds.
597
+ * - For a forward interval (when y is after or equal to x):
598
+ * If the composite for y is less than that for x, then the final month is incomplete, so subtract 1.
599
+ * - For a backward interval (when y is before x):
600
+ * If the composite for y is greater than that for x, then the final month is incomplete, so add 1.
601
+ *
602
+ * 3. Leap-Year Adjustment:
603
+ * - The composite value inherently captures all day/time details (including the leap day, Feb 29),
604
+ * so the extra day in a leap year is automatically accounted for in the partial-month correction.
605
+ *
606
+ * @param {object} signature - Contains the function arguments.
607
+ * @returns {string} A SQL expression that calculates the adjusted month difference.
608
+ */
609
+ months_between(signature) {
610
+ // Ensure exactly two arguments (startDate, endDate)
611
+ checkArgs.call(this, 'months_between', signature.args, 2);
612
+
613
+ // Render the arguments as SQL expressions.
614
+ const x = this.renderArgs({ ...signature, args: [ signature.args[0] ] });
615
+ const y = this.renderArgs({ ...signature, args: [ signature.args[1] ] });
616
+
617
+ // Construct the SQL expression:
618
+ // 1. Base month difference from the year and month components.
619
+ // 2. Partial-month correction using a composite integer of day and time.
620
+ const res = `
621
+ (
622
+ (
623
+ (CAST(strftime('%Y', ${y}) AS Integer) - CAST(strftime('%Y', ${x}) AS Integer)) * 12
624
+ )
625
+ +
626
+ (
627
+ CAST(strftime('%m', ${y}) AS Integer) - CAST(strftime('%m', ${x}) AS Integer)
628
+ )
629
+ +
630
+ (
631
+ CASE
632
+ /* For backward intervals: if the composite (day + time) of y is greater than x, add 1. */
633
+ WHEN CAST(strftime('%Y%m', ${y}) AS Integer) < CAST(strftime('%Y%m', ${x}) AS Integer)
634
+ THEN (CAST(strftime('%d%H%M%S%f0000', ${y}) AS Integer) > CAST(strftime('%d%H%M%S%f0000', ${x}) AS Integer))
635
+ /* For forward intervals: if the composite of y is less than x, subtract 1. */
636
+ ELSE (CAST(strftime('%d%H%M%S%f0000', ${y}) AS Integer) < CAST(strftime('%d%H%M%S%f0000', ${x}) AS Integer)) * -1
637
+ END
638
+ )
639
+ )
640
+ `;
641
+ // Remove extra whitespace and return the single-line SQL expression.
642
+ return res.replace(/\s+/g, ' ');
643
+ },
644
+ years_between(signature) {
645
+ const { args } = signature;
646
+ checkArgs.call(this, 'years_between', args, 2);
647
+ return `floor((${hanaFunctions.sqlite.months_between.call(this, signature)}) / 12)`;
648
+ },
649
+ },
650
+ postgres: {
651
+ nano100_between(signature) {
652
+ const { args } = signature;
653
+ checkArgs.call(this, 'nano100_between', args, 2);
654
+ const x = this.renderArgs({ ...signature, args: [ args[0] ] });
655
+ const y = this.renderArgs({ ...signature, args: [ args[1] ] });
656
+ // make sure to cast NUMERIC to BIGINT (corresponds to cds.Int64)
657
+ return `(EXTRACT(EPOCH FROM (${y}) - (${x})) * 10000000)::BIGINT`;
658
+ },
659
+ seconds_between(signature) {
660
+ const { args } = signature;
661
+ checkArgs.call(this, 'seconds_between', args, 2);
662
+ const x = this.renderArgs({ ...signature, args: [ args[0] ] });
663
+ const y = this.renderArgs({ ...signature, args: [ args[1] ] });
664
+
665
+ return `EXTRACT(EPOCH FROM (${y}) - (${x}))::BIGINT`;
666
+ },
667
+ days_between(signature) {
668
+ const { args } = signature;
669
+ checkArgs.call(this, 'days_between', args, 2);
670
+ const x = this.renderArgs({ ...signature, args: [ args[0] ] });
671
+ const y = this.renderArgs({ ...signature, args: [ args[1] ] });
672
+ return `EXTRACT(DAY FROM ${y}::timestamp - ${x}::timestamp)::integer`;
673
+ },
674
+ months_between(signature) {
675
+ const { args } = signature;
676
+ checkArgs.call(this, 'months_between', args, 2);
677
+ const x = this.renderArgs({ ...signature, args: [ args[0] ] });
678
+ const y = this.renderArgs({ ...signature, args: [ args[1] ] });
679
+
680
+ return `(EXTRACT(YEAR FROM AGE(${y}, ${x})) * 12 + EXTRACT(MONTH FROM AGE(${y}, ${x})))::INTEGER`;
681
+ },
682
+ years_between(signature) {
683
+ const { args } = signature;
684
+ checkArgs.call(this, 'years_between', args, 2);
685
+ return `floor((${hanaFunctions.postgres.months_between.call(this, signature)}) / 12)::INTEGER`;
686
+ },
687
+ },
688
+ h2: {
689
+ nano100_between(signature) {
690
+ const { args } = signature;
691
+ checkArgs.call(this, 'nano100_between', args, 2);
692
+ const x = this.renderArgs({ ...signature, args: [ args[0] ] });
693
+ const y = this.renderArgs({ ...signature, args: [ args[1] ] });
694
+
695
+ return `CAST(DATEDIFF('MICROSECOND', ${x}, ${y}) * 10 AS BIGINT)`;
696
+ },
697
+ seconds_between(signature) {
698
+ const { args } = signature;
699
+ checkArgs.call(this, 'seconds_between', args, 2);
700
+ const x = this.renderArgs({ ...signature, args: [ args[0] ] });
701
+ const y = this.renderArgs({ ...signature, args: [ args[1] ] });
702
+
703
+ return `CAST(DATEDIFF('SECOND', ${x}, ${y}) AS BIGINT)`;
704
+ },
705
+ days_between(signature) {
706
+ const { args } = signature;
707
+ checkArgs.call(this, 'days_between', args, 2);
708
+ const x = this.renderArgs({ ...signature, args: [ args[0] ] });
709
+ const y = this.renderArgs({ ...signature, args: [ args[1] ] });
710
+ return `CASE WHEN ABS(DATEDIFF('SECOND', ${x}, ${y})) < 86400 THEN 0 ELSE CAST(FLOOR(DATEDIFF('SECOND', ${x}, ${y}) / 86400) AS INTEGER) END`;
711
+ },
712
+ /**
713
+ * Uses DATEDIFF('MONTH') and then applies a partial-month correction for day-of-month boundaries in both
714
+ * forward and backward (negative) scenarios.
715
+ */
716
+ months_between(signature) {
717
+ const { args } = signature;
718
+ checkArgs.call(this, 'months_between', args, 2);
719
+
720
+ const x = this.renderArgs({ ...signature, args: [ args[0] ] });
721
+ const y = this.renderArgs({ ...signature, args: [ args[1] ] });
722
+
723
+ const res = `
724
+ CAST(
725
+ DATEDIFF('MONTH', ${x}, ${y})
726
+ + CASE
727
+ WHEN DATEDIFF('DAY', ${x}, ${y}) >= 0
728
+ AND EXTRACT(DAY FROM ${y}) < EXTRACT(DAY FROM ${x})
729
+ THEN -1
730
+
731
+ WHEN DATEDIFF('DAY', ${x}, ${y}) < 0
732
+ AND EXTRACT(DAY FROM ${y}) > EXTRACT(DAY FROM ${x})
733
+ THEN 1
734
+
735
+ ELSE 0
736
+ END
737
+ AS INTEGER
738
+ )
739
+ `;
740
+ return res.replace(/\s+/g, ' ');
741
+ },
742
+ years_between(signature) {
743
+ const { args } = signature;
744
+ checkArgs.call(this, 'years_between', args, 2);
745
+ return `floor((${hanaFunctions.h2.months_between.call(this, signature)}) / 12)`;
746
+ },
747
+ },
553
748
  common: {},
749
+ // identity functions + argument check
750
+ hana: {
751
+ nano100_between(signature) {
752
+ const { args } = signature;
753
+ checkArgs.call(this, 'nano100_between', args, 2);
754
+ const x = this.renderArgs({ ...signature, args: [ args[0] ] });
755
+ const y = this.renderArgs({ ...signature, args: [ args[1] ] });
756
+
757
+ return `nano100_between(${x}, ${y})`;
758
+ },
759
+ seconds_between(signature) {
760
+ const { args } = signature;
761
+ checkArgs.call(this, 'seconds_between', args, 2);
762
+ const x = this.renderArgs({ ...signature, args: [ args[0] ] });
763
+ const y = this.renderArgs({ ...signature, args: [ args[1] ] });
764
+
765
+ return `seconds_between(${x}, ${y})`;
766
+ },
767
+ days_between(signature) {
768
+ const { args } = signature;
769
+ checkArgs.call(this, 'days_between', args, 2);
770
+ const x = this.renderArgs({ ...signature, args: [ args[0] ] });
771
+ const y = this.renderArgs({ ...signature, args: [ args[1] ] });
772
+ return `days_between(${x}, ${y})`;
773
+ },
774
+ months_between(signature) {
775
+ const { args } = signature;
776
+ checkArgs.call(this, 'months_between', args, 2);
777
+ const x = this.renderArgs({ ...signature, args: [ args[0] ] });
778
+ const y = this.renderArgs({ ...signature, args: [ args[1] ] });
779
+
780
+ return `months_between(${x}, ${y})`;
781
+ },
782
+ years_between(signature) {
783
+ const { args } = signature;
784
+ checkArgs.call(this, 'years_between', args, 2);
785
+ return `years_between(${this.renderArgs(signature)})`;
786
+ },
787
+ },
554
788
  };
555
789
 
556
790
  function checkArgs( funcName, receivedArgs, expectedLength, alternativeLength = null ) {
557
791
  const expectedMismatch = receivedArgs.length < expectedLength;
558
- const alternativeMismatch = expectedMismatch && (!alternativeLength || alternativeLength && receivedArgs.length < alternativeLength);
792
+ const alternativeMismatch
793
+ = expectedMismatch &&
794
+ (!alternativeLength ||
795
+ (alternativeLength && receivedArgs.length < alternativeLength));
559
796
  if (expectedMismatch && alternativeMismatch) {
560
797
  this.error('def-missing-argument', [ ...this.path, 'args' ], {
561
798
  '#': alternativeLength ? 'alternative' : 'std',
@@ -565,7 +802,7 @@ function checkArgs( funcName, receivedArgs, expectedLength, alternativeLength =
565
802
  name: funcName,
566
803
  });
567
804
  }
568
- };
805
+ }
569
806
 
570
807
  module.exports.standardDatabaseFunctions = {
571
808
  sqlite: { ...oDataFunctions.sqlite, ...hanaFunctions.sqlite },
@@ -18,7 +18,12 @@
18
18
  'use strict';
19
19
 
20
20
  const { createMessageFunctions } = require( '../base/messages' );
21
- const { csnRefs, traverseQuery, implicitAs } = require( '../model/csnRefs' );
21
+ const {
22
+ csnRefs,
23
+ traverseQuery,
24
+ implicitAs,
25
+ pathId,
26
+ } = require( '../model/csnRefs' );
22
27
 
23
28
  const annoTenantIndep = '@cds.tenant.independent';
24
29
 
@@ -85,7 +90,7 @@ function addTenantFields( csn, options, messageFunctions ) {
85
90
  independent = art.kind; // might be used for message variant
86
91
  checkIncludes( art ); // recompile should work
87
92
  }
88
- else if (projection) { // events - TODO: mention in doc
93
+ else if (projection) { // events, types - TODO: mention in doc
89
94
  independent = art.kind; // might be used for message variant
90
95
  // recompile should work: no new `tenant` source element for `select *`
91
96
  traverseQuery( projection, null, null, handleQuery );
@@ -187,12 +192,13 @@ function addTenantFields( csn, options, messageFunctions ) {
187
192
 
188
193
  function handleQuerySource( query ) {
189
194
  if (independent) {
190
- const art = query.ref[0]; // yes, the base
195
+ const art = pathId(query.ref[0]); // yes, the base
191
196
  if (csn.definitions[art][annoTenantIndep])
192
197
  return true;
193
198
  error( 'tenant-invalid-query-source', msgLocations( csnPath ), { art, '#': independent }, {
194
199
  std: 'Can\'t use a tenant-dependent query source $(ART) in a tenant-independent entity',
195
200
  event: 'Can\'t use a tenant-dependent query source $(ART) in an event',
201
+ type: 'Can\'t use a tenant-dependent query source $(ART) in a type definition',
196
202
  } );
197
203
  return true;
198
204
  }
@@ -346,6 +346,9 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
346
346
  },
347
347
  };
348
348
 
349
+ if (assocRef.args) // copy named arguments
350
+ subselect.SELECT.from.ref = [ { id: target, args: assocRef.args } ];
351
+
349
352
  setProp(subselect.SELECT.from, '_art', csn.definitions[target]);
350
353
  setProp(subselect.SELECT.from, '_links', [ { idx: 0, art: csn.definitions[target] } ]);
351
354
 
@@ -106,14 +106,21 @@ function getHelpers( csn, inspectRef, error ) {
106
106
  function getFirstAssoc( xprPart, path ) {
107
107
  const { links, art } = getLinksAndArt({}, path);
108
108
  for (let i = 0; i < xprPart.ref.length - 1; i++) {
109
- if (links[i].art && links[i].art.target) {
109
+ if (links[i].art?.target) {
110
110
  return {
111
- head: (i === 0 ? [] : xprPart.ref.slice(0, i)), root: links[i].art, ref: xprPart.ref[i], tail: xprPart.ref.slice(i + 1),
111
+ head: (i === 0 ? [] : xprPart.ref.slice(0, i)),
112
+ root: links[i].art,
113
+ ref: xprPart.ref[i],
114
+ tail: xprPart.ref.slice(i + 1),
112
115
  };
113
116
  }
114
117
  }
118
+ const { ref } = xprPart;
115
119
  return {
116
- head: (xprPart.ref.length === 1 ? [] : xprPart.ref.slice(0, xprPart.ref.length - 1)), root: art, ref: xprPart.ref[xprPart.ref.length - 1], tail: [],
120
+ head: (ref.length === 1 ? [] : ref.slice(0, ref.length - 1)),
121
+ root: art,
122
+ ref: ref.at(-1),
123
+ tail: [],
117
124
  };
118
125
  }
119
126
 
@@ -652,8 +652,10 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
652
652
  if (typeof root.$env === 'string' && (isComplexOrNestedQuery || options.transformation !== 'odata' || root.$env === pathId(obj.ref[0])))
653
653
  obj.ref = [ root.$env, ...obj.ref ];
654
654
 
655
- if (iterateOptions.keepKeysOrigin)
655
+ if (iterateOptions.keepKeysOrigin) {
656
656
  setProp(obj, '$originalKeyRef', { ref: root.ref, as: root.as });
657
+ setProp(obj, '$path', root.$path);
658
+ }
657
659
 
658
660
  return obj;
659
661
  });
@@ -15,7 +15,7 @@ const { forEach } = require('../../utils/objectUtils');
15
15
  const { transformExpression } = require('./applyTransformations');
16
16
  const { cloneCsnNonDict } = require('../../model/cloneCsn');
17
17
  const { EdmTypeFacetNames } = require('../../edm/EdmPrimitiveTypeDefinitions');
18
- const adaptAnnotationsRefs = require('../odata/adaptAnnotationRefs');
18
+ const { adaptAnnotationsRefs } = require('../odata/adaptAnnotationRefs');
19
19
 
20
20
  /**
21
21
  * Strip off leading $self from refs where applicable.
@@ -220,7 +220,8 @@ function getStructStepsFlattener( csn, options, messageFunctions, resolved, path
220
220
  // full path into target, uncomment this line and
221
221
  // comment/remove setProp in expansion.js
222
222
  // setProp(parent, '$structRef', parent.ref);
223
- [ parent.ref, refChanged ] = flattenStructStepsInRef(ref, scopedPath, links, scope, resolvedLinkTypes, suspend, suspendPos, parent.$bparam);
223
+ const flattenParameters = false; // structured parameters remain structured
224
+ [ parent.ref, refChanged ] = flattenStructStepsInRef(ref, scopedPath, links, scope, resolvedLinkTypes, suspend, suspendPos, parent.$bparam, flattenParameters);
224
225
  resolved.set(parent, { links, art, scope });
225
226
  // Explicitly set implicit alias for things that are now flattened - but only in columns
226
227
  // TODO: Can this be done elegantly during expand phase already?
@@ -649,8 +650,11 @@ function handleManagedAssociationsAndCreateForeignKeys( csn, options, messageFun
649
650
  delete element.default;
650
651
  }
651
652
 
652
- if (options.transformation === 'effective')
653
+ if (options.transformation === 'effective') {
653
654
  adaptAnnotationsRefs(fks, csnUtils, messageFunctions, eltPath);
655
+ const validAnnoNames = Object.keys(element).filter(pn => pn[0] === '@' && findAnnotationExpression(element, pn));
656
+ fks.forEach(fk => copyAnnotations(element, fk[1], false, {}, validAnnoNames));
657
+ }
654
658
  orderedElements.push(...fks);
655
659
  });
656
660
 
@@ -25,6 +25,7 @@ const requiredAnnos = {
25
25
  '@cds.redirection.target': true,
26
26
  '@Core.Computed': true,
27
27
  [sqlServiceAnnotation]: true,
28
+ '@cds.external': true, // for external ABAP SQL services and data products for now
28
29
  };
29
30
 
30
31
  /**
@@ -1,23 +1,26 @@
1
1
  'use strict';
2
2
 
3
- const { setProp } = require('../../base/model');
3
+ const { setProp, isBetaEnabled } = require('../../base/model');
4
4
 
5
5
  const sqlServiceAnnotation = '@protocol';
6
- // Problem: How can we clone a Symbol when sorting?
7
- // const sqlServiceEntities = Symbol.for('SQL Service enabled entities');
6
+
8
7
  /**
9
8
  * Find all entities in SQL services and mark them with an annotation and
10
9
  * remember them in a symbol property for easier processing in toSql-rendering.
11
10
  *
12
11
  * @param {CSN.Model} csn
12
+ * @param {CSN.Options} options
13
13
  * @returns {Function}
14
14
  */
15
- function processSqlServices(csn) {
15
+ function processSqlServices(csn, options) {
16
16
  setProp(csn, '$sqlServiceEntities', Object.create(null));
17
+ setProp(csn, '$dummyServiceEntities', Object.create(null));
17
18
  return function findAndMarkSqlServiceArtifacts(artifact, artifactName) {
18
- const sqlServiceName = isEntityInSqlService(artifact, artifactName, csn);
19
+ const { sqlServiceName, dummyServiceName } = isEntityInSqlService(artifact, artifactName, csn, options);
19
20
  if (sqlServiceName?.length > 0)
20
21
  setProp(artifact, '$sqlService', sqlServiceName);
22
+ if (dummyServiceName?.length > 0)
23
+ setProp(artifact, '$dummyService', dummyServiceName);
21
24
  };
22
25
  }
23
26
 
@@ -29,35 +32,85 @@ function processSqlServices(csn) {
29
32
  function isSqlService(artifact) {
30
33
  return artifact.kind === 'service' && artifact[sqlServiceAnnotation] === 'sql';
31
34
  }
35
+
32
36
  /**
37
+ * Checks if the given artifact is an external ABAP SQL service.
33
38
  *
34
- * @param {CSN.Artifact} artifact
35
- * @param {string} artifactName
36
- * @param {CSN.Model} csn
37
- * @returns {string|null}
39
+ * @param {object} artifact - The artifact to check.
40
+ * @param {CSN.Options} options
41
+ * @returns {boolean} - Returns true if the artifact is an external ABAP SQL service, otherwise false.
42
+ */
43
+ function isDummyService(artifact, options) {
44
+ return isBetaEnabled(options, 'sqlServiceDummies') && artifact.kind === 'service' && artifact['@cds.external'] && artifact[sqlServiceAnnotation] === 'dummy';
45
+ }
46
+
47
+ /**
48
+ * Determines if an artifact is part of a SQL service or an external ABAP SQL service.
49
+ *
50
+ * @param {object} artifact - The artifact to check.
51
+ * @param {string} artifactName - The name of the artifact.
52
+ * @param {object} csn - The CSN (Core Schema Notation) object containing definitions.
53
+ * @param {CSN.Options} options
54
+ * @returns {object} An object containing the names of the SQL service and external ABAP SQL service, if found.
38
55
  */
39
- function isEntityInSqlService(artifact, artifactName, csn) {
40
- if (artifact.kind !== 'entity' || !artifactName.includes('.'))
41
- return null;
56
+ function isEntityInSqlService(artifact, artifactName, csn, options) {
57
+ const result = { sqlServiceName: undefined, dummyServiceName: undefined };
58
+ if (artifact.kind !== 'entity' || !artifactName.includes('.') || artifact['@cds.persistence.skip'] === true)
59
+ return result;
42
60
 
43
61
  const nameParts = artifactName.split('.');
44
- for (let i = nameParts.length; i >= 0; i--) {
62
+ for (let i = nameParts.length - 1; i >= 0; i--) {
45
63
  const possibleServiceName = nameParts.slice(0, i).join('.');
46
64
  if (!csn.definitions[possibleServiceName])
47
65
  continue;
48
66
 
49
67
  const definition = csn.definitions[possibleServiceName];
50
68
  if (isSqlService(definition))
51
- return possibleServiceName;
69
+ result.sqlServiceName = possibleServiceName;
70
+
71
+ if (isDummyService(definition, options))
72
+ result.dummyServiceName = possibleServiceName;
52
73
 
53
74
  // We don't allow nested services/contexts - if we find one, we don't need to keep searching
54
75
  if (definition.kind === 'service' || definition.kind === 'context')
55
- return null;
76
+ return result;
56
77
  }
57
78
 
58
- return null;
79
+ return result;
80
+ }
81
+
82
+ /**
83
+ * Creates a dummy ABAP SQL service for the given artifact if it is marked as an external ABAP SQL service.
84
+ * The dummy service is a copy of the original artifact with certain properties removed.
85
+ * The dummy service is then added to the CSN (Core Schema Notation) definitions.
86
+ *
87
+ * @param {object} artifact - The artifact to create a dummy service for.
88
+ * @param {string} artifactName - The name of the artifact.
89
+ * @param {object} csn - The Core Schema Notation (CSN) object where the dummy service will be added.
90
+ * @param {object} messageFunctions
91
+ * @param {Function} messageFunctions.error
92
+ */
93
+ function createServiceDummy(artifact, artifactName, csn, { error }) {
94
+ if (!artifact.$dummyService)
95
+ return;
96
+
97
+ artifact['@cds.persistence.exists'] = true;
98
+ artifact.$ignore = true;
99
+
100
+ const dummy = { ...artifact };
101
+ delete dummy['@cds.persistence.exists'];
102
+ delete dummy.$ignore;
103
+
104
+ if (csn.definitions[`dummy.${artifactName}`])
105
+ error(null, [ 'definitions', artifactName ], { name: `dummy.${artifactName}` }, 'Generated artifact name $(NAME) conflicts with existing entity');
106
+ else
107
+ csn.definitions[`dummy.${artifactName}`] = dummy;
59
108
  }
60
109
 
61
110
  module.exports = {
62
- processSqlServices, isSqlService, sqlServiceAnnotation,
111
+ processSqlServices,
112
+ isSqlService,
113
+ isDummyService,
114
+ sqlServiceAnnotation,
115
+ createServiceDummy,
63
116
  };
@@ -4,7 +4,7 @@ const {
4
4
  hasAnnotationValue, getServiceNames, forEachDefinition,
5
5
  getResultingName, forEachMemberRecursively, applyAnnotationsFromExtensions,
6
6
  } = require('../../model/csnUtils');
7
- const { setProp, isDeprecatedEnabled } = require('../../base/model');
7
+ const { setProp, isDeprecatedEnabled, isBetaEnabled } = require('../../base/model');
8
8
  const { getTransformers } = require('../transformUtils');
9
9
  const { ModelError } = require('../../base/error');
10
10
  const { forEach } = require('../../utils/objectUtils');
@@ -25,8 +25,8 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
25
25
  const allServices = getServiceNames(csn);
26
26
  const draftRoots = new WeakMap();
27
27
  const {
28
- createAndAddDraftAdminDataProjection, createScalarElement, createAssociationElement,
29
- addElement, copyAndAddElement, createAssociationPathComparison, csnUtils,
28
+ createAndAddDraftAdminDataProjection, createScalarElement,
29
+ createAssociationElement, addElement, copyAndAddElement, createAssociationPathComparison, csnUtils,
30
30
  } = getTransformers(csn, options, messageFunctions, pathDelimiter);
31
31
  const { getCsnDef, isComposition } = csnUtils;
32
32
  const { error, warning } = messageFunctions;
@@ -231,6 +231,11 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
231
231
  };
232
232
  draftAdministrativeData.DraftAdministrativeData.notNull = true;
233
233
  addElement(draftAdministrativeData, draftsArtifact, artifactName);
234
+
235
+ if (isBetaEnabled(options, 'draftMessages')) {
236
+ const draftMessages = { DraftMessages: { '@Core.Computed': true, virtual: true, items: { type: 'DRAFT.DraftAdministrativeData_DraftMessage' } } };
237
+ addElement(draftMessages, draftsArtifact, artifactName);
238
+ }
234
239
  // Note that we may need to do the HANA transformation steps for managed associations
235
240
  // (foreign key field generation, generatedFieldName, creating ON-condition) by hand,
236
241
  // because the corresponding transformation steps have already been done on all artifacts