UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

826 lines (804 loc) 35.3 kB
'use strict'; /** * List of functions for which we provide a mapping to the respective SQL dialect. * All functions are lowercase, the caller may treat the function name case-insensitive. * * The `this` context within the functions hold the `renderArgs` function. */ const oDataFunctions = { // https://www.sqlite.org/lang_corefunc.html sqlite: { contains(signature) { const { args } = signature; checkArgs.call(this, 'contains', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `(ifnull(instr(${ x }, ${ y }),0) <> 0)`; }, startswith(signature) { const { args } = signature; checkArgs.call(this, 'startswith', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `coalesce((instr(${ x }, ${ y }) = 1), false)`; }, // instr is 1 indexed endswith(signature) { const { args } = signature; checkArgs.call(this, 'endswith', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `coalesce((substr(${ x }, length(${ x }) + 1 - length(${ y })) = ${ y }), false)`; }, indexof(signature) { const { args } = signature; checkArgs.call(this, 'indexof', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `(instr(${ x }, ${ y }) - 1)`; // instr is 1 indexed }, matchespattern(signature) { const { args } = signature; checkArgs.call(this, 'matchespattern', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `cast((${ x } regexp ${ y }) as INTEGER)`; // this is a udf, sqlite always returns a REAL w/o the cast }, matchesPattern(signature) { return oDataFunctions.sqlite.matchespattern.call(this, signature); }, year(signature) { const { args } = signature; checkArgs.call(this, 'year', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `cast(strftime('%Y', ${ x }) as Integer)`; }, month(signature) { const { args } = signature; checkArgs.call(this, 'month', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `cast(strftime('%m', ${ x }) as Integer)`; }, day(signature) { const { args } = signature; checkArgs.call(this, 'day', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `cast(strftime('%d', ${ x }) as Integer)`; }, hour(signature) { const { args } = signature; checkArgs.call(this, 'hour', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `cast(strftime('%H', ${ x }) as Integer)`; }, minute(signature) { const { args } = signature; checkArgs.call(this, 'minute', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `cast(strftime('%M', ${ x }) as Integer)`; }, second(signature) { const { args } = signature; checkArgs.call(this, 'second', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `cast(strftime('%S', ${ x }) as Integer)`; }, // REVISIT: currently runtimes normalize to milliseconds // we could allow this to be more precise fractionalseconds(signature) { const { args } = signature; checkArgs.call(this, 'fractionalseconds', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `cast(substr(strftime('%f', ${ x }), length(strftime('%f', ${ x })) - 3) as REAL)`; }, // The date(), time(), and datetime() functions all return text, and so their strftime() equivalents are exact. time(signature) { const { args } = signature; checkArgs.call(this, 'time', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `time(${ x })`; }, date(signature) { const { args } = signature; checkArgs.call(this, 'date', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `date(${ x })`; }, // this could also be a negative number // also, parts of the EDM.duration are optional which complicates // the implementation on SQL level. As the parameter may be an element // reference, we must do splitting and casting in the SQL as well as // considering the case where the duration is negative. // --> We do not support this function. // totalseconds(signature) { // const { args } = signature; // checkArgs.call(this, 'totalseconds', args, 1); // let x = this.renderArgs({ ...signature, args: [ args[0] ] }); // const isNegative = x.startsWith("'-"); // Check for leading '-' // x = isNegative ? x.replace('-', '') : x; // remove for easier processing // const sql = `((cast(substr(${x},2,instr(${x},'DT') - 2) as Integer) + (julianday('-4713-11-25T' || replace(replace(replace(substr(${x},instr(${x},'DT') + 2),'H',':'),'M',':'),'S','Z')) - 0.5)) * 86400)`; // return isNegative ? `-(${sql})` : sql; // }, }, // https://www.postgresql.org/docs/current/functions-string.html // https://www.postgresql.org/docs/current/functions-math.html postgres: { contains(signature) { const { args } = signature; checkArgs.call(this, 'contains', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `(coalesce(strpos(${ x }, ${ y }),0) > 0)`; }, startswith(signature) { const { args } = signature; checkArgs.call(this, 'startswith', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `coalesce((strpos(${ x }, ${ y }) = 1), false)`; // strpos is 1 indexed }, endswith(signature) { const { args } = signature; checkArgs.call(this, 'endswith', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `coalesce((substr(${ x }, (length(${ x }) + 1) - length(${ y })) = ${ y }), false)`; }, indexof(signature) { const { args } = signature; checkArgs.call(this, 'indexof', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `(strpos(${ x }, ${ y }) - 1)`; // strpos is 1 indexed }, matchespattern(signature) { const { args } = signature; checkArgs.call(this, 'matchespattern', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `regexp_like(${ x }, ${ y })`; }, matchesPattern(signature) { return oDataFunctions.postgres.matchespattern.call(this, signature); }, // TODO: PG docu recommends to use the "EXTRACT" function for improved precision // https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT year(signature) { const { args } = signature; checkArgs.call(this, 'year', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `cast(date_part('year', ${ x }) as Integer)`; }, month(signature) { const { args } = signature; checkArgs.call(this, 'month', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `cast(date_part('month', ${ x }) as Integer)`; }, day(signature) { const { args } = signature; checkArgs.call(this, 'day', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `cast(date_part('day', ${ x }) as Integer)`; }, hour(signature) { const { args } = signature; checkArgs.call(this, 'hour', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `cast(date_part('hour', ${ x }) as Integer)`; }, minute(signature) { const { args } = signature; checkArgs.call(this, 'minute', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `cast(date_part('minute', ${ x }) as Integer)`; }, second(signature) { const { args } = signature; checkArgs.call(this, 'second', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `cast(floor(date_part('second', ${ x })) as Integer)`; }, // REVISIT: currently runtimes normalize to milliseconds // we could allow this to be more precise fractionalseconds(signature) { const { args } = signature; checkArgs.call(this, 'fractionalseconds', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `cast(date_part('second', ${ x }) - floor(date_part('second', ${ x })) AS DECIMAL(3,3))`; }, time(signature) { const { args } = signature; checkArgs.call(this, 'time', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `to_char(${ x }, 'HH24:MI:SS')::TIME`; }, date(signature) { const { args } = signature; checkArgs.call(this, 'date', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `${ x }::DATE`; }, }, // https://help.sap.com/docs/HANA_SERVICE_CF/7c78579ce9b14a669c1f3295b0d8ca16/f12b86a6284c4aeeb449e57eb5dd3ebd.html?locale=en-US hana: { contains(signature) { const { args } = signature; checkArgs.call(this, 'contains', args, 2, 3); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); if (signature.args.length > 2) { const z = this.renderArgs({ ...signature, args: [ args[2] ] }); // While CONTAINS() looks like a function because of its syntax, // it is classified as a predicate because it is designed to evaluate a condition // and return a Boolean result. return `CONTAINS(${ x }, ${ y }, ${ z })`; } return `(CASE WHEN coalesce(locate(${ this.renderArgs(signature) }),0)>0 THEN TRUE ELSE FALSE END)`; }, startswith(signature) { const { args } = signature; checkArgs.call(this, 'startswith', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `(CASE WHEN locate(${ x }, ${ y }) = 1 THEN TRUE ELSE FALSE END)`; }, // locate is 1 indexed endswith(signature) { const { args } = signature; checkArgs.call(this, 'endswith', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `(CASE WHEN substring(${ x }, (length(${ x }) + 1) - length(${ y })) = ${ y } THEN TRUE ELSE FALSE END)`; }, indexof(signature) { const { args } = signature; checkArgs.call(this, 'indexof', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `(locate(${ x }, ${ y }) - 1)`; // locate is 1 indexed }, matchespattern(signature) { // case … when only works as column expression (not in where) // in the where clause, only "${x} LIKE_REGEXPR ${y}" works const { args } = signature; checkArgs.call(this, 'matchespattern', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `(CASE WHEN ${ x } LIKE_REGEXPR ${ y } THEN TRUE ELSE FALSE END)`; }, matchesPattern(signature) { return oDataFunctions.hana.matchespattern.call(this, signature); }, year(signature) { const { args } = signature; checkArgs.call(this, 'year', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `year(${ x })`; }, month(signature) { const { args } = signature; checkArgs.call(this, 'month', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `month(${ x })`; }, day(signature) { const { args } = signature; checkArgs.call(this, 'day', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `dayofmonth(${ x })`; }, hour(signature) { const { args } = signature; checkArgs.call(this, 'hour', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `hour(${ x })`; }, minute(signature) { const { args } = signature; checkArgs.call(this, 'minute', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `minute(${ x })`; }, second(signature) { const { args } = signature; checkArgs.call(this, 'second', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `to_integer(second(${ x }))`; }, // REVISIT: currently runtimes normalize to milliseconds // we could allow this to be more precise fractionalseconds(signature) { const { args } = signature; checkArgs.call(this, 'fractionalseconds', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `(to_decimal(second(${ x }),5,3) - to_integer(second(${ x })))`; }, time(signature) { const { args } = signature; checkArgs.call(this, 'time', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `to_time(${ x })`; }, date(signature) { const { args } = signature; checkArgs.call(this, 'date', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `to_date(${ x })`; }, }, // https://www.h2database.com/html/functions.html h2: { contains(signature) { const args = [ ...signature.args ]; checkArgs.call(this, 'contains', args, 2); // defined as { LOCATE(searchString, string [, startInt]) } args.reverse(); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `(coalesce(locate(${ x }, ${ y }),0) > 0)`; }, startswith(signature) { const args = [ ...signature.args ]; checkArgs.call(this, 'startswith', args, 2); // defined as { LOCATE(searchString, string [, startInt]) } args.reverse(); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `coalesce((locate(${ x }, ${ y }) = 1), false)`; // locate is 1 indexed }, endswith(signature) { const { args } = signature; checkArgs.call(this, 'endswith', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `coalesce((substring(${ x } FROM (char_length(${ x }) + 1) - char_length(${ y })) = ${ y }), false)`; }, substring(signature) { const { args } = signature; checkArgs.call(this, 'substring', args, 2, 3); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); const z = args[2] ? this.renderArgs({ ...signature, args: [ args[2] ] }) : null; return z ? `substring(${ x } FROM CASE WHEN ${ y } < 0 THEN char_length(${ x }) + ${ y } + 1 ELSE ${ y } + 1 END FOR ${ z })` : `substring(${ x } FROM CASE WHEN ${ y } < 0 THEN char_length(${ x }) + ${ y } + 1 ELSE ${ y } + 1 END)`; }, // char_length is preferred over length -> REVISIT: returns a BIGINT, is this ok? // https://www.h2database.com/html/functions.html#char_length length(signature) { const { args } = signature; checkArgs.call(this, 'length', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `cast(char_length(${ x }) as Integer)`; }, indexof(signature) { const args = [ ...signature.args ]; checkArgs.call(this, 'indexof', args, 2); // defined as { LOCATE(searchString, string [, startInt]) } args.reverse(); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `(locate(${ x }, ${ y }) - 1)`; // locate is 1 indexed }, matchespattern(signature) { const { args } = signature; checkArgs.call(this, 'matchespattern', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `regexp_like(${ x }, ${ y })`; }, matchesPattern(signature) { return oDataFunctions.h2.matchespattern.call(this, signature); }, year(signature) { const { args } = signature; checkArgs.call(this, 'year', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `extract(YEAR FROM ${ x })`; }, month(signature) { const { args } = signature; checkArgs.call(this, 'month', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `extract(MONTH FROM ${ x })`; }, day(signature) { const { args } = signature; checkArgs.call(this, 'day', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `extract(DAY FROM ${ x })`; }, hour(signature) { const { args } = signature; checkArgs.call(this, 'hour', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `extract(HOUR FROM ${ x })`; }, minute(signature) { const { args } = signature; checkArgs.call(this, 'minute', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `extract(MINUTE FROM ${ x })`; }, second(signature) { const { args } = signature; checkArgs.call(this, 'second', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `extract(SECOND FROM ${ x })`; }, // REVISIT: currently runtimes normalize to milliseconds // we could allow this to be more precise fractionalseconds(signature) { const { args } = signature; checkArgs.call(this, 'fractionalseconds', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `cast(extract(MILLISECOND FROM ${ x }) / 1000.0 AS NUMERIC(3,3))`; }, time(signature) { const { args } = signature; checkArgs.call(this, 'time', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `cast(${ x } AS TIME)`; }, date(signature) { const { args } = signature; checkArgs.call(this, 'date', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `cast(${ x } AS DATE)`; }, }, common: { concat(signature) { const separator = '||'; const args = signature.args.reduce((acc, current, index) => { if (index > 0) acc.push(separator); acc.push(current); return acc; }, []); const res = this.renderArgs({ signature, ...{ args: [ args ] } }); return `(${ res })`; }, ceiling(signature) { const { args } = signature; checkArgs.call(this, 'ceiling', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `ceil(${ x })`; }, floor(signature) { const { args } = signature; checkArgs.call(this, 'floor', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `floor(${ x })`; }, trim(signature) { const { args } = signature; checkArgs.call(this, 'trim', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `trim(${ x })`; }, // SAP HANA, sqlite and postgres share the same implementation substring(signature) { const { args } = signature; checkArgs.call(this, 'substring', args, 2, 3); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); const z = args[2] ? this.renderArgs({ ...signature, args: [ args[2] ] }) : null; return z ? `substr(${ x }, CASE WHEN ${ y } < 0 THEN length(${ x }) + ${ y } + 1 ELSE ${ y } + 1 END, ${ z })` : `substr(${ x }, CASE WHEN ${ y } < 0 THEN length(${ x }) + ${ y } + 1 ELSE ${ y } + 1 END)`; }, min(signature) { const { args } = signature; checkArgs.call(this, 'min', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `min(${ x })`; }, max(signature) { const { args } = signature; checkArgs.call(this, 'max', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `max(${ x })`; }, sum(signature) { const { args } = signature; checkArgs.call(this, 'sum', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `sum(${ x })`; }, count(signature) { const { args } = signature; const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `count(${ x || '*' })`; }, countdistinct(signature) { const { args } = signature; return `count(distinct ${ args.length > 0 ? this.renderArgs(signature) : "'*'" })`; }, average(signature) { const { args } = signature; checkArgs.call(this, 'average', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `avg(${ x })`; }, length(signature) { const { args } = signature; checkArgs.call(this, 'length', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `length(${ x })`; }, tolower(signature) { const { args } = signature; checkArgs.call(this, 'tolower', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `lower(${ x })`; }, toupper(signature) { const { args } = signature; checkArgs.call(this, 'toupper', args, 1); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); return `upper(${ x })`; }, // eslint-disable-next-line no-unused-vars maxdatetime(signature) { return "'9999-12-31T23:59:59.999Z'"; }, // eslint-disable-next-line no-unused-vars mindatetime(signature) { return "'0001-01-01T00:00:00.000Z'"; }, }, }; const hanaFunctions = { sqlite: { /** * SQLite relies on floating-point arithmetic for date/time calculations, which can introduce * slight imprecisions due to the use of the `julianday` function. The `julianday` function * computes the difference between two timestamps as a floating-point value in days, which * is then scaled to nano100 units (0.1 microseconds). While this approach is efficient, * the inherent precision limits of floating-point arithmetic can result in small deviations * (e.g., off by a few nano100 units). * * @param {Object} signature - The function signature containing arguments. * @returns {string} - SQL expression to calculate the nano100 difference in SQLite. */ nano100_between(signature) { const { args } = signature; checkArgs.call(this, 'nano100_between', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); // 1 day = 24h*60m*60s*10'000'000 = 864'000'000'000 nano100 return `CAST(((julianday(${ y }) - julianday(${ x })) * 864000000000) as INTEGER)`; }, seconds_between(signature) { const { args } = signature; checkArgs.call(this, 'seconds_between', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `CAST(strftime('%s', ${ y }) - strftime('%s', ${ x }) AS INTEGER)`; }, days_between(signature) { const { args } = signature; checkArgs.call(this, 'days_between', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); 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)`; }, /** * Calculates the difference in months between two dates, `x` and `y`, with a correction for partial months. * * The computation consists of: * * 1. Year/Month Difference: * - Extracts the year and month parts from both dates and computes a raw difference: * (year(y) - year(x)) * 12 + (month(y) - month(x)). * * 2. Partial-Month Correction: * - Generates a composite value of day and time components from each date using: * strftime('%d%H%M%S%f0000', date) * This zero-padded composite includes day, hour, minute, second, and fractional seconds. * - For a forward interval (when y is after or equal to x): * If the composite for y is less than that for x, then the final month is incomplete, so subtract 1. * - For a backward interval (when y is before x): * If the composite for y is greater than that for x, then the final month is incomplete, so add 1. * * 3. Leap-Year Adjustment: * - The composite value inherently captures all day/time details (including the leap day, Feb 29), * so the extra day in a leap year is automatically accounted for in the partial-month correction. * * @param {object} signature - Contains the function arguments. * @returns {string} A SQL expression that calculates the adjusted month difference. */ months_between(signature) { // Ensure exactly two arguments (startDate, endDate) checkArgs.call(this, 'months_between', signature.args, 2); // Render the arguments as SQL expressions. const x = this.renderArgs({ ...signature, args: [ signature.args[0] ] }); const y = this.renderArgs({ ...signature, args: [ signature.args[1] ] }); // Construct the SQL expression: // 1. Base month difference from the year and month components. // 2. Partial-month correction using a composite integer of day and time. const res = ` ( ( (CAST(strftime('%Y', ${ y }) AS Integer) - CAST(strftime('%Y', ${ x }) AS Integer)) * 12 ) + ( CAST(strftime('%m', ${ y }) AS Integer) - CAST(strftime('%m', ${ x }) AS Integer) ) + ( CASE /* For backward intervals: if the composite (day + time) of y is greater than x, add 1. */ WHEN CAST(strftime('%Y%m', ${ y }) AS Integer) < CAST(strftime('%Y%m', ${ x }) AS Integer) THEN (CAST(strftime('%d%H%M%S%f0000', ${ y }) AS Integer) > CAST(strftime('%d%H%M%S%f0000', ${ x }) AS Integer)) /* For forward intervals: if the composite of y is less than x, subtract 1. */ ELSE (CAST(strftime('%d%H%M%S%f0000', ${ y }) AS Integer) < CAST(strftime('%d%H%M%S%f0000', ${ x }) AS Integer)) * -1 END ) ) `; // Remove extra whitespace and return the single-line SQL expression. return res.replace(/\s+/g, ' '); }, years_between(signature) { const { args } = signature; checkArgs.call(this, 'years_between', args, 2); return `floor((${ hanaFunctions.sqlite.months_between.call(this, signature) }) / 12)`; }, }, postgres: { nano100_between(signature) { const { args } = signature; checkArgs.call(this, 'nano100_between', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); // make sure to cast NUMERIC to BIGINT (corresponds to cds.Int64) return `(EXTRACT(EPOCH FROM (${ y }) - (${ x })) * 10000000)::BIGINT`; }, seconds_between(signature) { const { args } = signature; checkArgs.call(this, 'seconds_between', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `EXTRACT(EPOCH FROM (${ y }) - (${ x }))::BIGINT`; }, days_between(signature) { const { args } = signature; checkArgs.call(this, 'days_between', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `EXTRACT(DAY FROM ${ y }::timestamp - ${ x }::timestamp)::integer`; }, months_between(signature) { const { args } = signature; checkArgs.call(this, 'months_between', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `(EXTRACT(YEAR FROM AGE(${ y }, ${ x })) * 12 + EXTRACT(MONTH FROM AGE(${ y }, ${ x })))::INTEGER`; }, years_between(signature) { const { args } = signature; checkArgs.call(this, 'years_between', args, 2); return `floor((${ hanaFunctions.postgres.months_between.call(this, signature) }) / 12)::INTEGER`; }, }, h2: { nano100_between(signature) { const { args } = signature; checkArgs.call(this, 'nano100_between', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `CAST(DATEDIFF('MICROSECOND', ${ x }, ${ y }) * 10 AS BIGINT)`; }, seconds_between(signature) { const { args } = signature; checkArgs.call(this, 'seconds_between', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `CAST(DATEDIFF('SECOND', ${ x }, ${ y }) AS BIGINT)`; }, days_between(signature) { const { args } = signature; checkArgs.call(this, 'days_between', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `CASE WHEN ABS(DATEDIFF('SECOND', ${ x }, ${ y })) < 86400 THEN 0 ELSE CAST(FLOOR(DATEDIFF('SECOND', ${ x }, ${ y }) / 86400) AS INTEGER) END`; }, /** * Uses DATEDIFF('MONTH') and then applies a partial-month correction for day-of-month boundaries in both * forward and backward (negative) scenarios. */ months_between(signature) { const { args } = signature; checkArgs.call(this, 'months_between', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); const res = ` CAST( DATEDIFF('MONTH', ${ x }, ${ y }) + CASE WHEN DATEDIFF('DAY', ${ x }, ${ y }) >= 0 AND EXTRACT(DAY FROM ${ y }) < EXTRACT(DAY FROM ${ x }) THEN -1 WHEN DATEDIFF('DAY', ${ x }, ${ y }) < 0 AND EXTRACT(DAY FROM ${ y }) > EXTRACT(DAY FROM ${ x }) THEN 1 ELSE 0 END AS INTEGER ) `; return res.replace(/\s+/g, ' '); }, years_between(signature) { const { args } = signature; checkArgs.call(this, 'years_between', args, 2); return `floor((${ hanaFunctions.h2.months_between.call(this, signature) }) / 12)`; }, }, common: {}, // identity functions + argument check hana: { nano100_between(signature) { const { args } = signature; checkArgs.call(this, 'nano100_between', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `nano100_between(${ x }, ${ y })`; }, seconds_between(signature) { const { args } = signature; checkArgs.call(this, 'seconds_between', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `seconds_between(${ x }, ${ y })`; }, days_between(signature) { const { args } = signature; checkArgs.call(this, 'days_between', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `days_between(${ x }, ${ y })`; }, months_between(signature) { const { args } = signature; checkArgs.call(this, 'months_between', args, 2); const x = this.renderArgs({ ...signature, args: [ args[0] ] }); const y = this.renderArgs({ ...signature, args: [ args[1] ] }); return `months_between(${ x }, ${ y })`; }, years_between(signature) { const { args } = signature; checkArgs.call(this, 'years_between', args, 2); return `years_between(${ this.renderArgs(signature) })`; }, }, }; function checkArgs( funcName, receivedArgs, expectedLength, alternativeLength = null ) { const expectedMismatch = receivedArgs.length < expectedLength; const alternativeMismatch = expectedMismatch && (!alternativeLength || (alternativeLength && receivedArgs.length < alternativeLength)); if (expectedMismatch && alternativeMismatch) { this.error('def-missing-argument', [ ...this.path, 'args' ], { '#': alternativeLength ? 'alternative' : 'std', n: expectedLength, m: alternativeLength, literal: receivedArgs.length, name: funcName, }); } } module.exports.standardDatabaseFunctions = { sqlite: { ...oDataFunctions.sqlite, ...hanaFunctions.sqlite }, postgres: { ...oDataFunctions.postgres, ...hanaFunctions.postgres }, hana: { ...oDataFunctions.hana, ...hanaFunctions.hana }, h2: { ...oDataFunctions.h2, ...hanaFunctions.h2 }, common: { ...oDataFunctions.common }, };