@cap-js/db-service 1.5.0 → 1.5.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,19 @@
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.5.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.5.0...db-service-v1.5.1) (2023-12-20)
8
+
9
+
10
+ ### Fixed
11
+
12
+ * **cqn2sql:** supporting calculated elements ([#387](https://github.com/cap-js/cds-dbs/issues/387)) ([2153fb9](https://github.com/cap-js/cds-dbs/commit/2153fb9a3910cd4afa3a91918e6cf682646492b7))
13
+ * do not rely on db constraints for deep delete ([#390](https://github.com/cap-js/cds-dbs/issues/390)) ([9623af6](https://github.com/cap-js/cds-dbs/commit/9623af64db97cfe15ef07b659635850fc908f77c))
14
+
15
+
16
+ ### Performance Improvements
17
+
18
+ * HANA list placeholder ([#380](https://github.com/cap-js/cds-dbs/issues/380)) ([3eadfea](https://github.com/cap-js/cds-dbs/commit/3eadfea7b94f485030cc8bd0bd298ce088586422))
19
+
7
20
  ## [1.5.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.4.0...db-service-v1.5.0) (2023-12-06)
8
21
 
9
22
 
package/lib/SQLService.js CHANGED
@@ -153,7 +153,8 @@ class SQLService extends DatabaseService {
153
153
  }
154
154
 
155
155
  get onDELETE() {
156
- return super.onDELETE = cds.env.features.assert_integrity === 'db' ? this.onSIMPLE : deep_delete
156
+ // REVISIT: It's not yet 100 % clear under which circumstances we can rely on db constraints
157
+ return super.onDELETE = /* cds.env.features.assert_integrity === 'db' ? this.onSIMPLE : */ deep_delete
157
158
  async function deep_delete(/** @type {Request} */ req) {
158
159
  let { compositions } = req.target
159
160
  if (compositions) {
package/lib/cqn2sql.js CHANGED
@@ -27,7 +27,7 @@ class CQN2SQLRenderer {
27
27
  this.class._init() // is a noop for subsequent calls
28
28
  }
29
29
 
30
- static _add_mixins (aspect, mixins) {
30
+ static _add_mixins(aspect, mixins) {
31
31
  const fqn = this.name + aspect
32
32
  const types = cds.builtin.types
33
33
  for (let each in mixins) {
@@ -52,7 +52,7 @@ class CQN2SQLRenderer {
52
52
  this.ReservedWords[each[0] + each.slice(1).toLowerCase()] = 1 // Order
53
53
  this.ReservedWords[each.toLowerCase()] = 1 // order
54
54
  }
55
- this._init = () => {} // makes this a noop for subsequent calls
55
+ this._init = () => { } // makes this a noop for subsequent calls
56
56
  }
57
57
 
58
58
  /**
@@ -208,7 +208,7 @@ class CQN2SQLRenderer {
208
208
  if (limit) sql += ` LIMIT ${this.limit(limit)}`
209
209
  // Expand cannot work without an inferred query
210
210
  if (expand) {
211
- if ('elements' in q) sql = this.SELECT_expand (q,sql)
211
+ if ('elements' in q) sql = this.SELECT_expand(q, sql)
212
212
  else cds.error`Query was not inferred and includes expand. For which the metadata is missing.`
213
213
  }
214
214
  return (this.sql = sql)
@@ -249,7 +249,7 @@ class CQN2SQLRenderer {
249
249
  // Prevent SQLite from hitting function argument limit of 100
250
250
  let obj = ''
251
251
 
252
- if(cols.length < 50) obj = `json_object(${cols.slice(0, 50)})`
252
+ if (cols.length < 50) obj = `json_object(${cols.slice(0, 50)})`
253
253
  else {
254
254
  const chunks = []
255
255
  for (let i = 0; i < cols.length; i += 50) {
@@ -342,9 +342,9 @@ class CQN2SQLRenderer {
342
342
  return orderBy.map(
343
343
  localized
344
344
  ? c =>
345
- this.expr(c) +
346
- (c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
347
- (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
345
+ this.expr(c) +
346
+ (c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
347
+ (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
348
348
  : c => this.expr(c) + (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC'),
349
349
  )
350
350
  }
@@ -372,12 +372,12 @@ class CQN2SQLRenderer {
372
372
  return INSERT.entries
373
373
  ? this.INSERT_entries(q)
374
374
  : INSERT.rows
375
- ? this.INSERT_rows(q)
376
- : INSERT.values
377
- ? this.INSERT_values(q)
378
- : INSERT.as
379
- ? this.INSERT_select(q)
380
- : cds.error`Missing .entries, .rows, or .values in ${q}`
375
+ ? this.INSERT_rows(q)
376
+ : INSERT.values
377
+ ? this.INSERT_values(q)
378
+ : INSERT.as
379
+ ? this.INSERT_select(q)
380
+ : cds.error`Missing .entries, .rows, or .values in ${q}`
381
381
  }
382
382
 
383
383
  /**
@@ -394,7 +394,7 @@ class CQN2SQLRenderer {
394
394
  return // REVISIT: mtx sends an insert statement without entries and no reference entity
395
395
  }
396
396
  const columns = elements
397
- ? ObjectKeys(elements).filter(c => c in elements && !elements[c].virtual && !elements[c].isAssociation)
397
+ ? ObjectKeys(elements).filter(c => c in elements && !elements[c].virtual && !elements[c].value && !elements[c].isAssociation)
398
398
  : ObjectKeys(INSERT.entries[0])
399
399
 
400
400
  /** @type {string[]} */
@@ -427,9 +427,8 @@ class CQN2SQLRenderer {
427
427
  // Include this.values for placeholders
428
428
  /** @type {unknown[][]} */
429
429
  this.entries = [[...this.values, JSON.stringify(INSERT.entries)]]
430
- return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${
431
- this.columns
432
- }) SELECT ${extraction} FROM json_each(?)`)
430
+ return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns
431
+ }) SELECT ${extraction} FROM json_each(?)`)
433
432
  }
434
433
 
435
434
  /**
@@ -443,21 +442,20 @@ class CQN2SQLRenderer {
443
442
  const alias = INSERT.into.as
444
443
  const elements = q.elements || q.target?.elements
445
444
  const columns = INSERT.columns
446
- || cds.error`Cannot insert rows without columns or elements`
445
+ || cds.error`Cannot insert rows without columns or elements`
447
446
 
448
447
  const inputConverter = this.class._convertInput
449
- const extraction = columns.map((c,i) => {
448
+ const extraction = columns.map((c, i) => {
450
449
  const extract = `value->>'$[${i}]'`
451
450
  const element = elements?.[c]
452
451
  const converter = element?.[inputConverter]
453
- return converter?.(extract,element) || extract
452
+ return converter?.(extract, element) || extract
454
453
  })
455
454
 
456
455
  this.columns = columns.map(c => this.quote(c))
457
456
  this.entries = [[JSON.stringify(INSERT.rows)]]
458
- return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${
459
- this.columns
460
- }) SELECT ${extraction} FROM json_each(?)`)
457
+ return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns
458
+ }) SELECT ${extraction} FROM json_each(?)`)
461
459
  }
462
460
 
463
461
  /**
@@ -552,9 +550,9 @@ class CQN2SQLRenderer {
552
550
  if (entity.as) sql += ` AS ${entity.as}`
553
551
 
554
552
  let columns = []
555
- if (data) _add (data, val => this.val({val}))
556
- if (_with) _add (_with, x => this.expr(x))
557
- function _add (data, sql4) {
553
+ if (data) _add(data, val => this.val({ val }))
554
+ if (_with) _add(_with, x => this.expr(x))
555
+ function _add(data, sql4) {
558
556
  for (let c in data) {
559
557
  if (!elements || (c in elements && !elements[c].virtual)) {
560
558
  columns.push({ name: c, sql: sql4(data[c]) })
@@ -602,8 +600,8 @@ class CQN2SQLRenderer {
602
600
  return STREAM.from
603
601
  ? this.STREAM_from(q)
604
602
  : STREAM.into
605
- ? this.STREAM_into(q)
606
- : cds.error`Missing .form or .into in ${q}`
603
+ ? this.STREAM_into(q)
604
+ : cds.error`Missing .form or .into in ${q}`
607
605
  }
608
606
 
609
607
  /**
@@ -668,7 +666,7 @@ class CQN2SQLRenderer {
668
666
  expr(x) {
669
667
  const wrap = x.cast ? sql => `cast(${sql} as ${this.type4(x.cast)})` : sql => sql
670
668
  if (typeof x === 'string') throw cds.error`Unsupported expr: ${x}`
671
- if ('param' in x) return wrap(this.param(x))
669
+ if (x.param) return wrap(this.param(x))
672
670
  if ('ref' in x) return wrap(this.ref(x))
673
671
  if ('val' in x) return wrap(this.val(x))
674
672
  if ('xpr' in x) return wrap(this.xpr(x))
@@ -704,15 +702,15 @@ class CQN2SQLRenderer {
704
702
  operator(x, i, xpr) {
705
703
 
706
704
  // Translate = to IS NULL for rhs operand being NULL literal
707
- if (x === '=') return xpr[i+1]?.val === null ? 'is' : '='
705
+ if (x === '=') return xpr[i + 1]?.val === null ? 'is' : '='
708
706
 
709
707
  // Translate == to IS NOT NULL for rhs operand being NULL literal, otherwise ...
710
708
  // Translate == to IS NOT DISTINCT FROM, unless both operands cannot be NULL
711
- if (x === '==') return xpr[i+1]?.val === null ? 'is' : _not_null(i-1) && _not_null(i+1) ? '=' : this.is_not_distinct_from_
709
+ if (x === '==') return xpr[i + 1]?.val === null ? 'is' : _not_null(i - 1) && _not_null(i + 1) ? '=' : this.is_not_distinct_from_
712
710
 
713
711
  // Translate != to IS NULL for rhs operand being NULL literal, otherwise...
714
712
  // Translate != to IS DISTINCT FROM, unless both operands cannot be NULL
715
- if (x === '!=') return xpr[i+1]?.val === null ? 'is not' : _not_null(i-1) && _not_null(i+1) ? '<>' : this.is_distinct_from_
713
+ if (x === '!=') return xpr[i + 1]?.val === null ? 'is not' : _not_null(i - 1) && _not_null(i + 1) ? '<>' : this.is_distinct_from_
716
714
 
717
715
  else return x
718
716
 
@@ -749,9 +747,9 @@ class CQN2SQLRenderer {
749
747
  */
750
748
  ref({ ref }) {
751
749
  switch (ref[0]) {
752
- case '$now': return this.func({ func: 'session_context', args: [{ val: '$now' }]})
750
+ case '$now': return this.func({ func: 'session_context', args: [{ val: '$now', param: false }] })
753
751
  case '$user':
754
- case '$user.id': return this.func({ func: 'session_context', args: [{ val: '$user.id' }]})
752
+ case '$user.id': return this.func({ func: 'session_context', args: [{ val: '$user.id', param: false }] })
755
753
  default: return ref.map(r => this.quote(r)).join('.')
756
754
  }
757
755
  }
@@ -761,7 +759,7 @@ class CQN2SQLRenderer {
761
759
  * @param {import('./infer/cqn').val} param0
762
760
  * @returns {string} SQL
763
761
  */
764
- val({ val }) {
762
+ val({ val, param }) {
765
763
  switch (typeof val) {
766
764
  case 'function': throw new Error('Function values not supported.')
767
765
  case 'undefined': return 'NULL'
@@ -770,13 +768,13 @@ class CQN2SQLRenderer {
770
768
  case 'object':
771
769
  if (val === null) return 'NULL'
772
770
  if (val instanceof Date) return `'${val.toISOString()}'`
773
- if (val instanceof Readable) ; // go on with default below
771
+ if (val instanceof Readable); // go on with default below
774
772
  else if (Buffer.isBuffer(val)) val = val.toString('base64')
775
773
  else if (is_regexp(val)) val = val.source
776
774
  else val = JSON.stringify(val)
777
775
  case 'string': // eslint-disable-line no-fallthrough
778
776
  }
779
- if (!this.values) return this.string(val)
777
+ if (!this.values || param === false) return this.string(val)
780
778
  else this.values.push(val)
781
779
  return '?'
782
780
  }
@@ -860,12 +858,12 @@ class CQN2SQLRenderer {
860
858
  const requiredColumns = !elements
861
859
  ? []
862
860
  : Object.keys(elements)
863
- .filter(
864
- e =>
865
- (elements[e]?.[annotation] || (!isUpdate && elements[e]?.default && !elements[e].virtual && !elements[e].isAssociation)) &&
866
- !columns.find(c => c.name === e),
867
- )
868
- .map(name => ({ name, sql: 'NULL' }))
861
+ .filter(
862
+ e =>
863
+ (elements[e]?.[annotation] || (!isUpdate && elements[e]?.default && !elements[e].virtual && !elements[e].isAssociation)) &&
864
+ !columns.find(c => c.name === e),
865
+ )
866
+ .map(name => ({ name, sql: 'NULL' }))
869
867
 
870
868
  return [...columns, ...requiredColumns].map(({ name, sql }) => {
871
869
  let element = elements?.[name] || {}
@@ -875,14 +873,13 @@ class CQN2SQLRenderer {
875
873
  if (converter && sql[0] !== '$') sql = converter(sql, element)
876
874
 
877
875
  let val = _managed[element[annotation]?.['=']]
878
- if (val) sql = `coalesce(${sql}, ${this.func({ func: 'session_context', args: [{ val }] })})`
876
+ if (val) sql = `coalesce(${sql}, ${this.func({ func: 'session_context', args: [{ val, param: false }] })})`
879
877
  else if (!isUpdate && element.default) {
880
878
  const d = element.default
881
879
  if (d.val !== undefined || d.ref?.[0] === '$now') {
882
880
  // REVISIT: d.ref is not used afterwards
883
- sql = `(CASE WHEN json_type(value,'$."${name}"') IS NULL THEN ${
884
- this.defaultValue(d.val) // REVISIT: this.defaultValue is a strange function
885
- } ELSE ${sql} END)`
881
+ sql = `(CASE WHEN json_type(value,'$."${name}"') IS NULL THEN ${this.defaultValue(d.val) // REVISIT: this.defaultValue is a strange function
882
+ } ELSE ${sql} END)`
886
883
  }
887
884
  }
888
885
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "1.5.0",
3
+ "version": "1.5.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": {