@cap-js/db-service 2.4.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 +22 -0
- package/lib/cql-functions.js +1 -1
- package/lib/cqn2sql.js +1 -0
- package/lib/cqn4sql.js +43 -26
- package/lib/search.js +9 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@
|
|
|
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
|
+
|
|
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)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
* make hana server version accessible to sub classes ([#1263](https://github.com/cap-js/cds-dbs/issues/1263)) ([a3ccc3e](https://github.com/cap-js/cds-dbs/commit/a3ccc3ed2fd6a65f1fd5924756a4a7b965adf9a3))
|
|
20
|
+
* sets default to hana cloud if server version cannot be detected ([a3ccc3e](https://github.com/cap-js/cds-dbs/commit/a3ccc3ed2fd6a65f1fd5924756a4a7b965adf9a3))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
* **`@cds.search`:** no duplicates for search along `to-many` paths ([#1341](https://github.com/cap-js/cds-dbs/issues/1341)) ([5c5f4fb](https://github.com/cap-js/cds-dbs/commit/5c5f4fbf790f718c3cf1bcb6f3bf7be421be598f))
|
|
26
|
+
* associations in `[@cds](https://github.com/cds).search` are additive ([#1355](https://github.com/cap-js/cds-dbs/issues/1355)) ([ea931cb](https://github.com/cap-js/cds-dbs/commit/ea931cb120c2857aa18a4eb68b893926c0999a9f))
|
|
27
|
+
* set proper element link for path into fk ([#1344](https://github.com/cap-js/cds-dbs/issues/1344)) ([9f365d3](https://github.com/cap-js/cds-dbs/commit/9f365d35ac614969d8fd2c2a9a1a2e0cd643969d))
|
|
28
|
+
|
|
7
29
|
## [2.4.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.3.0...db-service-v2.4.0) (2025-08-27)
|
|
8
30
|
|
|
9
31
|
|
package/lib/cql-functions.js
CHANGED
package/lib/cqn2sql.js
CHANGED
|
@@ -17,6 +17,7 @@ class CQN2SQLRenderer {
|
|
|
17
17
|
* @param {import('@sap/cds/apis/services').ContextProperties} context the cds.context of the request
|
|
18
18
|
*/
|
|
19
19
|
constructor(srv) {
|
|
20
|
+
this.srv = srv
|
|
20
21
|
this.context = srv?.context || cds.context // Using srv.context is required due to stakeholders doing unmanaged txs without cds.context being set
|
|
21
22
|
this.class = new.target // for IntelliSense
|
|
22
23
|
this.class._init() // is a noop for subsequent calls
|
package/lib/cqn4sql.js
CHANGED
|
@@ -61,7 +61,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
61
61
|
if (!hasCustomJoins && inferred.SELECT?.search) {
|
|
62
62
|
// we need an instance of query because the elements of the query are needed for the calculation of the search columns
|
|
63
63
|
if (!inferred.SELECT.elements) Object.setPrototypeOf(inferred, SELECT.class.prototype)
|
|
64
|
-
const searchTerm =
|
|
64
|
+
const searchTerm = getSearch(inferred.SELECT.search, inferred)
|
|
65
65
|
if (searchTerm) {
|
|
66
66
|
// Search target can be a navigation, in that case use _target to get the correct entity
|
|
67
67
|
const { where, having } = transformSearch(searchTerm)
|
|
@@ -123,14 +123,9 @@ function cqn4sql(originalQuery, model) {
|
|
|
123
123
|
// calculate the primary keys of the target entity, there is always exactly
|
|
124
124
|
// one query source for UPDATE / DELETE
|
|
125
125
|
const queryTarget = Object.values(inferred.sources)[0].definition
|
|
126
|
-
const primaryKey = { list:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (e.key === true && !e.virtual && e.isAssociation !== true) {
|
|
130
|
-
subquery.SELECT.columns.push({ ref: [e.name] })
|
|
131
|
-
primaryKey.list.push({ ref: [transformedFrom.as, e.name] })
|
|
132
|
-
}
|
|
133
|
-
}
|
|
126
|
+
const primaryKey = { list: getPrimaryKey(queryTarget, uniqueSubqueryAlias) }
|
|
127
|
+
// match primary keys of the target entity with the subquery
|
|
128
|
+
primaryKey.list.forEach(k => subquery.SELECT.columns.push({ ref: k.ref.slice(1) }))
|
|
134
129
|
|
|
135
130
|
const transformedSubquery = cqn4sql(subquery, model)
|
|
136
131
|
|
|
@@ -258,7 +253,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
258
253
|
if (inferred.SELECT[prop]) {
|
|
259
254
|
return { [prop]: [asXpr(inferred.SELECT.where), 'and', searchTerm] }
|
|
260
255
|
} else {
|
|
261
|
-
return { [prop]: [searchTerm] }
|
|
256
|
+
return { [prop]: searchTerm.xpr ? [...searchTerm.xpr] : [searchTerm] }
|
|
262
257
|
}
|
|
263
258
|
}
|
|
264
259
|
|
|
@@ -1211,7 +1206,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1211
1206
|
if(column.element && !isAssocOrStruct(column.element)) {
|
|
1212
1207
|
columnAlias = column.as || leafAssocIndex === -1 ? columnAlias : column.ref.slice(leafAssocIndex - 1).map(idOnly).join('_')
|
|
1213
1208
|
const res = { ref: [tableAlias, calculateElementName(column)], as: columnAlias }
|
|
1214
|
-
setElementOnColumns(res, element)
|
|
1209
|
+
setElementOnColumns(res, column.element)
|
|
1215
1210
|
return [res]
|
|
1216
1211
|
}
|
|
1217
1212
|
|
|
@@ -1789,7 +1784,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1789
1784
|
filterConditions.forEach(f => {
|
|
1790
1785
|
transformedWhere.push('and')
|
|
1791
1786
|
if (filterConditions.length > 1) transformedWhere.push(asXpr(f))
|
|
1792
|
-
else if (f.length > 3 || f.includes('or') || f.includes('and')) transformedWhere.push(asXpr(f))
|
|
1787
|
+
else if (f.length > 3 || f.includes('or') || f.includes('and') || f.includes('in')) transformedWhere.push(asXpr(f))
|
|
1793
1788
|
else transformedWhere.push(...f)
|
|
1794
1789
|
})
|
|
1795
1790
|
} else {
|
|
@@ -2221,30 +2216,41 @@ function cqn4sql(originalQuery, model) {
|
|
|
2221
2216
|
return SELECT
|
|
2222
2217
|
}
|
|
2223
2218
|
|
|
2224
|
-
|
|
2225
|
-
* For a given search
|
|
2226
|
-
*
|
|
2219
|
+
/**
|
|
2220
|
+
* For a given search term calculate a search expression which can be used in a where clause.
|
|
2221
|
+
* The search function is pushed to a subquery and the primary key(s) of the entity is/are used to match
|
|
2222
|
+
* the search results of the subquery.
|
|
2227
2223
|
*
|
|
2228
|
-
* @param {object}
|
|
2224
|
+
* @param {object} searchTerm - The search expression which shall be applied to the searchable columns on the query source.
|
|
2229
2225
|
* @param {object} query - The FROM clause of the CQN statement.
|
|
2230
2226
|
*
|
|
2231
2227
|
* @returns {(Object|null)} returns either:
|
|
2228
|
+
* - an expression of the form `<primaryKey> in (select <primaryKey> from <entity> where search(<searchableColumns>, <searchTerm>))`
|
|
2232
2229
|
* - a function with two arguments: The first one being the list of searchable columns, the second argument holds the search expression.
|
|
2233
2230
|
* - or null, if no searchable columns are found in neither in `@cds.search` nor in the target entity itself.
|
|
2234
2231
|
*/
|
|
2235
|
-
function
|
|
2232
|
+
function getSearch(searchTerm, query) {
|
|
2236
2233
|
const entity = query.SELECT.from.SELECT ? query.SELECT.from : cds.infer.target(query) // REVISIT: we should reliably use inferred._target instead
|
|
2237
2234
|
const searchIn = computeColumnsToBeSearched(inferred, entity)
|
|
2238
|
-
if (searchIn.length
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
return null
|
|
2235
|
+
if (searchIn.length === 0) return null
|
|
2236
|
+
|
|
2237
|
+
const searchFunc = {
|
|
2238
|
+
func: 'search',
|
|
2239
|
+
args: [
|
|
2240
|
+
searchIn.length === 1 ? searchIn[0] : { list: searchIn },
|
|
2241
|
+
searchTerm.length === 1 && 'val' in searchTerm[0] ? searchTerm[0] : { xpr: searchTerm },
|
|
2242
|
+
],
|
|
2247
2243
|
}
|
|
2244
|
+
// for aggregated queries / search on subqueries we do not do a subquery search
|
|
2245
|
+
if (inferred.SELECT.groupBy || entity.SELECT)
|
|
2246
|
+
return searchFunc
|
|
2247
|
+
|
|
2248
|
+
const matchColumns = getPrimaryKey(entity)
|
|
2249
|
+
if (matchColumns.length === 0 || searchIn.every(r => r.ref.length === 1)) // keyless or not deep, fallback to old behavior
|
|
2250
|
+
return searchFunc
|
|
2251
|
+
|
|
2252
|
+
const subquery = SELECT.from(entity).columns(...matchColumns).where(searchFunc)
|
|
2253
|
+
return { xpr: [ matchColumns.length === 1 ? matchColumns[0] : {list: matchColumns}, 'in', subquery] }
|
|
2248
2254
|
}
|
|
2249
2255
|
|
|
2250
2256
|
/**
|
|
@@ -2410,6 +2416,17 @@ function setElementOnColumns(col, element) {
|
|
|
2410
2416
|
defineProperty(col, 'element', element)
|
|
2411
2417
|
}
|
|
2412
2418
|
|
|
2419
|
+
function getPrimaryKey(entity, tableAlias = null) {
|
|
2420
|
+
const primaryKey = []
|
|
2421
|
+
for (const k of Object.keys(entity.elements)) {
|
|
2422
|
+
const e = entity.elements[k]
|
|
2423
|
+
if (e.key === true && !e.virtual && e.isAssociation !== true) {
|
|
2424
|
+
primaryKey.push({ ref: tableAlias ? [tableAlias, e.name] : [e.name] })
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
return primaryKey
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2413
2430
|
const getName = col => col.as || col.ref?.at(-1)
|
|
2414
2431
|
const idOnly = ref => ref.id || ref
|
|
2415
2432
|
const refWithConditions = step => {
|
package/lib/search.js
CHANGED
|
@@ -66,7 +66,7 @@ const _getSearchableColumns = entity => {
|
|
|
66
66
|
if (key.startsWith(cdsSearchTerm)) cdsSearchKeys.push(key)
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
let
|
|
69
|
+
let skipDefaultSearchableElements = false
|
|
70
70
|
const deepSearchCandidates = []
|
|
71
71
|
|
|
72
72
|
// build a map of columns annotated with the @cds.search annotation
|
|
@@ -74,13 +74,18 @@ const _getSearchableColumns = entity => {
|
|
|
74
74
|
const columnName = key.split(cdsSearchTerm + '.').pop()
|
|
75
75
|
const annotationKey = `${cdsSearchTerm}.${columnName}`
|
|
76
76
|
const annotationValue = entity[annotationKey]
|
|
77
|
-
if (annotationValue) atLeastOneColumnIsSearchable = true
|
|
78
77
|
|
|
79
78
|
const column = entity.elements[columnName]
|
|
79
|
+
// always ignore virtual elements from search
|
|
80
|
+
if(column?.virtual) continue
|
|
80
81
|
if (column?.isAssociation || columnName.includes('.')) {
|
|
81
|
-
|
|
82
|
+
const ref = columnName.split('.')
|
|
83
|
+
if(ref.length > 1) skipDefaultSearchableElements = true
|
|
84
|
+
deepSearchCandidates.push({ ref })
|
|
82
85
|
continue
|
|
83
86
|
}
|
|
87
|
+
|
|
88
|
+
if(annotationValue) skipDefaultSearchableElements = true
|
|
84
89
|
cdsSearchColumnMap.set(columnName, annotationValue)
|
|
85
90
|
}
|
|
86
91
|
|
|
@@ -99,7 +104,7 @@ const _getSearchableColumns = entity => {
|
|
|
99
104
|
// if at least one element is explicitly annotated as searchable, e.g.:
|
|
100
105
|
// `@cds.search { element1: true }` or `@cds.search { element1 }`
|
|
101
106
|
// and it is not the current column name, then it must be excluded from the search
|
|
102
|
-
if (
|
|
107
|
+
if (skipDefaultSearchableElements) return false
|
|
103
108
|
|
|
104
109
|
// the element is considered searchable if it is explicitly annotated as such or
|
|
105
110
|
// if it is not annotated and the column is typed as a string (excluding elements/elements expressions)
|
package/package.json
CHANGED