@cap-js/db-service 1.16.2 → 1.17.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/cql-functions.js +3 -3
- package/lib/cqn2sql.js +5 -1
- package/lib/cqn4sql.js +8 -5
- package/lib/infer/index.js +7 -2
- 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
|
+
## [1.17.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.16.2...db-service-v1.17.0) (2025-01-28)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* support for cds.Map ([#889](https://github.com/cap-js/cds-dbs/issues/889)) ([cde7514](https://github.com/cap-js/cds-dbs/commit/cde7514df20396383e0179ffce838596e3706bb2))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
* **`UPDATE`:** no assocs in list which matches subquery results ([4bcb88a](https://github.com/cap-js/cds-dbs/commit/4bcb88a1f40540e26cebd4907bdd33e90d08bb9d))
|
|
18
|
+
* **`UPDATE`:** only perform subselect matching if necessary ([#989](https://github.com/cap-js/cds-dbs/issues/989)) ([4bcb88a](https://github.com/cap-js/cds-dbs/commit/4bcb88a1f40540e26cebd4907bdd33e90d08bb9d))
|
|
19
|
+
* contains not evaluting to bool ([#980](https://github.com/cap-js/cds-dbs/issues/980)) ([760484b](https://github.com/cap-js/cds-dbs/commit/760484be4cf3d0c755254e90f7740ba0b34b9249))
|
|
20
|
+
* nested ternary in calculated element ([#981](https://github.com/cap-js/cds-dbs/issues/981)) ([5f4a1fe](https://github.com/cap-js/cds-dbs/commit/5f4a1feed7b74bb1736f6140527e70b1e261f368))
|
|
21
|
+
* reject virtual elements in all expressions ([#972](https://github.com/cap-js/cds-dbs/issues/972)) ([d0c949d](https://github.com/cap-js/cds-dbs/commit/d0c949d8a3a9851ccd70b3f998caec0b5f01ce0e))
|
|
22
|
+
* starts endswith for null values ([#975](https://github.com/cap-js/cds-dbs/issues/975)) ([f0330bc](https://github.com/cap-js/cds-dbs/commit/f0330bc334fd3a8ed5377afcdd04b731baa8c753))
|
|
23
|
+
* use "$$value$$" as internal column name for UPSERT ([#976](https://github.com/cap-js/cds-dbs/issues/976)) ([8c86b86](https://github.com/cap-js/cds-dbs/commit/8c86b863a69833d50cff91483150bf0314bb7258))
|
|
24
|
+
|
|
7
25
|
## [1.16.2](https://github.com/cap-js/cds-dbs/compare/db-service-v1.16.1...db-service-v1.16.2) (2024-12-18)
|
|
8
26
|
|
|
9
27
|
|
package/lib/cql-functions.js
CHANGED
|
@@ -49,7 +49,7 @@ const StandardFunctions = {
|
|
|
49
49
|
* @param {...string} args
|
|
50
50
|
* @returns {string}
|
|
51
51
|
*/
|
|
52
|
-
contains: (...args) => `ifnull(instr(${args}),0)`,
|
|
52
|
+
contains: (...args) => `(ifnull(instr(${args}),0) > 0)`,
|
|
53
53
|
/**
|
|
54
54
|
* Generates SQL statement that produces the number of elements in a given collection
|
|
55
55
|
* @param {string} x
|
|
@@ -75,7 +75,7 @@ const StandardFunctions = {
|
|
|
75
75
|
* @param {string} y
|
|
76
76
|
* @returns {string}
|
|
77
77
|
*/
|
|
78
|
-
startswith: (x, y) => `instr(${x},${y}) = 1`, // sqlite instr is 1 indexed
|
|
78
|
+
startswith: (x, y) => `coalesce(instr(${x},${y}) = 1,false)`, // sqlite instr is 1 indexed
|
|
79
79
|
// takes the end of the string of the size of the target and compares it with the target
|
|
80
80
|
/**
|
|
81
81
|
* Generates SQL statement that produces a boolean value indicating whether the first string ends with the second string
|
|
@@ -83,7 +83,7 @@ const StandardFunctions = {
|
|
|
83
83
|
* @param {string} y
|
|
84
84
|
* @returns {string}
|
|
85
85
|
*/
|
|
86
|
-
endswith: (x, y) => `substr(${x}, length(${x}) + 1 - length(${y})) = ${y}`,
|
|
86
|
+
endswith: (x, y) => `coalesce(substr(${x}, length(${x}) + 1 - length(${y})) = ${y},false)`,
|
|
87
87
|
/**
|
|
88
88
|
* Generates SQL statement that produces the substring of a given string
|
|
89
89
|
* @example
|
package/lib/cqn2sql.js
CHANGED
|
@@ -197,6 +197,7 @@ class CQN2SQLRenderer {
|
|
|
197
197
|
Association: () => false,
|
|
198
198
|
Composition: () => false,
|
|
199
199
|
array: () => 'NCLOB',
|
|
200
|
+
Map: () => 'NCLOB',
|
|
200
201
|
// HANA types
|
|
201
202
|
'cds.hana.TINYINT': () => 'TINYINT',
|
|
202
203
|
'cds.hana.REAL': () => 'REAL',
|
|
@@ -744,7 +745,10 @@ class CQN2SQLRenderer {
|
|
|
744
745
|
.map(c => `${c.onInsert || c.sql} as ${this.quote(c.name)}`)
|
|
745
746
|
|
|
746
747
|
const entity = this.name(q.target?.name || UPSERT.into.ref[0], q)
|
|
747
|
-
sql = `SELECT ${managed.map(c => c.upsert
|
|
748
|
+
sql = `SELECT ${managed.map(c => c.upsert
|
|
749
|
+
.replace(/value->/g, '"$$$$value$$$$"->')
|
|
750
|
+
.replace(/json_type\(value,/g, 'json_type("$$$$value$$$$",'))
|
|
751
|
+
} FROM (SELECT value as "$$value$$", ${extractkeys} from json_each(?)) as NEW LEFT JOIN ${this.quote(entity)} AS OLD ON ${keyCompare}`
|
|
748
752
|
|
|
749
753
|
const updateColumns = columns.filter(c => {
|
|
750
754
|
if (keys.includes(c)) return false //> keys go into ON CONFLICT clause
|
package/lib/cqn4sql.js
CHANGED
|
@@ -134,7 +134,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
134
134
|
const primaryKey = { list: [] }
|
|
135
135
|
for (const k of Object.keys(queryTarget.elements)) {
|
|
136
136
|
const e = queryTarget.elements[k]
|
|
137
|
-
if (e.key === true && !e.virtual) {
|
|
137
|
+
if (e.key === true && !e.virtual && e.isAssociation !== true) {
|
|
138
138
|
subquery.SELECT.columns.push({ ref: [e.name] })
|
|
139
139
|
primaryKey.list.push({ ref: [transformedFrom.as, e.name] })
|
|
140
140
|
}
|
|
@@ -1286,7 +1286,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1286
1286
|
}
|
|
1287
1287
|
}
|
|
1288
1288
|
return flatColumns
|
|
1289
|
-
} else if (element.elements) {
|
|
1289
|
+
} else if (element.elements && element.type !== 'cds.Map') {
|
|
1290
1290
|
const flatRefs = []
|
|
1291
1291
|
Object.values(element.elements).forEach(e => {
|
|
1292
1292
|
const alias = columnAlias ? `${columnAlias}_${e.name}` : null
|
|
@@ -1450,7 +1450,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1450
1450
|
transformedTokenStream.push({ ...token })
|
|
1451
1451
|
} else {
|
|
1452
1452
|
// expand `struct = null | struct2`
|
|
1453
|
-
const
|
|
1453
|
+
const definition = token.$refLinks?.at(-1).definition
|
|
1454
1454
|
const next = tokenStream[i + 1]
|
|
1455
1455
|
if (allOps.some(([firstOp]) => firstOp === next) && (definition?.elements || definition?.keys)) {
|
|
1456
1456
|
const ops = [next]
|
|
@@ -1477,6 +1477,9 @@ function cqn4sql(originalQuery, model) {
|
|
|
1477
1477
|
} else {
|
|
1478
1478
|
// reject associations in expression, except if we are in an infix filter -> $baseLink is set
|
|
1479
1479
|
assertNoStructInXpr(token, $baseLink)
|
|
1480
|
+
// reject virtual elements in expressions as they will lead to a sql error down the line
|
|
1481
|
+
if(definition?.virtual)
|
|
1482
|
+
throw new Error(`Virtual elements are not allowed in expressions`)
|
|
1480
1483
|
|
|
1481
1484
|
let result = is_regexp(token?.val) ? token : copy(token) // REVISIT: too expensive! //
|
|
1482
1485
|
if (token.ref) {
|
|
@@ -1620,10 +1623,10 @@ function cqn4sql(originalQuery, model) {
|
|
|
1620
1623
|
rejectStructInExpression()
|
|
1621
1624
|
|
|
1622
1625
|
function rejectAssocInExpression() {
|
|
1623
|
-
throw new Error(
|
|
1626
|
+
throw new Error(`An association can't be used as a value in an expression`)
|
|
1624
1627
|
}
|
|
1625
1628
|
function rejectStructInExpression() {
|
|
1626
|
-
throw new Error(
|
|
1629
|
+
throw new Error(`A structured element can't be used as a value in an expression`)
|
|
1627
1630
|
}
|
|
1628
1631
|
}
|
|
1629
1632
|
|
package/lib/infer/index.js
CHANGED
|
@@ -859,7 +859,7 @@ function infer(originalQuery, model) {
|
|
|
859
859
|
} else if (arg.xpr || arg.args) {
|
|
860
860
|
const prop = arg.xpr ? 'xpr' : 'args'
|
|
861
861
|
arg[prop].forEach(step => {
|
|
862
|
-
|
|
862
|
+
let subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
|
|
863
863
|
if (step.ref) {
|
|
864
864
|
step.$refLinks.forEach((link, i) => {
|
|
865
865
|
const { definition } = link
|
|
@@ -874,6 +874,10 @@ function infer(originalQuery, model) {
|
|
|
874
874
|
} else if (step.args || step.xpr) {
|
|
875
875
|
const nestedProp = step.xpr ? 'xpr' : 'args'
|
|
876
876
|
step[nestedProp].forEach(a => {
|
|
877
|
+
// reset sub path for each nested argument
|
|
878
|
+
// e.g. case when <path> then <otherPath> else <anotherPath> end
|
|
879
|
+
if(!a.ref)
|
|
880
|
+
subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
|
|
877
881
|
mergePathsIntoJoinTree(a, subPath)
|
|
878
882
|
})
|
|
879
883
|
}
|
|
@@ -959,7 +963,8 @@ function infer(originalQuery, model) {
|
|
|
959
963
|
if (element.type !== 'cds.LargeBinary') {
|
|
960
964
|
queryElements[k] = element
|
|
961
965
|
}
|
|
962
|
-
if
|
|
966
|
+
// only relevant if we actually select the calculated element
|
|
967
|
+
if (originalQuery.SELECT && isCalculatedOnRead(element)) {
|
|
963
968
|
linkCalculatedElement(element)
|
|
964
969
|
}
|
|
965
970
|
}
|
package/package.json
CHANGED