@cap-js/db-service 1.16.0 → 1.16.2
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 +18 -0
- package/lib/common/DatabaseService.js +7 -6
- package/lib/common/generic-pool.js +7 -9
- package/lib/cqn2sql.js +27 -39
- package/lib/cqn4sql.js +89 -68
- package/lib/infer/index.js +618 -731
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@
|
|
|
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
|
+
## [1.16.2](https://github.com/cap-js/cds-dbs/compare/db-service-v1.16.1...db-service-v1.16.2) (2024-12-18)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
* do not override .toJSON of buffers ([#949](https://github.com/cap-js/cds-dbs/issues/949)) ([ed52f72](https://github.com/cap-js/cds-dbs/commit/ed52f72206df6e683106ab0bbbecf4b778cf36b5))
|
|
13
|
+
|
|
14
|
+
## [1.16.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.16.0...db-service-v1.16.1) (2024-12-16)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
* handle undefined DEBUG ([#942](https://github.com/cap-js/cds-dbs/issues/942)) ([dd2da3a](https://github.com/cap-js/cds-dbs/commit/dd2da3a8d8feb5beaae5860d493e9e1158dbf99f)), closes [#941](https://github.com/cap-js/cds-dbs/issues/941)
|
|
20
|
+
* only expand partial foreign key if explicitly requested ([#916](https://github.com/cap-js/cds-dbs/issues/916)) ([96911ad](https://github.com/cap-js/cds-dbs/commit/96911ada1831e71febb84d8a382b57d55d24c1bc))
|
|
21
|
+
* quoted mode ([#937](https://github.com/cap-js/cds-dbs/issues/937)) ([9e62b22](https://github.com/cap-js/cds-dbs/commit/9e62b22a1be90ada9f57cfa63505735d8b8eed88))
|
|
22
|
+
* sort property is case insensitive ([#924](https://github.com/cap-js/cds-dbs/issues/924)) ([2c72c87](https://github.com/cap-js/cds-dbs/commit/2c72c871d6c7f65797b8bd8692305149b3ea65f8))
|
|
23
|
+
* wildcard expand with aggregation ([#923](https://github.com/cap-js/cds-dbs/issues/923)) ([bbe7be0](https://github.com/cap-js/cds-dbs/commit/bbe7be00498ad083cf951daf344b7f5fd9f68ab9))
|
|
24
|
+
|
|
7
25
|
## [1.16.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.15.1...db-service-v1.16.0) (2024-11-25)
|
|
8
26
|
|
|
9
27
|
|
|
@@ -47,13 +47,14 @@ class DatabaseService extends cds.Service {
|
|
|
47
47
|
* transaction with `BEGIN`
|
|
48
48
|
* @returns this
|
|
49
49
|
*/
|
|
50
|
-
async begin() {
|
|
50
|
+
async begin (min) {
|
|
51
51
|
// We expect tx.begin() being called for an txed db service
|
|
52
52
|
const ctx = this.context
|
|
53
53
|
|
|
54
54
|
// If .begin is called explicitly it starts a new transaction and executes begin
|
|
55
|
-
if (!ctx) return this.tx().begin()
|
|
55
|
+
if (!ctx) return this.tx().begin(min)
|
|
56
56
|
|
|
57
|
+
// REVISIT: can we revisit the below revisit now?
|
|
57
58
|
// REVISIT: tenant should be undefined if !this.isMultitenant
|
|
58
59
|
let isMultitenant = 'multiTenant' in this.options ? this.options.multiTenant : cds.env.requires.multitenancy
|
|
59
60
|
let tenant = isMultitenant && ctx.tenant
|
|
@@ -63,10 +64,10 @@ class DatabaseService extends cds.Service {
|
|
|
63
64
|
|
|
64
65
|
// Acquire a pooled connection
|
|
65
66
|
this.dbc = await this.acquire()
|
|
66
|
-
this.dbc.destroy = this.destroy.bind(this)
|
|
67
|
+
this.dbc.destroy = this.destroy.bind(this) // REVISIT: this is bad
|
|
67
68
|
|
|
68
69
|
// Begin a session...
|
|
69
|
-
try {
|
|
70
|
+
if (!min) try {
|
|
70
71
|
await this.set(new SessionContext(ctx))
|
|
71
72
|
await this.send('BEGIN')
|
|
72
73
|
} catch (e) {
|
|
@@ -153,8 +154,8 @@ class DatabaseService extends cds.Service {
|
|
|
153
154
|
*/
|
|
154
155
|
run(query, data, ...etc) {
|
|
155
156
|
// Allow db.run('...',1,2,3,4)
|
|
156
|
-
if (data !== undefined && typeof query === 'string' && typeof data !== 'object')
|
|
157
|
-
return super.run(
|
|
157
|
+
if (data !== undefined && typeof query === 'string' && typeof data !== 'object') arguments[1] = [data, ...etc]
|
|
158
|
+
return super.run(...arguments) //> important to call like that for tagged template literal args
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
/**
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
const { createPool } = require('generic-pool')
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
return _track_connections4(createPool(bound_factory, factory.options))
|
|
7
|
-
}
|
|
3
|
+
function ConnectionPool (factory, tenant) {
|
|
4
|
+
let bound_factory = { __proto__: factory, create: factory.create.bind(null, tenant) }
|
|
5
|
+
return createPool(bound_factory, factory.options)
|
|
8
6
|
}
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
function TrackedConnectionPool (factory, tenant) {
|
|
9
|
+
const pool = new ConnectionPool (factory, tenant)
|
|
12
10
|
const { acquire, release } = pool
|
|
13
11
|
return Object.assign(pool, {
|
|
14
12
|
async acquire() {
|
|
@@ -23,7 +21,6 @@ function _track_connections4(pool) {
|
|
|
23
21
|
throw err
|
|
24
22
|
}
|
|
25
23
|
},
|
|
26
|
-
|
|
27
24
|
release(dbc) {
|
|
28
25
|
this._trackedConnections?.delete(dbc._beginStack)
|
|
29
26
|
return release.call(this, dbc)
|
|
@@ -31,4 +28,5 @@ function _track_connections4(pool) {
|
|
|
31
28
|
})
|
|
32
29
|
}
|
|
33
30
|
|
|
34
|
-
|
|
31
|
+
const DEBUG = /\bpool\b/.test(process.env.DEBUG)
|
|
32
|
+
module.exports = DEBUG ? TrackedConnectionPool : ConnectionPool
|
package/lib/cqn2sql.js
CHANGED
|
@@ -8,7 +8,7 @@ const { Readable } = require('stream')
|
|
|
8
8
|
|
|
9
9
|
const DEBUG = cds.debug('sql|sqlite')
|
|
10
10
|
const LOG_SQL = cds.log('sql')
|
|
11
|
-
const LOG_SQLITE =
|
|
11
|
+
const LOG_SQLITE = cds.log('sqlite')
|
|
12
12
|
|
|
13
13
|
class CQN2SQLRenderer {
|
|
14
14
|
/**
|
|
@@ -21,10 +21,12 @@ class CQN2SQLRenderer {
|
|
|
21
21
|
this.class = new.target // for IntelliSense
|
|
22
22
|
this.class._init() // is a noop for subsequent calls
|
|
23
23
|
this.model = srv?.model
|
|
24
|
-
|
|
25
24
|
// Overwrite smart quoting
|
|
26
25
|
if (cds.env.sql.names === 'quoted') {
|
|
27
|
-
this.class.prototype.name = (name) =>
|
|
26
|
+
this.class.prototype.name = (name, query) => {
|
|
27
|
+
const e = name.id || name
|
|
28
|
+
return (query?.target || this.model?.definitions[e])?.['@cds.persistence.name'] || e
|
|
29
|
+
}
|
|
28
30
|
this.class.prototype.quote = (s) => `"${String(s).replace(/"/g, '""')}"`
|
|
29
31
|
}
|
|
30
32
|
}
|
|
@@ -84,8 +86,8 @@ class CQN2SQLRenderer {
|
|
|
84
86
|
if (vars && Object.keys(vars).length && !this.values?.length) this.values = vars
|
|
85
87
|
const sanitize_values = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
if (LOG_SQL._debug || LOG_SQLITE._debug) {
|
|
89
|
+
|
|
90
|
+
if (DEBUG && (LOG_SQL._debug || LOG_SQLITE._debug)) {
|
|
89
91
|
let values = sanitize_values && (this.entries || this.values?.length > 0) ? ['***'] : this.entries || this.values || []
|
|
90
92
|
if (values && !Array.isArray(values)) {
|
|
91
93
|
values = [values]
|
|
@@ -93,7 +95,7 @@ class CQN2SQLRenderer {
|
|
|
93
95
|
DEBUG(this.sql, ...values)
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
|
|
98
|
+
|
|
97
99
|
return this
|
|
98
100
|
}
|
|
99
101
|
|
|
@@ -124,7 +126,7 @@ class CQN2SQLRenderer {
|
|
|
124
126
|
target = typeof entity === 'string' ? { name: entity } : q.CREATE.entity
|
|
125
127
|
}
|
|
126
128
|
|
|
127
|
-
const name = this.name(target.name)
|
|
129
|
+
const name = this.name(target.name, q)
|
|
128
130
|
// Don't allow place holders inside views
|
|
129
131
|
delete this.values
|
|
130
132
|
this.sql =
|
|
@@ -213,7 +215,7 @@ class CQN2SQLRenderer {
|
|
|
213
215
|
const { target } = q
|
|
214
216
|
const isView = target?.query || target?.projection || q.DROP.view
|
|
215
217
|
const name = target?.name || q.DROP.table?.ref?.[0] || q.DROP.view?.ref?.[0]
|
|
216
|
-
return (this.sql = `DROP ${isView ? 'VIEW' : 'TABLE'} IF EXISTS ${this.quote(this.name(name))}`)
|
|
218
|
+
return (this.sql = `DROP ${isView ? 'VIEW' : 'TABLE'} IF EXISTS ${this.quote(this.name(name, q))}`)
|
|
217
219
|
}
|
|
218
220
|
|
|
219
221
|
// SELECT Statements ------------------------------------------------
|
|
@@ -352,15 +354,10 @@ class CQN2SQLRenderer {
|
|
|
352
354
|
const _aliased = as ? s => s + ` as ${this.quote(as)}` : s => s
|
|
353
355
|
if (ref) {
|
|
354
356
|
let z = ref[0]
|
|
355
|
-
if (cds.env.sql.names === 'quoted') {
|
|
356
|
-
// use SELECT.from to infer query, cds.infer also expects a query
|
|
357
|
-
const { target } = q || SELECT.from(from)
|
|
358
|
-
z = target?.['@cds.persistence.name'] || ref[0]
|
|
359
|
-
}
|
|
360
357
|
if (z.args) {
|
|
361
|
-
return _aliased(`${this.quote(this.name(z))}${this.from_args(z.args)}`)
|
|
358
|
+
return _aliased(`${this.quote(this.name(z, q))}${this.from_args(z.args)}`)
|
|
362
359
|
}
|
|
363
|
-
return _aliased(this.quote(this.name(z)))
|
|
360
|
+
return _aliased(this.quote(this.name(z, q)))
|
|
364
361
|
}
|
|
365
362
|
if (from.SELECT) return _aliased(`(${this.SELECT(from)})`)
|
|
366
363
|
if (from.join) return `${this.from(from.args[0])} ${from.join} JOIN ${this.from(from.args[1])}${from.on ? ` ON ${this.where(from.on)}` : ''}`
|
|
@@ -423,8 +420,8 @@ class CQN2SQLRenderer {
|
|
|
423
420
|
? c =>
|
|
424
421
|
this.expr(c) +
|
|
425
422
|
(c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
|
|
426
|
-
(c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
|
|
427
|
-
: c => this.expr(c) + (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC'),
|
|
423
|
+
(c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
|
|
424
|
+
: c => this.expr(c) + (c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC'),
|
|
428
425
|
)
|
|
429
426
|
}
|
|
430
427
|
|
|
@@ -504,7 +501,7 @@ class CQN2SQLRenderer {
|
|
|
504
501
|
this.columns = columns
|
|
505
502
|
|
|
506
503
|
const alias = INSERT.into.as
|
|
507
|
-
const entity = this.name(q.target?.name || INSERT.into.ref[0])
|
|
504
|
+
const entity = this.name(q.target?.name || INSERT.into.ref[0], q)
|
|
508
505
|
if (!elements) {
|
|
509
506
|
this.entries = INSERT.entries.map(e => columns.map(c => e[c]))
|
|
510
507
|
const param = this.param.bind(this, { ref: ['?'] })
|
|
@@ -531,9 +528,6 @@ class CQN2SQLRenderer {
|
|
|
531
528
|
|
|
532
529
|
async *INSERT_entries_stream(entries, binaryEncoding = 'base64') {
|
|
533
530
|
const elements = this.cqn.target?.elements || {}
|
|
534
|
-
const transformBase64 = binaryEncoding === 'base64'
|
|
535
|
-
? a => a
|
|
536
|
-
: a => a != null ? Buffer.from(a, 'base64').toString(binaryEncoding) : a
|
|
537
531
|
const bufferLimit = 65536 // 1 << 16
|
|
538
532
|
let buffer = '['
|
|
539
533
|
|
|
@@ -564,8 +558,8 @@ class CQN2SQLRenderer {
|
|
|
564
558
|
|
|
565
559
|
buffer += '"'
|
|
566
560
|
} else {
|
|
567
|
-
if (elements[key]?.type in this.BINARY_TYPES) {
|
|
568
|
-
val =
|
|
561
|
+
if (val != null && elements[key]?.type in this.BINARY_TYPES) {
|
|
562
|
+
val = Buffer.from(val, 'base64').toString(binaryEncoding)
|
|
569
563
|
}
|
|
570
564
|
buffer += `${keyJSON}${JSON.stringify(val)}`
|
|
571
565
|
}
|
|
@@ -583,9 +577,6 @@ class CQN2SQLRenderer {
|
|
|
583
577
|
|
|
584
578
|
async *INSERT_rows_stream(entries, binaryEncoding = 'base64') {
|
|
585
579
|
const elements = this.cqn.target?.elements || {}
|
|
586
|
-
const transformBase64 = binaryEncoding === 'base64'
|
|
587
|
-
? a => a
|
|
588
|
-
: a => a != null ? Buffer.from(a, 'base64').toString(binaryEncoding) : a
|
|
589
580
|
const bufferLimit = 65536 // 1 << 16
|
|
590
581
|
let buffer = '['
|
|
591
582
|
|
|
@@ -612,8 +603,8 @@ class CQN2SQLRenderer {
|
|
|
612
603
|
|
|
613
604
|
buffer += '"'
|
|
614
605
|
} else {
|
|
615
|
-
if (elements[this.columns[key]]?.type in this.BINARY_TYPES) {
|
|
616
|
-
val =
|
|
606
|
+
if (val != null && elements[this.columns[key]]?.type in this.BINARY_TYPES) {
|
|
607
|
+
val = Buffer.from(val, 'base64').toString(binaryEncoding)
|
|
617
608
|
}
|
|
618
609
|
buffer += `${sepsub}${val === undefined ? 'null' : JSON.stringify(val)}`
|
|
619
610
|
}
|
|
@@ -638,7 +629,7 @@ class CQN2SQLRenderer {
|
|
|
638
629
|
*/
|
|
639
630
|
INSERT_rows(q) {
|
|
640
631
|
const { INSERT } = q
|
|
641
|
-
const entity = this.name(q.target?.name || INSERT.into.ref[0])
|
|
632
|
+
const entity = this.name(q.target?.name || INSERT.into.ref[0], q)
|
|
642
633
|
const alias = INSERT.into.as
|
|
643
634
|
const elements = q.elements || q.target?.elements
|
|
644
635
|
const columns = this.columns = INSERT.columns || cds.error`Cannot insert rows without columns or elements`
|
|
@@ -684,7 +675,7 @@ class CQN2SQLRenderer {
|
|
|
684
675
|
*/
|
|
685
676
|
INSERT_select(q) {
|
|
686
677
|
const { INSERT } = q
|
|
687
|
-
const entity = this.name(q.target.name)
|
|
678
|
+
const entity = this.name(q.target.name, q)
|
|
688
679
|
const alias = INSERT.into.as
|
|
689
680
|
const elements = q.elements || q.target?.elements || {}
|
|
690
681
|
const columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter(
|
|
@@ -752,7 +743,7 @@ class CQN2SQLRenderer {
|
|
|
752
743
|
.filter(c => keys.includes(c.name))
|
|
753
744
|
.map(c => `${c.onInsert || c.sql} as ${this.quote(c.name)}`)
|
|
754
745
|
|
|
755
|
-
const entity = this.name(q.target?.name || UPSERT.into.ref[0])
|
|
746
|
+
const entity = this.name(q.target?.name || UPSERT.into.ref[0], q)
|
|
756
747
|
sql = `SELECT ${managed.map(c => c.upsert)} FROM (SELECT value, ${extractkeys} from json_each(?)) as NEW LEFT JOIN ${this.quote(entity)} AS OLD ON ${keyCompare}`
|
|
757
748
|
|
|
758
749
|
const updateColumns = columns.filter(c => {
|
|
@@ -779,7 +770,7 @@ class CQN2SQLRenderer {
|
|
|
779
770
|
UPDATE(q) {
|
|
780
771
|
const { entity, with: _with, data, where } = q.UPDATE
|
|
781
772
|
const elements = q.target?.elements
|
|
782
|
-
let sql = `UPDATE ${this.quote(this.name(entity.ref?.[0] || entity))}`
|
|
773
|
+
let sql = `UPDATE ${this.quote(this.name(entity.ref?.[0] || entity, q))}`
|
|
783
774
|
if (entity.as) sql += ` AS ${this.quote(entity.as)}`
|
|
784
775
|
|
|
785
776
|
let columns = []
|
|
@@ -811,8 +802,9 @@ class CQN2SQLRenderer {
|
|
|
811
802
|
* @param {import('./infer/cqn').DELETE} param0
|
|
812
803
|
* @returns {string} SQL
|
|
813
804
|
*/
|
|
814
|
-
DELETE(
|
|
815
|
-
|
|
805
|
+
DELETE(q) {
|
|
806
|
+
const { DELETE: { from, where } } = q
|
|
807
|
+
let sql = `DELETE FROM ${this.from(from, q)}`
|
|
816
808
|
if (where) sql += ` WHERE ${this.where(where)}`
|
|
817
809
|
return (this.sql = sql)
|
|
818
810
|
}
|
|
@@ -1028,6 +1020,7 @@ class CQN2SQLRenderer {
|
|
|
1028
1020
|
/**
|
|
1029
1021
|
* Calculates the Database name of the given name
|
|
1030
1022
|
* @param {string|import('./infer/cqn').ref} name
|
|
1023
|
+
* @param {import('./infer/cqn').Query} query
|
|
1031
1024
|
* @returns {string} Database name
|
|
1032
1025
|
*/
|
|
1033
1026
|
name(name) {
|
|
@@ -1150,11 +1143,6 @@ class CQN2SQLRenderer {
|
|
|
1150
1143
|
}
|
|
1151
1144
|
}
|
|
1152
1145
|
|
|
1153
|
-
// REVISIT: Workaround for JSON.stringify to work with buffers
|
|
1154
|
-
Buffer.prototype.toJSON = function () {
|
|
1155
|
-
return this.toString('base64')
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
1146
|
Readable.prototype[require('node:util').inspect.custom] = Readable.prototype.toJSON = function () { return this._raw || `[object ${this.constructor.name}]` }
|
|
1159
1147
|
|
|
1160
1148
|
const ObjectKeys = o => (o && [...ObjectKeys(o.__proto__), ...Object.keys(o)]) || []
|
package/lib/cqn4sql.js
CHANGED
|
@@ -51,7 +51,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
51
51
|
|
|
52
52
|
if (!hasCustomJoins && inferred.SELECT?.search) {
|
|
53
53
|
// we need an instance of query because the elements of the query are needed for the calculation of the search columns
|
|
54
|
-
if (!inferred.SELECT.elements) Object.setPrototypeOf(inferred,
|
|
54
|
+
if (!inferred.SELECT.elements) Object.setPrototypeOf(inferred, SELECT.class.prototype)
|
|
55
55
|
const searchTerm = getSearchTerm(inferred.SELECT.search, inferred)
|
|
56
56
|
if (searchTerm) {
|
|
57
57
|
// Search target can be a navigation, in that case use _target to get the correct entity
|
|
@@ -408,6 +408,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
408
408
|
const last = $refLinks?.[$refLinks.length - 1]
|
|
409
409
|
if (last && !last.skipExpand && last.definition.isAssociation) {
|
|
410
410
|
const expandedSubqueryColumn = expandColumn(col)
|
|
411
|
+
if (!expandedSubqueryColumn) return []
|
|
411
412
|
setElementOnColumns(expandedSubqueryColumn, col.element)
|
|
412
413
|
res.push(expandedSubqueryColumn)
|
|
413
414
|
} else if (!last?.skipExpand) {
|
|
@@ -438,7 +439,10 @@ function cqn4sql(originalQuery, model) {
|
|
|
438
439
|
|
|
439
440
|
let baseName
|
|
440
441
|
if (col.ref.length >= 2) {
|
|
441
|
-
baseName = col.ref
|
|
442
|
+
baseName = col.ref
|
|
443
|
+
.map(idOnly)
|
|
444
|
+
.slice(col.ref[0] === tableAlias ? 1 : 0, col.ref.length - 1)
|
|
445
|
+
.join('_')
|
|
442
446
|
}
|
|
443
447
|
|
|
444
448
|
let columnAlias = col.as || (col.isJoinRelevant ? col.flatName : null)
|
|
@@ -863,7 +867,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
863
867
|
* @param {Object} column - To expand.
|
|
864
868
|
* @param {Array} baseRef - The base reference for the expanded column.
|
|
865
869
|
* @param {string} subqueryAlias - The alias of the `expand` subquery column.
|
|
866
|
-
* @returns {Object} - The subquery object.
|
|
870
|
+
* @returns {Object} - The subquery object or null if the expand has a wildcard.
|
|
867
871
|
* @throws {Error} - If one of the `ref`s in the `column.expand` is not part of the GROUP BY clause.
|
|
868
872
|
*/
|
|
869
873
|
function _subqueryForGroupBy(column, baseRef, subqueryAlias) {
|
|
@@ -873,7 +877,13 @@ function cqn4sql(originalQuery, model) {
|
|
|
873
877
|
|
|
874
878
|
// to be attached to dummy query
|
|
875
879
|
const elements = {}
|
|
880
|
+
const wildcardIndex = column.expand.findIndex(e => e === '*')
|
|
881
|
+
if (wildcardIndex !== -1) {
|
|
882
|
+
// expand with wildcard vanishes as expand is part of the group by (OData $apply + $expand)
|
|
883
|
+
return null
|
|
884
|
+
}
|
|
876
885
|
const expandedColumns = column.expand.flatMap(expand => {
|
|
886
|
+
if (!expand.ref) return expand
|
|
877
887
|
const fullRef = [...baseRef, ...expand.ref]
|
|
878
888
|
|
|
879
889
|
if (expand.expand) {
|
|
@@ -976,7 +986,10 @@ function cqn4sql(originalQuery, model) {
|
|
|
976
986
|
let baseName
|
|
977
987
|
if (col.ref.length >= 2) {
|
|
978
988
|
// leaf might be intermediate structure
|
|
979
|
-
baseName = col.ref
|
|
989
|
+
baseName = col.ref
|
|
990
|
+
.map(idOnly)
|
|
991
|
+
.slice(col.ref[0] === tableAlias ? 1 : 0, col.ref.length - 1)
|
|
992
|
+
.join('_')
|
|
980
993
|
}
|
|
981
994
|
const flatColumns = getFlatColumnsFor(col, { baseName, tableAlias })
|
|
982
995
|
/**
|
|
@@ -1160,10 +1173,9 @@ function cqn4sql(originalQuery, model) {
|
|
|
1160
1173
|
else if (element.virtual === true) return []
|
|
1161
1174
|
else if (!isJoinRelevant && flatName) baseName = flatName
|
|
1162
1175
|
else if (isJoinRelevant) {
|
|
1163
|
-
const leaf = column.$refLinks
|
|
1176
|
+
const leaf = column.$refLinks.at(-1)
|
|
1164
1177
|
leafAssoc = [...column.$refLinks].reverse().find(link => link.definition.isAssociation)
|
|
1165
|
-
let elements
|
|
1166
|
-
elements = leafAssoc.definition.elements || leafAssoc.definition.foreignKeys
|
|
1178
|
+
let elements = leafAssoc.definition.elements || leafAssoc.definition.foreignKeys
|
|
1167
1179
|
if (elements && leaf.definition.name in elements) {
|
|
1168
1180
|
element = leafAssoc.definition
|
|
1169
1181
|
baseName = getFullName(leafAssoc.definition)
|
|
@@ -1203,68 +1215,76 @@ function cqn4sql(originalQuery, model) {
|
|
|
1203
1215
|
|
|
1204
1216
|
if (element.keys) {
|
|
1205
1217
|
const flatColumns = []
|
|
1206
|
-
element.keys
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1218
|
+
for (const k of element.keys) {
|
|
1219
|
+
// if only one part of a foreign key is requested, only flatten the partial key
|
|
1220
|
+
const keyElement = getElementForRef(k.ref, getDefinition(element.target))
|
|
1221
|
+
const flattenThisForeignKey =
|
|
1222
|
+
!$refLinks || // the association is passed as element, not as ref --> flatten full foreign key
|
|
1223
|
+
element === $refLinks.at(-1).definition || // the association is the leaf of the ref --> flatten full foreign key
|
|
1224
|
+
keyElement === $refLinks.at(-1).definition // the foreign key is the leaf of the ref --> only flatten this specific foreign key
|
|
1225
|
+
if (flattenThisForeignKey) {
|
|
1226
|
+
const fkElement = getElementForRef(k.ref, getDefinition(element.target))
|
|
1227
|
+
let fkBaseName
|
|
1228
|
+
if (!leafAssoc || leafAssoc.onlyForeignKeyAccess)
|
|
1229
|
+
fkBaseName = `${baseName}_${k.as || k.ref.at(-1)}`
|
|
1230
|
+
// e.g. if foreign key is accessed via infix filter - use join alias to access key in target
|
|
1231
|
+
else fkBaseName = k.ref.at(-1)
|
|
1232
|
+
const fkPath = [...csnPath, k.ref.at(-1)]
|
|
1233
|
+
if (fkElement.elements) {
|
|
1234
|
+
// structured key
|
|
1235
|
+
for (const e of Object.values(fkElement.elements)) {
|
|
1236
|
+
let alias
|
|
1237
|
+
if (columnAlias) {
|
|
1238
|
+
const fkName = k.as
|
|
1239
|
+
? `${k.as}_${e.name}` // foreign key might also be re-named: `assoc { id as foo }`
|
|
1240
|
+
: `${k.ref.join('_')}_${e.name}`
|
|
1241
|
+
alias = `${columnAlias}_${fkName}`
|
|
1242
|
+
}
|
|
1243
|
+
flatColumns.push(
|
|
1244
|
+
...getFlatColumnsFor(
|
|
1245
|
+
e,
|
|
1246
|
+
{ baseName: fkBaseName, columnAlias: alias, tableAlias },
|
|
1247
|
+
[...fkPath],
|
|
1248
|
+
excludeAndReplace,
|
|
1249
|
+
isWildcard,
|
|
1250
|
+
),
|
|
1251
|
+
)
|
|
1223
1252
|
}
|
|
1253
|
+
} else if (fkElement.isAssociation) {
|
|
1254
|
+
// assoc as key
|
|
1224
1255
|
flatColumns.push(
|
|
1225
1256
|
...getFlatColumnsFor(
|
|
1226
|
-
|
|
1227
|
-
{ baseName
|
|
1228
|
-
|
|
1257
|
+
fkElement,
|
|
1258
|
+
{ baseName, columnAlias, tableAlias },
|
|
1259
|
+
csnPath,
|
|
1229
1260
|
excludeAndReplace,
|
|
1230
1261
|
isWildcard,
|
|
1231
1262
|
),
|
|
1232
1263
|
)
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
columnAliasWithFlatFk = `${columnAlias}_${fk.as || fk.ref.join('_')}`
|
|
1256
|
-
flatColumn = { ref: [fkBaseName], as: columnAliasWithFlatFk || columnAlias }
|
|
1257
|
-
} else flatColumn = { ref: [fkBaseName] }
|
|
1258
|
-
if (tableAlias) flatColumn.ref.unshift(tableAlias)
|
|
1259
|
-
|
|
1260
|
-
// in a flat model, we must assign the foreign key rather than the key in the target
|
|
1261
|
-
const flatForeignKey = getDefinition(element.parent.name)?.elements[fkBaseName]
|
|
1262
|
-
|
|
1263
|
-
setElementOnColumns(flatColumn, flatForeignKey || fkElement)
|
|
1264
|
-
Object.defineProperty(flatColumn, '_csnPath', { value: csnPath, writable: true })
|
|
1265
|
-
flatColumns.push(flatColumn)
|
|
1264
|
+
} else {
|
|
1265
|
+
// leaf reached
|
|
1266
|
+
let flatColumn
|
|
1267
|
+
if (columnAlias) {
|
|
1268
|
+
// if the column has an explicit alias AND the original ref
|
|
1269
|
+
// directly resolves to the foreign key, we must not append the fk name to the column alias
|
|
1270
|
+
// e.g. `assoc.fk as FOO` => columns.alias = FOO
|
|
1271
|
+
// `assoc as FOO` => columns.alias = FOO_fk
|
|
1272
|
+
let columnAliasWithFlatFk
|
|
1273
|
+
if (!(column.as && fkElement === column.$refLinks?.at(-1).definition))
|
|
1274
|
+
columnAliasWithFlatFk = `${columnAlias}_${k.as || k.ref.join('_')}`
|
|
1275
|
+
flatColumn = { ref: [fkBaseName], as: columnAliasWithFlatFk || columnAlias }
|
|
1276
|
+
} else flatColumn = { ref: [fkBaseName] }
|
|
1277
|
+
if (tableAlias) flatColumn.ref.unshift(tableAlias)
|
|
1278
|
+
|
|
1279
|
+
// in a flat model, we must assign the foreign key rather than the key in the target
|
|
1280
|
+
const flatForeignKey = getDefinition(element.parent.name)?.elements[fkBaseName]
|
|
1281
|
+
|
|
1282
|
+
setElementOnColumns(flatColumn, flatForeignKey || fkElement)
|
|
1283
|
+
Object.defineProperty(flatColumn, '_csnPath', { value: csnPath, writable: true })
|
|
1284
|
+
flatColumns.push(flatColumn)
|
|
1285
|
+
}
|
|
1266
1286
|
}
|
|
1267
|
-
}
|
|
1287
|
+
}
|
|
1268
1288
|
return flatColumns
|
|
1269
1289
|
} else if (element.elements) {
|
|
1270
1290
|
const flatRefs = []
|
|
@@ -1297,7 +1317,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1297
1317
|
|
|
1298
1318
|
function getReplacement(from) {
|
|
1299
1319
|
return from?.find(replacement => {
|
|
1300
|
-
const nameOfExcludedColumn = replacement.as || replacement.ref?.
|
|
1320
|
+
const nameOfExcludedColumn = replacement.as || replacement.ref?.at(-1) || replacement
|
|
1301
1321
|
return nameOfExcludedColumn === element.name
|
|
1302
1322
|
})
|
|
1303
1323
|
}
|
|
@@ -1581,7 +1601,10 @@ function cqn4sql(originalQuery, model) {
|
|
|
1581
1601
|
if (leaf.definition.parent.kind !== 'entity')
|
|
1582
1602
|
// we need the base name
|
|
1583
1603
|
return getFlatColumnsFor(leaf.definition, {
|
|
1584
|
-
baseName: def.ref
|
|
1604
|
+
baseName: def.ref
|
|
1605
|
+
.map(idOnly)
|
|
1606
|
+
.slice(0, def.ref.length - 1)
|
|
1607
|
+
.join('_'),
|
|
1585
1608
|
tableAlias,
|
|
1586
1609
|
})
|
|
1587
1610
|
return getFlatColumnsFor(leaf.definition, { tableAlias })
|
|
@@ -1726,7 +1749,8 @@ function cqn4sql(originalQuery, model) {
|
|
|
1726
1749
|
transformedFrom.$refLinks.splice(0, transformedFrom.$refLinks.length - 1)
|
|
1727
1750
|
|
|
1728
1751
|
let args = from.ref.at(-1).args
|
|
1729
|
-
const subquerySource =
|
|
1752
|
+
const subquerySource =
|
|
1753
|
+
getDefinition(transformedFrom.$refLinks[0].definition.target) || transformedFrom.$refLinks[0].target
|
|
1730
1754
|
if (subquerySource.params && !args) args = {}
|
|
1731
1755
|
const id = localized(subquerySource)
|
|
1732
1756
|
transformedFrom.ref = [args ? { id, args } : id]
|
|
@@ -2202,10 +2226,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2202
2226
|
const xpr = search
|
|
2203
2227
|
const searchFunc = {
|
|
2204
2228
|
func: 'search',
|
|
2205
|
-
args: [
|
|
2206
|
-
{ list: searchIn },
|
|
2207
|
-
xpr.length === 1 && 'val' in xpr[0] ? xpr[0] : { xpr },
|
|
2208
|
-
],
|
|
2229
|
+
args: [{ list: searchIn }, xpr.length === 1 && 'val' in xpr[0] ? xpr[0] : { xpr }],
|
|
2209
2230
|
}
|
|
2210
2231
|
return searchFunc
|
|
2211
2232
|
} else {
|