@cap-js/db-service 1.14.1 → 1.15.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 +13 -0
- package/lib/cqn4sql.js +23 -8
- package/lib/infer/index.js +55 -38
- package/lib/infer/join-tree.js +1 -0
- package/lib/search.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
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
|
+
## [1.15.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.14.1...db-service-v1.15.0) (2024-11-14)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* enable path expressions in infix filter after `exists` predicate ([#875](https://github.com/cap-js/cds-dbs/issues/875)) ([7e50359](https://github.com/cap-js/cds-dbs/commit/7e5035932ac3bf39f052aa67e1565567e9d6b1ad))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
* **`search`:** ignore invalid path expressions inside `@cds.search` ([#849](https://github.com/cap-js/cds-dbs/issues/849)) ([250edd5](https://github.com/cap-js/cds-dbs/commit/250edd5ec9f7ba1d8e40e1330e4b4f9ad9e599b0))
|
|
18
|
+
* nested exists wrapped in xpr ([7e50359](https://github.com/cap-js/cds-dbs/commit/7e5035932ac3bf39f052aa67e1565567e9d6b1ad))
|
|
19
|
+
|
|
7
20
|
## [1.14.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.14.0...db-service-v1.14.1) (2024-10-28)
|
|
8
21
|
|
|
9
22
|
|
package/lib/cqn4sql.js
CHANGED
|
@@ -266,10 +266,10 @@ function cqn4sql(originalQuery, model) {
|
|
|
266
266
|
const id = localized(r.queryArtifact)
|
|
267
267
|
args.push({ ref: [r.args ? { id, args: r.args } : id], as: r.alias })
|
|
268
268
|
}
|
|
269
|
-
from = { join: 'left', args, on: [] }
|
|
269
|
+
from = { join: r.join || 'left', args, on: [] }
|
|
270
270
|
r.children.forEach(c => {
|
|
271
271
|
from = joinForBranch(from, c)
|
|
272
|
-
from = { join: 'left', args: [from], on: [] }
|
|
272
|
+
from = { join: c.join || 'left', args: [from], on: [] }
|
|
273
273
|
})
|
|
274
274
|
})
|
|
275
275
|
return from.args.length > 1 ? from : from.args[0]
|
|
@@ -309,7 +309,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
309
309
|
}
|
|
310
310
|
if (node.children) {
|
|
311
311
|
node.children.forEach(c => {
|
|
312
|
-
lhs = { join: 'left', args: [lhs], on: [] }
|
|
312
|
+
lhs = { join: c.join || 'left', args: [lhs], on: [] }
|
|
313
313
|
lhs = joinForBranch(lhs, c)
|
|
314
314
|
})
|
|
315
315
|
}
|
|
@@ -2093,11 +2093,6 @@ function cqn4sql(originalQuery, model) {
|
|
|
2093
2093
|
const unmanagedOn = onCondFor(inWhere ? next : current, inWhere ? current : next, inWhere)
|
|
2094
2094
|
on.push(...(customWhere && hasLogicalOr(unmanagedOn) ? [asXpr(unmanagedOn)] : unmanagedOn))
|
|
2095
2095
|
}
|
|
2096
|
-
// infix filter conditions are wrapped in `xpr` when added to the on-condition
|
|
2097
|
-
if (customWhere) {
|
|
2098
|
-
const filter = getTransformedTokenStream(customWhere, next)
|
|
2099
|
-
on.push(...['and', ...(hasLogicalOr(filter) ? [asXpr(filter)] : filter)])
|
|
2100
|
-
}
|
|
2101
2096
|
|
|
2102
2097
|
const subquerySource = assocTarget(nextDefinition) || nextDefinition
|
|
2103
2098
|
const id = localized(subquerySource)
|
|
@@ -2115,6 +2110,26 @@ function cqn4sql(originalQuery, model) {
|
|
|
2115
2110
|
],
|
|
2116
2111
|
where: on,
|
|
2117
2112
|
}
|
|
2113
|
+
if (next.pathExpressionInsideFilter) {
|
|
2114
|
+
SELECT.where = customWhere
|
|
2115
|
+
const transformedExists = transformSubquery({ SELECT })
|
|
2116
|
+
// infix filter conditions are wrapped in `xpr` when added to the on-condition
|
|
2117
|
+
if (transformedExists.SELECT.where) {
|
|
2118
|
+
on.push(
|
|
2119
|
+
...[
|
|
2120
|
+
'and',
|
|
2121
|
+
...(hasLogicalOr(transformedExists.SELECT.where)
|
|
2122
|
+
? [asXpr(transformedExists.SELECT.where)]
|
|
2123
|
+
: transformedExists.SELECT.where),
|
|
2124
|
+
],
|
|
2125
|
+
)
|
|
2126
|
+
}
|
|
2127
|
+
transformedExists.SELECT.where = on
|
|
2128
|
+
return transformedExists.SELECT
|
|
2129
|
+
} else if (customWhere) {
|
|
2130
|
+
const filter = getTransformedTokenStream(customWhere, next)
|
|
2131
|
+
on.push(...['and', ...(hasLogicalOr(filter) ? [asXpr(filter)] : filter)])
|
|
2132
|
+
}
|
|
2118
2133
|
return SELECT
|
|
2119
2134
|
}
|
|
2120
2135
|
|
package/lib/infer/index.js
CHANGED
|
@@ -184,16 +184,13 @@ function infer(originalQuery, model) {
|
|
|
184
184
|
if (e.target) {
|
|
185
185
|
// only fk access in infix filter
|
|
186
186
|
const nextStep = ref[1]?.id || ref[1]
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
if (isNonForeignKeyNavigation(e, nextStep)) {
|
|
188
|
+
if (expandOrExists) {
|
|
189
|
+
Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
|
|
190
|
+
} else {
|
|
191
|
+
rejectNonFkNavigation(e, e.on ? $baseLink.definition.name : nextStep)
|
|
192
|
+
}
|
|
191
193
|
}
|
|
192
|
-
// no non-fk traversal in infix filter
|
|
193
|
-
if (!expandOrExists && nextStep && !isForeignKeyOf(nextStep, e))
|
|
194
|
-
throw new Error(
|
|
195
|
-
`Only foreign keys of “${e.name}” can be accessed in infix filter, but found “${nextStep}”`,
|
|
196
|
-
)
|
|
197
194
|
}
|
|
198
195
|
arg.$refLinks.push({ definition: e, target: definition })
|
|
199
196
|
// filter paths are flattened
|
|
@@ -226,7 +223,7 @@ function infer(originalQuery, model) {
|
|
|
226
223
|
// don't miss an exists within an expression
|
|
227
224
|
token.xpr.forEach(walkTokenStream)
|
|
228
225
|
} else {
|
|
229
|
-
attachRefLinksToArg(token, arg.$refLinks[i], existsPredicate)
|
|
226
|
+
attachRefLinksToArg(token, arg.$refLinks[i], existsPredicate || expandOrExists)
|
|
230
227
|
existsPredicate = false
|
|
231
228
|
}
|
|
232
229
|
}
|
|
@@ -235,6 +232,7 @@ function infer(originalQuery, model) {
|
|
|
235
232
|
}
|
|
236
233
|
i += 1
|
|
237
234
|
}
|
|
235
|
+
if ($baseLink?.pathExpressionInsideFilter) Object.defineProperty(arg, 'join', { value: 'inner' })
|
|
238
236
|
const { definition, target } = arg.$refLinks[arg.$refLinks.length - 1]
|
|
239
237
|
if (definition.value) {
|
|
240
238
|
// nested calculated element
|
|
@@ -542,9 +540,19 @@ function infer(originalQuery, model) {
|
|
|
542
540
|
const elements = getDefinition(definition.target)?.elements || definition.elements
|
|
543
541
|
if (elements && id in elements) {
|
|
544
542
|
const element = elements[id]
|
|
545
|
-
|
|
543
|
+
if (inInfixFilter) {
|
|
544
|
+
const nextStep = column.ref[1]?.id || column.ref[1]
|
|
545
|
+
if (isNonForeignKeyNavigation(element, nextStep)) {
|
|
546
|
+
if (inExists) {
|
|
547
|
+
Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
|
|
548
|
+
} else {
|
|
549
|
+
rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
546
553
|
const resolvableIn = getDefinition(definition.target) || target
|
|
547
|
-
|
|
554
|
+
const $refLink = { definition: elements[id], target: resolvableIn }
|
|
555
|
+
column.$refLinks.push($refLink)
|
|
548
556
|
} else {
|
|
549
557
|
stepNotFoundInPredecessor(id, definition.name)
|
|
550
558
|
}
|
|
@@ -593,7 +601,16 @@ function infer(originalQuery, model) {
|
|
|
593
601
|
|
|
594
602
|
const target = getDefinition(definition.target) || column.$refLinks[i - 1].target
|
|
595
603
|
if (element) {
|
|
596
|
-
if ($baseLink)
|
|
604
|
+
if ($baseLink && inInfixFilter) {
|
|
605
|
+
const nextStep = column.ref[i + 1]?.id || column.ref[i + 1]
|
|
606
|
+
if (isNonForeignKeyNavigation(element, nextStep)) {
|
|
607
|
+
if (inExists) {
|
|
608
|
+
Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
|
|
609
|
+
} else {
|
|
610
|
+
rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
597
614
|
const $refLink = { definition: elements[id], target }
|
|
598
615
|
column.$refLinks.push($refLink)
|
|
599
616
|
} else if (firstStepIsSelf) {
|
|
@@ -637,7 +654,7 @@ function infer(originalQuery, model) {
|
|
|
637
654
|
skipJoinsForFilter = true
|
|
638
655
|
} else if (token.ref || token.xpr) {
|
|
639
656
|
inferQueryElement(token, false, column.$refLinks[i], {
|
|
640
|
-
inExists: skipJoinsForFilter,
|
|
657
|
+
inExists: skipJoinsForFilter || inExists,
|
|
641
658
|
inExpr: !!token.xpr,
|
|
642
659
|
inInfixFilter: true,
|
|
643
660
|
})
|
|
@@ -646,7 +663,7 @@ function infer(originalQuery, model) {
|
|
|
646
663
|
applyToFunctionArgs(token.args, inferQueryElement, [
|
|
647
664
|
false,
|
|
648
665
|
column.$refLinks[i],
|
|
649
|
-
{ inExists: skipJoinsForFilter, inExpr: true, inInfixFilter: true },
|
|
666
|
+
{ inExists: skipJoinsForFilter || inExists, inExpr: true, inInfixFilter: true },
|
|
650
667
|
])
|
|
651
668
|
}
|
|
652
669
|
}
|
|
@@ -700,31 +717,11 @@ function infer(originalQuery, model) {
|
|
|
700
717
|
}
|
|
701
718
|
}
|
|
702
719
|
}
|
|
703
|
-
|
|
704
|
-
/**
|
|
705
|
-
* Check if the next step in the ref is foreign key of `assoc`
|
|
706
|
-
* if not, an error is thrown.
|
|
707
|
-
*
|
|
708
|
-
* @param {CSN.Element} assoc if this is an association, the next step must be a foreign key of the element.
|
|
709
|
-
*/
|
|
710
|
-
function rejectNonFkAccess(assoc) {
|
|
711
|
-
if (inInfixFilter && assoc.target) {
|
|
712
|
-
// only fk access in infix filter
|
|
713
|
-
const nextStep = column.ref[i + 1]?.id || column.ref[i + 1]
|
|
714
|
-
// no unmanaged assoc in infix filter path
|
|
715
|
-
if (!inExists && assoc.on) {
|
|
716
|
-
const err = `Unexpected unmanaged association “${assoc.name}” in filter expression of “${$baseLink.definition.name}”`
|
|
717
|
-
throw new Error(err)
|
|
718
|
-
}
|
|
719
|
-
// no non-fk traversal in infix filter in non-exists path
|
|
720
|
-
if (nextStep && !assoc.on && !isForeignKeyOf(nextStep, assoc))
|
|
721
|
-
throw new Error(
|
|
722
|
-
`Only foreign keys of “${assoc.name}” can be accessed in infix filter, but found “${nextStep}”`,
|
|
723
|
-
)
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
720
|
})
|
|
727
721
|
|
|
722
|
+
// we need inner joins for the path expressions inside filter expressions after exists predicate
|
|
723
|
+
if ($baseLink?.pathExpressionInsideFilter) Object.defineProperty(column, 'join', { value: 'inner' })
|
|
724
|
+
|
|
728
725
|
// ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
|
|
729
726
|
if (column.expand) {
|
|
730
727
|
const { $refLinks } = column
|
|
@@ -1214,6 +1211,26 @@ function infer(originalQuery, model) {
|
|
|
1214
1211
|
}
|
|
1215
1212
|
}
|
|
1216
1213
|
|
|
1214
|
+
/**
|
|
1215
|
+
* Determines if a given association is a non-foreign key navigation.
|
|
1216
|
+
*
|
|
1217
|
+
* @param {Object} assoc - The association.
|
|
1218
|
+
* @param {Object} nextStep - The next step in the navigation path.
|
|
1219
|
+
* @returns {boolean} - Returns true if the next step is a non-foreign key navigation, otherwise false.
|
|
1220
|
+
*/
|
|
1221
|
+
function isNonForeignKeyNavigation(assoc, nextStep) {
|
|
1222
|
+
if (!nextStep || !assoc.target) return false
|
|
1223
|
+
|
|
1224
|
+
return assoc.on || !isForeignKeyOf(nextStep, assoc)
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function rejectNonFkNavigation(assoc, additionalInfo) {
|
|
1228
|
+
if (assoc.on) {
|
|
1229
|
+
throw new Error(`Unexpected unmanaged association “${assoc.name}” in filter expression of “${additionalInfo}”`)
|
|
1230
|
+
}
|
|
1231
|
+
throw new Error(`Only foreign keys of “${assoc.name}” can be accessed in infix filter, but found “${additionalInfo}”`)
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1217
1234
|
/**
|
|
1218
1235
|
* Returns true if e is a foreign key of assoc.
|
|
1219
1236
|
* this function is also compatible with unfolded csn (UCSN),
|
package/lib/infer/join-tree.js
CHANGED
|
@@ -181,6 +181,7 @@ class JoinTree {
|
|
|
181
181
|
// if no root node was found, the column is selected from a subquery
|
|
182
182
|
if (!node) return
|
|
183
183
|
while (i < col.ref.length) {
|
|
184
|
+
if(col.join === 'inner') node.join = 'inner'
|
|
184
185
|
const step = col.ref[i]
|
|
185
186
|
const { where, args } = step
|
|
186
187
|
const id = joinId(step, args, where)
|
package/lib/search.js
CHANGED
|
@@ -114,7 +114,7 @@ const _getSearchableColumns = entity => {
|
|
|
114
114
|
deepSearchCandidates.forEach(c => {
|
|
115
115
|
const element = c.ref.reduce((resolveIn, curr, i) => {
|
|
116
116
|
const next = resolveIn.elements?.[curr] || resolveIn._target.elements[curr]
|
|
117
|
-
if (next
|
|
117
|
+
if (next?.isAssociation && !c.ref[i + 1]) {
|
|
118
118
|
const searchInTarget = _getSearchableColumns(next._target)
|
|
119
119
|
searchInTarget.forEach(elementRefInTarget => {
|
|
120
120
|
searchableColumns.push({ ref: c.ref.concat(...elementRefInTarget.ref) })
|
package/package.json
CHANGED