@cap-js/db-service 1.6.4 → 1.8.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 +39 -0
- package/lib/SQLService.js +27 -8
- package/lib/common/DatabaseService.js +1 -1
- package/lib/cql-functions.js +21 -7
- package/lib/cqn2sql.js +64 -12
- package/lib/cqn4sql.js +123 -52
- package/lib/deep-queries.js +15 -2
- package/lib/infer/index.js +74 -47
- package/lib/infer/join-tree.js +22 -6
- package/lib/search.js +42 -24
- package/package.json +2 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,45 @@
|
|
|
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.8.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.7.0...db-service-v1.8.0) (2024-04-12)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* Odata built-in query functions ([#558](https://github.com/cap-js/cds-dbs/issues/558)) ([6e63367](https://github.com/cap-js/cds-dbs/commit/6e6336757129c4a9dac56f93fd768bb41d071c46))
|
|
13
|
+
* support HANA stored procedures ([#542](https://github.com/cap-js/cds-dbs/issues/542)) ([52a00a0](https://github.com/cap-js/cds-dbs/commit/52a00a0d642ba3c58dcad97b3ea1456f1bf3b04a))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
* **`expand`:** Only accept on structures, assocs or table aliases ([#551](https://github.com/cap-js/cds-dbs/issues/551)) ([3248512](https://github.com/cap-js/cds-dbs/commit/32485129147cd1b376f1d2faf2ea7c7232ba3794))
|
|
19
|
+
* **`order by`:** for localized sorting, prepend table alias ([#546](https://github.com/cap-js/cds-dbs/issues/546)) ([a273a92](https://github.com/cap-js/cds-dbs/commit/a273a9278b2551ed3381795effe28cf8de41b1bd))
|
|
20
|
+
* etag with stream_compat ([#562](https://github.com/cap-js/cds-dbs/issues/562)) ([b0a3a41](https://github.com/cap-js/cds-dbs/commit/b0a3a418fbcff7eb7e7b8fa4ff031e1c0c0faac4))
|
|
21
|
+
* exclude `cds.LargeBinary` from wildcard expansion ([#577](https://github.com/cap-js/cds-dbs/issues/577)) ([6661d63](https://github.com/cap-js/cds-dbs/commit/6661d635b2895a13d47e42495acf6fbd7247c535))
|
|
22
|
+
* Reduce insert queries for deep update ([#568](https://github.com/cap-js/cds-dbs/issues/568)) ([55e5114](https://github.com/cap-js/cds-dbs/commit/55e511471743c0445d41e8297f5530abe167a270))
|
|
23
|
+
* Reduced count query complexity when possible ([#553](https://github.com/cap-js/cds-dbs/issues/553)) ([3331f02](https://github.com/cap-js/cds-dbs/commit/3331f0224f02bd2e6cc9c6d2cd5f1c37a36ec8dd))
|
|
24
|
+
|
|
25
|
+
## [1.7.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.6.4...db-service-v1.7.0) (2024-03-22)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
* also support lowercase matchespattern function ([#528](https://github.com/cap-js/cds-dbs/issues/528)) ([6ea574e](https://github.com/cap-js/cds-dbs/commit/6ea574ee67ef5e42e4f8ccbe4fe91b46097de129))
|
|
31
|
+
* forUpdate and forShareLock ([#148](https://github.com/cap-js/cds-dbs/issues/148)) ([99a1170](https://github.com/cap-js/cds-dbs/commit/99a1170e61de4fd0c505834c25a9c03fc34da85b))
|
|
32
|
+
* **hana:** drop prepared statements after end of transaction ([#537](https://github.com/cap-js/cds-dbs/issues/537)) ([b1f864e](https://github.com/cap-js/cds-dbs/commit/b1f864e0a3a0e5efacd803d3709379cab76d61cc))
|
|
33
|
+
* **hana:** Add views with parameters support ([#488](https://github.com/cap-js/cds-dbs/issues/488)) ([3790ec0](https://github.com/cap-js/cds-dbs/commit/3790ec0178aab2cdb429272bb3e813b13441785c))
|
|
34
|
+
* **orderby:** allow to disable collations with [@cds](https://github.com/cds).collate: false ([#492](https://github.com/cap-js/cds-dbs/issues/492)) ([820f971](https://github.com/cap-js/cds-dbs/commit/820f971e1ad21fa8f8ca289c1e29b373365df484))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
* **cqn2sql:** Smart quoting of columns inside UPSERT rows ([#519](https://github.com/cap-js/cds-dbs/issues/519)) ([78fe10b](https://github.com/cap-js/cds-dbs/commit/78fe10b1df3691614dc77b1d4f82df10a1d641d3))
|
|
40
|
+
* Getting rid of quirks mode ([#514](https://github.com/cap-js/cds-dbs/issues/514)) ([c9aa6e8](https://github.com/cap-js/cds-dbs/commit/c9aa6e835761ace38447f37cad6a5f39cb0b910c))
|
|
41
|
+
* issue with reused select cqns ([#505](https://github.com/cap-js/cds-dbs/issues/505)) ([916d175](https://github.com/cap-js/cds-dbs/commit/916d1756422f0caf02c323052f2addafed39182a))
|
|
42
|
+
* joins without columns are rejected ([#535](https://github.com/cap-js/cds-dbs/issues/535)) ([eb9beda](https://github.com/cap-js/cds-dbs/commit/eb9beda728de60081d7afbfcd49305eeb241f3fb))
|
|
43
|
+
* **search:** dont search non string aggregations ([#527](https://github.com/cap-js/cds-dbs/issues/527)) ([c87900c](https://github.com/cap-js/cds-dbs/commit/c87900cb157041a6ff76c45192c1d33180840d0f))
|
|
44
|
+
* **search:** search on aggregated results in HAVING clause ([#524](https://github.com/cap-js/cds-dbs/issues/524)) ([61d348e](https://github.com/cap-js/cds-dbs/commit/61d348ebc2528b7f1c6da8c78a7455a438e1b7cf))
|
|
45
|
+
|
|
7
46
|
## [1.6.4](https://github.com/cap-js/cds-dbs/compare/db-service-v1.6.3...db-service-v1.6.4) (2024-02-28)
|
|
8
47
|
|
|
9
48
|
|
package/lib/SQLService.js
CHANGED
|
@@ -123,15 +123,26 @@ class SQLService extends DatabaseService {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
const { sql, values, cqn } = this.cqn2sql(query, data)
|
|
126
|
+
const expand = query.SELECT.expand
|
|
127
|
+
delete query.SELECT.expand
|
|
128
|
+
|
|
126
129
|
let ps = await this.prepare(sql)
|
|
127
130
|
let rows = await ps.all(values)
|
|
128
131
|
if (rows.length)
|
|
129
|
-
if (
|
|
132
|
+
if (expand) rows = rows.map(r => (typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r))
|
|
130
133
|
|
|
131
134
|
if (cds.env.features.stream_compat) {
|
|
132
135
|
if (query._streaming) {
|
|
133
136
|
this._changeToStreams(cqn.SELECT.columns, rows, true, true)
|
|
134
|
-
|
|
137
|
+
if (!rows.length) return
|
|
138
|
+
|
|
139
|
+
const result = rows[0]
|
|
140
|
+
// stream is always on position 0. Further properties like etag are inserted later.
|
|
141
|
+
let [key, val] = Object.entries(result)[0]
|
|
142
|
+
result.value = val
|
|
143
|
+
delete result[key]
|
|
144
|
+
|
|
145
|
+
return result
|
|
135
146
|
}
|
|
136
147
|
} else {
|
|
137
148
|
this._changeToStreams(cqn.SELECT.columns, rows, query.SELECT.one, false)
|
|
@@ -262,7 +273,7 @@ class SQLService extends DatabaseService {
|
|
|
262
273
|
* @param {string} sql
|
|
263
274
|
*/
|
|
264
275
|
hasResults(sql) {
|
|
265
|
-
return
|
|
276
|
+
return /^\s*(SELECT|WITH|CALL|PRAGMA table_info)/i.test(sql)
|
|
266
277
|
}
|
|
267
278
|
|
|
268
279
|
/**
|
|
@@ -278,17 +289,23 @@ class SQLService extends DatabaseService {
|
|
|
278
289
|
const [max, offset = 0] = one ? [1] : _ ? [_.rows?.val, _.offset?.val] : []
|
|
279
290
|
if (max === undefined || (n < max && (n || !offset))) return n + offset
|
|
280
291
|
}
|
|
281
|
-
|
|
282
|
-
|
|
292
|
+
|
|
293
|
+
// Keep original query columns when potentially used insde conditions
|
|
294
|
+
const { having, groupBy } = query.SELECT
|
|
295
|
+
const columns = (having?.length || groupBy?.length)
|
|
296
|
+
? query.SELECT.columns.filter(c => !c.expand)
|
|
297
|
+
: [{ val: 1 }]
|
|
298
|
+
const cq = SELECT.one([{ func: 'count' }]).from(
|
|
283
299
|
cds.ql.clone(query, {
|
|
300
|
+
columns,
|
|
284
301
|
localized: false,
|
|
285
302
|
expand: false,
|
|
286
303
|
limit: undefined,
|
|
287
304
|
orderBy: undefined,
|
|
288
305
|
}),
|
|
289
306
|
)
|
|
290
|
-
const { count
|
|
291
|
-
return count
|
|
307
|
+
const { count } = await this.onSELECT({ query: cq })
|
|
308
|
+
return count
|
|
292
309
|
}
|
|
293
310
|
|
|
294
311
|
/**
|
|
@@ -428,6 +445,8 @@ SQLService.prototype.PreparedStatement = PreparedStatement
|
|
|
428
445
|
|
|
429
446
|
const _target_name4 = q => {
|
|
430
447
|
const target =
|
|
448
|
+
q._target_ref ||
|
|
449
|
+
q.from_into_ntt ||
|
|
431
450
|
q.SELECT?.from ||
|
|
432
451
|
q.INSERT?.into ||
|
|
433
452
|
q.UPSERT?.into ||
|
|
@@ -441,7 +460,7 @@ const _target_name4 = q => {
|
|
|
441
460
|
return first.id || first
|
|
442
461
|
}
|
|
443
462
|
|
|
444
|
-
const _unquirked = q => {
|
|
463
|
+
const _unquirked = !cds.env.ql.quirks_mode ? q => q : q => {
|
|
445
464
|
if (!q) return q
|
|
446
465
|
else if (typeof q.SELECT?.from === 'string') q.SELECT.from = { ref: [q.SELECT.from] }
|
|
447
466
|
else if (typeof q.INSERT?.into === 'string') q.INSERT.into = { ref: [q.INSERT.into] }
|
|
@@ -132,9 +132,9 @@ class DatabaseService extends cds.Service {
|
|
|
132
132
|
const tenants = tenant ? [tenant] : Object.keys(this.pools)
|
|
133
133
|
await Promise.all (tenants.map (async t => {
|
|
134
134
|
const pool = this.pools[t]; if (!pool) return
|
|
135
|
+
delete this.pools[t]
|
|
135
136
|
await pool.drain()
|
|
136
137
|
await pool.clear()
|
|
137
|
-
delete this.pools[t]
|
|
138
138
|
}))
|
|
139
139
|
}
|
|
140
140
|
|
package/lib/cql-functions.js
CHANGED
|
@@ -99,6 +99,13 @@ const StandardFunctions = {
|
|
|
99
99
|
* @returns {string}
|
|
100
100
|
*/
|
|
101
101
|
matchesPattern: (x, y) => `(${x} regexp ${y})`,
|
|
102
|
+
/**
|
|
103
|
+
* Generates SQL statement that matches the given string against a regular expression
|
|
104
|
+
* @param {string} x
|
|
105
|
+
* @param {string} y
|
|
106
|
+
* @returns {string}
|
|
107
|
+
*/
|
|
108
|
+
matchespattern: (x, y) => `(${x} regexp ${y})`,
|
|
102
109
|
/**
|
|
103
110
|
* Generates SQL statement that produces the lower case value of a given string
|
|
104
111
|
* @param {string} x
|
|
@@ -145,6 +152,13 @@ const StandardFunctions = {
|
|
|
145
152
|
current_time: p => (p ? `current_time(${p})` : 'current_time'),
|
|
146
153
|
current_timestamp: p => (p ? `current_timestamp(${p})` : 'current_timestamp'),
|
|
147
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Generates SQL statement that produces current point in time (date and time with time zone)
|
|
157
|
+
* @returns {string}
|
|
158
|
+
*/
|
|
159
|
+
now: function() {
|
|
160
|
+
return this.session_context({val: '$now'})
|
|
161
|
+
},
|
|
148
162
|
/**
|
|
149
163
|
* Generates SQL statement that produces the year of a given timestamp
|
|
150
164
|
* @param {string} x
|
|
@@ -182,29 +196,27 @@ const StandardFunctions = {
|
|
|
182
196
|
* /
|
|
183
197
|
second: x => `cast( strftime('%S',${x}) as Integer )`,
|
|
184
198
|
|
|
199
|
+
// REVISIT: make precision configurable
|
|
185
200
|
/**
|
|
186
201
|
* Generates SQL statement that produces the fractional seconds of a given timestamp
|
|
187
202
|
* @param {string} x
|
|
188
203
|
* @returns {string}
|
|
189
204
|
*/
|
|
190
|
-
fractionalseconds: x => `cast( strftime('%
|
|
205
|
+
fractionalseconds: x => `cast( substr( strftime('%f', ${x}), length(strftime('%f', ${x})) - 3) as REAL)`,
|
|
191
206
|
|
|
192
207
|
/**
|
|
193
208
|
* maximum date time value
|
|
194
209
|
* @returns {string}
|
|
195
210
|
*/
|
|
196
|
-
maxdatetime: () => '9999-12-
|
|
211
|
+
maxdatetime: () => "'9999-12-31T23:59:59.999Z'",
|
|
197
212
|
/**
|
|
198
213
|
* minimum date time value
|
|
199
214
|
* @returns {string}
|
|
200
215
|
*/
|
|
201
|
-
mindatetime: () => '0001-01-
|
|
216
|
+
mindatetime: () => "'0001-01-01T00:00:00.000Z'",
|
|
202
217
|
|
|
203
218
|
// odata spec defines the date time offset type as a normal ISO time stamp
|
|
204
219
|
// Where the timezone can either be 'Z' (for UTC) or [+|-]xx:xx for the time offset
|
|
205
|
-
// sqlite understands this so by splitting the timezone from the actual date
|
|
206
|
-
// prefixing it with 1970 it allows sqlite to give back the number of seconds
|
|
207
|
-
// which can be divided by 60 back to minutes
|
|
208
220
|
/**
|
|
209
221
|
* Generates SQL statement that produces the offset in minutes of a given date time offset string
|
|
210
222
|
* @param {string} x
|
|
@@ -212,7 +224,9 @@ const StandardFunctions = {
|
|
|
212
224
|
*/
|
|
213
225
|
totaloffsetminutes: x => `case
|
|
214
226
|
when substr(${x}, length(${x})) = 'z' then 0
|
|
215
|
-
else
|
|
227
|
+
else sign( cast( substr(${x}, length(${x}) - 5) as Integer )) *
|
|
228
|
+
( cast( strftime('%H', substr(${x}, length(${x}) - 4 )) as Integer ) * 60 +
|
|
229
|
+
cast( strftime('%M', substr(${x},length(${x}) - 4 )) as Integer ))
|
|
216
230
|
end`,
|
|
217
231
|
|
|
218
232
|
// odata spec defines the value format for totalseconds as a duration like: P12DT23H59M59.999999999999S
|
package/lib/cqn2sql.js
CHANGED
|
@@ -40,7 +40,9 @@ class CQN2SQLRenderer {
|
|
|
40
40
|
for (let each in mixins) {
|
|
41
41
|
const def = types[each]
|
|
42
42
|
if (!def) continue
|
|
43
|
-
|
|
43
|
+
const value = mixins[each]
|
|
44
|
+
if (value?.get) Object.defineProperty(def, fqn, { get: value.get })
|
|
45
|
+
else Object.defineProperty(def, fqn, { value })
|
|
44
46
|
}
|
|
45
47
|
return fqn
|
|
46
48
|
}
|
|
@@ -193,7 +195,7 @@ class CQN2SQLRenderer {
|
|
|
193
195
|
DROP(q) {
|
|
194
196
|
const { target } = q
|
|
195
197
|
const isView = target.query || target.projection
|
|
196
|
-
return (this.sql = `DROP ${isView ? 'VIEW' : 'TABLE'} IF EXISTS ${this.name(target.name)}`)
|
|
198
|
+
return (this.sql = `DROP ${isView ? 'VIEW' : 'TABLE'} IF EXISTS ${this.quote(this.name(target.name))}`)
|
|
197
199
|
}
|
|
198
200
|
|
|
199
201
|
// SELECT Statements ------------------------------------------------
|
|
@@ -203,7 +205,13 @@ class CQN2SQLRenderer {
|
|
|
203
205
|
* @param {import('./infer/cqn').SELECT} q
|
|
204
206
|
*/
|
|
205
207
|
SELECT(q) {
|
|
206
|
-
let { from, expand, where, groupBy, having, orderBy, limit, one, distinct, localized } =
|
|
208
|
+
let { from, expand, where, groupBy, having, orderBy, limit, one, distinct, localized, forUpdate, forShareLock } =
|
|
209
|
+
q.SELECT
|
|
210
|
+
|
|
211
|
+
if (from?.join && !q.SELECT.columns) {
|
|
212
|
+
throw new Error('CQN query using joins must specify the selected columns.')
|
|
213
|
+
}
|
|
214
|
+
|
|
207
215
|
// REVISIT: When selecting from an entity that is not in the model the from.where are not normalized (as cqn4sql is skipped)
|
|
208
216
|
if (!where && from?.ref?.length === 1 && from.ref[0]?.where) where = from.ref[0]?.where
|
|
209
217
|
let columns = this.SELECT_columns(q)
|
|
@@ -217,6 +225,8 @@ class CQN2SQLRenderer {
|
|
|
217
225
|
if (!_empty(orderBy)) sql += ` ORDER BY ${this.orderBy(orderBy, localized)}`
|
|
218
226
|
if (one) limit = Object.assign({}, limit, { rows: { val: 1 } })
|
|
219
227
|
if (limit) sql += ` LIMIT ${this.limit(limit)}`
|
|
228
|
+
if (forUpdate) sql += ` ${this.forUpdate(forUpdate)}`
|
|
229
|
+
else if (forShareLock) sql += ` ${this.forShareLock(forShareLock)}`
|
|
220
230
|
// Expand cannot work without an inferred query
|
|
221
231
|
if (expand) {
|
|
222
232
|
if ('elements' in q) sql = this.SELECT_expand(q, sql)
|
|
@@ -303,12 +313,28 @@ class CQN2SQLRenderer {
|
|
|
303
313
|
from(from) {
|
|
304
314
|
const { ref, as } = from
|
|
305
315
|
const _aliased = as ? s => s + ` as ${this.quote(as)}` : s => s
|
|
306
|
-
if (ref)
|
|
316
|
+
if (ref) {
|
|
317
|
+
const z = ref[0]
|
|
318
|
+
if (z.args) {
|
|
319
|
+
return _aliased(`${this.quote(this.name(z))}${this.from_args(z.args)}`)
|
|
320
|
+
}
|
|
321
|
+
return _aliased(this.quote(this.name(z)))
|
|
322
|
+
}
|
|
307
323
|
if (from.SELECT) return _aliased(`(${this.SELECT(from)})`)
|
|
308
324
|
if (from.join)
|
|
309
325
|
return `${this.from(from.args[0])} ${from.join} JOIN ${this.from(from.args[1])} ON ${this.where(from.on)}`
|
|
310
326
|
}
|
|
311
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Renders a FROM clause into generic SQL
|
|
330
|
+
* @param {import('./infer/cqn').ref['ref'][0]['args']} args
|
|
331
|
+
* @returns {string} SQL
|
|
332
|
+
*/
|
|
333
|
+
from_args(args) {
|
|
334
|
+
args
|
|
335
|
+
cds.error`Parameterized views are not supported by ${this.constructor.name}`
|
|
336
|
+
}
|
|
337
|
+
|
|
312
338
|
/**
|
|
313
339
|
* Renders a WHERE clause into generic SQL
|
|
314
340
|
* @param {import('./infer/cqn').predicate} xpr
|
|
@@ -364,6 +390,32 @@ class CQN2SQLRenderer {
|
|
|
364
390
|
return !offset ? rows.val : `${rows.val} OFFSET ${offset.val}`
|
|
365
391
|
}
|
|
366
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Renders an forUpdate clause into generic SQL
|
|
395
|
+
* @param {import('./infer/cqn').SELECT["SELECT"]["forUpdate"]} update
|
|
396
|
+
* @returns {string} SQL
|
|
397
|
+
*/
|
|
398
|
+
forUpdate(update) {
|
|
399
|
+
const { wait, of } = update
|
|
400
|
+
let sql = 'FOR UPDATE'
|
|
401
|
+
if (!_empty(of)) sql += ` OF ${of.map(x => this.expr(x)).join(', ')}`
|
|
402
|
+
if (typeof wait === 'number') sql += ` WAIT ${wait}`
|
|
403
|
+
return sql
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Renders an forShareLock clause into generic SQL
|
|
408
|
+
* @param {import('./infer/cqn').SELECT["SELECT"]["forShareLock"]} update
|
|
409
|
+
* @returns {string} SQL
|
|
410
|
+
*/
|
|
411
|
+
forShareLock(lock) {
|
|
412
|
+
const { wait, of } = lock
|
|
413
|
+
let sql = 'FOR SHARE LOCK'
|
|
414
|
+
if (!_empty(of)) sql += ` OF ${of.map(x => this.expr(x)).join(', ')}`
|
|
415
|
+
if (typeof wait === 'number') sql += ` WAIT ${wait}`
|
|
416
|
+
return sql
|
|
417
|
+
}
|
|
418
|
+
|
|
367
419
|
// INSERT Statements ------------------------------------------------
|
|
368
420
|
|
|
369
421
|
/**
|
|
@@ -402,12 +454,12 @@ class CQN2SQLRenderer {
|
|
|
402
454
|
: ObjectKeys(INSERT.entries[0])
|
|
403
455
|
|
|
404
456
|
/** @type {string[]} */
|
|
405
|
-
this.columns = columns.filter(elements ? c => !elements[c]?.['@cds.extension'] : () => true)
|
|
457
|
+
this.columns = columns.filter(elements ? c => !elements[c]?.['@cds.extension'] : () => true)
|
|
406
458
|
|
|
407
459
|
if (!elements) {
|
|
408
460
|
this.entries = INSERT.entries.map(e => columns.map(c => e[c]))
|
|
409
461
|
const param = this.param.bind(this, { ref: ['?'] })
|
|
410
|
-
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns}) VALUES (${columns.map(param)})`)
|
|
462
|
+
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))}) VALUES (${columns.map(param)})`)
|
|
411
463
|
}
|
|
412
464
|
|
|
413
465
|
const extractions = this.managed(
|
|
@@ -446,7 +498,7 @@ class CQN2SQLRenderer {
|
|
|
446
498
|
this.entries = [[...this.values, stream]]
|
|
447
499
|
}
|
|
448
500
|
|
|
449
|
-
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns
|
|
501
|
+
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))
|
|
450
502
|
}) SELECT ${extraction} FROM json_each(?)`)
|
|
451
503
|
}
|
|
452
504
|
|
|
@@ -573,12 +625,12 @@ class CQN2SQLRenderer {
|
|
|
573
625
|
return converter?.(extract, element) || extract
|
|
574
626
|
})
|
|
575
627
|
|
|
576
|
-
this.columns = columns
|
|
628
|
+
this.columns = columns
|
|
577
629
|
|
|
578
630
|
if (!elements) {
|
|
579
631
|
this.entries = INSERT.rows
|
|
580
632
|
const param = this.param.bind(this, { ref: ['?'] })
|
|
581
|
-
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns}) VALUES (${columns.map(param)})`)
|
|
633
|
+
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))}) VALUES (${columns.map(param)})`)
|
|
582
634
|
}
|
|
583
635
|
|
|
584
636
|
if (INSERT.rows[0] instanceof Readable) {
|
|
@@ -590,7 +642,7 @@ class CQN2SQLRenderer {
|
|
|
590
642
|
this.entries = [[...this.values, stream]]
|
|
591
643
|
}
|
|
592
644
|
|
|
593
|
-
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns
|
|
645
|
+
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))
|
|
594
646
|
}) SELECT ${extraction} FROM json_each(?)`)
|
|
595
647
|
}
|
|
596
648
|
|
|
@@ -617,7 +669,7 @@ class CQN2SQLRenderer {
|
|
|
617
669
|
const columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter(
|
|
618
670
|
c => c in elements && !elements[c].virtual && !elements[c].isAssociation,
|
|
619
671
|
))
|
|
620
|
-
this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns}) ${this.SELECT(
|
|
672
|
+
this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns.map(c => this.quote(c))}) ${this.SELECT(
|
|
621
673
|
this.cqn4sql(INSERT.as),
|
|
622
674
|
)}`
|
|
623
675
|
this.entries = [this.values]
|
|
@@ -641,7 +693,7 @@ class CQN2SQLRenderer {
|
|
|
641
693
|
/** @type {import('./converters').Converters} */
|
|
642
694
|
static OutputConverters = {} // subclasses to override
|
|
643
695
|
|
|
644
|
-
static localized = { String:
|
|
696
|
+
static localized = { String: { get() { return this['@cds.collate'] !== false } }, UUID: false }
|
|
645
697
|
|
|
646
698
|
// UPSERT Statements ------------------------------------------------
|
|
647
699
|
|
package/lib/cqn4sql.js
CHANGED
|
@@ -111,7 +111,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
111
111
|
|
|
112
112
|
// calculate the primary keys of the target entity, there is always exactly
|
|
113
113
|
// one query source for UPDATE / DELETE
|
|
114
|
-
const queryTarget = Object.values(originalQuery.sources)[0]
|
|
114
|
+
const queryTarget = Object.values(originalQuery.sources)[0].definition
|
|
115
115
|
const keys = Object.values(queryTarget.elements).filter(e => e.key === true)
|
|
116
116
|
const primaryKey = { list: [] }
|
|
117
117
|
keys.forEach(k => {
|
|
@@ -181,10 +181,9 @@ function cqn4sql(originalQuery, model) {
|
|
|
181
181
|
|
|
182
182
|
if (inferred.SELECT.search) {
|
|
183
183
|
// Search target can be a navigation, in that case use _target to get the correct entity
|
|
184
|
-
const where =
|
|
185
|
-
if (where)
|
|
186
|
-
|
|
187
|
-
}
|
|
184
|
+
const { where, having } = transformSearch(inferred.SELECT.search, transformedFrom) || {}
|
|
185
|
+
if (where) transformedQuery.SELECT.where = where
|
|
186
|
+
else if (having) transformedQuery.SELECT.having = having
|
|
188
187
|
}
|
|
189
188
|
return transformedQuery
|
|
190
189
|
}
|
|
@@ -204,20 +203,26 @@ function cqn4sql(originalQuery, model) {
|
|
|
204
203
|
}
|
|
205
204
|
|
|
206
205
|
/**
|
|
207
|
-
* Transforms a search expression
|
|
206
|
+
* Transforms a search expression into a WHERE or HAVING clause for a SELECT operation, depending on the context of the query.
|
|
207
|
+
* The function decides whether to use a WHERE or HAVING clause based on the presence of aggregated columns in the search criteria.
|
|
208
208
|
*
|
|
209
|
-
* @param {object} search - The search expression
|
|
209
|
+
* @param {object} search - The search expression to be applied to the searchable columns within the query source.
|
|
210
210
|
* @param {object} from - The FROM clause of the CQN statement.
|
|
211
211
|
*
|
|
212
|
-
* @returns {(Object|Array|
|
|
213
|
-
* If the
|
|
214
|
-
*
|
|
215
|
-
* If the
|
|
212
|
+
* @returns {(Object|Array|null)} - The function returns an object representing the WHERE or HAVING clause of the query:
|
|
213
|
+
* - If the target of the query contains searchable elements, an array representing the WHERE or HAVING clause is returned.
|
|
214
|
+
* This includes appending to an existing clause with an AND condition or creating a new clause solely with the 'contains' clause.
|
|
215
|
+
* - If the SELECT query does not initially contain a WHERE or HAVING clause, the returned object solely consists of the 'contains' clause.
|
|
216
|
+
* - If the target entity of the query does not contain searchable elements, the function returns null.
|
|
216
217
|
*
|
|
218
|
+
* Note: The WHERE clause is used for filtering individual rows before any aggregation occurs.
|
|
219
|
+
* The HAVING clause is utilized for conditions on aggregated data, applied after grouping operations.
|
|
217
220
|
*/
|
|
218
|
-
function
|
|
219
|
-
const entity = from.$refLinks[0].definition.
|
|
220
|
-
|
|
221
|
+
function transformSearch(search, from) {
|
|
222
|
+
const entity = getDefinition(from.$refLinks[0].definition.target) || from.$refLinks[0].definition
|
|
223
|
+
// pass transformedQuery because we may need to search in the columns directly
|
|
224
|
+
// in case of aggregation
|
|
225
|
+
const searchIn = computeColumnsToBeSearched(transformedQuery, entity, from.as)
|
|
221
226
|
if (searchIn.length > 0) {
|
|
222
227
|
const xpr = search
|
|
223
228
|
const contains = {
|
|
@@ -228,10 +233,16 @@ function cqn4sql(originalQuery, model) {
|
|
|
228
233
|
],
|
|
229
234
|
}
|
|
230
235
|
|
|
231
|
-
if
|
|
232
|
-
|
|
236
|
+
// if the query is grouped and the queries columns contain an aggregate function,
|
|
237
|
+
// we must put the search term into the `having` clause, as the search expression
|
|
238
|
+
// is defined on the aggregated result, not on the individual rows
|
|
239
|
+
let prop = 'where'
|
|
240
|
+
|
|
241
|
+
if (inferred.SELECT.groupBy && searchIn.some(c => c.func || c.xpr)) prop = 'having'
|
|
242
|
+
if (transformedQuery.SELECT[prop]) {
|
|
243
|
+
return { [prop]: [asXpr(transformedQuery.SELECT.where), 'and', contains] }
|
|
233
244
|
} else {
|
|
234
|
-
return [contains]
|
|
245
|
+
return { [prop]: [contains] }
|
|
235
246
|
}
|
|
236
247
|
} else {
|
|
237
248
|
return null
|
|
@@ -255,9 +266,12 @@ function cqn4sql(originalQuery, model) {
|
|
|
255
266
|
*/
|
|
256
267
|
const alreadySeen = new Map()
|
|
257
268
|
inferred.joinTree._roots.forEach(r => {
|
|
258
|
-
const args =
|
|
259
|
-
|
|
260
|
-
|
|
269
|
+
const args = []
|
|
270
|
+
if (r.queryArtifact.SELECT) args.push({ SELECT: transformSubquery(r.queryArtifact).SELECT, as: r.alias })
|
|
271
|
+
else {
|
|
272
|
+
const id = localized(r.queryArtifact)
|
|
273
|
+
args.push({ ref: [r.args ? { id, args: r.args } : id], as: r.alias })
|
|
274
|
+
}
|
|
261
275
|
from = { join: 'left', args, on: [] }
|
|
262
276
|
r.children.forEach(c => {
|
|
263
277
|
from = joinForBranch(from, c)
|
|
@@ -282,10 +296,13 @@ function cqn4sql(originalQuery, model) {
|
|
|
282
296
|
),
|
|
283
297
|
)
|
|
284
298
|
|
|
299
|
+
const id = localized(getDefinition(nextAssoc.$refLink.definition.target))
|
|
300
|
+
const { args } = nextAssoc
|
|
285
301
|
const arg = {
|
|
286
|
-
ref: [
|
|
302
|
+
ref: [args ? { id, args } : id],
|
|
287
303
|
as: nextAssoc.$refLink.alias,
|
|
288
304
|
}
|
|
305
|
+
|
|
289
306
|
lhs.args.push(arg)
|
|
290
307
|
alreadySeen.set(nextAssoc.$refLink.alias, true)
|
|
291
308
|
if (nextAssoc.where) {
|
|
@@ -438,7 +455,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
438
455
|
const refNavigation = col.ref.slice(col.$refLinks[0].definition.kind !== 'element' ? 1 : 0).join('_')
|
|
439
456
|
if (!columnAlias && col.flatName && col.flatName !== refNavigation) columnAlias = refNavigation
|
|
440
457
|
|
|
441
|
-
if (col.$refLinks.some(link => link.definition.
|
|
458
|
+
if (col.$refLinks.some(link => getDefinition(link.definition.target)?.['@cds.persistence.skip'] === true)) return
|
|
442
459
|
|
|
443
460
|
const flatColumns = getFlatColumnsFor(col, { baseName, columnAlias, tableAlias })
|
|
444
461
|
flatColumns.forEach(flatColumn => {
|
|
@@ -472,13 +489,17 @@ function cqn4sql(originalQuery, model) {
|
|
|
472
489
|
|
|
473
490
|
function getTransformedColumn(col) {
|
|
474
491
|
if (col.xpr) {
|
|
475
|
-
|
|
492
|
+
const xpr = { xpr: getTransformedTokenStream(col.xpr) }
|
|
493
|
+
if (col.cast) xpr.cast = col.cast
|
|
494
|
+
return xpr
|
|
476
495
|
} else if (col.func) {
|
|
477
|
-
|
|
496
|
+
const func = {
|
|
478
497
|
func: col.func,
|
|
479
498
|
args: col.args && getTransformedTokenStream(col.args),
|
|
480
|
-
as: col.func,
|
|
499
|
+
as: col.func, // may be overwritten by the explicit alias
|
|
481
500
|
}
|
|
501
|
+
if (col.cast) func.cast = col.cast
|
|
502
|
+
return func
|
|
482
503
|
} else {
|
|
483
504
|
return copy(col)
|
|
484
505
|
}
|
|
@@ -680,7 +701,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
680
701
|
// select from books { { * } as bar }
|
|
681
702
|
// only possible if there is exactly one query source
|
|
682
703
|
if (!baseRef.length) {
|
|
683
|
-
const [tableAlias, definition] = Object.entries(inferred.sources)[0]
|
|
704
|
+
const [tableAlias, { definition }] = Object.entries(inferred.sources)[0]
|
|
684
705
|
baseRef.push(tableAlias)
|
|
685
706
|
baseRefLinks.push({ definition, source: definition })
|
|
686
707
|
}
|
|
@@ -833,7 +854,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
833
854
|
function getTransformedOrderByGroupBy(columns, inOrderBy = false) {
|
|
834
855
|
const res = []
|
|
835
856
|
for (let i = 0; i < columns.length; i++) {
|
|
836
|
-
|
|
857
|
+
let col = columns[i]
|
|
837
858
|
if (isCalculatedOnRead(col.$refLinks?.[col.$refLinks.length - 1].definition)) {
|
|
838
859
|
const calcElement = resolveCalculatedElement(col, true)
|
|
839
860
|
res.push(calcElement)
|
|
@@ -849,14 +870,44 @@ function cqn4sql(originalQuery, model) {
|
|
|
849
870
|
} else if (pseudos.elements[col.ref?.[0]]) {
|
|
850
871
|
res.push({ ...col })
|
|
851
872
|
} else if (col.ref) {
|
|
852
|
-
if (col.$refLinks.some(link => link.definition.
|
|
873
|
+
if (col.$refLinks.some(link => getDefinition(link.definition.target)?.['@cds.persistence.skip'] === true))
|
|
874
|
+
continue
|
|
853
875
|
if (col.ref.length > 1 && col.ref[0] === '$self' && !col.$refLinks[0].definition.kind) {
|
|
854
876
|
const dollarSelfReplacement = calculateDollarSelfColumn(col)
|
|
855
877
|
res.push(...getTransformedOrderByGroupBy([dollarSelfReplacement], inOrderBy))
|
|
856
878
|
continue
|
|
857
879
|
}
|
|
858
|
-
const { target } = col.$refLinks[0]
|
|
859
|
-
|
|
880
|
+
const { target, definition } = col.$refLinks[0]
|
|
881
|
+
let tableAlias = null
|
|
882
|
+
if (target.SELECT?.columns && inOrderBy) {
|
|
883
|
+
// usually TA is omitted if order by ref is a column
|
|
884
|
+
// if a localized sorting is requested, we add `COLLATE`s
|
|
885
|
+
// later on, which transforms the simple name to an expression
|
|
886
|
+
// --> in an expression, only source elements can be addressed, hence we must add TA
|
|
887
|
+
if (target.SELECT.localized && definition.type === 'cds.String') {
|
|
888
|
+
const referredCol = target.SELECT.columns.find(c => {
|
|
889
|
+
return c.as === col.ref[0] || c.ref?.at(-1) === col.ref[0]
|
|
890
|
+
})
|
|
891
|
+
if (referredCol) {
|
|
892
|
+
// keep sort and nulls properties
|
|
893
|
+
referredCol.sort = col.sort
|
|
894
|
+
referredCol.nulls = col.nulls
|
|
895
|
+
col = referredCol
|
|
896
|
+
if (definition.kind === 'element') {
|
|
897
|
+
tableAlias = getQuerySourceName(col)
|
|
898
|
+
} else {
|
|
899
|
+
// we must replace the reference with the underlying expression
|
|
900
|
+
const { val, func, args, xpr } = col
|
|
901
|
+
if (val) res.push({ val })
|
|
902
|
+
if (func) res.push({ func, args })
|
|
903
|
+
if (xpr) res.push({ xpr })
|
|
904
|
+
continue
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
} else {
|
|
909
|
+
tableAlias = getQuerySourceName(col) // do not prepend TA if orderBy column addresses element of query
|
|
910
|
+
}
|
|
860
911
|
const leaf = col.$refLinks[col.$refLinks.length - 1].definition
|
|
861
912
|
if (leaf.virtual === true) continue // already in getFlatColumnForElement
|
|
862
913
|
let baseName
|
|
@@ -950,7 +1001,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
950
1001
|
const { index, tableAlias } = inferred.$combinedElements[k][0]
|
|
951
1002
|
const element = tableAlias.elements[k]
|
|
952
1003
|
// ignore FK for odata csn / ignore blobs from wildcard expansion
|
|
953
|
-
if (isManagedAssocInFlatMode(element) ||
|
|
1004
|
+
if (isManagedAssocInFlatMode(element) || element.type === 'cds.LargeBinary') return
|
|
954
1005
|
// for wildcard on subquery in from, just reference the elements
|
|
955
1006
|
if (tableAlias.SELECT && !element.elements && !element.target) {
|
|
956
1007
|
wildcardColumns.push(index ? { ref: [index, k] } : { ref: [k] })
|
|
@@ -991,7 +1042,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
991
1042
|
*/
|
|
992
1043
|
function getElementForRef(ref, def) {
|
|
993
1044
|
return ref.reduce((prev, res) => {
|
|
994
|
-
return (prev?.elements || prev?.foreignKeys)?.[res] || prev?.
|
|
1045
|
+
return (prev?.elements || prev?.foreignKeys)?.[res] || getDefinition(prev?.target)?.elements[res] // PLEASE REVIEW: should we add the .foreignKey check here for the non-ucsn case?
|
|
995
1046
|
}, def)
|
|
996
1047
|
}
|
|
997
1048
|
|
|
@@ -1087,7 +1138,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1087
1138
|
if (element.keys) {
|
|
1088
1139
|
const flatColumns = []
|
|
1089
1140
|
element.keys.forEach(fk => {
|
|
1090
|
-
const fkElement = getElementForRef(fk.ref, element.
|
|
1141
|
+
const fkElement = getElementForRef(fk.ref, getDefinition(element.target))
|
|
1091
1142
|
let fkBaseName
|
|
1092
1143
|
if (!leafAssoc || leafAssoc.onlyForeignKeyAccess)
|
|
1093
1144
|
fkBaseName = `${baseName}_${fk.as || fk.ref[fk.ref.length - 1]}`
|
|
@@ -1141,7 +1192,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1141
1192
|
if (tableAlias) flatColumn.ref.unshift(tableAlias)
|
|
1142
1193
|
|
|
1143
1194
|
// in a flat model, we must assign the foreign key rather than the key in the target
|
|
1144
|
-
const flatForeignKey =
|
|
1195
|
+
const flatForeignKey = getDefinition(element.parent.name)?.elements[fkBaseName]
|
|
1145
1196
|
|
|
1146
1197
|
setElementOnColumns(flatColumn, flatForeignKey || fkElement)
|
|
1147
1198
|
Object.defineProperty(flatColumn, '_csnPath', { value: csnPath, writable: true })
|
|
@@ -1260,7 +1311,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1260
1311
|
}”`,
|
|
1261
1312
|
)
|
|
1262
1313
|
}
|
|
1263
|
-
whereExistsSubSelects.push(getWhereExistsSubquery(current, next, step.where, true))
|
|
1314
|
+
whereExistsSubSelects.push(getWhereExistsSubquery(current, next, step.where, true, step.args))
|
|
1264
1315
|
}
|
|
1265
1316
|
|
|
1266
1317
|
const whereExists = { SELECT: whereExistsSubqueries(whereExistsSubSelects) }
|
|
@@ -1292,7 +1343,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1292
1343
|
}
|
|
1293
1344
|
} else if (tokenStream.length === 1 && token.val && $baseLink) {
|
|
1294
1345
|
// infix filter - OData variant w/o mentioning key --> flatten out and compare each leaf to token.val
|
|
1295
|
-
const def = $baseLink.definition.
|
|
1346
|
+
const def = getDefinition($baseLink.definition.target) || $baseLink.definition
|
|
1296
1347
|
const keys = def.keys // use key aspect on entity
|
|
1297
1348
|
const keyValComparisons = []
|
|
1298
1349
|
const flatKeys = []
|
|
@@ -1552,7 +1603,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1552
1603
|
const nextStep = refReverse[i + 1] // only because we want the filter condition
|
|
1553
1604
|
|
|
1554
1605
|
if (stepLink.definition.target && nextStepLink) {
|
|
1555
|
-
const { where } = nextStep
|
|
1606
|
+
const { where, args } = nextStep
|
|
1556
1607
|
if (isStructured(nextStepLink.definition)) {
|
|
1557
1608
|
// find next association / entity in the ref because this is actually our real nextStep
|
|
1558
1609
|
const nextStepIndex =
|
|
@@ -1573,7 +1624,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1573
1624
|
as = getNextAvailableTableAlias(as)
|
|
1574
1625
|
}
|
|
1575
1626
|
nextStepLink.alias = as
|
|
1576
|
-
whereExistsSubSelects.push(getWhereExistsSubquery(stepLink, nextStepLink, where))
|
|
1627
|
+
whereExistsSubSelects.push(getWhereExistsSubquery(stepLink, nextStepLink, where, false, args))
|
|
1577
1628
|
}
|
|
1578
1629
|
}
|
|
1579
1630
|
|
|
@@ -1607,7 +1658,12 @@ function cqn4sql(originalQuery, model) {
|
|
|
1607
1658
|
|
|
1608
1659
|
// adjust ref & $refLinks after associations have turned into where exists subqueries
|
|
1609
1660
|
transformedFrom.$refLinks.splice(0, transformedFrom.$refLinks.length - 1)
|
|
1610
|
-
|
|
1661
|
+
|
|
1662
|
+
let args = from.ref.at(-1).args
|
|
1663
|
+
const subquerySource = transformedFrom.$refLinks[0].target
|
|
1664
|
+
if (subquerySource.params && !args) args = {}
|
|
1665
|
+
const id = localized(subquerySource)
|
|
1666
|
+
transformedFrom.ref = [args ? { id, args } : id]
|
|
1611
1667
|
|
|
1612
1668
|
return { transformedWhere, transformedFrom }
|
|
1613
1669
|
}
|
|
@@ -1661,7 +1717,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1661
1717
|
*/
|
|
1662
1718
|
function backlinkFor(assoc) {
|
|
1663
1719
|
if (!assoc.on) return null
|
|
1664
|
-
const target =
|
|
1720
|
+
const target = getDefinition(assoc.target)
|
|
1665
1721
|
// technically we could have multiple backlinks
|
|
1666
1722
|
const backlinks = []
|
|
1667
1723
|
for (let i = 0; i < assoc.on.length; i += 3) {
|
|
@@ -1687,7 +1743,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1687
1743
|
*/
|
|
1688
1744
|
function onCondFor(assocRefLink, targetSideRefLink, inWhereOrJoin) {
|
|
1689
1745
|
const { on, keys } = assocRefLink.definition
|
|
1690
|
-
const target =
|
|
1746
|
+
const target = getDefinition(assocRefLink.definition.target)
|
|
1691
1747
|
let res
|
|
1692
1748
|
// technically we could have multiple backlinks
|
|
1693
1749
|
if (keys) {
|
|
@@ -1738,10 +1794,11 @@ function cqn4sql(originalQuery, model) {
|
|
|
1738
1794
|
if (res === '$self')
|
|
1739
1795
|
// next is resolvable in entity
|
|
1740
1796
|
return prev
|
|
1741
|
-
const definition =
|
|
1797
|
+
const definition =
|
|
1798
|
+
prev?.elements?.[res] || getDefinition(prev?.target)?.elements[res] || pseudos.elements[res]
|
|
1742
1799
|
const target = getParentEntity(definition)
|
|
1743
1800
|
thing.$refLinks[i] = { definition, target, alias: definition.name }
|
|
1744
|
-
return prev?.elements?.[res] || prev?.
|
|
1801
|
+
return prev?.elements?.[res] || getDefinition(prev?.target)?.elements[res] || pseudos.elements[res]
|
|
1745
1802
|
}, assocHost)
|
|
1746
1803
|
}
|
|
1747
1804
|
|
|
@@ -1843,7 +1900,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1843
1900
|
result[i].ref.splice(0, 1, assocRefLink.alias)
|
|
1844
1901
|
} else if (
|
|
1845
1902
|
definition.name in
|
|
1846
|
-
(targetSideRefLink.definition.elements || targetSideRefLink.definition.
|
|
1903
|
+
(targetSideRefLink.definition.elements || getDefinition(targetSideRefLink.definition.target).elements)
|
|
1847
1904
|
) {
|
|
1848
1905
|
// first step is association which refers to its foreign key by dot notation
|
|
1849
1906
|
result[i].ref = [targetSideRefLink.alias, lhs.ref.join('_')]
|
|
@@ -1865,7 +1922,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1865
1922
|
// pseudo element
|
|
1866
1923
|
return element
|
|
1867
1924
|
if (element.kind === 'entity') return element
|
|
1868
|
-
else return
|
|
1925
|
+
else return getDefinition(localized(getParentEntity(element.parent)))
|
|
1869
1926
|
}
|
|
1870
1927
|
}
|
|
1871
1928
|
|
|
@@ -1880,11 +1937,11 @@ function cqn4sql(originalQuery, model) {
|
|
|
1880
1937
|
function getParentKeyForeignKeyPairs(assoc, targetSideRefLink, flipSourceAndTarget = false) {
|
|
1881
1938
|
const res = []
|
|
1882
1939
|
const backlink = backlinkFor(assoc)?.[0]
|
|
1883
|
-
const { keys,
|
|
1940
|
+
const { keys, target } = backlink || assoc
|
|
1884
1941
|
if (keys) {
|
|
1885
1942
|
keys.forEach(fk => {
|
|
1886
1943
|
const { ref, as } = fk
|
|
1887
|
-
const elem = getElementForRef(ref,
|
|
1944
|
+
const elem = getElementForRef(ref, getDefinition(target)) // find the element (the target element of the foreign key) in the target of the (backlink) association
|
|
1888
1945
|
const flatParentKeys = getFlatColumnsFor(elem, { baseName: ref.slice(0, ref.length - 1).join('_') }) // it might be a structured element, so expand it into the full parent key tuple
|
|
1889
1946
|
const flatAssociationName = getFullName(backlink || assoc) // get the name of the (backlink) association
|
|
1890
1947
|
const flatForeignKeys = getFlatColumnsFor(elem, { baseName: flatAssociationName, columnAlias: as }) // the name of the (backlink) association is the base of the foreign key tuple, also respect aliased fk.
|
|
@@ -1933,7 +1990,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1933
1990
|
* -> if it is, target and source side are flipped in the where exists subquery
|
|
1934
1991
|
* @returns {CQN.SELECT}
|
|
1935
1992
|
*/
|
|
1936
|
-
function getWhereExistsSubquery(current, next, customWhere = null, inWhere = false) {
|
|
1993
|
+
function getWhereExistsSubquery(current, next, customWhere = null, inWhere = false, customArgs = null) {
|
|
1937
1994
|
const { definition } = current
|
|
1938
1995
|
const { definition: nextDefinition } = next
|
|
1939
1996
|
const on = []
|
|
@@ -1957,9 +2014,12 @@ function cqn4sql(originalQuery, model) {
|
|
|
1957
2014
|
on.push(...['and', ...(hasLogicalOr(filter) ? [asXpr(filter)] : filter)])
|
|
1958
2015
|
}
|
|
1959
2016
|
|
|
2017
|
+
const subquerySource = assocTarget(nextDefinition) || nextDefinition
|
|
2018
|
+
const id = localized(subquerySource)
|
|
2019
|
+
if (subquerySource.params && !customArgs) customArgs = {}
|
|
1960
2020
|
const SELECT = {
|
|
1961
2021
|
from: {
|
|
1962
|
-
ref: [
|
|
2022
|
+
ref: [customArgs ? { id, args: customArgs } : id],
|
|
1963
2023
|
as: next.alias,
|
|
1964
2024
|
},
|
|
1965
2025
|
columns: [
|
|
@@ -1982,7 +2042,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1982
2042
|
*/
|
|
1983
2043
|
function localized(definition) {
|
|
1984
2044
|
if (!isLocalized(definition)) return definition.name
|
|
1985
|
-
const view =
|
|
2045
|
+
const view = getDefinition(`localized.${definition.name}`)
|
|
1986
2046
|
return view?.name || definition.name
|
|
1987
2047
|
}
|
|
1988
2048
|
|
|
@@ -1995,7 +2055,18 @@ function cqn4sql(originalQuery, model) {
|
|
|
1995
2055
|
* @returns true if the given definition shall be localized
|
|
1996
2056
|
*/
|
|
1997
2057
|
function isLocalized(definition) {
|
|
1998
|
-
return
|
|
2058
|
+
return (
|
|
2059
|
+
inferred.SELECT?.localized &&
|
|
2060
|
+
definition['@cds.localized'] !== false &&
|
|
2061
|
+
!inferred.SELECT.forUpdate &&
|
|
2062
|
+
!inferred.SELECT.forShareLock
|
|
2063
|
+
)
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
/** returns the CSN definition for the given name from the model */
|
|
2067
|
+
function getDefinition(name) {
|
|
2068
|
+
if (!name) return null
|
|
2069
|
+
return model.definitions[name]
|
|
1999
2070
|
}
|
|
2000
2071
|
|
|
2001
2072
|
/**
|
|
@@ -2005,7 +2076,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2005
2076
|
* @returns the csn definition of the association target or null if it is not an association
|
|
2006
2077
|
*/
|
|
2007
2078
|
function assocTarget(assoc) {
|
|
2008
|
-
return
|
|
2079
|
+
return getDefinition(assoc.target) || null
|
|
2009
2080
|
}
|
|
2010
2081
|
|
|
2011
2082
|
/**
|
package/lib/deep-queries.js
CHANGED
|
@@ -242,10 +242,23 @@ const _getDeepQueries = (diff, target, root = false) => {
|
|
|
242
242
|
queries.push(...subQueries)
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
|
|
245
|
+
const insertQueries = new Map()
|
|
246
|
+
|
|
247
|
+
return queries.map(q => {
|
|
248
|
+
// Merge all INSERT statements for each target
|
|
249
|
+
if (q.INSERT) {
|
|
250
|
+
const target = q.target
|
|
251
|
+
if (insertQueries.has(target)) {
|
|
252
|
+
insertQueries.get(target).INSERT.entries.push(...q.INSERT.entries)
|
|
253
|
+
return
|
|
254
|
+
} else {
|
|
255
|
+
insertQueries.set(target, q)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
246
258
|
Object.defineProperty(q, handledDeep, { value: true })
|
|
259
|
+
return q
|
|
247
260
|
})
|
|
248
|
-
|
|
261
|
+
.filter(a => a)
|
|
249
262
|
}
|
|
250
263
|
|
|
251
264
|
module.exports = {
|
package/lib/infer/index.js
CHANGED
|
@@ -47,13 +47,16 @@ function infer(originalQuery, model) {
|
|
|
47
47
|
Object.defineProperties(inferred, {
|
|
48
48
|
// REVISIT: public, or for local reuse, or in cqn4sql only?
|
|
49
49
|
sources: { value: sources, writable: true },
|
|
50
|
-
target: {
|
|
50
|
+
target: {
|
|
51
|
+
value: aliases.length === 1 ? getDefinitionFromSources(sources, aliases[0]) : originalQuery,
|
|
52
|
+
writable: true,
|
|
53
|
+
}, // REVISIT: legacy?
|
|
51
54
|
})
|
|
52
55
|
// also enrich original query -> writable because it may be inferred again
|
|
53
56
|
Object.defineProperties(originalQuery, {
|
|
54
57
|
sources: { value: sources, writable: true },
|
|
55
58
|
target: {
|
|
56
|
-
value: aliases.length === 1 ? sources
|
|
59
|
+
value: aliases.length === 1 ? getDefinitionFromSources(sources, aliases[0]) : originalQuery,
|
|
57
60
|
writable: true,
|
|
58
61
|
},
|
|
59
62
|
})
|
|
@@ -94,12 +97,13 @@ function infer(originalQuery, model) {
|
|
|
94
97
|
function inferTarget(from, querySources) {
|
|
95
98
|
const { ref } = from
|
|
96
99
|
if (ref) {
|
|
97
|
-
const
|
|
98
|
-
|
|
100
|
+
const { id, args } = ref[0]
|
|
101
|
+
const first = id || ref[0]
|
|
102
|
+
let target = getDefinition(first) || cds.error`"${first}" not found in the definitions of your model`
|
|
99
103
|
if (!target) throw new Error(`"${first}" not found in the definitions of your model`)
|
|
100
104
|
if (ref.length > 1) {
|
|
101
105
|
target = from.ref.slice(1).reduce((d, r) => {
|
|
102
|
-
const next = d.elements[r.id || r]?.
|
|
106
|
+
const next = getDefinition(d.elements[r.id || r]?.target) || d.elements[r.id || r]
|
|
103
107
|
if (!next) throw new Error(`No association “${r.id || r}” in ${d.kind} “${d.name}”`)
|
|
104
108
|
return next
|
|
105
109
|
}, target)
|
|
@@ -113,16 +117,18 @@ function infer(originalQuery, model) {
|
|
|
113
117
|
from.as ||
|
|
114
118
|
(ref.length === 1 ? first.match(/[^.]+$/)[0] : ref[ref.length - 1].id || ref[ref.length - 1])
|
|
115
119
|
if (alias in querySources) throw new Error(`Duplicate alias "${alias}"`)
|
|
116
|
-
querySources[alias] = target
|
|
120
|
+
querySources[alias] = { definition: target, args }
|
|
117
121
|
const last = from.$refLinks.at(-1)
|
|
118
122
|
last.alias = alias
|
|
119
123
|
} else if (from.args) {
|
|
120
124
|
from.args.forEach(a => inferTarget(a, querySources))
|
|
121
125
|
} else if (from.SELECT) {
|
|
122
126
|
infer(from, model) // we need the .elements in the sources
|
|
123
|
-
querySources[from.as || ''] = from
|
|
127
|
+
querySources[from.as || ''] = { definition: from }
|
|
124
128
|
} else if (typeof from === 'string') {
|
|
125
|
-
|
|
129
|
+
// TODO: Create unique alias, what about duplicates?
|
|
130
|
+
const definition = getDefinition(from) || cds.error`"${from}" not found in the definitions of your model`
|
|
131
|
+
querySources[/([^.]*)$/.exec(from)[0]] = { definition }
|
|
126
132
|
} else if (from.SET) {
|
|
127
133
|
infer(from, model)
|
|
128
134
|
}
|
|
@@ -168,7 +174,7 @@ function infer(originalQuery, model) {
|
|
|
168
174
|
// we need to search for first step in ´model.definitions[infixAlias]`
|
|
169
175
|
if ($baseLink) {
|
|
170
176
|
const { definition } = $baseLink
|
|
171
|
-
const elements = definition.
|
|
177
|
+
const elements = getDefinition(definition.target)?.elements || definition.elements
|
|
172
178
|
const e = elements?.[id] || cds.error`"${id}" not found in the elements of "${definition.name}"`
|
|
173
179
|
if (e.target) {
|
|
174
180
|
// only fk access in infix filter
|
|
@@ -188,22 +194,22 @@ function infer(originalQuery, model) {
|
|
|
188
194
|
Object.defineProperty(arg, 'flatName', { value: ref.join('_'), writable: true })
|
|
189
195
|
} else {
|
|
190
196
|
// must be in model.definitions
|
|
191
|
-
const definition = getDefinition(id
|
|
197
|
+
const definition = getDefinition(id) || cds.error`"${id}" not found in the definitions of your model`
|
|
192
198
|
arg.$refLinks[0] = { definition, target: definition }
|
|
193
199
|
}
|
|
194
200
|
} else {
|
|
195
201
|
const recent = arg.$refLinks[i - 1]
|
|
196
|
-
const { elements } = recent.definition.
|
|
202
|
+
const { elements } = getDefinition(recent.definition.target) || recent.definition
|
|
197
203
|
const e = elements[id]
|
|
198
204
|
if (!e) throw new Error(`"${id}" not found in the elements of "${arg.$refLinks[i - 1].definition.name}"`)
|
|
199
|
-
arg.$refLinks.push({ definition: e, target: e.
|
|
205
|
+
arg.$refLinks.push({ definition: e, target: getDefinition(e.target) || e })
|
|
200
206
|
}
|
|
201
207
|
arg.$refLinks[i].alias = !ref[i + 1] && arg.as ? arg.as : id.split('.').pop()
|
|
202
208
|
|
|
203
209
|
// link refs in where
|
|
204
210
|
if (step.where) {
|
|
205
211
|
// REVISIT: why do we need to walk through these so early?
|
|
206
|
-
if (arg.$refLinks[i].definition.kind === 'entity' || arg.$refLinks[i].definition.
|
|
212
|
+
if (arg.$refLinks[i].definition.kind === 'entity' || getDefinition(arg.$refLinks[i].definition.target)) {
|
|
207
213
|
let existsPredicate = false
|
|
208
214
|
const walkTokenStream = token => {
|
|
209
215
|
if (token === 'exists') {
|
|
@@ -243,7 +249,7 @@ function infer(originalQuery, model) {
|
|
|
243
249
|
function inferCombinedElements() {
|
|
244
250
|
const combinedElements = {}
|
|
245
251
|
for (const index in sources) {
|
|
246
|
-
const tableAlias = sources
|
|
252
|
+
const tableAlias = getDefinitionFromSources(sources, index)
|
|
247
253
|
for (const key in tableAlias.elements) {
|
|
248
254
|
if (key in combinedElements) combinedElements[key].push({ index, tableAlias })
|
|
249
255
|
else combinedElements[key] = [{ index, tableAlias }]
|
|
@@ -497,24 +503,30 @@ function infer(originalQuery, model) {
|
|
|
497
503
|
nameSegments.push(id)
|
|
498
504
|
} else if ($baseLink) {
|
|
499
505
|
const { definition, target } = $baseLink
|
|
500
|
-
const elements = definition.
|
|
506
|
+
const elements = getDefinition(definition.target)?.elements || definition.elements
|
|
501
507
|
if (elements && id in elements) {
|
|
502
508
|
const element = elements[id]
|
|
503
509
|
rejectNonFkAccess(element)
|
|
504
|
-
const resolvableIn = definition.target
|
|
510
|
+
const resolvableIn = getDefinition(definition.target) || target
|
|
505
511
|
column.$refLinks.push({ definition: elements[id], target: resolvableIn })
|
|
506
512
|
} else {
|
|
507
513
|
stepNotFoundInPredecessor(id, definition.name)
|
|
508
514
|
}
|
|
509
515
|
nameSegments.push(id)
|
|
510
516
|
} else if (firstStepIsTableAlias) {
|
|
511
|
-
column.$refLinks.push({
|
|
517
|
+
column.$refLinks.push({
|
|
518
|
+
definition: getDefinitionFromSources(sources, id),
|
|
519
|
+
target: getDefinitionFromSources(sources, id),
|
|
520
|
+
})
|
|
512
521
|
} else if (firstStepIsSelf) {
|
|
513
522
|
column.$refLinks.push({ definition: { elements: queryElements }, target: { elements: queryElements } })
|
|
514
523
|
} else if (column.ref.length > 1 && inferred.outerQueries?.find(outer => id in outer.sources)) {
|
|
515
524
|
// outer query accessed via alias
|
|
516
525
|
const outerAlias = inferred.outerQueries.find(outer => id in outer.sources)
|
|
517
|
-
column.$refLinks.push({
|
|
526
|
+
column.$refLinks.push({
|
|
527
|
+
definition: getDefinitionFromSources(outerAlias.sources, id),
|
|
528
|
+
target: getDefinitionFromSources(outerAlias.sources, id),
|
|
529
|
+
})
|
|
518
530
|
} else if (id in $combinedElements) {
|
|
519
531
|
if ($combinedElements[id].length > 1) stepIsAmbiguous(id) // exit
|
|
520
532
|
const definition = $combinedElements[id][0].tableAlias.elements[id]
|
|
@@ -524,15 +536,15 @@ function infer(originalQuery, model) {
|
|
|
524
536
|
} else if (expandOnTableAlias) {
|
|
525
537
|
// expand on table alias
|
|
526
538
|
column.$refLinks.push({
|
|
527
|
-
definition: sources
|
|
528
|
-
target: sources
|
|
539
|
+
definition: getDefinitionFromSources(sources, id),
|
|
540
|
+
target: getDefinitionFromSources(sources, id),
|
|
529
541
|
})
|
|
530
542
|
} else {
|
|
531
543
|
stepNotFoundInCombinedElements(id) // REVISIT: fails with {__proto__:elements)
|
|
532
544
|
}
|
|
533
545
|
} else {
|
|
534
546
|
const { definition } = column.$refLinks[i - 1]
|
|
535
|
-
const elements = definition.
|
|
547
|
+
const elements = getDefinition(definition.target)?.elements || definition.elements //> go for assoc._target first, instead of assoc as struct
|
|
536
548
|
const element = elements?.[id]
|
|
537
549
|
|
|
538
550
|
if (firstStepIsSelf && element?.isAssociation) {
|
|
@@ -543,7 +555,7 @@ function infer(originalQuery, model) {
|
|
|
543
555
|
)
|
|
544
556
|
}
|
|
545
557
|
|
|
546
|
-
const target = definition.
|
|
558
|
+
const target = getDefinition(definition.target) || column.$refLinks[i - 1].target
|
|
547
559
|
if (element) {
|
|
548
560
|
if ($baseLink) rejectNonFkAccess(element)
|
|
549
561
|
const $refLink = { definition: elements[id], target }
|
|
@@ -602,7 +614,8 @@ function infer(originalQuery, model) {
|
|
|
602
614
|
}
|
|
603
615
|
|
|
604
616
|
column.$refLinks[i].alias = !column.ref[i + 1] && column.as ? column.as : id.split('.').pop()
|
|
605
|
-
if (column.$refLinks[i].definition.
|
|
617
|
+
if (getDefinition(column.$refLinks[i].definition.target)?.['@cds.persistence.skip'] === true)
|
|
618
|
+
isPersisted = false
|
|
606
619
|
if (!column.ref[i + 1]) {
|
|
607
620
|
const flatName = nameSegments.join('_')
|
|
608
621
|
Object.defineProperty(column, 'flatName', { value: flatName, writable: true })
|
|
@@ -673,9 +686,7 @@ function infer(originalQuery, model) {
|
|
|
673
686
|
// ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
|
|
674
687
|
if (column.expand) {
|
|
675
688
|
const { $refLinks } = column
|
|
676
|
-
const skip = $refLinks.some(
|
|
677
|
-
link => model.definitions[link.definition.target]?.['@cds.persistence.skip'] === true,
|
|
678
|
-
)
|
|
689
|
+
const skip = $refLinks.some(link => getDefinition(link.definition.target)?.['@cds.persistence.skip'] === true)
|
|
679
690
|
if (skip) {
|
|
680
691
|
$refLinks[$refLinks.length - 1].skipExpand = true
|
|
681
692
|
return
|
|
@@ -716,13 +727,17 @@ function infer(originalQuery, model) {
|
|
|
716
727
|
function resolveInline(col, namePrefix = col.as || col.flatName) {
|
|
717
728
|
const { inline, $refLinks } = col
|
|
718
729
|
const $leafLink = $refLinks[$refLinks.length - 1]
|
|
730
|
+
if(!$leafLink.definition.target && !$leafLink.definition.elements) {
|
|
731
|
+
throw new Error(`Unexpected “inline” on “${col.ref.map(idOnly)}”; can only be used after a reference to a structure, association or table alias`)
|
|
732
|
+
}
|
|
719
733
|
let elements = {}
|
|
720
734
|
inline.forEach(inlineCol => {
|
|
721
735
|
inferQueryElement(inlineCol, false, $leafLink, { inExpr: true, inNestedProjection: true, baseColumn: col })
|
|
722
736
|
if (inlineCol === '*') {
|
|
723
737
|
const wildCardElements = {}
|
|
724
738
|
// either the `.elements´ of the struct or the `.elements` of the assoc target
|
|
725
|
-
const leafLinkElements =
|
|
739
|
+
const leafLinkElements =
|
|
740
|
+
getDefinition($leafLink.definition.target)?.elements || $leafLink.definition.elements
|
|
726
741
|
Object.entries(leafLinkElements).forEach(([k, v]) => {
|
|
727
742
|
const name = namePrefix ? `${namePrefix}_${k}` : k
|
|
728
743
|
// if overwritten/excluded omit from wildcard elements
|
|
@@ -768,10 +783,14 @@ function infer(originalQuery, model) {
|
|
|
768
783
|
function resolveExpand(col) {
|
|
769
784
|
const { expand, $refLinks } = col
|
|
770
785
|
const $leafLink = $refLinks?.[$refLinks.length - 1] || inferred.SELECT.from.$refLinks.at(-1) // fallback to anonymous expand
|
|
771
|
-
if
|
|
786
|
+
if(!$leafLink.definition.target && !$leafLink.definition.elements) {
|
|
787
|
+
throw new Error(`Unexpected “expand” on “${col.ref.map(idOnly)}”; can only be used after a reference to a structure, association or table alias`)
|
|
788
|
+
}
|
|
789
|
+
const target = getDefinition($leafLink.definition.target)
|
|
790
|
+
if (target) {
|
|
772
791
|
const expandSubquery = {
|
|
773
792
|
SELECT: {
|
|
774
|
-
from:
|
|
793
|
+
from: target.name,
|
|
775
794
|
columns: expand.filter(c => !c.inline),
|
|
776
795
|
},
|
|
777
796
|
}
|
|
@@ -782,19 +801,20 @@ function infer(originalQuery, model) {
|
|
|
782
801
|
? new cds.struct({ elements: inferredExpandSubquery.elements })
|
|
783
802
|
: new cds.array({ items: new cds.struct({ elements: inferredExpandSubquery.elements }) })
|
|
784
803
|
return Object.defineProperty(res, '$assocExpand', { value: true })
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
804
|
+
} else if ($leafLink.definition.elements) {
|
|
805
|
+
let elements = {}
|
|
806
|
+
expand.forEach(e => {
|
|
807
|
+
if (e === '*') {
|
|
808
|
+
elements = { ...elements, ...$leafLink.definition.elements }
|
|
809
|
+
} else {
|
|
810
|
+
inferQueryElement(e, false, $leafLink, { inExpr: true, inNestedProjection: true })
|
|
811
|
+
if (e.expand) elements[e.as || e.flatName] = resolveExpand(e)
|
|
812
|
+
if (e.inline) elements = { ...elements, ...resolveInline(e) }
|
|
813
|
+
else elements[e.as || e.flatName] = e.$refLinks ? e.$refLinks[e.$refLinks.length - 1].definition : e
|
|
814
|
+
}
|
|
815
|
+
})
|
|
816
|
+
return new cds.struct({ elements })
|
|
817
|
+
}
|
|
798
818
|
}
|
|
799
819
|
|
|
800
820
|
function stepNotFoundInPredecessor(step, def) {
|
|
@@ -812,6 +832,7 @@ function infer(originalQuery, model) {
|
|
|
812
832
|
function stepNotFoundInCombinedElements(step) {
|
|
813
833
|
throw new Error(
|
|
814
834
|
`"${step}" not found in the elements of ${Object.values(sources)
|
|
835
|
+
.map(s => s.definition)
|
|
815
836
|
.map(def => `"${def.name || /* subquery */ def.as}"`)
|
|
816
837
|
.join(', ')}`,
|
|
817
838
|
)
|
|
@@ -972,12 +993,12 @@ function infer(originalQuery, model) {
|
|
|
972
993
|
const exclude = _.excluding ? x => _.excluding.includes(x) : () => false
|
|
973
994
|
|
|
974
995
|
if (Object.keys(queryElements).length === 0 && aliases.length === 1) {
|
|
975
|
-
const { elements } = sources
|
|
996
|
+
const { elements } = getDefinitionFromSources(sources, aliases[0])
|
|
976
997
|
// only one query source and no overwritten columns
|
|
977
998
|
Object.keys(elements)
|
|
978
999
|
.filter(k => !exclude(k))
|
|
979
1000
|
.forEach(k => {
|
|
980
|
-
const element =
|
|
1001
|
+
const element = elements[k]
|
|
981
1002
|
if (element.type !== 'cds.LargeBinary') queryElements[k] = element
|
|
982
1003
|
if (element.value) {
|
|
983
1004
|
linkCalculatedElement(element)
|
|
@@ -1105,9 +1126,14 @@ function infer(originalQuery, model) {
|
|
|
1105
1126
|
}
|
|
1106
1127
|
}
|
|
1107
1128
|
|
|
1108
|
-
/**
|
|
1109
|
-
function getDefinition(name
|
|
1110
|
-
|
|
1129
|
+
/** returns the CSN definition for the given name from the model */
|
|
1130
|
+
function getDefinition(name) {
|
|
1131
|
+
if (!name) return null
|
|
1132
|
+
return model.definitions[name]
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function getDefinitionFromSources(sources, id) {
|
|
1136
|
+
return sources[id].definition
|
|
1111
1137
|
}
|
|
1112
1138
|
|
|
1113
1139
|
/**
|
|
@@ -1129,6 +1155,7 @@ function infer(originalQuery, model) {
|
|
|
1129
1155
|
}, '')
|
|
1130
1156
|
}
|
|
1131
1157
|
}
|
|
1158
|
+
|
|
1132
1159
|
/**
|
|
1133
1160
|
* Returns true if e is a foreign key of assoc.
|
|
1134
1161
|
* this function is also compatible with unfolded csn (UCSN),
|
package/lib/infer/join-tree.js
CHANGED
|
@@ -43,13 +43,17 @@ class Node {
|
|
|
43
43
|
* @param {parent} parent
|
|
44
44
|
* @param {where} where
|
|
45
45
|
*/
|
|
46
|
-
constructor($refLink, parent, where = null) {
|
|
46
|
+
constructor($refLink, parent, where = null, args = null) {
|
|
47
47
|
/** @type {$refLink} - A reference link to this node. */
|
|
48
48
|
this.$refLink = $refLink
|
|
49
49
|
/** @type {parent} - The parent Node of this node. */
|
|
50
50
|
this.parent = parent
|
|
51
51
|
/** @type {where} - An optional condition to be applied to this node. */
|
|
52
52
|
this.where = where
|
|
53
|
+
/** @type {args} - optional parameter object to be applied to this node. */
|
|
54
|
+
const targetHasParams = $refLink.definition._target?.params || $refLink.definition._target?.['@cds.persistence.udf']
|
|
55
|
+
if (!args && targetHasParams) args = {} // if no args are provided, provide empty argument list
|
|
56
|
+
this.args = args
|
|
53
57
|
/** @type {children} - A Map of children nodes belonging to this node. */
|
|
54
58
|
this.children = new Map()
|
|
55
59
|
}
|
|
@@ -63,9 +67,13 @@ class Root {
|
|
|
63
67
|
* @param {[alias, queryArtifact]} querySource
|
|
64
68
|
*/
|
|
65
69
|
constructor(querySource) {
|
|
66
|
-
|
|
70
|
+
let [alias, { definition, args }] = querySource
|
|
67
71
|
/** @type {queryArtifact} - The artifact used to make the query. */
|
|
68
|
-
this.queryArtifact =
|
|
72
|
+
this.queryArtifact = definition
|
|
73
|
+
/** @type {args} - optional parameter object to be applied to this node. */
|
|
74
|
+
const definitionHasParams = definition.params || definition['@cds.persistence.udf']
|
|
75
|
+
if (!args && definitionHasParams) args = {} // if no args are provided, provide empty argument list
|
|
76
|
+
this.args = args
|
|
69
77
|
/** @type {alias} - The alias of the artifact. */
|
|
70
78
|
this.alias = alias
|
|
71
79
|
/** @type {parent} - The parent Node of this root, null for the root Node. */
|
|
@@ -170,8 +178,8 @@ class JoinTree {
|
|
|
170
178
|
|
|
171
179
|
while (i < col.ref.length) {
|
|
172
180
|
const step = col.ref[i]
|
|
173
|
-
const { where } = step
|
|
174
|
-
const id =
|
|
181
|
+
const { where, args } = step
|
|
182
|
+
const id = joinId(step, args, where)
|
|
175
183
|
const next = node.children.get(id)
|
|
176
184
|
const $refLink = col.$refLinks[i]
|
|
177
185
|
if (next) {
|
|
@@ -187,7 +195,7 @@ class JoinTree {
|
|
|
187
195
|
node.$refLink.onlyForeignKeyAccess = false
|
|
188
196
|
return true
|
|
189
197
|
}
|
|
190
|
-
const child = new Node($refLink, node, where)
|
|
198
|
+
const child = new Node($refLink, node, where, args)
|
|
191
199
|
if (child.$refLink.definition.isAssociation) {
|
|
192
200
|
if (child.where || col.inline) {
|
|
193
201
|
// filter is always join relevant
|
|
@@ -212,6 +220,14 @@ class JoinTree {
|
|
|
212
220
|
i += 1
|
|
213
221
|
}
|
|
214
222
|
return true
|
|
223
|
+
|
|
224
|
+
function joinId(step, args, where) {
|
|
225
|
+
let appendix
|
|
226
|
+
if (where && args) appendix = JSON.stringify(where) + JSON.stringify(args)
|
|
227
|
+
else if (where) appendix = JSON.stringify(where)
|
|
228
|
+
else if (args) appendix = JSON.stringify(args)
|
|
229
|
+
return appendix ? step.id + appendix : step
|
|
230
|
+
}
|
|
215
231
|
}
|
|
216
232
|
|
|
217
233
|
/**
|
package/lib/search.js
CHANGED
|
@@ -10,10 +10,16 @@ const DRAFT_COLUMNS_UNION = {
|
|
|
10
10
|
}
|
|
11
11
|
const DEFAULT_SEARCHABLE_TYPE = 'cds.String'
|
|
12
12
|
|
|
13
|
+
// only those which return strings are relevant for search
|
|
14
|
+
const aggregateFunctions = {
|
|
15
|
+
MAX: true,
|
|
16
|
+
MIN: true,
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
/**
|
|
14
20
|
* This method gets all columns for an entity.
|
|
15
21
|
* It includes the generated foreign keys from managed associations, structured elements and complex and custom types.
|
|
16
|
-
*
|
|
22
|
+
* Moreover, it provides the annotations starting with '@' for each column.
|
|
17
23
|
*
|
|
18
24
|
* @param {object} entity - the csn entity
|
|
19
25
|
* @param {object} [options]
|
|
@@ -120,37 +126,49 @@ const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }, a
|
|
|
120
126
|
// aggregations case
|
|
121
127
|
// in the new parser groupBy is moved to sub select.
|
|
122
128
|
if (cqn._aggregated || /* new parser */ cqn.SELECT.groupBy || cqn.SELECT?.from?.SELECT?.groupBy) {
|
|
123
|
-
cqn.SELECT.columns
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
) {
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
toBeSearched.push(column)
|
|
129
|
+
cqn.SELECT.columns?.forEach(column => {
|
|
130
|
+
if (column.func || column.xpr) {
|
|
131
|
+
// exclude $count by SELECT of number of Items in a Collection
|
|
132
|
+
if (
|
|
133
|
+
cqn.SELECT.columns.length === 1 &&
|
|
134
|
+
column.func === 'count' &&
|
|
135
|
+
(column.as === '_counted_' || column.as === '$count')
|
|
136
|
+
) {
|
|
136
137
|
return
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
if (
|
|
142
|
-
column
|
|
143
|
-
if (alias) column.ref.unshift(alias)
|
|
144
|
-
toBeSearched.push(column)
|
|
140
|
+
// only strings can be searched
|
|
141
|
+
if (column.element.type !== DEFAULT_SEARCHABLE_TYPE) {
|
|
142
|
+
if (column.xpr) return
|
|
143
|
+
if (column.func && !(column.func in aggregateFunctions)) return
|
|
145
144
|
}
|
|
146
|
-
|
|
145
|
+
|
|
146
|
+
const searchTerm = {}
|
|
147
|
+
if (column.func) {
|
|
148
|
+
searchTerm.func = column.func
|
|
149
|
+
searchTerm.args = column.args
|
|
150
|
+
} else if (column.xpr) {
|
|
151
|
+
searchTerm.xpr = column.xpr
|
|
152
|
+
}
|
|
153
|
+
toBeSearched.push(searchTerm)
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// no need to set ref[0] to alias, because columns were already properly transformed
|
|
158
|
+
if (column.ref) {
|
|
159
|
+
if (column.element.type !== DEFAULT_SEARCHABLE_TYPE) return
|
|
160
|
+
column = { ref: [...column.ref] }
|
|
161
|
+
toBeSearched.push(column)
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
})
|
|
147
165
|
} else {
|
|
148
166
|
toBeSearched = entity.own('__searchableColumns') || entity.set('__searchableColumns', _getSearchableColumns(entity))
|
|
149
167
|
if (cqn.SELECT.groupBy) toBeSearched = toBeSearched.filter(tbs => cqn.SELECT.groupBy.some(gb => gb.ref[0] === tbs))
|
|
150
168
|
toBeSearched = toBeSearched.map(c => {
|
|
151
|
-
const
|
|
152
|
-
if (alias)
|
|
153
|
-
return
|
|
169
|
+
const column = { ref: [c] }
|
|
170
|
+
if (alias) column.ref.unshift(alias)
|
|
171
|
+
return column
|
|
154
172
|
})
|
|
155
173
|
}
|
|
156
174
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/db-service",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "CDS base database service",
|
|
5
5
|
"homepage": "https://github.com/cap-js/cds-dbs/tree/main/db-service#cds-base-database-service",
|
|
6
6
|
"repository": {
|
|
@@ -31,9 +31,5 @@
|
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"@sap/cds": ">=7.6"
|
|
33
33
|
},
|
|
34
|
-
"license": "SEE LICENSE"
|
|
35
|
-
"devDependencies": {
|
|
36
|
-
"@typescript-eslint/eslint-plugin": "^6.2.0",
|
|
37
|
-
"typescript": "^5.1.6"
|
|
38
|
-
}
|
|
34
|
+
"license": "SEE LICENSE"
|
|
39
35
|
}
|