@cap-js/db-service 1.20.0 → 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 +20 -0
- package/lib/InsertResults.js +3 -3
- package/lib/SQLService.js +9 -9
- package/lib/cql-functions.js +231 -4
- package/lib/cqn2sql.js +305 -7
- package/lib/cqn4sql.js +9 -18
- package/lib/infer/index.js +33 -34
- package/lib/utils.js +9 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@
|
|
|
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
|
+
|
|
7
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)
|
|
8
28
|
|
|
9
29
|
|
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
|
@@ -495,16 +495,16 @@ class PreparedStatement {
|
|
|
495
495
|
}
|
|
496
496
|
SQLService.prototype.PreparedStatement = PreparedStatement
|
|
497
497
|
|
|
498
|
+
/** @param {import('@sap/cds').ql.Query} q */
|
|
498
499
|
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
|
|
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
|
|
508
508
|
if (target?.SET?.op === 'union') throw new cds.error('UNION-based queries are not supported')
|
|
509
509
|
if (!target?.ref) return target
|
|
510
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]
|
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,273 @@ 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(this.quote(fk.childElement.name))
|
|
342
|
+
parentKeys.push(this.quote(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
|
+
func: 'HIERARCHY',
|
|
369
|
+
args: [{ xpr: ['SOURCE', { SELECT: { columns: columnsIn, from: stableFrom } }, ...(orderBy ? ['SIBLING', 'ORDER', 'BY', `${this.orderBy(orderBy)}`] : [])] }],
|
|
370
|
+
as: alias
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
const expandedByNr = { list: [] } // DistanceTo(...,null)
|
|
374
|
+
const expandedByOne = { list: [] } // DistanceTo(...,1)
|
|
375
|
+
const expandedByZero = { list: [] } // not DistanceTo(...,null)
|
|
376
|
+
let expandedFilter = []
|
|
377
|
+
let distanceType = 'DistanceFromRoot'
|
|
378
|
+
let distanceVal
|
|
379
|
+
|
|
380
|
+
if (recurse.where) {
|
|
381
|
+
distanceType = 'Distance'
|
|
382
|
+
if (recurse.where[0] === 'and') recurse.where = recurse.where.slice(1)
|
|
383
|
+
expandedFilter = [...recurse.where]
|
|
384
|
+
collectDistanceTo(expandedFilter)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const direction = where?.length ? 'ANCESTORS' : 'DESCENDANTS'
|
|
388
|
+
// Ensure that the distance value is being computed
|
|
389
|
+
if (distanceType) addComputedColumn(distanceType)
|
|
390
|
+
|
|
391
|
+
let distanceClause = []
|
|
392
|
+
if (distanceType === 'Distance') {
|
|
393
|
+
const isOne = expandedByOne.list.length
|
|
394
|
+
distanceClause = ['DISTANCE', ...(
|
|
395
|
+
isOne
|
|
396
|
+
? [{ val: 1 }]
|
|
397
|
+
: ['FROM', { val: 1 }]
|
|
398
|
+
)]
|
|
399
|
+
where = [{ ref: ['NODE_ID'] }, 'IN', isOne ? expandedByOne : expandedByNr]
|
|
400
|
+
expandedFilter = []
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
availableComputedColumns.DrillState = {
|
|
404
|
+
xpr: [ // When the node doesn't have children make it a leaf
|
|
405
|
+
'CASE', 'WHEN', { ref: ['HIERARCHY_TREE_SIZE'] }, '=', { val: 1, param: false }, 'THEN', { val: 'leaf', param: false },
|
|
406
|
+
...(where?.length // When there is a where filter the final node will always be a leaf
|
|
407
|
+
? ['WHEN', { func: where?.length ? 'min' : 'max', args: [{ ref: ['HIERARCHY_DISTANCE'] }] }, '=', { val: 0, param: false }, 'THEN', { val: 'leaf', param: false }]
|
|
408
|
+
: []
|
|
409
|
+
), // When having expanded by 0 level nodes make sure they are collapsed
|
|
410
|
+
...(expandedByZero.list.length
|
|
411
|
+
? ['WHEN', { ref: ['NODE_ID'] }, 'IN', expandedByZero, 'THEN', { val: 'collapsed', param: false }]
|
|
412
|
+
: []
|
|
413
|
+
), // When having expanded by null or one nodes compute them as expanded
|
|
414
|
+
...(expandedByNr.list.length || expandedByOne.list.length
|
|
415
|
+
? ['WHEN', { ref: ['NODE_ID'] }, 'IN', { list: [...expandedByNr.list, ...expandedByOne.list] }, 'THEN', { val: 'expanded', param: false }]
|
|
416
|
+
: []
|
|
417
|
+
), // When having expanded by one level node make its children collapsed
|
|
418
|
+
...(expandedByOne.list.length
|
|
419
|
+
? ['WHEN', { ref: ['PARENT_ID'] }, 'IN', expandedByOne, 'THEN', { val: 'collapsed', param: false }]
|
|
420
|
+
: []
|
|
421
|
+
), // When using DistanceFromRoot compute all entries within the levels as expanded
|
|
422
|
+
...(distanceType === 'DistanceFromRoot' && distanceVal
|
|
423
|
+
? [
|
|
424
|
+
'WHEN', { ref: ['HIERARCHY_LEVEL'] }, '<>', { val: distanceVal.val + 1 },
|
|
425
|
+
'THEN', { val: 'expanded', param: false },
|
|
426
|
+
]
|
|
427
|
+
: []
|
|
428
|
+
), // Default to expanded when default filter behavior is truthy
|
|
429
|
+
'ELSE', { val: (recurse.where && !expandedByZero.list.length) && distanceType ? 'collapsed' : 'expanded', param: false },
|
|
430
|
+
'END',
|
|
431
|
+
],
|
|
432
|
+
as: 'DrillState'
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
for (const name in requiredComputedColumns) {
|
|
436
|
+
const def = availableComputedColumns[name]
|
|
437
|
+
if (def) columnsOut.push(def)
|
|
438
|
+
}
|
|
439
|
+
if (_internal) columnsOut.push({ ref: ['NODE_ID'] })
|
|
440
|
+
|
|
441
|
+
const graph = distanceType === 'DistanceFromRoot' && !where
|
|
442
|
+
? { SELECT: { columns: columnsOut, from: source(), where: expandedFilter } }
|
|
443
|
+
: {
|
|
444
|
+
SELECT: {
|
|
445
|
+
columns: columnsOut,
|
|
446
|
+
from: {
|
|
447
|
+
func: `HIERARCHY_${direction}`,
|
|
448
|
+
args: [{
|
|
449
|
+
xpr: [
|
|
450
|
+
'SOURCE', source(), 'AS', this.quote(alias),
|
|
451
|
+
'START', 'WHERE', {
|
|
452
|
+
xpr: where // Requires special where logic before being put into the args
|
|
453
|
+
? from.args
|
|
454
|
+
? [{ ref: ['NODE_ID'] }, 'IN', { SELECT: { columns: [columnsIn.find(c => c.as === 'NODE_ID')], from, where: where } }]
|
|
455
|
+
: this.is_comparator?.({ xpr: where }) ?? true ? where : [...where, '=', { val: true, param: false }]
|
|
456
|
+
: [{ ref: ['PARENT_ID'] }, '=', { val: null }]
|
|
457
|
+
},
|
|
458
|
+
...distanceClause
|
|
459
|
+
]
|
|
460
|
+
}]
|
|
461
|
+
},
|
|
462
|
+
where: expandedFilter.length ? expandedFilter : undefined,
|
|
463
|
+
orderBy: [{ ref: ['HIERARCHY_RANK'], sort: 'asc' }],
|
|
464
|
+
groupBy: [{ ref: ['NODE_ID'] },{ ref: ['PARENT_ID'] }, { ref: ['HIERARCHY_RANK'] }, { ref: ['HIERARCHY_LEVEL'] }, { ref: ['HIERARCHY_TREE_SIZE'] }, ...columnsOut.filter(c => c.ref)],
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Only apply result join if the columns contain a references which doesn't start with the source alias
|
|
469
|
+
if (from.args && columns.find(c => c.ref?.[0] === alias)) {
|
|
470
|
+
graph.as = alias
|
|
471
|
+
return this.from(setStableFrom(from, graph))
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return `(${this.SELECT(graph)})${alias ? ` AS ${this.quote(alias)}` : ''} `
|
|
475
|
+
|
|
476
|
+
function collectDistanceTo(where, innot = false) {
|
|
477
|
+
for (let i = 0; i < where.length; i++) {
|
|
478
|
+
const c = where[i]
|
|
479
|
+
if (c === 'not') {
|
|
480
|
+
distanceType = 'DistanceFromRoot'
|
|
481
|
+
innot = true
|
|
482
|
+
}
|
|
483
|
+
else if (c.func === 'DistanceTo') {
|
|
484
|
+
const expr = c.args[0]
|
|
485
|
+
// { func: 'HIERARCHY_COMPOSITE_ID', args: nodeKeys.map(n => ({ val: cur[n] })) }
|
|
486
|
+
const to = c.args[1].val
|
|
487
|
+
const list = to === 1
|
|
488
|
+
? expandedByOne
|
|
489
|
+
: innot
|
|
490
|
+
? expandedByZero
|
|
491
|
+
: expandedByNr
|
|
492
|
+
|
|
493
|
+
if (!list._where) {
|
|
494
|
+
list._where = []
|
|
495
|
+
where.splice(i, 1,
|
|
496
|
+
...(to === 1
|
|
497
|
+
? [{ ref: ['PARENT_ID'] }, 'IN', list]
|
|
498
|
+
: [{ ref: ['NODE_ID'] }, 'IN', {
|
|
499
|
+
SELECT: {
|
|
500
|
+
_internal: true,
|
|
501
|
+
columns: [{ ref: ['NODE_ID'], element: { '@Core.Computed': true } }],
|
|
502
|
+
from: q.SELECT.from,
|
|
503
|
+
recurse: {
|
|
504
|
+
ref: recurse.ref,
|
|
505
|
+
where: list._where,
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
target,
|
|
509
|
+
}])
|
|
510
|
+
)
|
|
511
|
+
i += 2
|
|
512
|
+
} else {
|
|
513
|
+
// Remove current entry from where
|
|
514
|
+
if (where[i - 1] === 'not') {
|
|
515
|
+
where.splice(i - 2, 3)
|
|
516
|
+
i -= 3
|
|
517
|
+
} else {
|
|
518
|
+
where.splice(i - 1, 2)
|
|
519
|
+
i -= 2
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
list.list.push(expr)
|
|
523
|
+
list._where.push(c)
|
|
524
|
+
}
|
|
525
|
+
else if (c.ref?.[0] === 'DistanceFromRoot') {
|
|
526
|
+
distanceType = 'DistanceFromRoot'
|
|
527
|
+
where[i] = { ref: ['HIERARCHY_LEVEL'] }
|
|
528
|
+
i += 2
|
|
529
|
+
distanceVal = where[i]
|
|
530
|
+
where[i] = { val: where[i].val + 1 }
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function getStableFrom(from) {
|
|
536
|
+
if (from.args) return getStableFrom(from.args[0])
|
|
537
|
+
return from
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function setStableFrom(from, src) {
|
|
541
|
+
if (from.args) {
|
|
542
|
+
const ret = { ...from }
|
|
543
|
+
ret.args = [...ret.args]
|
|
544
|
+
ret.args[0] = setStableFrom(ret.args[0], src)
|
|
545
|
+
return ret
|
|
546
|
+
}
|
|
547
|
+
return src
|
|
548
|
+
}
|
|
263
549
|
}
|
|
264
550
|
|
|
265
551
|
/**
|
|
@@ -371,6 +657,18 @@ class CQN2SQLRenderer {
|
|
|
371
657
|
}
|
|
372
658
|
if (from.SELECT) return _aliased(`(${this.SELECT(from)})`)
|
|
373
659
|
if (from.join) return `${this.from(from.args[0])} ${from.join} JOIN ${this.from(from.args[1])}${from.on ? ` ON ${this.where(from.on)}` : ''}`
|
|
660
|
+
if (from.func) return _aliased(this.func(from))
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Renders a FROM clause into generic SQL
|
|
665
|
+
* @param {import('./infer/cqn').source} from
|
|
666
|
+
* @returns {string} SQL
|
|
667
|
+
*/
|
|
668
|
+
with(query) {
|
|
669
|
+
this._with ??= []
|
|
670
|
+
this._with.push(query)
|
|
671
|
+
return { ref: [query.as] }
|
|
374
672
|
}
|
|
375
673
|
|
|
376
674
|
/**
|
|
@@ -489,7 +787,7 @@ class CQN2SQLRenderer {
|
|
|
489
787
|
? this.INSERT_rows(q)
|
|
490
788
|
: INSERT.values
|
|
491
789
|
? this.INSERT_values(q)
|
|
492
|
-
: INSERT.as
|
|
790
|
+
: INSERT.from || INSERT.as
|
|
493
791
|
? this.INSERT_select(q)
|
|
494
792
|
: cds.error`Missing .entries, .rows, or .values in ${q}`
|
|
495
793
|
}
|
|
@@ -695,7 +993,7 @@ class CQN2SQLRenderer {
|
|
|
695
993
|
c => c in elements && !elements[c].virtual && !elements[c].isAssociation,
|
|
696
994
|
))
|
|
697
995
|
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),
|
|
996
|
+
this.cqn4sql(INSERT.from || INSERT.as),
|
|
699
997
|
)}`
|
|
700
998
|
this.entries = [this.values]
|
|
701
999
|
return this.sql
|
|
@@ -998,7 +1296,7 @@ class CQN2SQLRenderer {
|
|
|
998
1296
|
} else {
|
|
999
1297
|
cds.error`Invalid arguments provided for function '${func}' (${args})`
|
|
1000
1298
|
}
|
|
1001
|
-
const fn = this.class.Functions[func]?.apply(this
|
|
1299
|
+
const fn = this.class.Functions[func]?.apply(this, args) || `${func}(${args})`
|
|
1002
1300
|
if (xpr) return `${fn} ${this.xpr({ xpr })}`
|
|
1003
1301
|
return fn
|
|
1004
1302
|
}
|
|
@@ -1102,7 +1400,7 @@ class CQN2SQLRenderer {
|
|
|
1102
1400
|
|
|
1103
1401
|
let onInsert = this.managed_session_context(element[cdsOnInsert]?.['='])
|
|
1104
1402
|
|| this.managed_session_context(element.default?.ref?.[0])
|
|
1105
|
-
|| (element.default
|
|
1403
|
+
|| (element.default && { __proto__: element.default, param: false })
|
|
1106
1404
|
let onUpdate = this.managed_session_context(element[cdsOnUpdate]?.['='])
|
|
1107
1405
|
|
|
1108
1406
|
if (onInsert) onInsert = this.expr(onInsert)
|
package/lib/cqn4sql.js
CHANGED
|
@@ -5,7 +5,7 @@ cds.infer.target ??= q => q._target || q.target // instanceof cds.entity ? q._ta
|
|
|
5
5
|
|
|
6
6
|
const infer = require('./infer')
|
|
7
7
|
const { computeColumnsToBeSearched } = require('./search')
|
|
8
|
-
const { prettyPrintRef, isCalculatedOnRead, isCalculatedElement, getImplicitAlias, getModelUtils } = require('./utils')
|
|
8
|
+
const { prettyPrintRef, isCalculatedOnRead, isCalculatedElement, getImplicitAlias, defineProperty, getModelUtils } = require('./utils')
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* For operators of <eqOps>, this is replaced by comparing all leaf elements with null, combined with and.
|
|
@@ -845,10 +845,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
845
845
|
}
|
|
846
846
|
const expanded = transformSubquery(subquery)
|
|
847
847
|
const correlated = _correlate({ ...expanded, as: columnAlias }, outerAlias)
|
|
848
|
-
|
|
849
|
-
value: expanded.elements,
|
|
850
|
-
writable: true,
|
|
851
|
-
})
|
|
848
|
+
defineProperty(correlated, 'elements', expanded.elements)
|
|
852
849
|
return correlated
|
|
853
850
|
|
|
854
851
|
function _correlate(subq, outer) {
|
|
@@ -1070,7 +1067,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1070
1067
|
else {
|
|
1071
1068
|
const outerQueries = inferred.outerQueries || []
|
|
1072
1069
|
outerQueries.push(inferred)
|
|
1073
|
-
|
|
1070
|
+
defineProperty(q, 'outerQueries', outerQueries)
|
|
1074
1071
|
}
|
|
1075
1072
|
const target = cds.infer.target(inferred) // REVISIT: we should reliably use inferred._target instead
|
|
1076
1073
|
if (isLocalized(target)) q.SELECT.localized = true
|
|
@@ -1084,7 +1081,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1084
1081
|
getImplicitAlias(last.id || last),
|
|
1085
1082
|
inferred.outerQueries,
|
|
1086
1083
|
)
|
|
1087
|
-
|
|
1084
|
+
defineProperty(q.SELECT.from, 'uniqueSubqueryAlias', uniqueSubqueryAlias)
|
|
1088
1085
|
}
|
|
1089
1086
|
}
|
|
1090
1087
|
|
|
@@ -1311,7 +1308,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1311
1308
|
const flatForeignKey = getDefinition(element.parent.name)?.elements[fkBaseName]
|
|
1312
1309
|
|
|
1313
1310
|
setElementOnColumns(flatColumn, flatForeignKey || fkElement)
|
|
1314
|
-
|
|
1311
|
+
defineProperty(flatColumn, '_csnPath', csnPath)
|
|
1315
1312
|
flatColumns.push(flatColumn)
|
|
1316
1313
|
}
|
|
1317
1314
|
}
|
|
@@ -1343,7 +1340,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1343
1340
|
if (column.sort) flatRef.sort = column.sort
|
|
1344
1341
|
if (columnAlias) flatRef.as = columnAlias
|
|
1345
1342
|
setElementOnColumns(flatRef, element)
|
|
1346
|
-
|
|
1343
|
+
defineProperty(flatRef, '_csnPath', csnPath)
|
|
1347
1344
|
return [flatRef]
|
|
1348
1345
|
|
|
1349
1346
|
function getReplacement(from) {
|
|
@@ -1676,7 +1673,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1676
1673
|
const transformedWhere = []
|
|
1677
1674
|
let transformedFrom = copy(from) // REVISIT: too expensive!
|
|
1678
1675
|
if (from.$refLinks)
|
|
1679
|
-
|
|
1676
|
+
defineProperty(transformedFrom, '$refLinks', [...from.$refLinks])
|
|
1680
1677
|
if (from.args) {
|
|
1681
1678
|
transformedFrom.args = []
|
|
1682
1679
|
from.args.forEach(arg => {
|
|
@@ -1924,10 +1921,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1924
1921
|
const refLinkFaker = thing => {
|
|
1925
1922
|
const { ref } = thing
|
|
1926
1923
|
const assocHost = getParentEntity(assocRefLink.definition)
|
|
1927
|
-
|
|
1928
|
-
value: [],
|
|
1929
|
-
writable: true,
|
|
1930
|
-
})
|
|
1924
|
+
defineProperty(thing, '$refLinks', [])
|
|
1931
1925
|
let pseudoPath = false
|
|
1932
1926
|
ref.reduce((prev, res, i) => {
|
|
1933
1927
|
if (res === '$self') {
|
|
@@ -2370,10 +2364,7 @@ function getParentEntity(element) {
|
|
|
2370
2364
|
* @param {csn.Element} element
|
|
2371
2365
|
*/
|
|
2372
2366
|
function setElementOnColumns(col, element) {
|
|
2373
|
-
|
|
2374
|
-
value: element,
|
|
2375
|
-
writable: true,
|
|
2376
|
-
})
|
|
2367
|
+
defineProperty(col, 'element', element)
|
|
2377
2368
|
}
|
|
2378
2369
|
|
|
2379
2370
|
const getName = col => col.as || col.ref?.at(-1)
|
package/lib/infer/index.js
CHANGED
|
@@ -4,7 +4,7 @@ const cds = require('@sap/cds')
|
|
|
4
4
|
|
|
5
5
|
const JoinTree = require('./join-tree')
|
|
6
6
|
const { pseudos } = require('./pseudos')
|
|
7
|
-
const { isCalculatedOnRead, getImplicitAlias, getModelUtils } = require('../utils')
|
|
7
|
+
const { isCalculatedOnRead, getImplicitAlias, getModelUtils, defineProperty } = require('../utils')
|
|
8
8
|
const cdsTypes = cds.linked({
|
|
9
9
|
definitions: {
|
|
10
10
|
Timestamp: { type: 'cds.Timestamp' },
|
|
@@ -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
|
|
@@ -75,7 +75,7 @@ function infer(originalQuery, model) {
|
|
|
75
75
|
joinTree: { value: joinTree, writable: true, configurable: true }, // REVISIT: eliminate
|
|
76
76
|
})
|
|
77
77
|
// also enrich original query -> writable because it may be inferred again
|
|
78
|
-
|
|
78
|
+
defineProperty(originalQuery, 'elements', elements)
|
|
79
79
|
}
|
|
80
80
|
return inferred
|
|
81
81
|
|
|
@@ -171,10 +171,7 @@ function infer(originalQuery, model) {
|
|
|
171
171
|
* @param {csn.Element} element
|
|
172
172
|
*/
|
|
173
173
|
function setElementOnColumns(col, element) {
|
|
174
|
-
|
|
175
|
-
value: element,
|
|
176
|
-
writable: true,
|
|
177
|
-
})
|
|
174
|
+
defineProperty(col, 'element', element)
|
|
178
175
|
}
|
|
179
176
|
|
|
180
177
|
/**
|
|
@@ -245,7 +242,7 @@ function infer(originalQuery, model) {
|
|
|
245
242
|
// link $refLinks -> special name resolution rules for orderBy
|
|
246
243
|
orderBy.forEach(token => {
|
|
247
244
|
let $baseLink
|
|
248
|
-
let
|
|
245
|
+
let needsElementsOfQueryAsBase
|
|
249
246
|
// first check if token ref is resolvable in query elements
|
|
250
247
|
if (columns) {
|
|
251
248
|
const firstStep = token.ref?.[0].id || token.ref?.[0]
|
|
@@ -253,14 +250,11 @@ function infer(originalQuery, model) {
|
|
|
253
250
|
const columnName = c.as || c.flatName || c.ref?.at(-1).id || c.ref?.at(-1) || c.func
|
|
254
251
|
return columnName === firstStep
|
|
255
252
|
})
|
|
256
|
-
|
|
253
|
+
needsElementsOfQueryAsBase =
|
|
257
254
|
tokenPointsToQueryElements &&
|
|
258
|
-
queryElements[
|
|
259
|
-
/* expand on structure can be addressed */ !queryElements[
|
|
255
|
+
queryElements[firstStep] &&
|
|
256
|
+
/* expand on structure can be addressed */ !queryElements[firstStep].$assocExpand
|
|
260
257
|
|
|
261
|
-
// if the ref points into the query itself and follows an exposed association
|
|
262
|
-
// to a non-fk column, we must reject the ref, as we can't join with the queries own results
|
|
263
|
-
rejectJoinRelevantPath = needsElementsOfQueryAsBase
|
|
264
258
|
if (needsElementsOfQueryAsBase) $baseLink = { definition: { elements: queryElements }, target: inferred }
|
|
265
259
|
} else {
|
|
266
260
|
// fallback to elements of query source
|
|
@@ -268,7 +262,9 @@ function infer(originalQuery, model) {
|
|
|
268
262
|
}
|
|
269
263
|
|
|
270
264
|
inferArg(token, queryElements, $baseLink, { inQueryModifier: true })
|
|
271
|
-
if
|
|
265
|
+
// if the ref points into the query itself and follows an exposed association
|
|
266
|
+
// to a non-fk column, we must reject the ref, as we can't join with the queries own results
|
|
267
|
+
if (token.isJoinRelevant && needsElementsOfQueryAsBase) {
|
|
272
268
|
// reverse the array, find the last association and calculate the index of the association in non-reversed order
|
|
273
269
|
const assocIndex =
|
|
274
270
|
token.$refLinks.length - 1 - token.$refLinks.reverse().findIndex(link => link.definition.isAssociation)
|
|
@@ -406,18 +402,22 @@ function infer(originalQuery, model) {
|
|
|
406
402
|
if (arg.param || arg.SELECT) return // parameter references are only resolved into values on execution e.g. :val, :1 or ?
|
|
407
403
|
if (arg.args) applyToFunctionArgs(arg.args, inferArg, [null, $baseLink, context])
|
|
408
404
|
if (arg.list) arg.list.forEach(arg => inferArg(arg, null, $baseLink, context))
|
|
409
|
-
if (arg.xpr)
|
|
405
|
+
if (arg.xpr)
|
|
406
|
+
arg.xpr.forEach((token, i) =>
|
|
407
|
+
inferArg(token, queryElements, $baseLink, { ...context, inXpr: true, inExists: arg.xpr[i - 1] === 'exists' }),
|
|
408
|
+
) // e.g. function in expression
|
|
410
409
|
|
|
411
410
|
if (!arg.ref) {
|
|
412
411
|
if (arg.expand && queryElements) queryElements[arg.as] = resolveExpand(arg)
|
|
413
412
|
return
|
|
414
413
|
}
|
|
415
414
|
|
|
416
|
-
//
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
415
|
+
// Before the arg is linked, it's meta information should be cleaned up.
|
|
416
|
+
// This may be important if one manipulates the arg object
|
|
417
|
+
// __after__ a query has been fired and re-uses the manipulated query
|
|
418
|
+
defineProperty(arg, '$refLinks', [])
|
|
419
|
+
defineProperty(arg, 'isJoinRelevant', false)
|
|
420
|
+
|
|
421
421
|
// if any path step points to an artifact with `@cds.persistence.skip`
|
|
422
422
|
// we must ignore the element from the queries elements
|
|
423
423
|
let isPersisted = true
|
|
@@ -428,7 +428,7 @@ function infer(originalQuery, model) {
|
|
|
428
428
|
expandOnTableAlias = arg.ref.length === 1 && arg.ref[0] in sources && (arg.expand || arg.inline)
|
|
429
429
|
}
|
|
430
430
|
if (dollarSelfRefs && firstStepIsSelf) {
|
|
431
|
-
|
|
431
|
+
defineProperty(arg, 'inXpr', true)
|
|
432
432
|
dollarSelfRefs.push(arg)
|
|
433
433
|
return
|
|
434
434
|
}
|
|
@@ -455,7 +455,7 @@ function infer(originalQuery, model) {
|
|
|
455
455
|
const nextStep = arg.ref[1]?.id || arg.ref[1]
|
|
456
456
|
if (isNonForeignKeyNavigation(element, nextStep)) {
|
|
457
457
|
if (inExists) {
|
|
458
|
-
|
|
458
|
+
defineProperty($baseLink, 'pathExpressionInsideFilter', true)
|
|
459
459
|
} else {
|
|
460
460
|
rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
|
|
461
461
|
}
|
|
@@ -519,7 +519,7 @@ function infer(originalQuery, model) {
|
|
|
519
519
|
const nextStep = arg.ref[i + 1]?.id || arg.ref[i + 1]
|
|
520
520
|
if (isNonForeignKeyNavigation(element, nextStep)) {
|
|
521
521
|
if (inExists) {
|
|
522
|
-
|
|
522
|
+
defineProperty($baseLink, 'pathExpressionInsideFilter', true)
|
|
523
523
|
} else {
|
|
524
524
|
rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
|
|
525
525
|
}
|
|
@@ -535,7 +535,7 @@ function infer(originalQuery, model) {
|
|
|
535
535
|
} else if (id === '$dummy') {
|
|
536
536
|
// `some.known.element.$dummy` -> no error; used by cds.ql to simulate joins
|
|
537
537
|
arg.$refLinks.push({ definition: { name: '$dummy', parent: arg.$refLinks[i - 1].target } })
|
|
538
|
-
|
|
538
|
+
defineProperty(arg, 'isJoinRelevant', true)
|
|
539
539
|
} else {
|
|
540
540
|
const notFoundIn = pseudoPath ? arg.ref[i - 1] : getFullPathForLinkedArg(arg)
|
|
541
541
|
stepNotFoundInPredecessor(id, notFoundIn)
|
|
@@ -561,7 +561,7 @@ function infer(originalQuery, model) {
|
|
|
561
561
|
const definition = arg.$refLinks[i].definition
|
|
562
562
|
if ((!definition.target && definition.kind !== 'entity') || (!inFrom && danglingFilter))
|
|
563
563
|
throw new Error('A filter can only be provided when navigating along associations')
|
|
564
|
-
if (!inFrom && !arg.expand)
|
|
564
|
+
if (!inFrom && !arg.expand)defineProperty(arg, 'isJoinRelevant', true)
|
|
565
565
|
let skipJoinsForFilter = false
|
|
566
566
|
step.where.forEach(token => {
|
|
567
567
|
if (token === 'exists') {
|
|
@@ -590,7 +590,7 @@ function infer(originalQuery, model) {
|
|
|
590
590
|
if (getDefinition(arg.$refLinks[i].definition.target)?.['@cds.persistence.skip'] === true) isPersisted = false
|
|
591
591
|
if (!arg.ref[i + 1]) {
|
|
592
592
|
const flatName = nameSegments.join('_')
|
|
593
|
-
|
|
593
|
+
defineProperty(arg, 'flatName', flatName)
|
|
594
594
|
// if column is casted, we overwrite it's origin with the new type
|
|
595
595
|
if (arg.cast) {
|
|
596
596
|
const base = getElementForCast(arg)
|
|
@@ -635,7 +635,7 @@ function infer(originalQuery, model) {
|
|
|
635
635
|
})
|
|
636
636
|
|
|
637
637
|
// we need inner joins for the path expressions inside filter expressions after exists predicate
|
|
638
|
-
if ($baseLink?.pathExpressionInsideFilter)
|
|
638
|
+
if ($baseLink?.pathExpressionInsideFilter) defineProperty(arg, 'join', 'inner')
|
|
639
639
|
|
|
640
640
|
// ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
|
|
641
641
|
if (arg.expand) {
|
|
@@ -655,7 +655,7 @@ function infer(originalQuery, model) {
|
|
|
655
655
|
? { ref: [...baseColumn.ref, ...arg.ref], $refLinks: [...baseColumn.$refLinks, ...arg.$refLinks] }
|
|
656
656
|
: arg
|
|
657
657
|
if (isColumnJoinRelevant(colWithBase)) {
|
|
658
|
-
|
|
658
|
+
defineProperty(arg, 'isJoinRelevant', true)
|
|
659
659
|
joinTree.mergeColumn(colWithBase, originalQuery.outerQueries)
|
|
660
660
|
}
|
|
661
661
|
}
|
|
@@ -761,7 +761,7 @@ function infer(originalQuery, model) {
|
|
|
761
761
|
const res = $leafLink.definition.is2one
|
|
762
762
|
? new cds.struct({ elements: inferredExpandSubquery.elements })
|
|
763
763
|
: new cds.array({ items: new cds.struct({ elements: inferredExpandSubquery.elements }) })
|
|
764
|
-
return
|
|
764
|
+
return defineProperty(res, '$assocExpand', true)
|
|
765
765
|
} else if ($leafLink.definition.elements) {
|
|
766
766
|
let elements = {}
|
|
767
767
|
expand.forEach(e => {
|
|
@@ -900,13 +900,13 @@ function infer(originalQuery, model) {
|
|
|
900
900
|
const calcElementIsJoinRelevant = isColumnJoinRelevant(p)
|
|
901
901
|
if (calcElementIsJoinRelevant) {
|
|
902
902
|
if (!calcElement.value.isJoinRelevant)
|
|
903
|
-
|
|
903
|
+
defineProperty(step, 'isJoinRelevant',true)
|
|
904
904
|
joinTree.mergeColumn(p, originalQuery.outerQueries)
|
|
905
905
|
} else {
|
|
906
906
|
// we need to explicitly set the value to false in this case,
|
|
907
907
|
// e.g. `SELECT from booksCalc.Books { ID, author.{name }, author {name } }`
|
|
908
908
|
// --> for the inline column, the name is join relevant, while for the expand, it is not
|
|
909
|
-
|
|
909
|
+
defineProperty(step, 'isJoinRelevant', false)
|
|
910
910
|
}
|
|
911
911
|
}
|
|
912
912
|
}
|
|
@@ -1064,7 +1064,6 @@ function infer(originalQuery, model) {
|
|
|
1064
1064
|
* @returns {object} a copy of @param base with all annotations of @param from
|
|
1065
1065
|
* @TODO prototype based
|
|
1066
1066
|
*/
|
|
1067
|
-
// REVISIT: TODO: inferred.elements should be linked
|
|
1068
1067
|
function getCopyWithAnnos(from, base) {
|
|
1069
1068
|
const result = { ...base }
|
|
1070
1069
|
// REVISIT: we don't need to and hence should not handle annotations at runtime
|
|
@@ -1072,7 +1071,7 @@ function infer(originalQuery, model) {
|
|
|
1072
1071
|
if (prop.startsWith('@')) result[prop] = from[prop]
|
|
1073
1072
|
}
|
|
1074
1073
|
|
|
1075
|
-
if (from.as && base.name !== from.as)
|
|
1074
|
+
if (from.as && base.name !== from.as) defineProperty(result, 'name', from.as) // TODO double check if this is needed
|
|
1076
1075
|
// in subqueries we need the linked element if an outer query accesses it
|
|
1077
1076
|
return Object.setPrototypeOf(result, base)
|
|
1078
1077
|
}
|
package/lib/utils.js
CHANGED
|
@@ -62,6 +62,14 @@ function getImplicitAlias(str, useTechnicalAlias = true) {
|
|
|
62
62
|
return index != -1 ? str.substring(index + 1) : str
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function defineProperty(obj, prop, value) {
|
|
66
|
+
return Object.defineProperty(obj, prop, {
|
|
67
|
+
value,
|
|
68
|
+
writable: true,
|
|
69
|
+
configurable: true,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
65
73
|
/**
|
|
66
74
|
* Shared utility functions which operate dynamically on the model / query.
|
|
67
75
|
*
|
|
@@ -129,5 +137,6 @@ module.exports = {
|
|
|
129
137
|
isCalculatedOnRead,
|
|
130
138
|
isCalculatedElement,
|
|
131
139
|
getImplicitAlias,
|
|
140
|
+
defineProperty,
|
|
132
141
|
getModelUtils,
|
|
133
142
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/db-service",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.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": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/cap-js/cds-dbs"
|
|
8
|
+
"url": "git+https://github.com/cap-js/cds-dbs.git"
|
|
9
9
|
},
|
|
10
10
|
"bugs": {
|
|
11
11
|
"url": "https://github.com/cap-js/cds-dbs/issues"
|
|
@@ -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
|
}
|