diff --git a/hana/cds-plugin.js b/hana/cds-plugin.js index 9f3d21fcb..b4d6fdbce 100644 --- a/hana/cds-plugin.js +++ b/hana/cds-plugin.js @@ -8,32 +8,36 @@ if (cds.requires.db?.impl === '@cap-js/hana') { cds.env.sql.dialect = 'hana' } +// Consider that '-' is only allowed as timezone after ':' or 'T' +const ISO = `FUNCTION ISO(RAW NVARCHAR(36)) +RETURNS RET TIMESTAMP LANGUAGE SQLSCRIPT AS +BEGIN + DECLARE REGEXP NVARCHAR(255); + DECLARE TIMEZONE NVARCHAR(36); + DECLARE MULTIPLIER INTEGER; + DECLARE HOURS INTEGER; + DECLARE MINUTES INTEGER; + REGEXP := '(([-+])([[:digit:]]{2}):?([[:digit:]]{2})?|Z)$'; + TIMEZONE := SUBSTR_REGEXPR(:REGEXP IN RAW GROUP 1); + RET := TO_TIMESTAMP(RAW); + IF :TIMEZONE = 'Z' OR :TIMEZONE IS NULL THEN + RETURN; + END IF; + MULTIPLIER := TO_INTEGER(SUBSTR_REGEXPR(:REGEXP IN TIMEZONE GROUP 2) || '1'); + HOURS := TO_INTEGER(SUBSTR_REGEXPR(:REGEXP IN TIMEZONE GROUP 3)); + MINUTES := COALESCE(TO_INTEGER(SUBSTR_REGEXPR(:REGEXP IN TIMEZONE GROUP 4)),0); + RET := ADD_SECONDS(:RET, (HOURS * 60 + MINUTES) * 60 * MULTIPLIER * -1); +END;` + // monkey patch as not extendable as class const compiler_to_hdi = cds.compiler.to.hdi -cds.compiler.to.hdi = function capjs_compile_hdi(...args) { +cds.compiler.to.hdi = Object.assign(function capjs_compile_hdi(...args) { const artifacts = compiler_to_hdi(...args) - artifacts['ISO.hdbfunction'] = - `CREATE OR REPLACE FUNCTION ISO(RAW NVARCHAR(36)) - RETURNS RET TIMESTAMP LANGUAGE SQLSCRIPT AS - BEGIN - DECLARE REGEXP NVARCHAR(255); - DECLARE TIMEZONE NVARCHAR(36); - DECLARE MULTIPLIER INTEGER; - DECLARE HOURS INTEGER; - DECLARE MINUTES INTEGER; - REGEXP := '(([-+])([[:digit:]]{2}):?([[:digit:]]{2})?|Z)$'; - TIMEZONE := SUBSTR_REGEXPR(:REGEXP IN RAW GROUP 1); - RET := TO_TIMESTAMP(RAW); - IF :TIMEZONE = 'Z' OR :TIMEZONE IS NULL THEN - RETURN; - END IF; - MULTIPLIER := TO_INTEGER(SUBSTR_REGEXPR(:REGEXP IN TIMEZONE GROUP 2) || '1'); - HOURS := TO_INTEGER(SUBSTR_REGEXPR(:REGEXP IN TIMEZONE GROUP 3)); - MINUTES := COALESCE(TO_INTEGER(SUBSTR_REGEXPR(:REGEXP IN TIMEZONE GROUP 4)),0); - RET := ADD_SECONDS(:RET, (HOURS * 60 + MINUTES) * 60 * MULTIPLIER * -1); - END;` - + artifacts['ISO.hdbfunction'] = ISO return artifacts -} +}, +{...compiler_to_hdi} // take over other stuff like keywords +) // TODO: we can override cds.compile.to.sql/.delta in a similar fashion for our tests +module.exports.ISO = ISO diff --git a/hana/lib/HANAService.js b/hana/lib/HANAService.js index 80211aac9..809a9b22f 100644 --- a/hana/lib/HANAService.js +++ b/hana/lib/HANAService.js @@ -60,6 +60,23 @@ class HANAService extends SQLService { const dbc = new driver(credentials) await dbc.connect() HANAVERSION = dbc.server.major + + // Check whether the ISO function is available in the current deployment + if (!service.hasIsoFunction) { + service.hasIsoFunction = dbc.exec(`SELECT ISO('') FROM DUMMY`).catch(() => { + if (!service.class.CQN2SQL.InputConverters.Timestamp) { + service.class.CQN2SQL.InputConverters = service.class.CQN2SQL.InputConverters.__proto__ + } + }, () => { + service.class.CQN2SQL.InputConverters = { + __proto__: service.class.CQN2SQL.InputConverters, + DateTime: undefined, + Timestamp: undefined + } + }) + } + await service.hasIsoFunction + return dbc } catch (err) { if (isMultitenant) { @@ -137,7 +154,7 @@ class HANAService extends SQLService { // REVISIT: add prepare options when param:true is used const sqlScript = isLockQuery || isSimple ? sql : this.wrapTemporary(temporary, withclause, blobs) - let rows + let rows if (values?.length || blobs.length > 0) { const ps = await this.prepare(sqlScript, blobs.length) rows = this.ensureDBC() && await ps.all(values || []) diff --git a/hana/lib/drivers/base.js b/hana/lib/drivers/base.js index 799ebbdc5..4d8241eb3 100644 --- a/hana/lib/drivers/base.js +++ b/hana/lib/drivers/base.js @@ -134,32 +134,6 @@ class HANADriver { minor: split[2], patch: split[3], } - - // Consider that '-' is only allowed as timezone after ':' or 'T' - await prom(this._native, 'exec')(` - CREATE OR REPLACE FUNCTION ISO(RAW NVARCHAR(36)) - RETURNS RET TIMESTAMP LANGUAGE SQLSCRIPT AS - BEGIN - DECLARE REGEXP NVARCHAR(255); - DECLARE TIMEZONE NVARCHAR(36); - DECLARE MULTIPLIER INTEGER; - DECLARE HOURS INTEGER; - DECLARE MINUTES INTEGER; - - REGEXP := '(([-+])([[:digit:]]{2}):?([[:digit:]]{2})?|Z)$'; - TIMEZONE := SUBSTR_REGEXPR(:REGEXP IN RAW GROUP 1); - RET := TO_TIMESTAMP(RAW); - IF :TIMEZONE = 'Z' OR :TIMEZONE IS NULL THEN - RETURN; - END IF; - - MULTIPLIER := TO_INTEGER(SUBSTR_REGEXPR(:REGEXP IN TIMEZONE GROUP 2) || '1'); - HOURS := TO_INTEGER(SUBSTR_REGEXPR(:REGEXP IN TIMEZONE GROUP 3)); - MINUTES := COALESCE(TO_INTEGER(SUBSTR_REGEXPR(:REGEXP IN TIMEZONE GROUP 4)),0); - - RET := ADD_SECONDS(:RET, (HOURS * 60 + MINUTES) * 60 * MULTIPLIER * -1); - END; - `) } /** diff --git a/postgres/lib/PostgresService.js b/postgres/lib/PostgresService.js index 8d9834002..97ea72d1f 100644 --- a/postgres/lib/PostgresService.js +++ b/postgres/lib/PostgresService.js @@ -500,8 +500,8 @@ GROUP BY k Int64: e => `CAST(${e} as bigint)`, Date: e => `CAST(${e} as DATE)`, Time: e => `CAST(${e} as TIME)`, - DateTime: e => `CAST(${e} as TIMESTAMP)`, - Timestamp: e => `CAST(${e} as TIMESTAMP)`, + DateTime: e => `CAST(${e} as TIMESTAMP WITH TIME ZONE)`, + Timestamp: e => `CAST(${e} as TIMESTAMP WITH TIME ZONE)`, // REVISIT: Remove that with upcomming fixes in cds.linked Double: (e, t) => `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`, DecimalFloat: (e, t) => `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`, diff --git a/sqlite/lib/SQLiteService.js b/sqlite/lib/SQLiteService.js index 0615c72a4..3e1d3cb81 100644 --- a/sqlite/lib/SQLiteService.js +++ b/sqlite/lib/SQLiteService.js @@ -27,7 +27,7 @@ class SQLiteService extends SQLService { const deterministic = { deterministic: true } dbc.function('session_context', key => dbc[$session][key]) dbc.function('regexp', deterministic, (re, x) => (RegExp(re).test(x) ? 1 : 0)) - dbc.function('ISO', deterministic, d => d && new Date(d).toISOString()) + dbc.function('ISO', deterministic, d => d && new Date(/([+-]\d{2}:?(\d{2})?|Z)$/.test(d) ? d : `${d}Z`).toISOString()) // define date and time functions in js to allow for throwing errors const isTime = /^\d{1,2}:\d{1,2}:\d{1,2}$/ diff --git a/test/cds.js b/test/cds.js index ba0c89c18..bfe976d28 100644 --- a/test/cds.js +++ b/test/cds.js @@ -112,6 +112,11 @@ cds.test = Object.setPrototypeOf(function () { ret.data.autoIsolation(true) global.beforeAll(async () => { + if (cds.db.options.impl === '@cap-js/hana') { + await cds.run(`CREATE OR REPLACE ${require('../hana/cds-plugin.js').ISO}`) + await cds.disconnect() // Reset database connection to retrigger ISO detection + } + if (ret.data._autoIsolation && !ret.data._deployed) { ret.data._deployed = cds.deploy(cds.options.from[0]) await ret.data._deployed diff --git a/test/compliance/resources/db/basic/literals/basic.literals.timestamp.js b/test/compliance/resources/db/basic/literals/basic.literals.timestamp.js index 762556c2f..95805478e 100644 --- a/test/compliance/resources/db/basic/literals/basic.literals.timestamp.js +++ b/test/compliance/resources/db/basic/literals/basic.literals.timestamp.js @@ -4,32 +4,37 @@ module.exports = [ }, { timestamp: '1970-01-01T00:00:00.000Z', - '=timestamp': '1970-01-01T00:00:00.000Z', + '=timestamp': /1970-01-01T00:00:00.0000{0,6}Z/, }, { timestamp: new Date('1970-01-01Z'), - '=timestamp': '1970-01-01T00:00:00.000Z', + '=timestamp': /1970-01-01T00:00:00.0000{0,6}Z/, }, { timestamp: '1970-01-01T00:00:00.000Z', + '=timestamp': /1970-01-01T00:00:00.0000{0,6}Z/ }, - /* Ignoring transformations + { + timestamp: '2020-01-01T10:00:00.000+10:00', + '=timestamp': /2020-01-01T00:00:00.0000{0,6}Z/ + }, + /* { timestamp: '1970-01-01', - '=timestamp': '1970-01-01 00:00:00.0000000' + '=timestamp': /1970-01-01T00:00:00.0000{0,6}Z/ }, { timestamp: '1970-1-1', - '=timestamp': '1970-01-01 00:00:00.0000000' + '=timestamp': /1970-01-01T00:00:00.0000{0,6}Z/ }, { timestamp: '2', - '=timestamp': '0002-01-01 00:00:00.0000000' + '=timestamp': /0002-01-01T00:00:00.0000{0,6}Z/ }, { // HANA supports left trim timestamp: ' 2', - '=timestamp': '0002-01-01 00:00:00.0000000' + '=timestamp': /0002-01-01T00:00:00.0000{0,6}Z/ }, { // HANA does not support right trim @@ -43,15 +48,15 @@ module.exports = [ }, { timestamp: '2-2', - '=timestamp': '0002-02-01 00:00:00.0000000' + '=timestamp': /0002-02-01T00:00:00.0000{0,6}Z/ }, { timestamp: '2-2-2', - '=timestamp': '0002-02-02 00:00:00.0000000' + '=timestamp': /0002-02-02T00:00:00.0000{0,6}Z/ }, { timestamp: () => new Date('1970-01-01Z'), - '=timestamp': '1970-01-01 00:00:00.0000000' + '=timestamp': /1970-01-01T00:00:00.0000{0,6}Z/ }, { // Z+2359 adds 23 hour and 59 minutes to the UTC time @@ -68,26 +73,28 @@ module.exports = [ timestamp: '1970-01-01+2359', '!': 'Invalid cds.Timestamp "1970-01-01+2359"' }, + */ { timestamp: '1970-01-01T01:10:59', - '=timestamp': '1970-01-01 01:10:59.0000000' + '=timestamp': /1970-01-01T01:10:59.0000{0,6}Z/ }, { - timestamp: '1970-01-01T00:00:00-2359', - '=timestamp': '1970-01-01 23:59:00.0000000' + timestamp: '1970-01-01T00:00:00-11:59', + '=timestamp': /1970-01-01T11:59:00.0000{0,6}Z/ }, { timestamp: '1970-01-01T00:00:00.999', - '=timestamp': '1970-01-01 00:00:00.9990000' + '=timestamp': /1970-01-01T00:00:00.9990{0,6}Z/ }, { timestamp: '1970-01-01T00:00:00.1234567', - '=timestamp': '1970-01-01 00:00:00.1234567' + '=timestamp': /1970-01-01T00:00:00.1234?5?6?7?0{0,2}Z/ }, { timestamp: '1970-01-01T00:00:00.123456789', - '=timestamp': '1970-01-01 00:00:00.1234567' + '=timestamp': /1970-01-01T00:00:00.1234?5?6?7?8?9?Z/ }, + /* { // HANA SECONDDATE does not support year 0 or lower timestamp: '0000-01-01',