@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.
- package/CHANGELOG.md +62 -2
- package/bin/cdsse.js +13 -1
- package/doc/CHANGELOG_BETA.md +7 -0
- package/lib/api/options.js +2 -1
- package/lib/api/validate.js +9 -0
- package/lib/base/location.js +1 -1
- package/lib/base/message-registry.js +55 -20
- package/lib/base/messages.js +5 -2
- package/lib/base/model.js +8 -6
- package/lib/checks/assocOutsideService.js +40 -0
- package/lib/checks/featureFlags.js +4 -1
- package/lib/checks/types.js +7 -4
- package/lib/checks/validator.js +3 -0
- package/lib/compiler/assert-consistency.js +11 -5
- package/lib/compiler/checks.js +79 -17
- package/lib/compiler/define.js +60 -3
- package/lib/compiler/extend.js +1 -2
- package/lib/compiler/generate.js +1 -1
- package/lib/compiler/populate.js +17 -6
- package/lib/compiler/propagator.js +1 -1
- package/lib/compiler/resolve.js +181 -150
- package/lib/compiler/shared.js +276 -22
- package/lib/compiler/tweak-assocs.js +15 -4
- package/lib/compiler/xpr-rewrite.js +76 -50
- package/lib/edm/annotations/edmJson.js +1 -1
- package/lib/edm/annotations/genericTranslation.js +2 -2
- package/lib/edm/csn2edm.js +2 -2
- package/lib/edm/edmPreprocessor.js +15 -9
- package/lib/edm/edmUtils.js +12 -5
- package/lib/gen/CdlGrammar.checksum +1 -0
- package/lib/gen/CdlParser.js +2239 -2229
- package/lib/gen/Dictionary.json +55 -8
- package/lib/json/from-csn.js +37 -17
- package/lib/json/to-csn.js +4 -0
- package/lib/language/genericAntlrParser.js +7 -0
- package/lib/main.d.ts +5 -1
- package/lib/model/cloneCsn.js +1 -0
- package/lib/model/csnRefs.js +1 -0
- package/lib/model/csnUtils.js +0 -5
- package/lib/modelCompare/utils/filter.js +2 -2
- package/lib/optionProcessor.js +2 -0
- package/lib/parsers/AstBuildingParser.js +72 -34
- package/lib/parsers/CdlGrammar.g4 +20 -19
- package/lib/parsers/XprTree.js +206 -0
- package/lib/parsers/index.js +1 -1
- package/lib/render/toCdl.js +61 -89
- package/lib/render/toSql.js +59 -29
- package/lib/render/utils/standardDatabaseFunctions.js +252 -15
- package/lib/transform/addTenantFields.js +9 -3
- package/lib/transform/db/assocsToQueries/transformExists.js +3 -0
- package/lib/transform/db/assocsToQueries/utils.js +10 -3
- package/lib/transform/db/expansion.js +3 -1
- package/lib/transform/db/flattening.js +7 -3
- package/lib/transform/db/killAnnotations.js +1 -0
- package/lib/transform/db/processSqlServices.js +70 -17
- package/lib/transform/draft/db.js +8 -3
- package/lib/transform/draft/odata.js +27 -4
- package/lib/transform/effective/main.js +37 -10
- package/lib/transform/effective/misc.js +4 -9
- package/lib/transform/effective/service.js +34 -0
- package/lib/transform/effective/types.js +28 -17
- package/lib/transform/forOdata.js +36 -10
- package/lib/transform/forRelationalDB.js +30 -18
- package/lib/transform/odata/adaptAnnotationRefs.js +37 -21
- package/lib/transform/odata/createForeignKeys.js +120 -116
- package/lib/transform/odata/flattening.js +10 -8
- package/lib/transform/transformUtils.js +58 -25
- package/lib/transform/translateAssocsToJoins.js +10 -6
- package/lib/transform/universalCsn/coreComputed.js +5 -1
- package/package.json +1 -1
- package/share/messages/message-explanations.json +1 -0
- package/share/messages/rewrite-not-supported.md +5 -0
- package/share/messages/rewrite-undefined-key.md +94 -0
package/lib/render/toSql.js
CHANGED
|
@@ -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 &&
|
|
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 &&
|
|
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 &&
|
|
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 &&
|
|
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 (
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1030
|
+
if (card.srcmin === 1)
|
|
1006
1031
|
result += 'EXACT ';
|
|
1007
|
-
result += card.src
|
|
1032
|
+
result += card.src === 1 ? 'ONE ' : 'MANY ';
|
|
1008
1033
|
result += 'TO ';
|
|
1009
|
-
if (card.min
|
|
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
|
|
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
|
|
1160
|
-
if (leaf && elements[leaf]
|
|
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
|
|
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
|
|
1296
|
+
return (arg.SET || arg.SELECT?.orderBy || arg.SELECT?.limit) ? `(${queryString})` : queryString;
|
|
1272
1297
|
})
|
|
1273
|
-
.join(`\n${env.indent}${query.SET.op
|
|
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
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
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
|
|
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
|
|
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 `
|
|
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
|
|
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 `
|
|
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 `
|
|
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(
|
|
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(
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
|
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 {
|
|
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
|
|
109
|
+
if (links[i].art?.target) {
|
|
110
110
|
return {
|
|
111
|
-
head: (i === 0 ? [] : xprPart.ref.slice(0, i)),
|
|
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: (
|
|
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
|
});
|