@cap-js/db-service 1.14.0 → 1.15.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 +22 -0
- package/lib/SQLService.js +31 -4
- package/lib/cqn2sql.js +137 -81
- package/lib/cqn4sql.js +23 -8
- package/lib/infer/index.js +55 -38
- package/lib/infer/join-tree.js +1 -0
- package/lib/search.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@
|
|
|
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.15.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.14.1...db-service-v1.15.0) (2024-11-14)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* enable path expressions in infix filter after `exists` predicate ([#875](https://github.com/cap-js/cds-dbs/issues/875)) ([7e50359](https://github.com/cap-js/cds-dbs/commit/7e5035932ac3bf39f052aa67e1565567e9d6b1ad))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
* **`search`:** ignore invalid path expressions inside `@cds.search` ([#849](https://github.com/cap-js/cds-dbs/issues/849)) ([250edd5](https://github.com/cap-js/cds-dbs/commit/250edd5ec9f7ba1d8e40e1330e4b4f9ad9e599b0))
|
|
18
|
+
* nested exists wrapped in xpr ([7e50359](https://github.com/cap-js/cds-dbs/commit/7e5035932ac3bf39f052aa67e1565567e9d6b1ad))
|
|
19
|
+
|
|
20
|
+
## [1.14.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.14.0...db-service-v1.14.1) (2024-10-28)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
* deep delete for views ([#496](https://github.com/cap-js/cds-dbs/issues/496)) ([82154ef](https://github.com/cap-js/cds-dbs/commit/82154ef8b837f17e81e2516056e03ff215f1dff8))
|
|
26
|
+
* properly support `default`, `cds.on.insert` and `cds.on.update` for `UPSERT` queries ([#425](https://github.com/cap-js/cds-dbs/issues/425)) ([338e9f5](https://github.com/cap-js/cds-dbs/commit/338e9f5de9109d36013208547fc648c17ce8c7b0))
|
|
27
|
+
* SELECT cds.hana.BINARY ([#870](https://github.com/cap-js/cds-dbs/issues/870)) ([33c3ebe](https://github.com/cap-js/cds-dbs/commit/33c3ebe84be4c0181b1c230d5f2d332332201ce0))
|
|
28
|
+
|
|
7
29
|
## [1.14.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.13.0...db-service-v1.14.0) (2024-10-15)
|
|
8
30
|
|
|
9
31
|
|
package/lib/SQLService.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const cds = require('@sap/cds'),
|
|
2
2
|
DEBUG = cds.debug('sql|db')
|
|
3
3
|
const { Readable } = require('stream')
|
|
4
|
-
const { resolveView } = require('@sap/cds/libx/_runtime/common/utils/resolveView')
|
|
4
|
+
const { resolveView, getDBTable, getTransition } = require('@sap/cds/libx/_runtime/common/utils/resolveView')
|
|
5
5
|
const DatabaseService = require('./common/DatabaseService')
|
|
6
6
|
const cqn4sql = require('./cqn4sql')
|
|
7
7
|
|
|
@@ -25,14 +25,16 @@ class SQLService extends DatabaseService {
|
|
|
25
25
|
this.on(['INSERT', 'UPSERT', 'UPDATE'], require('./deep-queries').onDeep)
|
|
26
26
|
if (cds.env.features.db_strict) {
|
|
27
27
|
this.before(['INSERT', 'UPSERT', 'UPDATE'], ({ query }) => {
|
|
28
|
-
const elements = query.target?.elements
|
|
28
|
+
const elements = query.target?.elements
|
|
29
|
+
if (!elements) return
|
|
29
30
|
const kind = query.kind || Object.keys(query)[0]
|
|
30
31
|
const operation = query[kind]
|
|
31
32
|
if (!operation.columns && !operation.entries && !operation.data) return
|
|
32
33
|
const columns =
|
|
33
34
|
operation.columns ||
|
|
34
35
|
Object.keys(
|
|
35
|
-
operation.data ||
|
|
36
|
+
operation.data ||
|
|
37
|
+
operation.entries?.reduce((acc, obj) => {
|
|
36
38
|
return Object.assign(acc, obj)
|
|
37
39
|
}, {}),
|
|
38
40
|
)
|
|
@@ -214,7 +216,31 @@ class SQLService extends DatabaseService {
|
|
|
214
216
|
// REVISIT: It's not yet 100 % clear under which circumstances we can rely on db constraints
|
|
215
217
|
return (super.onDELETE = /* cds.env.features.assert_integrity === 'db' ? this.onSIMPLE : */ deep_delete)
|
|
216
218
|
async function deep_delete(/** @type {Request} */ req) {
|
|
217
|
-
|
|
219
|
+
const transitions = getTransition(req.target, this, false, req.query.cmd || 'DELETE')
|
|
220
|
+
if (transitions.target !== transitions.queryTarget) {
|
|
221
|
+
const keys = []
|
|
222
|
+
const transitionsTarget = transitions.queryTarget.keys || transitions.queryTarget.elements
|
|
223
|
+
for (const key in transitionsTarget) {
|
|
224
|
+
const exists = e => e && !e.virtual && !e.value && !e.isAssociation
|
|
225
|
+
if (exists(transitionsTarget[key])) keys.push(key)
|
|
226
|
+
}
|
|
227
|
+
const matchedKeys = keys.filter(key => transitions.mapping.has(key)).map(k => ({ ref: [k] }))
|
|
228
|
+
const query = DELETE.from({
|
|
229
|
+
ref: [
|
|
230
|
+
{
|
|
231
|
+
id: transitions.target.name,
|
|
232
|
+
where: [
|
|
233
|
+
{ list: matchedKeys.map(k => transitions.mapping.get(k.ref[0])) },
|
|
234
|
+
'in',
|
|
235
|
+
SELECT.from(req.query.DELETE.from).columns(matchedKeys).where(req.query.DELETE.where),
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
})
|
|
240
|
+
return this.onDELETE({ query, target: transitions.target })
|
|
241
|
+
}
|
|
242
|
+
const table = getDBTable(req.target)
|
|
243
|
+
const { compositions } = table
|
|
218
244
|
if (compositions) {
|
|
219
245
|
// Transform CQL`DELETE from Foo[p1] WHERE p2` into CQL`DELETE from Foo[p1 and p2]`
|
|
220
246
|
let { from, where } = req.query.DELETE
|
|
@@ -241,6 +267,7 @@ class SQLService extends DatabaseService {
|
|
|
241
267
|
)
|
|
242
268
|
// Prepare and run deep query, à la CQL`DELETE from Foo[pred]:comp1.comp2...`
|
|
243
269
|
const query = DELETE.from({ ref: [...from.ref, c.name] })
|
|
270
|
+
query.target = c._target
|
|
244
271
|
return this.onDELETE({ query, depth, visited: [...visited], target: c._target })
|
|
245
272
|
}),
|
|
246
273
|
)
|
package/lib/cqn2sql.js
CHANGED
|
@@ -4,12 +4,6 @@ const cqn4sql = require('./cqn4sql')
|
|
|
4
4
|
const _simple_queries = cds.env.features.sql_simple_queries
|
|
5
5
|
const _strict_booleans = _simple_queries < 2
|
|
6
6
|
|
|
7
|
-
const BINARY_TYPES = {
|
|
8
|
-
'cds.Binary': 1,
|
|
9
|
-
'cds.LargeBinary': 1,
|
|
10
|
-
'cds.hana.BINARY': 1,
|
|
11
|
-
}
|
|
12
|
-
|
|
13
7
|
const { Readable } = require('stream')
|
|
14
8
|
|
|
15
9
|
const DEBUG = (() => {
|
|
@@ -42,6 +36,12 @@ class CQN2SQLRenderer {
|
|
|
42
36
|
}
|
|
43
37
|
}
|
|
44
38
|
|
|
39
|
+
BINARY_TYPES = {
|
|
40
|
+
'cds.Binary': 1,
|
|
41
|
+
'cds.LargeBinary': 1,
|
|
42
|
+
'cds.hana.BINARY': 1,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
45
|
static _add_mixins(aspect, mixins) {
|
|
46
46
|
const fqn = this.name + aspect
|
|
47
47
|
const types = cds.builtin.types
|
|
@@ -142,13 +142,15 @@ class CQN2SQLRenderer {
|
|
|
142
142
|
*/
|
|
143
143
|
CREATE_elements(elements) {
|
|
144
144
|
let sql = ''
|
|
145
|
+
let keys = ''
|
|
145
146
|
for (let e in elements) {
|
|
146
147
|
const definition = elements[e]
|
|
147
148
|
if (definition.isAssociation) continue
|
|
149
|
+
if (definition.key) keys = `${keys}, ${this.quote(definition.name)}`
|
|
148
150
|
const s = this.CREATE_element(definition)
|
|
149
|
-
if (s) sql +=
|
|
151
|
+
if (s) sql += `, ${s}`
|
|
150
152
|
}
|
|
151
|
-
return sql.slice(
|
|
153
|
+
return `${sql.slice(2)}${keys && `, PRIMARY KEY(${keys.slice(2)})`}`
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
/**
|
|
@@ -491,8 +493,6 @@ class CQN2SQLRenderer {
|
|
|
491
493
|
*/
|
|
492
494
|
INSERT_entries(q) {
|
|
493
495
|
const { INSERT } = q
|
|
494
|
-
const entity = this.name(q.target?.name || INSERT.into.ref[0])
|
|
495
|
-
const alias = INSERT.into.as
|
|
496
496
|
const elements = q.elements || q.target?.elements
|
|
497
497
|
if (!elements && !INSERT.entries?.length) {
|
|
498
498
|
return // REVISIT: mtx sends an insert statement without entries and no reference entity
|
|
@@ -504,19 +504,14 @@ class CQN2SQLRenderer {
|
|
|
504
504
|
/** @type {string[]} */
|
|
505
505
|
this.columns = columns
|
|
506
506
|
|
|
507
|
+
const alias = INSERT.into.as
|
|
508
|
+
const entity = this.name(q.target?.name || INSERT.into.ref[0])
|
|
507
509
|
if (!elements) {
|
|
508
510
|
this.entries = INSERT.entries.map(e => columns.map(c => e[c]))
|
|
509
511
|
const param = this.param.bind(this, { ref: ['?'] })
|
|
510
512
|
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))}) VALUES (${columns.map(param)})`)
|
|
511
513
|
}
|
|
512
514
|
|
|
513
|
-
const extractions = this.managed(
|
|
514
|
-
columns.map(c => ({ name: c })),
|
|
515
|
-
elements,
|
|
516
|
-
!!q.UPSERT,
|
|
517
|
-
)
|
|
518
|
-
const extraction = extractions.map(c => c.sql)
|
|
519
|
-
|
|
520
515
|
// Include this.values for placeholders
|
|
521
516
|
/** @type {unknown[][]} */
|
|
522
517
|
this.entries = []
|
|
@@ -530,8 +525,9 @@ class CQN2SQLRenderer {
|
|
|
530
525
|
this.entries = [[...this.values, stream]]
|
|
531
526
|
}
|
|
532
527
|
|
|
528
|
+
const extractions = this._managed = this.managed(columns.map(c => ({ name: c })), elements)
|
|
533
529
|
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))
|
|
534
|
-
}) SELECT ${
|
|
530
|
+
}) SELECT ${extractions.map(c => c.insert)} FROM json_each(?)`)
|
|
535
531
|
}
|
|
536
532
|
|
|
537
533
|
async *INSERT_entries_stream(entries, binaryEncoding = 'base64') {
|
|
@@ -569,7 +565,7 @@ class CQN2SQLRenderer {
|
|
|
569
565
|
|
|
570
566
|
buffer += '"'
|
|
571
567
|
} else {
|
|
572
|
-
if (elements[key]?.type in BINARY_TYPES) {
|
|
568
|
+
if (elements[key]?.type in this.BINARY_TYPES) {
|
|
573
569
|
val = transformBase64(val)
|
|
574
570
|
}
|
|
575
571
|
buffer += `${keyJSON}${JSON.stringify(val)}`
|
|
@@ -617,7 +613,7 @@ class CQN2SQLRenderer {
|
|
|
617
613
|
|
|
618
614
|
buffer += '"'
|
|
619
615
|
} else {
|
|
620
|
-
if (elements[this.columns[key]]?.type in BINARY_TYPES) {
|
|
616
|
+
if (elements[this.columns[key]]?.type in this.BINARY_TYPES) {
|
|
621
617
|
val = transformBase64(val)
|
|
622
618
|
}
|
|
623
619
|
buffer += `${sepsub}${val === undefined ? 'null' : JSON.stringify(val)}`
|
|
@@ -646,18 +642,7 @@ class CQN2SQLRenderer {
|
|
|
646
642
|
const entity = this.name(q.target?.name || INSERT.into.ref[0])
|
|
647
643
|
const alias = INSERT.into.as
|
|
648
644
|
const elements = q.elements || q.target?.elements
|
|
649
|
-
const columns = INSERT.columns
|
|
650
|
-
|| cds.error`Cannot insert rows without columns or elements`
|
|
651
|
-
|
|
652
|
-
const inputConverter = this.class._convertInput
|
|
653
|
-
const extraction = columns.map((c, i) => {
|
|
654
|
-
const extract = `value->>'$[${i}]'`
|
|
655
|
-
const element = elements?.[c]
|
|
656
|
-
const converter = element?.[inputConverter]
|
|
657
|
-
return converter?.(extract, element) || extract
|
|
658
|
-
})
|
|
659
|
-
|
|
660
|
-
this.columns = columns
|
|
645
|
+
const columns = this.columns = INSERT.columns || cds.error`Cannot insert rows without columns or elements`
|
|
661
646
|
|
|
662
647
|
if (!elements) {
|
|
663
648
|
this.entries = INSERT.rows
|
|
@@ -675,6 +660,10 @@ class CQN2SQLRenderer {
|
|
|
675
660
|
this.entries = [[...this.values, stream]]
|
|
676
661
|
}
|
|
677
662
|
|
|
663
|
+
const extraction = (this._managed = this.managed(columns.map(c => ({ name: c })), elements))
|
|
664
|
+
.slice(0, columns.length)
|
|
665
|
+
.map(c => c.converter(c.extract))
|
|
666
|
+
|
|
678
667
|
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))
|
|
679
668
|
}) SELECT ${extraction} FROM json_each(?)`)
|
|
680
669
|
}
|
|
@@ -686,7 +675,7 @@ class CQN2SQLRenderer {
|
|
|
686
675
|
*/
|
|
687
676
|
INSERT_values(q) {
|
|
688
677
|
let { columns, values } = q.INSERT
|
|
689
|
-
return this.
|
|
678
|
+
return this.render({ __proto__: q, INSERT: { __proto__: q.INSERT, columns, rows: [values] } })
|
|
690
679
|
}
|
|
691
680
|
|
|
692
681
|
/**
|
|
@@ -737,14 +726,37 @@ class CQN2SQLRenderer {
|
|
|
737
726
|
*/
|
|
738
727
|
UPSERT(q) {
|
|
739
728
|
const { UPSERT } = q
|
|
740
|
-
|
|
729
|
+
|
|
741
730
|
let sql = this.INSERT({ __proto__: q, INSERT: UPSERT })
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
731
|
+
if (!q.target?.keys) return sql
|
|
732
|
+
const keys = []
|
|
733
|
+
for (const k of ObjectKeys(q.target?.keys)) {
|
|
734
|
+
const element = q.target.keys[k]
|
|
735
|
+
if (element.isAssociation || element.virtual) continue
|
|
736
|
+
keys.push(k)
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const elements = q.target?.elements || {}
|
|
740
|
+
// temporal data
|
|
741
|
+
for (const k of ObjectKeys(elements)) {
|
|
742
|
+
if (elements[k]['@cds.valid.from']) keys.push(k)
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const keyCompare = keys
|
|
746
|
+
.map(k => `NEW.${this.quote(k)}=OLD.${this.quote(k)}`)
|
|
747
|
+
.join(' AND ')
|
|
748
|
+
|
|
749
|
+
const columns = this.columns // this.columns is computed as part of this.INSERT
|
|
750
|
+
const managed = this._managed.slice(0, columns.length)
|
|
751
|
+
|
|
752
|
+
const extractkeys = managed
|
|
753
|
+
.filter(c => keys.includes(c.name))
|
|
754
|
+
.map(c => `${c.onInsert || c.sql} as ${this.quote(c.name)}`)
|
|
745
755
|
|
|
746
|
-
|
|
747
|
-
|
|
756
|
+
const entity = this.name(q.target?.name || UPSERT.into.ref[0])
|
|
757
|
+
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}`
|
|
758
|
+
|
|
759
|
+
const updateColumns = columns.filter(c => {
|
|
748
760
|
if (keys.includes(c)) return false //> keys go into ON CONFLICT clause
|
|
749
761
|
let e = elements[c]
|
|
750
762
|
if (!e) return true //> pass through to native SQL columns not in CDS model
|
|
@@ -754,14 +766,8 @@ class CQN2SQLRenderer {
|
|
|
754
766
|
else return true
|
|
755
767
|
}).map(c => `${this.quote(c)} = excluded.${this.quote(c)}`)
|
|
756
768
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
keys = keys.map(k => this.quote(k))
|
|
761
|
-
const conflict = updateColumns.length
|
|
762
|
-
? `ON CONFLICT(${keys}) DO UPDATE SET ` + updateColumns
|
|
763
|
-
: `ON CONFLICT(${keys}) DO NOTHING`
|
|
764
|
-
return (this.sql = `${sql} WHERE true ${conflict}`)
|
|
769
|
+
return (this.sql = `INSERT INTO ${this.quote(entity)} (${columns.map(c => this.quote(c))}) ${sql
|
|
770
|
+
} WHERE TRUE ON CONFLICT(${keys.map(c => this.quote(c))}) DO ${updateColumns.length ? `UPDATE SET ${updateColumns}` : 'NOTHING'}`)
|
|
765
771
|
}
|
|
766
772
|
|
|
767
773
|
// UPDATE Statements ------------------------------------------------
|
|
@@ -790,7 +796,9 @@ class CQN2SQLRenderer {
|
|
|
790
796
|
}
|
|
791
797
|
}
|
|
792
798
|
|
|
793
|
-
const extraction = this.managed(columns, elements
|
|
799
|
+
const extraction = this.managed(columns, elements)
|
|
800
|
+
.filter((c, i) => columns[i] || c.onUpdate)
|
|
801
|
+
.map((c, i) => `${this.quote(c.name)}=${!columns[i] ? c.onUpdate : c.sql}`)
|
|
794
802
|
|
|
795
803
|
sql += ` SET ${extraction}`
|
|
796
804
|
if (where) sql += ` WHERE ${this.where(where)}`
|
|
@@ -1042,56 +1050,104 @@ class CQN2SQLRenderer {
|
|
|
1042
1050
|
}
|
|
1043
1051
|
|
|
1044
1052
|
/**
|
|
1045
|
-
*
|
|
1053
|
+
* Converts the columns array into an array of SQL expressions that extract the correct value from inserted JSON data
|
|
1046
1054
|
* @param {object[]} columns
|
|
1047
1055
|
* @param {import('./infer/cqn').elements} elements
|
|
1048
1056
|
* @param {Boolean} isUpdate
|
|
1049
1057
|
* @returns {string[]} Array of SQL expressions for processing input JSON data
|
|
1050
1058
|
*/
|
|
1051
|
-
managed(columns, elements
|
|
1052
|
-
const
|
|
1059
|
+
managed(columns, elements) {
|
|
1060
|
+
const cdsOnInsert = '@cds.on.insert'
|
|
1061
|
+
const cdsOnUpdate = '@cds.on.update'
|
|
1062
|
+
|
|
1053
1063
|
const { _convertInput } = this.class
|
|
1054
1064
|
// Ensure that missing managed columns are added
|
|
1055
1065
|
const requiredColumns = !elements
|
|
1056
1066
|
? []
|
|
1057
|
-
:
|
|
1058
|
-
.filter(
|
|
1059
|
-
e
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1067
|
+
: ObjectKeys(elements)
|
|
1068
|
+
.filter(e => {
|
|
1069
|
+
const element = elements[e]
|
|
1070
|
+
// Actual mandatory check
|
|
1071
|
+
if (!(element.default || element[cdsOnInsert] || element[cdsOnUpdate])) return false
|
|
1072
|
+
// Physical column check
|
|
1073
|
+
if (!element || element.virtual || element.isAssociation) return false
|
|
1074
|
+
// Existence check
|
|
1075
|
+
if (columns.find(c => c.name === e)) return false
|
|
1076
|
+
return true
|
|
1077
|
+
})
|
|
1063
1078
|
.map(name => ({ name, sql: 'NULL' }))
|
|
1064
1079
|
|
|
1080
|
+
const keys = ObjectKeys(elements).filter(e => elements[e].key && !elements[e].isAssociation)
|
|
1081
|
+
const keyZero = keys[0] && this.quote(keys[0])
|
|
1082
|
+
|
|
1065
1083
|
return [...columns, ...requiredColumns].map(({ name, sql }) => {
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
let
|
|
1070
|
-
if (
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
else if (!isUpdate && element.default) {
|
|
1075
|
-
const d = element.default
|
|
1076
|
-
if (d.val !== undefined || d.ref?.[0] === '$now') {
|
|
1077
|
-
// REVISIT: d.ref is not used afterwards
|
|
1078
|
-
sql = `(CASE WHEN json_type(value,'$."${name}"') IS NULL THEN ${this.defaultValue(d.val) // REVISIT: this.defaultValue is a strange function
|
|
1079
|
-
} ELSE ${sql} END)`
|
|
1080
|
-
}
|
|
1084
|
+
const element = elements?.[name] || {}
|
|
1085
|
+
|
|
1086
|
+
const converter = a => element[_convertInput]?.(a, element) || a
|
|
1087
|
+
let extract
|
|
1088
|
+
if (!sql) {
|
|
1089
|
+
({ sql, extract } = this.managed_extract(name, element, converter))
|
|
1090
|
+
} else {
|
|
1091
|
+
extract = sql = converter(sql)
|
|
1081
1092
|
}
|
|
1093
|
+
// if (sql[0] !== '$') sql = converter(sql, element)
|
|
1094
|
+
|
|
1095
|
+
let onInsert = this.managed_session_context(element[cdsOnInsert]?.['='])
|
|
1096
|
+
|| this.managed_session_context(element.default?.ref?.[0])
|
|
1097
|
+
|| (element.default?.val !== undefined && { val: element.default.val, param: false })
|
|
1098
|
+
let onUpdate = this.managed_session_context(element[cdsOnUpdate]?.['='])
|
|
1099
|
+
|
|
1100
|
+
if (onInsert) onInsert = this.expr(onInsert)
|
|
1101
|
+
if (onUpdate) onUpdate = this.expr(onUpdate)
|
|
1102
|
+
|
|
1103
|
+
const qname = this.quote(name)
|
|
1104
|
+
|
|
1105
|
+
const insert = onInsert ? this.managed_default(name, converter(onInsert), sql) : sql
|
|
1106
|
+
const update = onUpdate ? this.managed_default(name, converter(onUpdate), sql) : sql
|
|
1107
|
+
const upsert = keyZero && (
|
|
1108
|
+
// upsert requires the keys to be provided for the existance join (default values optional)
|
|
1109
|
+
element.key
|
|
1110
|
+
// If both insert and update have the same managed definition exclude the old value check
|
|
1111
|
+
|| (onInsert && onUpdate && insert === update)
|
|
1112
|
+
? `${insert} as ${qname}`
|
|
1113
|
+
: `CASE WHEN OLD.${keyZero} IS NULL THEN ${
|
|
1114
|
+
// If key of old is null execute insert
|
|
1115
|
+
insert
|
|
1116
|
+
} ELSE ${
|
|
1117
|
+
// Else execute managed update or keep old if no new data if provided
|
|
1118
|
+
onUpdate ? update : this.managed_default(name, `OLD.${qname}`, update)
|
|
1119
|
+
} END as ${qname}`
|
|
1120
|
+
)
|
|
1082
1121
|
|
|
1083
|
-
return {
|
|
1122
|
+
return {
|
|
1123
|
+
name, // Element name
|
|
1124
|
+
sql, // Reference SQL
|
|
1125
|
+
extract, // Source SQL
|
|
1126
|
+
converter, // Converter logic
|
|
1127
|
+
// action specific full logic
|
|
1128
|
+
insert, update, upsert,
|
|
1129
|
+
// action specific isolated logic
|
|
1130
|
+
onInsert, onUpdate
|
|
1131
|
+
}
|
|
1084
1132
|
})
|
|
1085
1133
|
}
|
|
1086
1134
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1135
|
+
managed_extract(name, element, converter) {
|
|
1136
|
+
const { UPSERT, INSERT } = this.cqn
|
|
1137
|
+
const extract = !(INSERT?.entries || UPSERT?.entries) && (INSERT?.rows || UPSERT?.rows)
|
|
1138
|
+
? `value->>'$[${this.columns.indexOf(name)}]'`
|
|
1139
|
+
: `value->>'$."${name.replace(/"/g, '""')}"'`
|
|
1140
|
+
const sql = converter?.(extract) || extract
|
|
1141
|
+
return { extract, sql }
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
managed_session_context(src) {
|
|
1145
|
+
const val = _managed[src]
|
|
1146
|
+
return val && { func: 'session_context', args: [{ val, param: false }] }
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
managed_default(name, managed, src) {
|
|
1150
|
+
return `(CASE WHEN json_type(value,${this.managed_extract(name).extract.slice(8)}) IS NULL THEN ${managed} ELSE ${src} END)`
|
|
1095
1151
|
}
|
|
1096
1152
|
}
|
|
1097
1153
|
|
package/lib/cqn4sql.js
CHANGED
|
@@ -266,10 +266,10 @@ function cqn4sql(originalQuery, model) {
|
|
|
266
266
|
const id = localized(r.queryArtifact)
|
|
267
267
|
args.push({ ref: [r.args ? { id, args: r.args } : id], as: r.alias })
|
|
268
268
|
}
|
|
269
|
-
from = { join: 'left', args, on: [] }
|
|
269
|
+
from = { join: r.join || 'left', args, on: [] }
|
|
270
270
|
r.children.forEach(c => {
|
|
271
271
|
from = joinForBranch(from, c)
|
|
272
|
-
from = { join: 'left', args: [from], on: [] }
|
|
272
|
+
from = { join: c.join || 'left', args: [from], on: [] }
|
|
273
273
|
})
|
|
274
274
|
})
|
|
275
275
|
return from.args.length > 1 ? from : from.args[0]
|
|
@@ -309,7 +309,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
309
309
|
}
|
|
310
310
|
if (node.children) {
|
|
311
311
|
node.children.forEach(c => {
|
|
312
|
-
lhs = { join: 'left', args: [lhs], on: [] }
|
|
312
|
+
lhs = { join: c.join || 'left', args: [lhs], on: [] }
|
|
313
313
|
lhs = joinForBranch(lhs, c)
|
|
314
314
|
})
|
|
315
315
|
}
|
|
@@ -2093,11 +2093,6 @@ function cqn4sql(originalQuery, model) {
|
|
|
2093
2093
|
const unmanagedOn = onCondFor(inWhere ? next : current, inWhere ? current : next, inWhere)
|
|
2094
2094
|
on.push(...(customWhere && hasLogicalOr(unmanagedOn) ? [asXpr(unmanagedOn)] : unmanagedOn))
|
|
2095
2095
|
}
|
|
2096
|
-
// infix filter conditions are wrapped in `xpr` when added to the on-condition
|
|
2097
|
-
if (customWhere) {
|
|
2098
|
-
const filter = getTransformedTokenStream(customWhere, next)
|
|
2099
|
-
on.push(...['and', ...(hasLogicalOr(filter) ? [asXpr(filter)] : filter)])
|
|
2100
|
-
}
|
|
2101
2096
|
|
|
2102
2097
|
const subquerySource = assocTarget(nextDefinition) || nextDefinition
|
|
2103
2098
|
const id = localized(subquerySource)
|
|
@@ -2115,6 +2110,26 @@ function cqn4sql(originalQuery, model) {
|
|
|
2115
2110
|
],
|
|
2116
2111
|
where: on,
|
|
2117
2112
|
}
|
|
2113
|
+
if (next.pathExpressionInsideFilter) {
|
|
2114
|
+
SELECT.where = customWhere
|
|
2115
|
+
const transformedExists = transformSubquery({ SELECT })
|
|
2116
|
+
// infix filter conditions are wrapped in `xpr` when added to the on-condition
|
|
2117
|
+
if (transformedExists.SELECT.where) {
|
|
2118
|
+
on.push(
|
|
2119
|
+
...[
|
|
2120
|
+
'and',
|
|
2121
|
+
...(hasLogicalOr(transformedExists.SELECT.where)
|
|
2122
|
+
? [asXpr(transformedExists.SELECT.where)]
|
|
2123
|
+
: transformedExists.SELECT.where),
|
|
2124
|
+
],
|
|
2125
|
+
)
|
|
2126
|
+
}
|
|
2127
|
+
transformedExists.SELECT.where = on
|
|
2128
|
+
return transformedExists.SELECT
|
|
2129
|
+
} else if (customWhere) {
|
|
2130
|
+
const filter = getTransformedTokenStream(customWhere, next)
|
|
2131
|
+
on.push(...['and', ...(hasLogicalOr(filter) ? [asXpr(filter)] : filter)])
|
|
2132
|
+
}
|
|
2118
2133
|
return SELECT
|
|
2119
2134
|
}
|
|
2120
2135
|
|
package/lib/infer/index.js
CHANGED
|
@@ -184,16 +184,13 @@ function infer(originalQuery, model) {
|
|
|
184
184
|
if (e.target) {
|
|
185
185
|
// only fk access in infix filter
|
|
186
186
|
const nextStep = ref[1]?.id || ref[1]
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
if (isNonForeignKeyNavigation(e, nextStep)) {
|
|
188
|
+
if (expandOrExists) {
|
|
189
|
+
Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
|
|
190
|
+
} else {
|
|
191
|
+
rejectNonFkNavigation(e, e.on ? $baseLink.definition.name : nextStep)
|
|
192
|
+
}
|
|
191
193
|
}
|
|
192
|
-
// no non-fk traversal in infix filter
|
|
193
|
-
if (!expandOrExists && nextStep && !isForeignKeyOf(nextStep, e))
|
|
194
|
-
throw new Error(
|
|
195
|
-
`Only foreign keys of “${e.name}” can be accessed in infix filter, but found “${nextStep}”`,
|
|
196
|
-
)
|
|
197
194
|
}
|
|
198
195
|
arg.$refLinks.push({ definition: e, target: definition })
|
|
199
196
|
// filter paths are flattened
|
|
@@ -226,7 +223,7 @@ function infer(originalQuery, model) {
|
|
|
226
223
|
// don't miss an exists within an expression
|
|
227
224
|
token.xpr.forEach(walkTokenStream)
|
|
228
225
|
} else {
|
|
229
|
-
attachRefLinksToArg(token, arg.$refLinks[i], existsPredicate)
|
|
226
|
+
attachRefLinksToArg(token, arg.$refLinks[i], existsPredicate || expandOrExists)
|
|
230
227
|
existsPredicate = false
|
|
231
228
|
}
|
|
232
229
|
}
|
|
@@ -235,6 +232,7 @@ function infer(originalQuery, model) {
|
|
|
235
232
|
}
|
|
236
233
|
i += 1
|
|
237
234
|
}
|
|
235
|
+
if ($baseLink?.pathExpressionInsideFilter) Object.defineProperty(arg, 'join', { value: 'inner' })
|
|
238
236
|
const { definition, target } = arg.$refLinks[arg.$refLinks.length - 1]
|
|
239
237
|
if (definition.value) {
|
|
240
238
|
// nested calculated element
|
|
@@ -542,9 +540,19 @@ function infer(originalQuery, model) {
|
|
|
542
540
|
const elements = getDefinition(definition.target)?.elements || definition.elements
|
|
543
541
|
if (elements && id in elements) {
|
|
544
542
|
const element = elements[id]
|
|
545
|
-
|
|
543
|
+
if (inInfixFilter) {
|
|
544
|
+
const nextStep = column.ref[1]?.id || column.ref[1]
|
|
545
|
+
if (isNonForeignKeyNavigation(element, nextStep)) {
|
|
546
|
+
if (inExists) {
|
|
547
|
+
Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
|
|
548
|
+
} else {
|
|
549
|
+
rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
546
553
|
const resolvableIn = getDefinition(definition.target) || target
|
|
547
|
-
|
|
554
|
+
const $refLink = { definition: elements[id], target: resolvableIn }
|
|
555
|
+
column.$refLinks.push($refLink)
|
|
548
556
|
} else {
|
|
549
557
|
stepNotFoundInPredecessor(id, definition.name)
|
|
550
558
|
}
|
|
@@ -593,7 +601,16 @@ function infer(originalQuery, model) {
|
|
|
593
601
|
|
|
594
602
|
const target = getDefinition(definition.target) || column.$refLinks[i - 1].target
|
|
595
603
|
if (element) {
|
|
596
|
-
if ($baseLink)
|
|
604
|
+
if ($baseLink && inInfixFilter) {
|
|
605
|
+
const nextStep = column.ref[i + 1]?.id || column.ref[i + 1]
|
|
606
|
+
if (isNonForeignKeyNavigation(element, nextStep)) {
|
|
607
|
+
if (inExists) {
|
|
608
|
+
Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
|
|
609
|
+
} else {
|
|
610
|
+
rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
597
614
|
const $refLink = { definition: elements[id], target }
|
|
598
615
|
column.$refLinks.push($refLink)
|
|
599
616
|
} else if (firstStepIsSelf) {
|
|
@@ -637,7 +654,7 @@ function infer(originalQuery, model) {
|
|
|
637
654
|
skipJoinsForFilter = true
|
|
638
655
|
} else if (token.ref || token.xpr) {
|
|
639
656
|
inferQueryElement(token, false, column.$refLinks[i], {
|
|
640
|
-
inExists: skipJoinsForFilter,
|
|
657
|
+
inExists: skipJoinsForFilter || inExists,
|
|
641
658
|
inExpr: !!token.xpr,
|
|
642
659
|
inInfixFilter: true,
|
|
643
660
|
})
|
|
@@ -646,7 +663,7 @@ function infer(originalQuery, model) {
|
|
|
646
663
|
applyToFunctionArgs(token.args, inferQueryElement, [
|
|
647
664
|
false,
|
|
648
665
|
column.$refLinks[i],
|
|
649
|
-
{ inExists: skipJoinsForFilter, inExpr: true, inInfixFilter: true },
|
|
666
|
+
{ inExists: skipJoinsForFilter || inExists, inExpr: true, inInfixFilter: true },
|
|
650
667
|
])
|
|
651
668
|
}
|
|
652
669
|
}
|
|
@@ -700,31 +717,11 @@ function infer(originalQuery, model) {
|
|
|
700
717
|
}
|
|
701
718
|
}
|
|
702
719
|
}
|
|
703
|
-
|
|
704
|
-
/**
|
|
705
|
-
* Check if the next step in the ref is foreign key of `assoc`
|
|
706
|
-
* if not, an error is thrown.
|
|
707
|
-
*
|
|
708
|
-
* @param {CSN.Element} assoc if this is an association, the next step must be a foreign key of the element.
|
|
709
|
-
*/
|
|
710
|
-
function rejectNonFkAccess(assoc) {
|
|
711
|
-
if (inInfixFilter && assoc.target) {
|
|
712
|
-
// only fk access in infix filter
|
|
713
|
-
const nextStep = column.ref[i + 1]?.id || column.ref[i + 1]
|
|
714
|
-
// no unmanaged assoc in infix filter path
|
|
715
|
-
if (!inExists && assoc.on) {
|
|
716
|
-
const err = `Unexpected unmanaged association “${assoc.name}” in filter expression of “${$baseLink.definition.name}”`
|
|
717
|
-
throw new Error(err)
|
|
718
|
-
}
|
|
719
|
-
// no non-fk traversal in infix filter in non-exists path
|
|
720
|
-
if (nextStep && !assoc.on && !isForeignKeyOf(nextStep, assoc))
|
|
721
|
-
throw new Error(
|
|
722
|
-
`Only foreign keys of “${assoc.name}” can be accessed in infix filter, but found “${nextStep}”`,
|
|
723
|
-
)
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
720
|
})
|
|
727
721
|
|
|
722
|
+
// we need inner joins for the path expressions inside filter expressions after exists predicate
|
|
723
|
+
if ($baseLink?.pathExpressionInsideFilter) Object.defineProperty(column, 'join', { value: 'inner' })
|
|
724
|
+
|
|
728
725
|
// ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
|
|
729
726
|
if (column.expand) {
|
|
730
727
|
const { $refLinks } = column
|
|
@@ -1214,6 +1211,26 @@ function infer(originalQuery, model) {
|
|
|
1214
1211
|
}
|
|
1215
1212
|
}
|
|
1216
1213
|
|
|
1214
|
+
/**
|
|
1215
|
+
* Determines if a given association is a non-foreign key navigation.
|
|
1216
|
+
*
|
|
1217
|
+
* @param {Object} assoc - The association.
|
|
1218
|
+
* @param {Object} nextStep - The next step in the navigation path.
|
|
1219
|
+
* @returns {boolean} - Returns true if the next step is a non-foreign key navigation, otherwise false.
|
|
1220
|
+
*/
|
|
1221
|
+
function isNonForeignKeyNavigation(assoc, nextStep) {
|
|
1222
|
+
if (!nextStep || !assoc.target) return false
|
|
1223
|
+
|
|
1224
|
+
return assoc.on || !isForeignKeyOf(nextStep, assoc)
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function rejectNonFkNavigation(assoc, additionalInfo) {
|
|
1228
|
+
if (assoc.on) {
|
|
1229
|
+
throw new Error(`Unexpected unmanaged association “${assoc.name}” in filter expression of “${additionalInfo}”`)
|
|
1230
|
+
}
|
|
1231
|
+
throw new Error(`Only foreign keys of “${assoc.name}” can be accessed in infix filter, but found “${additionalInfo}”`)
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1217
1234
|
/**
|
|
1218
1235
|
* Returns true if e is a foreign key of assoc.
|
|
1219
1236
|
* this function is also compatible with unfolded csn (UCSN),
|
package/lib/infer/join-tree.js
CHANGED
|
@@ -181,6 +181,7 @@ class JoinTree {
|
|
|
181
181
|
// if no root node was found, the column is selected from a subquery
|
|
182
182
|
if (!node) return
|
|
183
183
|
while (i < col.ref.length) {
|
|
184
|
+
if(col.join === 'inner') node.join = 'inner'
|
|
184
185
|
const step = col.ref[i]
|
|
185
186
|
const { where, args } = step
|
|
186
187
|
const id = joinId(step, args, where)
|
package/lib/search.js
CHANGED
|
@@ -114,7 +114,7 @@ const _getSearchableColumns = entity => {
|
|
|
114
114
|
deepSearchCandidates.forEach(c => {
|
|
115
115
|
const element = c.ref.reduce((resolveIn, curr, i) => {
|
|
116
116
|
const next = resolveIn.elements?.[curr] || resolveIn._target.elements[curr]
|
|
117
|
-
if (next
|
|
117
|
+
if (next?.isAssociation && !c.ref[i + 1]) {
|
|
118
118
|
const searchInTarget = _getSearchableColumns(next._target)
|
|
119
119
|
searchInTarget.forEach(elementRefInTarget => {
|
|
120
120
|
searchableColumns.push({ ref: c.ref.concat(...elementRefInTarget.ref) })
|
package/package.json
CHANGED