@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 +13 -0
- package/lib/SQLService.js +2 -1
- package/lib/cqn2sql.js +44 -47
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
556
|
-
if (_with) _add
|
|
557
|
-
function _add
|
|
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
|
-
|
|
606
|
-
|
|
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 (
|
|
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 === '=')
|
|
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)
|
|
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
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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
|
-
|
|
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