@sap/cds-compiler 5.7.2 → 5.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +62 -2
  2. package/bin/cdsse.js +13 -1
  3. package/doc/CHANGELOG_BETA.md +7 -0
  4. package/lib/api/options.js +2 -1
  5. package/lib/api/validate.js +9 -0
  6. package/lib/base/location.js +1 -1
  7. package/lib/base/message-registry.js +55 -20
  8. package/lib/base/messages.js +5 -2
  9. package/lib/base/model.js +8 -6
  10. package/lib/checks/assocOutsideService.js +40 -0
  11. package/lib/checks/featureFlags.js +4 -1
  12. package/lib/checks/types.js +7 -4
  13. package/lib/checks/validator.js +3 -0
  14. package/lib/compiler/assert-consistency.js +11 -5
  15. package/lib/compiler/checks.js +79 -17
  16. package/lib/compiler/define.js +60 -3
  17. package/lib/compiler/extend.js +1 -2
  18. package/lib/compiler/generate.js +1 -1
  19. package/lib/compiler/populate.js +17 -6
  20. package/lib/compiler/propagator.js +1 -1
  21. package/lib/compiler/resolve.js +181 -150
  22. package/lib/compiler/shared.js +276 -22
  23. package/lib/compiler/tweak-assocs.js +15 -4
  24. package/lib/compiler/xpr-rewrite.js +76 -50
  25. package/lib/edm/annotations/edmJson.js +1 -1
  26. package/lib/edm/annotations/genericTranslation.js +2 -2
  27. package/lib/edm/csn2edm.js +2 -2
  28. package/lib/edm/edmPreprocessor.js +15 -9
  29. package/lib/edm/edmUtils.js +12 -5
  30. package/lib/gen/CdlGrammar.checksum +1 -0
  31. package/lib/gen/CdlParser.js +2239 -2229
  32. package/lib/gen/Dictionary.json +55 -8
  33. package/lib/json/from-csn.js +37 -17
  34. package/lib/json/to-csn.js +4 -0
  35. package/lib/language/genericAntlrParser.js +7 -0
  36. package/lib/main.d.ts +5 -1
  37. package/lib/model/cloneCsn.js +1 -0
  38. package/lib/model/csnRefs.js +1 -0
  39. package/lib/model/csnUtils.js +0 -5
  40. package/lib/modelCompare/utils/filter.js +2 -2
  41. package/lib/optionProcessor.js +2 -0
  42. package/lib/parsers/AstBuildingParser.js +72 -34
  43. package/lib/parsers/CdlGrammar.g4 +20 -19
  44. package/lib/parsers/XprTree.js +206 -0
  45. package/lib/parsers/index.js +1 -1
  46. package/lib/render/toCdl.js +61 -89
  47. package/lib/render/toSql.js +59 -29
  48. package/lib/render/utils/standardDatabaseFunctions.js +252 -15
  49. package/lib/transform/addTenantFields.js +9 -3
  50. package/lib/transform/db/assocsToQueries/transformExists.js +3 -0
  51. package/lib/transform/db/assocsToQueries/utils.js +10 -3
  52. package/lib/transform/db/expansion.js +3 -1
  53. package/lib/transform/db/flattening.js +7 -3
  54. package/lib/transform/db/killAnnotations.js +1 -0
  55. package/lib/transform/db/processSqlServices.js +70 -17
  56. package/lib/transform/draft/db.js +8 -3
  57. package/lib/transform/draft/odata.js +27 -4
  58. package/lib/transform/effective/main.js +37 -10
  59. package/lib/transform/effective/misc.js +4 -9
  60. package/lib/transform/effective/service.js +34 -0
  61. package/lib/transform/effective/types.js +28 -17
  62. package/lib/transform/forOdata.js +36 -10
  63. package/lib/transform/forRelationalDB.js +30 -18
  64. package/lib/transform/odata/adaptAnnotationRefs.js +37 -21
  65. package/lib/transform/odata/createForeignKeys.js +120 -116
  66. package/lib/transform/odata/flattening.js +10 -8
  67. package/lib/transform/transformUtils.js +58 -25
  68. package/lib/transform/translateAssocsToJoins.js +10 -6
  69. package/lib/transform/universalCsn/coreComputed.js +5 -1
  70. package/package.json +1 -1
  71. package/share/messages/message-explanations.json +1 -0
  72. package/share/messages/rewrite-not-supported.md +5 -0
  73. package/share/messages/rewrite-undefined-key.md +94 -0
