@cap-js/db-service 1.16.1 → 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 +25 -0
- package/lib/cql-functions.js +3 -3
- package/lib/cqn2sql.js +13 -20
- 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,31 @@
|
|
|
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
|
+
|
|
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)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
* do not override .toJSON of buffers ([#949](https://github.com/cap-js/cds-dbs/issues/949)) ([ed52f72](https://github.com/cap-js/cds-dbs/commit/ed52f72206df6e683106ab0bbbecf4b778cf36b5))
|
|
31
|
+
|
|
7
32
|
## [1.16.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.16.0...db-service-v1.16.1) (2024-12-16)
|
|
8
33
|
|
|
9
34
|
|
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
|
@@ -8,7 +8,7 @@ const { Readable } = require('stream')
|
|
|
8
8
|
|
|
9
9
|
const DEBUG = cds.debug('sql|sqlite')
|
|
10
10
|
const LOG_SQL = cds.log('sql')
|
|
11
|
-
const LOG_SQLITE =
|
|
11
|
+
const LOG_SQLITE = cds.log('sqlite')
|
|
12
12
|
|
|
13
13
|
class CQN2SQLRenderer {
|
|
14
14
|
/**
|
|
@@ -25,7 +25,7 @@ class CQN2SQLRenderer {
|
|
|
25
25
|
if (cds.env.sql.names === 'quoted') {
|
|
26
26
|
this.class.prototype.name = (name, query) => {
|
|
27
27
|
const e = name.id || name
|
|
28
|
-
return (query?.target || this.model?.definitions[e])?.['@cds.persistence.name'] || e
|
|
28
|
+
return (query?.target || this.model?.definitions[e])?.['@cds.persistence.name'] || e
|
|
29
29
|
}
|
|
30
30
|
this.class.prototype.quote = (s) => `"${String(s).replace(/"/g, '""')}"`
|
|
31
31
|
}
|
|
@@ -86,7 +86,7 @@ class CQN2SQLRenderer {
|
|
|
86
86
|
if (vars && Object.keys(vars).length && !this.values?.length) this.values = vars
|
|
87
87
|
const sanitize_values = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
|
|
90
90
|
if (DEBUG && (LOG_SQL._debug || LOG_SQLITE._debug)) {
|
|
91
91
|
let values = sanitize_values && (this.entries || this.values?.length > 0) ? ['***'] : this.entries || this.values || []
|
|
92
92
|
if (values && !Array.isArray(values)) {
|
|
@@ -95,7 +95,7 @@ class CQN2SQLRenderer {
|
|
|
95
95
|
DEBUG(this.sql, ...values)
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
|
|
99
99
|
return this
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -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',
|
|
@@ -528,9 +529,6 @@ class CQN2SQLRenderer {
|
|
|
528
529
|
|
|
529
530
|
async *INSERT_entries_stream(entries, binaryEncoding = 'base64') {
|
|
530
531
|
const elements = this.cqn.target?.elements || {}
|
|
531
|
-
const transformBase64 = binaryEncoding === 'base64'
|
|
532
|
-
? a => a
|
|
533
|
-
: a => a != null ? Buffer.from(a, 'base64').toString(binaryEncoding) : a
|
|
534
532
|
const bufferLimit = 65536 // 1 << 16
|
|
535
533
|
let buffer = '['
|
|
536
534
|
|
|
@@ -561,8 +559,8 @@ class CQN2SQLRenderer {
|
|
|
561
559
|
|
|
562
560
|
buffer += '"'
|
|
563
561
|
} else {
|
|
564
|
-
if (elements[key]?.type in this.BINARY_TYPES) {
|
|
565
|
-
val =
|
|
562
|
+
if (val != null && elements[key]?.type in this.BINARY_TYPES) {
|
|
563
|
+
val = Buffer.from(val, 'base64').toString(binaryEncoding)
|
|
566
564
|
}
|
|
567
565
|
buffer += `${keyJSON}${JSON.stringify(val)}`
|
|
568
566
|
}
|
|
@@ -580,9 +578,6 @@ class CQN2SQLRenderer {
|
|
|
580
578
|
|
|
581
579
|
async *INSERT_rows_stream(entries, binaryEncoding = 'base64') {
|
|
582
580
|
const elements = this.cqn.target?.elements || {}
|
|
583
|
-
const transformBase64 = binaryEncoding === 'base64'
|
|
584
|
-
? a => a
|
|
585
|
-
: a => a != null ? Buffer.from(a, 'base64').toString(binaryEncoding) : a
|
|
586
581
|
const bufferLimit = 65536 // 1 << 16
|
|
587
582
|
let buffer = '['
|
|
588
583
|
|
|
@@ -609,8 +604,8 @@ class CQN2SQLRenderer {
|
|
|
609
604
|
|
|
610
605
|
buffer += '"'
|
|
611
606
|
} else {
|
|
612
|
-
if (elements[this.columns[key]]?.type in this.BINARY_TYPES) {
|
|
613
|
-
val =
|
|
607
|
+
if (val != null && elements[this.columns[key]]?.type in this.BINARY_TYPES) {
|
|
608
|
+
val = Buffer.from(val, 'base64').toString(binaryEncoding)
|
|
614
609
|
}
|
|
615
610
|
buffer += `${sepsub}${val === undefined ? 'null' : JSON.stringify(val)}`
|
|
616
611
|
}
|
|
@@ -750,7 +745,10 @@ class CQN2SQLRenderer {
|
|
|
750
745
|
.map(c => `${c.onInsert || c.sql} as ${this.quote(c.name)}`)
|
|
751
746
|
|
|
752
747
|
const entity = this.name(q.target?.name || UPSERT.into.ref[0], q)
|
|
753
|
-
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}`
|
|
754
752
|
|
|
755
753
|
const updateColumns = columns.filter(c => {
|
|
756
754
|
if (keys.includes(c)) return false //> keys go into ON CONFLICT clause
|
|
@@ -1149,11 +1147,6 @@ class CQN2SQLRenderer {
|
|
|
1149
1147
|
}
|
|
1150
1148
|
}
|
|
1151
1149
|
|
|
1152
|
-
// REVISIT: Workaround for JSON.stringify to work with buffers
|
|
1153
|
-
Buffer.prototype.toJSON = function () {
|
|
1154
|
-
return this.toString('base64')
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
1150
|
Readable.prototype[require('node:util').inspect.custom] = Readable.prototype.toJSON = function () { return this._raw || `[object ${this.constructor.name}]` }
|
|
1158
1151
|
|
|
1159
1152
|
const ObjectKeys = o => (o && [...ObjectKeys(o.__proto__), ...Object.keys(o)]) || []
|
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