Skip to content
26 changes: 15 additions & 11 deletions db-service/lib/cqn2sql.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
Expand Down Expand Up @@ -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]
}
Expand Down Expand Up @@ -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 }
}
Expand All @@ -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)`
}
}

Expand Down
22 changes: 12 additions & 10 deletions hana/lib/HANAService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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]
}

Expand Down Expand Up @@ -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() {
Expand Down
4 changes: 3 additions & 1 deletion postgres/lib/PostgresService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) {
Expand Down
20 changes: 20 additions & 0 deletions sqlite/test/general/managed.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' } })
Expand Down
47 changes: 45 additions & 2 deletions test/compliance/INSERT.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
])
})

Expand All @@ -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 () => {
Expand Down
36 changes: 36 additions & 0 deletions test/compliance/UPSERT.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down