@cap-js/db-service 2.3.0 → 2.4.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 +18 -0
- package/lib/SQLService.js +9 -0
- package/lib/cqn4sql.js +110 -79
- package/lib/infer/index.js +12 -5
- package/lib/infer/join-tree.js +13 -0
- package/lib/utils.js +7 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@
|
|
|
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.4.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.3.0...db-service-v2.4.0) (2025-08-27)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* **`tuple expansion`:** allow structs with exactly one element/fk in comparison ([#1291](https://github.com/cap-js/cds-dbs/issues/1291)) ([75ea826](https://github.com/cap-js/cds-dbs/commit/75ea82694faeafcaf78df9d4b0bbce37b4f65b63))
|
|
13
|
+
* cds.db.foreach uses real object mode streaming ([#1318](https://github.com/cap-js/cds-dbs/issues/1318)) ([cd28b53](https://github.com/cap-js/cds-dbs/commit/cd28b53966dbe28ad1d5ef3827767e78742e0fbd))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
* **`assoc2join`:** target side access detection ([#1282](https://github.com/cap-js/cds-dbs/issues/1282)) ([6f9befa](https://github.com/cap-js/cds-dbs/commit/6f9befa24a06bcc629fe853aa66290613734c3ef))
|
|
19
|
+
* **`cqn4sql`:** only consider `own` property `[@cds](https://github.com/cds).persistence.skip` ([#1324](https://github.com/cap-js/cds-dbs/issues/1324)) ([bd1f52f](https://github.com/cap-js/cds-dbs/commit/bd1f52f67fb4709dce3a27fea8856cb9b875da6b))
|
|
20
|
+
* **`exists`:** do not loose custom where ([#1322](https://github.com/cap-js/cds-dbs/issues/1322)) ([644918c](https://github.com/cap-js/cds-dbs/commit/644918c56d9d939f43f4a0346f42e16722bd6fe9))
|
|
21
|
+
* arithmetic operators can only be used with scalar operands ([#1307](https://github.com/cap-js/cds-dbs/issues/1307)) ([d58d335](https://github.com/cap-js/cds-dbs/commit/d58d33539e22f818d18240bb86ba596fc6fe21d1))
|
|
22
|
+
* detect path expression inside nested xpr after `exists` ([#1292](https://github.com/cap-js/cds-dbs/issues/1292)) ([852d915](https://github.com/cap-js/cds-dbs/commit/852d9155d5bb09a56a6c152259c8282662ceb29d)), closes [#1225](https://github.com/cap-js/cds-dbs/issues/1225)
|
|
23
|
+
* reject comparison of two empty structures ([#1306](https://github.com/cap-js/cds-dbs/issues/1306)) ([d97304d](https://github.com/cap-js/cds-dbs/commit/d97304d95c7f629afe75aba57192277b7124eb3e))
|
|
24
|
+
|
|
7
25
|
## [2.3.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.2.0...db-service-v2.3.0) (2025-07-28)
|
|
8
26
|
|
|
9
27
|
|
package/lib/SQLService.js
CHANGED
|
@@ -355,6 +355,15 @@ class SQLService extends DatabaseService {
|
|
|
355
355
|
return count
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Streaming API variant of .run().
|
|
360
|
+
* @param {import('@sap/cds/apis/cqn').SELECT} query - SELECT CQN
|
|
361
|
+
* @param {function} callback - Function to be invoked for each row
|
|
362
|
+
*/
|
|
363
|
+
foreach (query, callback) {
|
|
364
|
+
return query.foreach(callback)
|
|
365
|
+
}
|
|
366
|
+
|
|
358
367
|
/**
|
|
359
368
|
* Helper class for results of INSERTs.
|
|
360
369
|
* Subclasses may override this.
|
package/lib/cqn4sql.js
CHANGED
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
getImplicitAlias,
|
|
13
13
|
defineProperty,
|
|
14
14
|
getModelUtils,
|
|
15
|
+
hasOwnSkip,
|
|
15
16
|
} = require('./utils')
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -20,14 +21,14 @@ const {
|
|
|
20
21
|
*/
|
|
21
22
|
const eqOps = [['is'], ['='] /* ['=='] */]
|
|
22
23
|
/**
|
|
23
|
-
* For operators of <notEqOps>, do the same but use or instead of and
|
|
24
|
-
* This ensures that not struct == <value
|
|
24
|
+
* For operators of <notEqOps>, do the same but use `or` instead of `and`.
|
|
25
|
+
* This ensures that `not struct == <value>` is the same as `struct != <value>`.
|
|
25
26
|
*/
|
|
26
27
|
const notEqOps = [['is', 'not'], ['<>'], ['!=']]
|
|
27
28
|
/**
|
|
28
29
|
* not supported in comparison w/ struct because of unclear semantics
|
|
29
30
|
*/
|
|
30
|
-
const notSupportedOps = [['>'], ['<'], ['>='], ['<=']]
|
|
31
|
+
const notSupportedOps = [['>'], ['<'], ['>='], ['<='], ['*'], ['+'], ['-'], ['/']]
|
|
31
32
|
|
|
32
33
|
const allOps = eqOps.concat(eqOps).concat(notEqOps).concat(notSupportedOps)
|
|
33
34
|
|
|
@@ -467,7 +468,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
467
468
|
const refNavigation = col.ref.slice(col.$refLinks[0].definition.kind !== 'element' ? 1 : 0).join('_')
|
|
468
469
|
if (!columnAlias && col.flatName && col.flatName !== refNavigation) columnAlias = refNavigation
|
|
469
470
|
|
|
470
|
-
if (col.$refLinks.some(link => getDefinition(link.definition.target)
|
|
471
|
+
if (col.$refLinks.some(link => hasOwnSkip(getDefinition(link.definition.target)))) return
|
|
471
472
|
|
|
472
473
|
const flatColumns = getFlatColumnsFor(col, { baseName, columnAlias, tableAlias })
|
|
473
474
|
flatColumns.forEach(flatColumn => {
|
|
@@ -966,7 +967,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
966
967
|
} else if (pseudos.elements[col.ref?.[0]]) {
|
|
967
968
|
res.push({ ...col })
|
|
968
969
|
} else if (col.ref) {
|
|
969
|
-
if (col.$refLinks.some(link => getDefinition(link.definition.target)
|
|
970
|
+
if (col.$refLinks.some(link => hasOwnSkip(getDefinition(link.definition.target))))
|
|
970
971
|
continue
|
|
971
972
|
if (col.ref.length > 1 && col.ref[0] === '$self' && !col.$refLinks[0].definition.kind) {
|
|
972
973
|
const dollarSelfReplacement = calculateDollarSelfColumn(col)
|
|
@@ -1189,7 +1190,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1189
1190
|
let { baseName, columnAlias = column.as, tableAlias } = names
|
|
1190
1191
|
const { exclude, replace } = excludeAndReplace || {}
|
|
1191
1192
|
const { $refLinks, flatName, isJoinRelevant } = column
|
|
1192
|
-
let
|
|
1193
|
+
let firstNonJoinRelevantAssoc, stepAfterAssoc
|
|
1193
1194
|
let element = $refLinks ? $refLinks[$refLinks.length - 1].definition : column
|
|
1194
1195
|
if (isWildcard && element.type === 'cds.LargeBinary') return []
|
|
1195
1196
|
if (element.on && !element.keys)
|
|
@@ -1197,14 +1198,23 @@ function cqn4sql(originalQuery, model) {
|
|
|
1197
1198
|
else if (element.virtual === true) return []
|
|
1198
1199
|
else if (!isJoinRelevant && flatName) baseName = flatName
|
|
1199
1200
|
else if (isJoinRelevant) {
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1201
|
+
const leafAssocIndex = column.$refLinks.findIndex(link => link.definition.isAssociation && link.onlyForeignKeyAccess)
|
|
1202
|
+
firstNonJoinRelevantAssoc = column.$refLinks[leafAssocIndex] || [...column.$refLinks].reverse().find(link => link.definition.isAssociation)
|
|
1203
|
+
stepAfterAssoc = column.$refLinks.at(leafAssocIndex + 1) || column.$refLinks.at(-1)
|
|
1204
|
+
let elements = firstNonJoinRelevantAssoc.definition.elements || firstNonJoinRelevantAssoc.definition.foreignKeys
|
|
1205
|
+
if (elements && stepAfterAssoc.definition.name in elements) {
|
|
1206
|
+
element = firstNonJoinRelevantAssoc.definition
|
|
1207
|
+
baseName = getFullName(firstNonJoinRelevantAssoc.definition)
|
|
1206
1208
|
columnAlias = column.as || column.ref.slice(0, -1).map(idOnly).join('_')
|
|
1207
1209
|
} else baseName = getFullName(column.$refLinks[column.$refLinks.length - 1].definition)
|
|
1210
|
+
|
|
1211
|
+
if(column.element && !isAssocOrStruct(column.element)) {
|
|
1212
|
+
columnAlias = column.as || leafAssocIndex === -1 ? columnAlias : column.ref.slice(leafAssocIndex - 1).map(idOnly).join('_')
|
|
1213
|
+
const res = { ref: [tableAlias, calculateElementName(column)], as: columnAlias }
|
|
1214
|
+
setElementOnColumns(res, element)
|
|
1215
|
+
return [res]
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1208
1218
|
} else if (!baseName && structsAreUnfoldedAlready) {
|
|
1209
1219
|
baseName = element.name // name is already fully constructed
|
|
1210
1220
|
} else {
|
|
@@ -1245,11 +1255,11 @@ function cqn4sql(originalQuery, model) {
|
|
|
1245
1255
|
const flattenThisForeignKey =
|
|
1246
1256
|
!$refLinks || // the association is passed as element, not as ref --> flatten full foreign key
|
|
1247
1257
|
element === $refLinks.at(-1).definition || // the association is the leaf of the ref --> flatten full foreign key
|
|
1248
|
-
keyElement ===
|
|
1258
|
+
keyElement === stepAfterAssoc.definition // the foreign key is the leaf of the ref --> only flatten this specific foreign key
|
|
1249
1259
|
if (flattenThisForeignKey) {
|
|
1250
1260
|
const fkElement = getElementForRef(k.ref, getDefinition(element.target))
|
|
1251
1261
|
let fkBaseName
|
|
1252
|
-
if (!
|
|
1262
|
+
if (!firstNonJoinRelevantAssoc || firstNonJoinRelevantAssoc.onlyForeignKeyAccess) fkBaseName = `${baseName}_${k.as || k.ref.at(-1)}`
|
|
1253
1263
|
// e.g. if foreign key is accessed via infix filter - use join alias to access key in target
|
|
1254
1264
|
else fkBaseName = k.ref.at(-1)
|
|
1255
1265
|
const fkPath = [...csnPath, k.ref.at(-1)]
|
|
@@ -1462,6 +1472,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1462
1472
|
flatKeys.push(...getFlatColumnsFor(v, { tableAlias: $baseLink.alias }))
|
|
1463
1473
|
}
|
|
1464
1474
|
}
|
|
1475
|
+
// TODO: improve error message, the current message is generally not true (only for OData shortcut notation)
|
|
1465
1476
|
if (flatKeys.length > 1)
|
|
1466
1477
|
throw new Error('Filters can only be applied to managed associations which result in a single foreign key')
|
|
1467
1478
|
flatKeys.forEach(c => keyValComparisons.push([...[c, '=', token]]))
|
|
@@ -1474,35 +1485,38 @@ function cqn4sql(originalQuery, model) {
|
|
|
1474
1485
|
transformedTokenStream.push({ ...token })
|
|
1475
1486
|
} else {
|
|
1476
1487
|
// expand `struct = null | struct2`
|
|
1477
|
-
const definition = token.$refLinks?.at(-1).definition
|
|
1478
1488
|
const next = tokenStream[i + 1]
|
|
1479
|
-
|
|
1489
|
+
let indexRhs = i + 2
|
|
1490
|
+
let rhs = tokenStream[indexRhs] // either another operator (i.e. `not like` et. al.) or the operand, i.e. the val | null
|
|
1491
|
+
const lhsDef = token.$refLinks?.at(-1).definition
|
|
1492
|
+
let rhsDef = rhs?.$refLinks?.at(-1)?.definition
|
|
1493
|
+
if (
|
|
1494
|
+
allOps.some(([firstOp]) => firstOp === next) &&
|
|
1495
|
+
(lhsDef?.elements || lhsDef?.keys || rhsDef?.elements || rhsDef?.keys)
|
|
1496
|
+
) {
|
|
1480
1497
|
const ops = [next]
|
|
1481
|
-
let indexRhs = i + 2
|
|
1482
|
-
let rhs = tokenStream[i + 2] // either another operator (i.e. `not like` et. al.) or the operand, i.e. the val | null
|
|
1483
1498
|
if (allOps.some(([, secondOp]) => secondOp === rhs)) {
|
|
1484
1499
|
ops.push(rhs)
|
|
1485
1500
|
rhs = tokenStream[i + 3]
|
|
1486
1501
|
indexRhs += 1
|
|
1502
|
+
rhsDef = rhs?.$refLinks?.at(-1)?.definition
|
|
1487
1503
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
)
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
i = indexRhs // jump to next relevant index
|
|
1500
|
-
}
|
|
1504
|
+
|
|
1505
|
+
if (notSupportedOps.some(([firstOp]) => firstOp === next))
|
|
1506
|
+
throw new Error(`The operator "${next}" can only be used with scalar operands`)
|
|
1507
|
+
|
|
1508
|
+
const newTokens = expandComparison(token, ops, rhs, $baseLink)
|
|
1509
|
+
if(newTokens.length === 0)
|
|
1510
|
+
throw new Error(`Can't compare two empty structures`)
|
|
1511
|
+
|
|
1512
|
+
const needXpr = Boolean(tokenStream[i - 1] || tokenStream[indexRhs + 1])
|
|
1513
|
+
transformedTokenStream.push(...(needXpr ? [asXpr(newTokens)] : newTokens))
|
|
1514
|
+
i = indexRhs // jump to next relevant index
|
|
1501
1515
|
} else {
|
|
1502
1516
|
// reject associations in expression, except if we are in an infix filter -> $baseLink is set
|
|
1503
1517
|
assertNoStructInXpr(token, $baseLink)
|
|
1504
1518
|
// reject virtual elements in expressions as they will lead to a sql error down the line
|
|
1505
|
-
if (
|
|
1519
|
+
if (lhsDef?.virtual) throw new Error(`Virtual elements are not allowed in expressions`)
|
|
1506
1520
|
|
|
1507
1521
|
let result = is_regexp(token?.val) ? token : copy(token) // REVISIT: too expensive! //
|
|
1508
1522
|
if (token.ref) {
|
|
@@ -1528,7 +1542,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1528
1542
|
token.isJoinRelevant && [...token.$refLinks].reverse().find(l => l.definition.isAssociation)
|
|
1529
1543
|
const tableAlias = getTableAlias(token, (!lastAssoc?.onlyForeignKeyAccess && lastAssoc) || $baseLink)
|
|
1530
1544
|
if ((!$baseLink || lastAssoc) && token.isJoinRelevant) {
|
|
1531
|
-
let name = calculateElementName(token
|
|
1545
|
+
let name = calculateElementName(token)
|
|
1532
1546
|
result.ref = [tableAlias, name]
|
|
1533
1547
|
} else if (tableAlias) {
|
|
1534
1548
|
result.ref = [tableAlias, token.flatName]
|
|
@@ -1557,9 +1571,9 @@ function cqn4sql(originalQuery, model) {
|
|
|
1557
1571
|
/**
|
|
1558
1572
|
* Expand the given definition and compare all leafs to `val`.
|
|
1559
1573
|
*
|
|
1560
|
-
* @param {object}
|
|
1574
|
+
* @param {object} lhs with $refLinks
|
|
1561
1575
|
* @param {string} operator one of allOps
|
|
1562
|
-
* @param {object}
|
|
1576
|
+
* @param {object} rhs either `null` or a column (with `ref` and `$refLinks`)
|
|
1563
1577
|
* @param {object} $baseLink optional base `$refLink`, e.g. for infix filters of scoped queries.
|
|
1564
1578
|
* In the following example, we must pass `bookshop:Reproduce` as $baseLink for `author`:
|
|
1565
1579
|
*
|
|
@@ -1567,26 +1581,21 @@ function cqn4sql(originalQuery, model) {
|
|
|
1567
1581
|
* ^^^^^^
|
|
1568
1582
|
* @returns {array}
|
|
1569
1583
|
*/
|
|
1570
|
-
function expandComparison(
|
|
1571
|
-
const { definition } =
|
|
1572
|
-
|
|
1584
|
+
function expandComparison(lhs, operator, rhs, $baseLink = null) {
|
|
1585
|
+
const { definition: lhsDef, val: lhsVal } = lhs.val ? lhs : lhs.$refLinks.at(-1)
|
|
1586
|
+
const { definition: rhsDef, val: rhsVal } = rhs.val ? rhs : rhs.$refLinks?.at(-1) || {}
|
|
1573
1587
|
const result = []
|
|
1574
|
-
if (
|
|
1575
|
-
//
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
value.$refLinks[value.$refLinks.length - 1].definition.name
|
|
1586
|
-
}": the operands must have the same structure`,
|
|
1587
|
-
)
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1588
|
+
if (lhsDef && rhsDef) {
|
|
1589
|
+
// both must be structured
|
|
1590
|
+
const lhsIsStructured = isAssocOrStruct(lhsDef)
|
|
1591
|
+
const rhsIsStructured = isAssocOrStruct(rhsDef)
|
|
1592
|
+
if (!lhsIsStructured)
|
|
1593
|
+
throw new Error(`Can't compare structure “${rhs.ref.map(idOnly).join('.')}” with non-structure “${lhs.ref.map(idOnly).join('.')}”`)
|
|
1594
|
+
if (!rhsIsStructured)
|
|
1595
|
+
throw new Error(`Can't compare structure “${lhs.ref.map(idOnly).join('.')}” with non-structure “${rhs.ref.map(idOnly).join('.')}”`)
|
|
1596
|
+
|
|
1597
|
+
const flatLhs = flattenWithBaseName(lhs)
|
|
1598
|
+
const flatRhs = flattenWithBaseName(rhs)
|
|
1590
1599
|
const boolOp = notEqOps.some(([f, s]) => operator[0] === f && operator[1] === s) ? 'or' : 'and'
|
|
1591
1600
|
while (flatLhs.length > 0) {
|
|
1592
1601
|
// retrieve and remove one flat element from LHS and search for it in RHS (remove it there too)
|
|
@@ -1598,27 +1607,47 @@ function cqn4sql(originalQuery, model) {
|
|
|
1598
1607
|
})
|
|
1599
1608
|
// not found in rhs --> exit
|
|
1600
1609
|
if (indexOfElementOnRhs === -1) {
|
|
1601
|
-
const lhsPath =
|
|
1602
|
-
const rhsPath =
|
|
1610
|
+
const lhsPath = lhs.ref.map(idOnly).join('.')
|
|
1611
|
+
const rhsPath = rhs.ref.map(idOnly).join('.')
|
|
1603
1612
|
throw new Error(`Can't compare "${lhsPath}" with "${rhsPath}": the operands must have the same structure`)
|
|
1604
1613
|
}
|
|
1605
|
-
const
|
|
1606
|
-
result.push({ ref }, ...operator,
|
|
1614
|
+
const cleansedRhs = flatRhs.splice(indexOfElementOnRhs, 1)[0] // remove the element also from RHS
|
|
1615
|
+
result.push({ ref }, ...operator, cleansedRhs)
|
|
1607
1616
|
if (flatLhs.length > 0) result.push(boolOp)
|
|
1608
1617
|
}
|
|
1609
|
-
} else {
|
|
1618
|
+
} else if (lhsDef && (rhsVal || rhs === 'null' || rhs.val === null)) {
|
|
1610
1619
|
// compare with value
|
|
1611
|
-
const flatLhs = flattenWithBaseName(
|
|
1612
|
-
if (flatLhs.length
|
|
1613
|
-
|
|
1620
|
+
const flatLhs = flattenWithBaseName(lhs)
|
|
1621
|
+
if (flatLhs.length !== 1 && rhsVal && rhs !== 'null')
|
|
1622
|
+
canOnlyCompareToExactlyOneLeaf(lhsDef, lhs.ref, rhsVal)
|
|
1623
|
+
|
|
1614
1624
|
const boolOp = notEqOps.some(([f, s]) => operator[0] === f && operator[1] === s) ? 'or' : 'and'
|
|
1615
1625
|
flatLhs.forEach((column, i) => {
|
|
1616
|
-
result.push(column, ...operator,
|
|
1626
|
+
result.push(column, ...operator, rhs)
|
|
1617
1627
|
if (flatLhs[i + 1]) result.push(boolOp)
|
|
1618
1628
|
})
|
|
1629
|
+
} else if (lhsVal && rhsDef) {
|
|
1630
|
+
const flatRhs = flattenWithBaseName(rhs)
|
|
1631
|
+
// comparing a struct to a value is ok if structure has exactly one leaf
|
|
1632
|
+
if (flatRhs.length !== 1 && lhsVal)
|
|
1633
|
+
canOnlyCompareToExactlyOneLeaf(rhsDef, rhs.ref, lhsVal)
|
|
1634
|
+
|
|
1635
|
+
const boolOp = notEqOps.some(([f, s]) => operator[0] === f && operator[1] === s) ? 'or' : 'and'
|
|
1636
|
+
flatRhs.forEach((column, i) => {
|
|
1637
|
+
result.push(lhs, ...operator, column)
|
|
1638
|
+
if (flatRhs[i + 1]) result.push(boolOp)
|
|
1639
|
+
})
|
|
1619
1640
|
}
|
|
1620
1641
|
return result
|
|
1621
1642
|
|
|
1643
|
+
function canOnlyCompareToExactlyOneLeaf(struct, structRef, val) {
|
|
1644
|
+
const what = struct.isAssociation ? 'association' : 'structure'
|
|
1645
|
+
const postfix = struct.isAssociation ? 'associations with one foreign key' : 'structures with one sub-element'
|
|
1646
|
+
throw new Error(
|
|
1647
|
+
`Can't compare ${what} "${structRef.map(idOnly).join('.')}" to value "${val}"; only possible for ${postfix}`
|
|
1648
|
+
)
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1622
1651
|
function flattenWithBaseName(def) {
|
|
1623
1652
|
if (!def.$refLinks) return def
|
|
1624
1653
|
const leaf = def.$refLinks[def.$refLinks.length - 1]
|
|
@@ -2127,7 +2156,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2127
2156
|
* @param {boolean} inWhere whether or not the path is part of the queries where clause
|
|
2128
2157
|
* -> if it is, target and source side are flipped in the where exists subquery
|
|
2129
2158
|
* @param {object} queryModifier optional query modifiers: group by, order by, limit, offset
|
|
2130
|
-
*
|
|
2159
|
+
*
|
|
2131
2160
|
* @returns {CQN.SELECT}
|
|
2132
2161
|
*/
|
|
2133
2162
|
function getWhereExistsSubquery(current, next, customWhere = null, inWhere = false, queryModifier = null) {
|
|
@@ -2165,26 +2194,29 @@ function cqn4sql(originalQuery, model) {
|
|
|
2165
2194
|
where: on,
|
|
2166
2195
|
}
|
|
2167
2196
|
// this requires sub-sequent transformation of the subquery
|
|
2168
|
-
if (
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2197
|
+
if (
|
|
2198
|
+
next.pathExpressionInsideFilter ||
|
|
2199
|
+
(queryModifier && ['orderBy', 'groupBy', 'having', 'limit', 'offset'].some(key => key in queryModifier))
|
|
2200
|
+
) {
|
|
2201
|
+
SELECT.where = customWhere || []
|
|
2202
|
+
if (queryModifier) assignQueryModifiers(SELECT, queryModifier)
|
|
2203
|
+
|
|
2204
|
+
const transformedExists = transformSubquery({ SELECT })
|
|
2173
2205
|
if (transformedExists.SELECT.where?.length) {
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2206
|
+
const wrappedWhere = hasLogicalOr(transformedExists.SELECT.where)
|
|
2207
|
+
? [asXpr(transformedExists.SELECT.where)]
|
|
2208
|
+
: transformedExists.SELECT.where
|
|
2177
2209
|
|
|
2178
|
-
|
|
2210
|
+
on.push('and', ...wrappedWhere)
|
|
2179
2211
|
}
|
|
2180
|
-
transformedExists.SELECT.where = on
|
|
2181
|
-
return transformedExists.SELECT
|
|
2212
|
+
transformedExists.SELECT.where = on
|
|
2213
|
+
return transformedExists.SELECT
|
|
2182
2214
|
}
|
|
2183
2215
|
|
|
2184
2216
|
if (customWhere) {
|
|
2185
|
-
const filter = getTransformedTokenStream(customWhere, next)
|
|
2186
|
-
const wrappedFilter = hasLogicalOr(filter) ? [asXpr(filter)] : filter
|
|
2187
|
-
on.push('and', ...wrappedFilter)
|
|
2217
|
+
const filter = getTransformedTokenStream(customWhere, next)
|
|
2218
|
+
const wrappedFilter = hasLogicalOr(filter) ? [asXpr(filter)] : filter
|
|
2219
|
+
on.push('and', ...wrappedFilter)
|
|
2188
2220
|
}
|
|
2189
2221
|
return SELECT
|
|
2190
2222
|
}
|
|
@@ -2349,7 +2381,6 @@ function getParentEntity(element) {
|
|
|
2349
2381
|
else return getParentEntity(element.parent)
|
|
2350
2382
|
}
|
|
2351
2383
|
|
|
2352
|
-
|
|
2353
2384
|
function asXpr(thing) {
|
|
2354
2385
|
return { xpr: thing }
|
|
2355
2386
|
}
|
package/lib/infer/index.js
CHANGED
|
@@ -4,7 +4,7 @@ const cds = require('@sap/cds')
|
|
|
4
4
|
|
|
5
5
|
const JoinTree = require('./join-tree')
|
|
6
6
|
const { pseudos } = require('./pseudos')
|
|
7
|
-
const { isCalculatedOnRead, getImplicitAlias, getModelUtils, defineProperty } = require('../utils')
|
|
7
|
+
const { isCalculatedOnRead, getImplicitAlias, getModelUtils, defineProperty, hasOwnSkip } = require('../utils')
|
|
8
8
|
const cdsTypes = cds.linked({
|
|
9
9
|
definitions: {
|
|
10
10
|
Timestamp: { type: 'cds.Timestamp' },
|
|
@@ -404,7 +404,7 @@ function infer(originalQuery, model) {
|
|
|
404
404
|
if (arg.list) arg.list.forEach(arg => inferArg(arg, null, $baseLink, context))
|
|
405
405
|
if (arg.xpr)
|
|
406
406
|
arg.xpr.forEach((token, i) =>
|
|
407
|
-
inferArg(token, queryElements, $baseLink, { ...context, inXpr: true, inExists: arg.xpr[i - 1] === 'exists' }),
|
|
407
|
+
inferArg(token, queryElements, $baseLink, { ...context, inXpr: true, inExists: inExists || arg.xpr[i - 1] === 'exists' }),
|
|
408
408
|
) // e.g. function in expression
|
|
409
409
|
|
|
410
410
|
if (!arg.ref) {
|
|
@@ -587,7 +587,7 @@ function infer(originalQuery, model) {
|
|
|
587
587
|
}
|
|
588
588
|
|
|
589
589
|
arg.$refLinks[i].alias = !arg.ref[i + 1] && arg.as ? arg.as : id.split('.').pop()
|
|
590
|
-
if (getDefinition(arg.$refLinks[i].definition.target)
|
|
590
|
+
if (hasOwnSkip(getDefinition(arg.$refLinks[i].definition.target))) isPersisted = false
|
|
591
591
|
if (!arg.ref[i + 1]) {
|
|
592
592
|
const flatName = nameSegments.join('_')
|
|
593
593
|
defineProperty(arg, 'flatName', flatName)
|
|
@@ -640,7 +640,7 @@ function infer(originalQuery, model) {
|
|
|
640
640
|
// ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
|
|
641
641
|
if (arg.expand) {
|
|
642
642
|
const { $refLinks } = arg
|
|
643
|
-
const skip = $refLinks.some(link => getDefinition(link.definition.target)
|
|
643
|
+
const skip = $refLinks.some(link => hasOwnSkip(getDefinition(link.definition.target)))
|
|
644
644
|
if (skip) {
|
|
645
645
|
$refLinks[$refLinks.length - 1].skipExpand = true
|
|
646
646
|
return
|
|
@@ -937,8 +937,15 @@ function infer(originalQuery, model) {
|
|
|
937
937
|
return true
|
|
938
938
|
}
|
|
939
939
|
if (assoc) {
|
|
940
|
+
// if(!link.definition.isAssociation) continue
|
|
941
|
+
let fkIndex = assoc.keys?.findIndex(key => key.ref.every((step, j) => column.ref[i + j] === step))
|
|
940
942
|
// foreign key access without filters never join relevant
|
|
941
|
-
if (
|
|
943
|
+
if (fkIndex !== -1) {
|
|
944
|
+
if(column.ref.slice(i).some(s => s.where)) continue // probably join relevant later on
|
|
945
|
+
fkAccess = true
|
|
946
|
+
assoc = null
|
|
947
|
+
continue
|
|
948
|
+
}
|
|
942
949
|
// <assoc>.<anotherAssoc>.<…> is join relevant as <anotherAssoc> is not fk of <assoc>
|
|
943
950
|
return true
|
|
944
951
|
}
|
package/lib/infer/join-tree.js
CHANGED
|
@@ -212,6 +212,8 @@ class JoinTree {
|
|
|
212
212
|
// filter is always join relevant
|
|
213
213
|
// if the column ends up in an `inline` -> each assoc step is join relevant
|
|
214
214
|
child.$refLink.onlyForeignKeyAccess = false
|
|
215
|
+
// all parents are now also join relevant
|
|
216
|
+
markParentAsJoinRelevant(child.parent)
|
|
215
217
|
} else {
|
|
216
218
|
child.$refLink.onlyForeignKeyAccess = true
|
|
217
219
|
}
|
|
@@ -223,6 +225,8 @@ class JoinTree {
|
|
|
223
225
|
if (node.$refLink && (!elements || !(child.$refLink.definition.name in elements))) {
|
|
224
226
|
// no foreign key access
|
|
225
227
|
node.$refLink.onlyForeignKeyAccess = false
|
|
228
|
+
markParentAsJoinRelevant(node.parent)
|
|
229
|
+
|
|
226
230
|
col.$refLinks[i - 1] = node.$refLink
|
|
227
231
|
}
|
|
228
232
|
|
|
@@ -233,6 +237,15 @@ class JoinTree {
|
|
|
233
237
|
}
|
|
234
238
|
return true
|
|
235
239
|
|
|
240
|
+
function markParentAsJoinRelevant(parent) {
|
|
241
|
+
while (parent) {
|
|
242
|
+
if (parent.$refLink?.definition.isAssociation) {
|
|
243
|
+
parent.$refLink.onlyForeignKeyAccess = false
|
|
244
|
+
}
|
|
245
|
+
parent = parent.parent
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
236
249
|
function joinId(step, args, where) {
|
|
237
250
|
let appendix
|
|
238
251
|
if (where && args) appendix = JSON.stringify(where) + JSON.stringify(args)
|
package/lib/utils.js
CHANGED
|
@@ -21,6 +21,12 @@ function prettyPrintRef(ref, model = null) {
|
|
|
21
21
|
}, '')
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function hasOwnSkip(definition) {
|
|
25
|
+
return (
|
|
26
|
+
definition && Object.hasOwn(definition, '@cds.persistence.skip') && definition['@cds.persistence.skip'] === true
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
/**
|
|
25
31
|
* Determines if a definition is calculated on read.
|
|
26
32
|
* - Stored calculated elements are not unfolded
|
|
@@ -123,7 +129,6 @@ function getModelUtils(model, query) {
|
|
|
123
129
|
if (!def || !isLocalized(def)) return def
|
|
124
130
|
return model.definitions[`localized.${def.name}`] || def
|
|
125
131
|
}
|
|
126
|
-
|
|
127
132
|
return {
|
|
128
133
|
getLocalizedName,
|
|
129
134
|
isLocalized,
|
|
@@ -139,4 +144,5 @@ module.exports = {
|
|
|
139
144
|
getImplicitAlias,
|
|
140
145
|
defineProperty,
|
|
141
146
|
getModelUtils,
|
|
147
|
+
hasOwnSkip,
|
|
142
148
|
}
|
package/package.json
CHANGED