@cap-js/db-service 2.10.0 → 3.0.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 +39 -0
- package/index.js +1 -0
- package/lib/InsertResults.js +74 -68
- package/lib/SQLService.js +17 -16
- package/lib/common/DatabaseService.js +9 -1
- package/lib/common/generic-pool.js +9 -1
- package/lib/cqn4sql.js +11 -0
- package/lib/infer/index.js +48 -14
- package/package.json +7 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,45 @@
|
|
|
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
|
+
## [3.0.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.11.0...db-service-v3.0.0) (2026-06-01)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### ⚠ BREAKING CHANGES
|
|
11
|
+
|
|
12
|
+
* require cds10 ([#1628](https://github.com/cap-js/cds-dbs/issues/1628))
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
* own flag for db service results ([#1630](https://github.com/cap-js/cds-dbs/issues/1630)) ([3946e59](https://github.com/cap-js/cds-dbs/commit/3946e59b19ca11afda999d44babeeff835c23817))
|
|
17
|
+
* **pool:** make builtin pool the default ([#1537](https://github.com/cap-js/cds-dbs/issues/1537)) ([919dfe6](https://github.com/cap-js/cds-dbs/commit/919dfe6c5d0cf0ee37a695d2edcc870c1353a93d))
|
|
18
|
+
* require cds10 ([#1628](https://github.com/cap-js/cds-dbs/issues/1628)) ([93e8305](https://github.com/cap-js/cds-dbs/commit/93e83053679f9ef94293caa08917855404db880d))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
* **builtin-pool:** ignore released/destroyed resource if already draining ([#1631](https://github.com/cap-js/cds-dbs/issues/1631)) ([de37489](https://github.com/cap-js/cds-dbs/commit/de37489bb80b25be1d292a284119c1c0f6405970))
|
|
24
|
+
* calc element with exists through association ([#1608](https://github.com/cap-js/cds-dbs/issues/1608)) ([0d24b28](https://github.com/cap-js/cds-dbs/commit/0d24b285ae8eb71eeca0c6f4bf811a729a8cff4b))
|
|
25
|
+
* InsertResult for keyless entities ([#1617](https://github.com/cap-js/cds-dbs/issues/1617)) ([2efe922](https://github.com/cap-js/cds-dbs/commit/2efe922e2eb4b0c750eb09a74dfd155e8febc9bc))
|
|
26
|
+
* InsertResult is broken if entity has assoc keys ([#1616](https://github.com/cap-js/cds-dbs/issues/1616)) ([9925d22](https://github.com/cap-js/cds-dbs/commit/9925d22236846081e1013381f79249ab0f8f9447))
|
|
27
|
+
* query results fixed gaps ([#1625](https://github.com/cap-js/cds-dbs/issues/1625)) ([5d60e7f](https://github.com/cap-js/cds-dbs/commit/5d60e7f15b4f0a9e7859b0662e690a1790b350ad))
|
|
28
|
+
* resolve `$self` in infix filters with path expressions and nested exists ([#1604](https://github.com/cap-js/cds-dbs/issues/1604)) ([baa3528](https://github.com/cap-js/cds-dbs/commit/baa3528aa735cc3707c4c2a128cf6b57feaf913b))
|
|
29
|
+
* resolve nested assoc-into-assoc inline path resolution ([#1602](https://github.com/cap-js/cds-dbs/issues/1602)) ([ec6fba0](https://github.com/cap-js/cds-dbs/commit/ec6fba0597c75bb206c859bea4e50ce61be40b40))
|
|
30
|
+
* we can't return keys for `INSERT.from(SELECT.from(...))` ([#1622](https://github.com/cap-js/cds-dbs/issues/1622)) ([ccc9f88](https://github.com/cap-js/cds-dbs/commit/ccc9f881adc62cbc16f1feef13da7c658b27d551))
|
|
31
|
+
|
|
32
|
+
## [2.11.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.10.1...db-service-v2.11.0) (2026-04-29)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
* supersede potentially compromised release ([#1590](https://github.com/cap-js/cds-dbs/issues/1590)) ([3be4044](https://github.com/cap-js/cds-dbs/commit/3be404417229a2dd539e4b40393d3bd346e4388c))
|
|
38
|
+
|
|
39
|
+
## [2.10.1](https://github.com/cap-js/cds-dbs/compare/db-service-v2.10.0...db-service-v2.10.1) (2026-04-29)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
* supersede potentially compromised release ([#1589](https://github.com/cap-js/cds-dbs/issues/1589)) ([bd73895](https://github.com/cap-js/cds-dbs/commit/bd7389524d00ddd6ed73fc79308e19e7bf952b53))
|
|
45
|
+
|
|
7
46
|
## [2.10.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.9.0...db-service-v2.10.0) (2026-04-22)
|
|
8
47
|
|
|
9
48
|
|
package/index.js
CHANGED
package/lib/InsertResults.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const iterator = Symbol.iterator
|
|
2
1
|
|
|
3
2
|
// eslint-disable-next-line no-unused-vars
|
|
4
3
|
const USAGE_SAMPLE = async () => {
|
|
@@ -11,106 +10,113 @@ const USAGE_SAMPLE = async () => {
|
|
|
11
10
|
])
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
module.exports = class InsertResult {
|
|
13
|
+
module.exports = class InsertResult extends Array {
|
|
14
|
+
|
|
15
15
|
/**
|
|
16
16
|
* @param {import('@sap/cds/apis/cqn').INSERT} query
|
|
17
17
|
* @param {unknown[]} results
|
|
18
18
|
*/
|
|
19
|
-
constructor(query, results) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
constructor (query, results) {
|
|
20
|
+
Object.defineProperties (super(), { // using non-enumerable properties to avoid polluting trace output
|
|
21
|
+
results: { value: results },
|
|
22
|
+
query: { value: query },
|
|
23
|
+
})
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
26
|
+
/** Legacy alias for compatibility */
|
|
27
|
+
get affectedRows() { return this.affected }
|
|
28
|
+
|
|
29
|
+
/** The number of affected rows is determined as follows:
|
|
30
|
+
* - For INSERTs with entries/rows/values the number of these entries/rows/values is returned
|
|
31
|
+
* - For other INSERTs the number of affected rows as returned by the database is returned
|
|
32
|
+
* - For other statements the number of affected rows as returned by the database is returned
|
|
27
33
|
*/
|
|
28
|
-
get
|
|
29
|
-
// For INSERT.from(SELECT.from(...)) return a dummy iterator with correct length
|
|
34
|
+
get affected() {
|
|
30
35
|
const { INSERT } = this.query
|
|
31
|
-
if (INSERT.from || INSERT.as)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
if (INSERT.from || INSERT.as) return super.affected = this.affectedRows4 (this.results[0] || this.results)
|
|
37
|
+
else return super.affected = INSERT.entries?.length || INSERT.rows?.length || this.results.length || 1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
[Symbol.iterator]() {
|
|
41
|
+
if (!this.length) this.#materialize() // materialize on first access, e.g. for [...results]
|
|
42
|
+
return super[Symbol.iterator]()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
toJSON(){
|
|
46
|
+
if (!this.length) this.#materialize() // ensure materialized keys for JSON.stringify
|
|
47
|
+
return this
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Lazy materialization of auto-generated keys.
|
|
52
|
+
*/
|
|
53
|
+
#materialize() {
|
|
36
54
|
|
|
37
55
|
const target = this.query._target
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
56
|
+
const keys = target?.keys && Object.keys(target.keys).filter(k => !target.keys[k].virtual && !target.keys[k].value && !target.keys[k].isAssociation)
|
|
57
|
+
if (!keys?.length) {
|
|
58
|
+
return this
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { INSERT } = this.query
|
|
62
|
+
|
|
63
|
+
// For INSERT.from(SELECT.from(...)), we can't return keys
|
|
64
|
+
if (INSERT.from || INSERT.as) return this
|
|
65
|
+
|
|
66
|
+
const k0 = keys[0]
|
|
41
67
|
|
|
42
68
|
// For INSERT.entries() with generated keys in there return these keys
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
yield keys.reduce((p, k) => {
|
|
48
|
-
p[k] = each[k]
|
|
49
|
-
return p
|
|
50
|
-
}, {})
|
|
51
|
-
})
|
|
69
|
+
if (INSERT.entries && k0 in INSERT.entries[0]) {
|
|
70
|
+
for (const d of INSERT.entries) {
|
|
71
|
+
this.push (keys.reduce((p,k) => (p[k] = d[k], p), {}))
|
|
72
|
+
}
|
|
52
73
|
}
|
|
53
74
|
|
|
54
75
|
// For INSERT.rows/values() with generated keys in there return these keys
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
yield keys.reduce((p, k) => {
|
|
65
|
-
p[k] = each[indices[k]]
|
|
66
|
-
return p
|
|
67
|
-
}, {})
|
|
68
|
-
})
|
|
76
|
+
else if (INSERT.columns && INSERT.columns.includes(k0)) {
|
|
77
|
+
const indices = keys.reduce((p, k) => {
|
|
78
|
+
let i = INSERT.columns.indexOf(k)
|
|
79
|
+
if (i >= 0) p[k] = i
|
|
80
|
+
return p
|
|
81
|
+
}, {})
|
|
82
|
+
for (const d of INSERT.rows || [INSERT.values]) {
|
|
83
|
+
this.push (keys.reduce((p,k) => (p[k] = d[indices[k]], p), {}))
|
|
84
|
+
}
|
|
69
85
|
}
|
|
70
86
|
|
|
71
87
|
// If no generated keys in entries/rows/values we might have database-generated keys
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
88
|
+
else for (const row of this.results) {
|
|
89
|
+
const affectedRows = this.affectedRows4(row) - 1
|
|
90
|
+
const lastInsertRowid = this.insertedRowId4(row)
|
|
91
|
+
for (let i = lastInsertRowid - affectedRows; i<=lastInsertRowid;i++) {
|
|
92
|
+
this.push ({ [k0]: i })
|
|
77
93
|
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
94
|
+
}
|
|
80
95
|
|
|
81
|
-
|
|
82
|
-
* the number of inserted (root) entries or the number of affectedRows in case of INSERT into SELECT
|
|
83
|
-
* @return {number}
|
|
84
|
-
*/
|
|
85
|
-
get affectedRows() {
|
|
86
|
-
const { INSERT: _ } = this.query
|
|
87
|
-
if (_.from || _.as) return (super.affectedRows = this.affectedRows4(this.results[0] || this.results))
|
|
88
|
-
else return (super.affectedRows = _.entries?.length || _.rows?.length || this.results.length || 1)
|
|
96
|
+
return this
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
/**
|
|
92
|
-
*
|
|
93
|
-
* @
|
|
100
|
+
* Number of affected rows
|
|
101
|
+
* @param {unknown[]} result
|
|
102
|
+
* @returns {number}
|
|
94
103
|
*/
|
|
95
|
-
|
|
96
|
-
return
|
|
104
|
+
affectedRows4(result) {
|
|
105
|
+
return result.changes
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
/**
|
|
100
|
-
* The last id of
|
|
101
|
-
* @param {unknown[]} result
|
|
102
|
-
* @returns {number}
|
|
109
|
+
* The last id of auto-incremented key columns
|
|
103
110
|
*/
|
|
104
111
|
insertedRowId4(result) {
|
|
105
112
|
return result.lastInsertRowid
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
/**
|
|
109
|
-
*
|
|
110
|
-
* @
|
|
111
|
-
* @returns {number}
|
|
116
|
+
* for checks such as res > 2
|
|
117
|
+
* @return {number}
|
|
112
118
|
*/
|
|
113
|
-
|
|
114
|
-
return
|
|
119
|
+
valueOf() {
|
|
120
|
+
return this.affected
|
|
115
121
|
}
|
|
116
122
|
}
|
package/lib/SQLService.js
CHANGED
|
@@ -18,7 +18,7 @@ const BINARY_TYPES = {
|
|
|
18
18
|
/**
|
|
19
19
|
* Checks if parameter is an object that at least contains one property.
|
|
20
20
|
*
|
|
21
|
-
* @param {*} obj
|
|
21
|
+
* @param {*} obj
|
|
22
22
|
* @returns Boolean
|
|
23
23
|
*/
|
|
24
24
|
const _hasProps = (obj) => {
|
|
@@ -199,10 +199,10 @@ class SQLService extends DatabaseService {
|
|
|
199
199
|
async onUPSERT({ query, data }) {
|
|
200
200
|
const { sql, entries } = this.cqn2sql(query, data)
|
|
201
201
|
if (!sql) return // Do nothing when there is nothing to be done // REVISIT: When does this happen?
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return
|
|
202
|
+
let ps = await this.prepare(sql)
|
|
203
|
+
let results = entries ? await Promise.all(entries.map(e => ps.run(e))) : await ps.run()
|
|
204
|
+
let changes = results.reduce?.((total,r) => total + r.changes, 0) ?? results.changes
|
|
205
|
+
return this._return_affected(changes)
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
/**
|
|
@@ -216,7 +216,7 @@ class SQLService extends DatabaseService {
|
|
|
216
216
|
!_hasProps(req.query.UPDATE.with) &&
|
|
217
217
|
!Object.values(req.target?.elements || {}).some(e => e['@cds.on.update'])
|
|
218
218
|
)
|
|
219
|
-
return 0
|
|
219
|
+
return this._return_affected(0)
|
|
220
220
|
return this.onSIMPLE(req)
|
|
221
221
|
}
|
|
222
222
|
|
|
@@ -227,7 +227,8 @@ class SQLService extends DatabaseService {
|
|
|
227
227
|
async onSIMPLE({ query, data }) {
|
|
228
228
|
const { sql, values } = this.cqn2sql(query, data)
|
|
229
229
|
let ps = await this.prepare(sql)
|
|
230
|
-
|
|
230
|
+
let { changes } = await ps.run(values)
|
|
231
|
+
return this._return_affected(changes)
|
|
231
232
|
}
|
|
232
233
|
|
|
233
234
|
get onDELETE() {
|
|
@@ -300,7 +301,7 @@ class SQLService extends DatabaseService {
|
|
|
300
301
|
* @type {Handler}
|
|
301
302
|
*/
|
|
302
303
|
async onEVENT({ event }) {
|
|
303
|
-
if(DEBUG._debug) DEBUG.debug(event) // in the other cases above DEBUG happens in cqn2sql
|
|
304
|
+
if (DEBUG._debug) DEBUG.debug(event) // in the other cases above DEBUG happens in cqn2sql
|
|
304
305
|
return await this.exec(event)
|
|
305
306
|
}
|
|
306
307
|
|
|
@@ -310,7 +311,7 @@ class SQLService extends DatabaseService {
|
|
|
310
311
|
*/
|
|
311
312
|
async onPlainSQL({ query, data }, next) {
|
|
312
313
|
if (typeof query === 'string') {
|
|
313
|
-
if(DEBUG._debug) DEBUG.debug(query, data)
|
|
314
|
+
if (DEBUG._debug) DEBUG.debug(query, data)
|
|
314
315
|
const ps = await this.prepare(query)
|
|
315
316
|
const exec = this.hasResults(query) ? d => ps.all(d) : d => ps.run(d)
|
|
316
317
|
if (Array.isArray(data) && Array.isArray(data[0])) return await Promise.all(data.map(exec))
|
|
@@ -415,7 +416,7 @@ class SQLService extends DatabaseService {
|
|
|
415
416
|
* @param {import('@sap/cds/apis/cqn').Query} q
|
|
416
417
|
* @returns {import('./infer/cqn').Query}
|
|
417
418
|
*/
|
|
418
|
-
cqn4sql(q, useTechnicalAlias=true) {
|
|
419
|
+
cqn4sql(q, useTechnicalAlias = true) {
|
|
419
420
|
if (
|
|
420
421
|
!cds.env.features.db_strict &&
|
|
421
422
|
!q.SELECT?.from?.join &&
|
|
@@ -517,7 +518,7 @@ const DEBUG_PQL = cds.log('pql')
|
|
|
517
518
|
if (DEBUG_PQL._debug || cds.repl) {
|
|
518
519
|
|
|
519
520
|
// Add helper method to convert CQN to PQL, used below...
|
|
520
|
-
SQLService.prototype.cqn2pql = function cqn2pql
|
|
521
|
+
SQLService.prototype.cqn2pql = function cqn2pql(query, values) {
|
|
521
522
|
const CQN2PQL = cqn2pql.renderer ??= require('./cqn2pql')
|
|
522
523
|
return new CQN2PQL(this).render(query, values)
|
|
523
524
|
}
|
|
@@ -533,7 +534,7 @@ if (DEBUG_PQL._debug || cds.repl) {
|
|
|
533
534
|
}
|
|
534
535
|
}
|
|
535
536
|
|
|
536
|
-
// If running in the REPL, extend cds.ql.Query with helpers to inspect queries.
|
|
537
|
+
// If running in the REPL, extend cds.ql.Query with helpers to inspect queries.
|
|
537
538
|
if (cds.repl) {
|
|
538
539
|
|
|
539
540
|
cds.extend(cds.ql.Query).with(
|
|
@@ -563,12 +564,12 @@ if (DEBUG_PQL._debug || cds.repl) {
|
|
|
563
564
|
}
|
|
564
565
|
)
|
|
565
566
|
|
|
566
|
-
/**
|
|
567
|
-
* Dummy SQL service used in extensions to cds.ql above,
|
|
568
|
-
* if no real SQL service is available yet through cds.db.
|
|
567
|
+
/**
|
|
568
|
+
* Dummy SQL service used in extensions to cds.ql above,
|
|
569
|
+
* if no real SQL service is available yet through cds.db.
|
|
569
570
|
*/
|
|
570
571
|
class db extends SQLService {
|
|
571
|
-
/** @returns {SQLService} */
|
|
572
|
+
/** @returns {SQLService} */
|
|
572
573
|
static get srv() { return cds.db || (this.singleton ??= new this) }
|
|
573
574
|
get factory() { return null }
|
|
574
575
|
get model() { return cds.model }
|
|
@@ -167,5 +167,13 @@ class DatabaseService extends cds.Service {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
const _as_array = changes => Object.assign ([],{ affected: changes })
|
|
171
|
+
const _as_number = changes => changes
|
|
172
|
+
DatabaseService.prototype._return_affected = _as_number
|
|
173
|
+
DatabaseService._always_return_arrays = on_off => {
|
|
174
|
+
if (on_off === undefined) return DatabaseService.prototype._return_affected === _as_array
|
|
175
|
+
DatabaseService.prototype._return_affected = on_off ? _as_array : _as_number
|
|
176
|
+
}
|
|
177
|
+
DatabaseService._always_return_arrays (cds.env.features.legacy_db_results === false) // TODO: invert default value
|
|
170
178
|
DatabaseService.prototype.isDatabaseService = true
|
|
171
|
-
module.exports = DatabaseService
|
|
179
|
+
module.exports = exports = DatabaseService
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const cds = require('@sap/cds')
|
|
2
2
|
const LOG = cds.log('db')
|
|
3
3
|
|
|
4
|
-
const use_new_pool = cds.requires.db?.pool?.builtin || cds.env.features.pool === '
|
|
4
|
+
const use_new_pool = cds.requires.db?.pool?.builtin === false || cds.env.features.pool === 'generic-pool' ? false : true
|
|
5
5
|
const createPool = use_new_pool ? (...args) => new Pool(...args) : require('generic-pool').createPool
|
|
6
6
|
|
|
7
7
|
function ConnectionPool (factory, tenant) {
|
|
@@ -145,6 +145,10 @@ constructor (factory, options = {}) {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
async release(resource) {
|
|
148
|
+
if (this._draining) {
|
|
149
|
+
LOG.debug('Pool is already draining. Resource cannot be returned to pool')
|
|
150
|
+
return
|
|
151
|
+
}
|
|
148
152
|
const loan = this._loans.get(resource)
|
|
149
153
|
if (!loan) throw new Error('Resource not currently part of this pool')
|
|
150
154
|
this._loans.delete(resource)
|
|
@@ -155,6 +159,10 @@ constructor (factory, options = {}) {
|
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
async destroy(resource) {
|
|
162
|
+
if (this._draining) {
|
|
163
|
+
LOG.debug('Pool is already draining. Resource will be destroyed anyway')
|
|
164
|
+
return
|
|
165
|
+
}
|
|
158
166
|
const loan = this._loans.get(resource)
|
|
159
167
|
if (!loan) throw new Error('Resource not currently part of this pool')
|
|
160
168
|
this._loans.delete(resource)
|
package/lib/cqn4sql.js
CHANGED
|
@@ -1823,6 +1823,17 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
|
|
|
1823
1823
|
continue
|
|
1824
1824
|
}
|
|
1825
1825
|
if (token.ref.length > 1 && token.ref[0] === '$self' && !token.$refLinks[0].definition.kind) {
|
|
1826
|
+
if (inferred.outerQueries) {
|
|
1827
|
+
const outerQuery = inferred.outerQueries[0]
|
|
1828
|
+
const stepToFind = token.ref[1]?.id || token.ref[1]
|
|
1829
|
+
const outerAlias = outerQuery.$combinedElements?.[stepToFind]?.[0].index
|
|
1830
|
+
if (outerAlias) {
|
|
1831
|
+
let result = copy(token)
|
|
1832
|
+
result.ref = [outerAlias, token.flatName]
|
|
1833
|
+
transformedTokenStream.push(result)
|
|
1834
|
+
continue
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1826
1837
|
const dollarSelfReplacement = [calculateDollarSelfColumn(token, true)]
|
|
1827
1838
|
transformedTokenStream.push(...getTransformedTokenStream(dollarSelfReplacement))
|
|
1828
1839
|
continue
|
package/lib/infer/index.js
CHANGED
|
@@ -453,7 +453,7 @@ function infer(originalQuery, model, useTechnicalAlias = true) {
|
|
|
453
453
|
arg.$refLinks.push({ definition: pseudos.elements[id], target: pseudos })
|
|
454
454
|
pseudoPath = true // only first path step must be well defined
|
|
455
455
|
nameSegments.push(id)
|
|
456
|
-
} else if ($baseLink) {
|
|
456
|
+
} else if ($baseLink && !firstStepIsSelf) {
|
|
457
457
|
const { definition, target } = $baseLink
|
|
458
458
|
const elements = getDefinition(definition.target)?.elements || definition.elements
|
|
459
459
|
if (elements && id in elements) {
|
|
@@ -484,7 +484,15 @@ function infer(originalQuery, model, useTechnicalAlias = true) {
|
|
|
484
484
|
target: getDefinitionFromSources(sources, id),
|
|
485
485
|
})
|
|
486
486
|
} else if (firstStepIsSelf) {
|
|
487
|
-
|
|
487
|
+
const nextStep = arg.ref[1]?.id || arg.ref[1]
|
|
488
|
+
let elements = queryElements
|
|
489
|
+
if (nextStep && (!queryElements || !(nextStep in queryElements)) && inferred.outerQueries) {
|
|
490
|
+
const outerQuery = inferred.outerQueries[0]
|
|
491
|
+
if (outerQuery?.elements && nextStep in outerQuery.elements) {
|
|
492
|
+
elements = outerQuery.elements
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
arg.$refLinks.push({ definition: { elements }, target: { elements } })
|
|
488
496
|
} else if (arg.ref.length > 1 && inferred.outerQueries?.find(outer => id in outer.sources)) {
|
|
489
497
|
// outer query accessed via alias
|
|
490
498
|
const outerAlias = inferred.outerQueries.find(outer => id in outer.sources)
|
|
@@ -707,7 +715,7 @@ function infer(originalQuery, model, useTechnicalAlias = true) {
|
|
|
707
715
|
* d. Otherwise, the corresponding `$refLinks` definition is added to the `elements` object.
|
|
708
716
|
* 2. Returns the `elements` object.
|
|
709
717
|
*/
|
|
710
|
-
function resolveInline(col, namePrefix = col.as || col.flatName) {
|
|
718
|
+
function resolveInline(col, namePrefix = col.as || col.flatName, outerBase = null) {
|
|
711
719
|
const { inline, $refLinks } = col
|
|
712
720
|
const $leafLink = $refLinks[$refLinks.length - 1]
|
|
713
721
|
if (!$leafLink.definition.target && !$leafLink.definition.elements) {
|
|
@@ -715,10 +723,13 @@ function infer(originalQuery, model, useTechnicalAlias = true) {
|
|
|
715
723
|
`Unexpected “inline” on “${col.ref.map(idOnly)}”; can only be used after a reference to a structure, association or table alias`,
|
|
716
724
|
)
|
|
717
725
|
}
|
|
726
|
+
const effectiveBase = outerBase
|
|
727
|
+
? { ref: [...outerBase.ref, ...col.ref], $refLinks: [...outerBase.$refLinks, ...col.$refLinks] }
|
|
728
|
+
: col
|
|
718
729
|
let elements = {}
|
|
719
730
|
let seenWildcard = false
|
|
720
731
|
inline.forEach(inlineCol => {
|
|
721
|
-
inferArg(inlineCol, null, $leafLink, { inXpr: true, baseColumn:
|
|
732
|
+
inferArg(inlineCol, null, $leafLink, { inXpr: true, baseColumn: effectiveBase })
|
|
722
733
|
if (inlineCol === '*') {
|
|
723
734
|
if (seenWildcard) throw new Error(`Duplicate wildcard "*" in inline of "${col.as || col.ref.map(idOnly).join('_')}"`)
|
|
724
735
|
seenWildcard = true
|
|
@@ -777,7 +788,7 @@ function infer(originalQuery, model, useTechnicalAlias = true) {
|
|
|
777
788
|
else if (inlineCol.ref) nameParts.push(...inlineCol.ref.map(idOnly))
|
|
778
789
|
const name = nameParts.join('_')
|
|
779
790
|
if (inlineCol.inline) {
|
|
780
|
-
const inlineElements = resolveInline(inlineCol, name)
|
|
791
|
+
const inlineElements = resolveInline(inlineCol, name, effectiveBase)
|
|
781
792
|
elements = { ...elements, ...inlineElements }
|
|
782
793
|
} else if (inlineCol.expand) {
|
|
783
794
|
const expandElements = resolveExpand(inlineCol)
|
|
@@ -953,19 +964,39 @@ function infer(originalQuery, model, useTechnicalAlias = true) {
|
|
|
953
964
|
mergePathIfNecessary(basePath, arg)
|
|
954
965
|
} else if (arg.xpr || arg.args) {
|
|
955
966
|
const prop = arg.xpr ? 'xpr' : 'args'
|
|
967
|
+
let inExists = false
|
|
956
968
|
arg[prop].forEach(step => {
|
|
969
|
+
if (step === 'exists') {
|
|
970
|
+
inExists = true
|
|
971
|
+
return
|
|
972
|
+
}
|
|
957
973
|
let subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
|
|
958
974
|
if (step.ref) {
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
975
|
+
if (inExists) {
|
|
976
|
+
// refs following `exists` become subqueries in cqn4sql — only the basePath prefix
|
|
977
|
+
// needs a JOIN (for correlation), the exists-target association itself does not.
|
|
978
|
+
if (subPath.$refLinks.length > 0) {
|
|
979
|
+
inferred.joinTree.mergeColumn(subPath, originalQuery.outerQueries)
|
|
980
|
+
// The exists subquery correlates against the last assoc in basePath,
|
|
981
|
+
// so it's not just a FK access — force a real JOIN.
|
|
982
|
+
const lastLink = subPath.$refLinks[subPath.$refLinks.length - 1]
|
|
983
|
+
if (lastLink.onlyForeignKeyAccess) lastLink.onlyForeignKeyAccess = false
|
|
984
|
+
if (!calcElement.value.isJoinRelevant)
|
|
985
|
+
defineProperty(step, 'isJoinRelevant', true)
|
|
966
986
|
}
|
|
967
|
-
}
|
|
968
|
-
|
|
987
|
+
} else {
|
|
988
|
+
step.$refLinks.forEach((link, i) => {
|
|
989
|
+
const { definition } = link
|
|
990
|
+
if (definition.value) {
|
|
991
|
+
mergePathsIntoJoinTree(definition.value, subPath)
|
|
992
|
+
} else {
|
|
993
|
+
subPath.$refLinks.push(link)
|
|
994
|
+
subPath.ref.push(step.ref[i])
|
|
995
|
+
}
|
|
996
|
+
})
|
|
997
|
+
mergePathIfNecessary(subPath, step)
|
|
998
|
+
}
|
|
999
|
+
inExists = false
|
|
969
1000
|
} else if (step.args || step.xpr) {
|
|
970
1001
|
const nestedProp = step.xpr ? 'xpr' : 'args'
|
|
971
1002
|
step[nestedProp].forEach(a => {
|
|
@@ -974,6 +1005,9 @@ function infer(originalQuery, model, useTechnicalAlias = true) {
|
|
|
974
1005
|
if (!a.ref) subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
|
|
975
1006
|
mergePathsIntoJoinTree(a, subPath)
|
|
976
1007
|
})
|
|
1008
|
+
inExists = false
|
|
1009
|
+
} else {
|
|
1010
|
+
if (step !== 'not') inExists = false
|
|
977
1011
|
}
|
|
978
1012
|
})
|
|
979
1013
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/db-service",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
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": {
|
|
@@ -23,11 +23,14 @@
|
|
|
23
23
|
"scripts": {
|
|
24
24
|
"test": "cds-test"
|
|
25
25
|
},
|
|
26
|
-
"
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@sap/cds": "^10",
|
|
27
28
|
"generic-pool": "^3.9.0"
|
|
28
29
|
},
|
|
29
|
-
"
|
|
30
|
-
"
|
|
30
|
+
"peerDependenciesMeta": {
|
|
31
|
+
"generic-pool": {
|
|
32
|
+
"optional": true
|
|
33
|
+
}
|
|
31
34
|
},
|
|
32
35
|
"license": "Apache-2.0"
|
|
33
36
|
}
|