@cap-js/db-service 2.5.0 → 2.5.1
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 +7 -0
- package/lib/SQLService.js +7 -6
- package/lib/cqn2sql.js +29 -114
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
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.5.1](https://github.com/cap-js/cds-dbs/compare/db-service-v2.5.0...db-service-v2.5.1) (2025-09-30)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
* revert own resolve ([#1366](https://github.com/cap-js/cds-dbs/issues/1366)) ([9037570](https://github.com/cap-js/cds-dbs/commit/9037570c5dda08eb8bc168c0a68045ef9fc85a9f))
|
|
13
|
+
|
|
7
14
|
## [2.5.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.4.0...db-service-v2.5.0) (2025-09-30)
|
|
8
15
|
|
|
9
16
|
|
package/lib/SQLService.js
CHANGED
|
@@ -2,7 +2,7 @@ const cds = require('@sap/cds'),
|
|
|
2
2
|
DEBUG = cds.debug('sql|db')
|
|
3
3
|
const { Readable, Transform } = require('stream')
|
|
4
4
|
const { pipeline } = require('stream/promises')
|
|
5
|
-
const { getDBTable, getTransition } = require('@sap/cds/libx/_runtime/common/utils/resolveView')
|
|
5
|
+
const { resolveView, getDBTable, getTransition } = require('@sap/cds/libx/_runtime/common/utils/resolveView')
|
|
6
6
|
const DatabaseService = require('./common/DatabaseService')
|
|
7
7
|
const cqn4sql = require('./cqn4sql')
|
|
8
8
|
|
|
@@ -230,9 +230,7 @@ class SQLService extends DatabaseService {
|
|
|
230
230
|
// REVISIT: It's not yet 100 % clear under which circumstances we can rely on db constraints
|
|
231
231
|
return (super.onDELETE = /* cds.env.features.assert_integrity === 'db' ? this.onSIMPLE : */ deep_delete)
|
|
232
232
|
async function deep_delete(/** @type {Request} */ req) {
|
|
233
|
-
const
|
|
234
|
-
// REVISIT: remove fallback when cds.dbs requires cds >= 9.3
|
|
235
|
-
const transitions = resolve?.transitions4db ? resolve.transitions4db(req.query, false) : getTransition(req.target, this, false, req.query.cmd || 'DELETE')
|
|
233
|
+
const transitions = getTransition(req.target, this, false, req.query.cmd || 'DELETE')
|
|
236
234
|
if (transitions.target !== transitions.queryTarget) {
|
|
237
235
|
const keys = []
|
|
238
236
|
const transitionsTarget = transitions.queryTarget.keys || transitions.queryTarget.elements
|
|
@@ -255,8 +253,7 @@ class SQLService extends DatabaseService {
|
|
|
255
253
|
})
|
|
256
254
|
return this.onDELETE({ query, target: transitions.target })
|
|
257
255
|
}
|
|
258
|
-
|
|
259
|
-
const table = resolve?.table ? resolve.table(req.target) : getDBTable(req.target)
|
|
256
|
+
const table = getDBTable(req.target)
|
|
260
257
|
const { compositions } = table
|
|
261
258
|
if (compositions) {
|
|
262
259
|
// Transform CQL`DELETE from Foo[p1] WHERE p2` into CQL`DELETE from Foo[p1 and p2]`
|
|
@@ -407,6 +404,10 @@ class SQLService extends DatabaseService {
|
|
|
407
404
|
*/
|
|
408
405
|
cqn2sql(query, values) {
|
|
409
406
|
let q = this.cqn4sql(query)
|
|
407
|
+
let kind = q.kind || Object.keys(q)[0]
|
|
408
|
+
if (kind in { INSERT: 1, DELETE: 1, UPSERT: 1, UPDATE: 1 }) {
|
|
409
|
+
q = resolveView(q, this.model, this) // REVISIT: before resolveView was called on flat cqn obtained from cqn4sql -> is it correct to call on original q instead?
|
|
410
|
+
}
|
|
410
411
|
let cqn2sql = new this.class.CQN2SQL(this)
|
|
411
412
|
return cqn2sql.render(q, values)
|
|
412
413
|
}
|
package/lib/cqn2sql.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const cds = require('@sap/cds')
|
|
2
2
|
const cds_infer = require('./infer')
|
|
3
3
|
const cqn4sql = require('./cqn4sql')
|
|
4
|
-
const { getDBTable, getTransition } = require('@sap/cds/libx/_runtime/common/utils/resolveView')
|
|
5
4
|
const _simple_queries = cds.env.features.sql_simple_queries
|
|
6
5
|
const _strict_booleans = _simple_queries < 2
|
|
7
6
|
|
|
@@ -27,7 +26,7 @@ class CQN2SQLRenderer {
|
|
|
27
26
|
if (cds.env.sql.names === 'quoted') {
|
|
28
27
|
this.class.prototype.name = (name, query) => {
|
|
29
28
|
const e = name.id || name
|
|
30
|
-
return (this.model?.definitions[e])?.['@cds.persistence.name'] || e
|
|
29
|
+
return (query?._target || this.model?.definitions[e])?.['@cds.persistence.name'] || e
|
|
31
30
|
}
|
|
32
31
|
this.class.prototype.quote = (s) => `"${String(s).replace(/"/g, '""')}"`
|
|
33
32
|
}
|
|
@@ -91,6 +90,7 @@ class CQN2SQLRenderer {
|
|
|
91
90
|
if (vars && Object.keys(vars).length && !this.values?.length) this.values = vars
|
|
92
91
|
const sanitize_values = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
|
|
93
92
|
|
|
93
|
+
|
|
94
94
|
if (DEBUG && (LOG_SQL._debug || LOG_SQLITE._debug)) {
|
|
95
95
|
let values = sanitize_values && (this.entries || this.values?.length > 0) ? ['***'] : this.entries || this.values || []
|
|
96
96
|
if (values && !Array.isArray(values)) {
|
|
@@ -722,49 +722,6 @@ class CQN2SQLRenderer {
|
|
|
722
722
|
return this.xpr({ xpr })
|
|
723
723
|
}
|
|
724
724
|
|
|
725
|
-
/**
|
|
726
|
-
* Renders a transformed where clause that maps the query target view to the source table
|
|
727
|
-
* @param {import('./infer/cqn').source} from
|
|
728
|
-
* @param {import('./infer/cqn').predicate} where
|
|
729
|
-
* @param {import('./infer/cqn').query} q
|
|
730
|
-
* @returns SQL
|
|
731
|
-
*/
|
|
732
|
-
where_resolved(from, where, q) {
|
|
733
|
-
// REVISIT: remove fallback when cds.dbs requires cds >= 9.3
|
|
734
|
-
const kind = q.kind || (
|
|
735
|
-
q.SELECT ? 'SELECT' :
|
|
736
|
-
q.INSERT ? 'INSERT' :
|
|
737
|
-
q.UPSERT ? 'UPSERT' :
|
|
738
|
-
q.UPDATE ? 'UPDATE' :
|
|
739
|
-
q.DELETE ? 'DELETE' :
|
|
740
|
-
undefined
|
|
741
|
-
)
|
|
742
|
-
const transitions = this.srv.resolve?.transitions4db ? this.srv.resolve.transitions4db(q) : getTransition(q._target, this.srv, false, kind)
|
|
743
|
-
if (transitions.target === transitions.queryTarget) return this.where(where)
|
|
744
|
-
|
|
745
|
-
// view and table column refs to be matched
|
|
746
|
-
const viewCols = []
|
|
747
|
-
const tableCols = []
|
|
748
|
-
|
|
749
|
-
// Only match key columns when possible
|
|
750
|
-
const elements = q._target.keys || q._target.elements
|
|
751
|
-
for (const c in elements) {
|
|
752
|
-
if (
|
|
753
|
-
c in elements
|
|
754
|
-
&& transitions.mapping.has(c)
|
|
755
|
-
&& !elements[c].virtual
|
|
756
|
-
&& !elements[c].value
|
|
757
|
-
&& !elements[c].isAssociation
|
|
758
|
-
) {
|
|
759
|
-
viewCols.push({ ref: [c] })
|
|
760
|
-
tableCols.push(transitions.mapping.get(c))
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
return tableCols.length > 0
|
|
764
|
-
? this.where([{ list: tableCols }, 'in', SELECT.from(from).columns(viewCols).where(where)])
|
|
765
|
-
: this.where(where)
|
|
766
|
-
}
|
|
767
|
-
|
|
768
725
|
/**
|
|
769
726
|
* Renders a HAVING clause into generic SQL
|
|
770
727
|
* @param {import('./infer/cqn').predicate} xpr
|
|
@@ -870,22 +827,15 @@ class CQN2SQLRenderer {
|
|
|
870
827
|
if (!elements && !INSERT.entries?.length) {
|
|
871
828
|
return // REVISIT: mtx sends an insert statement without entries and no reference entity
|
|
872
829
|
}
|
|
873
|
-
// REVISIT: remove fallback when cds.dbs requires cds >= 9.3
|
|
874
|
-
const transitions = this.srv.resolve?.transitions4db ? this.srv.resolve.transitions4db(q) : getTransition(q._target, this.srv, false, 'INSERT')
|
|
875
830
|
const columns = elements
|
|
876
|
-
? ObjectKeys(elements).filter(c =>
|
|
877
|
-
&& c in transitions.target.elements
|
|
878
|
-
&& !transitions.target.elements[c].virtual
|
|
879
|
-
&& !transitions.target.elements[c].value
|
|
880
|
-
&& !transitions.target.elements[c].isAssociation
|
|
881
|
-
)
|
|
831
|
+
? ObjectKeys(elements).filter(c => c in elements && !elements[c].virtual && !elements[c].value && !elements[c].isAssociation)
|
|
882
832
|
: ObjectKeys(INSERT.entries[0])
|
|
883
833
|
|
|
884
834
|
/** @type {string[]} */
|
|
885
835
|
this.columns = columns
|
|
886
836
|
|
|
887
837
|
const alias = INSERT.into.as
|
|
888
|
-
const entity =
|
|
838
|
+
const entity = this.name(q._target?.name || INSERT.into.ref[0], q)
|
|
889
839
|
if (!elements) {
|
|
890
840
|
this.entries = INSERT.entries.map(e => columns.map(c => e[c]))
|
|
891
841
|
const param = this.param.bind(this, { ref: ['?'] })
|
|
@@ -907,8 +857,8 @@ class CQN2SQLRenderer {
|
|
|
907
857
|
}
|
|
908
858
|
|
|
909
859
|
const extractions = this._managed = this.managed(columns.map(c => ({ name: c })), elements)
|
|
910
|
-
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(
|
|
911
|
-
}) SELECT ${extractions.
|
|
860
|
+
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))
|
|
861
|
+
}) SELECT ${extractions.map(c => c.insert)} FROM json_each(?)`)
|
|
912
862
|
}
|
|
913
863
|
|
|
914
864
|
async *INSERT_entries_stream(entries, binaryEncoding = 'base64') {
|
|
@@ -1014,7 +964,7 @@ class CQN2SQLRenderer {
|
|
|
1014
964
|
*/
|
|
1015
965
|
INSERT_rows(q) {
|
|
1016
966
|
const { INSERT } = q
|
|
1017
|
-
const entity =
|
|
967
|
+
const entity = this.name(q._target?.name || INSERT.into.ref[0], q)
|
|
1018
968
|
const alias = INSERT.into.as
|
|
1019
969
|
const elements = q.elements || q._target?.elements
|
|
1020
970
|
const columns = this.columns = INSERT.columns || cds.error`Cannot insert rows without columns or elements`
|
|
@@ -1038,10 +988,8 @@ class CQN2SQLRenderer {
|
|
|
1038
988
|
const extraction = (this._managed = this.managed(columns.map(c => ({ name: c })), elements))
|
|
1039
989
|
.slice(0, columns.length)
|
|
1040
990
|
.map(c => c.converter(c.extract))
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const transitions = this.srv.resolve?.transitions4db ? this.srv.resolve.transitions4db(q) : getTransition(q._target, this.srv, false, 'INSERT')
|
|
1044
|
-
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))
|
|
991
|
+
|
|
992
|
+
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))
|
|
1045
993
|
}) SELECT ${extraction} FROM json_each(?)`)
|
|
1046
994
|
}
|
|
1047
995
|
|
|
@@ -1062,26 +1010,15 @@ class CQN2SQLRenderer {
|
|
|
1062
1010
|
*/
|
|
1063
1011
|
INSERT_select(q) {
|
|
1064
1012
|
const { INSERT } = q
|
|
1065
|
-
const entity = q._target
|
|
1013
|
+
const entity = this.name(q._target.name, q)
|
|
1066
1014
|
const alias = INSERT.into.as
|
|
1067
|
-
const src = this.cqn4sql(INSERT.from)
|
|
1068
1015
|
const elements = q.elements || q._target?.elements || {}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
&& !transitions.target.elements[c].value
|
|
1076
|
-
&& !transitions.target.elements[c].isAssociation
|
|
1077
|
-
))
|
|
1078
|
-
|
|
1079
|
-
const extractions = this._managed = this.managed(columns.map(c => ({ name: c, sql: `NEW.${this.quote(c)}` })), elements)
|
|
1080
|
-
const sql = extractions.length > columns.length
|
|
1081
|
-
? `SELECT ${extractions.map(c => `${c.insert} AS ${this.quote(c.name)}`)} FROM (${this.SELECT(src)}) AS NEW`
|
|
1082
|
-
: this.SELECT(src)
|
|
1083
|
-
if (extractions.length > columns.length) columns = this.columns = extractions.map(c => c.name)
|
|
1084
|
-
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}`
|
|
1016
|
+
const columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter(
|
|
1017
|
+
c => c in elements && !elements[c].virtual && !elements[c].isAssociation,
|
|
1018
|
+
))
|
|
1019
|
+
this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns.map(c => this.quote(c))}) ${this.SELECT(
|
|
1020
|
+
this.cqn4sql(INSERT.from || INSERT.as),
|
|
1021
|
+
)}`
|
|
1085
1022
|
this.entries = [this.values]
|
|
1086
1023
|
return this.sql
|
|
1087
1024
|
}
|
|
@@ -1141,7 +1078,7 @@ class CQN2SQLRenderer {
|
|
|
1141
1078
|
.filter(c => keys.includes(c.name))
|
|
1142
1079
|
.map(c => `${c.onInsert || c.sql} as ${this.quote(c.name)}`)
|
|
1143
1080
|
|
|
1144
|
-
const entity =
|
|
1081
|
+
const entity = this.name(q._target?.name || UPSERT.into.ref[0], q)
|
|
1145
1082
|
sql = `SELECT ${managed.map(c => c.upsert
|
|
1146
1083
|
.replace(/value->/g, '"$$$$value$$$$"->')
|
|
1147
1084
|
.replace(/json_type\(value,/g, 'json_type("$$$$value$$$$",'))
|
|
@@ -1157,9 +1094,7 @@ class CQN2SQLRenderer {
|
|
|
1157
1094
|
else return true
|
|
1158
1095
|
}).map(c => `${this.quote(c)} = excluded.${this.quote(c)}`)
|
|
1159
1096
|
|
|
1160
|
-
|
|
1161
|
-
const transitions = this.srv.resolve.transitions4db ? this.srv.resolve.transitions4db(q) : getTransition(q._target, this.srv, false, 'UPDATE')
|
|
1162
|
-
return (this.sql = `INSERT INTO ${this.quote(entity)} (${columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c))}) ${sql
|
|
1097
|
+
return (this.sql = `INSERT INTO ${this.quote(entity)} (${columns.map(c => this.quote(c))}) ${sql
|
|
1163
1098
|
} WHERE TRUE ON CONFLICT(${keys.map(c => this.quote(c))}) DO ${updateColumns.length ? `UPDATE SET ${updateColumns}` : 'NOTHING'}`)
|
|
1164
1099
|
}
|
|
1165
1100
|
|
|
@@ -1172,37 +1107,29 @@ class CQN2SQLRenderer {
|
|
|
1172
1107
|
*/
|
|
1173
1108
|
UPDATE(q) {
|
|
1174
1109
|
const { entity, with: _with, data, where } = q.UPDATE
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
const elements = q._target?.elements
|
|
1178
|
-
let sql = `UPDATE ${this.quote(this.table_name(q))}`
|
|
1110
|
+
const elements = q._target?.elements
|
|
1111
|
+
let sql = `UPDATE ${this.quote(this.name(entity.ref?.[0] || entity, q))}`
|
|
1179
1112
|
if (entity.as) sql += ` AS ${this.quote(entity.as)}`
|
|
1180
1113
|
|
|
1181
1114
|
let columns = []
|
|
1182
1115
|
if (data) _add(data, val => this.val({ val }))
|
|
1183
1116
|
if (_with) _add(_with, x => this.expr(x))
|
|
1184
1117
|
function _add(data, sql4) {
|
|
1185
|
-
for (let
|
|
1186
|
-
const
|
|
1187
|
-
|
|
1188
|
-
&& c in transitions.target.elements
|
|
1189
|
-
&& !transitions.target.elements[c].virtual
|
|
1190
|
-
&& !transitions.target.elements[c].value
|
|
1191
|
-
&& !transitions.target.elements[c].isAssociation
|
|
1118
|
+
for (let c in data) {
|
|
1119
|
+
const columnExistsInDatabase =
|
|
1120
|
+
elements && c in elements && !elements[c].virtual && !elements[c].isAssociation && !elements[c].value
|
|
1192
1121
|
if (!elements || columnExistsInDatabase) {
|
|
1193
|
-
columns.push({ name: c, sql: sql4(data[
|
|
1122
|
+
columns.push({ name: c, sql: sql4(data[c]) })
|
|
1194
1123
|
}
|
|
1195
1124
|
}
|
|
1196
1125
|
}
|
|
1197
1126
|
|
|
1198
1127
|
const extraction = this.managed(columns, elements)
|
|
1199
|
-
.filter((c, i) =>
|
|
1200
|
-
|
|
1201
|
-
return columns[i] || c.onUpdate
|
|
1202
|
-
}).map((c, i) => `${this.quote(transitions.mapping.get(c.name)?.ref?.[0] || c.name)}=${!columns[i] ? c.onUpdate : c.sql}`)
|
|
1128
|
+
.filter((c, i) => columns[i] || c.onUpdate)
|
|
1129
|
+
.map((c, i) => `${this.quote(c.name)}=${!columns[i] ? c.onUpdate : c.sql}`)
|
|
1203
1130
|
|
|
1204
1131
|
sql += ` SET ${extraction}`
|
|
1205
|
-
if (where) sql += ` WHERE ${this.
|
|
1132
|
+
if (where) sql += ` WHERE ${this.where(where)}`
|
|
1206
1133
|
return (this.sql = sql)
|
|
1207
1134
|
}
|
|
1208
1135
|
|
|
@@ -1214,9 +1141,8 @@ class CQN2SQLRenderer {
|
|
|
1214
1141
|
* @returns {string} SQL
|
|
1215
1142
|
*/
|
|
1216
1143
|
DELETE(q) {
|
|
1217
|
-
const { DELETE: {
|
|
1218
|
-
let sql = `DELETE FROM ${this.
|
|
1219
|
-
if (from.as) sql += ` AS ${this.quote(from.as)}`
|
|
1144
|
+
const { DELETE: { from, where } } = q
|
|
1145
|
+
let sql = `DELETE FROM ${this.from(from, q)}`
|
|
1220
1146
|
if (where) sql += ` WHERE ${this.where(where)}`
|
|
1221
1147
|
return (this.sql = sql)
|
|
1222
1148
|
}
|
|
@@ -1429,17 +1355,6 @@ class CQN2SQLRenderer {
|
|
|
1429
1355
|
return (typeof col.as === 'string' && col.as) || ('val' in col && col.val + '') || col.func || col.ref.at(-1)
|
|
1430
1356
|
}
|
|
1431
1357
|
|
|
1432
|
-
/**
|
|
1433
|
-
* Calculates the Database table name of the query target
|
|
1434
|
-
* @param {import('./infer/cqn').Query} query
|
|
1435
|
-
* @returns {string} Database table name
|
|
1436
|
-
*/
|
|
1437
|
-
table_name(q) {
|
|
1438
|
-
// REVISIT: remove fallback when cds.dbs requires cds >= 9.3
|
|
1439
|
-
const table = cds.db.resolve?.table ? cds.db.resolve.table(q._target) : getDBTable(q._target)
|
|
1440
|
-
return this.name(table.name, q)
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
1358
|
/**
|
|
1444
1359
|
* Calculates the Database name of the given name
|
|
1445
1360
|
* @param {string|import('./infer/cqn').ref} name
|
package/package.json
CHANGED