@cap-js/db-service 2.5.1 → 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 +14 -0
- package/lib/cqn2sql.js +7 -1
- 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,20 @@
|
|
|
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
|
+
|
|
7
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)
|
|
8
22
|
|
|
9
23
|
|
package/lib/cqn2sql.js
CHANGED
|
@@ -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
|
}
|
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