@@ -189,9 +189,11 @@ function toSqlDdl( csn, options, messageFunctions ) {
189
189
  constraintDeletions: [],
190
190
  migrations: Object.create(null),
191
191
  hdbrole: Object.create(null),
192
+ hdbsynonym: Object.create(null),
192
193
  };
193
194
 
194
195
  const sqlServiceEntities = Object.create(null);
196
+ const dummySqlServiceEntities = Object.create(null);
195
197
 
196
198
  // Registries for artifact and element names per CSN section
197
199
  const definitionsDuplicateChecker = new DuplicateChecker(options.sqlMapping);
@@ -209,8 +211,9 @@ function toSqlDdl( csn, options, messageFunctions ) {
209
211
  for (const artifactName in csn.deletions)
210
212
  renderArtifactDeletionInto(artifactName, csn.deletions[artifactName], mainResultObj);
211
213
 
214
+ const supportsSqlExtensions = (options.sqlDialect === 'hana' || isBetaEnabled(options, 'sqlExtensions'));
212
215
 
213
- if (csn.changedPrimaryKeys && (options.sqlDialect === 'hana' || isBetaEnabled(options, 'sqlExtensions'))) {
216
+ if (csn.changedPrimaryKeys && supportsSqlExtensions) {
214
217
  csn.changedPrimaryKeys = options.testMode ? sortCsn(csn.changedPrimaryKeys) : csn.changedPrimaryKeys;
215
218
  csn.changedPrimaryKeys.forEach((artifactName) => {
216
219
  const drop = render.dropKey(artifactName);
@@ -221,7 +224,7 @@ function toSqlDdl( csn, options, messageFunctions ) {
221
224
  // Render each artifact extension
222
225
  // Only SAP HANA SQL is currently supported.
223
226
  // Note that extensions may contain new elements referenced in migrations, thus should be compiled first.
224
- if (csn.extensions && (options.sqlDialect === 'hana' || isBetaEnabled(options, 'sqlExtensions'))) {
227
+ if (csn.extensions && supportsSqlExtensions) {
225
228
  csn.extensions = options.testMode ? sortCsn(csn.extensions) : csn.extensions;
226
229
  for (let i = 0; i < csn.extensions.length; ++i) {
227
230
  const extension = csn.extensions[i];
@@ -236,7 +239,7 @@ function toSqlDdl( csn, options, messageFunctions ) {
236
239
 
237
240
  // Render each artifact change
238
241
  // Only SAP HANA SQL is currently supported.
239
- if (csn.migrations && (options.sqlDialect === 'hana' || isBetaEnabled(options, 'sqlExtensions'))) {
242
+ if (csn.migrations && supportsSqlExtensions) {
240
243
  csn.migrations = options.testMode ? sortCsn(csn.migrations) : csn.migrations;
241
244
  for (const migration of csn.migrations) {
242
245
  if (migration.migrate) {
@@ -249,7 +252,7 @@ function toSqlDdl( csn, options, messageFunctions ) {
249
252
  }
250
253
  }
251
254
 
252
- if (csn.changedPrimaryKeys && (options.sqlDialect === 'hana' || isBetaEnabled(options, 'sqlExtensions'))) {
255
+ if (csn.changedPrimaryKeys && supportsSqlExtensions) {
253
256
  csn.changedPrimaryKeys = options.testMode ? sortCsn(csn.changedPrimaryKeys) : csn.changedPrimaryKeys;
254
257
  csn.changedPrimaryKeys.forEach((artifactName) => {
255
258
  const add = render.addKey(artifactName, csn.definitions[artifactName].elements);
@@ -276,6 +279,22 @@ function toSqlDdl( csn, options, messageFunctions ) {
276
279
  mainResultObj.hdbrole[`${sqlServiceName }_access`] = JSON.stringify(accessRole, null, 2);
277
280
  });
278
281
 
282
+ // Can only happen for HDI based deployment
283
+ Object.keys(dummySqlServiceEntities).forEach((sqlServiceName) => {
284
+ const synonym = Object.create(null);
285
+ Object.entries(dummySqlServiceEntities[sqlServiceName]).forEach(([ name ]) => {
286
+ const artName = renderArtifactNameWithoutQuotes(name);
287
+ const dummyArtName = renderArtifactNameWithoutQuotes(`dummy.${ name}`);
288
+ synonym[artName] = {
289
+ target: {
290
+ object: dummyArtName,
291
+ },
292
+ };
293
+ });
294
+
295
+ mainResultObj.hdbsynonym[`${sqlServiceName}`] = JSON.stringify(synonym, null, 2);
296
+ });
297
+
279
298
  // trigger artifact and element name checks
280
299
  definitionsDuplicateChecker.check(error, options);
281
300
  extensionsDuplicateChecker.check(error);
@@ -340,8 +359,14 @@ function toSqlDdl( csn, options, messageFunctions ) {
340
359
  function renderDefinitionInto( artifactName, art, resultObj, env ) {
341
360
  env.path = [ 'definitions', artifactName ];
342
361
  // Ignore whole artifacts if forRelationalDB says so
343
- if (art.abstract || hasValidSkipOrExists(art))
362
+ if (art.abstract || hasValidSkipOrExists(art)) {
363
+ if (art.$dummyService) { // collect entities that are in an external ABAP sql service so we can render the .hdbsynonym later
364
+ dummySqlServiceEntities[art.$dummyService] ??= Object.create(null);
365
+ dummySqlServiceEntities[art.$dummyService][artifactName] = art;
366
+ }
367
+
344
368
  return;
369
+ }
345
370
 
346
371
  switch (art.kind) {
347
372
  case 'entity':
@@ -349,6 +374,7 @@ function toSqlDdl( csn, options, messageFunctions ) {
349
374
  sqlServiceEntities[art.$sqlService] ??= Object.create(null);
350
375
  sqlServiceEntities[art.$sqlService][artifactName] = art;
351
376
  }
377
+
352
378
  if (art.query || art.projection) {
353
379
  const result = renderView(artifactName, art, env);
354
380
  if (result)
@@ -587,13 +613,12 @@ function toSqlDdl( csn, options, messageFunctions ) {
587
613
  */
588
614
  function renderEntityInto( artifactName, art, resultObj, env ) {
589
615
  const childEnv = env.withIncreasedIndent();
590
- const hanaTc = art.technicalConfig && art.technicalConfig.hana;
591
616
  // tables can have @sql.prepend and @sql.append
592
617
  const { front, back } = getSqlSnippets(options, art);
593
618
  let result = front;
594
619
  // Only SAP HANA has row/column tables
595
620
  if (options.sqlDialect === 'hana') {
596
- if (hanaTc && hanaTc.storeType) {
621
+ if (art.technicalConfig?.hana?.storeType) {
597
622
  // Explicitly specified
598
623
  result += `${art.technicalConfig.hana.storeType.toUpperCase()} `;
599
624
  }
@@ -607,7 +632,7 @@ function toSqlDdl( csn, options, messageFunctions ) {
607
632
  result += `TABLE ${tableName}`;
608
633
  result += ' (\n';
609
634
  result += Object.keys(art.elements)
610
- .map(eltName => renderElement(eltName, art.elements[eltName], definitionsDuplicateChecker, getFzIndex(eltName, hanaTc), childEnv))
635
+ .map(eltName => renderElement(eltName, art.elements[eltName], definitionsDuplicateChecker, getFzIndex(eltName, art.technicalConfig?.hana), childEnv))
611
636
  .filter(s => s !== '')
612
637
  .join(',\n');
613
638
 
@@ -643,7 +668,7 @@ function toSqlDdl( csn, options, messageFunctions ) {
643
668
  // Append table constraints if any
644
669
  // 'CONSTRAINT <name> UNIQUE (<column_list>)
645
670
  // OR create a unique index for HDI
646
- const uniqueConstraints = art.$tableConstraints && art.$tableConstraints.unique;
671
+ const uniqueConstraints = art.$tableConstraints?.unique;
647
672
  for (const cn in uniqueConstraints) {
648
673
  const constraint = renderUniqueConstraintString(uniqueConstraints[cn], renderArtifactName(`${artifactName}_${cn}`), tableName, quoteSqlId, options);
649
674
  if (options.src === 'hdi')
@@ -670,7 +695,7 @@ function toSqlDdl( csn, options, messageFunctions ) {
670
695
  // Only HANA has indices
671
696
  // FIXME: Really? We should provide a DB-agnostic way to specify that
672
697
  if (options.sqlDialect === 'hana')
673
- renderIndexesInto(art.technicalConfig && art.technicalConfig.hana.indexes, artifactName, resultObj, env);
698
+ renderIndexesInto(art.technicalConfig?.hana?.indexes, artifactName, resultObj, env);
674
699
 
675
700
  if (options.sqlDialect === 'hana' && hasHanaComment(art, options))
676
701
  result += ` COMMENT ${renderStringForSql(getHanaComment(art), options.sqlDialect)}`;
@@ -746,7 +771,7 @@ function toSqlDdl( csn, options, messageFunctions ) {
746
771
  * @returns {object} fzindex for the element
747
772
  */
748
773
  function getFzIndex( elemName, hanaTc ) {
749
- if (!hanaTc || !hanaTc.fzindexes || !hanaTc.fzindexes[elemName])
774
+ if (!hanaTc?.fzindexes?.[elemName])
750
775
  return undefined;
751
776
 
752
777
  if (Array.isArray(hanaTc.fzindexes[elemName][0])) {
@@ -823,12 +848,12 @@ function toSqlDdl( csn, options, messageFunctions ) {
823
848
  if (elm.target) {
824
849
  result += env.indent;
825
850
  if (elm.cardinality) {
826
- if (isBetaEnabled(options, 'hanaAssocRealCardinality') && elm.cardinality.src && elm.cardinality.src === 1)
851
+ if (isBetaEnabled(options, 'hanaAssocRealCardinality') && elm.cardinality.src === 1)
827
852
  result += 'ONE TO ';
828
853
  else
829
854
  result += 'MANY TO ';
830
855
 
831
- if (elm.cardinality.max && (elm.cardinality.max === '*' || Number(elm.cardinality.max) > 1))
856
+ if (elm.cardinality.max === '*' || Number(elm.cardinality.max) > 1)
832
857
  result += 'MANY';
833
858
  else
834
859
  result += 'ONE';
@@ -1002,11 +1027,11 @@ function toSqlDdl( csn, options, messageFunctions ) {
1002
1027
  function renderJoinCardinality( card ) {
1003
1028
  let result = '';
1004
1029
  if (card) {
1005
- if (card.srcmin && card.srcmin === 1)
1030
+ if (card.srcmin === 1)
1006
1031
  result += 'EXACT ';
1007
- result += card.src && card.src === 1 ? 'ONE ' : 'MANY ';
1032
+ result += card.src === 1 ? 'ONE ' : 'MANY ';
1008
1033
  result += 'TO ';
1009
- if (card.min && card.min === 1)
1034
+ if (card.min === 1)
1010
1035
  result += 'EXACT ';
1011
1036
  if (card.max)
1012
1037
  result += (card.max === 1) ? 'ONE ' : 'MANY ';
@@ -1080,7 +1105,7 @@ function toSqlDdl( csn, options, messageFunctions ) {
1080
1105
  // the ref is not rendered as { id: ...; args: } but as short form of ref[0] ;)
1081
1106
  // An empty actual parameter list is rendered as `()`.
1082
1107
  const ref = csn.definitions[path.ref[0].id] || csn.definitions[path.ref[0]];
1083
- if (ref && ref.params) {
1108
+ if (ref?.params) {
1084
1109
  result += path.ref[0]?.args
1085
1110
  ? `(${renderArgs(path.ref[0], '=>', env.withSubPath([ 'ref', 0 ]), syntax)})`
1086
1111
  : '()';
@@ -1156,8 +1181,8 @@ function toSqlDdl( csn, options, messageFunctions ) {
1156
1181
  */
1157
1182
  function renderViewColumn( col, elements, env ) {
1158
1183
  let result = '';
1159
- const leaf = col.as || col.ref && col.ref[col.ref.length - 1] || col.func;
1160
- if (leaf && elements[leaf] && elements[leaf].virtual) {
1184
+ const leaf = col.as || col.ref?.[col.ref.length - 1] || col.func;
1185
+ if (leaf && elements[leaf]?.virtual) {
1161
1186
  if (isDeprecatedEnabled(options, '_renderVirtualElements'))
1162
1187
  // render a virtual column 'null as <alias>'
1163
1188
  result += `${env.indent}NULL AS ${quoteSqlId(col.as || leaf)}`;
@@ -1182,7 +1207,7 @@ function toSqlDdl( csn, options, messageFunctions ) {
1182
1207
  */
1183
1208
  function renderView( artifactName, art, env ) {
1184
1209
  const viewName = renderArtifactName(artifactName);
1185
- definitionsDuplicateChecker.addArtifact(art['@cds.persistence.name'], art && art.$location, artifactName);
1210
+ definitionsDuplicateChecker.addArtifact(art['@cds.persistence.name'], art?.$location, artifactName);
1186
1211
  let result = `VIEW ${viewName}`;
1187
1212
 
1188
1213
  if (options.sqlDialect === 'hana' && hasHanaComment(art, options))
@@ -1268,9 +1293,10 @@ function toSqlDdl( csn, options, messageFunctions ) {
1268
1293
  // - has an ORDER BY/LIMIT (because UNION etc. can't stand directly behind an ORDER BY)
1269
1294
  const argEnv = env.withSubPath([ 'args', index ]);
1270
1295
  const queryString = renderQuery( arg, argEnv, elements || query.SET.elements, false);
1271
- return (arg.SET || arg.SELECT && (arg.SELECT.orderBy || arg.SELECT.limit)) ? `(${queryString})` : queryString;
1296
+ return (arg.SET || arg.SELECT?.orderBy || arg.SELECT?.limit) ? `(${queryString})` : queryString;
1272
1297
  })
1273
- .join(`\n${env.indent}${query.SET.op && query.SET.op.toUpperCase()}${query.SET.all ? ' ALL ' : ' '}`);
1298
+ .join(`\n${env.indent}${query.SET.op?.toUpperCase()}${query.SET.all ? ' ALL ' : ' '}`);
1299
+
1274
1300
  // Set operation may also have an ORDER BY and LIMIT/OFFSET (in contrast to the ones belonging to
1275
1301
  // each SELECT)
1276
1302
  // If the whole SET has an ORDER BY/LIMIT, wrap the part before that in parentheses
@@ -1278,12 +1304,16 @@ function toSqlDdl( csn, options, messageFunctions ) {
1278
1304
  // to the last SET argument, not to the whole SET)
1279
1305
  if (query.SET.orderBy || query.SET.limit) {
1280
1306
  result = `(${result})`;
1281
- if (query.SET.orderBy)
1282
- result += `\n${env.indent}ORDER BY ${query.SET.orderBy.map(entry => renderOrderByEntry(entry, env.withSubPath([ 'orderBy' ]))).join(', ')}`;
1283
-
1284
- if (query.SET.limit)
1285
- result += `\n${env.indent}${renderLimit(query.SET.limit, env.withSubPath([ 'limit' ]))}`;
1307
+ if (query.SET.orderBy) {
1308
+ const orderBy = query.SET.orderBy.map(entry => renderOrderByEntry(entry, env.withSubPath([ 'orderBy' ]))).join(', ');
1309
+ result += `\n${env.indent}ORDER BY ${orderBy}`;
1310
+ }
1311
+ if (query.SET.limit) {
1312
+ const limit = renderLimit(query.SET.limit, env.withSubPath([ 'limit' ]));
1313
+ result += `\n${env.indent}${limit}`;
1314
+ }
1286
1315
  }
1316
+
1287
1317
  return result;
1288
1318
  }
1289
1319
  // Otherwise must have a SELECT
@@ -1332,7 +1362,7 @@ function toSqlDdl( csn, options, messageFunctions ) {
1332
1362
  * @returns {string|undefined} Id of first path step
1333
1363
  */
1334
1364
  function firstPathStepId( ref ) {
1335
- return ref && ref[0] && (ref[0].id || ref[0]);
1365
+ return (ref?.[0]?.id || ref?.[0]);
1336
1366
  }
1337
1367
 
1338
1368
  /**
@@ -1431,7 +1461,7 @@ function toSqlDdl( csn, options, messageFunctions ) {
1431
1461
  */
1432
1462
  function renderBuiltinType( typeName ) {
1433
1463
  const types = cdsToSqlTypes[options.sqlDialect];
1434
- const result = types && types[typeName] || cdsToSqlTypes.standard[typeName];
1464
+ const result = types?.[typeName] || cdsToSqlTypes.standard[typeName];
1435
1465
  if (!result && options.testMode)
1436
1466
  throw new CompilerAssertion(`Expected to find a type mapping for ${typeName}`);
1437
1467
  return result || 'CHAR';
@@ -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
  });