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: Add fallback for @cap-js/hana for unknown entities #403

Merged
merged 14 commits into from
Jan 31, 2024
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
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