@cap-js/db-service 1.19.1 → 2.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 +34 -0
- package/lib/InsertResults.js +3 -3
- package/lib/SQLService.js +59 -37
- package/lib/cql-functions.js +231 -4
- package/lib/cqn2sql.js +318 -20
- package/lib/cqn4sql.js +22 -77
- package/lib/infer/index.js +57 -55
- package/lib/infer/join-tree.js +1 -1
- package/lib/utils.js +76 -5
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +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
|
+
## [2.0.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.20.0...db-service-v2.0.0) (2025-05-07)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### ⚠ BREAKING CHANGES
|
|
11
|
+
|
|
12
|
+
* update peer dependency to @sap/cds@9 ([#1178](https://github.com/cap-js/cds-dbs/issues/1178))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
* 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))
|
|
18
|
+
* current_utctimestamp as default ([#1161](https://github.com/cap-js/cds-dbs/issues/1161)) ([7c6b2f5](https://github.com/cap-js/cds-dbs/commit/7c6b2f5a6837afbeb1e24daef9a49e25cf7e92f0))
|
|
19
|
+
* 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))
|
|
20
|
+
* 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))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
* 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))
|
|
26
|
+
|
|
27
|
+
## [1.20.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.19.1...db-service-v1.20.0) (2025-04-17)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
* Result set streaming ([#702](https://github.com/cap-js/cds-dbs/issues/702)) ([2fe02ea](https://github.com/cap-js/cds-dbs/commit/2fe02eafd02993e5697efbdab90ad997fb2c9e00))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
* **`expand`:** proper subquery `from` construction ([#1126](https://github.com/cap-js/cds-dbs/issues/1126)) ([e343e79](https://github.com/cap-js/cds-dbs/commit/e343e7978acc0c181a012f61b6181b7c558aa178)), closes [#1114](https://github.com/cap-js/cds-dbs/issues/1114) [#1112](https://github.com/cap-js/cds-dbs/issues/1112)
|
|
38
|
+
* Improved support for special characters in column names ([#1141](https://github.com/cap-js/cds-dbs/issues/1141)) ([ba04697](https://github.com/cap-js/cds-dbs/commit/ba046971921d645e8571a80c27ef07988c8c01ad))
|
|
39
|
+
* **infer:** for localized queries, use `localized.<entity>` as `_target` ([#1140](https://github.com/cap-js/cds-dbs/issues/1140)) ([b08707b](https://github.com/cap-js/cds-dbs/commit/b08707b76a53c74f1c4388a8be4d0818506388c5))
|
|
40
|
+
|
|
7
41
|
## [1.19.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.19.0...db-service-v1.19.1) (2025-04-01)
|
|
8
42
|
|
|
9
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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const cds = require('@sap/cds'),
|
|
2
2
|
DEBUG = cds.debug('sql|db')
|
|
3
|
-
const { Readable } = require('stream')
|
|
3
|
+
const { Readable, Transform } = require('stream')
|
|
4
|
+
const { pipeline } = require('stream/promises')
|
|
4
5
|
const { resolveView, getDBTable, getTransition } = require('@sap/cds/libx/_runtime/common/utils/resolveView')
|
|
5
6
|
const DatabaseService = require('./common/DatabaseService')
|
|
6
7
|
const cqn4sql = require('./cqn4sql')
|
|
@@ -117,7 +118,7 @@ class SQLService extends DatabaseService {
|
|
|
117
118
|
* Handler for SELECT
|
|
118
119
|
* @type {Handler}
|
|
119
120
|
*/
|
|
120
|
-
async onSELECT({ query, data }) {
|
|
121
|
+
async onSELECT({ query, data, iterator, objectMode }) {
|
|
121
122
|
// REVISIT: for custom joins, infer is called twice, which is bad
|
|
122
123
|
// --> make cds.infer properly work with custom joins and remove this
|
|
123
124
|
if (!(query._target instanceof cds.entity)) {
|
|
@@ -131,35 +132,56 @@ class SQLService extends DatabaseService {
|
|
|
131
132
|
const { sql, values, cqn } = this.cqn2sql(query, data)
|
|
132
133
|
const expand = query.SELECT.expand
|
|
133
134
|
delete query.SELECT.expand
|
|
135
|
+
const isOne = cqn.SELECT.one || query.SELECT.from?.ref?.[0].cardinality?.max === 1
|
|
134
136
|
|
|
135
137
|
let ps = await this.prepare(sql)
|
|
136
|
-
let rows = await ps.all(values)
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
138
|
+
let rows = iterator ? await ps.stream(values, isOne, objectMode) : await ps.all(values)
|
|
139
|
+
try {
|
|
140
|
+
if (rows.length)
|
|
141
|
+
if (expand) rows = rows.map(r => (typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r))
|
|
142
|
+
|
|
143
|
+
if (!iterator) {
|
|
144
|
+
// REVISIT: remove after removing stream_compat feature flag
|
|
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
|
+
}
|
|
161
|
+
} else if (objectMode) {
|
|
162
|
+
const converter = (row) => this._changeToStreams(cqn.SELECT.columns, row, true)
|
|
163
|
+
const changeToStreams = new Transform({
|
|
164
|
+
objectMode: true,
|
|
165
|
+
transform(row, enc, cb) {
|
|
166
|
+
converter(row)
|
|
167
|
+
cb(null, row)
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
pipeline(rows, changeToStreams)
|
|
171
|
+
rows = changeToStreams
|
|
152
172
|
}
|
|
153
|
-
} else {
|
|
154
|
-
this._changeToStreams(cqn.SELECT.columns, rows, query.SELECT.one, false)
|
|
155
|
-
}
|
|
156
173
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
174
|
+
if (cqn.SELECT.count) {
|
|
175
|
+
// REVISIT: the runtime always expects that the count is preserved with .map, required for renaming in mocks
|
|
176
|
+
return SQLService._arrayWithCount(rows, await this.count(query, rows))
|
|
177
|
+
}
|
|
161
178
|
|
|
162
|
-
|
|
179
|
+
return iterator !== false && isOne ? rows[0] : rows
|
|
180
|
+
} catch (err) {
|
|
181
|
+
// Ensure that iterators receive pre stream errors
|
|
182
|
+
if (iterator) rows.emit('error', err)
|
|
183
|
+
throw err
|
|
184
|
+
}
|
|
163
185
|
}
|
|
164
186
|
|
|
165
187
|
/**
|
|
@@ -314,7 +336,7 @@ class SQLService extends DatabaseService {
|
|
|
314
336
|
* @returns {Promise<number>}
|
|
315
337
|
*/
|
|
316
338
|
async count(query, ret) {
|
|
317
|
-
if (ret) {
|
|
339
|
+
if (ret?.length) {
|
|
318
340
|
const { one, limit: _ } = query.SELECT,
|
|
319
341
|
n = ret.length
|
|
320
342
|
const [max, offset = 0] = one ? [1] : _ ? [_.rows?.val, _.offset?.val] : []
|
|
@@ -355,6 +377,7 @@ class SQLService extends DatabaseService {
|
|
|
355
377
|
// preserves $count for .map calls on array
|
|
356
378
|
static _arrayWithCount = function (a, count) {
|
|
357
379
|
const _map = a.map
|
|
380
|
+
|
|
358
381
|
const map = function (..._) {
|
|
359
382
|
return SQLService._arrayWithCount(_map.call(a, ..._), count)
|
|
360
383
|
}
|
|
@@ -472,17 +495,16 @@ class PreparedStatement {
|
|
|
472
495
|
}
|
|
473
496
|
SQLService.prototype.PreparedStatement = PreparedStatement
|
|
474
497
|
|
|
498
|
+
/** @param {import('@sap/cds').ql.Query} q */
|
|
475
499
|
const _target_name4 = q => {
|
|
476
|
-
const target =
|
|
477
|
-
q.
|
|
478
|
-
q.
|
|
479
|
-
q.
|
|
480
|
-
q.
|
|
481
|
-
q.
|
|
482
|
-
q.
|
|
483
|
-
q.
|
|
484
|
-
q.CREATE?.entity ||
|
|
485
|
-
q.DROP?.entity
|
|
500
|
+
const target = q._subject
|
|
501
|
+
|| q.SELECT?.from
|
|
502
|
+
|| q.INSERT?.into
|
|
503
|
+
|| q.UPSERT?.into
|
|
504
|
+
|| q.UPDATE?.entity
|
|
505
|
+
|| q.DELETE?.from
|
|
506
|
+
|| q.CREATE?.entity
|
|
507
|
+
|| q.DROP?.entity
|
|
486
508
|
if (target?.SET?.op === 'union') throw new cds.error('UNION-based queries are not supported')
|
|
487
509
|
if (!target?.ref) return target
|
|
488
510
|
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]
|