diff --git a/db-service/lib/cqn2sql.js b/db-service/lib/cqn2sql.js index a678b2159..767503aae 100644 --- a/db-service/lib/cqn2sql.js +++ b/db-service/lib/cqn2sql.js @@ -1088,7 +1088,7 @@ class CQN2SQLRenderer { const extractions = this._managed = this.managed(columns.map(c => ({ name: c, sql: `NEW.${this.quote(c)}` })), elements) const sql = extractions.length > columns.length - ? `SELECT ${extractions.map(c => `${c.insert} AS ${this.quote(c.name)}`)} FROM (${this.SELECT(src)}) AS NEW` + ? `SELECT ${extractions.map((c, i) => `${i < columns.length ? c.insert : c.onInsert} AS ${this.quote(c.name)}`)} FROM (${this.SELECT(src)}) AS NEW` : this.SELECT(src) if (extractions.length > columns.length) columns = this.columns = extractions.map(c => c.name) this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c))}) ${sql}` @@ -1161,12 +1161,12 @@ class CQN2SQLRenderer { const extractions = this._managed if (this.values) this.values = [] // Clear previously computed values const src = this.cqn4sql(UPSERT.from || UPSERT.as) - const aliasedQuery = cds.ql.SELECT - .columns(src.SELECT.columns - .map((c, i) => ({ ref: [this.column_name(c)], as: this.columns[i] })) - ) - .from(src) - sql = `SELECT ${extractions.map(c => `${c.upsert}`)} FROM (${this.SELECT(aliasedQuery)}) AS NEW LEFT JOIN ${this.quote(entity)} AS OLD ON ${keyCompare}` + const aliasedQuery = `SELECT ${[ + ...src.SELECT.columns.map((c, i) => this.column_expr({ ref: [this.column_name(c)], as: this.columns[i] })), + ...extractions.slice(src.SELECT.columns.length).map(c => `${elements[c.name].key ? c.onInsert : 'NULL'} AS ${this.quote(c.name)}`), // fill in missing default values + ]} FROM (${this.SELECT(src)})` + + sql = `SELECT ${extractions.map(c => elements[c.name].key ? `NEW.${this.quote(c.name)}` : c.upsert)} FROM(${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) this.entries = [this.values] } @@ -1574,9 +1574,11 @@ class CQN2SQLRenderer { managed_extract(name, element, converter) { const { UPSERT, INSERT } = this.cqn - const extract = !(INSERT?.entries || UPSERT?.entries) && (INSERT?.rows || UPSERT?.rows) - ? `value->>${this.string(`$[${this.columns.indexOf(name)}]`)}` - : `value->>${this.string(`$.${JSON.stringify(name)}`)}` + const extract = (INSERT?.entries || UPSERT?.entries) + ? `value->>${this.string(`$.${JSON.stringify(name)}`)}` + : (INSERT?.rows || UPSERT?.rows) + ? `value->>${this.string(`$[${this.columns.indexOf(name)}]`)}` + : `NEW.${this.quote(name)}` // in case of (INSERT?.from || UPSERT?.from) const sql = converter?.(extract) || extract return { extract, sql } } @@ -1587,7 +1589,9 @@ class CQN2SQLRenderer { } managed_default(name, managed, src) { - return `(CASE WHEN json_type(value,${this.managed_extract(name).extract.slice(8)}) IS NULL THEN ${managed} ELSE ${src} END)` + const { UPSERT, INSERT } = this.cqn + const isJson = INSERT?.entries || UPSERT?.entries || INSERT?.rows || UPSERT?.rows + return `(CASE WHEN ${isJson ? `json_type(value,${this.managed_extract(name).extract.slice(8)})` : `NEW.${this.quote(name)}`} IS NULL THEN ${managed} ELSE ${src} END)` } } diff --git a/hana/lib/HANAService.js b/hana/lib/HANAService.js index 557e19af1..bee6d7b36 100644 --- a/hana/lib/HANAService.js +++ b/hana/lib/HANAService.js @@ -73,8 +73,8 @@ class HANAService extends SQLService { try { await require('@sap/cds-mtxs/lib').xt.serviceManager.get(tenant, { disableCache: true, invalidCredentials: credentials, retryUntil: deadline }) } catch (smErr) { - smErr.cause = err - throw new Error(`Failed connecting to pool - could not get valid credentials from Service Manager`, { cause: smErr }) + smErr.cause = err + throw new Error(`Failed connecting to pool - could not get valid credentials from Service Manager`, { cause: smErr }) } if (Date.now() < deadline) return create(tenant, start) else throw new Error(`Pool exceeded for '${tenant}' within ${acquireTimeoutMillis}ms`, { cause: err }) @@ -603,7 +603,7 @@ class HANAService extends SQLService { // if (col.ref?.length === 1) { col.ref.unshift(parent.as) } if (col.ref?.length > 1) { const colName = this.column_name(col) - + const isSource = from => { if (from.as === col.ref[0]) return true return from.args?.some(a => { @@ -893,12 +893,12 @@ 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 aliasedQuery = cds.ql.SELECT - .columns(src.SELECT.columns - .map((c, i) => ({ ref: [this.column_name(c)], as: this.columns[i] })) - ) - .from(src) - sql = `SELECT ${mixing} FROM (${this.SELECT(aliasedQuery)}) AS NEW LEFT JOIN ${this.quote(entity)} AS OLD ON ${keyCompare}` + const aliasedQuery = `SELECT ${[ + ...src.SELECT.columns.map((c, i) => this.column_expr({ ref: [this.column_name(c)], as: this.columns[i] })), + ...managed.slice(src.SELECT.columns.length).map(c => `${elements[c.name].key ? c.onInsert : 'NULL'} AS ${this.quote(c.name)}`), // fill in missing default values + ]} FROM (${this.SELECT(src)})` + + sql = `SELECT ${mixing} FROM (${aliasedQuery}) AS NEW LEFT JOIN ${this.quote(entity)} AS OLD ON ${keyCompare}` this.entries = [this.values] } @@ -1206,7 +1206,9 @@ SELECT ${mixing} FROM JSON_TABLE(SRC.JSON, '$' COLUMNS(${extraction}) ERROR ON E } managed_default(name, managed, src) { - return `(CASE WHEN ${this.quote('$.' + name)} IS NULL THEN ${managed} ELSE ${src} END)` + const { UPSERT, INSERT } = this.cqn + const isJson = INSERT?.entries || UPSERT?.entries || INSERT?.rows || UPSERT?.rows + return `(CASE WHEN ${isJson ? this.quote('$.' + name) : `NEW.${this.quote(name)}`} IS NULL THEN ${managed} ELSE ${src} END)` } render_with() { diff --git a/postgres/lib/PostgresService.js b/postgres/lib/PostgresService.js index d25a408cd..b41ba5bfa 100644 --- a/postgres/lib/PostgresService.js +++ b/postgres/lib/PostgresService.js @@ -459,7 +459,9 @@ GROUP BY k } managed_default(name, managed, src) { - return `(CASE WHEN json_typeof(value->${this.managed_extract(name).extract.slice(8)}) IS NULL THEN ${managed} ELSE ${src} END)` + const { UPSERT, INSERT } = this.cqn + const isJson = INSERT?.entries || UPSERT?.entries || INSERT?.rows || UPSERT?.rows + return `(CASE WHEN ${isJson ? `json_typeof(value->${this.managed_extract(name).extract.slice(8)})` : `NEW.${this.quote(name)}`} IS NULL THEN ${managed} ELSE ${src} END)` } param({ ref }) { diff --git a/sqlite/test/general/managed.test.js b/sqlite/test/general/managed.test.js index 5a3f2d959..76fb6b096 100644 --- a/sqlite/test/general/managed.test.js +++ b/sqlite/test/general/managed.test.js @@ -125,6 +125,26 @@ describe('Managed thingies', () => { expect(updateTime).to.be.greaterThan(insertTime) }) + test('INSERT.into().from(SELECT) applies managed and default values', async () => { + await INSERT.into('test.foo').entries({ ID: 99, defaultValue: 50 }) + + // INSERT.from(SELECT) with only a subset of columns + await INSERT.into('test.foo').from( + cds.ql`SELECT ID + 900 as ID from test.foo where ID = 99`, + ) + + const result = await SELECT.from('test.foo').where({ ID: 999 }) + expect(result).to.have.length(1) + expect(result[0]).to.containSubset({ + ID: 999, + defaultValue: 100, // default applied, not carried over from source + createdBy: 'anonymous', + modifiedBy: 'anonymous', + }) + expect(result[0].createdAt).to.be.a('string') + expect(result[0].modifiedAt).to.be.a('string') + }) + test('managed attributes are shared within a transaction', async () => { const db = await cds.connect.to('db') const tx = db.tx({ user: { id: 'tom' } }) diff --git a/test/compliance/INSERT.test.js b/test/compliance/INSERT.test.js index dc8aa1923..1ab16c629 100644 --- a/test/compliance/INSERT.test.js +++ b/test/compliance/INSERT.test.js @@ -175,8 +175,8 @@ describe('INSERT', () => { .from(cds.ql`SELECT id || '-' || default as ID FROM ${keys} WHERE id = ${1}`) const select = await SELECT.from(cuid).orderBy('ID') expect(select).deep.eq([ - {ID:'1-defaulted'}, - {ID:'1-overwritten'}, + { ID: '1-defaulted' }, + { ID: '1-overwritten' }, ]) }) @@ -194,6 +194,49 @@ describe('INSERT', () => { const select = await SELECT.from(Alter).where('number = 42') expect(select[0]).to.eql({ ID: 1, number: 42, order_ID: null }) }) + + test('defaults', async () => { + const { cuid, keys, default: _default } = cds.entities('basic.common') + + // fill other table first + const ID = '1234' + await cds.run(INSERT([{ ID }]).into(cuid)) + await INSERT.into(keys).from(cds.ql`SELECT cast(ID as Integer) as id FROM ${cuid} WHERE ID = ${ID}`) + await INSERT.into(keys).from(cds.ql`SELECT cast(ID as Integer) as id, 'overwritten' as default FROM ${cuid} WHERE ID = ${ID}`) + + // default key column + const select = await SELECT.from(keys).where`id = ${ID}`.orderBy('default') + expect(select).deep.eq([ + { id: 1234, default: 'defaulted', data: null }, + { id: 1234, default: 'overwritten', data: null }, + ]) + + // default column + await INSERT.into(_default).from(cds.ql`SELECT ID FROM ${cuid} WHERE ID = ${ID}`) + const defaultInsert = await SELECT.from(_default).where`ID = ${ID}` + expect(defaultInsert).deep.eq([{ + ID, + uuidDflt: '00000000-0000-0000-4000-000000000000', + bool: false, + integer8: 8, + integer16: 9, + integer32: 10, + integer64: '11', + double: 1.1, + float: '1.1', + decimal: '1.1111', + string: 'default', + char: 'd', + short: 'default', + medium: 'default', + large: 'default', + date: '1970-01-01', + date_lit: '2021-05-05', + time: '01:02:03', + dateTime: '1970-01-01T01:02:03Z', + timestamp: '1970-01-01T01:02:03.123Z', + }]) + }) }) test('InsertResult', async () => { diff --git a/test/compliance/UPSERT.test.js b/test/compliance/UPSERT.test.js index fb53640f7..2218b1092 100644 --- a/test/compliance/UPSERT.test.js +++ b/test/compliance/UPSERT.test.js @@ -88,6 +88,42 @@ describe('UPSERT', () => { { ID: '1-overwritten' }, ]) }) + + test('defaults', async () => { + const { cuid, keys, default: _default } = cds.entities('basic.common') + + const ID = '1234' + // fill other table first + await cds.run(UPSERT([{ ID }]).into(cuid)) + + // default key column + await UPSERT.into(keys).from(cds.ql`SELECT cast(ID as Integer) as id FROM ${cuid} WHERE ID = ${ID}`) + await UPSERT.into(keys).from(cds.ql`SELECT cast(ID as Integer) as id, 'overwritten' as default FROM ${cuid} WHERE ID = ${ID}`) + + const keysAfter = await SELECT.from(keys).where`id = ${ID}`.orderBy('default') + expect(keysAfter).deep.eq([ + { id: 1234, default: 'defaulted', data: null }, + { id: 1234, default: 'overwritten', data: null }, + ]) + + // default column + const unchangedQuery = UPSERT.into(_default).from(cds.ql`SELECT ID FROM ${cuid} WHERE ID = ${ID}`) + const changedQuery = UPSERT.into(_default).from(cds.ql`SELECT ID, 'c' as char FROM ${cuid} WHERE ID = ${ID}`) + + await unchangedQuery.clone() + const defaultInsert = await SELECT.from(_default).where`ID = ${ID}` + await unchangedQuery.clone() + const defaultUnchanged = await SELECT.from(_default).where`ID = ${ID}` + expect(defaultUnchanged).deep.eq(defaultInsert) + + await changedQuery.clone() + const defaultChanged = await SELECT.from(_default).where`ID = ${ID}` + expect(defaultChanged).property(0).property('char').eq('c') + + await unchangedQuery.clone() + const defaultUnchangedChanged = await SELECT.from(_default).where`ID = ${ID}` + expect(defaultChanged).deep.eq(defaultUnchangedChanged) + }) }) test('affected row', async () => {