@cap-js/db-service 1.18.0 → 1.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/lib/InsertResults.js +1 -1
- package/lib/SQLService.js +5 -5
- package/lib/common/session-context.js +2 -2
- package/lib/cqn2sql.js +36 -29
- package/lib/cqn4sql.js +84 -77
- package/lib/deep-queries.js +4 -4
- package/lib/fill-in-keys.js +1 -1
- package/lib/infer/index.js +9 -14
- package/lib/utils.js +26 -1
- package/package.json +1 -2
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.19.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.19.0...db-service-v1.19.1) (2025-04-01)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
* **scoped queries:** wrap filter in `xpr` if needed ([#1105](https://github.com/cap-js/cds-dbs/issues/1105)) ([8f44df3](https://github.com/cap-js/cds-dbs/commit/8f44df37db7bc283933023dab92b348cf92e12bf))
|
|
13
|
+
|
|
14
|
+
## [1.19.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.18.0...db-service-v1.19.0) (2025-03-31)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
* **forUpdate:** ignore locked ([#1074](https://github.com/cap-js/cds-dbs/issues/1074)) ([163480b](https://github.com/cap-js/cds-dbs/commit/163480b245b18a2829cd871c2f053c82bcc1abef))
|
|
20
|
+
* support recursive cqn queries ([#1089](https://github.com/cap-js/cds-dbs/issues/1089)) ([f09b0f8](https://github.com/cap-js/cds-dbs/commit/f09b0f815c3788349f3d39419990cd1c00963b7d))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
* assign more technical, implicit table aliases ([#1082](https://github.com/cap-js/cds-dbs/issues/1082)) ([1f8925a](https://github.com/cap-js/cds-dbs/commit/1f8925a5d4bcc0123f3abbee2c65ed77877da591))
|
|
26
|
+
* consider `nulls first | last` on `orderBy` ([#1064](https://github.com/cap-js/cds-dbs/issues/1064)) ([c6bed60](https://github.com/cap-js/cds-dbs/commit/c6bed60f0d93b9f4a73c976727f30172707c60d9)), closes [#1062](https://github.com/cap-js/cds-dbs/issues/1062)
|
|
27
|
+
|
|
7
28
|
## [1.18.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.17.2...db-service-v1.18.0) (2025-03-04)
|
|
8
29
|
|
|
9
30
|
|
package/lib/InsertResults.js
CHANGED
package/lib/SQLService.js
CHANGED
|
@@ -25,7 +25,7 @@ class SQLService extends DatabaseService {
|
|
|
25
25
|
this.on(['INSERT', 'UPSERT', 'UPDATE'], require('./deep-queries').onDeep)
|
|
26
26
|
if (cds.env.features.db_strict) {
|
|
27
27
|
this.before(['INSERT', 'UPSERT', 'UPDATE'], ({ query }) => {
|
|
28
|
-
const elements = query.
|
|
28
|
+
const elements = query._target?.elements
|
|
29
29
|
if (!elements) return
|
|
30
30
|
const kind = query.kind || Object.keys(query)[0]
|
|
31
31
|
const operation = query[kind]
|
|
@@ -120,10 +120,10 @@ class SQLService extends DatabaseService {
|
|
|
120
120
|
async onSELECT({ query, data }) {
|
|
121
121
|
// REVISIT: for custom joins, infer is called twice, which is bad
|
|
122
122
|
// --> make cds.infer properly work with custom joins and remove this
|
|
123
|
-
if (!query.
|
|
123
|
+
if (!(query._target instanceof cds.entity)) {
|
|
124
124
|
try { this.infer(query) } catch { /**/ }
|
|
125
125
|
}
|
|
126
|
-
if (query.
|
|
126
|
+
if (!query._target?._unresolved) { // REVISIT: use query._target instead
|
|
127
127
|
// Will return multiple rows with objects inside
|
|
128
128
|
query.SELECT.expand = 'root'
|
|
129
129
|
}
|
|
@@ -267,7 +267,7 @@ class SQLService extends DatabaseService {
|
|
|
267
267
|
)
|
|
268
268
|
// Prepare and run deep query, à la CQL`DELETE from Foo[pred]:comp1.comp2...`
|
|
269
269
|
const query = DELETE.from({ ref: [...from.ref, c.name] })
|
|
270
|
-
query.
|
|
270
|
+
query._target = c._target
|
|
271
271
|
return this.onDELETE({ query, depth, visited: [...visited], target: c._target })
|
|
272
272
|
}),
|
|
273
273
|
)
|
|
@@ -382,7 +382,7 @@ class SQLService extends DatabaseService {
|
|
|
382
382
|
if (kind in { INSERT: 1, DELETE: 1, UPSERT: 1, UPDATE: 1 }) {
|
|
383
383
|
q = resolveView(q, this.model, this) // REVISIT: before resolveView was called on flat cqn obtained from cqn4sql -> is it correct to call on original q instead?
|
|
384
384
|
let target = q[kind]._transitions?.[0].target
|
|
385
|
-
if (target) q.
|
|
385
|
+
if (target) q._target = target // REVISIT: Why isn't that done in resolveView?
|
|
386
386
|
}
|
|
387
387
|
let cqn2sql = new this.class.CQN2SQL(this)
|
|
388
388
|
return cqn2sql.render(q, values)
|
|
@@ -23,8 +23,8 @@ class TemporalSessionContext extends SessionContext {
|
|
|
23
23
|
get '$valid.to'() {
|
|
24
24
|
return (super['$valid.to'] =
|
|
25
25
|
this.ctx._?.['VALID-TO'] ??
|
|
26
|
-
this.ctx._?.['VALID-AT']?.replace(
|
|
27
|
-
new Date(
|
|
26
|
+
this.ctx._?.['VALID-AT']?.replace(/\.(\d*)(Z?)$/, (_, d, z) => `.${parseInt(d) + 1}${z}`) ??
|
|
27
|
+
(new Date(Date.now() + 1)).toISOString())
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
package/lib/cqn2sql.js
CHANGED
|
@@ -25,7 +25,7 @@ class CQN2SQLRenderer {
|
|
|
25
25
|
if (cds.env.sql.names === 'quoted') {
|
|
26
26
|
this.class.prototype.name = (name, query) => {
|
|
27
27
|
const e = name.id || name
|
|
28
|
-
return (query?.
|
|
28
|
+
return (query?._target || this.model?.definitions[e])?.['@cds.persistence.name'] || e
|
|
29
29
|
}
|
|
30
30
|
this.class.prototype.quote = (s) => `"${String(s).replace(/"/g, '""')}"`
|
|
31
31
|
}
|
|
@@ -105,7 +105,7 @@ class CQN2SQLRenderer {
|
|
|
105
105
|
* @returns {import('./infer/cqn').Query}
|
|
106
106
|
*/
|
|
107
107
|
infer(q) {
|
|
108
|
-
return q.
|
|
108
|
+
return q._target instanceof cds.entity ? q : cds_infer(q)
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
cqn4sql(q) {
|
|
@@ -226,7 +226,7 @@ class CQN2SQLRenderer {
|
|
|
226
226
|
* @param {import('./infer/cqn').SELECT} q
|
|
227
227
|
*/
|
|
228
228
|
SELECT(q) {
|
|
229
|
-
let { from, expand, where, groupBy, having, orderBy, limit, one, distinct, localized, forUpdate, forShareLock } =
|
|
229
|
+
let { from, expand, where, groupBy, having, orderBy, limit, one, distinct, localized, forUpdate, forShareLock, recurse } =
|
|
230
230
|
q.SELECT
|
|
231
231
|
|
|
232
232
|
if (from?.join && !q.SELECT.columns) {
|
|
@@ -239,12 +239,13 @@ class CQN2SQLRenderer {
|
|
|
239
239
|
let sql = `SELECT`
|
|
240
240
|
if (distinct) sql += ` DISTINCT`
|
|
241
241
|
if (!_empty(columns)) sql += ` ${columns}`
|
|
242
|
-
if (
|
|
242
|
+
if (recurse) sql += ` FROM ${this.SELECT_recurse(q)}`
|
|
243
|
+
else if (!_empty(from)) sql += ` FROM ${this.from(from, q)}`
|
|
243
244
|
else sql += this.from_dummy()
|
|
244
|
-
if (!_empty(where)) sql += ` WHERE ${this.where(where)}`
|
|
245
|
-
if (!_empty(groupBy)) sql += ` GROUP BY ${this.groupBy(groupBy)}`
|
|
246
|
-
if (!_empty(having)) sql += ` HAVING ${this.having(having)}`
|
|
247
|
-
if (!_empty(orderBy)) sql += ` ORDER BY ${this.orderBy(orderBy, localized)}`
|
|
245
|
+
if (!recurse && !_empty(where)) sql += ` WHERE ${this.where(where)}`
|
|
246
|
+
if (!recurse && !_empty(groupBy)) sql += ` GROUP BY ${this.groupBy(groupBy)}`
|
|
247
|
+
if (!recurse && !_empty(having)) sql += ` HAVING ${this.having(having)}`
|
|
248
|
+
if (!recurse && !_empty(orderBy)) sql += ` ORDER BY ${this.orderBy(orderBy, localized)}`
|
|
248
249
|
if (one) limit = Object.assign({}, limit, { rows: { val: 1 } })
|
|
249
250
|
if (limit) sql += ` LIMIT ${this.limit(limit)}`
|
|
250
251
|
if (forUpdate) sql += ` ${this.forUpdate(forUpdate)}`
|
|
@@ -257,6 +258,10 @@ class CQN2SQLRenderer {
|
|
|
257
258
|
return (this.sql = sql)
|
|
258
259
|
}
|
|
259
260
|
|
|
261
|
+
SELECT_recurse() {
|
|
262
|
+
cds.error`Feature "recurse" queries not supported.`
|
|
263
|
+
}
|
|
264
|
+
|
|
260
265
|
/**
|
|
261
266
|
* Renders a column clause into generic SQL
|
|
262
267
|
* @param {import('./infer/cqn').SELECT} param0
|
|
@@ -421,14 +426,15 @@ class CQN2SQLRenderer {
|
|
|
421
426
|
* @returns {string[] | string} SQL
|
|
422
427
|
*/
|
|
423
428
|
orderBy(orderBy, localized) {
|
|
424
|
-
return orderBy.map(
|
|
425
|
-
localized
|
|
426
|
-
? c
|
|
427
|
-
this.expr(c) +
|
|
429
|
+
return orderBy.map(c => {
|
|
430
|
+
const o = localized
|
|
431
|
+
? this.expr(c) +
|
|
428
432
|
(c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
|
|
429
433
|
(c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
|
|
430
|
-
:
|
|
431
|
-
|
|
434
|
+
: this.expr(c) + (c.sort?.toLowerCase() === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
|
|
435
|
+
if (c.nulls) return o + ' NULLS ' + (c.nulls.toLowerCase() === 'first' ? 'FIRST' : 'LAST')
|
|
436
|
+
return o
|
|
437
|
+
})
|
|
432
438
|
}
|
|
433
439
|
|
|
434
440
|
/**
|
|
@@ -448,9 +454,10 @@ class CQN2SQLRenderer {
|
|
|
448
454
|
* @returns {string} SQL
|
|
449
455
|
*/
|
|
450
456
|
forUpdate(update) {
|
|
451
|
-
const { wait, of } = update
|
|
457
|
+
const { wait, of, ignoreLocked } = update
|
|
452
458
|
let sql = 'FOR UPDATE'
|
|
453
459
|
if (!_empty(of)) sql += ` OF ${of.map(x => this.expr(x)).join(', ')}`
|
|
460
|
+
if (ignoreLocked) sql += ' IGNORE LOCKED'
|
|
454
461
|
if (typeof wait === 'number') sql += ` WAIT ${wait}`
|
|
455
462
|
return sql
|
|
456
463
|
}
|
|
@@ -495,7 +502,7 @@ class CQN2SQLRenderer {
|
|
|
495
502
|
*/
|
|
496
503
|
INSERT_entries(q) {
|
|
497
504
|
const { INSERT } = q
|
|
498
|
-
const elements = q.elements || q.
|
|
505
|
+
const elements = q.elements || q._target?.elements
|
|
499
506
|
if (!elements && !INSERT.entries?.length) {
|
|
500
507
|
return // REVISIT: mtx sends an insert statement without entries and no reference entity
|
|
501
508
|
}
|
|
@@ -507,7 +514,7 @@ class CQN2SQLRenderer {
|
|
|
507
514
|
this.columns = columns
|
|
508
515
|
|
|
509
516
|
const alias = INSERT.into.as
|
|
510
|
-
const entity = this.name(q.
|
|
517
|
+
const entity = this.name(q._target?.name || INSERT.into.ref[0], q)
|
|
511
518
|
if (!elements) {
|
|
512
519
|
this.entries = INSERT.entries.map(e => columns.map(c => e[c]))
|
|
513
520
|
const param = this.param.bind(this, { ref: ['?'] })
|
|
@@ -533,7 +540,7 @@ class CQN2SQLRenderer {
|
|
|
533
540
|
}
|
|
534
541
|
|
|
535
542
|
async *INSERT_entries_stream(entries, binaryEncoding = 'base64') {
|
|
536
|
-
const elements = this.cqn.
|
|
543
|
+
const elements = this.cqn._target?.elements || {}
|
|
537
544
|
const bufferLimit = 65536 // 1 << 16
|
|
538
545
|
let buffer = '['
|
|
539
546
|
|
|
@@ -582,7 +589,7 @@ class CQN2SQLRenderer {
|
|
|
582
589
|
}
|
|
583
590
|
|
|
584
591
|
async *INSERT_rows_stream(entries, binaryEncoding = 'base64') {
|
|
585
|
-
const elements = this.cqn.
|
|
592
|
+
const elements = this.cqn._target?.elements || {}
|
|
586
593
|
const bufferLimit = 65536 // 1 << 16
|
|
587
594
|
let buffer = '['
|
|
588
595
|
|
|
@@ -635,9 +642,9 @@ class CQN2SQLRenderer {
|
|
|
635
642
|
*/
|
|
636
643
|
INSERT_rows(q) {
|
|
637
644
|
const { INSERT } = q
|
|
638
|
-
const entity = this.name(q.
|
|
645
|
+
const entity = this.name(q._target?.name || INSERT.into.ref[0], q)
|
|
639
646
|
const alias = INSERT.into.as
|
|
640
|
-
const elements = q.elements || q.
|
|
647
|
+
const elements = q.elements || q._target?.elements
|
|
641
648
|
const columns = this.columns = INSERT.columns || cds.error`Cannot insert rows without columns or elements`
|
|
642
649
|
|
|
643
650
|
if (!elements) {
|
|
@@ -681,9 +688,9 @@ class CQN2SQLRenderer {
|
|
|
681
688
|
*/
|
|
682
689
|
INSERT_select(q) {
|
|
683
690
|
const { INSERT } = q
|
|
684
|
-
const entity = this.name(q.
|
|
691
|
+
const entity = this.name(q._target.name, q)
|
|
685
692
|
const alias = INSERT.into.as
|
|
686
|
-
const elements = q.elements || q.
|
|
693
|
+
const elements = q.elements || q._target?.elements || {}
|
|
687
694
|
const columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter(
|
|
688
695
|
c => c in elements && !elements[c].virtual && !elements[c].isAssociation,
|
|
689
696
|
))
|
|
@@ -724,15 +731,15 @@ class CQN2SQLRenderer {
|
|
|
724
731
|
const { UPSERT } = q
|
|
725
732
|
|
|
726
733
|
let sql = this.INSERT({ __proto__: q, INSERT: UPSERT })
|
|
727
|
-
if (!q.
|
|
734
|
+
if (!q._target?.keys) return sql
|
|
728
735
|
const keys = []
|
|
729
|
-
for (const k of ObjectKeys(q.
|
|
730
|
-
const element = q.
|
|
736
|
+
for (const k of ObjectKeys(q._target?.keys)) {
|
|
737
|
+
const element = q._target.keys[k]
|
|
731
738
|
if (element.isAssociation || element.virtual) continue
|
|
732
739
|
keys.push(k)
|
|
733
740
|
}
|
|
734
741
|
|
|
735
|
-
const elements = q.
|
|
742
|
+
const elements = q._target?.elements || {}
|
|
736
743
|
// temporal data
|
|
737
744
|
for (const k of ObjectKeys(elements)) {
|
|
738
745
|
if (elements[k]['@cds.valid.from']) keys.push(k)
|
|
@@ -749,7 +756,7 @@ class CQN2SQLRenderer {
|
|
|
749
756
|
.filter(c => keys.includes(c.name))
|
|
750
757
|
.map(c => `${c.onInsert || c.sql} as ${this.quote(c.name)}`)
|
|
751
758
|
|
|
752
|
-
const entity = this.name(q.
|
|
759
|
+
const entity = this.name(q._target?.name || UPSERT.into.ref[0], q)
|
|
753
760
|
sql = `SELECT ${managed.map(c => c.upsert
|
|
754
761
|
.replace(/value->/g, '"$$$$value$$$$"->')
|
|
755
762
|
.replace(/json_type\(value,/g, 'json_type("$$$$value$$$$",'))
|
|
@@ -778,7 +785,7 @@ class CQN2SQLRenderer {
|
|
|
778
785
|
*/
|
|
779
786
|
UPDATE(q) {
|
|
780
787
|
const { entity, with: _with, data, where } = q.UPDATE
|
|
781
|
-
const elements = q.
|
|
788
|
+
const elements = q._target?.elements
|
|
782
789
|
let sql = `UPDATE ${this.quote(this.name(entity.ref?.[0] || entity, q))}`
|
|
783
790
|
if (entity.as) sql += ` AS ${this.quote(entity.as)}`
|
|
784
791
|
|
package/lib/cqn4sql.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const cds = require('@sap/cds')
|
|
4
|
+
cds.infer.target ??= q => q._target || q.target // instanceof cds.entity ? q._target : q.target
|
|
4
5
|
|
|
5
6
|
const infer = require('./infer')
|
|
6
7
|
const { computeColumnsToBeSearched } = require('./search')
|
|
7
|
-
const { prettyPrintRef, isCalculatedOnRead, isCalculatedElement } = require('./utils')
|
|
8
|
+
const { prettyPrintRef, isCalculatedOnRead, isCalculatedElement, getImplicitAlias } = require('./utils')
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* For operators of <eqOps>, this is replaced by comparing all leaf elements with null, combined with and.
|
|
@@ -62,16 +63,16 @@ function cqn4sql(originalQuery, model) {
|
|
|
62
63
|
}
|
|
63
64
|
// query modifiers can also be defined in from ref leaf infix filter
|
|
64
65
|
// > SELECT from bookshop.Books[order by price] {ID}
|
|
65
|
-
if(inferred.SELECT?.from.ref) {
|
|
66
|
-
for(const [key, val] of Object.entries(inferred.SELECT.from.ref.at(-1))) {
|
|
67
|
-
if(key in { orderBy: 1, groupBy: 1 }) {
|
|
68
|
-
if(inferred.SELECT[key]) inferred.SELECT[key].push(...val)
|
|
66
|
+
if (inferred.SELECT?.from.ref) {
|
|
67
|
+
for (const [key, val] of Object.entries(inferred.SELECT.from.ref.at(-1))) {
|
|
68
|
+
if (key in { orderBy: 1, groupBy: 1 }) {
|
|
69
|
+
if (inferred.SELECT[key]) inferred.SELECT[key].push(...val)
|
|
69
70
|
else inferred.SELECT[key] = val
|
|
70
|
-
} else if(key === 'limit') {
|
|
71
|
+
} else if (key === 'limit') {
|
|
71
72
|
// limit defined on the query has precedence
|
|
72
|
-
if(!inferred.SELECT.limit) inferred.SELECT.limit = val
|
|
73
|
-
} else if(key === 'having') {
|
|
74
|
-
if(!inferred.SELECT.having) inferred.SELECT.having = val
|
|
73
|
+
if (!inferred.SELECT.limit) inferred.SELECT.limit = val
|
|
74
|
+
} else if (key === 'having') {
|
|
75
|
+
if (!inferred.SELECT.having) inferred.SELECT.having = val
|
|
75
76
|
else inferred.SELECT.having.push('and', ...val)
|
|
76
77
|
}
|
|
77
78
|
}
|
|
@@ -224,7 +225,8 @@ function cqn4sql(originalQuery, model) {
|
|
|
224
225
|
*/
|
|
225
226
|
function transformQueryForInsertUpsert(kind) {
|
|
226
227
|
const { as } = transformedQuery[kind].into
|
|
227
|
-
|
|
228
|
+
const target = cds.infer.target (inferred) // REVISIT: we should reliably use inferred._target instead
|
|
229
|
+
transformedQuery[kind].into = { ref: [target.name] }
|
|
228
230
|
if (as) transformedQuery[kind].into.as = as
|
|
229
231
|
return transformedQuery
|
|
230
232
|
}
|
|
@@ -800,7 +802,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
800
802
|
} else {
|
|
801
803
|
outerAlias = transformedQuery.SELECT.from.as
|
|
802
804
|
subqueryFromRef = [
|
|
803
|
-
...(transformedQuery.SELECT.from.ref || /* subq in from */
|
|
805
|
+
...(transformedQuery.SELECT.from.ref || /* subq in from */ transformedQuery.SELECT.from.SELECT.from.ref),
|
|
804
806
|
...ref,
|
|
805
807
|
]
|
|
806
808
|
}
|
|
@@ -816,16 +818,18 @@ function cqn4sql(originalQuery, model) {
|
|
|
816
818
|
return _subqueryForGroupBy(column, baseRef, columnAlias)
|
|
817
819
|
}
|
|
818
820
|
|
|
819
|
-
//
|
|
820
|
-
//
|
|
821
|
-
|
|
821
|
+
// Alias in expand subquery is derived from but not equal to
|
|
822
|
+
// the alias of the column because to account for potential ambiguities
|
|
823
|
+
// the alias cannot be addressed anyways
|
|
824
|
+
const uniqueSubqueryAlias = getNextAvailableTableAlias(getImplicitAlias(columnAlias), inferred.outerQueries)
|
|
822
825
|
|
|
823
826
|
// `SELECT from Authors { books.genre as genreOfBooks { name } } becomes `SELECT from Books:genre as genreOfBooks`
|
|
824
827
|
const from = { ref: subqueryFromRef, as: uniqueSubqueryAlias }
|
|
825
828
|
const subqueryBase = {}
|
|
826
829
|
const queryModifiers = { ...column }
|
|
827
830
|
for (const [key, value] of Object.entries(queryModifiers)) {
|
|
828
|
-
if (key in { limit: 1, orderBy: 1, groupBy: 1, excluding: 1, where: 1, having: 1, count: 1 })
|
|
831
|
+
if (key in { limit: 1, orderBy: 1, groupBy: 1, excluding: 1, where: 1, having: 1, count: 1 })
|
|
832
|
+
subqueryBase[key] = value
|
|
829
833
|
}
|
|
830
834
|
|
|
831
835
|
const subquery = {
|
|
@@ -869,6 +873,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
869
873
|
const existsSubqueryAlias = recent[existsIndex + 1].SELECT.from.as
|
|
870
874
|
if (existsSubqueryAlias === x.ref?.[0]) return { ref: [outer, ...x.ref.slice(1)] }
|
|
871
875
|
if (x.xpr) x.xpr = x.xpr.map(replaceAliasWithSubqueryAlias)
|
|
876
|
+
if (x.args) x.args = x.args.map(replaceAliasWithSubqueryAlias)
|
|
872
877
|
return x
|
|
873
878
|
}
|
|
874
879
|
return subq
|
|
@@ -898,37 +903,39 @@ function cqn4sql(originalQuery, model) {
|
|
|
898
903
|
// expand with wildcard vanishes as expand is part of the group by (OData $apply + $expand)
|
|
899
904
|
return null
|
|
900
905
|
}
|
|
901
|
-
const expandedColumns = column.expand
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
906
|
+
const expandedColumns = column.expand
|
|
907
|
+
.flatMap(expand => {
|
|
908
|
+
if (!expand.ref) return expand
|
|
909
|
+
const fullRef = [...baseRef, ...expand.ref]
|
|
910
|
+
|
|
911
|
+
if (expand.expand) {
|
|
912
|
+
const nested = _subqueryForGroupBy(expand, fullRef, expand.as || expand.ref.map(idOnly).join('_'))
|
|
913
|
+
if (nested) {
|
|
914
|
+
setElementOnColumns(nested, expand.element)
|
|
915
|
+
elements[expand.as || expand.ref.map(idOnly).join('_')] = nested
|
|
916
|
+
}
|
|
917
|
+
return nested
|
|
910
918
|
}
|
|
911
|
-
return nested
|
|
912
|
-
}
|
|
913
919
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
+
const groupByRef = groupByLookup.get(fullRef.map(refWithConditions).join('.'))
|
|
921
|
+
if (!groupByRef) {
|
|
922
|
+
throw new Error(
|
|
923
|
+
`The expanded column "${fullRef.map(refWithConditions).join('.')}" must be part of the group by clause`,
|
|
924
|
+
)
|
|
925
|
+
}
|
|
920
926
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
927
|
+
const copy = Object.create(groupByRef)
|
|
928
|
+
// always alias for this special case, so that they nested element names match the expected result structure
|
|
929
|
+
// otherwise we'd get `author { <outer>.author_ID }`, but we need `author { <outer>.author_ID as ID }`
|
|
930
|
+
copy.as = expand.as || expand.ref.at(-1)
|
|
931
|
+
const tableAlias = getTableAlias(copy)
|
|
932
|
+
const res = getFlatColumnsFor(copy, { tableAlias })
|
|
933
|
+
res.forEach(c => {
|
|
934
|
+
elements[c.as || c.ref.at(-1)] = c.element
|
|
935
|
+
})
|
|
936
|
+
return res
|
|
929
937
|
})
|
|
930
|
-
|
|
931
|
-
}).filter(c => c)
|
|
938
|
+
.filter(c => c)
|
|
932
939
|
|
|
933
940
|
if (expandedColumns.length === 0) {
|
|
934
941
|
return null
|
|
@@ -1063,7 +1070,8 @@ function cqn4sql(originalQuery, model) {
|
|
|
1063
1070
|
outerQueries.push(inferred)
|
|
1064
1071
|
Object.defineProperty(q, 'outerQueries', { value: outerQueries })
|
|
1065
1072
|
}
|
|
1066
|
-
|
|
1073
|
+
const target = cds.infer.target (inferred) // REVISIT: we should reliably use inferred._target instead
|
|
1074
|
+
if (isLocalized(target)) q.SELECT.localized = true
|
|
1067
1075
|
if (q.SELECT.from.ref && !q.SELECT.from.as) assignUniqueSubqueryAlias()
|
|
1068
1076
|
return cqn4sql(q, model)
|
|
1069
1077
|
|
|
@@ -1071,7 +1079,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1071
1079
|
if (q.SELECT.from.uniqueSubqueryAlias) return
|
|
1072
1080
|
const last = q.SELECT.from.ref.at(-1)
|
|
1073
1081
|
const uniqueSubqueryAlias = inferred.joinTree.addNextAvailableTableAlias(
|
|
1074
|
-
|
|
1082
|
+
getImplicitAlias(last.id || last),
|
|
1075
1083
|
inferred.outerQueries,
|
|
1076
1084
|
)
|
|
1077
1085
|
Object.defineProperty(q.SELECT.from, 'uniqueSubqueryAlias', { value: uniqueSubqueryAlias })
|
|
@@ -1400,7 +1408,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1400
1408
|
j = nextAssocIndex
|
|
1401
1409
|
}
|
|
1402
1410
|
|
|
1403
|
-
const as = getNextAvailableTableAlias(
|
|
1411
|
+
const as = getNextAvailableTableAlias(getImplicitAlias(next.alias))
|
|
1404
1412
|
next.alias = as
|
|
1405
1413
|
if (!next.definition.target) {
|
|
1406
1414
|
let type = next.definition.type
|
|
@@ -1696,12 +1704,12 @@ function cqn4sql(originalQuery, model) {
|
|
|
1696
1704
|
function _transformFrom() {
|
|
1697
1705
|
if (typeof from === 'string') {
|
|
1698
1706
|
// normalize to `ref`, i.e. for `UPDATE.entity('bookshop.Books')`
|
|
1699
|
-
return { transformedFrom: { ref: [from], as:
|
|
1707
|
+
return { transformedFrom: { ref: [from], as: getImplicitAlias(from) } }
|
|
1700
1708
|
}
|
|
1701
1709
|
transformedFrom.as =
|
|
1702
1710
|
from.uniqueSubqueryAlias ||
|
|
1703
1711
|
from.as ||
|
|
1704
|
-
|
|
1712
|
+
getImplicitAlias(transformedFrom.$refLinks[transformedFrom.$refLinks.length - 1].definition.name)
|
|
1705
1713
|
const whereExistsSubSelects = []
|
|
1706
1714
|
const filterConditions = []
|
|
1707
1715
|
const refReverse = [...from.ref].reverse()
|
|
@@ -1723,14 +1731,17 @@ function cqn4sql(originalQuery, model) {
|
|
|
1723
1731
|
.findIndex(rl => rl.definition.isAssociation || rl.definition.kind === 'entity')
|
|
1724
1732
|
next = $refLinksReverse[nextStepIndex]
|
|
1725
1733
|
}
|
|
1726
|
-
let as =
|
|
1734
|
+
let as = getImplicitAlias(next.alias)
|
|
1727
1735
|
/**
|
|
1728
1736
|
* for an `expand` subquery, we do not need to add
|
|
1729
1737
|
* the table alias of the `expand` host to the join tree
|
|
1730
1738
|
* --> This is an artificial query, which will later be correlated
|
|
1731
1739
|
* with the main query alias. see @function expandColumn()
|
|
1740
|
+
* There is one exception:
|
|
1741
|
+
* - if current and next have the same alias, we need to assign a new alias to the next
|
|
1742
|
+
*
|
|
1732
1743
|
*/
|
|
1733
|
-
if (!(inferred.SELECT?.expand === true)) {
|
|
1744
|
+
if (!(inferred.SELECT?.expand === true && current.alias.toLowerCase() !== as.toLowerCase())) {
|
|
1734
1745
|
as = getNextAvailableTableAlias(as)
|
|
1735
1746
|
}
|
|
1736
1747
|
next.alias = as
|
|
@@ -1755,7 +1766,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1755
1766
|
filterConditions.forEach(f => {
|
|
1756
1767
|
transformedWhere.push('and')
|
|
1757
1768
|
if (filterConditions.length > 1) transformedWhere.push(asXpr(f))
|
|
1758
|
-
else if (f.length > 3) transformedWhere.push(asXpr(f))
|
|
1769
|
+
else if (f.length > 3 || f.includes('or') || f.includes('and')) transformedWhere.push(asXpr(f))
|
|
1759
1770
|
else transformedWhere.push(...f)
|
|
1760
1771
|
})
|
|
1761
1772
|
} else {
|
|
@@ -1917,9 +1928,11 @@ function cqn4sql(originalQuery, model) {
|
|
|
1917
1928
|
})
|
|
1918
1929
|
let pseudoPath = false
|
|
1919
1930
|
ref.reduce((prev, res, i) => {
|
|
1920
|
-
if (res === '$self')
|
|
1931
|
+
if (res === '$self') {
|
|
1921
1932
|
// next is resolvable in entity
|
|
1933
|
+
thing.$refLinks.push({ definition: prev, target: prev })
|
|
1922
1934
|
return prev
|
|
1935
|
+
}
|
|
1923
1936
|
if (res in pseudos.elements) {
|
|
1924
1937
|
pseudoPath = true
|
|
1925
1938
|
thing.$refLinks.push({ definition: pseudos.elements[res], target: pseudos })
|
|
@@ -1931,7 +1944,11 @@ function cqn4sql(originalQuery, model) {
|
|
|
1931
1944
|
const definition =
|
|
1932
1945
|
prev?.elements?.[res] || getDefinition(prev?.target)?.elements[res] || pseudos.elements[res]
|
|
1933
1946
|
const target = getParentEntity(definition)
|
|
1934
|
-
thing.$refLinks[i] = {
|
|
1947
|
+
thing.$refLinks[i] = {
|
|
1948
|
+
definition,
|
|
1949
|
+
target,
|
|
1950
|
+
alias: definition === assocRefLink.definition ? assocRefLink.alias : definition.name,
|
|
1951
|
+
}
|
|
1935
1952
|
return prev?.elements?.[res] || getDefinition(prev?.target)?.elements[res] || pseudos.elements[res]
|
|
1936
1953
|
}, assocHost)
|
|
1937
1954
|
}
|
|
@@ -1956,15 +1973,16 @@ function cqn4sql(originalQuery, model) {
|
|
|
1956
1973
|
lhs.ref[0] in { $self: true, $projection: true } ? getParentEntity(assocRefLink.definition) : target,
|
|
1957
1974
|
)
|
|
1958
1975
|
else {
|
|
1959
|
-
const
|
|
1960
|
-
const
|
|
1976
|
+
const lhsLeafDef = lhs.ref && lhs.$refLinks.at(-1).definition
|
|
1977
|
+
const rhsLeafDef = rhs.ref && rhs.$refLinks.at(-1).definition
|
|
1978
|
+
const lhsFirstDef = lhs.$refLinks.find(r => r.definition.kind === 'element')?.definition
|
|
1961
1979
|
// compare structures in on-condition
|
|
1962
|
-
if ((
|
|
1980
|
+
if ((lhsLeafDef?.target && rhsLeafDef?.target) || (lhsLeafDef?.elements && rhsLeafDef?.elements)) {
|
|
1963
1981
|
if (rhs.$refLinks[0].definition !== assocRefLink.definition) {
|
|
1964
1982
|
rhs.ref.unshift(targetSideRefLink.alias)
|
|
1965
1983
|
rhs.$refLinks.unshift(targetSideRefLink)
|
|
1966
1984
|
}
|
|
1967
|
-
if (
|
|
1985
|
+
if (lhsFirstDef !== assocRefLink.definition) {
|
|
1968
1986
|
lhs.ref.unshift(targetSideRefLink.alias)
|
|
1969
1987
|
lhs.$refLinks.unshift(targetSideRefLink)
|
|
1970
1988
|
}
|
|
@@ -1974,16 +1992,15 @@ function cqn4sql(originalQuery, model) {
|
|
|
1974
1992
|
i += res.length
|
|
1975
1993
|
continue
|
|
1976
1994
|
}
|
|
1977
|
-
//
|
|
1995
|
+
// assumption: if first step is the association itself, all following ref steps must be resolvable
|
|
1978
1996
|
// within target `assoc.assoc.fk` -> `assoc.assoc_fk`
|
|
1979
1997
|
else if (
|
|
1980
|
-
|
|
1981
|
-
getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]
|
|
1998
|
+
lhsFirstDef === getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]
|
|
1982
1999
|
)
|
|
1983
|
-
result[i].ref = [assocRefLink.alias, lhs.ref.slice(1).join('_')]
|
|
2000
|
+
result[i].ref = [assocRefLink.alias, lhs.ref.slice(lhs.ref[0] === '$self' ? 2 : 1).join('_')]
|
|
1984
2001
|
// naive assumption: if the path starts with an association which is not the association from
|
|
1985
2002
|
// which the on-condition originates, it must be a foreign key and hence resolvable in the source
|
|
1986
|
-
else if (
|
|
2003
|
+
else if (lhsFirstDef?.target) result[i].ref = [result[i].ref.join('_')]
|
|
1987
2004
|
}
|
|
1988
2005
|
}
|
|
1989
2006
|
if (backlink) {
|
|
@@ -2006,7 +2023,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2006
2023
|
// sanity check: error out if we can't produce a join
|
|
2007
2024
|
if (backlink.keys.length === 0) {
|
|
2008
2025
|
throw new Error(
|
|
2009
|
-
`Path step “${assocRefLink.
|
|
2026
|
+
`Path step “${assocRefLink.definition.name}” is a self comparison with “${getFullName(backlink)}” that has no foreign keys`,
|
|
2010
2027
|
)
|
|
2011
2028
|
}
|
|
2012
2029
|
// managed backlink -> calculate fk-pk pairs
|
|
@@ -2032,7 +2049,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2032
2049
|
if (lhs.ref[0] !== assocRefLink.alias && lhs.ref[0] !== targetSideRefLink.alias) {
|
|
2033
2050
|
// we need to find correct table alias for the structured access
|
|
2034
2051
|
const { definition } = lhs.$refLinks[0]
|
|
2035
|
-
if (definition === assocRefLink.definition) {
|
|
2052
|
+
if (definition === getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]) {
|
|
2036
2053
|
// first step is the association itself -> use it's name as it becomes the table alias
|
|
2037
2054
|
result[i].ref.splice(0, 1, assocRefLink.alias)
|
|
2038
2055
|
} else if (
|
|
@@ -2050,6 +2067,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2050
2067
|
}
|
|
2051
2068
|
/**
|
|
2052
2069
|
* Recursively calculates the containing entity for a given element.
|
|
2070
|
+
* If the query is localized, this function may return the localized entity.
|
|
2053
2071
|
*
|
|
2054
2072
|
* @param {CSN.element} element
|
|
2055
2073
|
* @returns {CSN.definition} the entity containing the given element
|
|
@@ -2243,7 +2261,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2243
2261
|
* - or null, if no searchable columns are found in neither in `@cds.search` nor in the target entity itself.
|
|
2244
2262
|
*/
|
|
2245
2263
|
function getSearchTerm(search, query) {
|
|
2246
|
-
const entity = query.SELECT.from.SELECT ? query.SELECT.from : query.
|
|
2264
|
+
const entity = query.SELECT.from.SELECT ? query.SELECT.from : cds.infer.target(query) // REVISIT: we should reliably use inferred._target instead
|
|
2247
2265
|
const searchIn = computeColumnsToBeSearched(inferred, entity)
|
|
2248
2266
|
if (searchIn.length > 0) {
|
|
2249
2267
|
const xpr = search
|
|
@@ -2304,7 +2322,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2304
2322
|
if (
|
|
2305
2323
|
inferred.SELECT?.from.uniqueSubqueryAlias &&
|
|
2306
2324
|
!inferred.SELECT?.from.as &&
|
|
2307
|
-
firstStep ===
|
|
2325
|
+
getImplicitAlias(firstStep) === getImplicitAlias(transformedQuery.SELECT.from.ref[0])
|
|
2308
2326
|
) {
|
|
2309
2327
|
return inferred.SELECT?.from.uniqueSubqueryAlias
|
|
2310
2328
|
}
|
|
@@ -2313,7 +2331,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2313
2331
|
}
|
|
2314
2332
|
|
|
2315
2333
|
function getCombinedElementAlias(node) {
|
|
2316
|
-
return
|
|
2334
|
+
return inferred.$combinedElements[node.ref[0].id || node.ref[0]]?.[0].index
|
|
2317
2335
|
}
|
|
2318
2336
|
}
|
|
2319
2337
|
function getTransformedFunctionArgs(args, $baseLink = null) {
|
|
@@ -2386,17 +2404,6 @@ function hasLogicalOr(tokenStream) {
|
|
|
2386
2404
|
return tokenStream.some(t => t in { OR: true, or: true })
|
|
2387
2405
|
}
|
|
2388
2406
|
|
|
2389
|
-
/**
|
|
2390
|
-
* Returns the last segment of a string after the last dot.
|
|
2391
|
-
*
|
|
2392
|
-
* @param {string} str - The input string.
|
|
2393
|
-
* @returns {string} The last segment of the string after the last dot. If there is no dot in the string, the function returns the original string.
|
|
2394
|
-
*/
|
|
2395
|
-
function getLastStringSegment(str) {
|
|
2396
|
-
const index = str.lastIndexOf('.')
|
|
2397
|
-
return index != -1 ? str.substring(index + 1) : str
|
|
2398
|
-
}
|
|
2399
|
-
|
|
2400
2407
|
function getParentEntity(element) {
|
|
2401
2408
|
if (element.kind === 'entity') return element
|
|
2402
2409
|
else return getParentEntity(element.parent)
|
package/lib/deep-queries.js
CHANGED
|
@@ -39,9 +39,9 @@ async function onDeep(req, next) {
|
|
|
39
39
|
// const target = query.sources[Object.keys(query.sources)[0]]
|
|
40
40
|
if (!this.model?.definitions[_target_name4(req.query)]) return next()
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
if (!hasDeep(query, target)) return next()
|
|
42
|
+
if (!hasDeep(query)) return next()
|
|
44
43
|
|
|
44
|
+
const target = this.infer(query)._target
|
|
45
45
|
const beforeData = query.INSERT ? [] : await this.run(getExpandForDeep(query, target, true))
|
|
46
46
|
if (query.UPDATE && !beforeData.length) return 0
|
|
47
47
|
|
|
@@ -64,10 +64,10 @@ async function onDeep(req, next) {
|
|
|
64
64
|
return rootResult ?? beforeData.length
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
const hasDeep = (q
|
|
67
|
+
const hasDeep = (q) => {
|
|
68
68
|
const data = q.INSERT?.entries || (q.UPDATE?.data && [q.UPDATE.data]) || (q.UPDATE?.with && [q.UPDATE.with])
|
|
69
69
|
if (data)
|
|
70
|
-
for (const c in
|
|
70
|
+
for (const c in q._target.compositions) {
|
|
71
71
|
for (const row of data) if (row[c] !== undefined) return true
|
|
72
72
|
}
|
|
73
73
|
}
|
package/lib/fill-in-keys.js
CHANGED
|
@@ -59,7 +59,7 @@ module.exports = async function fill_in_keys(req, next) {
|
|
|
59
59
|
// REVISIT dummy handler until we have input processing
|
|
60
60
|
if (!req.target || !this.model || req.target._unresolved) return next()
|
|
61
61
|
// only for deep update
|
|
62
|
-
if (req.event === 'UPDATE' && hasDeep(req.query
|
|
62
|
+
if (req.event === 'UPDATE' && hasDeep(req.query)) {
|
|
63
63
|
// REVISIT for deep update we need to inject the keys first
|
|
64
64
|
enrichDataWithKeysFromWhere(req.data, req, this)
|
|
65
65
|
}
|
package/lib/infer/index.js
CHANGED
|
@@ -4,7 +4,7 @@ const cds = require('@sap/cds')
|
|
|
4
4
|
|
|
5
5
|
const JoinTree = require('./join-tree')
|
|
6
6
|
const { pseudos } = require('./pseudos')
|
|
7
|
-
const { isCalculatedOnRead } = require('../utils')
|
|
7
|
+
const { isCalculatedOnRead, getImplicitAlias } = require('../utils')
|
|
8
8
|
const cdsTypes = cds.linked({
|
|
9
9
|
definitions: {
|
|
10
10
|
Timestamp: { type: 'cds.Timestamp' },
|
|
@@ -47,21 +47,16 @@ function infer(originalQuery, model) {
|
|
|
47
47
|
const sources = inferTarget(_.from || _.into || _.entity, {})
|
|
48
48
|
const joinTree = new JoinTree(sources)
|
|
49
49
|
const aliases = Object.keys(sources)
|
|
50
|
+
const target = aliases.length === 1 ? getDefinitionFromSources(sources, aliases[0]) : originalQuery
|
|
50
51
|
Object.defineProperties(inferred, {
|
|
51
52
|
// REVISIT: public, or for local reuse, or in cqn4sql only?
|
|
52
53
|
sources: { value: sources, writable: true },
|
|
53
|
-
target:
|
|
54
|
-
value: aliases.length === 1 ? getDefinitionFromSources(sources, aliases[0]) : originalQuery,
|
|
55
|
-
writable: true,
|
|
56
|
-
}, // REVISIT: legacy?
|
|
54
|
+
_target: { value: target, writable: true, configurable: true }, // REVISIT: legacy?
|
|
57
55
|
})
|
|
58
56
|
// also enrich original query -> writable because it may be inferred again
|
|
59
57
|
Object.defineProperties(originalQuery, {
|
|
60
58
|
sources: { value: sources, writable: true },
|
|
61
|
-
target:
|
|
62
|
-
value: aliases.length === 1 ? getDefinitionFromSources(sources, aliases[0]) : originalQuery,
|
|
63
|
-
writable: true,
|
|
64
|
-
},
|
|
59
|
+
_target: { value: target, writable: true, configurable: true },
|
|
65
60
|
})
|
|
66
61
|
if (originalQuery.SELECT || originalQuery.DELETE || originalQuery.UPDATE) {
|
|
67
62
|
$combinedElements = inferCombinedElements()
|
|
@@ -97,7 +92,7 @@ function infer(originalQuery, model) {
|
|
|
97
92
|
* Each key is a query source alias, and its value is the corresponding CSN Definition.
|
|
98
93
|
* @returns {object} The updated `querySources` object with inferred sources from the `from` clause.
|
|
99
94
|
*/
|
|
100
|
-
function inferTarget(from, querySources) {
|
|
95
|
+
function inferTarget(from, querySources, useTechnicalAlias = true) {
|
|
101
96
|
const { ref } = from
|
|
102
97
|
if (ref) {
|
|
103
98
|
const { id, args } = ref[0]
|
|
@@ -119,14 +114,14 @@ function infer(originalQuery, model) {
|
|
|
119
114
|
from.uniqueSubqueryAlias ||
|
|
120
115
|
from.as ||
|
|
121
116
|
(ref.length === 1
|
|
122
|
-
?
|
|
123
|
-
: (ref.at(-1).id || ref.at(-1)));
|
|
117
|
+
? getImplicitAlias(first, useTechnicalAlias)
|
|
118
|
+
: getImplicitAlias(ref.at(-1).id || ref.at(-1), useTechnicalAlias));
|
|
124
119
|
if (alias in querySources) throw new Error(`Duplicate alias "${alias}"`)
|
|
125
120
|
querySources[alias] = { definition: target, args }
|
|
126
121
|
const last = from.$refLinks.at(-1)
|
|
127
122
|
last.alias = alias
|
|
128
123
|
} else if (from.args) {
|
|
129
|
-
from.args.forEach(a => inferTarget(a, querySources))
|
|
124
|
+
from.args.forEach(a => inferTarget(a, querySources, false))
|
|
130
125
|
} else if (from.SELECT) {
|
|
131
126
|
const subqueryInFrom = infer(from, model) // we need the .elements in the sources
|
|
132
127
|
// if no explicit alias is provided, we make up one
|
|
@@ -136,7 +131,7 @@ function infer(originalQuery, model) {
|
|
|
136
131
|
} else if (typeof from === 'string') {
|
|
137
132
|
// TODO: Create unique alias, what about duplicates?
|
|
138
133
|
const definition = getDefinition(from) || cds.error`"${from}" not found in the definitions of your model`
|
|
139
|
-
querySources[
|
|
134
|
+
querySources[getImplicitAlias(from, useTechnicalAlias)] = { definition }
|
|
140
135
|
} else if (from.SET) {
|
|
141
136
|
infer(from, model)
|
|
142
137
|
}
|
package/lib/utils.js
CHANGED
|
@@ -38,9 +38,34 @@ function isCalculatedElement(def) {
|
|
|
38
38
|
return def?.value
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Calculates the implicit table alias for a given string.
|
|
43
|
+
*
|
|
44
|
+
* Based on the last part of the string, the implicit alias is calculated
|
|
45
|
+
* by taking the first character and prepending it with '$'.
|
|
46
|
+
* A leading '$' is removed if the last part already starts with '$'.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* getImplicitAlias('Books') => '$B'
|
|
50
|
+
* getImplicitAlias('bookshop.Books') => '$B'
|
|
51
|
+
* getImplicitAlias('bookshop.$B') => '$B'
|
|
52
|
+
*
|
|
53
|
+
* @param {string} str - The input string.
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
function getImplicitAlias(str, useTechnicalAlias = true) {
|
|
57
|
+
const index = str.lastIndexOf('.')
|
|
58
|
+
if(useTechnicalAlias) {
|
|
59
|
+
const postfix = (index != -1 ? str.substring(index + 1) : str).replace(/^\$/, '')[0] || /* str === '$' */ '$'
|
|
60
|
+
return '$' + postfix
|
|
61
|
+
}
|
|
62
|
+
return index != -1 ? str.substring(index + 1) : str
|
|
63
|
+
}
|
|
64
|
+
|
|
41
65
|
// export the function to be used in other modules
|
|
42
66
|
module.exports = {
|
|
43
67
|
prettyPrintRef,
|
|
44
68
|
isCalculatedOnRead,
|
|
45
|
-
isCalculatedElement
|
|
69
|
+
isCalculatedElement,
|
|
70
|
+
getImplicitAlias,
|
|
46
71
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/db-service",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.1",
|
|
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": {
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
],
|
|
17
17
|
"author": "SAP SE (https://www.sap.com)",
|
|
18
18
|
"main": "index.js",
|
|
19
|
-
"types": "./dist/index.d.ts",
|
|
20
19
|
"files": [
|
|
21
20
|
"lib",
|
|
22
21
|
"CHANGELOG.md"
|