@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 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
 
@@ -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 = cds.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 = transformBase64(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 = transformBase64(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)} FROM (SELECT value, ${extractkeys} from json_each(?)) as NEW LEFT JOIN ${this.quote(entity)} AS OLD ON ${keyCompare}`
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 { definition } = token.$refLinks?.[token.$refLinks.length - 1] || {}
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("An association can't be used as a value in an expression")
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("A structured element can't be used as a value in an expression")
1629
+ throw new Error(`A structured element can't be used as a value in an expression`)
1627
1630
  }
1628
1631
  }
1629
1632
 
@@ -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
- const subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
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 (isCalculatedOnRead(element)) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "1.16.1",
3
+ "version": "1.17.0",
4
4
  "description": "CDS base database service",
5
5
  "homepage": "https://github.com/cap-js/cds-dbs/tree/main/db-service#cds-base-database-service",
6
6
  "repository": {