Skip to content

Commit

Permalink
feat: Add fallback for @cap-js/hana for unknown entities (#403)
Browse files Browse the repository at this point in the history
  • Loading branch information
BobdenOs authored Jan 31, 2024
1 parent 9b7b5a0 commit e7dd6de
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 13 deletions.
8 changes: 7 additions & 1 deletion db-service/lib/SQLService.js
Original file line number Diff line number Diff line change
Expand Up @@ -338,8 +338,14 @@ class SQLService extends DatabaseService {
* @returns {import('./infer/cqn').Query}
*/
cqn4sql(q) {
if (!q.SELECT?.from?.join && !q.SELECT?.from?.SELECT && !this.model?.definitions[_target_name4(q)])
if (
!cds.env.features.db_strict &&
!q.SELECT?.from?.join &&
!q.SELECT?.from?.SELECT &&
!this.model?.definitions[_target_name4(q)]
) {
return _unquirked(q)
}
return cqn4sql(q, this.model)
}

Expand Down
12 changes: 12 additions & 0 deletions db-service/lib/cqn2sql.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,12 @@ class CQN2SQLRenderer {
/** @type {string[]} */
this.columns = columns.filter(elements ? c => !elements[c]?.['@cds.extension'] : () => true).map(c => this.quote(c))

if (!elements) {
this.entries = INSERT.entries.map(e => columns.map(c => e[c]))
const param = this.param.bind(this, { ref: ['?'] })
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns}) VALUES (${columns.map(param)})`)
}

const extractions = this.managed(
columns.map(c => ({ name: c })),
elements,
Expand Down Expand Up @@ -563,6 +569,12 @@ class CQN2SQLRenderer {

this.columns = columns.map(c => this.quote(c))

if (!elements) {
this.entries = INSERT.rows
const param = this.param.bind(this, { ref: ['?'] })
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns}) VALUES (${columns.map(param)})`)
}

