@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/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
|
|
@@ -119,7 +140,7 @@ class CQN2SQLRenderer {
|
|
|
119
140
|
* @param {import('./infer/cqn').CREATE} q
|
|
120
141
|
*/
|
|
121
142
|
CREATE(q) {
|
|
122
|
-
let { target } = q
|
|
143
|
+
let { _target: target } = q
|
|
123
144
|
let query = target?.query || q.CREATE.as
|
|
124
145
|
if (!target || target._unresolved) {
|
|
125
146
|
const entity = q.CREATE.entity
|
|
@@ -213,7 +234,7 @@ class CQN2SQLRenderer {
|
|
|
213
234
|
* @param {import('./infer/cqn').DROP} q
|
|
214
235
|
*/
|
|
215
236
|
DROP(q) {
|
|
216
|
-
const { target } = q
|
|
237
|
+
const { _target: target } = q
|
|
217
238
|
const isView = target?.query || target?.projection || q.DROP.view
|
|
218
239
|
const name = target?.name || q.DROP.table?.ref?.[0] || q.DROP.view?.ref?.[0]
|
|
219
240
|
return (this.sql = `DROP ${isView ? 'VIEW' : 'TABLE'} IF EXISTS ${this.quote(this.name(name, 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
|
/**
|
|
@@ -307,8 +593,7 @@ class CQN2SQLRenderer {
|
|
|
307
593
|
}
|
|
308
594
|
: x => {
|
|
309
595
|
const name = this.column_name(x)
|
|
310
|
-
|
|
311
|
-
return `'$."${escaped}"',${this.output_converter4(x.element, this.quote(name))}`
|
|
596
|
+
return `${this.string(`$.${JSON.stringify(name)}`)},${this.output_converter4(x.element, this.quote(name))}`
|
|
312
597
|
}).flat()
|
|
313
598
|
|
|
314
599
|
if (isSimple) return `SELECT ${cols} FROM (${sql})`
|
|
@@ -372,6 +657,18 @@ class CQN2SQLRenderer {
|
|
|
372
657
|
}
|
|
373
658
|
if (from.SELECT) return _aliased(`(${this.SELECT(from)})`)
|
|
374
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] }
|
|
375
672
|
}
|
|
376
673
|
|
|
377
674
|
/**
|
|
@@ -429,8 +726,8 @@ class CQN2SQLRenderer {
|
|
|
429
726
|
return orderBy.map(c => {
|
|
430
727
|
const o = localized
|
|
431
728
|
? this.expr(c) +
|
|
432
|
-
|
|
433
|
-
|
|
729
|
+
(c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
|
|
730
|
+
(c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
|
|
434
731
|
: this.expr(c) + (c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
|
|
435
732
|
if (c.nulls) return o + ' NULLS ' + (c.nulls.toLowerCase() === 'first' ? 'FIRST' : 'LAST')
|
|
436
733
|
return o
|
|
@@ -490,7 +787,7 @@ class CQN2SQLRenderer {
|
|
|
490
787
|
? this.INSERT_rows(q)
|
|
491
788
|
: INSERT.values
|
|
492
789
|
? this.INSERT_values(q)
|
|
493
|
-
: INSERT.as
|
|
790
|
+
: INSERT.from || INSERT.as
|
|
494
791
|
? this.INSERT_select(q)
|
|
495
792
|
: cds.error`Missing .entries, .rows, or .values in ${q}`
|
|
496
793
|
}
|
|
@@ -524,13 +821,14 @@ class CQN2SQLRenderer {
|
|
|
524
821
|
// Include this.values for placeholders
|
|
525
822
|
/** @type {unknown[][]} */
|
|
526
823
|
this.entries = []
|
|
527
|
-
if (INSERT.entries[0] instanceof Readable) {
|
|
824
|
+
if (INSERT.entries[0] instanceof Readable && !INSERT.entries[0].readableObjectMode) {
|
|
528
825
|
INSERT.entries[0].type = 'json'
|
|
529
826
|
this.entries = [[...this.values, INSERT.entries[0]]]
|
|
530
827
|
} else {
|
|
531
|
-
const
|
|
828
|
+
const entries = INSERT.entries[0]?.[Symbol.iterator] || INSERT.entries[0]?.[Symbol.asyncIterator] || INSERT.entries[0] instanceof Readable ? INSERT.entries[0] : INSERT.entries
|
|
829
|
+
const stream = Readable.from(this.INSERT_entries_stream(entries), { objectMode: false })
|
|
532
830
|
stream.type = 'json'
|
|
533
|
-
stream._raw =
|
|
831
|
+
stream._raw = entries
|
|
534
832
|
this.entries = [[...this.values, stream]]
|
|
535
833
|
}
|
|
536
834
|
|
|
@@ -545,7 +843,7 @@ class CQN2SQLRenderer {
|
|
|
545
843
|
let buffer = '['
|
|
546
844
|
|
|
547
845
|
let sep = ''
|
|
548
|
-
for (const row of entries) {
|
|
846
|
+
for await (const row of entries) {
|
|
549
847
|
buffer += `${sep}{`
|
|
550
848
|
if (!sep) sep = ','
|
|
551
849
|
|
|
@@ -619,7 +917,7 @@ class CQN2SQLRenderer {
|
|
|
619
917
|
if (val != null && elements[this.columns[key]]?.type in this.BINARY_TYPES) {
|
|
620
918
|
val = Buffer.from(val, 'base64').toString(binaryEncoding)
|
|
621
919
|
}
|
|
622
|
-
buffer += `${sepsub}${val
|
|
920
|
+
buffer += `${sepsub}${val == null ? 'null' : JSON.stringify(val)}`
|
|
623
921
|
}
|
|
624
922
|
|
|
625
923
|
if (!sepsub) sepsub = ','
|
|
@@ -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)
|
|
@@ -1143,8 +1441,8 @@ class CQN2SQLRenderer {
|
|
|
1143
1441
|
managed_extract(name, element, converter) {
|
|
1144
1442
|
const { UPSERT, INSERT } = this.cqn
|
|
1145
1443
|
const extract = !(INSERT?.entries || UPSERT?.entries) && (INSERT?.rows || UPSERT?.rows)
|
|
1146
|
-
? `value
|
|
1147
|
-
: `value
|
|
1444
|
+
? `value->>${this.string(`$[${this.columns.indexOf(name)}]`)}`
|
|
1445
|
+
: `value->>${this.string(`$.${JSON.stringify(name)}`)}`
|
|
1148
1446
|
const sql = converter?.(extract) || extract
|
|
1149
1447
|
return { extract, sql }
|
|
1150
1448
|
}
|
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 } = 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.
|
|
@@ -78,6 +78,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
inferred = infer(inferred, model)
|
|
81
|
+
const { getLocalizedName, isLocalized, getDefinition } = getModelUtils(model, originalQuery) // TODO: pass model to getModelUtils
|
|
81
82
|
// if the query has custom joins we don't want to transform it
|
|
82
83
|
// TODO: move all the way to the top of this function once cds.infer supports joins as well
|
|
83
84
|
// we need to infer the query even if no transformation will happen because cds.infer can't calculate the target
|
|
@@ -225,7 +226,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
225
226
|
*/
|
|
226
227
|
function transformQueryForInsertUpsert(kind) {
|
|
227
228
|
const { as } = transformedQuery[kind].into
|
|
228
|
-
const target = cds.infer.target
|
|
229
|
+
const target = cds.infer.target(inferred) // REVISIT: we should reliably use inferred._target instead
|
|
229
230
|
transformedQuery[kind].into = { ref: [target.name] }
|
|
230
231
|
if (as) transformedQuery[kind].into.as = as
|
|
231
232
|
return transformedQuery
|
|
@@ -281,7 +282,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
281
282
|
const args = []
|
|
282
283
|
if (r.queryArtifact.SELECT) args.push({ SELECT: transformSubquery(r.queryArtifact).SELECT, as: r.alias })
|
|
283
284
|
else {
|
|
284
|
-
const id =
|
|
285
|
+
const id = getLocalizedName(r.queryArtifact)
|
|
285
286
|
args.push({ ref: [r.args ? { id, args: r.args } : id], as: r.alias })
|
|
286
287
|
}
|
|
287
288
|
from = { join: r.join || 'left', args, on: [] }
|
|
@@ -308,7 +309,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
308
309
|
),
|
|
309
310
|
)
|
|
310
311
|
|
|
311
|
-
const id =
|
|
312
|
+
const id = getDefinition(nextAssoc.$refLink.definition.target).name
|
|
312
313
|
const { args } = nextAssoc
|
|
313
314
|
const arg = {
|
|
314
315
|
ref: [args ? { id, args } : id],
|
|
@@ -801,8 +802,9 @@ function cqn4sql(originalQuery, model) {
|
|
|
801
802
|
})
|
|
802
803
|
} else {
|
|
803
804
|
outerAlias = transformedQuery.SELECT.from.as
|
|
805
|
+
const getInnermostTarget = q => (q._target ? getInnermostTarget(q._target) : q)
|
|
804
806
|
subqueryFromRef = [
|
|
805
|
-
...(transformedQuery.SELECT.from.ref || /* subq in from */ transformedQuery.
|
|
807
|
+
...(transformedQuery.SELECT.from.ref || /* subq in from */ [getInnermostTarget(transformedQuery).name]),
|
|
806
808
|
...ref,
|
|
807
809
|
]
|
|
808
810
|
}
|
|
@@ -843,10 +845,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
843
845
|
}
|
|
844
846
|
const expanded = transformSubquery(subquery)
|
|
845
847
|
const correlated = _correlate({ ...expanded, as: columnAlias }, outerAlias)
|
|
846
|
-
|
|
847
|
-
value: expanded.elements,
|
|
848
|
-
writable: true,
|
|
849
|
-
})
|
|
848
|
+
defineProperty(correlated, 'elements', expanded.elements)
|
|
850
849
|
return correlated
|
|
851
850
|
|
|
852
851
|
function _correlate(subq, outer) {
|
|
@@ -1068,9 +1067,9 @@ function cqn4sql(originalQuery, model) {
|
|
|
1068
1067
|
else {
|
|
1069
1068
|
const outerQueries = inferred.outerQueries || []
|
|
1070
1069
|
outerQueries.push(inferred)
|
|
1071
|
-
|
|
1070
|
+
defineProperty(q, 'outerQueries', outerQueries)
|
|
1072
1071
|
}
|
|
1073
|
-
const target = cds.infer.target
|
|
1072
|
+
const target = cds.infer.target(inferred) // REVISIT: we should reliably use inferred._target instead
|
|
1074
1073
|
if (isLocalized(target)) q.SELECT.localized = true
|
|
1075
1074
|
if (q.SELECT.from.ref && !q.SELECT.from.as) assignUniqueSubqueryAlias()
|
|
1076
1075
|
return cqn4sql(q, model)
|
|
@@ -1082,7 +1081,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1082
1081
|
getImplicitAlias(last.id || last),
|
|
1083
1082
|
inferred.outerQueries,
|
|
1084
1083
|
)
|
|
1085
|
-
|
|
1084
|
+
defineProperty(q.SELECT.from, 'uniqueSubqueryAlias', uniqueSubqueryAlias)
|
|
1086
1085
|
}
|
|
1087
1086
|
}
|
|
1088
1087
|
|
|
@@ -1309,7 +1308,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1309
1308
|
const flatForeignKey = getDefinition(element.parent.name)?.elements[fkBaseName]
|
|
1310
1309
|
|
|
1311
1310
|
setElementOnColumns(flatColumn, flatForeignKey || fkElement)
|
|
1312
|
-
|
|
1311
|
+
defineProperty(flatColumn, '_csnPath', csnPath)
|
|
1313
1312
|
flatColumns.push(flatColumn)
|
|
1314
1313
|
}
|
|
1315
1314
|
}
|
|
@@ -1341,7 +1340,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1341
1340
|
if (column.sort) flatRef.sort = column.sort
|
|
1342
1341
|
if (columnAlias) flatRef.as = columnAlias
|
|
1343
1342
|
setElementOnColumns(flatRef, element)
|
|
1344
|
-
|
|
1343
|
+
defineProperty(flatRef, '_csnPath', csnPath)
|
|
1345
1344
|
return [flatRef]
|
|
1346
1345
|
|
|
1347
1346
|
function getReplacement(from) {
|
|
@@ -1674,7 +1673,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1674
1673
|
const transformedWhere = []
|
|
1675
1674
|
let transformedFrom = copy(from) // REVISIT: too expensive!
|
|
1676
1675
|
if (from.$refLinks)
|
|
1677
|
-
|
|
1676
|
+
defineProperty(transformedFrom, '$refLinks', [...from.$refLinks])
|
|
1678
1677
|
if (from.args) {
|
|
1679
1678
|
transformedFrom.args = []
|
|
1680
1679
|
from.args.forEach(arg => {
|
|
@@ -1739,7 +1738,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1739
1738
|
* with the main query alias. see @function expandColumn()
|
|
1740
1739
|
* There is one exception:
|
|
1741
1740
|
* - if current and next have the same alias, we need to assign a new alias to the next
|
|
1742
|
-
*
|
|
1741
|
+
*
|
|
1743
1742
|
*/
|
|
1744
1743
|
if (!(inferred.SELECT?.expand === true && current.alias.toLowerCase() !== as.toLowerCase())) {
|
|
1745
1744
|
as = getNextAvailableTableAlias(as)
|
|
@@ -1786,7 +1785,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1786
1785
|
const subquerySource =
|
|
1787
1786
|
getDefinition(transformedFrom.$refLinks[0].definition.target) || transformedFrom.$refLinks[0].target
|
|
1788
1787
|
if (subquerySource.params && !args) args = {}
|
|
1789
|
-
const id =
|
|
1788
|
+
const id = getLocalizedName(subquerySource)
|
|
1790
1789
|
transformedFrom.ref = [args ? { id, args } : id]
|
|
1791
1790
|
|
|
1792
1791
|
return { transformedWhere, transformedFrom }
|
|
@@ -1922,10 +1921,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1922
1921
|
const refLinkFaker = thing => {
|
|
1923
1922
|
const { ref } = thing
|
|
1924
1923
|
const assocHost = getParentEntity(assocRefLink.definition)
|
|
1925
|
-
|
|
1926
|
-
value: [],
|
|
1927
|
-
writable: true,
|
|
1928
|
-
})
|
|
1924
|
+
defineProperty(thing, '$refLinks', [])
|
|
1929
1925
|
let pseudoPath = false
|
|
1930
1926
|
ref.reduce((prev, res, i) => {
|
|
1931
1927
|
if (res === '$self') {
|
|
@@ -1994,9 +1990,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1994
1990
|
}
|
|
1995
1991
|
// assumption: if first step is the association itself, all following ref steps must be resolvable
|
|
1996
1992
|
// within target `assoc.assoc.fk` -> `assoc.assoc_fk`
|
|
1997
|
-
else if (
|
|
1998
|
-
lhsFirstDef === getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]
|
|
1999
|
-
)
|
|
1993
|
+
else if (lhsFirstDef === getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name])
|
|
2000
1994
|
result[i].ref = [assocRefLink.alias, lhs.ref.slice(lhs.ref[0] === '$self' ? 2 : 1).join('_')]
|
|
2001
1995
|
// naive assumption: if the path starts with an association which is not the association from
|
|
2002
1996
|
// which the on-condition originates, it must be a foreign key and hence resolvable in the source
|
|
@@ -2077,7 +2071,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2077
2071
|
// pseudo element
|
|
2078
2072
|
return element
|
|
2079
2073
|
if (element.kind === 'entity') return element
|
|
2080
|
-
else return getDefinition(
|
|
2074
|
+
else return getDefinition(getParentEntity(element.parent).name)
|
|
2081
2075
|
}
|
|
2082
2076
|
}
|
|
2083
2077
|
|
|
@@ -2164,8 +2158,8 @@ function cqn4sql(originalQuery, model) {
|
|
|
2164
2158
|
on.push(...(customWhere && hasLogicalOr(unmanagedOn) ? [asXpr(unmanagedOn)] : unmanagedOn))
|
|
2165
2159
|
}
|
|
2166
2160
|
|
|
2167
|
-
const subquerySource =
|
|
2168
|
-
const id =
|
|
2161
|
+
const subquerySource = getDefinition(nextDefinition.target) || nextDefinition
|
|
2162
|
+
const id = getLocalizedName(subquerySource)
|
|
2169
2163
|
if (subquerySource.params && !customArgs) customArgs = {}
|
|
2170
2164
|
const SELECT = {
|
|
2171
2165
|
from: {
|
|
@@ -2203,52 +2197,6 @@ function cqn4sql(originalQuery, model) {
|
|
|
2203
2197
|
return SELECT
|
|
2204
2198
|
}
|
|
2205
2199
|
|
|
2206
|
-
/**
|
|
2207
|
-
* If the query is `localized`, return the name of the `localized` entity for the `definition`.
|
|
2208
|
-
* If there is no `localized` entity for the `definition`, return the name of the `definition`
|
|
2209
|
-
*
|
|
2210
|
-
* @param {CSN.definition} definition
|
|
2211
|
-
* @returns the name of the localized entity for the given `definition` or `definition.name`
|
|
2212
|
-
*/
|
|
2213
|
-
function localized(definition) {
|
|
2214
|
-
if (!isLocalized(definition)) return definition.name
|
|
2215
|
-
const view = getDefinition(`localized.${definition.name}`)
|
|
2216
|
-
return view?.name || definition.name
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
/**
|
|
2220
|
-
* If a given query is required to be translated, the query has
|
|
2221
|
-
* the `.localized` property set to `true`. If that is the case,
|
|
2222
|
-
* and the definition has not set the `@cds.localized` annotation
|
|
2223
|
-
* to `false`, the given definition must be translated.
|
|
2224
|
-
*
|
|
2225
|
-
* @returns true if the given definition shall be localized
|
|
2226
|
-
*/
|
|
2227
|
-
function isLocalized(definition) {
|
|
2228
|
-
return (
|
|
2229
|
-
inferred.SELECT?.localized &&
|
|
2230
|
-
definition['@cds.localized'] !== false &&
|
|
2231
|
-
!inferred.SELECT.forUpdate &&
|
|
2232
|
-
!inferred.SELECT.forShareLock
|
|
2233
|
-
)
|
|
2234
|
-
}
|
|
2235
|
-
|
|
2236
|
-
/** returns the CSN definition for the given name from the model */
|
|
2237
|
-
function getDefinition(name) {
|
|
2238
|
-
if (!name) return null
|
|
2239
|
-
return model.definitions[name]
|
|
2240
|
-
}
|
|
2241
|
-
|
|
2242
|
-
/**
|
|
2243
|
-
* Get the csn definition of the target of a given association
|
|
2244
|
-
*
|
|
2245
|
-
* @param assoc
|
|
2246
|
-
* @returns the csn definition of the association target or null if it is not an association
|
|
2247
|
-
*/
|
|
2248
|
-
function assocTarget(assoc) {
|
|
2249
|
-
return getDefinition(assoc.target) || null
|
|
2250
|
-
}
|
|
2251
|
-
|
|
2252
2200
|
/**
|
|
2253
2201
|
* For a given search expression return a function "search" which holds the search expression
|
|
2254
2202
|
* as well as the searchable columns as arguments.
|
|
@@ -2416,10 +2364,7 @@ function getParentEntity(element) {
|
|
|
2416
2364
|
* @param {csn.Element} element
|
|
2417
2365
|
*/
|
|
2418
2366
|
function setElementOnColumns(col, element) {
|
|
2419
|
-
|
|
2420
|
-
value: element,
|
|
2421
|
-
writable: true,
|
|
2422
|
-
})
|
|
2367
|
+
defineProperty(col, 'element', element)
|
|
2423
2368
|
}
|
|
2424
2369
|
|
|
2425
2370
|
const getName = col => col.as || col.ref?.at(-1)
|