@cap-js/db-service 2.5.0 → 2.6.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 +21 -0
- package/lib/SQLService.js +7 -6
- package/lib/cqn2sql.js +36 -115
- package/lib/cqn4sql.js +45 -29
- package/lib/deep-queries.js +1 -14
- package/lib/search.js +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@
|
|
|
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.6.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.5.1...db-service-v2.6.0) (2025-10-23)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* **`flattening`:** allow flattening of `n`-ary structures in `list` ([#1337](https://github.com/cap-js/cds-dbs/issues/1337)) ([7ec18f2](https://github.com/cap-js/cds-dbs/commit/7ec18f24dba80ba31ad4e46f816c17fa64cba91a))
|
|
13
|
+
* **`flattening`:** allow flattening of structures with exactly one leaf ([7ec18f2](https://github.com/cap-js/cds-dbs/commit/7ec18f24dba80ba31ad4e46f816c17fa64cba91a))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
* **`@cds.search`:** properly exclude an association from being searched ([#1385](https://github.com/cap-js/cds-dbs/issues/1385)) ([9ed4245](https://github.com/cap-js/cds-dbs/commit/9ed42458417c15dc33409befd0ef40889f04a69f))
|
|
19
|
+
* tree table with expand ([#1363](https://github.com/cap-js/cds-dbs/issues/1363)) ([bdad412](https://github.com/cap-js/cds-dbs/commit/bdad412f0362165b532ce35261773e5ecc7c696a))
|
|
20
|
+
|
|
21
|
+
## [2.5.1](https://github.com/cap-js/cds-dbs/compare/db-service-v2.5.0...db-service-v2.5.1) (2025-09-30)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
* revert own resolve ([#1366](https://github.com/cap-js/cds-dbs/issues/1366)) ([9037570](https://github.com/cap-js/cds-dbs/commit/9037570c5dda08eb8bc168c0a68045ef9fc85a9f))
|
|
27
|
+
|
|
7
28
|
## [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
29
|
|
|
9
30
|
|
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)) {
|
|
@@ -350,7 +350,13 @@ class CQN2SQLRenderer {
|
|
|
350
350
|
if (element['@Core.Computed'] && name in availableComputedColumns) continue
|
|
351
351
|
if (name.toUpperCase() in reservedColumnNames) ref.as = `$$${name}$$`
|
|
352
352
|
columnsIn.push(ref)
|
|
353
|
-
|
|
353
|
+
const foreignkey4 = element._foreignKey4
|
|
354
|
+
if (
|
|
355
|
+
from.args ||
|
|
356
|
+
columnsFiltered.find(c => this.column_name(c) === name) ||
|
|
357
|
+
// foreignkey needs to be included when the association is expanded
|
|
358
|
+
(foreignkey4 && q.SELECT.columns.some(c => c.element?.isAssociation && c.element.name === foreignkey4))
|
|
359
|
+
) {
|
|
354
360
|
columnsOut.push(ref.as ? { ref: [ref.as], as: name } : ref)
|
|
355
361
|
}
|
|
356
362
|
}
|
|
@@ -722,49 +728,6 @@ class CQN2SQLRenderer {
|
|
|
722
728
|
return this.xpr({ xpr })
|
|
723
729
|
}
|
|
724
730
|
|
|
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
731
|
/**
|
|
769
732
|
* Renders a HAVING clause into generic SQL
|
|
770
733
|
* @param {import('./infer/cqn').predicate} xpr
|
|
@@ -870,22 +833,15 @@ class CQN2SQLRenderer {
|
|
|
870
833
|
if (!elements && !INSERT.entries?.length) {
|
|
871
834
|
return // REVISIT: mtx sends an insert statement without entries and no reference entity
|
|
872
835
|
}
|
|
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
836
|
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
|
-
)
|
|
837
|
+
? ObjectKeys(elements).filter(c => c in elements && !elements[c].virtual && !elements[c].value && !elements[c].isAssociation)
|
|
882
838
|
: ObjectKeys(INSERT.entries[0])
|
|
883
839
|
|
|
884
840
|
/** @type {string[]} */
|
|
885
841
|
this.columns = columns
|
|
886
842
|
|
|
887
843
|
const alias = INSERT.into.as
|
|
888
|
-
const entity =
|
|
844
|
+
const entity = this.name(q._target?.name || INSERT.into.ref[0], q)
|
|
889
845
|
if (!elements) {
|
|
890
846
|
this.entries = INSERT.entries.map(e => columns.map(c => e[c]))
|
|
891
847
|
const param = this.param.bind(this, { ref: ['?'] })
|
|
@@ -907,8 +863,8 @@ class CQN2SQLRenderer {
|
|
|
907
863
|
}
|
|
908
864
|
|
|
909
865
|
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.
|
|
866
|
+
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))
|
|
867
|
+
}) SELECT ${extractions.map(c => c.insert)} FROM json_each(?)`)
|
|
912
868
|
}
|
|
913
869
|
|
|
914
870
|
async *INSERT_entries_stream(entries, binaryEncoding = 'base64') {
|
|
@@ -1014,7 +970,7 @@ class CQN2SQLRenderer {
|
|
|
1014
970
|
*/
|
|
1015
971
|
INSERT_rows(q) {
|
|
1016
972
|
const { INSERT } = q
|
|
1017
|
-
const entity =
|
|
973
|
+
const entity = this.name(q._target?.name || INSERT.into.ref[0], q)
|
|
1018
974
|
const alias = INSERT.into.as
|
|
1019
975
|
const elements = q.elements || q._target?.elements
|
|
1020
976
|
const columns = this.columns = INSERT.columns || cds.error`Cannot insert rows without columns or elements`
|
|
@@ -1038,10 +994,8 @@ class CQN2SQLRenderer {
|
|
|
1038
994
|
const extraction = (this._managed = this.managed(columns.map(c => ({ name: c })), elements))
|
|
1039
995
|
.slice(0, columns.length)
|
|
1040
996
|
.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))
|
|
997
|
+
|
|
998
|
+
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))
|
|
1045
999
|
}) SELECT ${extraction} FROM json_each(?)`)
|
|
1046
1000
|
}
|
|
1047
1001
|
|
|
@@ -1062,26 +1016,15 @@ class CQN2SQLRenderer {
|
|
|
1062
1016
|
*/
|
|
1063
1017
|
INSERT_select(q) {
|
|
1064
1018
|
const { INSERT } = q
|
|
1065
|
-
const entity = q._target
|
|
1019
|
+
const entity = this.name(q._target.name, q)
|
|
1066
1020
|
const alias = INSERT.into.as
|
|
1067
|
-
const src = this.cqn4sql(INSERT.from)
|
|
1068
1021
|
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}`
|
|
1022
|
+
const columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter(
|
|
1023
|
+
c => c in elements && !elements[c].virtual && !elements[c].isAssociation,
|
|
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
|
+
)}`
|
|
1085
1028
|
this.entries = [this.values]
|
|
1086
1029
|
return this.sql
|
|
1087
1030
|
}
|
|
@@ -1141,7 +1084,7 @@ class CQN2SQLRenderer {
|
|
|
1141
1084
|
.filter(c => keys.includes(c.name))
|
|
1142
1085
|
.map(c => `${c.onInsert || c.sql} as ${this.quote(c.name)}`)
|
|
1143
1086
|
|
|
1144
|
-
const entity =
|
|
1087
|
+
const entity = this.name(q._target?.name || UPSERT.into.ref[0], q)
|
|
1145
1088
|
sql = `SELECT ${managed.map(c => c.upsert
|
|
1146
1089
|
.replace(/value->/g, '"$$$$value$$$$"->')
|
|
1147
1090
|
.replace(/json_type\(value,/g, 'json_type("$$$$value$$$$",'))
|
|
@@ -1157,9 +1100,7 @@ class CQN2SQLRenderer {
|
|
|
1157
1100
|
else return true
|
|
1158
1101
|
}).map(c => `${this.quote(c)} = excluded.${this.quote(c)}`)
|
|
1159
1102
|
|
|
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
|
|
1103
|
+
return (this.sql = `INSERT INTO ${this.quote(entity)} (${columns.map(c => this.quote(c))}) ${sql
|
|
1163
1104
|
} WHERE TRUE ON CONFLICT(${keys.map(c => this.quote(c))}) DO ${updateColumns.length ? `UPDATE SET ${updateColumns}` : 'NOTHING'}`)
|
|
1164
1105
|
}
|
|
1165
1106
|
|
|
@@ -1172,37 +1113,29 @@ class CQN2SQLRenderer {
|
|
|
1172
1113
|
*/
|
|
1173
1114
|
UPDATE(q) {
|
|
1174
1115
|
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))}`
|
|
1116
|
+
const elements = q._target?.elements
|
|
1117
|
+
let sql = `UPDATE ${this.quote(this.name(entity.ref?.[0] || entity, q))}`
|
|
1179
1118
|
if (entity.as) sql += ` AS ${this.quote(entity.as)}`
|
|
1180
1119
|
|
|
1181
1120
|
let columns = []
|
|
1182
1121
|
if (data) _add(data, val => this.val({ val }))
|
|
1183
1122
|
if (_with) _add(_with, x => this.expr(x))
|
|
1184
1123
|
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
|
|
1124
|
+
for (let c in data) {
|
|
1125
|
+
const columnExistsInDatabase =
|
|
1126
|
+
elements && c in elements && !elements[c].virtual && !elements[c].isAssociation && !elements[c].value
|
|
1192
1127
|
if (!elements || columnExistsInDatabase) {
|
|
1193
|
-
columns.push({ name: c, sql: sql4(data[
|
|
1128
|
+
columns.push({ name: c, sql: sql4(data[c]) })
|
|
1194
1129
|
}
|
|
1195
1130
|
}
|
|
1196
1131
|
}
|
|
1197
1132
|
|
|
1198
1133
|
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}`)
|
|
1134
|
+
.filter((c, i) => columns[i] || c.onUpdate)
|
|
1135
|
+
.map((c, i) => `${this.quote(c.name)}=${!columns[i] ? c.onUpdate : c.sql}`)
|
|
1203
1136
|
|
|
1204
1137
|
sql += ` SET ${extraction}`
|
|
1205
|
-
if (where) sql += ` WHERE ${this.
|
|
1138
|
+
if (where) sql += ` WHERE ${this.where(where)}`
|
|
1206
1139
|
return (this.sql = sql)
|
|
1207
1140
|
}
|
|
1208
1141
|
|
|
@@ -1214,9 +1147,8 @@ class CQN2SQLRenderer {
|
|
|
1214
1147
|
* @returns {string} SQL
|
|
1215
1148
|
*/
|
|
1216
1149
|
DELETE(q) {
|
|
1217
|
-
const { DELETE: {
|
|
1218
|
-
let sql = `DELETE FROM ${this.
|
|
1219
|
-
if (from.as) sql += ` AS ${this.quote(from.as)}`
|
|
1150
|
+
const { DELETE: { from, where } } = q
|
|
1151
|
+
let sql = `DELETE FROM ${this.from(from, q)}`
|
|
1220
1152
|
if (where) sql += ` WHERE ${this.where(where)}`
|
|
1221
1153
|
return (this.sql = sql)
|
|
1222
1154
|
}
|
|
@@ -1429,17 +1361,6 @@ class CQN2SQLRenderer {
|
|
|
1429
1361
|
return (typeof col.as === 'string' && col.as) || ('val' in col && col.val + '') || col.func || col.ref.at(-1)
|
|
1430
1362
|
}
|
|
1431
1363
|
|
|
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
1364
|
/**
|
|
1444
1365
|
* Calculates the Database name of the given name
|
|
1445
1366
|
* @param {string|import('./infer/cqn').ref} name
|
package/lib/cqn4sql.js
CHANGED
|
@@ -96,7 +96,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
96
96
|
|
|
97
97
|
// Transform the existing where, prepend table aliases, and so on...
|
|
98
98
|
if (where) {
|
|
99
|
-
transformedProp.where = getTransformedTokenStream(where)
|
|
99
|
+
transformedProp.where = getTransformedTokenStream(where, { prop: 'where' })
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
// Transform the from clause: association path steps turn into `WHERE EXISTS` subqueries.
|
|
@@ -191,7 +191,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
191
191
|
|
|
192
192
|
// Like the WHERE clause, aliases from the SELECT list are not accessible for `group by`/`having` (in most DB's)
|
|
193
193
|
if (having) {
|
|
194
|
-
transformedQuery.SELECT.having = getTransformedTokenStream(having)
|
|
194
|
+
transformedQuery.SELECT.having = getTransformedTokenStream(having, { prop: 'having' })
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
if (groupBy) {
|
|
@@ -314,7 +314,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
314
314
|
lhs.args.push(arg)
|
|
315
315
|
alreadySeen.set(nextAssoc.$refLink.alias, true)
|
|
316
316
|
if (nextAssoc.where) {
|
|
317
|
-
const filter = getTransformedTokenStream(nextAssoc.where, nextAssoc.$refLink)
|
|
317
|
+
const filter = getTransformedTokenStream(nextAssoc.where, { $baseLink: nextAssoc.$refLink })
|
|
318
318
|
lhs.on = [
|
|
319
319
|
...(hasLogicalOr(lhs.on) ? [asXpr(lhs.on)] : lhs.on),
|
|
320
320
|
'and',
|
|
@@ -521,14 +521,14 @@ function cqn4sql(originalQuery, model) {
|
|
|
521
521
|
}
|
|
522
522
|
}
|
|
523
523
|
|
|
524
|
-
function resolveCalculatedElement(column, omitAlias = false, baseLink = null) {
|
|
524
|
+
function resolveCalculatedElement(column, omitAlias = false, $baseLink = null) {
|
|
525
525
|
let value
|
|
526
526
|
|
|
527
527
|
if (column.$refLinks) {
|
|
528
528
|
const { $refLinks } = column
|
|
529
529
|
value = $refLinks[$refLinks.length - 1].definition.value
|
|
530
530
|
if (column.$refLinks.length > 1) {
|
|
531
|
-
baseLink =
|
|
531
|
+
$baseLink =
|
|
532
532
|
[...$refLinks].reverse().find($refLink => $refLink.definition.isAssociation) ||
|
|
533
533
|
// if there is no association in the path, the table alias is the base link
|
|
534
534
|
// TA might refer to subquery -> we need to propagate the alias to all paths of the calc element
|
|
@@ -541,13 +541,13 @@ function cqn4sql(originalQuery, model) {
|
|
|
541
541
|
|
|
542
542
|
let res
|
|
543
543
|
if (ref) {
|
|
544
|
-
res = getTransformedTokenStream([value], baseLink)[0]
|
|
544
|
+
res = getTransformedTokenStream([value], { $baseLink })[0]
|
|
545
545
|
} else if (xpr) {
|
|
546
|
-
res = { xpr: getTransformedTokenStream(value.xpr, baseLink) }
|
|
546
|
+
res = { xpr: getTransformedTokenStream(value.xpr, { $baseLink }) }
|
|
547
547
|
} else if (val !== undefined) {
|
|
548
548
|
res = { val }
|
|
549
549
|
} else if (func) {
|
|
550
|
-
res = { args: getTransformedFunctionArgs(value.args, baseLink), func: value.func }
|
|
550
|
+
res = { args: getTransformedFunctionArgs(value.args, $baseLink), func: value.func }
|
|
551
551
|
}
|
|
552
552
|
if (!omitAlias) res.as = column.as || column.name || column.flatName
|
|
553
553
|
return res
|
|
@@ -1018,7 +1018,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1018
1018
|
* the result.
|
|
1019
1019
|
*/
|
|
1020
1020
|
if (inOrderBy && flatColumns.length > 1)
|
|
1021
|
-
throw new Error(`
|
|
1021
|
+
throw new Error(`Structured element “${getFullName(leaf)}” expands to multiple fields and can't be used in order by`)
|
|
1022
1022
|
flatColumns.forEach(fc => {
|
|
1023
1023
|
if (col.nulls) fc.nulls = col.nulls
|
|
1024
1024
|
if (col.sort) fc.sort = col.sort
|
|
@@ -1182,7 +1182,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1182
1182
|
if (column.val || column.func || column.SELECT) return [column]
|
|
1183
1183
|
|
|
1184
1184
|
const structsAreUnfoldedAlready = model.meta.unfolded?.includes('structs')
|
|
1185
|
-
let { baseName, columnAlias = column.as, tableAlias } = names
|
|
1185
|
+
let { baseName, columnAlias = column.as, tableAlias } = names || {}
|
|
1186
1186
|
const { exclude, replace } = excludeAndReplace || {}
|
|
1187
1187
|
const { $refLinks, flatName, isJoinRelevant } = column
|
|
1188
1188
|
let firstNonJoinRelevantAssoc, stepAfterAssoc
|
|
@@ -1360,14 +1360,18 @@ function cqn4sql(originalQuery, model) {
|
|
|
1360
1360
|
* @param {object[]} tokenStream - The token stream to transform. Each token in the stream is an
|
|
1361
1361
|
* object representing a CQN construct such as a column, an operator,
|
|
1362
1362
|
* or a subquery.
|
|
1363
|
-
* @param {object} [
|
|
1364
|
-
*
|
|
1365
|
-
*
|
|
1366
|
-
*
|
|
1363
|
+
* @param {object} [context] - Optional context object.
|
|
1364
|
+
* @param {object} [context.$baseLink] - The context in which the `ref`s in the token stream are resolvable.
|
|
1365
|
+
* It serves as the reference point for resolving associations in
|
|
1366
|
+
* statements like `{…} WHERE exists assoc[exists anotherAssoc]`.
|
|
1367
|
+
* Here, the $baseLink for `anotherAssoc` would be `assoc`.
|
|
1368
|
+
* @param {string} [context.prop] - The query property which holds the token stream which shall be
|
|
1369
|
+
* transformed by this function, e.g. "where".
|
|
1367
1370
|
* @returns {object[]} - The transformed token stream.
|
|
1368
1371
|
*/
|
|
1369
|
-
function getTransformedTokenStream(tokenStream,
|
|
1372
|
+
function getTransformedTokenStream(tokenStream, context = {}) {
|
|
1370
1373
|
const transformedTokenStream = []
|
|
1374
|
+
const { $baseLink, /* prop */ } = context
|
|
1371
1375
|
for (let i = 0; i < tokenStream.length; i++) {
|
|
1372
1376
|
const token = tokenStream[i]
|
|
1373
1377
|
if (token === 'exists') {
|
|
@@ -1453,7 +1457,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1453
1457
|
if (list.every(e => e.val))
|
|
1454
1458
|
// no need for transformation
|
|
1455
1459
|
transformedTokenStream.push({ list })
|
|
1456
|
-
else transformedTokenStream.push({ list: getTransformedTokenStream(list, $baseLink) })
|
|
1460
|
+
else transformedTokenStream.push({ list: getTransformedTokenStream(list, { $baseLink, prop: 'list' }) })
|
|
1457
1461
|
}
|
|
1458
1462
|
} else if (tokenStream.length === 1 && token.val && $baseLink) {
|
|
1459
1463
|
// infix filter - OData variant w/o mentioning key --> flatten out and compare each leaf to token.val
|
|
@@ -1509,13 +1513,13 @@ function cqn4sql(originalQuery, model) {
|
|
|
1509
1513
|
i = indexRhs // jump to next relevant index
|
|
1510
1514
|
} else {
|
|
1511
1515
|
// reject associations in expression, except if we are in an infix filter -> $baseLink is set
|
|
1512
|
-
assertNoStructInXpr(token,
|
|
1516
|
+
assertNoStructInXpr(token, context)
|
|
1513
1517
|
// reject virtual elements in expressions as they will lead to a sql error down the line
|
|
1514
1518
|
if (lhsDef?.virtual) throw new Error(`Virtual elements are not allowed in expressions`)
|
|
1515
1519
|
|
|
1516
1520
|
let result = is_regexp(token?.val) ? token : copy(token) // REVISIT: too expensive! //
|
|
1517
1521
|
if (token.ref) {
|
|
1518
|
-
const { definition } = token.$refLinks
|
|
1522
|
+
const { definition } = token.$refLinks.at(-1)
|
|
1519
1523
|
// Add definition to result
|
|
1520
1524
|
setElementOnColumns(result, definition)
|
|
1521
1525
|
if (isCalculatedOnRead(definition)) {
|
|
@@ -1536,7 +1540,15 @@ function cqn4sql(originalQuery, model) {
|
|
|
1536
1540
|
const lastAssoc =
|
|
1537
1541
|
token.isJoinRelevant && [...token.$refLinks].reverse().find(l => l.definition.isAssociation)
|
|
1538
1542
|
const tableAlias = getTableAlias(token, (!lastAssoc?.onlyForeignKeyAccess && lastAssoc) || $baseLink)
|
|
1539
|
-
if
|
|
1543
|
+
if(isAssocOrStruct(definition)) {
|
|
1544
|
+
const flat = getFlatColumnsFor(token, { tableAlias: $baseLink?.alias || tableAlias })
|
|
1545
|
+
if(flat.length === 0)
|
|
1546
|
+
throw new Error(`Structured element “${getFullName(definition)}” expands to nothing and can't be used in expressions`)
|
|
1547
|
+
else if (flat.length > 1 && context.prop !== 'list') // only acceptable in `list`
|
|
1548
|
+
throw new Error(`Structured element “${getFullName(definition)}” expands to multiple fields and can't be used in expressions`)
|
|
1549
|
+
transformedTokenStream.push(...flat)
|
|
1550
|
+
continue
|
|
1551
|
+
} else if ((!$baseLink || lastAssoc) && token.isJoinRelevant) {
|
|
1540
1552
|
let name = calculateElementName(token)
|
|
1541
1553
|
result.ref = [tableAlias, name]
|
|
1542
1554
|
} else if (tableAlias) {
|
|
@@ -1549,7 +1561,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1549
1561
|
result = transformSubquery(token)
|
|
1550
1562
|
} else {
|
|
1551
1563
|
if (token.xpr) {
|
|
1552
|
-
result.xpr = getTransformedTokenStream(token.xpr, $baseLink)
|
|
1564
|
+
result.xpr = getTransformedTokenStream(token.xpr, { $baseLink })
|
|
1553
1565
|
}
|
|
1554
1566
|
if (token.func && token.args) {
|
|
1555
1567
|
result.args = getTransformedFunctionArgs(token.args, $baseLink)
|
|
@@ -1661,11 +1673,15 @@ function cqn4sql(originalQuery, model) {
|
|
|
1661
1673
|
}
|
|
1662
1674
|
}
|
|
1663
1675
|
|
|
1664
|
-
function assertNoStructInXpr(token,
|
|
1665
|
-
|
|
1666
|
-
|
|
1676
|
+
function assertNoStructInXpr(token, context) {
|
|
1677
|
+
const definition = token.$refLinks?.at(-1).definition
|
|
1678
|
+
if(!definition) return
|
|
1679
|
+
const rejectStructs = context && (context.prop in { where: 1, having: 1 })
|
|
1680
|
+
// unmanaged is always forbidden
|
|
1681
|
+
// expanding a ref in a `where`/`having` context
|
|
1682
|
+
if ((rejectStructs && definition?.target) || definition?.on)
|
|
1667
1683
|
rejectAssocInExpression()
|
|
1668
|
-
if (isStructured(
|
|
1684
|
+
if (rejectStructs && isStructured(definition))
|
|
1669
1685
|
// REVISIT: let this through if not requested otherwise
|
|
1670
1686
|
rejectStructInExpression()
|
|
1671
1687
|
|
|
@@ -1771,7 +1787,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1771
1787
|
|
|
1772
1788
|
// only append infix filter to outer where if it is the leaf of the from ref
|
|
1773
1789
|
if (refReverse[0].where)
|
|
1774
|
-
filterConditions.push(getTransformedTokenStream(refReverse[0].where, $refLinksReverse[0]))
|
|
1790
|
+
filterConditions.push(getTransformedTokenStream(refReverse[0].where,{ $baseLink: $refLinksReverse[0] }))
|
|
1775
1791
|
|
|
1776
1792
|
if (existingWhere.length > 0) filterConditions.push(existingWhere)
|
|
1777
1793
|
if (whereExistsSubSelects.length > 0) {
|
|
@@ -2209,7 +2225,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2209
2225
|
}
|
|
2210
2226
|
|
|
2211
2227
|
if (customWhere) {
|
|
2212
|
-
const filter = getTransformedTokenStream(customWhere, next)
|
|
2228
|
+
const filter = getTransformedTokenStream(customWhere, { $baseLink: next })
|
|
2213
2229
|
const wrappedFilter = hasLogicalOr(filter) ? [asXpr(filter)] : filter
|
|
2214
2230
|
on.push('and', ...wrappedFilter)
|
|
2215
2231
|
}
|
|
@@ -2315,10 +2331,10 @@ function cqn4sql(originalQuery, model) {
|
|
|
2315
2331
|
function getTransformedFunctionArgs(args, $baseLink = null) {
|
|
2316
2332
|
let result = null
|
|
2317
2333
|
if (Array.isArray(args)) {
|
|
2318
|
-
result = args.
|
|
2334
|
+
result = args.flatMap(t => {
|
|
2319
2335
|
if (!t.val)
|
|
2320
2336
|
// this must not be touched
|
|
2321
|
-
return getTransformedTokenStream([t], $baseLink)
|
|
2337
|
+
return getTransformedTokenStream([t], { $baseLink })
|
|
2322
2338
|
return t
|
|
2323
2339
|
})
|
|
2324
2340
|
} else if (typeof args === 'object') {
|
|
@@ -2327,7 +2343,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2327
2343
|
const t = args[prop]
|
|
2328
2344
|
if (!t.val)
|
|
2329
2345
|
// this must not be touched
|
|
2330
|
-
result[prop] = getTransformedTokenStream([t], $baseLink)[0]
|
|
2346
|
+
result[prop] = getTransformedTokenStream([t], { $baseLink })[0]
|
|
2331
2347
|
else result[prop] = t
|
|
2332
2348
|
}
|
|
2333
2349
|
}
|
package/lib/deep-queries.js
CHANGED
|
@@ -3,20 +3,7 @@ const { _target_name4 } = require('./SQLService')
|
|
|
3
3
|
|
|
4
4
|
const ROOT = Symbol('root')
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
let _compareJson
|
|
8
|
-
const compareJson = (...args) => {
|
|
9
|
-
if (!_compareJson) {
|
|
10
|
-
try {
|
|
11
|
-
// new path
|
|
12
|
-
_compareJson = require('@sap/cds/libx/_runtime/common/utils/compareJson').compareJson
|
|
13
|
-
} catch {
|
|
14
|
-
// old path
|
|
15
|
-
_compareJson = require('@sap/cds/libx/_runtime/cds-services/services/utils/compareJson').compareJson
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return _compareJson(...args)
|
|
19
|
-
}
|
|
6
|
+
const compareJson = require('@sap/cds/libx/_runtime/common/utils/compareJson').compareJson
|
|
20
7
|
|
|
21
8
|
const handledDeep = Symbol('handledDeep')
|
|
22
9
|
|
package/lib/search.js
CHANGED
|
@@ -79,6 +79,9 @@ const _getSearchableColumns = entity => {
|
|
|
79
79
|
// always ignore virtual elements from search
|
|
80
80
|
if(column?.virtual) continue
|
|
81
81
|
if (column?.isAssociation || columnName.includes('.')) {
|
|
82
|
+
if(!annotationValue)
|
|
83
|
+
continue
|
|
84
|
+
|
|
82
85
|
const ref = columnName.split('.')
|
|
83
86
|
if(ref.length > 1) skipDefaultSearchableElements = true
|
|
84
87
|
deepSearchCandidates.push({ ref })
|
package/package.json
CHANGED