Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(temporal data): add time slice key to conflict clause #249

Merged
merged 8 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 })
})
})