Skip to content

Commit

Permalink
fix: align time function behavior (#322)
Browse files Browse the repository at this point in the history
Co-authored-by: Patrice Bender <[email protected]>
Co-authored-by: I543501 <[email protected]>
Co-authored-by: Bob den Os <[email protected]>
Co-authored-by: Bob den Os <[email protected]>
Co-authored-by: Johannes Vogel <[email protected]>
  • Loading branch information
6 people authored Nov 17, 2023
1 parent 67b8edf commit c3ab40a
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 20 deletions.
12 changes: 6 additions & 6 deletions db-service/lib/cql-functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,37 +149,37 @@ const StandardFunctions = {
* Generates SQL statement that produces the year of a given timestamp
* @param {string} x
* @returns {string}
*/
* /
year: x => `cast( strftime('%Y',${x}) as Integer )`,
/**
* Generates SQL statement that produces the month of a given timestamp
* @param {string} x
* @returns {string}
*/
* /
month: x => `cast( strftime('%m',${x}) as Integer )`,
/**
* Generates SQL statement that produces the day of a given timestamp
* @param {string} x
* @returns {string}
*/
* /
day: x => `cast( strftime('%d',${x}) as Integer )`,
/**
* Generates SQL statement that produces the hours of a given timestamp
* @param {string} x
* @returns {string}
*/
* /
hour: x => `cast( strftime('%H',${x}) as Integer )`,
/**
* Generates SQL statement that produces the minutes of a given timestamp
* @param {string} x
* @returns {string}
*/
* /
minute: x => `cast( strftime('%M',${x}) as Integer )`,
/**
* Generates SQL statement that produces the seconds of a given timestamp
* @param {string} x
* @returns {string}
*/
* /
second: x => `cast( strftime('%S',${x}) as Integer )`,
/**
Expand Down
16 changes: 10 additions & 6 deletions postgres/lib/func.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@ const StandardFunctions = {
endswith: (x, y) => `substr(${x},length(${x}) + 1 - length(${y})) = ${y}`,

// Date and Time Functions
year: x => `date_part('year',(${x})::TIMESTAMP)`,
month: x => `date_part('month',(${x})::TIMESTAMP)`,
day: x => `date_part('day',(${x})::TIMESTAMP)`,
hour: x => `date_part('hour',(${x})::TIMESTAMP)`,
minute: x => `date_part('minute',(${x})::TIMESTAMP)`,
second: x => `date_part('second',(${x})::TIMESTAMP)`,
year: x => `date_part('year', ${castVal(x)})`,
month: x => `date_part('month', ${castVal(x)})`,
day: x => `date_part('day', ${castVal(x)})`,
hour: x => `date_part('hour', ${castVal(x)})`,
minute: x => `date_part('minute', ${castVal(x)})`,
second: x => `date_part('second', ${castVal(x)})`,
}

const isTime = /^\d{1,2}:\d{1,2}:\d{1,2}$/
const isVal = x => x && 'val' in x
const castVal = (x) => `${x}${isVal(x) ? isTime.test(x.val) ? '::TIME' : '::TIMESTAMP' : ''}`

const HANAFunctions = {
// https://help.sap.com/docs/SAP_HANA_PLATFORM/4fe29514fd584807ac9f2a04f6754767/f12b86a6284c4aeeb449e57eb5dd3ebd.html

Expand Down
23 changes: 21 additions & 2 deletions sqlite/lib/SQLiteService.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,28 @@ class SQLiteService extends SQLService {
create: tenant => {
const database = this.url4(tenant)
const dbc = new sqlite(database)

const deterministic = { deterministic: true }
dbc.function('session_context', key => dbc[$session][key])
dbc.function('regexp', { deterministic: true }, (re, x) => (RegExp(re).test(x) ? 1 : 0))
dbc.function('ISO', { deterministic: true }, d => d && new Date(d).toISOString())
dbc.function('regexp', deterministic, (re, x) => (RegExp(re).test(x) ? 1 : 0))
dbc.function('ISO', deterministic, d => d && new Date(d).toISOString())

// define date and time functions in js to allow for throwing errors
const isTime = /^\d{1,2}:\d{1,2}:\d{1,2}$/
const hasTimezone = /([+-]\d{1,2}:?\d{0,2}|Z)$/
const toDate = (d, allowTime = false) => {
if (d === null) return null
const date = new Date(allowTime && isTime.test(d) ? `1970-01-01T${d}Z` : hasTimezone.test(d) ? d : d + 'Z')
if (Number.isNaN(date.getTime())) throw new Error(`Value does not contain a valid ${allowTime ? 'time' : 'date'} "${d}"`)
return date
}
dbc.function('year', deterministic, d => toDate(d).getUTCFullYear())
dbc.function('month', deterministic, d => toDate(d).getUTCMonth() + 1)
dbc.function('day', deterministic, d => toDate(d).getUTCDate())
dbc.function('hour', deterministic, d => toDate(d, true).getUTCHours())
dbc.function('minute', deterministic, d => toDate(d, true).getUTCMinutes())
dbc.function('second', deterministic, d => toDate(d, true).getUTCSeconds())

dbc.function('json_merge', { varargs: true, deterministic: true }, (...args) =>
args.join('').replace(/}{/g, ','),
)
Expand Down
66 changes: 60 additions & 6 deletions test/scenarios/bookshop/funcs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ describe('Bookshop - Functions', () => {
})

describe('Collection Functions', () => {
test.skip('hassubset', () => {})
test.skip('hassubsequence', () => {})
test.skip('hassubset', () => { })
test.skip('hassubsequence', () => { })
})

describe('Arithmetic Functions', () => {
Expand All @@ -155,8 +155,62 @@ describe('Bookshop - Functions', () => {
})

describe('Date and Time Functions', () => {

const types = {
invalid: 0,
time: 1,
date: 2,
}

const toDate = d => new Date(
d.type === (types.date | types.time)
? d.value.endsWith('Z') ? d.value : d.value + 'Z'
: d.type & types.time
? '1970-01-01T' + d.value + 'Z'
: d.type & types.date
? d.value + 'T00:00:00Z'
: d.value
)

const data = [
{ value: '1970-01-02', type: types.date },
{ value: '1970-01-02T03:04:05', type: types.time | types.date },
{ value: '03:04:05', type: types.time },
{ value: 'INVALID', type: types.invalid },
]

const funcs = [
{ func: 'year', type: types.date, extract: d => new Date(d.value).getUTCFullYear() },
{ func: 'month', type: types.date, extract: d => toDate(d).getUTCMonth() + 1 },
{ func: 'day', type: types.date, extract: d => toDate(d).getUTCDate() },
{ func: 'hour', type: types.time | types.date, extract: d => toDate(d).getUTCHours() },
{ func: 'minute', type: types.time | types.date, extract: d => toDate(d).getUTCMinutes() },
{ func: 'second', type: types.time | types.date, extract: d => toDate(d).getUTCSeconds() },
]

/**
* Test every combination of date(/)time function with date(/)time type
* year, month and day only accept types that contain a date
* hour, minute, second accept all date(/)time types by returning 0
*/
describe.each(funcs)('$func', (func) => {
test.each(data)('val $value', async (data) => {
const result = data.type ? func.extract(data) : data.value
const cqn = SELECT.one(`${func.func}('${data.value}') as result`)
.from('sap.capire.bookshop.Books')
.where([`${func.func}('${data.value}') = `], result)

if (data.type & func.type) {
const res = await cqn
expect(res.result).to.eq(result)
} else {
await expect(cqn).rejected
}
})
})

// REVISIT: does not seem database relevant
test.skip('date', () => {})
test.skip('date', () => { })
test('day', async () => {
const res = await GET(`/browse/Books?$select=ID&$filter=day(1970-01-31T00:00:00.000Z) eq 31&$top=1`)

Expand Down Expand Up @@ -218,7 +272,7 @@ describe('Bookshop - Functions', () => {
expect(res.data.value.length).to.be.eq(1)
})
// REVISIT: does not seem database relevant
test.skip('time', () => {})
test.skip('time', () => { })
test.skip('totaloffsetminutes', async () => {
// REVISIT: ERROR: Feature is not supported: Method "totaloffsetminutes" in $filter or $orderby query options
const res = await GET(
Expand All @@ -245,8 +299,8 @@ describe('Bookshop - Functions', () => {

describe('Type Functions', () => {
test.skip('isOf', async () => {
// REVISIT: ERROR: Feature is not supported: Expression "5" in $filter or $orderby query options
// ??? "5"
// REVISIT: ERROR: Feature is not supported: Expression "false" in $filter or $orderby query options
// ??? "false"
const res = await GET(`/browse/Books?$filter=isof(createdAt,Edm.Date)`)

expect(res.status).to.be.eq(200)
Expand Down

0 comments on commit c3ab40a

Please sign in to comment.