From e7dd6de4ef65881ef66f7ba9c164ff2b4e9b1111 Mon Sep 17 00:00:00 2001 From: Bob den Os <108393871+BobdenOs@users.noreply.github.com> Date: Wed, 31 Jan 2024 16:05:24 +0100 Subject: [PATCH] feat: Add fallback for @cap-js/hana for unknown entities (#403) --- db-service/lib/SQLService.js | 8 +++- db-service/lib/cqn2sql.js | 12 ++++++ hana/lib/HANAService.js | 24 +++++++---- test/scenarios/bookshop/update.test.js | 57 ++++++++++++++++++++++++-- 4 files changed, 88 insertions(+), 13 deletions(-) diff --git a/db-service/lib/SQLService.js b/db-service/lib/SQLService.js index 39b013a1d..d9175980a 100644 --- a/db-service/lib/SQLService.js +++ b/db-service/lib/SQLService.js @@ -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) } diff --git a/db-service/lib/cqn2sql.js b/db-service/lib/cqn2sql.js index 5b5172433..2db22e7e1 100644 --- a/db-service/lib/cqn2sql.js +++ b/db-service/lib/cqn2sql.js @@ -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, @@ -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]]] diff --git a/hana/lib/HANAService.js b/hana/lib/HANAService.js index 80ae5bfc2..5ee2e4546 100644 --- a/hana/lib/HANAService.js +++ b/hana/lib/HANAService.js @@ -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]) @@ -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 @@ -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 @@ -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')) { diff --git a/test/scenarios/bookshop/update.test.js b/test/scenarios/bookshop/update.test.js index 9afc0c55e..dbe44f959 100644 --- a/test/scenarios/bookshop/update.test.js +++ b/test/scenarios/bookshop/update.test.js @@ -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) })