if (INSERT.rows[0] instanceof Readable) {
INSERT.rows[0].type = 'json'
this.entries = [[...this.values, INSERT.rows[0]]]
Expand Down
24 changes: 16 additions & 8 deletions hana/lib/HANAService.js
Original file line number Diff line number Diff line change
Expand Up @@ -508,9 +508,10 @@ class HANAService extends SQLService {
const entity = q.target?.['@cds.persistence.name'] || this.name(q.target?.name || INSERT.into.ref[0])

const elements = q.elements || q.target?.elements
if (!elements && !INSERT.entries?.length) {
return // REVISIT: mtx sends an insert statement without entries and no reference entity
if (!elements) {
return super.INSERT_entries(q)
}

const columns = elements
? ObjectKeys(elements).filter(c => c in elements && !elements[c].virtual && !elements[c].value && !elements[c].isAssociation)
: ObjectKeys(INSERT.entries[0])
Expand Down Expand Up @@ -569,6 +570,10 @@ class HANAService extends SQLService {
// The problem with Simple INSERT is the type mismatch from csv files
// Recommendation is to always use entries
const elements = q.elements || q.target?.elements
if (!elements) {
return super.INSERT_rows(q)
}

const columns = INSERT.columns || (elements && ObjectKeys(elements))
const entries = new Array(INSERT.rows.length)
const rows = INSERT.rows
Expand All @@ -585,13 +590,17 @@ class HANAService extends SQLService {
}

UPSERT(q) {
let { UPSERT } = q,
sql = this.INSERT({ __proto__: q, INSERT: UPSERT })
const { UPSERT } = q
const sql = this.INSERT({ __proto__: q, INSERT: UPSERT })

// REVISIT: should @cds.persistence.name be considered ?
const entity = q.target?.['@cds.persistence.name'] || this.name(q.target?.name || INSERT.into.ref[0])
// If no definition is available fallback to INSERT statement
const elements = q.elements || q.target?.elements
if (!elements) {
return (this.sql = sql)
}

// REVISIT: should @cds.persistence.name be considered ?
const entity = q.target?.['@cds.persistence.name'] || this.name(q.target?.name || INSERT.into.ref[0])
const dataSelect = sql.substring(sql.indexOf('WITH'))

// Calculate @cds.on.insert
Expand Down Expand Up @@ -830,8 +839,7 @@ class HANAService extends SQLService {
const val = _managed[element[annotation]?.['=']]
let managed
if (val) managed = this.func({ func: 'session_context', args: [{ val, param: false }] })
const type = this.insertType4(element)
let extract = sql ?? `${this.quote(name)} ${type} PATH '$.${name}'`
let extract = sql ?? `${this.quote(name)} ${this.insertType4(element)} PATH '$.${name}'`
if (!isUpdate) {
const d = element.default
if (d && (d.val !== undefined || d.ref?.[0] === '$now')) {
Expand Down
57 changes: 53 additions & 4 deletions test/scenarios/bookshop/update.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,65 @@ describe('Bookshop - Update', () => {
expect(update.data.footnotes).to.be.eql(['one'])
})

test('programmatic insert/upsert/update/delete with unknown entity', async () => {
const books = 'sap_capire_bookshop_Books'
const ID = 999
let affectedRows = await INSERT.into(books)
.entries({
ID,
createdAt: (new Date()).toISOString(),
})
expect(affectedRows | 0).to.be.eq(1)

affectedRows = await DELETE(books)
.where({ ID })
expect(affectedRows | 0).to.be.eq(1)

affectedRows = await INSERT.into(books)
.columns(['ID', 'createdAt'])
.values([ID, (new Date()).toISOString()])
expect(affectedRows | 0).to.be.eq(1)

affectedRows = await UPDATE(books)
.with({ modifiedAt: (new Date()).toISOString() })
.where({ ID })
expect(affectedRows | 0).to.be.eq(1)

affectedRows = await DELETE(books)
.where({ ID })
expect(affectedRows | 0).to.be.eq(1)

// UPSERT fallback to an INSERT
affectedRows = await UPSERT.into(books)
.entries({
ID,
createdAt: (new Date()).toISOString(),
})
expect(affectedRows | 0).to.be.eq(1)

// UPSERT fallback to an INSERT (throws on secondary call)
affectedRows = UPSERT.into(books)
.entries({
ID,
createdAt: (new Date()).toISOString(),
})
await expect(affectedRows).rejected

affectedRows = await DELETE(books)
.where({ ID })
expect(affectedRows | 0).to.be.eq(1)
})

test('programmatic update without body incl. managed', async () => {
const { modifiedAt } = await cds.db.run(cds.ql.SELECT.from('sap.capire.bookshop.Books', { ID: 251 }))
const affectedRows = await cds.db.run(cds.ql.UPDATE('sap.capire.bookshop.Books', { ID: 251 }))
const { modifiedAt } = await SELECT.from('sap.capire.bookshop.Books', { ID: 251 })
const affectedRows = await UPDATE('sap.capire.bookshop.Books', { ID: 251 })
expect(affectedRows).to.be.eq(1)
const { modifiedAt: newModifiedAt } = await cds.db.run(cds.ql.SELECT.from('sap.capire.bookshop.Books', { ID: 251 }))
const { modifiedAt: newModifiedAt } = await SELECT.from('sap.capire.bookshop.Books', { ID: 251 })
expect(newModifiedAt).not.to.be.eq(modifiedAt)
})

test('programmatic update without body excl. managed', async () => {
const affectedRows = await cds.db.run(cds.ql.UPDATE('sap.capire.bookshop.Genres', { ID: 10 }))
const affectedRows = await UPDATE('sap.capire.bookshop.Genres', { ID: 10 })
expect(affectedRows).to.be.eq(0)
})

Expand Down

0 comments on commit e7dd6de

Please sign in to comment.