@cap-js/db-service 1.6.4 → 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 +21 -0
- package/lib/SQLService.js +7 -2
- package/lib/common/DatabaseService.js +1 -1
- package/lib/cql-functions.js +7 -0
- package/lib/cqn2sql.js +64 -12
- package/lib/cqn4sql.js +90 -48
- package/lib/infer/index.js +54 -34
- 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,27 @@
|
|
|
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
|
+
|
|
7
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)
|
|
8
29
|
|
|
9
30
|
|
package/lib/SQLService.js
CHANGED
|
@@ -123,10 +123,13 @@ 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) {
|
|
@@ -428,6 +431,8 @@ SQLService.prototype.PreparedStatement = PreparedStatement
|
|
|
428
431
|
|
|
429
432
|
const _target_name4 = q => {
|
|
430
433
|
const target =
|
|
434
|
+
q._target_ref ||
|
|
435
|
+
q.from_into_ntt ||
|
|
431
436
|
q.SELECT?.from ||
|
|
432
437
|
q.INSERT?.into ||
|
|
433
438
|
q.UPSERT?.into ||
|
|
@@ -441,7 +446,7 @@ const _target_name4 = q => {
|
|
|
441
446
|
return first.id || first
|
|
442
447
|
}
|
|
443
448
|
|
|
444
|
-
const _unquirked = q => {
|
|
449
|
+
const _unquirked = !cds.env.ql.quirks_mode ? q => q : q => {
|
|
445
450
|
if (!q) return q
|
|
446
451
|
else if (typeof q.SELECT?.from === 'string') q.SELECT.from = { ref: [q.SELECT.from] }
|
|
447
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
|
}
|
|
@@ -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
|
}
|
|
@@ -849,7 +870,8 @@ 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))
|
|
@@ -991,7 +1013,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
991
1013
|
*/
|
|
992
1014
|
function getElementForRef(ref, def) {
|
|
993
1015
|
return ref.reduce((prev, res) => {
|
|
994
|
-
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?
|
|
995
1017
|
}, def)
|
|
996
1018
|
}
|
|
997
1019
|
|
|
@@ -1087,7 +1109,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1087
1109
|
if (element.keys) {
|
|
1088
1110
|
const flatColumns = []
|
|
1089
1111
|
element.keys.forEach(fk => {
|
|
1090
|
-
const fkElement = getElementForRef(fk.ref, element.
|
|
1112
|
+
const fkElement = getElementForRef(fk.ref, getDefinition(element.target))
|
|
1091
1113
|
let fkBaseName
|
|
1092
1114
|
if (!leafAssoc || leafAssoc.onlyForeignKeyAccess)
|
|
1093
1115
|
fkBaseName = `${baseName}_${fk.as || fk.ref[fk.ref.length - 1]}`
|
|
@@ -1141,7 +1163,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1141
1163
|
if (tableAlias) flatColumn.ref.unshift(tableAlias)
|
|
1142
1164
|
|
|
1143
1165
|
// in a flat model, we must assign the foreign key rather than the key in the target
|
|
1144
|
-
const flatForeignKey =
|
|
1166
|
+
const flatForeignKey = getDefinition(element.parent.name)?.elements[fkBaseName]
|
|
1145
1167
|
|
|
1146
1168
|
setElementOnColumns(flatColumn, flatForeignKey || fkElement)
|
|
1147
1169
|
Object.defineProperty(flatColumn, '_csnPath', { value: csnPath, writable: true })
|
|
@@ -1260,7 +1282,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1260
1282
|
}”`,
|
|
1261
1283
|
)
|
|
1262
1284
|
}
|
|
1263
|
-
whereExistsSubSelects.push(getWhereExistsSubquery(current, next, step.where, true))
|
|
1285
|
+
whereExistsSubSelects.push(getWhereExistsSubquery(current, next, step.where, true, step.args))
|
|
1264
1286
|
}
|
|
1265
1287
|
|
|
1266
1288
|
const whereExists = { SELECT: whereExistsSubqueries(whereExistsSubSelects) }
|
|
@@ -1292,7 +1314,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1292
1314
|
}
|
|
1293
1315
|
} else if (tokenStream.length === 1 && token.val && $baseLink) {
|
|
1294
1316
|
// infix filter - OData variant w/o mentioning key --> flatten out and compare each leaf to token.val
|
|
1295
|
-
const def = $baseLink.definition.
|
|
1317
|
+
const def = getDefinition($baseLink.definition.target) || $baseLink.definition
|
|
1296
1318
|
const keys = def.keys // use key aspect on entity
|
|
1297
1319
|
const keyValComparisons = []
|
|
1298
1320
|
const flatKeys = []
|
|
@@ -1552,7 +1574,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1552
1574
|
const nextStep = refReverse[i + 1] // only because we want the filter condition
|
|
1553
1575
|
|
|
1554
1576
|
if (stepLink.definition.target && nextStepLink) {
|
|
1555
|
-
const { where } = nextStep
|
|
1577
|
+
const { where, args } = nextStep
|
|
1556
1578
|
if (isStructured(nextStepLink.definition)) {
|
|
1557
1579
|
// find next association / entity in the ref because this is actually our real nextStep
|
|
1558
1580
|
const nextStepIndex =
|
|
@@ -1573,7 +1595,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1573
1595
|
as = getNextAvailableTableAlias(as)
|
|
1574
1596
|
}
|
|
1575
1597
|
nextStepLink.alias = as
|
|
1576
|
-
whereExistsSubSelects.push(getWhereExistsSubquery(stepLink, nextStepLink, where))
|
|
1598
|
+
whereExistsSubSelects.push(getWhereExistsSubquery(stepLink, nextStepLink, where, false, args))
|
|
1577
1599
|
}
|
|
1578
1600
|
}
|
|
1579
1601
|
|
|
@@ -1607,7 +1629,12 @@ function cqn4sql(originalQuery, model) {
|
|
|
1607
1629
|
|
|
1608
1630
|
// adjust ref & $refLinks after associations have turned into where exists subqueries
|
|
1609
1631
|
transformedFrom.$refLinks.splice(0, transformedFrom.$refLinks.length - 1)
|
|
1610
|
-
|
|
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]
|
|
1611
1638
|
|
|
1612
1639
|
return { transformedWhere, transformedFrom }
|
|
1613
1640
|
}
|
|
@@ -1661,7 +1688,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1661
1688
|
*/
|
|
1662
1689
|
function backlinkFor(assoc) {
|
|
1663
1690
|
if (!assoc.on) return null
|
|
1664
|
-
const target =
|
|
1691
|
+
const target = getDefinition(assoc.target)
|
|
1665
1692
|
// technically we could have multiple backlinks
|
|
1666
1693
|
const backlinks = []
|
|
1667
1694
|
for (let i = 0; i < assoc.on.length; i += 3) {
|
|
@@ -1687,7 +1714,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1687
1714
|
*/
|
|
1688
1715
|
function onCondFor(assocRefLink, targetSideRefLink, inWhereOrJoin) {
|
|
1689
1716
|
const { on, keys } = assocRefLink.definition
|
|
1690
|
-
const target =
|
|
1717
|
+
const target = getDefinition(assocRefLink.definition.target)
|
|
1691
1718
|
let res
|
|
1692
1719
|
// technically we could have multiple backlinks
|
|
1693
1720
|
if (keys) {
|
|
@@ -1738,10 +1765,11 @@ function cqn4sql(originalQuery, model) {
|
|
|
1738
1765
|
if (res === '$self')
|
|
1739
1766
|
// next is resolvable in entity
|
|
1740
1767
|
return prev
|
|
1741
|
-
const definition =
|
|
1768
|
+
const definition =
|
|
1769
|
+
prev?.elements?.[res] || getDefinition(prev?.target)?.elements[res] || pseudos.elements[res]
|
|
1742
1770
|
const target = getParentEntity(definition)
|
|
1743
1771
|
thing.$refLinks[i] = { definition, target, alias: definition.name }
|
|
1744
|
-
return prev?.elements?.[res] || prev?.
|
|
1772
|
+
return prev?.elements?.[res] || getDefinition(prev?.target)?.elements[res] || pseudos.elements[res]
|
|
1745
1773
|
}, assocHost)
|
|
1746
1774
|
}
|
|
1747
1775
|
|
|
@@ -1843,7 +1871,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1843
1871
|
result[i].ref.splice(0, 1, assocRefLink.alias)
|
|
1844
1872
|
} else if (
|
|
1845
1873
|
definition.name in
|
|
1846
|
-
(targetSideRefLink.definition.elements || targetSideRefLink.definition.
|
|
1874
|
+
(targetSideRefLink.definition.elements || getDefinition(targetSideRefLink.definition.target).elements)
|
|
1847
1875
|
) {
|
|
1848
1876
|
// first step is association which refers to its foreign key by dot notation
|
|
1849
1877
|
result[i].ref = [targetSideRefLink.alias, lhs.ref.join('_')]
|
|
@@ -1865,7 +1893,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1865
1893
|
// pseudo element
|
|
1866
1894
|
return element
|
|
1867
1895
|
if (element.kind === 'entity') return element
|
|
1868
|
-
else return
|
|
1896
|
+
else return getDefinition(localized(getParentEntity(element.parent)))
|
|
1869
1897
|
}
|
|
1870
1898
|
}
|
|
1871
1899
|
|
|
@@ -1880,11 +1908,11 @@ function cqn4sql(originalQuery, model) {
|
|
|
1880
1908
|
function getParentKeyForeignKeyPairs(assoc, targetSideRefLink, flipSourceAndTarget = false) {
|
|
1881
1909
|
const res = []
|
|
1882
1910
|
const backlink = backlinkFor(assoc)?.[0]
|
|
1883
|
-
const { keys,
|
|
1911
|
+
const { keys, target } = backlink || assoc
|
|
1884
1912
|
if (keys) {
|
|
1885
1913
|
keys.forEach(fk => {
|
|
1886
1914
|
const { ref, as } = fk
|
|
1887
|
-
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
|
|
1888
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
|
|
1889
1917
|
const flatAssociationName = getFullName(backlink || assoc) // get the name of the (backlink) association
|
|
1890
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.
|
|
@@ -1933,7 +1961,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1933
1961
|
* -> if it is, target and source side are flipped in the where exists subquery
|
|
1934
1962
|
* @returns {CQN.SELECT}
|
|
1935
1963
|
*/
|
|
1936
|
-
function getWhereExistsSubquery(current, next, customWhere = null, inWhere = false) {
|
|
1964
|
+
function getWhereExistsSubquery(current, next, customWhere = null, inWhere = false, customArgs = null) {
|
|
1937
1965
|
const { definition } = current
|
|
1938
1966
|
const { definition: nextDefinition } = next
|
|
1939
1967
|
const on = []
|
|
@@ -1957,9 +1985,12 @@ function cqn4sql(originalQuery, model) {
|
|
|
1957
1985
|
on.push(...['and', ...(hasLogicalOr(filter) ? [asXpr(filter)] : filter)])
|
|
1958
1986
|
}
|
|
1959
1987
|
|
|
1988
|
+
const subquerySource = assocTarget(nextDefinition) || nextDefinition
|
|
1989
|
+
const id = localized(subquerySource)
|
|
1990
|
+
if (subquerySource.params && !customArgs) customArgs = {}
|
|
1960
1991
|
const SELECT = {
|
|
1961
1992
|
from: {
|
|
1962
|
-
ref: [
|
|
1993
|
+
ref: [customArgs ? { id, args: customArgs } : id],
|
|
1963
1994
|
as: next.alias,
|
|
1964
1995
|
},
|
|
1965
1996
|
columns: [
|
|
@@ -1982,7 +2013,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1982
2013
|
*/
|
|
1983
2014
|
function localized(definition) {
|
|
1984
2015
|
if (!isLocalized(definition)) return definition.name
|
|
1985
|
-
const view =
|
|
2016
|
+
const view = getDefinition(`localized.${definition.name}`)
|
|
1986
2017
|
return view?.name || definition.name
|
|
1987
2018
|
}
|
|
1988
2019
|
|
|
@@ -1995,7 +2026,18 @@ function cqn4sql(originalQuery, model) {
|
|
|
1995
2026
|
* @returns true if the given definition shall be localized
|
|
1996
2027
|
*/
|
|
1997
2028
|
function isLocalized(definition) {
|
|
1998
|
-
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]
|
|
1999
2041
|
}
|
|
2000
2042
|
|
|
2001
2043
|
/**
|
|
@@ -2005,7 +2047,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2005
2047
|
* @returns the csn definition of the association target or null if it is not an association
|
|
2006
2048
|
*/
|
|
2007
2049
|
function assocTarget(assoc) {
|
|
2008
|
-
return
|
|
2050
|
+
return getDefinition(assoc.target) || null
|
|
2009
2051
|
}
|
|
2010
2052
|
|
|
2011
2053
|
/**
|
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) {
|
|
@@ -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.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
|
}
|