Skip to content

Commit

Permalink
feat(temporal data): add time slice key to conflict clause (#249)
Browse files Browse the repository at this point in the history
Co-authored-by: Bob den Os <[email protected]>
Co-authored-by: Patrice Bender <[email protected]>
Co-authored-by: Johannes Vogel <[email protected]>
  • Loading branch information
4 people authored Nov 16, 2023
1 parent 87208af commit 67b8edf
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 33 deletions.
4 changes: 3 additions & 1 deletion db-service/lib/cqn2sql.js
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,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
Expand Down Expand Up @@ -872,7 +875,6 @@ class CQN2SQLRenderer {

let val = _managed[element[annotation]?.['=']]
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') {
Expand Down
25 changes: 12 additions & 13 deletions hana/lib/HANAService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']

this.dbc.set(variables)
}

Expand Down Expand Up @@ -837,19 +841,9 @@ 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]?.['=']]
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) {
Expand Down Expand Up @@ -1073,5 +1067,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
1 change: 1 addition & 0 deletions hana/test/temporal.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('../../sqlite/test/general/temporal.test')
47 changes: 28 additions & 19 deletions sqlite/test/general/model.cds
Original file line number Diff line number Diff line change
@@ -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
}
}
9 changes: 9 additions & 0 deletions sqlite/test/general/model.js
Original file line number Diff line number Diff line change
@@ -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
})
}
42 changes: 42 additions & 0 deletions sqlite/test/general/temporal.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const cds = require('../../../test/cds.js')

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')
await db.create(fooTemporal).entries([
{ 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' }
])
})

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 })
})
})

0 comments on commit 67b8edf

Please sign in to comment.