@cap-js/db-service 1.6.3 → 1.7.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 +31 -0
- package/lib/SQLService.js +10 -2
- package/lib/common/DatabaseService.js +1 -1
- package/lib/cql-functions.js +7 -0
- package/lib/cqn2sql.js +70 -18
- package/lib/cqn4sql.js +137 -74
- package/lib/infer/index.js +54 -34
- package/lib/infer/join-tree.js +24 -7
- package/lib/search.js +42 -24
- package/package.json +2 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,37 @@
|
|
|
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.7.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.6.4...db-service-v1.7.0) (2024-03-22)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* 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))
|
|
13
|
+
* forUpdate and forShareLock ([#148](https://github.com/cap-js/cds-dbs/issues/148)) ([99a1170](https://github.com/cap-js/cds-dbs/commit/99a1170e61de4fd0c505834c25a9c03fc34da85b))
|
|
14
|
+
* **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))
|
|
15
|
+
* **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))
|
|
16
|
+
* **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))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
* **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))
|
|
22
|
+
* 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))
|
|
23
|
+
* 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))
|
|
24
|
+
* 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))
|
|
25
|
+
* **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))
|
|
26
|
+
* **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))
|
|
27
|
+
|
|
28
|
+
## [1.6.4](https://github.com/cap-js/cds-dbs/compare/db-service-v1.6.3...db-service-v1.6.4) (2024-02-28)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
* **`cqn2sql`:** smart quoting also for update statements ([#475](https://github.com/cap-js/cds-dbs/issues/475)) ([1688f77](https://github.com/cap-js/cds-dbs/commit/1688f77158c2df37673e969074f1b6d210267336))
|
|
34
|
+
* `INSERT` with first `undefined` value ([#484](https://github.com/cap-js/cds-dbs/issues/484)) ([c21e3c4](https://github.com/cap-js/cds-dbs/commit/c21e3c44140c44ff6378d1fdac32869d9c1c988c))
|
|
35
|
+
* Allow SELECT.join queries again with full infer call ([#469](https://github.com/cap-js/cds-dbs/issues/469)) ([5329ec0](https://github.com/cap-js/cds-dbs/commit/5329ec0a25036a1e42513e8bb9347b0ff8c7aa2d))
|
|
36
|
+
* optimize foreign key access in a join relevant path ([#481](https://github.com/cap-js/cds-dbs/issues/481)) ([5e30de4](https://github.com/cap-js/cds-dbs/commit/5e30de439b62167c4b6d487c4d5cda4f2f0a806d)), closes [#479](https://github.com/cap-js/cds-dbs/issues/479)
|
|
37
|
+
|
|
7
38
|
## [1.6.3](https://github.com/cap-js/cds-dbs/compare/db-service-v1.6.2...db-service-v1.6.3) (2024-02-20)
|
|
8
39
|
|
|
9
40
|
|
package/lib/SQLService.js
CHANGED
|
@@ -114,16 +114,22 @@ class SQLService extends DatabaseService {
|
|
|
114
114
|
* @type {Handler}
|
|
115
115
|
*/
|
|
116
116
|
async onSELECT({ query, data }) {
|
|
117
|
+
if (!query.target) {
|
|
118
|
+
try { this.infer(query) } catch (e) { /**/ }
|
|
119
|
+
}
|
|
117
120
|
if (query.target && !query.target._unresolved) {
|
|
118
121
|
// Will return multiple rows with objects inside
|
|
119
122
|
query.SELECT.expand = 'root'
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
const { sql, values, cqn } = this.cqn2sql(query, data)
|
|
126
|
+
const expand = query.SELECT.expand
|
|
127
|
+
delete query.SELECT.expand
|
|
128
|
+
|
|
123
129
|
let ps = await this.prepare(sql)
|
|
124
130
|
let rows = await ps.all(values)
|
|
125
131
|
if (rows.length)
|
|
126
|
-
if (
|
|
132
|
+
if (expand) rows = rows.map(r => (typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r))
|
|
127
133
|
|
|
128
134
|
if (cds.env.features.stream_compat) {
|
|
129
135
|
if (query._streaming) {
|
|
@@ -425,6 +431,8 @@ SQLService.prototype.PreparedStatement = PreparedStatement
|
|
|
425
431
|
|
|
426
432
|
const _target_name4 = q => {
|
|
427
433
|
const target =
|
|
434
|
+
q._target_ref ||
|
|
435
|
+
q.from_into_ntt ||
|
|
428
436
|
q.SELECT?.from ||
|
|
429
437
|
q.INSERT?.into ||
|
|
430
438
|
q.UPSERT?.into ||
|
|
@@ -438,7 +446,7 @@ const _target_name4 = q => {
|
|
|
438
446
|
return first.id || first
|
|
439
447
|
}
|
|
440
448
|
|
|
441
|
-
const _unquirked = q => {
|
|
449
|
+
const _unquirked = !cds.env.ql.quirks_mode ? q => q : q => {
|
|
442
450
|
if (!q) return q
|
|
443
451
|
else if (typeof q.SELECT?.from === 'string') q.SELECT.from = { ref: [q.SELECT.from] }
|
|
444
452
|
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
|
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
|
}
|
|
@@ -113,8 +115,8 @@ class CQN2SQLRenderer {
|
|
|
113
115
|
delete this.values
|
|
114
116
|
this.sql =
|
|
115
117
|
!query || target['@cds.persistence.table']
|
|
116
|
-
? `CREATE TABLE ${name} ( ${this.CREATE_elements(target.elements)} )`
|
|
117
|
-
: `CREATE VIEW ${name} AS ${this.SELECT(this.cqn4sql(query))}`
|
|
118
|
+
? `CREATE TABLE ${this.quote(name)} ( ${this.CREATE_elements(target.elements)} )`
|
|
119
|
+
: `CREATE VIEW ${this.quote(name)} AS ${this.SELECT(this.cqn4sql(query))}`
|
|
118
120
|
this.values = []
|
|
119
121
|
return
|
|
120
122
|
}
|
|
@@ -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
|
|
|
@@ -465,10 +517,11 @@ class CQN2SQLRenderer {
|
|
|
465
517
|
|
|
466
518
|
let sepsub = ''
|
|
467
519
|
for (const key in row) {
|
|
520
|
+
let val = row[key]
|
|
521
|
+
if (val === undefined) continue
|
|
468
522
|
const keyJSON = `${sepsub}${JSON.stringify(key)}:`
|
|
469
523
|
if (!sepsub) sepsub = ','
|
|
470
524
|
|
|
471
|
-
let val = row[key]
|
|
472
525
|
if (val instanceof Readable) {
|
|
473
526
|
buffer += `${keyJSON}"`
|
|
474
527
|
|
|
@@ -484,7 +537,6 @@ class CQN2SQLRenderer {
|
|
|
484
537
|
|
|
485
538
|
buffer += '"'
|
|
486
539
|
} else {
|
|
487
|
-
if (val === undefined) continue
|
|
488
540
|
if (elements[key]?.type in BINARY_TYPES) {
|
|
489
541
|
val = transformBase64(val)
|
|
490
542
|
}
|
|
@@ -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 ${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
|
|
|
@@ -689,8 +741,8 @@ class CQN2SQLRenderer {
|
|
|
689
741
|
UPDATE(q) {
|
|
690
742
|
const { entity, with: _with, data, where } = q.UPDATE
|
|
691
743
|
const elements = q.target?.elements
|
|
692
|
-
let sql = `UPDATE ${this.name(entity.ref?.[0] || entity)}`
|
|
693
|
-
if (entity.as) sql += ` AS ${entity.as}`
|
|
744
|
+
let sql = `UPDATE ${this.quote(this.name(entity.ref?.[0] || entity))}`
|
|
745
|
+
if (entity.as) sql += ` AS ${this.quote(entity.as)}`
|
|
694
746
|
|
|
695
747
|
let columns = []
|
|
696
748
|
if (data) _add(data, val => this.val({ val }))
|
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
|
}
|
|
@@ -838,9 +859,10 @@ function cqn4sql(originalQuery, model) {
|
|
|
838
859
|
const calcElement = resolveCalculatedElement(col, true)
|
|
839
860
|
res.push(calcElement)
|
|
840
861
|
} else if (col.isJoinRelevant) {
|
|
841
|
-
const tableAlias
|
|
862
|
+
const tableAlias = getQuerySourceName(col)
|
|
863
|
+
const name = calculateElementName(col)
|
|
842
864
|
const transformedColumn = {
|
|
843
|
-
ref: [tableAlias
|
|
865
|
+
ref: [tableAlias, name],
|
|
844
866
|
}
|
|
845
867
|
if (col.sort) transformedColumn.sort = col.sort
|
|
846
868
|
if (col.nulls) transformedColumn.nulls = col.nulls
|
|
@@ -848,7 +870,8 @@ function cqn4sql(originalQuery, model) {
|
|
|
848
870
|
} else if (pseudos.elements[col.ref?.[0]]) {
|
|
849
871
|
res.push({ ...col })
|
|
850
872
|
} else if (col.ref) {
|
|
851
|
-
if (col.$refLinks.some(link => link.definition.
|
|
873
|
+
if (col.$refLinks.some(link => getDefinition(link.definition.target)?.['@cds.persistence.skip'] === true))
|
|
874
|
+
continue
|
|
852
875
|
if (col.ref.length > 1 && col.ref[0] === '$self' && !col.$refLinks[0].definition.kind) {
|
|
853
876
|
const dollarSelfReplacement = calculateDollarSelfColumn(col)
|
|
854
877
|
res.push(...getTransformedOrderByGroupBy([dollarSelfReplacement], inOrderBy))
|
|
@@ -990,7 +1013,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
990
1013
|
*/
|
|
991
1014
|
function getElementForRef(ref, def) {
|
|
992
1015
|
return ref.reduce((prev, res) => {
|
|
993
|
-
return (prev?.elements || prev?.foreignKeys)?.[res] || prev?.
|
|
1016
|
+
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?
|
|
994
1017
|
}, def)
|
|
995
1018
|
}
|
|
996
1019
|
|
|
@@ -1046,10 +1069,10 @@ function cqn4sql(originalQuery, model) {
|
|
|
1046
1069
|
leafAssoc = [...column.$refLinks].reverse().find(link => link.definition.isAssociation)
|
|
1047
1070
|
let elements
|
|
1048
1071
|
elements = leafAssoc.definition.elements || leafAssoc.definition.foreignKeys
|
|
1049
|
-
if (elements && leaf.
|
|
1072
|
+
if (elements && leaf.definition.name in elements) {
|
|
1050
1073
|
element = leafAssoc.definition
|
|
1051
1074
|
baseName = getFullName(leafAssoc.definition)
|
|
1052
|
-
columnAlias = column.ref.slice(0, -1).map(idOnly).join('_')
|
|
1075
|
+
columnAlias = column.as || column.ref.slice(0, -1).map(idOnly).join('_')
|
|
1053
1076
|
} else baseName = getFullName(column.$refLinks[column.$refLinks.length - 1].definition)
|
|
1054
1077
|
} else if (!baseName && structsAreUnfoldedAlready) {
|
|
1055
1078
|
baseName = element.name // name is already fully constructed
|
|
@@ -1086,7 +1109,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1086
1109
|
if (element.keys) {
|
|
1087
1110
|
const flatColumns = []
|
|
1088
1111
|
element.keys.forEach(fk => {
|
|
1089
|
-
const fkElement = getElementForRef(fk.ref, element.
|
|
1112
|
+
const fkElement = getElementForRef(fk.ref, getDefinition(element.target))
|
|
1090
1113
|
let fkBaseName
|
|
1091
1114
|
if (!leafAssoc || leafAssoc.onlyForeignKeyAccess)
|
|
1092
1115
|
fkBaseName = `${baseName}_${fk.as || fk.ref[fk.ref.length - 1]}`
|
|
@@ -1127,12 +1150,20 @@ function cqn4sql(originalQuery, model) {
|
|
|
1127
1150
|
} else {
|
|
1128
1151
|
// leaf reached
|
|
1129
1152
|
let flatColumn
|
|
1130
|
-
if (columnAlias)
|
|
1131
|
-
|
|
1153
|
+
if (columnAlias) {
|
|
1154
|
+
// if the column has an explicit alias AND the orignal ref
|
|
1155
|
+
// directly resolves to the foreign key, we must not append the fk name to the column alias
|
|
1156
|
+
// e.g. `assoc.fk as FOO` => columns.alias = FOO
|
|
1157
|
+
// `assoc as FOO` => columns.alias = FOO_fk
|
|
1158
|
+
let columnAliasWithFlatFk
|
|
1159
|
+
if (!(column.as && fkElement === column.$refLinks?.at(-1).definition))
|
|
1160
|
+
columnAliasWithFlatFk = `${columnAlias}_${fk.as || fk.ref.join('_')}`
|
|
1161
|
+
flatColumn = { ref: [fkBaseName], as: columnAliasWithFlatFk || columnAlias }
|
|
1162
|
+
} else flatColumn = { ref: [fkBaseName] }
|
|
1132
1163
|
if (tableAlias) flatColumn.ref.unshift(tableAlias)
|
|
1133
1164
|
|
|
1134
1165
|
// in a flat model, we must assign the foreign key rather than the key in the target
|
|
1135
|
-
const flatForeignKey =
|
|
1166
|
+
const flatForeignKey = getDefinition(element.parent.name)?.elements[fkBaseName]
|
|
1136
1167
|
|
|
1137
1168
|
setElementOnColumns(flatColumn, flatForeignKey || fkElement)
|
|
1138
1169
|
Object.defineProperty(flatColumn, '_csnPath', { value: csnPath, writable: true })
|
|
@@ -1251,7 +1282,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1251
1282
|
}”`,
|
|
1252
1283
|
)
|
|
1253
1284
|
}
|
|
1254
|
-
whereExistsSubSelects.push(getWhereExistsSubquery(current, next, step.where, true))
|
|
1285
|
+
whereExistsSubSelects.push(getWhereExistsSubquery(current, next, step.where, true, step.args))
|
|
1255
1286
|
}
|
|
1256
1287
|
|
|
1257
1288
|
const whereExists = { SELECT: whereExistsSubqueries(whereExistsSubSelects) }
|
|
@@ -1283,7 +1314,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1283
1314
|
}
|
|
1284
1315
|
} else if (tokenStream.length === 1 && token.val && $baseLink) {
|
|
1285
1316
|
// infix filter - OData variant w/o mentioning key --> flatten out and compare each leaf to token.val
|
|
1286
|
-
const def = $baseLink.definition.
|
|
1317
|
+
const def = getDefinition($baseLink.definition.target) || $baseLink.definition
|
|
1287
1318
|
const keys = def.keys // use key aspect on entity
|
|
1288
1319
|
const keyValComparisons = []
|
|
1289
1320
|
const flatKeys = []
|
|
@@ -1353,11 +1384,12 @@ function cqn4sql(originalQuery, model) {
|
|
|
1353
1384
|
// in that case, we have a baseLink `books` which we need to resolve the following steps
|
|
1354
1385
|
// however, the correct table alias has been assigned to the `author` step
|
|
1355
1386
|
// hence we need to ignore the alias of the `$baseLink`
|
|
1356
|
-
const
|
|
1387
|
+
const lastAssoc =
|
|
1357
1388
|
token.isJoinRelevant && [...token.$refLinks].reverse().find(l => l.definition.isAssociation)
|
|
1358
|
-
const tableAlias = getQuerySourceName(token,
|
|
1359
|
-
if ((!$baseLink ||
|
|
1360
|
-
|
|
1389
|
+
const tableAlias = getQuerySourceName(token, (!lastAssoc?.onlyForeignKeyAccess && lastAssoc) || $baseLink)
|
|
1390
|
+
if ((!$baseLink || lastAssoc) && token.isJoinRelevant) {
|
|
1391
|
+
let name = calculateElementName(token, getFullName)
|
|
1392
|
+
result.ref = [tableAlias, name]
|
|
1361
1393
|
} else if (tableAlias) {
|
|
1362
1394
|
result.ref = [tableAlias, token.flatName]
|
|
1363
1395
|
} else {
|
|
@@ -1542,7 +1574,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1542
1574
|
const nextStep = refReverse[i + 1] // only because we want the filter condition
|
|
1543
1575
|
|
|
1544
1576
|
if (stepLink.definition.target && nextStepLink) {
|
|
1545
|
-
const { where } = nextStep
|
|
1577
|
+
const { where, args } = nextStep
|
|
1546
1578
|
if (isStructured(nextStepLink.definition)) {
|
|
1547
1579
|
// find next association / entity in the ref because this is actually our real nextStep
|
|
1548
1580
|
const nextStepIndex =
|
|
@@ -1563,7 +1595,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1563
1595
|
as = getNextAvailableTableAlias(as)
|
|
1564
1596
|
}
|
|
1565
1597
|
nextStepLink.alias = as
|
|
1566
|
-
whereExistsSubSelects.push(getWhereExistsSubquery(stepLink, nextStepLink, where))
|
|
1598
|
+
whereExistsSubSelects.push(getWhereExistsSubquery(stepLink, nextStepLink, where, false, args))
|
|
1567
1599
|
}
|
|
1568
1600
|
}
|
|
1569
1601
|
|
|
@@ -1597,7 +1629,12 @@ function cqn4sql(originalQuery, model) {
|
|
|
1597
1629
|
|
|
1598
1630
|
// adjust ref & $refLinks after associations have turned into where exists subqueries
|
|
1599
1631
|
transformedFrom.$refLinks.splice(0, transformedFrom.$refLinks.length - 1)
|
|
1600
|
-
|
|
1632
|
+
|
|
1633
|
+
let args = from.ref.at(-1).args
|
|
1634
|
+
const subquerySource = transformedFrom.$refLinks[0].target
|
|
1635
|
+
if (subquerySource.params && !args) args = {}
|
|
1636
|
+
const id = localized(subquerySource)
|
|
1637
|
+
transformedFrom.ref = [args ? { id, args } : id]
|
|
1601
1638
|
|
|
1602
1639
|
return { transformedWhere, transformedFrom }
|
|
1603
1640
|
}
|
|
@@ -1651,7 +1688,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1651
1688
|
*/
|
|
1652
1689
|
function backlinkFor(assoc) {
|
|
1653
1690
|
if (!assoc.on) return null
|
|
1654
|
-
const target =
|
|
1691
|
+
const target = getDefinition(assoc.target)
|
|
1655
1692
|
// technically we could have multiple backlinks
|
|
1656
1693
|
const backlinks = []
|
|
1657
1694
|
for (let i = 0; i < assoc.on.length; i += 3) {
|
|
@@ -1677,7 +1714,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1677
1714
|
*/
|
|
1678
1715
|
function onCondFor(assocRefLink, targetSideRefLink, inWhereOrJoin) {
|
|
1679
1716
|
const { on, keys } = assocRefLink.definition
|
|
1680
|
-
const target =
|
|
1717
|
+
const target = getDefinition(assocRefLink.definition.target)
|
|
1681
1718
|
let res
|
|
1682
1719
|
// technically we could have multiple backlinks
|
|
1683
1720
|
if (keys) {
|
|
@@ -1728,10 +1765,11 @@ function cqn4sql(originalQuery, model) {
|
|
|
1728
1765
|
if (res === '$self')
|
|
1729
1766
|
// next is resolvable in entity
|
|
1730
1767
|
return prev
|
|
1731
|
-
const definition =
|
|
1768
|
+
const definition =
|
|
1769
|
+
prev?.elements?.[res] || getDefinition(prev?.target)?.elements[res] || pseudos.elements[res]
|
|
1732
1770
|
const target = getParentEntity(definition)
|
|
1733
1771
|
thing.$refLinks[i] = { definition, target, alias: definition.name }
|
|
1734
|
-
return prev?.elements?.[res] || prev?.
|
|
1772
|
+
return prev?.elements?.[res] || getDefinition(prev?.target)?.elements[res] || pseudos.elements[res]
|
|
1735
1773
|
}, assocHost)
|
|
1736
1774
|
}
|
|
1737
1775
|
|
|
@@ -1814,7 +1852,8 @@ function cqn4sql(originalQuery, model) {
|
|
|
1814
1852
|
result.splice(i, 3, ...(wrapInXpr ? [asXpr(backlinkOnCondition)] : backlinkOnCondition))
|
|
1815
1853
|
i += wrapInXpr ? 1 : backlinkOnCondition.length // skip inserted tokens
|
|
1816
1854
|
} else if (lhs.ref) {
|
|
1817
|
-
if (lhs.ref[0] === '$self') {
|
|
1855
|
+
if (lhs.ref[0] === '$self') {
|
|
1856
|
+
// $self in ref of length > 1
|
|
1818
1857
|
// if $self is followed by association, the alias of the association must be used
|
|
1819
1858
|
if (lhs.$refLinks[1].definition.isAssociation) result[i].ref.splice(0, 1)
|
|
1820
1859
|
// otherwise $self is replaced by the alias of the entity
|
|
@@ -1832,7 +1871,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1832
1871
|
result[i].ref.splice(0, 1, assocRefLink.alias)
|
|
1833
1872
|
} else if (
|
|
1834
1873
|
definition.name in
|
|
1835
|
-
(targetSideRefLink.definition.elements || targetSideRefLink.definition.
|
|
1874
|
+
(targetSideRefLink.definition.elements || getDefinition(targetSideRefLink.definition.target).elements)
|
|
1836
1875
|
) {
|
|
1837
1876
|
// first step is association which refers to its foreign key by dot notation
|
|
1838
1877
|
result[i].ref = [targetSideRefLink.alias, lhs.ref.join('_')]
|
|
@@ -1854,7 +1893,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1854
1893
|
// pseudo element
|
|
1855
1894
|
return element
|
|
1856
1895
|
if (element.kind === 'entity') return element
|
|
1857
|
-
else return
|
|
1896
|
+
else return getDefinition(localized(getParentEntity(element.parent)))
|
|
1858
1897
|
}
|
|
1859
1898
|
}
|
|
1860
1899
|
|
|
@@ -1869,11 +1908,11 @@ function cqn4sql(originalQuery, model) {
|
|
|
1869
1908
|
function getParentKeyForeignKeyPairs(assoc, targetSideRefLink, flipSourceAndTarget = false) {
|
|
1870
1909
|
const res = []
|
|
1871
1910
|
const backlink = backlinkFor(assoc)?.[0]
|
|
1872
|
-
const { keys,
|
|
1911
|
+
const { keys, target } = backlink || assoc
|
|
1873
1912
|
if (keys) {
|
|
1874
1913
|
keys.forEach(fk => {
|
|
1875
1914
|
const { ref, as } = fk
|
|
1876
|
-
const elem = getElementForRef(ref,
|
|
1915
|
+
const elem = getElementForRef(ref, getDefinition(target)) // find the element (the target element of the foreign key) in the target of the (backlink) association
|
|
1877
1916
|
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
|
|
1878
1917
|
const flatAssociationName = getFullName(backlink || assoc) // get the name of the (backlink) association
|
|
1879
1918
|
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.
|
|
@@ -1922,7 +1961,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1922
1961
|
* -> if it is, target and source side are flipped in the where exists subquery
|
|
1923
1962
|
* @returns {CQN.SELECT}
|
|
1924
1963
|
*/
|
|
1925
|
-
function getWhereExistsSubquery(current, next, customWhere = null, inWhere = false) {
|
|
1964
|
+
function getWhereExistsSubquery(current, next, customWhere = null, inWhere = false, customArgs = null) {
|
|
1926
1965
|
const { definition } = current
|
|
1927
1966
|
const { definition: nextDefinition } = next
|
|
1928
1967
|
const on = []
|
|
@@ -1946,9 +1985,12 @@ function cqn4sql(originalQuery, model) {
|
|
|
1946
1985
|
on.push(...['and', ...(hasLogicalOr(filter) ? [asXpr(filter)] : filter)])
|
|
1947
1986
|
}
|
|
1948
1987
|
|
|
1988
|
+
const subquerySource = assocTarget(nextDefinition) || nextDefinition
|
|
1989
|
+
const id = localized(subquerySource)
|
|
1990
|
+
if (subquerySource.params && !customArgs) customArgs = {}
|
|
1949
1991
|
const SELECT = {
|
|
1950
1992
|
from: {
|
|
1951
|
-
ref: [
|
|
1993
|
+
ref: [customArgs ? { id, args: customArgs } : id],
|
|
1952
1994
|
as: next.alias,
|
|
1953
1995
|
},
|
|
1954
1996
|
columns: [
|
|
@@ -1971,7 +2013,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1971
2013
|
*/
|
|
1972
2014
|
function localized(definition) {
|
|
1973
2015
|
if (!isLocalized(definition)) return definition.name
|
|
1974
|
-
const view =
|
|
2016
|
+
const view = getDefinition(`localized.${definition.name}`)
|
|
1975
2017
|
return view?.name || definition.name
|
|
1976
2018
|
}
|
|
1977
2019
|
|
|
@@ -1984,7 +2026,18 @@ function cqn4sql(originalQuery, model) {
|
|
|
1984
2026
|
* @returns true if the given definition shall be localized
|
|
1985
2027
|
*/
|
|
1986
2028
|
function isLocalized(definition) {
|
|
1987
|
-
return
|
|
2029
|
+
return (
|
|
2030
|
+
inferred.SELECT?.localized &&
|
|
2031
|
+
definition['@cds.localized'] !== false &&
|
|
2032
|
+
!inferred.SELECT.forUpdate &&
|
|
2033
|
+
!inferred.SELECT.forShareLock
|
|
2034
|
+
)
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
/** returns the CSN definition for the given name from the model */
|
|
2038
|
+
function getDefinition(name) {
|
|
2039
|
+
if (!name) return null
|
|
2040
|
+
return model.definitions[name]
|
|
1988
2041
|
}
|
|
1989
2042
|
|
|
1990
2043
|
/**
|
|
@@ -1994,22 +2047,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1994
2047
|
* @returns the csn definition of the association target or null if it is not an association
|
|
1995
2048
|
*/
|
|
1996
2049
|
function assocTarget(assoc) {
|
|
1997
|
-
return
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
/**
|
|
2001
|
-
* Calculate the flat name for a deeply nested element:
|
|
2002
|
-
* @example `entity E { struct: { foo: String} }` => `getFullName(foo)` => `struct_foo`
|
|
2003
|
-
*
|
|
2004
|
-
* @param {CSN.element} node an element
|
|
2005
|
-
* @param {object} name the last part of the name, e.g. the name of the deeply nested element
|
|
2006
|
-
* @returns the flat name of the element
|
|
2007
|
-
*/
|
|
2008
|
-
function getFullName(node, name = node.name) {
|
|
2009
|
-
// REVISIT: this is an unfortunate implementation
|
|
2010
|
-
if (!node.parent || node.parent.kind === 'entity') return name
|
|
2011
|
-
|
|
2012
|
-
return getFullName(node.parent, `${node.parent.name}_${name}`)
|
|
2050
|
+
return getDefinition(assoc.target) || null
|
|
2013
2051
|
}
|
|
2014
2052
|
|
|
2015
2053
|
/**
|
|
@@ -2080,6 +2118,31 @@ module.exports = Object.assign(cqn4sql, {
|
|
|
2080
2118
|
notSupportedOps,
|
|
2081
2119
|
})
|
|
2082
2120
|
|
|
2121
|
+
function calculateElementName(token) {
|
|
2122
|
+
const nonJoinRelevantAssoc = [...token.$refLinks].findIndex(l => l.definition.isAssociation && l.onlyForeignKeyAccess)
|
|
2123
|
+
let name
|
|
2124
|
+
if (nonJoinRelevantAssoc)
|
|
2125
|
+
// calculate fk name
|
|
2126
|
+
name = token.ref.slice(nonJoinRelevantAssoc).join('_')
|
|
2127
|
+
else name = token.$refLinks[token.$refLinks.length - 1].definition.name
|
|
2128
|
+
return name
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
/**
|
|
2132
|
+
* Calculate the flat name for a deeply nested element:
|
|
2133
|
+
* @example `entity E { struct: { foo: String} }` => `getFullName(foo)` => `struct_foo`
|
|
2134
|
+
*
|
|
2135
|
+
* @param {CSN.element} node an element
|
|
2136
|
+
* @param {object} name the last part of the name, e.g. the name of the deeply nested element
|
|
2137
|
+
* @returns the flat name of the element
|
|
2138
|
+
*/
|
|
2139
|
+
function getFullName(node, name = node.name) {
|
|
2140
|
+
// REVISIT: this is an unfortunate implementation
|
|
2141
|
+
if (!node.parent || node.parent.kind === 'entity') return name
|
|
2142
|
+
|
|
2143
|
+
return getFullName(node.parent, `${node.parent.name}_${name}`)
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2083
2146
|
function copy(obj) {
|
|
2084
2147
|
const walk = function (par, prop) {
|
|
2085
2148
|
const val = prop ? par[prop] : par
|
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
|
|
@@ -722,7 +733,8 @@ function infer(originalQuery, model) {
|
|
|
722
733
|
if (inlineCol === '*') {
|
|
723
734
|
const wildCardElements = {}
|
|
724
735
|
// either the `.elements´ of the struct or the `.elements` of the assoc target
|
|
725
|
-
const leafLinkElements =
|
|
736
|
+
const leafLinkElements =
|
|
737
|
+
getDefinition($leafLink.definition.target)?.elements || $leafLink.definition.elements
|
|
726
738
|
Object.entries(leafLinkElements).forEach(([k, v]) => {
|
|
727
739
|
const name = namePrefix ? `${namePrefix}_${k}` : k
|
|
728
740
|
// if overwritten/excluded omit from wildcard elements
|
|
@@ -768,10 +780,11 @@ function infer(originalQuery, model) {
|
|
|
768
780
|
function resolveExpand(col) {
|
|
769
781
|
const { expand, $refLinks } = col
|
|
770
782
|
const $leafLink = $refLinks?.[$refLinks.length - 1] || inferred.SELECT.from.$refLinks.at(-1) // fallback to anonymous expand
|
|
771
|
-
|
|
783
|
+
const target = getDefinition($leafLink.definition.target)
|
|
784
|
+
if (target) {
|
|
772
785
|
const expandSubquery = {
|
|
773
786
|
SELECT: {
|
|
774
|
-
from:
|
|
787
|
+
from: target.name,
|
|
775
788
|
columns: expand.filter(c => !c.inline),
|
|
776
789
|
},
|
|
777
790
|
}
|
|
@@ -812,6 +825,7 @@ function infer(originalQuery, model) {
|
|
|
812
825
|
function stepNotFoundInCombinedElements(step) {
|
|
813
826
|
throw new Error(
|
|
814
827
|
`"${step}" not found in the elements of ${Object.values(sources)
|
|
828
|
+
.map(s => s.definition)
|
|
815
829
|
.map(def => `"${def.name || /* subquery */ def.as}"`)
|
|
816
830
|
.join(', ')}`,
|
|
817
831
|
)
|
|
@@ -972,12 +986,12 @@ function infer(originalQuery, model) {
|
|
|
972
986
|
const exclude = _.excluding ? x => _.excluding.includes(x) : () => false
|
|
973
987
|
|
|
974
988
|
if (Object.keys(queryElements).length === 0 && aliases.length === 1) {
|
|
975
|
-
const { elements } = sources
|
|
989
|
+
const { elements } = getDefinitionFromSources(sources, aliases[0])
|
|
976
990
|
// only one query source and no overwritten columns
|
|
977
991
|
Object.keys(elements)
|
|
978
992
|
.filter(k => !exclude(k))
|
|
979
993
|
.forEach(k => {
|
|
980
|
-
const element =
|
|
994
|
+
const element = elements[k]
|
|
981
995
|
if (element.type !== 'cds.LargeBinary') queryElements[k] = element
|
|
982
996
|
if (element.value) {
|
|
983
997
|
linkCalculatedElement(element)
|
|
@@ -1105,9 +1119,14 @@ function infer(originalQuery, model) {
|
|
|
1105
1119
|
}
|
|
1106
1120
|
}
|
|
1107
1121
|
|
|
1108
|
-
/**
|
|
1109
|
-
function getDefinition(name
|
|
1110
|
-
|
|
1122
|
+
/** returns the CSN definition for the given name from the model */
|
|
1123
|
+
function getDefinition(name) {
|
|
1124
|
+
if (!name) return null
|
|
1125
|
+
return model.definitions[name]
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function getDefinitionFromSources(sources, id) {
|
|
1129
|
+
return sources[id].definition
|
|
1111
1130
|
}
|
|
1112
1131
|
|
|
1113
1132
|
/**
|
|
@@ -1129,6 +1148,7 @@ function infer(originalQuery, model) {
|
|
|
1129
1148
|
}, '')
|
|
1130
1149
|
}
|
|
1131
1150
|
}
|
|
1151
|
+
|
|
1132
1152
|
/**
|
|
1133
1153
|
* Returns true if e is a foreign key of assoc.
|
|
1134
1154
|
* 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) {
|
|
@@ -181,12 +189,13 @@ class JoinTree {
|
|
|
181
189
|
col.$refLinks[i].alias = node.$refLink.alias
|
|
182
190
|
col.$refLinks[i].definition = node.$refLink.definition
|
|
183
191
|
col.$refLinks[i].target = node.$refLink.target
|
|
192
|
+
col.$refLinks[i].onlyForeignKeyAccess = node.$refLink.onlyForeignKeyAccess
|
|
184
193
|
} else {
|
|
185
194
|
if (col.expand && !col.ref[i + 1]) {
|
|
186
195
|
node.$refLink.onlyForeignKeyAccess = false
|
|
187
196
|
return true
|
|
188
197
|
}
|
|
189
|
-
const child = new Node($refLink, node, where)
|
|
198
|
+
const child = new Node($refLink, node, where, args)
|
|
190
199
|
if (child.$refLink.definition.isAssociation) {
|
|
191
200
|
if (child.where || col.inline) {
|
|
192
201
|
// filter is always join relevant
|
|
@@ -201,7 +210,7 @@ class JoinTree {
|
|
|
201
210
|
const elements =
|
|
202
211
|
node.$refLink?.definition.isAssociation &&
|
|
203
212
|
(node.$refLink.definition.elements || node.$refLink.definition.foreignKeys)
|
|
204
|
-
if (node.$refLink && (!elements || !(child.$refLink.
|
|
213
|
+
if (node.$refLink && (!elements || !(child.$refLink.definition.name in elements)))
|
|
205
214
|
// foreign key access
|
|
206
215
|
node.$refLink.onlyForeignKeyAccess = false
|
|
207
216
|
|
|
@@ -211,6 +220,14 @@ class JoinTree {
|
|
|
211
220
|
i += 1
|
|
212
221
|
}
|
|
213
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
|
+
}
|
|
214
231
|
}
|
|
215
232
|
|
|
216
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.7.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
|
}
|