diff --git a/db-service/lib/cqn2sql.js b/db-service/lib/cqn2sql.js index a678b2159..c5a2eebc5 100644 --- a/db-service/lib/cqn2sql.js +++ b/db-service/lib/cqn2sql.js @@ -886,13 +886,16 @@ class CQN2SQLRenderer { return // REVISIT: mtx sends an insert statement without entries and no reference entity } const transitions = this.srv.resolve.transitions(q) - const columns = elements - ? ObjectKeys(elements).filter(c => this.physical_column(elements, c) + const transitionsTargetElements = transitions.target.elements + + const filteredElements = !elements ? {} : + ObjectKeys(elements).filter(c => this.physical_column(elements, c) && (c = transitions.mapping.get(c)?.ref?.[0] || c) - && c in transitions.target.elements - && this.physical_column(transitions.target.elements, c) - ) - : ObjectKeys(INSERT.entries[0]) + && c in transitionsTargetElements + && this.physical_column(transitionsTargetElements, c) + ).reduce( (res, key) => (res[key] = elements[key], res), {} ) + + const columns = elements ? ObjectKeys(filteredElements) : ObjectKeys(INSERT.entries[0]) /** @type {string[]} */ this.columns = columns @@ -919,9 +922,16 @@ class CQN2SQLRenderer { this.entries = [[...this.values, stream]] } - const extractions = this._managed = this.managed(columns.map(c => ({ name: c })), elements) - return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c)) - }) SELECT ${extractions.slice(0, columns.length).map(c => c.insert)} FROM json_each(?)`) + const mappedTransitionsTargets = new Set(columns.map(c => transitions.mapping.get(c)?.ref?.[0]).filter(Boolean)) + const targetElementsFiltered = Object.fromEntries( + ObjectKeys(transitionsTargetElements) + .filter(key => !mappedTransitionsTargets.has(key)) + .map(key => [key, transitionsTargetElements[key]]), + ) + + const extractions = this._managed = this.managed(columns.map(c => ({ name: c })), { ...targetElementsFiltered, ...filteredElements }) + return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${extractions.map(c => this.quote(transitions.mapping.get(c.name)?.ref?.[0] || c.name)) + }) SELECT ${extractions.map(c => c.insert)} FROM json_each(?)`) } async *INSERT_entries_stream(entries, binaryEncoding = 'base64') { @@ -1511,7 +1521,7 @@ class CQN2SQLRenderer { if (columns.find(c => c.name === e)) return false return true }) - .map(name => ({ name, sql: 'NULL' })) + .map(name => ({ name })) const keys = ObjectKeys(elements).filter(e => elements[e].key && !elements[e].isAssociation) const keyZero = keys[0] && this.quote(keys[0]) diff --git a/hana/lib/HANAService.js b/hana/lib/HANAService.js index 8ffd12af5..9f0fcb0dd 100644 --- a/hana/lib/HANAService.js +++ b/hana/lib/HANAService.js @@ -792,18 +792,26 @@ class HANAService extends SQLService { const entity = q._target ? this.table_name(q) : INSERT.into.ref[0] const transitions = this.srv.resolve.transitions(q) + const transitionsTargetElements = transitions.target.elements - const columns = elements - ? ObjectKeys(elements).filter(c => this.physical_column(elements, c) + const filteredElements = !elements ? {} : + ObjectKeys(elements).filter(c => this.physical_column(elements, c) && (c = transitions.mapping.get(c)?.ref?.[0] || c) - && c in transitions.target.elements - && this.physical_column(transitions.target.elements, c) + && c in transitionsTargetElements + && this.physical_column(transitionsTargetElements, c) && !elements[c]?.[SYSTEM_VERSIONED] - ) - : ObjectKeys(INSERT.entries[0]) - this.columns = columns + ).reduce( (res, key) => (res[key] = elements[key], res), {} ) + + const columns = this.columns = elements ? ObjectKeys(filteredElements) : ObjectKeys(INSERT.entries[0]) + + const mappedTransitionsTargets = new Set(columns.map(c => transitions.mapping.get(c)?.ref?.[0]).filter(Boolean)) + const targetElementsFiltered = Object.fromEntries( + ObjectKeys(transitionsTargetElements) + .filter(key => !mappedTransitionsTargets.has(key)) + .map(key => [key, transitionsTargetElements[key]]), + ) - const extractions = this._managed = this.managed(columns.map(c => ({ name: c })), elements).slice(0, columns.length) + const extractions = this._managed = this.managed(columns.map(c => ({ name: c })), { ...targetElementsFiltered, ...filteredElements }) // REVISIT: @cds.extension required const extraction = extractions.map(c => c.extract) @@ -843,7 +851,7 @@ class HANAService extends SQLService { // With the buffer table approach the data is processed in chunks of a configurable size // Which allows even smaller HANA systems to process large datasets // But the chunk size determines the maximum size of a single row - return (this.sql = `INSERT INTO ${this.quote(entity)} (${this.columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c)) + return (this.sql = `INSERT INTO ${this.quote(entity)} (${extractions.map(c => this.quote(transitions.mapping.get(c.name)?.ref?.[0] || c.name)) }) WITH SRC AS (SELECT ? AS JSON FROM DUMMY UNION ALL SELECT TO_NCLOB(NULL) AS JSON FROM DUMMY) SELECT ${converter} FROM JSON_TABLE(SRC.JSON, '$' COLUMNS(${extraction}) ERROR ON ERROR) AS NEW`) } diff --git a/test/scenarios/bookshop/insert.test.js b/test/scenarios/bookshop/insert.test.js index 60c345b49..11a191406 100644 --- a/test/scenarios/bookshop/insert.test.js +++ b/test/scenarios/bookshop/insert.test.js @@ -49,4 +49,13 @@ describe('Bookshop - Insert', () => { const res = await SELECT.from(Books, {ID: 344}) expect(res.genre_ID).to.be.eq(10) }) + + test('insert with default fields excluded from projection', async () => { + const { RenameKeys, Books } = cds.entities('AdminService') + await cds.run(INSERT({ foo: 345 }).into(RenameKeys)) + const res = await SELECT.from(Books, { ID: 345 }) + expect(res).to.containSubset({ genre_ID: 10, createdBy: 'anonymous', modifiedBy: 'anonymous' }) + expect(res.createdAt).to.not.be.null + expect(res.modifiedAt).to.not.be.null + }) })