@cap-js/db-service 1.16.2 → 1.17.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,6 +4,32 @@
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.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.17.0...db-service-v1.17.1) (2025-02-04)
8
+
9
+
10
+ ### Fixed
11
+
12
+ * deep update resulting in deep delete of sub-element ([#1006](https://github.com/cap-js/cds-dbs/issues/1006)) ([ef2f817](https://github.com/cap-js/cds-dbs/commit/ef2f8175df6fc7076fa8a9290e1863f44d267d8d))
13
+ * nested $self reference to other column ([#1009](https://github.com/cap-js/cds-dbs/issues/1009)) ([41a76d8](https://github.com/cap-js/cds-dbs/commit/41a76d89a884ac8266ccbd2d087af435e8f26ccb))
14
+
15
+ ## [1.17.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.16.2...db-service-v1.17.0) (2025-01-28)
16
+
17
+
18
+ ### Added
19
+
20
+ * support for cds.Map ([#889](https://github.com/cap-js/cds-dbs/issues/889)) ([cde7514](https://github.com/cap-js/cds-dbs/commit/cde7514df20396383e0179ffce838596e3706bb2))
21
+
22
+
23
+ ### Fixed
24
+
25
+ * **`UPDATE`:** no assocs in list which matches subquery results ([4bcb88a](https://github.com/cap-js/cds-dbs/commit/4bcb88a1f40540e26cebd4907bdd33e90d08bb9d))
26
+ * **`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))
27
+ * 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))
28
+ * 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))
29
+ * 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))
30
+ * 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))
31
+ * 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))
32
+
7
33
  ## [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
34
 
9
35
 
@@ -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)} 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}`
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 { 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
 
@@ -51,7 +51,7 @@ async function onDeep(req, next) {
51
51
  // - deletes never trigger unique constraints, but can prevent them -> execute first
52
52
  // - updates can trigger and prevent unique constraints -> execute second
53
53
  // - inserts can only trigger unique constraints -> execute last
54
- await Promise.all(Array.from(queries.deletes.values()).map(query => this.onSIMPLE({ query })))
54
+ await Promise.all(Array.from(queries.deletes.values()).map(query => this.onDELETE({ query, target: query._target })))
55
55
  await Promise.all(queries.updates.map(query => this.onUPDATE({ query })))
56
56
 
57
57
  const rootQuery = queries.inserts.get(ROOT)
@@ -207,12 +207,12 @@ function infer(originalQuery, model) {
207
207
  if (as === undefined) cds.error`Expecting expression to have an alias name`
208
208
  if (queryElements[as]) cds.error`Duplicate definition of element “${as}”`
209
209
  if (col.xpr || col.SELECT) {
210
- queryElements[as] = getElementForXprOrSubquery(col, queryElements)
210
+ queryElements[as] = getElementForXprOrSubquery(col, queryElements, dollarSelfRefs)
211
211
  }
212
212
  if (col.func) {
213
213
  if (col.args) {
214
214
  // {func}.args are optional
215
- applyToFunctionArgs(col.args, inferArg, [false])
215
+ applyToFunctionArgs(col.args, inferArg, [false, null, {dollarSelfRefs}])
216
216
  }
217
217
  queryElements[as] = getElementForCast(col)
218
218
  }
@@ -287,7 +287,7 @@ function infer(originalQuery, model) {
287
287
  if (having) walkTokenStream(having)
288
288
  if (_.with)
289
289
  // consider UPDATE.with
290
- Object.values(_.with).forEach(val => inferArg(val, queryElements, null, { inExpr: true }))
290
+ Object.values(_.with).forEach(val => inferArg(val, queryElements, null, { inXpr: true }))
291
291
 
292
292
  return queryElements
293
293
 
@@ -299,7 +299,7 @@ function infer(originalQuery, model) {
299
299
  *
300
300
  * @param {array} tokenStream
301
301
  */
302
- function walkTokenStream(tokenStream, inExpr = false) {
302
+ function walkTokenStream(tokenStream, inXpr = false) {
303
303
  let skipJoins
304
304
  const processToken = t => {
305
305
  if (t === 'exists') {
@@ -309,7 +309,7 @@ function infer(originalQuery, model) {
309
309
  // don't miss an exists within an expression
310
310
  t.xpr.forEach(processToken)
311
311
  } else {
312
- inferArg(t, queryElements, null, { inExists: skipJoins, inExpr, inQueryModifier: true })
312
+ inferArg(t, queryElements, null, { inExists: skipJoins, inXpr, inQueryModifier: true })
313
313
  skipJoins = false
314
314
  }
315
315
  }
@@ -329,11 +329,12 @@ function infer(originalQuery, model) {
329
329
  const unprocessedColumns = []
330
330
 
331
331
  for (const currentDollarSelfColumn of dollarSelfColumns) {
332
- const { ref } = currentDollarSelfColumn
332
+ const { ref, inXpr } = currentDollarSelfColumn
333
333
  const stepToFind = ref[1]
334
334
 
335
335
  const referencesOtherDollarSelfColumn = dollarSelfColumns.find(
336
336
  otherDollarSelfCol =>
337
+ !(stepToFind in queryElements) &&
337
338
  otherDollarSelfCol !== currentDollarSelfColumn &&
338
339
  (otherDollarSelfCol.as
339
340
  ? stepToFind === otherDollarSelfCol.as
@@ -343,7 +344,7 @@ function infer(originalQuery, model) {
343
344
  if (referencesOtherDollarSelfColumn) {
344
345
  unprocessedColumns.push(currentDollarSelfColumn)
345
346
  } else {
346
- handleRef(currentDollarSelfColumn)
347
+ handleRef(currentDollarSelfColumn, inXpr)
347
348
  }
348
349
  }
349
350
 
@@ -351,8 +352,8 @@ function infer(originalQuery, model) {
351
352
  } while (dollarSelfColumns.length > 0)
352
353
  }
353
354
 
354
- function handleRef(col) {
355
- inferArg(col, queryElements)
355
+ function handleRef(col, inXpr) {
356
+ inferArg(col, queryElements, null, { inXpr })
356
357
  const { definition } = col.$refLinks[col.$refLinks.length - 1]
357
358
  if (col.cast)
358
359
  // final type overwritten -> element not visible anymore
@@ -379,7 +380,7 @@ function infer(originalQuery, model) {
379
380
  * @param {object} [context={}] - Contextual information for element inference.
380
381
  * @param {boolean} [context.inExists=false] - Flag to control the creation of joins for non-association path traversals.
381
382
  * for `exists <assoc>` paths we do not need to create joins for path expressions as they are part of the semi-joined subquery.
382
- * @param {boolean} [context.inExpr=false] - Flag to signal whether the element is part of an expression.
383
+ * @param {boolean} [context.inXpr=false] - Flag to signal whether the element is part of an expression.
383
384
  * Used to ignore non-persisted elements.
384
385
  * @param {boolean} [context.inNestedProjection=false] - Flag to signal whether the element is part of a nested projection.
385
386
  *
@@ -401,11 +402,11 @@ function infer(originalQuery, model) {
401
402
  */
402
403
 
403
404
  function inferArg(arg, queryElements = null, $baseLink = null, context = {}) {
404
- const { inExists, inExpr, inCalcElement, baseColumn, inInfixFilter, inQueryModifier, inFrom } = context
405
+ const { inExists, inXpr, inCalcElement, baseColumn, inInfixFilter, inQueryModifier, inFrom, dollarSelfRefs } = context
405
406
  if (arg.param || arg.SELECT) return // parameter references are only resolved into values on execution e.g. :val, :1 or ?
406
407
  if (arg.args) applyToFunctionArgs(arg.args, inferArg, [null, $baseLink, context])
407
408
  if (arg.list) arg.list.forEach(arg => inferArg(arg, null, $baseLink, context))
408
- if (arg.xpr) arg.xpr.forEach(token => inferArg(token, queryElements, $baseLink, { ...context, inExpr: true })) // e.g. function in expression
409
+ if (arg.xpr) arg.xpr.forEach(token => inferArg(token, queryElements, $baseLink, { ...context, inXpr: true })) // e.g. function in expression
409
410
 
410
411
  if (!arg.ref) {
411
412
  if (arg.expand && queryElements) queryElements[arg.as] = resolveExpand(arg)
@@ -426,6 +427,11 @@ function infer(originalQuery, model) {
426
427
  firstStepIsSelf = !firstStepIsTableAlias && arg.ref.length > 1 && ['$self', '$projection'].includes(arg.ref[0])
427
428
  expandOnTableAlias = arg.ref.length === 1 && arg.ref[0] in sources && (arg.expand || arg.inline)
428
429
  }
430
+ if(dollarSelfRefs && firstStepIsSelf) {
431
+ Object.defineProperty(arg, 'inXpr', { value: true, writable: true })
432
+ dollarSelfRefs.push(arg)
433
+ return
434
+ }
429
435
  const nameSegments = []
430
436
  // if a (segment) of a (structured) foreign key is renamed, we must not include
431
437
  // the aliased ref segments into the name of the final foreign key which is e.g. used in
@@ -564,7 +570,7 @@ function infer(originalQuery, model) {
564
570
  } else if (token.ref || token.xpr || token.list) {
565
571
  inferArg(token, false, arg.$refLinks[i], {
566
572
  inExists: skipJoinsForFilter || inExists,
567
- inExpr: !!token.xpr,
573
+ inXpr: !!token.xpr,
568
574
  inInfixFilter: true,
569
575
  inFrom,
570
576
  })
@@ -573,7 +579,7 @@ function infer(originalQuery, model) {
573
579
  applyToFunctionArgs(token.args, inferArg, [
574
580
  false,
575
581
  arg.$refLinks[i],
576
- { inExists: skipJoinsForFilter || inExists, inExpr: true, inInfixFilter: true, inFrom },
582
+ { inExists: skipJoinsForFilter || inExists, inXpr: true, inInfixFilter: true, inFrom },
577
583
  ])
578
584
  }
579
585
  }
@@ -641,7 +647,7 @@ function infer(originalQuery, model) {
641
647
  }
642
648
  }
643
649
  const leafArt = arg.$refLinks[arg.$refLinks.length - 1].definition
644
- const virtual = (leafArt.virtual || !isPersisted) && !inExpr
650
+ const virtual = (leafArt.virtual || !isPersisted) && !inXpr
645
651
  // check if we need to merge the column `ref` into the join tree of the query
646
652
  if (!inFrom && !inExists && !virtual && !inCalcElement) {
647
653
  // for a ref inside an `inline` we need to consider the column `ref` which has the `inline` prop
@@ -658,7 +664,7 @@ function infer(originalQuery, model) {
658
664
  }
659
665
 
660
666
  function insertIntoQueryElements() {
661
- return queryElements && !inExpr && !inInfixFilter && !inQueryModifier
667
+ return queryElements && !inXpr && !inInfixFilter && !inQueryModifier
662
668
  }
663
669
 
664
670
  /**
@@ -686,7 +692,7 @@ function infer(originalQuery, model) {
686
692
  }
687
693
  let elements = {}
688
694
  inline.forEach(inlineCol => {
689
- inferArg(inlineCol, null, $leafLink, { inExpr: true, baseColumn: col })
695
+ inferArg(inlineCol, null, $leafLink, { inXpr: true, baseColumn: col })
690
696
  if (inlineCol === '*') {
691
697
  const wildCardElements = {}
692
698
  // either the `.elements´ of the struct or the `.elements` of the assoc target
@@ -762,7 +768,7 @@ function infer(originalQuery, model) {
762
768
  if (e === '*') {
763
769
  elements = { ...elements, ...$leafLink.definition.elements }
764
770
  } else {
765
- inferArg(e, false, $leafLink, { inExpr: true })
771
+ inferArg(e, false, $leafLink, { inXpr: true })
766
772
  if (e.expand) elements[e.as || e.flatName] = resolveExpand(e)
767
773
  if (e.inline) elements = { ...elements, ...resolveInline(e) }
768
774
  else elements[e.as || e.flatName] = e.$refLinks ? e.$refLinks[e.$refLinks.length - 1].definition : e
@@ -859,7 +865,7 @@ function infer(originalQuery, model) {
859
865
  } else if (arg.xpr || arg.args) {
860
866
  const prop = arg.xpr ? 'xpr' : 'args'
861
867
  arg[prop].forEach(step => {
862
- const subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
868
+ let subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
863
869
  if (step.ref) {
864
870
  step.$refLinks.forEach((link, i) => {
865
871
  const { definition } = link
@@ -874,6 +880,10 @@ function infer(originalQuery, model) {
874
880
  } else if (step.args || step.xpr) {
875
881
  const nestedProp = step.xpr ? 'xpr' : 'args'
876
882
  step[nestedProp].forEach(a => {
883
+ // reset sub path for each nested argument
884
+ // e.g. case when <path> then <otherPath> else <anotherPath> end
885
+ if(!a.ref)
886
+ subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
877
887
  mergePathsIntoJoinTree(a, subPath)
878
888
  })
879
889
  }
@@ -959,7 +969,8 @@ function infer(originalQuery, model) {
959
969
  if (element.type !== 'cds.LargeBinary') {
960
970
  queryElements[k] = element
961
971
  }
962
- if (isCalculatedOnRead(element)) {
972
+ // only relevant if we actually select the calculated element
973
+ if (originalQuery.SELECT && isCalculatedOnRead(element)) {
963
974
  linkCalculatedElement(element)
964
975
  }
965
976
  }
@@ -1003,7 +1014,7 @@ function infer(originalQuery, model) {
1003
1014
  * @param {object} col
1004
1015
  * @returns object
1005
1016
  */
1006
- function getElementForXprOrSubquery(col, queryElements) {
1017
+ function getElementForXprOrSubquery(col, queryElements, dollarSelfRefs) {
1007
1018
  const { xpr } = col
1008
1019
  let skipJoins = false
1009
1020
  xpr?.forEach(token => {
@@ -1011,7 +1022,7 @@ function infer(originalQuery, model) {
1011
1022
  // no joins for infix filters along `exists <path>`
1012
1023
  skipJoins = true
1013
1024
  } else {
1014
- inferArg(token, queryElements, null, { inExists: skipJoins, inExpr: true })
1025
+ inferArg(token, queryElements, null, { inExists: skipJoins, inXpr: true, dollarSelfRefs })
1015
1026
  skipJoins = false
1016
1027
  }
1017
1028
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "1.16.2",
3
+ "version": "1.17.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": {