@cap-js/db-service 1.17.2 → 1.19.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 +29 -0
- package/lib/InsertResults.js +1 -1
- package/lib/SQLService.js +5 -5
- package/lib/common/session-context.js +2 -2
- package/lib/cql-functions.js +88 -256
- package/lib/cqn2sql.js +57 -45
- package/lib/cqn4sql.js +102 -75
- package/lib/deep-queries.js +4 -4
- package/lib/fill-in-keys.js +1 -1
- package/lib/infer/index.js +9 -14
- package/lib/utils.js +26 -1
- package/package.json +1 -2
package/lib/cqn2sql.js
CHANGED
|
@@ -25,7 +25,7 @@ class CQN2SQLRenderer {
|
|
|
25
25
|
if (cds.env.sql.names === 'quoted') {
|
|
26
26
|
this.class.prototype.name = (name, query) => {
|
|
27
27
|
const e = name.id || name
|
|
28
|
-
return (query?.
|
|
28
|
+
return (query?._target || this.model?.definitions[e])?.['@cds.persistence.name'] || e
|
|
29
29
|
}
|
|
30
30
|
this.class.prototype.quote = (s) => `"${String(s).replace(/"/g, '""')}"`
|
|
31
31
|
}
|
|
@@ -92,7 +92,7 @@ class CQN2SQLRenderer {
|
|
|
92
92
|
if (values && !Array.isArray(values)) {
|
|
93
93
|
values = [values]
|
|
94
94
|
}
|
|
95
|
-
DEBUG(this.sql,
|
|
95
|
+
DEBUG(this.sql, values)
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
|
|
@@ -105,7 +105,7 @@ class CQN2SQLRenderer {
|
|
|
105
105
|
* @returns {import('./infer/cqn').Query}
|
|
106
106
|
*/
|
|
107
107
|
infer(q) {
|
|
108
|
-
return q.
|
|
108
|
+
return q._target instanceof cds.entity ? q : cds_infer(q)
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
cqn4sql(q) {
|
|
@@ -226,7 +226,7 @@ class CQN2SQLRenderer {
|
|
|
226
226
|
* @param {import('./infer/cqn').SELECT} q
|
|
227
227
|
*/
|
|
228
228
|
SELECT(q) {
|
|
229
|
-
let { from, expand, where, groupBy, having, orderBy, limit, one, distinct, localized, forUpdate, forShareLock } =
|
|
229
|
+
let { from, expand, where, groupBy, having, orderBy, limit, one, distinct, localized, forUpdate, forShareLock, recurse } =
|
|
230
230
|
q.SELECT
|
|
231
231
|
|
|
232
232
|
if (from?.join && !q.SELECT.columns) {
|
|
@@ -239,12 +239,13 @@ class CQN2SQLRenderer {
|
|
|
239
239
|
let sql = `SELECT`
|
|
240
240
|
if (distinct) sql += ` DISTINCT`
|
|
241
241
|
if (!_empty(columns)) sql += ` ${columns}`
|
|
242
|
-
if (
|
|
242
|
+
if (recurse) sql += ` FROM ${this.SELECT_recurse(q)}`
|
|
243
|
+
else if (!_empty(from)) sql += ` FROM ${this.from(from, q)}`
|
|
243
244
|
else sql += this.from_dummy()
|
|
244
|
-
if (!_empty(where)) sql += ` WHERE ${this.where(where)}`
|
|
245
|
-
if (!_empty(groupBy)) sql += ` GROUP BY ${this.groupBy(groupBy)}`
|
|
246
|
-
if (!_empty(having)) sql += ` HAVING ${this.having(having)}`
|
|
247
|
-
if (!_empty(orderBy)) sql += ` ORDER BY ${this.orderBy(orderBy, localized)}`
|
|
245
|
+
if (!recurse && !_empty(where)) sql += ` WHERE ${this.where(where)}`
|
|
246
|
+
if (!recurse && !_empty(groupBy)) sql += ` GROUP BY ${this.groupBy(groupBy)}`
|
|
247
|
+
if (!recurse && !_empty(having)) sql += ` HAVING ${this.having(having)}`
|
|
248
|
+
if (!recurse && !_empty(orderBy)) sql += ` ORDER BY ${this.orderBy(orderBy, localized)}`
|
|
248
249
|
if (one) limit = Object.assign({}, limit, { rows: { val: 1 } })
|
|
249
250
|
if (limit) sql += ` LIMIT ${this.limit(limit)}`
|
|
250
251
|
if (forUpdate) sql += ` ${this.forUpdate(forUpdate)}`
|
|
@@ -257,13 +258,23 @@ class CQN2SQLRenderer {
|
|
|
257
258
|
return (this.sql = sql)
|
|
258
259
|
}
|
|
259
260
|
|
|
261
|
+
SELECT_recurse() {
|
|
262
|
+
cds.error`Feature "recurse" queries not supported.`
|
|
263
|
+
}
|
|
264
|
+
|
|
260
265
|
/**
|
|
261
266
|
* Renders a column clause into generic SQL
|
|
262
267
|
* @param {import('./infer/cqn').SELECT} param0
|
|
263
268
|
* @returns {string} SQL
|
|
264
269
|
*/
|
|
265
270
|
SELECT_columns(q) {
|
|
266
|
-
|
|
271
|
+
const ret = []
|
|
272
|
+
const arr = q.SELECT.columns ?? ['*']
|
|
273
|
+
for (const x of arr) {
|
|
274
|
+
if (x.SELECT?.count) arr.push(this.SELECT_count(x))
|
|
275
|
+
ret.push(this.column_expr(x, q))
|
|
276
|
+
}
|
|
277
|
+
return ret
|
|
267
278
|
}
|
|
268
279
|
|
|
269
280
|
/**
|
|
@@ -292,24 +303,12 @@ class CQN2SQLRenderer {
|
|
|
292
303
|
? x => {
|
|
293
304
|
const name = this.column_name(x)
|
|
294
305
|
const escaped = `${name.replace(/"/g, '""')}`
|
|
295
|
-
|
|
296
|
-
if (x.SELECT?.count) {
|
|
297
|
-
// Return both the sub select and the count for @odata.count
|
|
298
|
-
const qc = cds.ql.clone(x, { columns: [{ func: 'count' }], one: 1, limit: 0, orderBy: 0 })
|
|
299
|
-
return [col, `${this.expr(qc)} AS "${escaped}@odata.count"`]
|
|
300
|
-
}
|
|
301
|
-
return col
|
|
306
|
+
return `${this.output_converter4(x.element, this.quote(name))} AS "${escaped}"`
|
|
302
307
|
}
|
|
303
308
|
: x => {
|
|
304
309
|
const name = this.column_name(x)
|
|
305
310
|
const escaped = `${name.replace(/"/g, '""')}`
|
|
306
|
-
|
|
307
|
-
if (x.SELECT?.count) {
|
|
308
|
-
// Return both the sub select and the count for @odata.count
|
|
309
|
-
const qc = cds.ql.clone(x, { columns: [{ func: 'count' }], one: 1, limit: 0, orderBy: 0 })
|
|
310
|
-
return [col, `'$."${escaped}@odata.count"',${this.expr(qc)}`]
|
|
311
|
-
}
|
|
312
|
-
return col
|
|
311
|
+
return `'$."${escaped}"',${this.output_converter4(x.element, this.quote(name))}`
|
|
313
312
|
}).flat()
|
|
314
313
|
|
|
315
314
|
if (isSimple) return `SELECT ${cols} FROM (${sql})`
|
|
@@ -322,6 +321,17 @@ class CQN2SQLRenderer {
|
|
|
322
321
|
return `SELECT ${isRoot || SELECT.one ? obj.replace('jsonb', 'json') : `jsonb_group_array(${obj})`} as _json_ FROM (${sql})`
|
|
323
322
|
}
|
|
324
323
|
|
|
324
|
+
SELECT_count(q) {
|
|
325
|
+
const countQuery = cds.ql.clone(q, {
|
|
326
|
+
columns: [{ func: 'count' }],
|
|
327
|
+
one: 0, limit: 0, orderBy: 0, expand: 0, count: 0
|
|
328
|
+
})
|
|
329
|
+
countQuery.as = q.as + '@odata.count'
|
|
330
|
+
countQuery.elements = undefined
|
|
331
|
+
countQuery.element = cds.builtin.types.Int64
|
|
332
|
+
return countQuery
|
|
333
|
+
}
|
|
334
|
+
|
|
325
335
|
/**
|
|
326
336
|
* Renders a SELECT column expression into generic SQL
|
|
327
337
|
* @param {import('./infer/cqn').col} x
|
|
@@ -416,14 +426,15 @@ class CQN2SQLRenderer {
|
|
|
416
426
|
* @returns {string[] | string} SQL
|
|
417
427
|
*/
|
|
418
428
|
orderBy(orderBy, localized) {
|
|
419
|
-
return orderBy.map(
|
|
420
|
-
localized
|
|
421
|
-
? c
|
|
422
|
-
this.expr(c) +
|
|
429
|
+
return orderBy.map(c => {
|
|
430
|
+
const o = localized
|
|
431
|
+
? this.expr(c) +
|
|
423
432
|
(c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
|
|
424
433
|
(c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
|
|
425
|
-
:
|
|
426
|
-
|
|
434
|
+
: this.expr(c) + (c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
|
|
435
|
+
if (c.nulls) return o + ' NULLS ' + (c.nulls.toLowerCase() === 'first' ? 'FIRST' : 'LAST')
|
|
436
|
+
return o
|
|
437
|
+
})
|
|
427
438
|
}
|
|
428
439
|
|
|
429
440
|
/**
|
|
@@ -443,9 +454,10 @@ class CQN2SQLRenderer {
|
|
|
443
454
|
* @returns {string} SQL
|
|
444
455
|
*/
|
|
445
456
|
forUpdate(update) {
|
|
446
|
-
const { wait, of } = update
|
|
457
|
+
const { wait, of, ignoreLocked } = update
|
|
447
458
|
let sql = 'FOR UPDATE'
|
|
448
459
|
if (!_empty(of)) sql += ` OF ${of.map(x => this.expr(x)).join(', ')}`
|
|
460
|
+
if (ignoreLocked) sql += ' IGNORE LOCKED'
|
|
449
461
|
if (typeof wait === 'number') sql += ` WAIT ${wait}`
|
|
450
462
|
return sql
|
|
451
463
|
}
|
|
@@ -490,7 +502,7 @@ class CQN2SQLRenderer {
|
|
|
490
502
|
*/
|
|
491
503
|
INSERT_entries(q) {
|
|
492
504
|
const { INSERT } = q
|
|
493
|
-
const elements = q.elements || q.
|
|
505
|
+
const elements = q.elements || q._target?.elements
|
|
494
506
|
if (!elements && !INSERT.entries?.length) {
|
|
495
507
|
return // REVISIT: mtx sends an insert statement without entries and no reference entity
|
|
496
508
|
}
|
|
@@ -502,7 +514,7 @@ class CQN2SQLRenderer {
|
|
|
502
514
|
this.columns = columns
|
|
503
515
|
|
|
504
516
|
const alias = INSERT.into.as
|
|
505
|
-
const entity = this.name(q.
|
|
517
|
+
const entity = this.name(q._target?.name || INSERT.into.ref[0], q)
|
|
506
518
|
if (!elements) {
|
|
507
519
|
this.entries = INSERT.entries.map(e => columns.map(c => e[c]))
|
|
508
520
|
const param = this.param.bind(this, { ref: ['?'] })
|
|
@@ -528,7 +540,7 @@ class CQN2SQLRenderer {
|
|
|
528
540
|
}
|
|
529
541
|
|
|
530
542
|
async *INSERT_entries_stream(entries, binaryEncoding = 'base64') {
|
|
531
|
-
const elements = this.cqn.
|
|
543
|
+
const elements = this.cqn._target?.elements || {}
|
|
532
544
|
const bufferLimit = 65536 // 1 << 16
|
|
533
545
|
let buffer = '['
|
|
534
546
|
|
|
@@ -577,7 +589,7 @@ class CQN2SQLRenderer {
|
|
|
577
589
|
}
|
|
578
590
|
|
|
579
591
|
async *INSERT_rows_stream(entries, binaryEncoding = 'base64') {
|
|
580
|
-
const elements = this.cqn.
|
|
592
|
+
const elements = this.cqn._target?.elements || {}
|
|
581
593
|
const bufferLimit = 65536 // 1 << 16
|
|
582
594
|
let buffer = '['
|
|
583
595
|
|
|
@@ -630,9 +642,9 @@ class CQN2SQLRenderer {
|
|
|
630
642
|
*/
|
|
631
643
|
INSERT_rows(q) {
|
|
632
644
|
const { INSERT } = q
|
|
633
|
-
const entity = this.name(q.
|
|
645
|
+
const entity = this.name(q._target?.name || INSERT.into.ref[0], q)
|
|
634
646
|
const alias = INSERT.into.as
|
|
635
|
-
const elements = q.elements || q.
|
|
647
|
+
const elements = q.elements || q._target?.elements
|
|
636
648
|
const columns = this.columns = INSERT.columns || cds.error`Cannot insert rows without columns or elements`
|
|
637
649
|
|
|
638
650
|
if (!elements) {
|
|
@@ -676,9 +688,9 @@ class CQN2SQLRenderer {
|
|
|
676
688
|
*/
|
|
677
689
|
INSERT_select(q) {
|
|
678
690
|
const { INSERT } = q
|
|
679
|
-
const entity = this.name(q.
|
|
691
|
+
const entity = this.name(q._target.name, q)
|
|
680
692
|
const alias = INSERT.into.as
|
|
681
|
-
const elements = q.elements || q.
|
|
693
|
+
const elements = q.elements || q._target?.elements || {}
|
|
682
694
|
const columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter(
|
|
683
695
|
c => c in elements && !elements[c].virtual && !elements[c].isAssociation,
|
|
684
696
|
))
|
|
@@ -719,15 +731,15 @@ class CQN2SQLRenderer {
|
|
|
719
731
|
const { UPSERT } = q
|
|
720
732
|
|
|
721
733
|
let sql = this.INSERT({ __proto__: q, INSERT: UPSERT })
|
|
722
|
-
if (!q.
|
|
734
|
+
if (!q._target?.keys) return sql
|
|
723
735
|
const keys = []
|
|
724
|
-
for (const k of ObjectKeys(q.
|
|
725
|
-
const element = q.
|
|
736
|
+
for (const k of ObjectKeys(q._target?.keys)) {
|
|
737
|
+
const element = q._target.keys[k]
|
|
726
738
|
if (element.isAssociation || element.virtual) continue
|
|
727
739
|
keys.push(k)
|
|
728
740
|
}
|
|
729
741
|
|
|
730
|
-
const elements = q.
|
|
742
|
+
const elements = q._target?.elements || {}
|
|
731
743
|
// temporal data
|
|
732
744
|
for (const k of ObjectKeys(elements)) {
|
|
733
745
|
if (elements[k]['@cds.valid.from']) keys.push(k)
|
|
@@ -744,7 +756,7 @@ class CQN2SQLRenderer {
|
|
|
744
756
|
.filter(c => keys.includes(c.name))
|
|
745
757
|
.map(c => `${c.onInsert || c.sql} as ${this.quote(c.name)}`)
|
|
746
758
|
|
|
747
|
-
const entity = this.name(q.
|
|
759
|
+
const entity = this.name(q._target?.name || UPSERT.into.ref[0], q)
|
|
748
760
|
sql = `SELECT ${managed.map(c => c.upsert
|
|
749
761
|
.replace(/value->/g, '"$$$$value$$$$"->')
|
|
750
762
|
.replace(/json_type\(value,/g, 'json_type("$$$$value$$$$",'))
|
|
@@ -773,7 +785,7 @@ class CQN2SQLRenderer {
|
|
|
773
785
|
*/
|
|
774
786
|
UPDATE(q) {
|
|
775
787
|
const { entity, with: _with, data, where } = q.UPDATE
|
|
776
|
-
const elements = q.
|
|
788
|
+
const elements = q._target?.elements
|
|
777
789
|
let sql = `UPDATE ${this.quote(this.name(entity.ref?.[0] || entity, q))}`
|
|
778
790
|
if (entity.as) sql += ` AS ${this.quote(entity.as)}`
|
|
779
791
|
|
package/lib/cqn4sql.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const cds = require('@sap/cds')
|
|
4
|
+
cds.infer.target ??= q => q._target || q.target // instanceof cds.entity ? q._target : q.target
|
|
4
5
|
|
|
5
6
|
const infer = require('./infer')
|
|
6
7
|
const { computeColumnsToBeSearched } = require('./search')
|
|
7
|
-
const { prettyPrintRef, isCalculatedOnRead, isCalculatedElement } = require('./utils')
|
|
8
|
+
const { prettyPrintRef, isCalculatedOnRead, isCalculatedElement, getImplicitAlias } = require('./utils')
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* For operators of <eqOps>, this is replaced by comparing all leaf elements with null, combined with and.
|
|
@@ -60,6 +61,22 @@ function cqn4sql(originalQuery, model) {
|
|
|
60
61
|
else if (having) inferred.SELECT.having = having
|
|
61
62
|
}
|
|
62
63
|
}
|
|
64
|
+
// query modifiers can also be defined in from ref leaf infix filter
|
|
65
|
+
// > SELECT from bookshop.Books[order by price] {ID}
|
|
66
|
+
if (inferred.SELECT?.from.ref) {
|
|
67
|
+
for (const [key, val] of Object.entries(inferred.SELECT.from.ref.at(-1))) {
|
|
68
|
+
if (key in { orderBy: 1, groupBy: 1 }) {
|
|
69
|
+
if (inferred.SELECT[key]) inferred.SELECT[key].push(...val)
|
|
70
|
+
else inferred.SELECT[key] = val
|
|
71
|
+
} else if (key === 'limit') {
|
|
72
|
+
// limit defined on the query has precedence
|
|
73
|
+
if (!inferred.SELECT.limit) inferred.SELECT.limit = val
|
|
74
|
+
} else if (key === 'having') {
|
|
75
|
+
if (!inferred.SELECT.having) inferred.SELECT.having = val
|
|
76
|
+
else inferred.SELECT.having.push('and', ...val)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
63
80
|
inferred = infer(inferred, model)
|
|
64
81
|
// if the query has custom joins we don't want to transform it
|
|
65
82
|
// TODO: move all the way to the top of this function once cds.infer supports joins as well
|
|
@@ -208,7 +225,8 @@ function cqn4sql(originalQuery, model) {
|
|
|
208
225
|
*/
|
|
209
226
|
function transformQueryForInsertUpsert(kind) {
|
|
210
227
|
const { as } = transformedQuery[kind].into
|
|
211
|
-
|
|
228
|
+
const target = cds.infer.target (inferred) // REVISIT: we should reliably use inferred._target instead
|
|
229
|
+
transformedQuery[kind].into = { ref: [target.name] }
|
|
212
230
|
if (as) transformedQuery[kind].into.as = as
|
|
213
231
|
return transformedQuery
|
|
214
232
|
}
|
|
@@ -784,7 +802,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
784
802
|
} else {
|
|
785
803
|
outerAlias = transformedQuery.SELECT.from.as
|
|
786
804
|
subqueryFromRef = [
|
|
787
|
-
...(transformedQuery.SELECT.from.ref || /* subq in from */
|
|
805
|
+
...(transformedQuery.SELECT.from.ref || /* subq in from */ transformedQuery.SELECT.from.SELECT.from.ref),
|
|
788
806
|
...ref,
|
|
789
807
|
]
|
|
790
808
|
}
|
|
@@ -800,18 +818,20 @@ function cqn4sql(originalQuery, model) {
|
|
|
800
818
|
return _subqueryForGroupBy(column, baseRef, columnAlias)
|
|
801
819
|
}
|
|
802
820
|
|
|
803
|
-
//
|
|
804
|
-
//
|
|
805
|
-
|
|
821
|
+
// Alias in expand subquery is derived from but not equal to
|
|
822
|
+
// the alias of the column because to account for potential ambiguities
|
|
823
|
+
// the alias cannot be addressed anyways
|
|
824
|
+
const uniqueSubqueryAlias = getNextAvailableTableAlias(getImplicitAlias(columnAlias), inferred.outerQueries)
|
|
806
825
|
|
|
807
826
|
// `SELECT from Authors { books.genre as genreOfBooks { name } } becomes `SELECT from Books:genre as genreOfBooks`
|
|
808
827
|
const from = { ref: subqueryFromRef, as: uniqueSubqueryAlias }
|
|
809
828
|
const subqueryBase = {}
|
|
810
|
-
|
|
811
|
-
|
|
829
|
+
const queryModifiers = { ...column }
|
|
830
|
+
for (const [key, value] of Object.entries(queryModifiers)) {
|
|
831
|
+
if (key in { limit: 1, orderBy: 1, groupBy: 1, excluding: 1, where: 1, having: 1, count: 1 })
|
|
812
832
|
subqueryBase[key] = value
|
|
813
|
-
}
|
|
814
833
|
}
|
|
834
|
+
|
|
815
835
|
const subquery = {
|
|
816
836
|
SELECT: {
|
|
817
837
|
...subqueryBase,
|
|
@@ -853,6 +873,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
853
873
|
const existsSubqueryAlias = recent[existsIndex + 1].SELECT.from.as
|
|
854
874
|
if (existsSubqueryAlias === x.ref?.[0]) return { ref: [outer, ...x.ref.slice(1)] }
|
|
855
875
|
if (x.xpr) x.xpr = x.xpr.map(replaceAliasWithSubqueryAlias)
|
|
876
|
+
if (x.args) x.args = x.args.map(replaceAliasWithSubqueryAlias)
|
|
856
877
|
return x
|
|
857
878
|
}
|
|
858
879
|
return subq
|
|
@@ -877,40 +898,48 @@ function cqn4sql(originalQuery, model) {
|
|
|
877
898
|
|
|
878
899
|
// to be attached to dummy query
|
|
879
900
|
const elements = {}
|
|
880
|
-
const
|
|
881
|
-
if (
|
|
901
|
+
const containsWildcard = column.expand.includes('*')
|
|
902
|
+
if (containsWildcard) {
|
|
882
903
|
// expand with wildcard vanishes as expand is part of the group by (OData $apply + $expand)
|
|
883
904
|
return null
|
|
884
905
|
}
|
|
885
|
-
const expandedColumns = column.expand
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
906
|
+
const expandedColumns = column.expand
|
|
907
|
+
.flatMap(expand => {
|
|
908
|
+
if (!expand.ref) return expand
|
|
909
|
+
const fullRef = [...baseRef, ...expand.ref]
|
|
910
|
+
|
|
911
|
+
if (expand.expand) {
|
|
912
|
+
const nested = _subqueryForGroupBy(expand, fullRef, expand.as || expand.ref.map(idOnly).join('_'))
|
|
913
|
+
if (nested) {
|
|
914
|
+
setElementOnColumns(nested, expand.element)
|
|
915
|
+
elements[expand.as || expand.ref.map(idOnly).join('_')] = nested
|
|
916
|
+
}
|
|
917
|
+
return nested
|
|
918
|
+
}
|
|
895
919
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
920
|
+
const groupByRef = groupByLookup.get(fullRef.map(refWithConditions).join('.'))
|
|
921
|
+
if (!groupByRef) {
|
|
922
|
+
throw new Error(
|
|
923
|
+
`The expanded column "${fullRef.map(refWithConditions).join('.')}" must be part of the group by clause`,
|
|
924
|
+
)
|
|
925
|
+
}
|
|
902
926
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
927
|
+
const copy = Object.create(groupByRef)
|
|
928
|
+
// always alias for this special case, so that they nested element names match the expected result structure
|
|
929
|
+
// otherwise we'd get `author { <outer>.author_ID }`, but we need `author { <outer>.author_ID as ID }`
|
|
930
|
+
copy.as = expand.as || expand.ref.at(-1)
|
|
931
|
+
const tableAlias = getTableAlias(copy)
|
|
932
|
+
const res = getFlatColumnsFor(copy, { tableAlias })
|
|
933
|
+
res.forEach(c => {
|
|
934
|
+
elements[c.as || c.ref.at(-1)] = c.element
|
|
935
|
+
})
|
|
936
|
+
return res
|
|
911
937
|
})
|
|
912
|
-
|
|
913
|
-
|
|
938
|
+
.filter(c => c)
|
|
939
|
+
|
|
940
|
+
if (expandedColumns.length === 0) {
|
|
941
|
+
return null
|
|
942
|
+
}
|
|
914
943
|
|
|
915
944
|
const SELECT = {
|
|
916
945
|
from: null,
|
|
@@ -1041,7 +1070,8 @@ function cqn4sql(originalQuery, model) {
|
|
|
1041
1070
|
outerQueries.push(inferred)
|
|
1042
1071
|
Object.defineProperty(q, 'outerQueries', { value: outerQueries })
|
|
1043
1072
|
}
|
|
1044
|
-
|
|
1073
|
+
const target = cds.infer.target (inferred) // REVISIT: we should reliably use inferred._target instead
|
|
1074
|
+
if (isLocalized(target)) q.SELECT.localized = true
|
|
1045
1075
|
if (q.SELECT.from.ref && !q.SELECT.from.as) assignUniqueSubqueryAlias()
|
|
1046
1076
|
return cqn4sql(q, model)
|
|
1047
1077
|
|
|
@@ -1049,7 +1079,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1049
1079
|
if (q.SELECT.from.uniqueSubqueryAlias) return
|
|
1050
1080
|
const last = q.SELECT.from.ref.at(-1)
|
|
1051
1081
|
const uniqueSubqueryAlias = inferred.joinTree.addNextAvailableTableAlias(
|
|
1052
|
-
|
|
1082
|
+
getImplicitAlias(last.id || last),
|
|
1053
1083
|
inferred.outerQueries,
|
|
1054
1084
|
)
|
|
1055
1085
|
Object.defineProperty(q.SELECT.from, 'uniqueSubqueryAlias', { value: uniqueSubqueryAlias })
|
|
@@ -1225,8 +1255,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1225
1255
|
if (flattenThisForeignKey) {
|
|
1226
1256
|
const fkElement = getElementForRef(k.ref, getDefinition(element.target))
|
|
1227
1257
|
let fkBaseName
|
|
1228
|
-
if (!leafAssoc || leafAssoc.onlyForeignKeyAccess)
|
|
1229
|
-
fkBaseName = `${baseName}_${k.as || k.ref.at(-1)}`
|
|
1258
|
+
if (!leafAssoc || leafAssoc.onlyForeignKeyAccess) fkBaseName = `${baseName}_${k.as || k.ref.at(-1)}`
|
|
1230
1259
|
// e.g. if foreign key is accessed via infix filter - use join alias to access key in target
|
|
1231
1260
|
else fkBaseName = k.ref.at(-1)
|
|
1232
1261
|
const fkPath = [...csnPath, k.ref.at(-1)]
|
|
@@ -1379,7 +1408,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1379
1408
|
j = nextAssocIndex
|
|
1380
1409
|
}
|
|
1381
1410
|
|
|
1382
|
-
const as = getNextAvailableTableAlias(
|
|
1411
|
+
const as = getNextAvailableTableAlias(getImplicitAlias(next.alias))
|
|
1383
1412
|
next.alias = as
|
|
1384
1413
|
if (!next.definition.target) {
|
|
1385
1414
|
let type = next.definition.type
|
|
@@ -1478,8 +1507,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1478
1507
|
// reject associations in expression, except if we are in an infix filter -> $baseLink is set
|
|
1479
1508
|
assertNoStructInXpr(token, $baseLink)
|
|
1480
1509
|
// reject virtual elements in expressions as they will lead to a sql error down the line
|
|
1481
|
-
if(definition?.virtual)
|
|
1482
|
-
throw new Error(`Virtual elements are not allowed in expressions`)
|
|
1510
|
+
if (definition?.virtual) throw new Error(`Virtual elements are not allowed in expressions`)
|
|
1483
1511
|
|
|
1484
1512
|
let result = is_regexp(token?.val) ? token : copy(token) // REVISIT: too expensive! //
|
|
1485
1513
|
if (token.ref) {
|
|
@@ -1676,12 +1704,12 @@ function cqn4sql(originalQuery, model) {
|
|
|
1676
1704
|
function _transformFrom() {
|
|
1677
1705
|
if (typeof from === 'string') {
|
|
1678
1706
|
// normalize to `ref`, i.e. for `UPDATE.entity('bookshop.Books')`
|
|
1679
|
-
return { transformedFrom: { ref: [from], as:
|
|
1707
|
+
return { transformedFrom: { ref: [from], as: getImplicitAlias(from) } }
|
|
1680
1708
|
}
|
|
1681
1709
|
transformedFrom.as =
|
|
1682
1710
|
from.uniqueSubqueryAlias ||
|
|
1683
1711
|
from.as ||
|
|
1684
|
-
|
|
1712
|
+
getImplicitAlias(transformedFrom.$refLinks[transformedFrom.$refLinks.length - 1].definition.name)
|
|
1685
1713
|
const whereExistsSubSelects = []
|
|
1686
1714
|
const filterConditions = []
|
|
1687
1715
|
const refReverse = [...from.ref].reverse()
|
|
@@ -1703,14 +1731,17 @@ function cqn4sql(originalQuery, model) {
|
|
|
1703
1731
|
.findIndex(rl => rl.definition.isAssociation || rl.definition.kind === 'entity')
|
|
1704
1732
|
next = $refLinksReverse[nextStepIndex]
|
|
1705
1733
|
}
|
|
1706
|
-
let as =
|
|
1734
|
+
let as = getImplicitAlias(next.alias)
|
|
1707
1735
|
/**
|
|
1708
1736
|
* for an `expand` subquery, we do not need to add
|
|
1709
1737
|
* the table alias of the `expand` host to the join tree
|
|
1710
1738
|
* --> This is an artificial query, which will later be correlated
|
|
1711
1739
|
* with the main query alias. see @function expandColumn()
|
|
1740
|
+
* There is one exception:
|
|
1741
|
+
* - if current and next have the same alias, we need to assign a new alias to the next
|
|
1742
|
+
*
|
|
1712
1743
|
*/
|
|
1713
|
-
if (!(inferred.SELECT?.expand === true)) {
|
|
1744
|
+
if (!(inferred.SELECT?.expand === true && current.alias.toLowerCase() !== as.toLowerCase())) {
|
|
1714
1745
|
as = getNextAvailableTableAlias(as)
|
|
1715
1746
|
}
|
|
1716
1747
|
next.alias = as
|
|
@@ -1897,9 +1928,11 @@ function cqn4sql(originalQuery, model) {
|
|
|
1897
1928
|
})
|
|
1898
1929
|
let pseudoPath = false
|
|
1899
1930
|
ref.reduce((prev, res, i) => {
|
|
1900
|
-
if (res === '$self')
|
|
1931
|
+
if (res === '$self') {
|
|
1901
1932
|
// next is resolvable in entity
|
|
1933
|
+
thing.$refLinks.push({ definition: prev, target: prev })
|
|
1902
1934
|
return prev
|
|
1935
|
+
}
|
|
1903
1936
|
if (res in pseudos.elements) {
|
|
1904
1937
|
pseudoPath = true
|
|
1905
1938
|
thing.$refLinks.push({ definition: pseudos.elements[res], target: pseudos })
|
|
@@ -1911,7 +1944,11 @@ function cqn4sql(originalQuery, model) {
|
|
|
1911
1944
|
const definition =
|
|
1912
1945
|
prev?.elements?.[res] || getDefinition(prev?.target)?.elements[res] || pseudos.elements[res]
|
|
1913
1946
|
const target = getParentEntity(definition)
|
|
1914
|
-
thing.$refLinks[i] = {
|
|
1947
|
+
thing.$refLinks[i] = {
|
|
1948
|
+
definition,
|
|
1949
|
+
target,
|
|
1950
|
+
alias: definition === assocRefLink.definition ? assocRefLink.alias : definition.name,
|
|
1951
|
+
}
|
|
1915
1952
|
return prev?.elements?.[res] || getDefinition(prev?.target)?.elements[res] || pseudos.elements[res]
|
|
1916
1953
|
}, assocHost)
|
|
1917
1954
|
}
|
|
@@ -1936,15 +1973,16 @@ function cqn4sql(originalQuery, model) {
|
|
|
1936
1973
|
lhs.ref[0] in { $self: true, $projection: true } ? getParentEntity(assocRefLink.definition) : target,
|
|
1937
1974
|
)
|
|
1938
1975
|
else {
|
|
1939
|
-
const
|
|
1940
|
-
const
|
|
1976
|
+
const lhsLeafDef = lhs.ref && lhs.$refLinks.at(-1).definition
|
|
1977
|
+
const rhsLeafDef = rhs.ref && rhs.$refLinks.at(-1).definition
|
|
1978
|
+
const lhsFirstDef = lhs.$refLinks.find(r => r.definition.kind === 'element')?.definition
|
|
1941
1979
|
// compare structures in on-condition
|
|
1942
|
-
if ((
|
|
1980
|
+
if ((lhsLeafDef?.target && rhsLeafDef?.target) || (lhsLeafDef?.elements && rhsLeafDef?.elements)) {
|
|
1943
1981
|
if (rhs.$refLinks[0].definition !== assocRefLink.definition) {
|
|
1944
1982
|
rhs.ref.unshift(targetSideRefLink.alias)
|
|
1945
1983
|
rhs.$refLinks.unshift(targetSideRefLink)
|
|
1946
1984
|
}
|
|
1947
|
-
if (
|
|
1985
|
+
if (lhsFirstDef !== assocRefLink.definition) {
|
|
1948
1986
|
lhs.ref.unshift(targetSideRefLink.alias)
|
|
1949
1987
|
lhs.$refLinks.unshift(targetSideRefLink)
|
|
1950
1988
|
}
|
|
@@ -1954,16 +1992,15 @@ function cqn4sql(originalQuery, model) {
|
|
|
1954
1992
|
i += res.length
|
|
1955
1993
|
continue
|
|
1956
1994
|
}
|
|
1957
|
-
//
|
|
1995
|
+
// assumption: if first step is the association itself, all following ref steps must be resolvable
|
|
1958
1996
|
// within target `assoc.assoc.fk` -> `assoc.assoc_fk`
|
|
1959
1997
|
else if (
|
|
1960
|
-
|
|
1961
|
-
getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]
|
|
1998
|
+
lhsFirstDef === getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]
|
|
1962
1999
|
)
|
|
1963
|
-
result[i].ref = [assocRefLink.alias, lhs.ref.slice(1).join('_')]
|
|
2000
|
+
result[i].ref = [assocRefLink.alias, lhs.ref.slice(lhs.ref[0] === '$self' ? 2 : 1).join('_')]
|
|
1964
2001
|
// naive assumption: if the path starts with an association which is not the association from
|
|
1965
2002
|
// which the on-condition originates, it must be a foreign key and hence resolvable in the source
|
|
1966
|
-
else if (
|
|
2003
|
+
else if (lhsFirstDef?.target) result[i].ref = [result[i].ref.join('_')]
|
|
1967
2004
|
}
|
|
1968
2005
|
}
|
|
1969
2006
|
if (backlink) {
|
|
@@ -1986,7 +2023,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1986
2023
|
// sanity check: error out if we can't produce a join
|
|
1987
2024
|
if (backlink.keys.length === 0) {
|
|
1988
2025
|
throw new Error(
|
|
1989
|
-
`Path step “${assocRefLink.
|
|
2026
|
+
`Path step “${assocRefLink.definition.name}” is a self comparison with “${getFullName(backlink)}” that has no foreign keys`,
|
|
1990
2027
|
)
|
|
1991
2028
|
}
|
|
1992
2029
|
// managed backlink -> calculate fk-pk pairs
|
|
@@ -2012,7 +2049,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2012
2049
|
if (lhs.ref[0] !== assocRefLink.alias && lhs.ref[0] !== targetSideRefLink.alias) {
|
|
2013
2050
|
// we need to find correct table alias for the structured access
|
|
2014
2051
|
const { definition } = lhs.$refLinks[0]
|
|
2015
|
-
if (definition === assocRefLink.definition) {
|
|
2052
|
+
if (definition === getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]) {
|
|
2016
2053
|
// first step is the association itself -> use it's name as it becomes the table alias
|
|
2017
2054
|
result[i].ref.splice(0, 1, assocRefLink.alias)
|
|
2018
2055
|
} else if (
|
|
@@ -2030,6 +2067,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2030
2067
|
}
|
|
2031
2068
|
/**
|
|
2032
2069
|
* Recursively calculates the containing entity for a given element.
|
|
2070
|
+
* If the query is localized, this function may return the localized entity.
|
|
2033
2071
|
*
|
|
2034
2072
|
* @param {CSN.element} element
|
|
2035
2073
|
* @returns {CSN.definition} the entity containing the given element
|
|
@@ -2223,7 +2261,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2223
2261
|
* - or null, if no searchable columns are found in neither in `@cds.search` nor in the target entity itself.
|
|
2224
2262
|
*/
|
|
2225
2263
|
function getSearchTerm(search, query) {
|
|
2226
|
-
const entity = query.SELECT.from.SELECT ? query.SELECT.from : query.
|
|
2264
|
+
const entity = query.SELECT.from.SELECT ? query.SELECT.from : cds.infer.target(query) // REVISIT: we should reliably use inferred._target instead
|
|
2227
2265
|
const searchIn = computeColumnsToBeSearched(inferred, entity)
|
|
2228
2266
|
if (searchIn.length > 0) {
|
|
2229
2267
|
const xpr = search
|
|
@@ -2284,7 +2322,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2284
2322
|
if (
|
|
2285
2323
|
inferred.SELECT?.from.uniqueSubqueryAlias &&
|
|
2286
2324
|
!inferred.SELECT?.from.as &&
|
|
2287
|
-
firstStep ===
|
|
2325
|
+
getImplicitAlias(firstStep) === getImplicitAlias(transformedQuery.SELECT.from.ref[0])
|
|
2288
2326
|
) {
|
|
2289
2327
|
return inferred.SELECT?.from.uniqueSubqueryAlias
|
|
2290
2328
|
}
|
|
@@ -2293,7 +2331,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2293
2331
|
}
|
|
2294
2332
|
|
|
2295
2333
|
function getCombinedElementAlias(node) {
|
|
2296
|
-
return
|
|
2334
|
+
return inferred.$combinedElements[node.ref[0].id || node.ref[0]]?.[0].index
|
|
2297
2335
|
}
|
|
2298
2336
|
}
|
|
2299
2337
|
function getTransformedFunctionArgs(args, $baseLink = null) {
|
|
@@ -2366,17 +2404,6 @@ function hasLogicalOr(tokenStream) {
|
|
|
2366
2404
|
return tokenStream.some(t => t in { OR: true, or: true })
|
|
2367
2405
|
}
|
|
2368
2406
|
|
|
2369
|
-
/**
|
|
2370
|
-
* Returns the last segment of a string after the last dot.
|
|
2371
|
-
*
|
|
2372
|
-
* @param {string} str - The input string.
|
|
2373
|
-
* @returns {string} The last segment of the string after the last dot. If there is no dot in the string, the function returns the original string.
|
|
2374
|
-
*/
|
|
2375
|
-
function getLastStringSegment(str) {
|
|
2376
|
-
const index = str.lastIndexOf('.')
|
|
2377
|
-
return index != -1 ? str.substring(index + 1) : str
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
2407
|
function getParentEntity(element) {
|
|
2381
2408
|
if (element.kind === 'entity') return element
|
|
2382
2409
|
else return getParentEntity(element.parent)
|
package/lib/deep-queries.js
CHANGED
|
@@ -39,9 +39,9 @@ async function onDeep(req, next) {
|
|
|
39
39
|
// const target = query.sources[Object.keys(query.sources)[0]]
|
|
40
40
|
if (!this.model?.definitions[_target_name4(req.query)]) return next()
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
if (!hasDeep(query, target)) return next()
|
|
42
|
+
if (!hasDeep(query)) return next()
|
|
44
43
|
|
|
44
|
+
const target = this.infer(query)._target
|
|
45
45
|
const beforeData = query.INSERT ? [] : await this.run(getExpandForDeep(query, target, true))
|
|
46
46
|
if (query.UPDATE && !beforeData.length) return 0
|
|
47
47
|
|
|
@@ -64,10 +64,10 @@ async function onDeep(req, next) {
|
|
|
64
64
|
return rootResult ?? beforeData.length
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
const hasDeep = (q
|
|
67
|
+
const hasDeep = (q) => {
|
|
68
68
|
const data = q.INSERT?.entries || (q.UPDATE?.data && [q.UPDATE.data]) || (q.UPDATE?.with && [q.UPDATE.with])
|
|
69
69
|
if (data)
|
|
70
|
-
for (const c in
|
|
70
|
+
for (const c in q._target.compositions) {
|
|
71
71
|
for (const row of data) if (row[c] !== undefined) return true
|
|
72
72
|
}
|
|
73
73
|
}
|