@cap-js/db-service 1.20.2 → 2.0.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 +25 -7
- package/lib/InsertResults.js +3 -3
- package/lib/SQLService.js +30 -40
- package/lib/cql-functions.js +231 -4
- package/lib/cqn2sql.js +307 -7
- package/lib/cqn4sql.js +1 -5
- package/lib/infer/index.js +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,22 +4,40 @@
|
|
|
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
|
-
## [
|
|
7
|
+
## [2.0.1](https://github.com/cap-js/cds-dbs/compare/db-service-v2.0.0...db-service-v2.0.1) (2025-05-27)
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
### Fixed
|
|
11
11
|
|
|
12
|
-
*
|
|
12
|
+
* **`search`:** do not search on non-projected elements ([#1198](https://github.com/cap-js/cds-dbs/issues/1198)) ([73d9e67](https://github.com/cap-js/cds-dbs/commit/73d9e67b1bc7d7727c04b4577cb73f4daaed852b))
|
|
13
|
+
* add shortcut for empty UPDATE.data ([#1203](https://github.com/cap-js/cds-dbs/issues/1203)) ([cf991ff](https://github.com/cap-js/cds-dbs/commit/cf991ff8179efee6a4621d2a2bd8bf6265e58893))
|
|
14
|
+
* hierarchies in quoted mode ([3465cba](https://github.com/cap-js/cds-dbs/commit/3465cbab579d4560d12d3b230c55b746d4d3f5a5))
|
|
15
|
+
* only sort by locale if locale is set ([#1193](https://github.com/cap-js/cds-dbs/issues/1193)) ([3465cba](https://github.com/cap-js/cds-dbs/commit/3465cbab579d4560d12d3b230c55b746d4d3f5a5))
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
* remove stream_compat ([#1139](https://github.com/cap-js/cds-dbs/issues/1139)) ([#1144](https://github.com/cap-js/cds-dbs/issues/1144)) ([1b8b2d9](https://github.com/cap-js/cds-dbs/commit/1b8b2d9539cd97be2cef088c98d88ef9ec7dd1bf))
|
|
21
|
+
|
|
22
|
+
## [2.0.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.20.0...db-service-v2.0.0) (2025-05-07)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### ⚠ BREAKING CHANGES
|
|
26
|
+
|
|
27
|
+
* update peer dependency to @sap/cds@9 ([#1178](https://github.com/cap-js/cds-dbs/issues/1178))
|
|
15
28
|
|
|
16
29
|
|
|
17
30
|
### Fixed
|
|
18
31
|
|
|
19
|
-
*
|
|
20
|
-
* current_utctimestamp as default ([#1161](https://github.com/cap-js/cds-dbs/issues/1161)) ([
|
|
21
|
-
* exists within expression is properly detected ([#1156](https://github.com/cap-js/cds-dbs/issues/1156)) ([
|
|
22
|
-
* resilience for query re-use scenarios ([#1175](https://github.com/cap-js/cds-dbs/issues/1175)) ([
|
|
32
|
+
* Adopt to recurse `DistanceTo` cqn format ([#1093](https://github.com/cap-js/cds-dbs/issues/1093)) ([246e0b3](https://github.com/cap-js/cds-dbs/commit/246e0b38840f7e132ea49cae335b6be7a55354b3))
|
|
33
|
+
* current_utctimestamp as default ([#1161](https://github.com/cap-js/cds-dbs/issues/1161)) ([7c6b2f5](https://github.com/cap-js/cds-dbs/commit/7c6b2f5a6837afbeb1e24daef9a49e25cf7e92f0))
|
|
34
|
+
* exists within expression is properly detected ([#1156](https://github.com/cap-js/cds-dbs/issues/1156)) ([5a7b50c](https://github.com/cap-js/cds-dbs/commit/5a7b50cb02776cf6052c79bd276421dd87161882))
|
|
35
|
+
* resilience for query re-use scenarios ([#1175](https://github.com/cap-js/cds-dbs/issues/1175)) ([2352767](https://github.com/cap-js/cds-dbs/commit/2352767465ea88db77dc89bcaa76e268583146e1))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
* update peer dependency to @sap/cds@9 ([#1178](https://github.com/cap-js/cds-dbs/issues/1178)) ([0507edd](https://github.com/cap-js/cds-dbs/commit/0507edd4e1dcb98983b1fb65ade1344d978b7524))
|
|
23
41
|
|
|
24
42
|
## [1.20.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.19.1...db-service-v1.20.0) (2025-04-17)
|
|
25
43
|
|
package/lib/InsertResults.js
CHANGED
|
@@ -26,9 +26,9 @@ module.exports = class InsertResult {
|
|
|
26
26
|
* Lazy access to auto-generated keys.
|
|
27
27
|
*/
|
|
28
28
|
get [iterator]() {
|
|
29
|
-
// For INSERT.
|
|
29
|
+
// For INSERT.from(SELECT.from(...)) return a dummy iterator with correct length
|
|
30
30
|
const { INSERT } = this.query
|
|
31
|
-
if (INSERT.as) {
|
|
31
|
+
if (INSERT.from || INSERT.as) {
|
|
32
32
|
return (super[iterator] = function* () {
|
|
33
33
|
for (let i = 0; i < this.affectedRows; i++) yield {}
|
|
34
34
|
})
|
|
@@ -81,7 +81,7 @@ module.exports = class InsertResult {
|
|
|
81
81
|
*/
|
|
82
82
|
get affectedRows() {
|
|
83
83
|
const { INSERT: _ } = this.query
|
|
84
|
-
if (_.as) return (super.affectedRows = this.affectedRows4(this.results[0] || this.results))
|
|
84
|
+
if (_.from || _.as) return (super.affectedRows = this.affectedRows4(this.results[0] || this.results))
|
|
85
85
|
else return (super.affectedRows = _.entries?.length || _.rows?.length || this.results.length || 1)
|
|
86
86
|
}
|
|
87
87
|
|
package/lib/SQLService.js
CHANGED
|
@@ -11,6 +11,20 @@ const BINARY_TYPES = {
|
|
|
11
11
|
'cds.hana.BINARY': 1
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Checks if parameter is an object that at least contains one property.
|
|
16
|
+
*
|
|
17
|
+
* @param {*} obj
|
|
18
|
+
* @returns Boolean
|
|
19
|
+
*/
|
|
20
|
+
const _hasProps = (obj) => {
|
|
21
|
+
if (!obj) return false
|
|
22
|
+
for (const p in obj) {
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
/** @typedef {import('@sap/cds/apis/services').Request} Request */
|
|
15
29
|
|
|
16
30
|
/**
|
|
@@ -57,24 +71,18 @@ class SQLService extends DatabaseService {
|
|
|
57
71
|
return super.init()
|
|
58
72
|
}
|
|
59
73
|
|
|
60
|
-
_changeToStreams(columns, rows, one
|
|
74
|
+
_changeToStreams(columns, rows, one) {
|
|
61
75
|
if (!rows || !columns) return
|
|
62
76
|
if (!Array.isArray(rows)) rows = [rows]
|
|
63
|
-
if (!rows.length || !Object.keys(rows[0]).length) return
|
|
64
|
-
|
|
65
|
-
// REVISIT: remove after removing stream_compat feature flag
|
|
66
|
-
if (compat) {
|
|
67
|
-
rows[0][Object.keys(rows[0])[0]] = this._stream(Object.values(rows[0])[0])
|
|
68
|
-
return
|
|
69
|
-
}
|
|
77
|
+
if (!rows.length || !Object.keys(rows[0]).length) return
|
|
70
78
|
|
|
71
79
|
let changes = false
|
|
72
80
|
for (let col of columns) {
|
|
73
81
|
const name = col.as || col.ref?.[col.ref.length - 1] || (typeof col === 'string' && col)
|
|
74
82
|
if (col.element?.isAssociation) {
|
|
75
|
-
if (one) this._changeToStreams(col.SELECT.columns, rows[0][name], false
|
|
83
|
+
if (one) this._changeToStreams(col.SELECT.columns, rows[0][name], false)
|
|
76
84
|
else
|
|
77
|
-
changes = rows.some(row => !this._changeToStreams(col.SELECT.columns, row[name], false
|
|
85
|
+
changes = rows.some(row => !this._changeToStreams(col.SELECT.columns, row[name], false))
|
|
78
86
|
} else if (col.element?.type === 'cds.LargeBinary') {
|
|
79
87
|
changes = true
|
|
80
88
|
if (one) rows[0][name] = this._stream(rows[0][name])
|
|
@@ -141,23 +149,7 @@ class SQLService extends DatabaseService {
|
|
|
141
149
|
if (expand) rows = rows.map(r => (typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r))
|
|
142
150
|
|
|
143
151
|
if (!iterator) {
|
|
144
|
-
|
|
145
|
-
if (cds.env.features.stream_compat) {
|
|
146
|
-
if (query._streaming) {
|
|
147
|
-
if (!rows.length) return
|
|
148
|
-
this._changeToStreams(cqn.SELECT.columns, rows, true, true)
|
|
149
|
-
const result = rows[0]
|
|
150
|
-
|
|
151
|
-
// stream is always on position 0. Further properties like etag are inserted later.
|
|
152
|
-
let [key, val] = Object.entries(result)[0]
|
|
153
|
-
result.value = val
|
|
154
|
-
delete result[key]
|
|
155
|
-
|
|
156
|
-
return result
|
|
157
|
-
}
|
|
158
|
-
} else {
|
|
159
|
-
this._changeToStreams(cqn.SELECT.columns, rows, query.SELECT.one, false)
|
|
160
|
-
}
|
|
152
|
+
this._changeToStreams(cqn.SELECT.columns, rows, query.SELECT.one)
|
|
161
153
|
} else if (objectMode) {
|
|
162
154
|
const converter = (row) => this._changeToStreams(cqn.SELECT.columns, row, true)
|
|
163
155
|
const changeToStreams = new Transform({
|
|
@@ -216,8 +208,8 @@ class SQLService extends DatabaseService {
|
|
|
216
208
|
async onUPDATE(req) {
|
|
217
209
|
// noop if not a touch for @cds.on.update
|
|
218
210
|
if (
|
|
219
|
-
!req.query.UPDATE.data &&
|
|
220
|
-
!req.query.UPDATE.with &&
|
|
211
|
+
!_hasProps(req.query.UPDATE.data) &&
|
|
212
|
+
!_hasProps(req.query.UPDATE.with) &&
|
|
221
213
|
!Object.values(req.target?.elements || {}).some(e => e['@cds.on.update'])
|
|
222
214
|
)
|
|
223
215
|
return 0
|
|
@@ -404,8 +396,6 @@ class SQLService extends DatabaseService {
|
|
|
404
396
|
let kind = q.kind || Object.keys(q)[0]
|
|
405
397
|
if (kind in { INSERT: 1, DELETE: 1, UPSERT: 1, UPDATE: 1 }) {
|
|
406
398
|
q = resolveView(q, this.model, this) // REVISIT: before resolveView was called on flat cqn obtained from cqn4sql -> is it correct to call on original q instead?
|
|
407
|
-
let target = q[kind]._transitions?.[0].target
|
|
408
|
-
if (target) q._target = target // REVISIT: Why isn't that done in resolveView?
|
|
409
399
|
}
|
|
410
400
|
let cqn2sql = new this.class.CQN2SQL(this)
|
|
411
401
|
return cqn2sql.render(q, values)
|
|
@@ -495,16 +485,16 @@ class PreparedStatement {
|
|
|
495
485
|
}
|
|
496
486
|
SQLService.prototype.PreparedStatement = PreparedStatement
|
|
497
487
|
|
|
488
|
+
/** @param {import('@sap/cds').ql.Query} q */
|
|
498
489
|
const _target_name4 = q => {
|
|
499
|
-
const target =
|
|
500
|
-
q.
|
|
501
|
-
q.
|
|
502
|
-
q.
|
|
503
|
-
q.
|
|
504
|
-
q.
|
|
505
|
-
q.
|
|
506
|
-
q.
|
|
507
|
-
q.DROP?.entity
|
|
490
|
+
const target = q._subject
|
|
491
|
+
|| q.SELECT?.from
|
|
492
|
+
|| q.INSERT?.into
|
|
493
|
+
|| q.UPSERT?.into
|
|
494
|
+
|| q.UPDATE?.entity
|
|
495
|
+
|| q.DELETE?.from
|
|
496
|
+
|| q.CREATE?.entity
|
|
497
|
+
|| q.DROP?.entity
|
|
508
498
|
if (target?.SET?.op === 'union') throw new cds.error('UNION-based queries are not supported')
|
|
509
499
|
if (!target?.ref) return target
|
|
510
500
|
const [first] = target.ref
|
package/lib/cql-functions.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const cds = require('@sap/cds')
|
|
4
|
+
|
|
3
5
|
// OData: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_CanonicalFunctions
|
|
4
6
|
const StandardFunctions = {
|
|
5
7
|
/**
|
|
@@ -18,10 +20,15 @@ const StandardFunctions = {
|
|
|
18
20
|
} catch {
|
|
19
21
|
val = sub[2] || sub[3] || ''
|
|
20
22
|
}
|
|
21
|
-
arg.val =
|
|
23
|
+
arg.val = val
|
|
22
24
|
const refs = ref.list
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
return `(${refs.map(ref => this.expr({
|
|
26
|
+
func: 'contains',
|
|
27
|
+
args: [
|
|
28
|
+
{ func: 'tolower', args: [ref] },
|
|
29
|
+
{ func: 'tolower', args: [arg] },
|
|
30
|
+
]
|
|
31
|
+
})).join(' or ')})`
|
|
25
32
|
},
|
|
26
33
|
|
|
27
34
|
// ==============================
|
|
@@ -141,7 +148,7 @@ const StandardFunctions = {
|
|
|
141
148
|
* @returns {string} - SQL statement
|
|
142
149
|
*/
|
|
143
150
|
now: function () {
|
|
144
|
-
return this.
|
|
151
|
+
return this.expr({ func: 'session_context', args: [{ val: '$now' }] })
|
|
145
152
|
},
|
|
146
153
|
|
|
147
154
|
/**
|
|
@@ -184,6 +191,226 @@ const HANAFunctions = {
|
|
|
184
191
|
* @returns {string} - SQL statement
|
|
185
192
|
*/
|
|
186
193
|
current_timestamp: p => (p ? `current_timestamp(${p})` : 'current_timestamp'),
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Generates SQL statement for the hierarchy function
|
|
197
|
+
* @param {string} [p] -
|
|
198
|
+
* @returns {string} - SQL statement
|
|
199
|
+
*/
|
|
200
|
+
HIERARCHY: function (args) {
|
|
201
|
+
let uniqueCounter = this._with?.length ?? 0
|
|
202
|
+
let src = args.xpr[1]
|
|
203
|
+
|
|
204
|
+
// Ensure that the orderBy column are exposed by the source for hierarchy sorting
|
|
205
|
+
const orderBy = args.xpr.find((_, i, arr) => /ORDER/i.test(arr[i - 2]) && /BY/i.test(arr[i - 1]))
|
|
206
|
+
|
|
207
|
+
const passThroughColumns = src.SELECT.columns.map(c => ({ ref: ['Source', this.column_name(c)] }))
|
|
208
|
+
src.as = 'H' + (uniqueCounter++)
|
|
209
|
+
src = this.expr(this.with(src))
|
|
210
|
+
|
|
211
|
+
let recursive = cds.ql(`
|
|
212
|
+
SELECT
|
|
213
|
+
1 as HIERARCHY_LEVEL,
|
|
214
|
+
NODE_ID as HIERARCHY_ROOT_ID
|
|
215
|
+
FROM ${src} AS Source
|
|
216
|
+
WHERE parent_ID IS NULL
|
|
217
|
+
UNION ALL
|
|
218
|
+
SELECT
|
|
219
|
+
Parent.HIERARCHY_LEVEL + 1,
|
|
220
|
+
Parent.HIERARCHY_ROOT_ID
|
|
221
|
+
FROM ${src} AS Source
|
|
222
|
+
JOIN H${uniqueCounter} AS Parent ON Source.PARENT_ID=Parent.NODE_ID
|
|
223
|
+
ORDER BY HIERARCHY_LEVEL DESC${orderBy ? `,${orderBy}` : ''}`)
|
|
224
|
+
recursive.as = 'H' + (uniqueCounter++)
|
|
225
|
+
recursive.SET.args[0].SELECT.columns = [...recursive.SET.args[0].SELECT.columns, ...passThroughColumns]
|
|
226
|
+
recursive.SET.args[1].SELECT.columns = [...recursive.SET.args[1].SELECT.columns, ...passThroughColumns]
|
|
227
|
+
recursive = this.expr(this.with(recursive))
|
|
228
|
+
|
|
229
|
+
let ranked = cds.ql(`
|
|
230
|
+
SELECT
|
|
231
|
+
HIERARCHY_LEVEL,
|
|
232
|
+
row_number() over () as HIERARCHY_RANK,
|
|
233
|
+
HIERARCHY_ROOT_ID
|
|
234
|
+
FROM ${recursive} AS Source`)
|
|
235
|
+
ranked.as = 'H' + (uniqueCounter++)
|
|
236
|
+
ranked.SELECT.columns = [...ranked.SELECT.columns, ...passThroughColumns]
|
|
237
|
+
ranked = this.expr(this.with(ranked))
|
|
238
|
+
|
|
239
|
+
let Hierarchy = cds.ql(`
|
|
240
|
+
SELECT
|
|
241
|
+
HIERARCHY_LEVEL,
|
|
242
|
+
HIERARCHY_RANK,
|
|
243
|
+
(SELECT HIERARCHY_RANK FROM ${ranked} AS Ranked WHERE Ranked.NODE_ID = Source.PARENT_ID) AS HIERARCHY_PARENT_RANK,
|
|
244
|
+
(SELECT HIERARCHY_RANK FROM ${ranked} AS Ranked WHERE Ranked.NODE_ID = Source.HIERARCHY_ROOT_ID) AS HIERARCHY_ROOT_RANK,
|
|
245
|
+
coalesce(
|
|
246
|
+
(SELECT MIN(HIERARCHY_RANK) FROM ${ranked} AS Ranked WHERE Ranked.HIERARCHY_RANK > Source.HIERARCHY_RANK AND Ranked.HIERARCHY_LEVEL <= Source.HIERARCHY_LEVEL),
|
|
247
|
+
(SELECT MAX(HIERARCHY_RANK) + 1 FROM ${ranked})
|
|
248
|
+
) - Source.HIERARCHY_RANK AS HIERARCHY_TREE_SIZE
|
|
249
|
+
FROM ${ranked} AS Source`)
|
|
250
|
+
Hierarchy.as = 'H' + (uniqueCounter++)
|
|
251
|
+
Hierarchy.SELECT.columns = [...Hierarchy.SELECT.columns, ...passThroughColumns]
|
|
252
|
+
Hierarchy = this.expr(this.with(Hierarchy))
|
|
253
|
+
|
|
254
|
+
return Hierarchy
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Generates SQL statement for the hierarchy_descendants function
|
|
259
|
+
* @param {string} [p] -
|
|
260
|
+
* @returns {string} - SQL statement
|
|
261
|
+
*/
|
|
262
|
+
HIERARCHY_DESCENDANTS: function (args) {
|
|
263
|
+
// Find Hierarchy function call source query
|
|
264
|
+
const passThroughColumns = args.xpr[1].args[0].xpr[1].SELECT.columns.map(c => ({ ref: [this.column_name(c)] }))
|
|
265
|
+
// REVISIT: currently only supports func: HIERARCHY as source
|
|
266
|
+
const src = this.expr(args.xpr[1])
|
|
267
|
+
|
|
268
|
+
let uniqueCounter = this._with?.length ?? 0
|
|
269
|
+
|
|
270
|
+
let alias = args.xpr.find((_, i, arr) => /AS/i.test(arr[i - 1]))
|
|
271
|
+
const where = args.xpr.find((a, i, arr) => a.xpr && /WHERE/i.test(arr[i - 1]) && /START/i.test(arr[i - 2]))
|
|
272
|
+
const distance = args.xpr.find((a, i, arr) => typeof a.val === 'number' && (/DISTANCE/i.test(arr[i - 1]) || /DISTANCE/i.test(arr[i - 2])))
|
|
273
|
+
const distanceFrom = args.xpr.find((a, i, arr) => /FROM/.test(a) && /DISTANCE/i.test(arr[i - 1]))
|
|
274
|
+
|
|
275
|
+
if (alias.startsWith('"') && alias.endsWith('"')) alias = alias.slice(1, -1).replace(/""/g, '"')
|
|
276
|
+
|
|
277
|
+
let HierarchyDescendants = cds.ql(`
|
|
278
|
+
SELECT
|
|
279
|
+
HIERARCHY_LEVEL,
|
|
280
|
+
HIERARCHY_PARENT_RANK,
|
|
281
|
+
HIERARCHY_RANK,
|
|
282
|
+
HIERARCHY_ROOT_RANK,
|
|
283
|
+
HIERARCHY_TREE_SIZE,
|
|
284
|
+
0 as HIERARCHY_DISTANCE
|
|
285
|
+
FROM ${src} AS ![${alias}]
|
|
286
|
+
UNION ALL
|
|
287
|
+
SELECT
|
|
288
|
+
Source.HIERARCHY_LEVEL,
|
|
289
|
+
Source.HIERARCHY_PARENT_RANK,
|
|
290
|
+
Source.HIERARCHY_RANK,
|
|
291
|
+
Source.HIERARCHY_ROOT_RANK,
|
|
292
|
+
Source.HIERARCHY_TREE_SIZE,
|
|
293
|
+
Child.HIERARCHY_DISTANCE + 1
|
|
294
|
+
FROM ${src} AS Source
|
|
295
|
+
JOIN H${uniqueCounter} AS Child ON Source.PARENT_ID=Child.NODE_ID`)
|
|
296
|
+
HierarchyDescendants.as = 'H' + uniqueCounter
|
|
297
|
+
HierarchyDescendants.SET.args[0].SELECT.where = where.xpr
|
|
298
|
+
HierarchyDescendants.SET.args[0].SELECT.columns = [...HierarchyDescendants.SET.args[0].SELECT.columns, ...passThroughColumns.map(r => ({ ref: [alias, r.ref[0]] }))]
|
|
299
|
+
HierarchyDescendants.SET.args[1].SELECT.columns = [...HierarchyDescendants.SET.args[1].SELECT.columns, ...passThroughColumns.map(r => ({ ref: ['Source', r.ref[0]] }))]
|
|
300
|
+
|
|
301
|
+
HierarchyDescendants = this.with(HierarchyDescendants)
|
|
302
|
+
HierarchyDescendants.as = 'HierarchyDescendants'
|
|
303
|
+
|
|
304
|
+
return this.expr({
|
|
305
|
+
SELECT: {
|
|
306
|
+
columns: [
|
|
307
|
+
{ ref: ['HIERARCHY_LEVEL'] },
|
|
308
|
+
{ ref: ['HIERARCHY_PARENT_RANK'] },
|
|
309
|
+
{ ref: ['HIERARCHY_RANK'] },
|
|
310
|
+
{ ref: ['HIERARCHY_ROOT_RANK'] },
|
|
311
|
+
{ ref: ['HIERARCHY_TREE_SIZE'] },
|
|
312
|
+
{
|
|
313
|
+
SELECT: {
|
|
314
|
+
columns: [{ func: 'MAX', args: [{ ref: ['HIERARCHY_DISTANCE'] }] }],
|
|
315
|
+
from: HierarchyDescendants,
|
|
316
|
+
where: [{ ref: [HierarchyDescendants.as, 'HIERARCHY_RANK'] }, '=', { ref: [src, 'HIERARCHY_RANK'] }]
|
|
317
|
+
},
|
|
318
|
+
as: 'HIERARCHY_DISTANCE',
|
|
319
|
+
},
|
|
320
|
+
...passThroughColumns,
|
|
321
|
+
],
|
|
322
|
+
from: { ref: [src] },
|
|
323
|
+
where: [
|
|
324
|
+
{ ref: ['HIERARCHY_RANK'] },
|
|
325
|
+
'IN',
|
|
326
|
+
{
|
|
327
|
+
SELECT: {
|
|
328
|
+
columns: [{ ref: ['HIERARCHY_RANK'] }],
|
|
329
|
+
from: HierarchyDescendants,
|
|
330
|
+
where: [{ ref: ['HIERARCHY_DISTANCE'] }, distanceFrom ? '>=' : '=', distance]
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
]
|
|
334
|
+
}
|
|
335
|
+
})
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Generates SQL statement for the hierarchy_ancestors function
|
|
340
|
+
* @param {string} [p] -
|
|
341
|
+
* @returns {string} - SQL statement
|
|
342
|
+
*/
|
|
343
|
+
HIERARCHY_ANCESTORS: function (args) {
|
|
344
|
+
// Find Hierarchy function call source query
|
|
345
|
+
const passThroughColumns = args.xpr[1].args[0].xpr[1].SELECT.columns.map(c => ({ ref: [this.column_name(c)] }))
|
|
346
|
+
// REVISIT: currently only supports func: HIERARCHY as source
|
|
347
|
+
const src = this.expr(args.xpr[1])
|
|
348
|
+
|
|
349
|
+
let uniqueCounter = this._with?.length ?? 0
|
|
350
|
+
|
|
351
|
+
let alias = args.xpr.find((_, i, arr) => /AS/i.test(arr[i - 1]))
|
|
352
|
+
const where = args.xpr.find((a, i, arr) => a.xpr && /WHERE/i.test(arr[i - 1]) && /START/i.test(arr[i - 2]))
|
|
353
|
+
|
|
354
|
+
if (alias.startsWith('"') && alias.endsWith('"')) alias = alias.slice(1, -1).replace(/""/g, '"')
|
|
355
|
+
|
|
356
|
+
let HierarchyAncestors = cds.ql(`
|
|
357
|
+
SELECT
|
|
358
|
+
HIERARCHY_LEVEL,
|
|
359
|
+
HIERARCHY_PARENT_RANK,
|
|
360
|
+
HIERARCHY_RANK,
|
|
361
|
+
HIERARCHY_ROOT_RANK,
|
|
362
|
+
HIERARCHY_TREE_SIZE,
|
|
363
|
+
0 as HIERARCHY_DISTANCE
|
|
364
|
+
FROM ${src} AS ![${alias}]
|
|
365
|
+
UNION ALL
|
|
366
|
+
SELECT
|
|
367
|
+
Source.HIERARCHY_LEVEL,
|
|
368
|
+
Source.HIERARCHY_PARENT_RANK,
|
|
369
|
+
Source.HIERARCHY_RANK,
|
|
370
|
+
Source.HIERARCHY_ROOT_RANK,
|
|
371
|
+
Source.HIERARCHY_TREE_SIZE,
|
|
372
|
+
Child.HIERARCHY_DISTANCE - 1
|
|
373
|
+
FROM ${src} AS Source
|
|
374
|
+
JOIN H${uniqueCounter} AS Child ON Source.NODE_ID=Child.PARENT_ID`)
|
|
375
|
+
HierarchyAncestors.as = 'H' + uniqueCounter
|
|
376
|
+
HierarchyAncestors.SET.args[0].SELECT.where = where.xpr
|
|
377
|
+
HierarchyAncestors.SET.args[0].SELECT.columns = [...HierarchyAncestors.SET.args[0].SELECT.columns, ...passThroughColumns.map(r => ({ ref: [alias, r.ref[0]] }))]
|
|
378
|
+
HierarchyAncestors.SET.args[1].SELECT.columns = [...HierarchyAncestors.SET.args[1].SELECT.columns, ...passThroughColumns.map(r => ({ ref: ['Source', r.ref[0]] }))]
|
|
379
|
+
|
|
380
|
+
HierarchyAncestors = this.with(HierarchyAncestors)
|
|
381
|
+
HierarchyAncestors.as = 'HierarchyAncestors'
|
|
382
|
+
return this.expr({
|
|
383
|
+
SELECT: {
|
|
384
|
+
columns: [
|
|
385
|
+
{ ref: ['HIERARCHY_LEVEL'] },
|
|
386
|
+
{ ref: ['HIERARCHY_PARENT_RANK'] },
|
|
387
|
+
{ ref: ['HIERARCHY_RANK'] },
|
|
388
|
+
{ ref: ['HIERARCHY_ROOT_RANK'] },
|
|
389
|
+
{ ref: ['HIERARCHY_TREE_SIZE'] },
|
|
390
|
+
{
|
|
391
|
+
SELECT: {
|
|
392
|
+
columns: [{ func: 'MIN', args: [{ ref: ['HIERARCHY_DISTANCE'] }] }],
|
|
393
|
+
from: HierarchyAncestors,
|
|
394
|
+
where: [{ ref: [HierarchyAncestors.as, 'HIERARCHY_RANK'] }, '=', { ref: [src, 'HIERARCHY_RANK'] }]
|
|
395
|
+
},
|
|
396
|
+
as: 'HIERARCHY_DISTANCE',
|
|
397
|
+
},
|
|
398
|
+
...passThroughColumns,
|
|
399
|
+
],
|
|
400
|
+
from: { ref: [src] },
|
|
401
|
+
where: [
|
|
402
|
+
{ ref: ['HIERARCHY_RANK'] },
|
|
403
|
+
'IN',
|
|
404
|
+
{
|
|
405
|
+
SELECT: {
|
|
406
|
+
columns: [{ ref: ['HIERARCHY_RANK'] }],
|
|
407
|
+
from: HierarchyAncestors,
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
]
|
|
411
|
+
}
|
|
412
|
+
})
|
|
413
|
+
},
|
|
187
414
|
}
|
|
188
415
|
|
|
189
416
|
for (let each in HANAFunctions) HANAFunctions[each.toUpperCase()] = HANAFunctions[each]
|
package/lib/cqn2sql.js
CHANGED
|
@@ -82,6 +82,9 @@ class CQN2SQLRenderer {
|
|
|
82
82
|
/** @type {unknown[]} */
|
|
83
83
|
this.values = [] // prepare values, filled in by subroutines
|
|
84
84
|
this[kind]((this.cqn = q)) // actual sql rendering happens here
|
|
85
|
+
if (this._with?.length) {
|
|
86
|
+
this.render_with()
|
|
87
|
+
}
|
|
85
88
|
if (vars?.length && !this.values?.length) this.values = vars
|
|
86
89
|
if (vars && Object.keys(vars).length && !this.values?.length) this.values = vars
|
|
87
90
|
const sanitize_values = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
|
|
@@ -95,10 +98,28 @@ class CQN2SQLRenderer {
|
|
|
95
98
|
DEBUG(this.sql, values)
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
|
|
99
101
|
return this
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
render_with() {
|
|
105
|
+
const sql = this.sql
|
|
106
|
+
let recursive = false
|
|
107
|
+
const values = this.values
|
|
108
|
+
const prefix = this._with.map(q => {
|
|
109
|
+
const values = this.values = []
|
|
110
|
+
let sql
|
|
111
|
+
if ('SELECT' in q) sql = `${this.quote(q.as)} AS (${this.SELECT(q)})`
|
|
112
|
+
else if ('SET' in q) {
|
|
113
|
+
recursive = true
|
|
114
|
+
const { SET } = q
|
|
115
|
+
sql = `${this.quote(q.as)}(${SET.args[0].SELECT.columns?.map(c => this.quote(this.column_name(c))) || ''}) AS (${this.SELECT(SET.args[0])} ${SET.op?.toUpperCase() || 'UNION'} ${SET.all ? 'ALL' : ''} ${this.SELECT(SET.args[1])}${SET.orderBy ? ` ORDER BY ${this.orderBy(SET.orderBy)}` : ''})`
|
|
116
|
+
}
|
|
117
|
+
return { sql, values }
|
|
118
|
+
})
|
|
119
|
+
this.sql = `WITH${recursive ? ' RECURSIVE' : ''} ${prefix.map(p => p.sql)} ${sql}`
|
|
120
|
+
this.values = [...prefix.map(p => p.values).flat(), ...values]
|
|
121
|
+
}
|
|
122
|
+
|
|
102
123
|
/**
|
|
103
124
|
* Links the incoming query with the current service model
|
|
104
125
|
* @param {import('./infer/cqn').Query} q
|
|
@@ -258,8 +279,275 @@ class CQN2SQLRenderer {
|
|
|
258
279
|
return (this.sql = sql)
|
|
259
280
|
}
|
|
260
281
|
|
|
261
|
-
SELECT_recurse() {
|
|
262
|
-
|
|
282
|
+
SELECT_recurse(q) {
|
|
283
|
+
let { from, columns, where, orderBy, recurse, _internal } = q.SELECT
|
|
284
|
+
|
|
285
|
+
const requiredComputedColumns = { PARENT_ID: true, NODE_ID: true }
|
|
286
|
+
if (!_internal) requiredComputedColumns.RANK = true
|
|
287
|
+
const addComputedColumn = (name) => {
|
|
288
|
+
if (requiredComputedColumns[name]) return
|
|
289
|
+
requiredComputedColumns[name] = true
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// The hierarchy functions will output the following columns. Which might clash with the entity columns
|
|
293
|
+
const reservedColumnNames = {
|
|
294
|
+
PARENT_ID: 1, NODE_ID: 1,
|
|
295
|
+
HIERARCHY_RANK: 1, HIERARCHY_DISTANCE: 1, HIERARCHY_LEVEL: 1, HIERARCHY_TREE_SIZE: 1
|
|
296
|
+
}
|
|
297
|
+
const availableComputedColumns = {
|
|
298
|
+
// Input computed columns
|
|
299
|
+
PARENT_ID: false,
|
|
300
|
+
NODE_ID: false,
|
|
301
|
+
|
|
302
|
+
// Output computed columns
|
|
303
|
+
RANK: { xpr: [{ ref: ['HIERARCHY_RANK'] }, '-', { val: 1, param: false }], as: 'RANK' },
|
|
304
|
+
Distance: { func: where?.length ? 'min' : 'max', args: [{ ref: ['HIERARCHY_DISTANCE'] }], as: 'Distance' },
|
|
305
|
+
DistanceFromRoot: { xpr: [{ ref: ['HIERARCHY_LEVEL'] }, '-', { val: 1, param: false }], as: 'DistanceFromRoot' },
|
|
306
|
+
DrillState: false,
|
|
307
|
+
LimitedDescendantCount: { xpr: [{ ref: ['HIERARCHY_TREE_SIZE'] }, '-', { val: 1, param: false }], as: 'LimitedDescendantCount' },
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const columnsFiltered = columns
|
|
311
|
+
.filter(x => {
|
|
312
|
+
if (x.element?.isAssociation) return false
|
|
313
|
+
const name = this.column_name(x)
|
|
314
|
+
if (name === '$$RN$$') return false
|
|
315
|
+
// REVISIT: ensure that the selected column is one of the hierarchy computed columns by unifying their common definition
|
|
316
|
+
if (x.element?.['@Core.Computed'] && name in availableComputedColumns) {
|
|
317
|
+
addComputedColumn(name)
|
|
318
|
+
return false
|
|
319
|
+
}
|
|
320
|
+
return true
|
|
321
|
+
})
|
|
322
|
+
const columnsOut = []
|
|
323
|
+
const columnsIn = []
|
|
324
|
+
const target = q._target || q.target
|
|
325
|
+
for (const name in target.elements) {
|
|
326
|
+
const ref = { ref: [name] }
|
|
327
|
+
const element = target.elements[name]
|
|
328
|
+
if (element.virtual || element.value || element.isAssociation) continue
|
|
329
|
+
if (element['@Core.Computed'] && name in availableComputedColumns) continue
|
|
330
|
+
if (name.toUpperCase() in reservedColumnNames) ref.as = `$$${name}$$`
|
|
331
|
+
columnsIn.push(ref)
|
|
332
|
+
if (from.args || columnsFiltered.find(c => this.column_name(c) === name)) {
|
|
333
|
+
columnsOut.push(ref.as ? { ref: [ref.as], as: name } : ref)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const nodeKeys = []
|
|
338
|
+
const parentKeys = []
|
|
339
|
+
const association = target.elements[recurse.ref[0]]
|
|
340
|
+
association._foreignKeys.forEach(fk => {
|
|
341
|
+
nodeKeys.push(fk.childElement.name)
|
|
342
|
+
parentKeys.push(fk.parentElement.name)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
columnsIn.push(
|
|
346
|
+
nodeKeys.length === 1
|
|
347
|
+
? { ref: nodeKeys, as: 'NODE_ID' }
|
|
348
|
+
: { func: 'HIERARCHY_COMPOSITE_ID', args: nodeKeys.map(n => ({ ref: [n] })), as: 'NODE_ID' },
|
|
349
|
+
parentKeys.length === 1
|
|
350
|
+
? { ref: parentKeys, as: 'PARENT_ID' }
|
|
351
|
+
: { func: 'HIERARCHY_COMPOSITE_ID', args: parentKeys.map(n => ({ ref: [n] })), as: 'PARENT_ID' },
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
if (orderBy) {
|
|
355
|
+
orderBy = orderBy.map(r => {
|
|
356
|
+
const col = r.ref.at(-1)
|
|
357
|
+
if (!columnsIn.find(c => this.column_name(c) === col)) {
|
|
358
|
+
columnsIn.push({ ref: [col] })
|
|
359
|
+
}
|
|
360
|
+
return { ...r, ref: [col] }
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// In the case of join operations make sure to compute the hierarchy from the source table only
|
|
365
|
+
const stableFrom = getStableFrom(from)
|
|
366
|
+
const alias = stableFrom.as
|
|
367
|
+
const source = () => {
|
|
368
|
+
return ({
|
|
369
|
+
func: 'HIERARCHY',
|
|
370
|
+
args: [{ xpr: ['SOURCE', { SELECT: { columns: columnsIn, from: stableFrom } }, ...(orderBy ? ['SIBLING', 'ORDER', 'BY', `${this.orderBy(orderBy)}`] : [])] }],
|
|
371
|
+
as: alias
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const expandedByNr = { list: [] } // DistanceTo(...,null)
|
|
376
|
+
const expandedByOne = { list: [] } // DistanceTo(...,1)
|
|
377
|
+
const expandedByZero = { list: [] } // not DistanceTo(...,null)
|
|
378
|
+
let expandedFilter = []
|
|
379
|
+
let distanceType = 'DistanceFromRoot'
|
|
380
|
+
let distanceVal
|
|
381
|
+
|
|
382
|
+
if (recurse.where) {
|
|
383
|
+
distanceType = 'Distance'
|
|
384
|
+
if (recurse.where[0] === 'and') recurse.where = recurse.where.slice(1)
|
|
385
|
+
expandedFilter = [...recurse.where]
|
|
386
|
+
collectDistanceTo(expandedFilter)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const direction = where?.length ? 'ANCESTORS' : 'DESCENDANTS'
|
|
390
|
+
// Ensure that the distance value is being computed
|
|
391
|
+
if (distanceType) addComputedColumn(distanceType)
|
|
392
|
+
|
|
393
|
+
let distanceClause = []
|
|
394
|
+
if (distanceType === 'Distance') {
|
|
395
|
+
const isOne = expandedByOne.list.length
|
|
396
|
+
distanceClause = ['DISTANCE', ...(
|
|
397
|
+
isOne
|
|
398
|
+
? [{ val: 1 }]
|
|
399
|
+
: ['FROM', { val: 1 }]
|
|
400
|
+
)]
|
|
401
|
+
where = [{ ref: ['NODE_ID'] }, 'IN', isOne ? expandedByOne : expandedByNr]
|
|
402
|
+
expandedFilter = []
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
availableComputedColumns.DrillState = {
|
|
406
|
+
xpr: [ // When the node doesn't have children make it a leaf
|
|
407
|
+
'CASE', 'WHEN', { ref: ['HIERARCHY_TREE_SIZE'] }, '=', { val: 1, param: false }, 'THEN', { val: 'leaf', param: false },
|
|
408
|
+
...(where?.length // When there is a where filter the final node will always be a leaf
|
|
409
|
+
? ['WHEN', { func: where?.length ? 'min' : 'max', args: [{ ref: ['HIERARCHY_DISTANCE'] }] }, '=', { val: 0, param: false }, 'THEN', { val: 'leaf', param: false }]
|
|
410
|
+
: []
|
|
411
|
+
), // When having expanded by 0 level nodes make sure they are collapsed
|
|
412
|
+
...(expandedByZero.list.length
|
|
413
|
+
? ['WHEN', { ref: ['NODE_ID'] }, 'IN', expandedByZero, 'THEN', { val: 'collapsed', param: false }]
|
|
414
|
+
: []
|
|
415
|
+
), // When having expanded by null or one nodes compute them as expanded
|
|
416
|
+
...(expandedByNr.list.length || expandedByOne.list.length
|
|
417
|
+
? ['WHEN', { ref: ['NODE_ID'] }, 'IN', { list: [...expandedByNr.list, ...expandedByOne.list] }, 'THEN', { val: 'expanded', param: false }]
|
|
418
|
+
: []
|
|
419
|
+
), // When having expanded by one level node make its children collapsed
|
|
420
|
+
...(expandedByOne.list.length
|
|
421
|
+
? ['WHEN', { ref: ['PARENT_ID'] }, 'IN', expandedByOne, 'THEN', { val: 'collapsed', param: false }]
|
|
422
|
+
: []
|
|
423
|
+
), // When using DistanceFromRoot compute all entries within the levels as expanded
|
|
424
|
+
...(distanceType === 'DistanceFromRoot' && distanceVal
|
|
425
|
+
? [
|
|
426
|
+
'WHEN', { ref: ['HIERARCHY_LEVEL'] }, '<>', { val: distanceVal.val + 1 },
|
|
427
|
+
'THEN', { val: 'expanded', param: false },
|
|
428
|
+
]
|
|
429
|
+
: []
|
|
430
|
+
), // Default to expanded when default filter behavior is truthy
|
|
431
|
+
'ELSE', { val: (recurse.where && !expandedByZero.list.length) && distanceType ? 'collapsed' : 'expanded', param: false },
|
|
432
|
+
'END',
|
|
433
|
+
],
|
|
434
|
+
as: 'DrillState'
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
for (const name in requiredComputedColumns) {
|
|
438
|
+
const def = availableComputedColumns[name]
|
|
439
|
+
if (def) columnsOut.push(def)
|
|
440
|
+
}
|
|
441
|
+
if (_internal) columnsOut.push({ ref: ['NODE_ID'] })
|
|
442
|
+
|
|
443
|
+
const graph = distanceType === 'DistanceFromRoot' && !where
|
|
444
|
+
? { SELECT: { columns: columnsOut, from: source(), where: expandedFilter } }
|
|
445
|
+
: {
|
|
446
|
+
SELECT: {
|
|
447
|
+
columns: columnsOut,
|
|
448
|
+
from: {
|
|
449
|
+
func: `HIERARCHY_${direction}`,
|
|
450
|
+
args: [{
|
|
451
|
+
xpr: [
|
|
452
|
+
'SOURCE', source(), 'AS', this.quote(alias),
|
|
453
|
+
'START', 'WHERE', {
|
|
454
|
+
xpr: where // Requires special where logic before being put into the args
|
|
455
|
+
? from.args
|
|
456
|
+
? [{ ref: ['NODE_ID'] }, 'IN', { SELECT: { columns: [columnsIn.find(c => c.as === 'NODE_ID')], from, where: where } }]
|
|
457
|
+
: this.is_comparator?.({ xpr: where }) ?? true ? where : [...where, '=', { val: true, param: false }]
|
|
458
|
+
: [{ ref: ['PARENT_ID'] }, '=', { val: null }]
|
|
459
|
+
},
|
|
460
|
+
...distanceClause
|
|
461
|
+
]
|
|
462
|
+
}]
|
|
463
|
+
},
|
|
464
|
+
where: expandedFilter.length ? expandedFilter : undefined,
|
|
465
|
+
orderBy: [{ ref: ['HIERARCHY_RANK'], sort: 'asc' }],
|
|
466
|
+
groupBy: [{ ref: ['NODE_ID'] },{ ref: ['PARENT_ID'] }, { ref: ['HIERARCHY_RANK'] }, { ref: ['HIERARCHY_LEVEL'] }, { ref: ['HIERARCHY_TREE_SIZE'] }, ...columnsOut.filter(c => c.ref)],
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Only apply result join if the columns contain a references which doesn't start with the source alias
|
|
471
|
+
if (from.args && columns.find(c => c.ref?.[0] === alias)) {
|
|
472
|
+
graph.as = alias
|
|
473
|
+
return this.from(setStableFrom(from, graph))
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return `(${this.SELECT(graph)})${alias ? ` AS ${this.quote(alias)}` : ''} `
|
|
477
|
+
|
|
478
|
+
function collectDistanceTo(where, innot = false) {
|
|
479
|
+
for (let i = 0; i < where.length; i++) {
|
|
480
|
+
const c = where[i]
|
|
481
|
+
if (c === 'not') {
|
|
482
|
+
distanceType = 'DistanceFromRoot'
|
|
483
|
+
innot = true
|
|
484
|
+
}
|
|
485
|
+
else if (c.func === 'DistanceTo') {
|
|
486
|
+
const expr = c.args[0]
|
|
487
|
+
// { func: 'HIERARCHY_COMPOSITE_ID', args: nodeKeys.map(n => ({ val: cur[n] })) }
|
|
488
|
+
const to = c.args[1].val
|
|
489
|
+
const list = to === 1
|
|
490
|
+
? expandedByOne
|
|
491
|
+
: innot
|
|
492
|
+
? expandedByZero
|
|
493
|
+
: expandedByNr
|
|
494
|
+
|
|
495
|
+
if (!list._where) {
|
|
496
|
+
list._where = []
|
|
497
|
+
where.splice(i, 1,
|
|
498
|
+
...(to === 1
|
|
499
|
+
? [{ ref: ['PARENT_ID'] }, 'IN', list]
|
|
500
|
+
: [{ ref: ['NODE_ID'] }, 'IN', {
|
|
501
|
+
SELECT: {
|
|
502
|
+
_internal: true,
|
|
503
|
+
columns: [{ ref: ['NODE_ID'], element: { '@Core.Computed': true } }],
|
|
504
|
+
from: q.SELECT.from,
|
|
505
|
+
recurse: {
|
|
506
|
+
ref: recurse.ref,
|
|
507
|
+
where: list._where,
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
target,
|
|
511
|
+
}])
|
|
512
|
+
)
|
|
513
|
+
i += 2
|
|
514
|
+
} else {
|
|
515
|
+
// Remove current entry from where
|
|
516
|
+
if (where[i - 1] === 'not') {
|
|
517
|
+
where.splice(i - 2, 3)
|
|
518
|
+
i -= 3
|
|
519
|
+
} else {
|
|
520
|
+
where.splice(i - 1, 2)
|
|
521
|
+
i -= 2
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
list.list.push(expr)
|
|
525
|
+
list._where.push(c)
|
|
526
|
+
}
|
|
527
|
+
else if (c.ref?.[0] === 'DistanceFromRoot') {
|
|
528
|
+
distanceType = 'DistanceFromRoot'
|
|
529
|
+
where[i] = { ref: ['HIERARCHY_LEVEL'] }
|
|
530
|
+
i += 2
|
|
531
|
+
distanceVal = where[i]
|
|
532
|
+
where[i] = { val: where[i].val + 1 }
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function getStableFrom(from) {
|
|
538
|
+
if (from.args) return getStableFrom(from.args[0])
|
|
539
|
+
return from
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function setStableFrom(from, src) {
|
|
543
|
+
if (from.args) {
|
|
544
|
+
const ret = { ...from }
|
|
545
|
+
ret.args = [...ret.args]
|
|
546
|
+
ret.args[0] = setStableFrom(ret.args[0], src)
|
|
547
|
+
return ret
|
|
548
|
+
}
|
|
549
|
+
return src
|
|
550
|
+
}
|
|
263
551
|
}
|
|
264
552
|
|
|
265
553
|
/**
|
|
@@ -371,6 +659,18 @@ class CQN2SQLRenderer {
|
|
|
371
659
|
}
|
|
372
660
|
if (from.SELECT) return _aliased(`(${this.SELECT(from)})`)
|
|
373
661
|
if (from.join) return `${this.from(from.args[0])} ${from.join} JOIN ${this.from(from.args[1])}${from.on ? ` ON ${this.where(from.on)}` : ''}`
|
|
662
|
+
if (from.func) return _aliased(this.func(from))
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Renders a FROM clause into generic SQL
|
|
667
|
+
* @param {import('./infer/cqn').source} from
|
|
668
|
+
* @returns {string} SQL
|
|
669
|
+
*/
|
|
670
|
+
with(query) {
|
|
671
|
+
this._with ??= []
|
|
672
|
+
this._with.push(query)
|
|
673
|
+
return { ref: [query.as] }
|
|
374
674
|
}
|
|
375
675
|
|
|
376
676
|
/**
|
|
@@ -426,7 +726,7 @@ class CQN2SQLRenderer {
|
|
|
426
726
|
*/
|
|
427
727
|
orderBy(orderBy, localized) {
|
|
428
728
|
return orderBy.map(c => {
|
|
429
|
-
const o = localized
|
|
729
|
+
const o = (localized && this.context.locale)
|
|
430
730
|
? this.expr(c) +
|
|
431
731
|
(c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
|
|
432
732
|
(c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
|
|
@@ -489,7 +789,7 @@ class CQN2SQLRenderer {
|
|
|
489
789
|
? this.INSERT_rows(q)
|
|
490
790
|
: INSERT.values
|
|
491
791
|
? this.INSERT_values(q)
|
|
492
|
-
: INSERT.as
|
|
792
|
+
: INSERT.from || INSERT.as
|
|
493
793
|
? this.INSERT_select(q)
|
|
494
794
|
: cds.error`Missing .entries, .rows, or .values in ${q}`
|
|
495
795
|
}
|
|
@@ -695,7 +995,7 @@ class CQN2SQLRenderer {
|
|
|
695
995
|
c => c in elements && !elements[c].virtual && !elements[c].isAssociation,
|
|
696
996
|
))
|
|
697
997
|
this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns.map(c => this.quote(c))}) ${this.SELECT(
|
|
698
|
-
this.cqn4sql(INSERT.as),
|
|
998
|
+
this.cqn4sql(INSERT.from || INSERT.as),
|
|
699
999
|
)}`
|
|
700
1000
|
this.entries = [this.values]
|
|
701
1001
|
return this.sql
|
|
@@ -998,7 +1298,7 @@ class CQN2SQLRenderer {
|
|
|
998
1298
|
} else {
|
|
999
1299
|
cds.error`Invalid arguments provided for function '${func}' (${args})`
|
|
1000
1300
|
}
|
|
1001
|
-
const fn = this.class.Functions[func]?.apply(this
|
|
1301
|
+
const fn = this.class.Functions[func]?.apply(this, args) || `${func}(${args})`
|
|
1002
1302
|
if (xpr) return `${fn} ${this.xpr({ xpr })}`
|
|
1003
1303
|
return fn
|
|
1004
1304
|
}
|
package/lib/cqn4sql.js
CHANGED
|
@@ -802,11 +802,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
802
802
|
})
|
|
803
803
|
} else {
|
|
804
804
|
outerAlias = transformedQuery.SELECT.from.as
|
|
805
|
-
|
|
806
|
-
subqueryFromRef = [
|
|
807
|
-
...(transformedQuery.SELECT.from.ref || /* subq in from */ [getInnermostTarget(transformedQuery).name]),
|
|
808
|
-
...ref,
|
|
809
|
-
]
|
|
805
|
+
subqueryFromRef = [transformedQuery._target.name, ...ref]
|
|
810
806
|
}
|
|
811
807
|
|
|
812
808
|
// this is the alias of the column which holds the correlated subquery
|
package/lib/infer/index.js
CHANGED
|
@@ -46,7 +46,7 @@ function infer(originalQuery, model) {
|
|
|
46
46
|
|
|
47
47
|
let $combinedElements
|
|
48
48
|
|
|
49
|
-
const sources = inferTarget(_.
|
|
49
|
+
const sources = inferTarget(_.into || _.from || _.entity, {}) // IMPORTANT: _.into has to go before _.from for INSERT.into().from(SELECT)
|
|
50
50
|
const joinTree = new JoinTree(sources)
|
|
51
51
|
const aliases = Object.keys(sources)
|
|
52
52
|
const target = aliases.length === 1 ? getDefinitionFromSources(sources, aliases[0]) : originalQuery
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/db-service",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"generic-pool": "^3.9.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@sap/cds": ">=
|
|
30
|
+
"@sap/cds": ">=9"
|
|
31
31
|
},
|
|
32
|
-
"license": "
|
|
32
|
+
"license": "Apache-2.0"
|
|
33
33
|
}
|