@cap-js/db-service 1.12.0 → 1.12.1

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,19 +4,30 @@
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.12.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.12.0...db-service-v1.12.1) (2024-09-03)
8
+
9
+
10
+ ### Fixed
11
+
12
+ * deep `groupby` expand queries ([#768](https://github.com/cap-js/cds-dbs/issues/768)) ([5423cf3](https://github.com/cap-js/cds-dbs/commit/5423cf38574962c09b94febab95f2e3dc118d2c9))
13
+ * **deep:** prevent false unique constraint errors and combine delete queries ([#781](https://github.com/cap-js/cds-dbs/issues/781)) ([01de95f](https://github.com/cap-js/cds-dbs/commit/01de95f5050a1d3325459ccb78a4e9a1e0dbcfde))
14
+ * **logging:** from changes in @sap/cds ([#791](https://github.com/cap-js/cds-dbs/issues/791)) ([1e8bf06](https://github.com/cap-js/cds-dbs/commit/1e8bf06c9ae92ba55d13fe9e3297d6a54c4fc8fe))
15
+ * prepend aliases to refs within function args in on conditions ([#795](https://github.com/cap-js/cds-dbs/issues/795)) ([9b34314](https://github.com/cap-js/cds-dbs/commit/9b34314d1ef8c6fd7e77451fe9bf0abdc12c27ea)), closes [#779](https://github.com/cap-js/cds-dbs/issues/779)
16
+ * prevent $search queries from throwing ([#772](https://github.com/cap-js/cds-dbs/issues/772)) ([cdf4d37](https://github.com/cap-js/cds-dbs/commit/cdf4d37590c2949cdfd6c6533370bc96cd8fd0fc))
17
+
7
18
  ## [1.12.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.11.0...db-service-v1.12.0) (2024-07-25)
8
19
 
9
20
 
10
21
  ### Fixed
11
22
 
12
- *** add placeholder for string values ([#733](https://github.com/cap-js/cds-dbs/issues/733)) ([8136a45](https://github.com/cap-js/cds-dbs/commit/8136a4526f596b67932908b8ab1336cb052100f3))
13
- *** for aggregated `expand` always set explicit alias ([#739](https://github.com/cap-js/cds-dbs/issues/739)) ([53a8075](https://github.com/cap-js/cds-dbs/commit/53a8075a609666a896296401a28b6183ff5aa487)), closes [#708](https://github.com/cap-js/cds-dbs/issues/708)
14
- *** quotations in vals ([#754](https://github.com/cap-js/cds-dbs/issues/754)) ([94d8e97](https://github.com/cap-js/cds-dbs/commit/94d8e977ed00776ff494287ce505d6b7e8017d2e))
23
+ * add placeholder for string values ([#733](https://github.com/cap-js/cds-dbs/issues/733)) ([8136a45](https://github.com/cap-js/cds-dbs/commit/8136a4526f596b67932908b8ab1336cb052100f3))
24
+ * for aggregated `expand` always set explicit alias ([#739](https://github.com/cap-js/cds-dbs/issues/739)) ([53a8075](https://github.com/cap-js/cds-dbs/commit/53a8075a609666a896296401a28b6183ff5aa487)), closes [#708](https://github.com/cap-js/cds-dbs/issues/708)
25
+ * quotations in vals ([#754](https://github.com/cap-js/cds-dbs/issues/754)) ([94d8e97](https://github.com/cap-js/cds-dbs/commit/94d8e977ed00776ff494287ce505d6b7e8017d2e))
15
26
 
16
27
 
17
28
  ### Changed
18
29
 
19
- *** generic-pool as real dep ([#750](https://github.com/cap-js/cds-dbs/issues/750)) ([b50c907](https://github.com/cap-js/cds-dbs/commit/b50c907880455a41a73826a736bc17ca17e5b9ae))
30
+ * generic-pool as real dep ([#750](https://github.com/cap-js/cds-dbs/issues/750)) ([b50c907](https://github.com/cap-js/cds-dbs/commit/b50c907880455a41a73826a736bc17ca17e5b9ae))
20
31
 
21
32
 
22
33
  ## [1.11.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.10.3...db-service-v1.11.0) (2024-07-08)
@@ -23,10 +23,16 @@ const StandardFunctions = {
23
23
  search: function (ref, arg) {
24
24
  if (!('val' in arg)) throw new Error(`Only single value arguments are allowed for $search`)
25
25
  // only apply first search term, rest is ignored
26
- const sub= /("")|("(?:[^"]|\\")*(?:[^\\]|\\\\)")|(\S*)/.exec(arg.val)
27
- arg.val = arg.__proto__.val = (sub[2] ? JSON.parse(sub[2]) : sub[3]) || ''
28
- const refs = ref.list || [ref],
29
- { toString } = ref
26
+ const sub = /("")|("(?:[^"]|\\")*(?:[^\\]|\\\\)")|(\S*)/.exec(arg.val)
27
+ let val
28
+ try {
29
+ val = (sub[2] ? JSON.parse(sub[2]) : sub[3]) || ''
30
+ } catch {
31
+ val = sub[2] || sub[3] || ''
32
+ }
33
+ arg.val = arg.__proto__.val = val
34
+ const refs = ref.list || [ref]
35
+ const { toString } = ref
30
36
  return '(' + refs.map(ref2 => this.contains(this.tolower(toString(ref2)), this.tolower(arg))).join(' or ') + ')'
31
37
  },
32
38
  /**
@@ -159,8 +165,8 @@ const StandardFunctions = {
159
165
  * Generates SQL statement that produces current point in time (date and time with time zone)
160
166
  * @returns {string}
161
167
  */
162
- now: function() {
163
- return this.session_context({val: '$now'})
168
+ now: function () {
169
+ return this.session_context({ val: '$now' })
164
170
  },
165
171
  /**
166
172
  * Generates SQL statement that produces the year of a given timestamp
package/lib/cqn2sql.js CHANGED
@@ -13,14 +13,14 @@ const BINARY_TYPES = {
13
13
  const { Readable } = require('stream')
14
14
 
15
15
  const DEBUG = (() => {
16
- let DEBUG = cds.debug('sql-json')
17
- if (DEBUG) return DEBUG
18
- else DEBUG = cds.debug('sql|sqlite')
19
- if (DEBUG) {
20
- return DEBUG
16
+ const LOG = cds.log('sql-json')
17
+ if (LOG._debug) return cds.debug('sql-json')
18
+ return cds.debug('sql|sqlite')
19
+ //if (DEBUG) {
20
+ // return DEBUG
21
21
  // (sql, ...more) => DEBUG (sql.replace(/(?:SELECT[\n\r\s]+(json_group_array\()?[\n\r\s]*json_insert\((\n|\r|.)*?\)[\n\r\s]*\)?[\n\r\s]+as[\n\r\s]+_json_[\n\r\s]+FROM[\n\r\s]*\(|\)[\n\r\s]*(\)[\n\r\s]+AS )|\)$)/gim,(a,b,c,d) => d || ''), ...more)
22
22
  // FIXME: looses closing ) on INSERT queries
23
- }
23
+ //}
24
24
  })()
25
25
 
26
26
  class CQN2SQLRenderer {
package/lib/cqn4sql.js CHANGED
@@ -530,7 +530,7 @@ function cqn4sql(originalQuery, model) {
530
530
  res = getTransformedTokenStream([value], baseLink)[0]
531
531
  } else if (xpr) {
532
532
  res = { xpr: getTransformedTokenStream(value.xpr, baseLink) }
533
- } else if (val) {
533
+ } else if (val !== undefined) {
534
534
  res = { val }
535
535
  } else if (func) {
536
536
  res = { args: getTransformedFunctionArgs(value.args, baseLink), func: value.func }
@@ -884,6 +884,7 @@ function cqn4sql(originalQuery, model) {
884
884
 
885
885
  if (expand.expand) {
886
886
  const nested = _subqueryForGroupBy(expand, fullRef, expand.as || expand.ref.map(idOnly).join('_'))
887
+ setElementOnColumns(nested, expand.element)
887
888
  elements[expand.as || expand.ref.map(idOnly).join('_')] = nested
888
889
  return nested
889
890
  }
@@ -1852,6 +1853,11 @@ function cqn4sql(originalQuery, model) {
1852
1853
  result[i] = asXpr(xpr)
1853
1854
  continue
1854
1855
  }
1856
+ if(lhs.args) {
1857
+ const args = calculateOnCondition(lhs.args)
1858
+ result[i] = { ...lhs, args }
1859
+ continue
1860
+ }
1855
1861
  const rhs = result[i + 2]
1856
1862
  if (rhs?.ref || lhs.ref) {
1857
1863
  // if we have refs on each side of the comparison, we might need to perform tuple expansion
@@ -1,6 +1,7 @@
1
1
  const cds = require('@sap/cds')
2
2
  const { _target_name4 } = require('./SQLService')
3
- const InsertResult = require('../lib/InsertResults')
3
+
4
+ const ROOT = Symbol('root')
4
5
 
5
6
  // REVISIT: remove old path with cds^8
6
7
  let _compareJson
@@ -45,20 +46,22 @@ async function onDeep(req, next) {
45
46
  if (query.UPDATE && !beforeData.length) return 0
46
47
 
47
48
  const queries = getDeepQueries(query, beforeData, target)
48
- const res = await Promise.all(queries.map(query => {
49
- if (query.INSERT) return this.onINSERT({ query })
50
- if (query.UPDATE) return this.onUPDATE({ query })
51
- if (query.DELETE) return this.onSIMPLE({ query })
52
- }))
53
- return (
54
- beforeData.length ||
55
- new InsertResult(query, [
56
- {
57
- changes: Array.isArray(req.data) ? req.data.length : 1,
58
- ...(res[0]?.results[0]?.lastInsertRowid ? { lastInsertRowid: res[0].results[0].lastInsertRowid } : {}),
59
- },
60
- ])
61
- )
49
+
50
+ // first delete, then update, then insert because of potential unique constraints:
51
+ // - deletes never trigger unique constraints, but can prevent them -> execute first
52
+ // - updates can trigger and prevent unique constraints -> execute second
53
+ // - inserts can only trigger unique constraints -> execute last
54
+ await Promise.all(Array.from(queries.deletes.values()).map(query => this.onSIMPLE({ query })))
55
+ await Promise.all(queries.updates.map(query => this.onUPDATE({ query })))
56
+
57
+ const rootQuery = queries.inserts.get(ROOT)
58
+ queries.inserts.delete(ROOT)
59
+ const [rootResult] = await Promise.all([
60
+ rootQuery && this.onINSERT({ query: rootQuery }),
61
+ ...Array.from(queries.inserts.values()).map(query => this.onINSERT({ query })),
62
+ ])
63
+
64
+ return beforeData.length ?? rootResult
62
65
  }
63
66
 
64
67
  const hasDeep = (q, target) => {
@@ -195,7 +198,7 @@ const getDeepQueries = (query, dbData, target) => {
195
198
  diff = [diff]
196
199
  }
197
200
 
198
- return _getDeepQueries(diff, target, true)
201
+ return _getDeepQueries(diff, target)
199
202
  }
200
203
 
201
204
  const _hasManagedElements = target => {
@@ -205,16 +208,19 @@ const _hasManagedElements = target => {
205
208
  /**
206
209
  * @param {unknown[]} diff
207
210
  * @param {import('@sap/cds/apis/csn').Definition} target
208
- * @param {boolean} [root=false]
209
- * @returns {import('@sap/cds/apis/cqn').Query[]}
211
+ * @param {Map<String, Object>} deletes
212
+ * @param {Map<String, Object>} inserts
213
+ * @param {Object[]} updates
214
+ * @param {boolean} [root=true]
215
+ * @returns {Object|Boolean}
210
216
  */
211
- const _getDeepQueries = (diff, target, root = false) => {
212
- const queries = []
213
-
217
+ const _getDeepQueries = (diff, target, deletes = new Map(), inserts = new Map(), updates = [], root = true) => {
218
+ // flag to determine if queries were created
219
+ let dirty = false
214
220
  for (const diffEntry of diff) {
215
221
  if (diffEntry === undefined) continue
216
- const subQueries = []
217
222
 
223
+ let childrenDirty = false
218
224
  for (const prop in diffEntry) {
219
225
  // handle deep operations
220
226
 
@@ -224,9 +230,12 @@ const _getDeepQueries = (diff, target, root = false) => {
224
230
  delete diffEntry[prop]
225
231
  } else if (target.compositions?.[prop]) {
226
232
  const arrayed = Array.isArray(propData) ? propData : [propData]
227
- arrayed.forEach(subEntry => {
228
- subQueries.push(..._getDeepQueries([subEntry], target.elements[prop]._target))
229
- })
233
+ childrenDirty =
234
+ arrayed
235
+ .map(subEntry =>
236
+ _getDeepQueries([subEntry], target.elements[prop]._target, deletes, inserts, updates, false),
237
+ )
238
+ .some(a => a) || childrenDirty
230
239
  delete diffEntry[prop]
231
240
  } else if (diffEntry[prop] === undefined) {
232
241
  // restore current behavior, if property is undefined, not part of payload
@@ -242,12 +251,32 @@ const _getDeepQueries = (diff, target, root = false) => {
242
251
  delete diffEntry._old
243
252
  }
244
253
 
245
- // first calculate subqueries and rm their properties, then build root query
246
254
  if (op === 'create') {
247
- queries.push(INSERT.into(target).entries(diffEntry))
255
+ dirty = true
256
+ const id = root ? ROOT : target.name
257
+ const insert = inserts.get(id)
258
+ if (insert) {
259
+ insert.INSERT.entries.push(diffEntry)
260
+ } else {
261
+ const q = INSERT.into(target).entries(diffEntry)
262
+ inserts.set(id, q)
263
+ }
248
264
  } else if (op === 'delete') {
249
- queries.push(DELETE.from(target).where(diffEntry))
250
- } else if (op === 'update' || (op === undefined && (root || subQueries.length) && _hasManagedElements(target))) {
265
+ dirty = true
266
+ const keys = cds.utils
267
+ .Object_keys(target.keys)
268
+ .filter(key => !target.keys[key].virtual && !target.keys[key].isAssociation)
269
+
270
+ const keyVals = keys.map(k => ({ val: diffEntry[k] }))
271
+ const currDelete = deletes.get(target.name)
272
+ if (currDelete) currDelete.DELETE.where[2].list.push({ list: keyVals })
273
+ else {
274
+ const left = { list: keys.map(k => ({ ref: [k] })) }
275
+ const right = { list: [{ list: keyVals }] }
276
+ deletes.set(target.name, DELETE.from(target).where([left, 'in', right]))
277
+ }
278
+ } else if (op === 'update' || (op === undefined && (root || childrenDirty) && _hasManagedElements(target))) {
279
+ dirty = true
251
280
  // TODO do we need the where here?
252
281
  const keys = target.keys
253
282
  const cqn = UPDATE(target).with(diffEntry)
@@ -259,34 +288,16 @@ const _getDeepQueries = (diff, target, root = false) => {
259
288
  delete diffEntry[key]
260
289
  }
261
290
  cqn.with(diffEntry)
262
- queries.push(cqn)
291
+ updates.push(cqn)
263
292
  }
264
-
265
- for (const q of subQueries) queries.push(q)
266
293
  }
267
294
 
268
- const insertQueries = new Map()
269
-
270
- return queries.map(q => {
271
- // Merge all INSERT statements for each target
272
- if (q.INSERT) {
273
- const target = q.target
274
- if (insertQueries.has(target)) {
275
- insertQueries.get(target).INSERT.entries.push(...q.INSERT.entries)
276
- return
277
- } else {
278
- insertQueries.set(target, q)
279
- }
280
- }
281
- Object.defineProperty(q, handledDeep, { value: true })
282
- return q
283
- })
284
- .filter(a => a)
295
+ return root ? { updates, inserts, deletes } : dirty
285
296
  }
286
297
 
287
298
  module.exports = {
288
299
  onDeep,
289
- getDeepQueries,
290
- getExpandForDeep,
291
300
  hasDeep,
301
+ getDeepQueries, // only for testing
302
+ getExpandForDeep, // only for testing
292
303
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "1.12.0",
3
+ "version": "1.12.1",
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": {