@cap-js/db-service 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@
4
4
  - The format is based on [Keep a Changelog](http://keepachangelog.com/).
5
5
  - This project adheres to [Semantic Versioning](http://semver.org/).
6
6
 
7
+ ## [2.8.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.7.0...db-service-v2.8.0) (2025-12-15)
8
+
9
+
10
+ ### Added
11
+
12
+ * Added support to use `UPSERT` from `SELECT` ([#1435](https://github.com/cap-js/cds-dbs/issues/1435)) ([68f3db8](https://github.com/cap-js/cds-dbs/commit/68f3db8d79aa120768fe81324cd164782b9eec1b))
13
+
14
+
15
+ ### Fixed
16
+
17
+ * propagte target to subquery ([#1438](https://github.com/cap-js/cds-dbs/issues/1438)) ([5460e43](https://github.com/cap-js/cds-dbs/commit/5460e4398079750ec3afec9a1747007618d23ecd))
18
+
7
19
  ## [2.7.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.6.0...db-service-v2.7.0) (2025-11-26)
8
20
 
9
21
 
package/lib/cqn2sql.js CHANGED
@@ -393,11 +393,11 @@ class CQN2SQLRenderer {
393
393
  const alias = stableFrom.as
394
394
  const source = () => {
395
395
  return ({
396
- func: 'HIERARCHY',
397
- args: [{ xpr: ['SOURCE', { SELECT: { columns: columnsIn, from: stableFrom } }, ...(orderBy ? ['SIBLING', 'ORDER', 'BY', `${this.orderBy(orderBy)}`] : [])] }],
398
- as: alias
399
- })
400
- }
396
+ func: 'HIERARCHY',
397
+ args: [{ xpr: ['SOURCE', { SELECT: { columns: columnsIn, from: stableFrom } }, ...(orderBy ? ['SIBLING', 'ORDER', 'BY', `${this.orderBy(orderBy)}`] : [])] }],
398
+ as: alias
399
+ })
400
+ }
401
401
 
402
402
  const expandedByNr = { list: [] } // DistanceTo(...,null)
403
403
  const expandedByOne = { list: [] } // DistanceTo(...,1)
@@ -1019,12 +1019,17 @@ class CQN2SQLRenderer {
1019
1019
  const entity = this.name(q._target.name, q)
1020
1020
  const alias = INSERT.into.as
1021
1021
  const elements = q.elements || q._target?.elements || {}
1022
- const columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter(
1022
+ let columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter(
1023
1023
  c => c in elements && !elements[c].virtual && !elements[c].isAssociation,
1024
1024
  ))
1025
- this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns.map(c => this.quote(c))}) ${this.SELECT(
1026
- this.cqn4sql(INSERT.from || INSERT.as),
1027
- )}`
1025
+
1026
+ const src = this.cqn4sql(INSERT.from)
1027
+ const extractions = this._managed = this.managed(columns.map(c => ({ name: c, sql: `NEW.${this.quote(c)}` })), elements)
1028
+ const sql = extractions.length > columns.length
1029
+ ? `SELECT ${extractions.map(c => `${c.insert} AS ${this.quote(c.name)}`)} FROM (${this.SELECT(src)}) AS NEW`
1030
+ : this.SELECT(src)
1031
+ if (extractions.length > columns.length) columns = this.columns = extractions.map(c => c.name)
1032
+ this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns.map(c => this.quote(c))}) ${sql}`
1028
1033
  this.entries = [this.values]
1029
1034
  return this.sql
1030
1035
  }
@@ -1077,18 +1082,32 @@ class CQN2SQLRenderer {
1077
1082
  .map(k => `NEW.${this.quote(k)}=OLD.${this.quote(k)}`)
1078
1083
  .join(' AND ')
1079
1084
 
1080
- const columns = this.columns // this.columns is computed as part of this.INSERT
1081
- const managed = this._managed.slice(0, columns.length)
1085
+ let columns = this.columns // this.columns is computed as part of this.INSERT
1086
+ const entity = this.name(q._target?.name || UPSERT.into.ref[0], q)
1087
+ if (UPSERT.entries || UPSERT.rows || UPSERT.values) {
1088
+ const managed = this._managed.slice(0, columns.length)
1082
1089
 
1083
- const extractkeys = managed
1084
- .filter(c => keys.includes(c.name))
1085
- .map(c => `${c.onInsert || c.sql} as ${this.quote(c.name)}`)
1090
+ const extractkeys = managed
1091
+ .filter(c => keys.includes(c.name))
1092
+ .map(c => `${c.onInsert || c.sql} as ${this.quote(c.name)}`)
1086
1093
 
1087
- const entity = this.name(q._target?.name || UPSERT.into.ref[0], q)
1088
- sql = `SELECT ${managed.map(c => c.upsert
1089
- .replace(/value->/g, '"$$$$value$$$$"->')
1090
- .replace(/json_type\(value,/g, 'json_type("$$$$value$$$$",'))
1091
- } FROM (SELECT value as "$$value$$", ${extractkeys} from json_each(?)) as NEW LEFT JOIN ${this.quote(entity)} AS OLD ON ${keyCompare}`
1094
+ sql = `SELECT ${managed.map(c => c.upsert
1095
+ .replace(/value->/g, '"$$$$value$$$$"->')
1096
+ .replace(/json_type\(value,/g, 'json_type("$$$$value$$$$",'))
1097
+ } FROM (SELECT value as "$$value$$", ${extractkeys} from json_each(?)) as NEW LEFT JOIN ${this.quote(entity)} AS OLD ON ${keyCompare}`
1098
+ } else {
1099
+ const extractions = this._managed
1100
+ if (this.values) this.values = [] // Clear previously computed values
1101
+ const src = this.cqn4sql(UPSERT.from || UPSERT.as)
1102
+ const aliasedQuery = cds.ql.SELECT
1103
+ .columns(src.SELECT.columns
1104
+ .map((c, i) => ({ ref: [this.column_name(c)], as: this.columns[i] }))
1105
+ )
1106
+ .from(src)
1107
+ sql = `SELECT ${extractions.map(c => `${c.upsert}`)} FROM (${this.SELECT(aliasedQuery)}) AS NEW LEFT JOIN ${this.quote(entity)} AS OLD ON ${keyCompare}`
1108
+ if (extractions.length > columns.length) columns = this.columns = extractions.map(c => c.name)
1109
+ this.entries = [this.values]
1110
+ }
1092
1111
 
1093
1112
  const updateColumns = columns.filter(c => {
1094
1113
  if (keys.includes(c)) return false //> keys go into ON CONFLICT clause
@@ -1326,7 +1345,7 @@ class CQN2SQLRenderer {
1326
1345
  } else {
1327
1346
  cds.error`Invalid arguments provided for function '${func}' (${args})`
1328
1347
  }
1329
- const fn = this.class.Functions[func]?.apply(this, Array.isArray(args) ? args: [args]) || `${func}(${args})`
1348
+ const fn = this.class.Functions[func]?.apply(this, Array.isArray(args) ? args : [args]) || `${func}(${args})`
1330
1349
  if (xpr) return `${fn} ${this.xpr({ xpr })}`
1331
1350
  return fn
1332
1351
  }
@@ -1430,7 +1449,7 @@ class CQN2SQLRenderer {
1430
1449
 
1431
1450
  let onInsert = this.managed_session_context(element[cdsOnInsert]?.['='])
1432
1451
  || this.managed_session_context(element.default?.ref?.[0])
1433
- || (element.default && { __proto__: element.default, param: false })
1452
+ || (element.default && { __proto__: element.default, param: false })
1434
1453
  let onUpdate = this.managed_session_context(element[cdsOnUpdate]?.['='])
1435
1454
 
1436
1455
  if (onInsert) onInsert = this.expr(onInsert)
package/lib/cqn4sql.js CHANGED
@@ -281,7 +281,11 @@ function cqn4sql(originalQuery, model) {
281
281
  const alreadySeen = new Map()
282
282
  inferred.joinTree._roots.forEach(r => {
283
283
  const args = []
284
- if (r.queryArtifact.SELECT) args.push({ SELECT: transformSubquery(r.queryArtifact).SELECT, as: r.alias })
284
+ if (r.queryArtifact.SELECT) {
285
+ const transformedSubquery = transformSubquery(r.queryArtifact)
286
+ transformedSubquery.as = r.alias
287
+ args.push(transformedSubquery)
288
+ }
285
289
  else {
286
290
  const id = getLocalizedName(r.queryArtifact)
287
291
  args.push({ ref: [r.args ? { id, args: r.args } : id], as: r.alias })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "CDS base database service",
5
5
  "homepage": "https://github.com/cap-js/cds-dbs/tree/main/db-service#cds-base-database-service",
6
6
  "repository": {