From 92659e785f6cb8de4edccd4f3186b97ea866e431 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Mon, 25 Sep 2023 11:24:06 +0200 Subject: [PATCH 1/4] feat(temporal data): add time slice key to conflict clause --- db-service/lib/cqn2sql.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/db-service/lib/cqn2sql.js b/db-service/lib/cqn2sql.js index 658c33044..903cb1614 100644 --- a/db-service/lib/cqn2sql.js +++ b/db-service/lib/cqn2sql.js @@ -518,6 +518,9 @@ class CQN2SQLRenderer { .filter(c => !keys.includes(c)) .map(c => `${this.quote(c)} = excluded.${this.quote(c)}`) + // temporal data + keys.push(...Object.values(q.target.elements).filter(e => e['@cds.valid.from']).map(e => e.name)) + keys = keys.map(k => this.quote(k)) const conflict = updateColumns.length ? `ON CONFLICT(${keys}) DO UPDATE SET ` + updateColumns From 06ccc95abe1efaf8671cf2be1a760b8f385ac32f Mon Sep 17 00:00:00 2001 From: D050513 Date: Thu, 5 Oct 2023 17:14:26 +0200 Subject: [PATCH 2/4] handle @cds.valid.* during c/u & tests --- db-service/lib/cqn2sql.js | 2 +- sqlite/test/general/model.cds | 47 +++++++++++++++++----------- sqlite/test/general/model.js | 9 ++++++ sqlite/test/general/temporal.test.js | 41 ++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 20 deletions(-) create mode 100644 sqlite/test/general/model.js create mode 100644 sqlite/test/general/temporal.test.js diff --git a/db-service/lib/cqn2sql.js b/db-service/lib/cqn2sql.js index 903cb1614..30c7c5dcb 100644 --- a/db-service/lib/cqn2sql.js +++ b/db-service/lib/cqn2sql.js @@ -865,8 +865,8 @@ class CQN2SQLRenderer { if (converter && sql[0] !== '$') sql = converter(sql, element) let val = _managed[element[annotation]?.['=']] + || !isUpdate && (element['@cds.valid.from'] ? '$valid.from' : element['@cds.valid.to'] ? '$valid.to' : null) if (val) sql = `coalesce(${sql}, ${this.func({ func: 'session_context', args: [{ val }] })})` - else if (!isUpdate && element.default) { const d = element.default if (d.val !== undefined || d.ref?.[0] === '$now') { diff --git a/sqlite/test/general/model.cds b/sqlite/test/general/model.cds index f7c2e10d6..61d263f7a 100644 --- a/sqlite/test/general/model.cds +++ b/sqlite/test/general/model.cds @@ -1,27 +1,36 @@ -using {managed} from '@sap/cds/common'; +using { + managed, + temporal +} from '@sap/cds/common'; + +entity db.fooTemporal : managed, temporal { + key ID : Integer; +} @path: '/test' service test { - entity foo : managed { - key ID : Integer; - } + entity foo : managed { + key ID : Integer; + } + + entity bar { + key ID : UUID; + } - entity bar { - key ID : UUID; - } + entity fooLocalized { + key ID : Integer; + text : localized String; + } - entity fooLocalized { - key ID : Integer; - text : localized String; - } + entity fooTemporal as projection on db.fooTemporal; - entity Images { - key ID : Integer; - data : LargeBinary @Core.MediaType: 'image/jpeg'; - } + entity Images { + key ID : Integer; + data : LargeBinary @Core.MediaType: 'image/jpeg'; + } - entity ImagesView as projection on Images { - *, - data as renamedData - } + entity ImagesView as projection on Images { + *, + data as renamedData + } } diff --git a/sqlite/test/general/model.js b/sqlite/test/general/model.js new file mode 100644 index 000000000..888cd688f --- /dev/null +++ b/sqlite/test/general/model.js @@ -0,0 +1,9 @@ +module.exports = srv => { + const { fooTemporal } = srv.entities + + srv.on('CREATE', fooTemporal, async function (req) { + // without the fix, this UPSERT throws + await UPSERT(req.data).into(fooTemporal) + return req.data + }) +} diff --git a/sqlite/test/general/temporal.test.js b/sqlite/test/general/temporal.test.js new file mode 100644 index 000000000..d7da4b759 --- /dev/null +++ b/sqlite/test/general/temporal.test.js @@ -0,0 +1,41 @@ +const cds = require('../../../test/cds.js') +const { GET, POST } = cds.test(__dirname, 'model.cds') + +describe('temporal', () => { + beforeAll(async () => { + const db = await cds.connect.to('db') + const { fooTemporal } = db.model.entities('test') + await db.create(fooTemporal).entries([ + { ID: 1, validFrom: '1990-01-01T00:00:00.000Z' }, + { ID: 2, validFrom: '2000-01-01T00:00:00.000Z' } + ]) + }) + + test('READ', async () => { + let validAt, res + + validAt = '1970-01-01T00:00:00.000Z' + res = await GET(`/test/fooTemporal?sap-valid-at=${validAt}`) + expect(res.data.value.length).toBe(0) + + validAt = '1995-01-01T00:00:00.000Z' + res = await GET(`/test/fooTemporal?sap-valid-at=${validAt}`) + expect(res.data.value.length).toBe(1) + const it = res.data.value[0] + expect(it).toMatchObject({ ID: 1 }) + // managed and temporal shall not clash + expect(it.createdAt).not.toEqual(it.validFrom) + + validAt = '2010-01-01T00:00:00.000Z' + res = await GET(`/test/fooTemporal?sap-valid-at=${validAt}`) + expect(res.data.value.length).toBe(2) + }) + + test('UPSERT', async () => { + const validFrom = '2000-01-01T00:00:00.000Z' + const url = `/test/fooTemporal?sap-valid-from=${validFrom}` + const data = { ID: 42, validFrom } + const res = await POST(url, data) + expect(res.data).toMatchObject({ validFrom }) + }) +}) From e37e3a45f735c4dc511b3f422ce9965688f66f91 Mon Sep 17 00:00:00 2001 From: Bob den Os Date: Tue, 17 Oct 2023 12:50:06 +0200 Subject: [PATCH 3/4] Add HANA implementation --- hana/lib/HANAService.js | 26 +++++++++++++------------- hana/test/temporal.test.js | 1 + sqlite/test/general/temporal.test.js | 3 ++- 3 files changed, 16 insertions(+), 14 deletions(-) create mode 100644 hana/test/temporal.test.js diff --git a/hana/lib/HANAService.js b/hana/lib/HANAService.js index a24b4669d..b39814ba3 100644 --- a/hana/lib/HANAService.js +++ b/hana/lib/HANAService.js @@ -64,6 +64,10 @@ class HANAService extends SQLService { } async set(variables) { + // REVISIT: required to be compatible with generated views + if (variables['$valid.from']) variables['VALID-FROM'] = variables['$valid.from'] + if (variables['$valid.to']) variables['VALID-TO'] = variables['$valid.to'] + const columns = Object.keys(variables).map( k => `SET '${k.replace(/'/g, "''")}'='${(variables[k] + '').replace(/'/g, "''")}';`, ) @@ -837,19 +841,10 @@ class HANAService extends SQLService { const element = elements?.[name] || {} // Don't apply input converters for place holders const converter = (sql !== '?' && element[inputConverterKey]) || (e => e) - let managed = element[annotation]?.['='] - switch (managed) { - case '$user.id': - case '$user': - managed = this.func({ func: 'session_context', args: [{ val: '$user.id' }] }) - break - case '$now': - managed = this.func({ func: 'session_context', args: [{ val: '$user.now' }] }) - break - default: - managed = undefined - } - + const val = _managed[element[annotation]?.['=']] + || !isUpdate && (element['@cds.valid.from'] ? '$valid.from' : element['@cds.valid.to'] ? '$valid.to' : null) + let managed + if (val) managed = this.func({ func: 'session_context', args: [{ val }] }) const type = this.insertType4(element) let extract = sql ?? `${this.quote(name)} ${type} PATH '$.${name}'` if (!isUpdate) { @@ -1073,5 +1068,10 @@ Buffer.prototype.toJSON = function () { const is_regexp = x => x?.constructor?.name === 'RegExp' // NOTE: x instanceof RegExp doesn't work in repl const ObjectKeys = o => (o && [...ObjectKeys(o.__proto__), ...Object.keys(o)]) || [] +const _managed = { + '$user.id': '$user.id', + $user: '$user.id', + $now: '$now', +} module.exports = HANAService diff --git a/hana/test/temporal.test.js b/hana/test/temporal.test.js new file mode 100644 index 000000000..0bdd79d6a --- /dev/null +++ b/hana/test/temporal.test.js @@ -0,0 +1 @@ +require('../../sqlite/test/general/temporal.test') diff --git a/sqlite/test/general/temporal.test.js b/sqlite/test/general/temporal.test.js index d7da4b759..71e0ab36f 100644 --- a/sqlite/test/general/temporal.test.js +++ b/sqlite/test/general/temporal.test.js @@ -1,7 +1,8 @@ const cds = require('../../../test/cds.js') -const { GET, POST } = cds.test(__dirname, 'model.cds') describe('temporal', () => { + const { GET, POST } = cds.test(__dirname, 'model.cds') + beforeAll(async () => { const db = await cds.connect.to('db') const { fooTemporal } = db.model.entities('test') From 3769eb45eac3456a8f548d595d0ebdba32dd834f Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Thu, 16 Nov 2023 12:26:08 +0100 Subject: [PATCH 4/4] provide validTo manually (#344) --- db-service/lib/cqn2sql.js | 1 - hana/lib/HANAService.js | 1 - sqlite/test/general/temporal.test.js | 4 ++-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/db-service/lib/cqn2sql.js b/db-service/lib/cqn2sql.js index 69e94fe1b..995c202f4 100644 --- a/db-service/lib/cqn2sql.js +++ b/db-service/lib/cqn2sql.js @@ -874,7 +874,6 @@ class CQN2SQLRenderer { if (converter && sql[0] !== '$') sql = converter(sql, element) let val = _managed[element[annotation]?.['=']] - || !isUpdate && (element['@cds.valid.from'] ? '$valid.from' : element['@cds.valid.to'] ? '$valid.to' : null) if (val) sql = `coalesce(${sql}, ${this.func({ func: 'session_context', args: [{ val }] })})` else if (!isUpdate && element.default) { const d = element.default diff --git a/hana/lib/HANAService.js b/hana/lib/HANAService.js index 2000a5fc7..1d924cad4 100644 --- a/hana/lib/HANAService.js +++ b/hana/lib/HANAService.js @@ -842,7 +842,6 @@ class HANAService extends SQLService { // Don't apply input converters for place holders const converter = (sql !== '?' && element[inputConverterKey]) || (e => e) const val = _managed[element[annotation]?.['=']] - || !isUpdate && (element['@cds.valid.from'] ? '$valid.from' : element['@cds.valid.to'] ? '$valid.to' : null) let managed if (val) managed = this.func({ func: 'session_context', args: [{ val }] }) const type = this.insertType4(element) diff --git a/sqlite/test/general/temporal.test.js b/sqlite/test/general/temporal.test.js index 71e0ab36f..9892badc5 100644 --- a/sqlite/test/general/temporal.test.js +++ b/sqlite/test/general/temporal.test.js @@ -7,8 +7,8 @@ describe('temporal', () => { const db = await cds.connect.to('db') const { fooTemporal } = db.model.entities('test') await db.create(fooTemporal).entries([ - { ID: 1, validFrom: '1990-01-01T00:00:00.000Z' }, - { ID: 2, validFrom: '2000-01-01T00:00:00.000Z' } + { ID: 1, validFrom: '1990-01-01T00:00:00.000Z', validTo: '9999-12-31T23:59:59.999Z' }, + { ID: 2, validFrom: '2000-01-01T00:00:00.000Z', validTo: '9999-12-31T23:59:59.999Z' } ]) })