@cap-js/db-service 1.15.1 → 1.16.1
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 +26 -0
- package/lib/common/DatabaseService.js +7 -6
- package/lib/common/generic-pool.js +7 -9
- package/lib/cql-functions.js +10 -11
- package/lib/cqn2sql.js +33 -34
- package/lib/cqn4sql.js +89 -68
- package/lib/fill-in-keys.js +1 -1
- package/lib/infer/index.js +619 -732
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@
|
|
|
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.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.16.0...db-service-v1.16.1) (2024-12-16)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
* 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)
|
|
13
|
+
* 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))
|
|
14
|
+
* quoted mode ([#937](https://github.com/cap-js/cds-dbs/issues/937)) ([9e62b22](https://github.com/cap-js/cds-dbs/commit/9e62b22a1be90ada9f57cfa63505735d8b8eed88))
|
|
15
|
+
* 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))
|
|
16
|
+
* wildcard expand with aggregation ([#923](https://github.com/cap-js/cds-dbs/issues/923)) ([bbe7be0](https://github.com/cap-js/cds-dbs/commit/bbe7be00498ad083cf951daf344b7f5fd9f68ab9))
|
|
17
|
+
|
|
18
|
+
## [1.16.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.15.1...db-service-v1.16.0) (2024-11-25)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
* single column in `search` function is also treated as CQN `list` ([#898](https://github.com/cap-js/cds-dbs/issues/898)) ([f6593e6](https://github.com/cap-js/cds-dbs/commit/f6593e69de6df3e85a39c048794a56c7eb842c4c))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
* foreignKeys aspect on association may not be set ([#903](https://github.com/cap-js/cds-dbs/issues/903)) ([732a2f3](https://github.com/cap-js/cds-dbs/commit/732a2f385074f50b87ff9715b8bdf48d28a36309))
|
|
29
|
+
* insert on uuid with default value ([#911](https://github.com/cap-js/cds-dbs/issues/911)) ([545e489](https://github.com/cap-js/cds-dbs/commit/545e489ecd07b5a3ece9441d95804fb2f3d436fa))
|
|
30
|
+
* `session_context`, `current_date`, `current_time` and `current_timestamp` are treated as SAP HANA functions and are callable in upper case ([#910](https://github.com/cap-js/cds-dbs/issues/910)) ([50ebd10](https://github.com/cap-js/cds-dbs/commit/50ebd106b9ee5bf7e1026658b89401e904ffe051))
|
|
31
|
+
* wrap values in array if it is object, so it can be spreaded ([#882](https://github.com/cap-js/cds-dbs/issues/882)) ([11f3e8b](https://github.com/cap-js/cds-dbs/commit/11f3e8bdf37d57295c1f2ffb40e217f86ec7d423))
|
|
32
|
+
|
|
7
33
|
## [1.15.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.15.0...db-service-v1.15.1) (2024-11-18)
|
|
8
34
|
|
|
9
35
|
|
|
@@ -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/cql-functions.js
CHANGED
|
@@ -33,7 +33,7 @@ const StandardFunctions = {
|
|
|
33
33
|
val = sub[2] || sub[3] || ''
|
|
34
34
|
}
|
|
35
35
|
arg.val = arg.__proto__.val = val
|
|
36
|
-
const refs = ref.list
|
|
36
|
+
const refs = ref.list
|
|
37
37
|
const { toString } = ref
|
|
38
38
|
return '(' + refs.map(ref2 => this.contains(this.tolower(toString(ref2)), this.tolower(arg))).join(' or ') + ')'
|
|
39
39
|
},
|
|
@@ -159,10 +159,6 @@ const StandardFunctions = {
|
|
|
159
159
|
|
|
160
160
|
// Date and Time Functions
|
|
161
161
|
|
|
162
|
-
current_date: p => (p ? `current_date(${p})` : 'current_date'),
|
|
163
|
-
current_time: p => (p ? `current_time(${p})` : 'current_time'),
|
|
164
|
-
current_timestamp: p => (p ? `current_timestamp(${p})` : 'current_timestamp'),
|
|
165
|
-
|
|
166
162
|
/**
|
|
167
163
|
* Generates SQL statement that produces current point in time (date and time with time zone)
|
|
168
164
|
* @returns {string}
|
|
@@ -257,20 +253,23 @@ const StandardFunctions = {
|
|
|
257
253
|
) - 0.5
|
|
258
254
|
)
|
|
259
255
|
) * 86400
|
|
260
|
-
)
|
|
256
|
+
)`
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const HANAFunctions = {
|
|
260
|
+
// https://help.sap.com/docs/SAP_HANA_PLATFORM/4fe29514fd584807ac9f2a04f6754767/f12b86a6284c4aeeb449e57eb5dd3ebd.html
|
|
261
261
|
|
|
262
262
|
/**
|
|
263
263
|
* Generates SQL statement that calls the session_context function with the given parameter
|
|
264
264
|
* @param {string} x session variable name or SQL expression
|
|
265
265
|
* @returns {string}
|
|
266
266
|
*/
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const HANAFunctions = {
|
|
271
|
-
// https://help.sap.com/docs/SAP_HANA_PLATFORM/4fe29514fd584807ac9f2a04f6754767/f12b86a6284c4aeeb449e57eb5dd3ebd.html
|
|
267
|
+
session_context: x => `session_context('${x.val}')`,
|
|
272
268
|
|
|
273
269
|
// Time functions
|
|
270
|
+
current_date: p => (p ? `current_date(${p})` : 'current_date'),
|
|
271
|
+
current_time: p => (p ? `current_time(${p})` : 'current_time'),
|
|
272
|
+
current_timestamp: p => (p ? `current_timestamp(${p})` : 'current_timestamp'),
|
|
274
273
|
/**
|
|
275
274
|
* Generates SQL statement that calculates the difference in 100nanoseconds between two timestamps
|
|
276
275
|
* @param {string} x left timestamp
|
package/lib/cqn2sql.js
CHANGED
|
@@ -6,16 +6,9 @@ const _strict_booleans = _simple_queries < 2
|
|
|
6
6
|
|
|
7
7
|
const { Readable } = require('stream')
|
|
8
8
|
|
|
9
|
-
const DEBUG = (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return cds.debug('sql|sqlite')
|
|
13
|
-
//if (DEBUG) {
|
|
14
|
-
// return DEBUG
|
|
15
|
-
// (sql, ...more) => DEBUG (sql.replace(/(?:SELECT[\n\r\s]+(json_group_array\()?[\n\r\s]*json_insert\((\n|\r|.)*?\)[\n\r\s]*\)?[\n\r\s]+as[\n\r\s]+_json_[\n\r\s]+FROM[\n\r\s]*\(|\)[\n\r\s]*(\)[\n\r\s]+AS )|\)$)/gim,(a,b,c,d) => d || ''), ...more)
|
|
16
|
-
// FIXME: looses closing ) on INSERT queries
|
|
17
|
-
//}
|
|
18
|
-
})()
|
|
9
|
+
const DEBUG = cds.debug('sql|sqlite')
|
|
10
|
+
const LOG_SQL = cds.log('sql')
|
|
11
|
+
const LOG_SQLITE = cds.log('sqlite')
|
|
19
12
|
|
|
20
13
|
class CQN2SQLRenderer {
|
|
21
14
|
/**
|
|
@@ -28,10 +21,12 @@ class CQN2SQLRenderer {
|
|
|
28
21
|
this.class = new.target // for IntelliSense
|
|
29
22
|
this.class._init() // is a noop for subsequent calls
|
|
30
23
|
this.model = srv?.model
|
|
31
|
-
|
|
32
24
|
// Overwrite smart quoting
|
|
33
25
|
if (cds.env.sql.names === 'quoted') {
|
|
34
|
-
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
|
+
}
|
|
35
30
|
this.class.prototype.quote = (s) => `"${String(s).replace(/"/g, '""')}"`
|
|
36
31
|
}
|
|
37
32
|
}
|
|
@@ -90,10 +85,17 @@ class CQN2SQLRenderer {
|
|
|
90
85
|
if (vars?.length && !this.values?.length) this.values = vars
|
|
91
86
|
if (vars && Object.keys(vars).length && !this.values?.length) this.values = vars
|
|
92
87
|
const sanitize_values = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if (DEBUG && (LOG_SQL._debug || LOG_SQLITE._debug)) {
|
|
91
|
+
let values = sanitize_values && (this.entries || this.values?.length > 0) ? ['***'] : this.entries || this.values || []
|
|
92
|
+
if (values && !Array.isArray(values)) {
|
|
93
|
+
values = [values]
|
|
94
|
+
}
|
|
95
|
+
DEBUG(this.sql, ...values)
|
|
96
|
+
}
|
|
97
|
+
|
|
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: ['?'] })
|
|
@@ -638,7 +635,7 @@ class CQN2SQLRenderer {
|
|
|
638
635
|
*/
|
|
639
636
|
INSERT_rows(q) {
|
|
640
637
|
const { INSERT } = q
|
|
641
|
-
const entity = this.name(q.target?.name || INSERT.into.ref[0])
|
|
638
|
+
const entity = this.name(q.target?.name || INSERT.into.ref[0], q)
|
|
642
639
|
const alias = INSERT.into.as
|
|
643
640
|
const elements = q.elements || q.target?.elements
|
|
644
641
|
const columns = this.columns = INSERT.columns || cds.error`Cannot insert rows without columns or elements`
|
|
@@ -684,7 +681,7 @@ class CQN2SQLRenderer {
|
|
|
684
681
|
*/
|
|
685
682
|
INSERT_select(q) {
|
|
686
683
|
const { INSERT } = q
|
|
687
|
-
const entity = this.name(q.target.name)
|
|
684
|
+
const entity = this.name(q.target.name, q)
|
|
688
685
|
const alias = INSERT.into.as
|
|
689
686
|
const elements = q.elements || q.target?.elements || {}
|
|
690
687
|
const columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter(
|
|
@@ -752,7 +749,7 @@ class CQN2SQLRenderer {
|
|
|
752
749
|
.filter(c => keys.includes(c.name))
|
|
753
750
|
.map(c => `${c.onInsert || c.sql} as ${this.quote(c.name)}`)
|
|
754
751
|
|
|
755
|
-
const entity = this.name(q.target?.name || UPSERT.into.ref[0])
|
|
752
|
+
const entity = this.name(q.target?.name || UPSERT.into.ref[0], q)
|
|
756
753
|
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
754
|
|
|
758
755
|
const updateColumns = columns.filter(c => {
|
|
@@ -779,7 +776,7 @@ class CQN2SQLRenderer {
|
|
|
779
776
|
UPDATE(q) {
|
|
780
777
|
const { entity, with: _with, data, where } = q.UPDATE
|
|
781
778
|
const elements = q.target?.elements
|
|
782
|
-
let sql = `UPDATE ${this.quote(this.name(entity.ref?.[0] || entity))}`
|
|
779
|
+
let sql = `UPDATE ${this.quote(this.name(entity.ref?.[0] || entity, q))}`
|
|
783
780
|
if (entity.as) sql += ` AS ${this.quote(entity.as)}`
|
|
784
781
|
|
|
785
782
|
let columns = []
|
|
@@ -811,8 +808,9 @@ class CQN2SQLRenderer {
|
|
|
811
808
|
* @param {import('./infer/cqn').DELETE} param0
|
|
812
809
|
* @returns {string} SQL
|
|
813
810
|
*/
|
|
814
|
-
DELETE(
|
|
815
|
-
|
|
811
|
+
DELETE(q) {
|
|
812
|
+
const { DELETE: { from, where } } = q
|
|
813
|
+
let sql = `DELETE FROM ${this.from(from, q)}`
|
|
816
814
|
if (where) sql += ` WHERE ${this.where(where)}`
|
|
817
815
|
return (this.sql = sql)
|
|
818
816
|
}
|
|
@@ -1028,6 +1026,7 @@ class CQN2SQLRenderer {
|
|
|
1028
1026
|
/**
|
|
1029
1027
|
* Calculates the Database name of the given name
|
|
1030
1028
|
* @param {string|import('./infer/cqn').ref} name
|
|
1029
|
+
* @param {import('./infer/cqn').Query} query
|
|
1031
1030
|
* @returns {string} Database name
|
|
1032
1031
|
*/
|
|
1033
1032
|
name(name) {
|
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
|
-
searchIn.length > 1 ? { list: searchIn } : { ...searchIn[0] },
|
|
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 {
|
package/lib/fill-in-keys.js
CHANGED
|
@@ -35,7 +35,7 @@ const generateUUIDandPropagateKeys = (entity, data, event) => {
|
|
|
35
35
|
if (event === 'CREATE') {
|
|
36
36
|
const keys = entity.keys
|
|
37
37
|
for (const k in keys)
|
|
38
|
-
if (keys[k].isUUID && !data[k] && !assoc4(keys[k])) //> skip key assocs, and foreign keys thereof
|
|
38
|
+
if (keys[k].isUUID && !data[k] && !assoc4(keys[k]) && !keys[k].default) //> skip key assocs, and foreign keys thereof
|
|
39
39
|
data[k] = cds.utils.uuid()
|
|
40
40
|
}
|
|
41
41
|
for (const each in entity.elements) {
|