diff --git a/db-service/lib/cqn2sql.js b/db-service/lib/cqn2sql.js index b3aec8cee..4f2412d1a 100644 --- a/db-service/lib/cqn2sql.js +++ b/db-service/lib/cqn2sql.js @@ -1100,10 +1100,21 @@ class CQN2SQLRenderer { const extractions = this._managed if (this.values) this.values = [] // Clear previously computed values const src = this.cqn4sql(UPSERT.from || UPSERT.as) + const missingKeys = this._managed.slice(src.SELECT.columns.length) + .filter(c => keys.includes(c.name)) + .map(c => ({ + __proto__: this.managed_session_context(elements[c.name]['@cds.on.insert']?.['=']) + || this.managed_session_context(elements[c.name].default?.ref?.[0]) + || (elements[c.name].default && { __proto__: elements[c.name].default, param: false }) + || { val: null, param: false }, + as: c.name, + })) const aliasedQuery = cds.ql.SELECT - .columns(src.SELECT.columns - .map((c, i) => ({ ref: [this.column_name(c)], as: this.columns[i] })) - ) + .columns([ + ...src.SELECT.columns + .map((c, i) => ({ ref: [this.column_name(c)], as: this.columns[i] })), + ...missingKeys, + ]) .from(src) sql = `SELECT ${extractions.map(c => `${c.upsert}`)} FROM (${this.SELECT(aliasedQuery)}) AS NEW LEFT JOIN ${this.quote(entity)} AS OLD ON ${keyCompare}` if (extractions.length > columns.length) columns = this.columns = extractions.map(c => c.name) @@ -1437,6 +1448,9 @@ class CQN2SQLRenderer { const keyZero = keys[0] && this.quote(keys[0]) return [...columns, ...requiredColumns].map(({ name, sql }) => { + const isNull = sql === 'NULL' + const json = !sql + const element = elements?.[name] || {} const converter = a => element[_convertInput]?.(a, element) || a @@ -1458,8 +1472,8 @@ class CQN2SQLRenderer { const qname = this.quote(name) - const insert = onInsert ? this.managed_default(name, converter(onInsert), sql) : sql - const update = onUpdate ? this.managed_default(name, converter(onUpdate), sql) : sql + const insert = onInsert ? json ? this.managed_default(name, converter(onInsert), sql) : !isNull ? sql : onInsert : sql + const update = onUpdate ? json ? this.managed_default(name, converter(onUpdate), sql) : !isNull ? sql : onUpdate : sql const upsert = keyZero && ( // upsert requires the keys to be provided for the existance join (default values optional) element.key @@ -1471,7 +1485,7 @@ class CQN2SQLRenderer { insert } ELSE ${ // Else execute managed update or keep old if no new data if provided - onUpdate ? update : this.managed_default(name, `OLD.${qname}`, update) + onUpdate || !json ? update : this.managed_default(name, `OLD.${qname}`, update) } END as ${qname}` ) diff --git a/hana/lib/HANAService.js b/hana/lib/HANAService.js index 745a61c8e..6d9801d1a 100644 --- a/hana/lib/HANAService.js +++ b/hana/lib/HANAService.js @@ -851,7 +851,7 @@ class HANAService extends SQLService { // temporal data keys.push(...ObjectKeys(q._target.elements).filter(e => q._target.elements[e]['@cds.valid.from'])) - const managed = this.managed( + const managed = this._managed || this.managed( this.columns.map(c => ({ name: c })), elements ) @@ -871,10 +871,21 @@ SELECT ${mixing} FROM JSON_TABLE(SRC.JSON, '$' COLUMNS(${extraction}) ERROR ON E } else { const src = this.cqn4sql(UPSERT.from || UPSERT.as) if (this.values) this.values = [] + const missingKeys = this._managed.slice(src.SELECT.columns.length) + .filter(c => keys.includes(c.name)) + .map(c => ({ + __proto__: this.managed_session_context(elements[c.name]['@cds.on.insert']?.['=']) + || this.managed_session_context(elements[c.name].default?.ref?.[0]) + || (elements[c.name].default && { __proto__: elements[c.name].default, param: false }) + || { val: null, param: false }, + as: c.name, + })) const aliasedQuery = cds.ql.SELECT - .columns(src.SELECT.columns - .map((c, i) => ({ ref: [this.column_name(c)], as: this.columns[i] })) - ) + .columns([ + ...src.SELECT.columns + .map((c, i) => ({ ref: [this.column_name(c)], as: this.columns[i] })), + ...missingKeys, + ]) .from(src) sql = `SELECT ${mixing} FROM (${this.SELECT(aliasedQuery)}) AS NEW LEFT JOIN ${this.quote(entity)} AS OLD ON ${keyCompare}` this.entries = [this.values] diff --git a/hana/package.json b/hana/package.json index 989c31c30..2f9b90bec 100644 --- a/hana/package.json +++ b/hana/package.json @@ -20,10 +20,8 @@ "CHANGELOG.md" ], "scripts": { - "test": "(([ -z \"${HANA_HOST}\" ] && npm start) || true) && npm run test:plain && npm run test:bookshop:quoted", - "test:bookshop:quoted": "cds_sql_names=quoted cds-test bookshop", - "test:plain": "cds-test", - "test:remote": "cds-test", + "test": "sh -c '(([ -z \"${HANA_HOST}\" ] && npm start) || true) && (([ -z $1 ] && npm run test:ci) || ([ -n $1 ] && cds-test $@))' --", + "test:ci": "cds-test && cds_sql_names=quoted cds-test bookshop", "start": "npm run start:hce || npm run start:hxe", "start:hce": "cd ./tools/docker/hce/ && ./start.sh", "start:hxe": "cd ./tools/docker/hxe/ && ./start.sh" diff --git a/test/compliance/UPSERT.test.js b/test/compliance/UPSERT.test.js index fb53640f7..edf3b1aef 100644 --- a/test/compliance/UPSERT.test.js +++ b/test/compliance/UPSERT.test.js @@ -62,6 +62,7 @@ describe('UPSERT', () => { { id: 1 }, { id: 1, default: 'overwritten' }, ]).into(keys) + await UPSERT.into(cuid) .columns(['ID']) .from(cds.ql`SELECT id || '-' || default as ![something that is not "ID"] FROM ${keys} WHERE id = ${1}`) @@ -79,6 +80,7 @@ describe('UPSERT', () => { { id: 1 }, { id: 1, default: 'overwritten' }, ]).into(keys) + await UPSERT.into(cuid) .columns(['ID']) .from(cds.ql`SELECT id || '-' || default as ![something that is not "ID"] FROM ${keys} WHERE id = ${1}`) @@ -88,6 +90,47 @@ describe('UPSERT', () => { { ID: '1-overwritten' }, ]) }) + + test('default key', async () => { + const { keys } = cds.entities('basic.projection') + // fill other table first + await UPSERT([ + { id: 1 }, + { id: 1, default: 'overwritten' }, + ]).into(keys) + let select + + await UPSERT.into(keys) + .columns(['id', 'default']) + .from(cds.ql`SELECT 2, default FROM ${keys} WHERE id = ${1}`) + select = await SELECT.from(keys).where`id = ${2}`.orderBy('default') + expect(select).deep.eq([ + { id: 2, default: 'defaulted', data: null }, + { id: 2, default: 'overwritten', data: null }, + ]) + + await UPSERT.into(keys) + .columns(['id']) + // FYI: default check is required to ensure only a single row is produced for HANA + .from(cds.ql`SELECT 3 FROM ${keys} WHERE id = ${1} and default = ${'overwritten'}`) + select = await SELECT.from(keys).where`id = ${3}`.orderBy('default') + expect(select).deep.eq([ + { id: 3, default: 'defaulted', data: null }, + ]) + + await UPSERT.into(keys) + .columns(['id', 'data']) + .from(cds.ql`SELECT 3, count() FROM ${keys} WHERE id = ${1}`) + // FYI: only HANA does unique key constraints on the source SELECT result + // example: the following query will return 2 rows with their row count + // the result for this UPSERT would be undefined behavior (data = 1 or 2) + // therefor HANA ensures that all keys of the source SELECT are unique + // .from(cds.ql`SELECT 3, row_number() over () FROM ${keys} WHERE id = ${1}`) + select = await SELECT.from(keys).where`id = ${3}`.orderBy('default') + expect(select).deep.eq([ + { id: 3, default: 'defaulted', data: '2' }, + ]) + }) }) test('affected row', async () => {