@cap-js/db-service 1.2.1 → 1.3.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 +14 -0
- package/README.md +17 -1
- package/lib/SQLService.js +40 -15
- package/lib/common/DatabaseService.js +8 -7
- package/lib/cql-functions.js +2 -2
- package/lib/cqn2sql.js +21 -14
- package/lib/cqn4sql.js +61 -33
- package/lib/deep-queries.js +18 -35
- package/lib/fill-in-keys.js +1 -1
- package/lib/infer/index.js +94 -60
- package/lib/infer/join-tree.js +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@
|
|
|
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
|
+
## Version 1.3.0 - 2023-10-06
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `INSERT.into(...).rows/values()` is not allowed anymore without specifying `.columns(...)`. #209
|
|
12
|
+
- Deep deletion uses correlated subqueries instead of materializing the to be deleted object before. #212
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Various fixes for calculated elements on read. #220 #223 #233
|
|
17
|
+
- Don't release to pool connections twice. #243
|
|
18
|
+
- Syntax error in `matchesPattern` function. #237
|
|
19
|
+
- SELECTs with more than 50 columns does not return `null` values. #238 #261
|
|
20
|
+
|
|
7
21
|
## Version 1.2.1 - 2023-09-08
|
|
8
22
|
|
|
9
23
|
### Fixed
|
package/README.md
CHANGED
|
@@ -2,4 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
Welcome to the new base database service for [SAP Cloud Application Programming Model](https://cap.cloud.sap) Node.js, based on new, streamlined database architecture.
|
|
4
4
|
|
|
5
|
-
Find documentation at https://cap.cloud.sap/docs/guides/databases
|
|
5
|
+
Find documentation at https://cap.cloud.sap/docs/guides/databases
|
|
6
|
+
|
|
7
|
+
## Support
|
|
8
|
+
|
|
9
|
+
This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/cap-js/cds-dbs/issues).
|
|
10
|
+
|
|
11
|
+
## Contribution
|
|
12
|
+
|
|
13
|
+
Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md).
|
|
14
|
+
|
|
15
|
+
## Code of Conduct
|
|
16
|
+
|
|
17
|
+
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its [Code of Conduct](CODE_OF_CONDUCT.md) at all times.
|
|
18
|
+
|
|
19
|
+
## Licensing
|
|
20
|
+
|
|
21
|
+
Copyright 2023 SAP SE or an SAP affiliate company and cds-dbs contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/cap-js/cds-dbs).
|
package/lib/SQLService.js
CHANGED
|
@@ -4,16 +4,12 @@ const { resolveView } = require('@sap/cds/libx/_runtime/common/utils/resolveView
|
|
|
4
4
|
const DatabaseService = require('./common/DatabaseService')
|
|
5
5
|
const cqn4sql = require('./cqn4sql')
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* @callback next
|
|
9
|
-
* @param {Error} param0
|
|
10
|
-
* @returns {Promise<unknown>}
|
|
11
|
-
*/
|
|
7
|
+
/** @typedef {import('@sap/cds/apis/services').Request} Request */
|
|
12
8
|
|
|
13
9
|
/**
|
|
14
10
|
* @callback Handler
|
|
15
|
-
* @param {
|
|
16
|
-
* @param {
|
|
11
|
+
* @param {Request} req
|
|
12
|
+
* @param {(err? : Error) => {Promise<unknown>}} next
|
|
17
13
|
* @returns {Promise<unknown>}
|
|
18
14
|
*/
|
|
19
15
|
|
|
@@ -22,13 +18,14 @@ class SQLService extends DatabaseService {
|
|
|
22
18
|
init() {
|
|
23
19
|
this.on(['SELECT'], this.transformStreamFromCQN)
|
|
24
20
|
this.on(['UPDATE'], this.transformStreamIntoCQN)
|
|
25
|
-
this.on(['INSERT', 'UPSERT', 'UPDATE'
|
|
26
|
-
this.on(['INSERT', 'UPSERT', 'UPDATE'
|
|
21
|
+
this.on(['INSERT', 'UPSERT', 'UPDATE'], require('./fill-in-keys')) // REVISIT should be replaced by correct input processing eventually
|
|
22
|
+
this.on(['INSERT', 'UPSERT', 'UPDATE'], require('./deep-queries').onDeep)
|
|
27
23
|
this.on(['SELECT'], this.onSELECT)
|
|
28
24
|
this.on(['INSERT'], this.onINSERT)
|
|
29
25
|
this.on(['UPSERT'], this.onUPSERT)
|
|
30
26
|
this.on(['UPDATE'], this.onUPDATE)
|
|
31
|
-
this.on(['DELETE'
|
|
27
|
+
this.on(['DELETE'], this.onDELETE)
|
|
28
|
+
this.on(['CREATE ENTITY', 'DROP ENTITY'], this.onSIMPLE)
|
|
32
29
|
this.on(['BEGIN', 'COMMIT', 'ROLLBACK'], this.onEVENT)
|
|
33
30
|
this.on(['STREAM'], this.onSTREAM)
|
|
34
31
|
this.on(['*'], this.onPlainSQL)
|
|
@@ -152,6 +149,39 @@ class SQLService extends DatabaseService {
|
|
|
152
149
|
return (await ps.run(values)).changes
|
|
153
150
|
}
|
|
154
151
|
|
|
152
|
+
get onDELETE() {
|
|
153
|
+
return super.onDELETE = cds.env.features.assert_integrity === 'db' ? this.onSIMPLE : deep_delete
|
|
154
|
+
async function deep_delete(/** @type {Request} */ req) {
|
|
155
|
+
let { compositions } = req.target
|
|
156
|
+
if (compositions) {
|
|
157
|
+
// Transform CQL`DELETE from Foo[p1] WHERE p2` into CQL`DELETE from Foo[p1 and p2]`
|
|
158
|
+
let { from, where } = req.query.DELETE
|
|
159
|
+
if (typeof from === 'string') from = { ref: [from] }
|
|
160
|
+
if (where) {
|
|
161
|
+
let last = from.ref.at(-1)
|
|
162
|
+
if (last.where) [ last, where ] = [ last.id, [ { xpr: last.where }, 'and', { xpr: where } ] ]
|
|
163
|
+
from = {ref:[ ...from.ref.slice(0,-1), { id: last, where }]}
|
|
164
|
+
}
|
|
165
|
+
// Process child compositions depth-first
|
|
166
|
+
let { depth=0, visited=[] } = req
|
|
167
|
+
visited.push (req.target.name)
|
|
168
|
+
await Promise.all (Object.values(compositions).map(c => {
|
|
169
|
+
if (c._target['@cds.persistence.skip'] === true) return
|
|
170
|
+
if (c._target === req.target) { // the Genre.children case
|
|
171
|
+
if (++depth > (c['@depth'] || 3)) return
|
|
172
|
+
} else if (visited.includes(c._target.name)) throw new Error(
|
|
173
|
+
`Transitive circular composition detected: \n\n`+
|
|
174
|
+
` ${visited.join(' > ')} > ${c._target.name} \n\n`+
|
|
175
|
+
`These are not supported by deep delete.`)
|
|
176
|
+
// Prepare and run deep query, à la CQL`DELETE from Foo[pred]:comp1.comp2...`
|
|
177
|
+
const query = DELETE.from({ref:[ ...from.ref, c.name ]})
|
|
178
|
+
return this.onDELETE({ query, depth, visited: [...visited], target: c._target })
|
|
179
|
+
}))
|
|
180
|
+
}
|
|
181
|
+
return this.onSIMPLE(req)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
155
185
|
/**
|
|
156
186
|
* Handler for BEGIN, COMMIT, ROLLBACK, which don't have any CQN
|
|
157
187
|
* @type {Handler}
|
|
@@ -167,11 +197,6 @@ class SQLService extends DatabaseService {
|
|
|
167
197
|
*/
|
|
168
198
|
async onPlainSQL({ query, data }, next) {
|
|
169
199
|
if (typeof query === 'string') {
|
|
170
|
-
// REVISIT: this is a hack the target of $now might not be a timestamp or date time
|
|
171
|
-
// Add input converter to CURRENT_TIMESTAMP inside views using $now
|
|
172
|
-
if(/^CREATE VIEW.* CURRENT_TIMESTAMP[( ]/is.test(query)) {
|
|
173
|
-
query = query.replace(/CURRENT_TIMESTAMP/gi, 'ISO(CURRENT_TIMESTAMP)')
|
|
174
|
-
}
|
|
175
200
|
DEBUG?.(query, data)
|
|
176
201
|
const ps = await this.prepare(query)
|
|
177
202
|
const exec = this.hasResults(query) ? d => ps.all(d) : d => ps.run(d)
|
|
@@ -81,12 +81,11 @@ class DatabaseService extends cds.Service {
|
|
|
81
81
|
*/
|
|
82
82
|
async rollback() {
|
|
83
83
|
if (!this.dbc) return
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
84
|
+
try {
|
|
85
|
+
await this.send('ROLLBACK')
|
|
86
|
+
} finally {
|
|
87
|
+
this.release()
|
|
88
|
+
}
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
/**
|
|
@@ -102,7 +101,9 @@ class DatabaseService extends cds.Service {
|
|
|
102
101
|
* This is for subclasses to intercept, if required.
|
|
103
102
|
*/
|
|
104
103
|
async release() {
|
|
105
|
-
|
|
104
|
+
if (!this.dbc) return
|
|
105
|
+
await this.pool.release(this.dbc)
|
|
106
|
+
this.dbc = undefined
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
// REVISIT: should happen automatically after a configurable time
|
package/lib/cql-functions.js
CHANGED
|
@@ -98,7 +98,7 @@ const StandardFunctions = {
|
|
|
98
98
|
* @param {string} y
|
|
99
99
|
* @returns {string}
|
|
100
100
|
*/
|
|
101
|
-
matchesPattern: (x, y) =>
|
|
101
|
+
matchesPattern: (x, y) => `(${x} regexp ${y})`,
|
|
102
102
|
/**
|
|
103
103
|
* Generates SQL statement that produces the lower case value of a given string
|
|
104
104
|
* @param {string} x
|
|
@@ -341,7 +341,7 @@ const HANAFunctions = {
|
|
|
341
341
|
*/
|
|
342
342
|
years_between(x, y) {
|
|
343
343
|
return `floor(${this.months_between(x, y)} / 12)`
|
|
344
|
-
}
|
|
344
|
+
}
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
for (let each in HANAFunctions) HANAFunctions[each.toUpperCase()] = HANAFunctions[each]
|
package/lib/cqn2sql.js
CHANGED
|
@@ -233,6 +233,7 @@ class CQN2SQLRenderer {
|
|
|
233
233
|
SELECT_expand({ SELECT, elements }, sql) {
|
|
234
234
|
if (!SELECT.columns) return sql
|
|
235
235
|
if (!elements) return sql // REVISIT: Above we say this is an error condition, but here we say it's ok?
|
|
236
|
+
|
|
236
237
|
let cols = SELECT.columns.map(x => {
|
|
237
238
|
const name = this.column_name(x)
|
|
238
239
|
let col = `'${name}',${this.output_converter4(x.element, this.quote(name))}`
|
|
@@ -246,11 +247,19 @@ class CQN2SQLRenderer {
|
|
|
246
247
|
|
|
247
248
|
// Prevent SQLite from hitting function argument limit of 100
|
|
248
249
|
let obj = ''
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
|
|
251
|
+
if(cols.length < 50) obj = `json_object(${cols.slice(0, 50)})`
|
|
252
|
+
else {
|
|
253
|
+
const chunks = []
|
|
254
|
+
for (let i = 0; i < cols.length; i += 50) {
|
|
255
|
+
chunks.push(`json_object(${cols.slice(i, i + 50)})`)
|
|
256
|
+
}
|
|
257
|
+
// REVISIT: json_merge is a user defined function, bad performance!
|
|
258
|
+
obj = `json_merge(${chunks})`
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
return `SELECT ${SELECT.one || SELECT.expand === 'root' ? obj : `json_group_array(${obj.includes('json_merge') ? `json_insert(${obj})` : obj})`} as _json_ FROM (${sql})`
|
|
254
263
|
}
|
|
255
264
|
|
|
256
265
|
/**
|
|
@@ -432,20 +441,18 @@ class CQN2SQLRenderer {
|
|
|
432
441
|
const entity = this.name(q.target?.name || INSERT.into.ref[0])
|
|
433
442
|
const alias = INSERT.into.as
|
|
434
443
|
const elements = q.elements || q.target?.elements
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
438
|
-
let columns = INSERT.columns || (elements && ObjectKeys(elements).filter(c => !elements[c].virtual && !elements[c].isAssociation))
|
|
439
|
-
this.columns = columns.map(c => this.quote(c))
|
|
444
|
+
const columns = INSERT.columns
|
|
445
|
+
|| cds.error`Cannot insert rows without columns or elements`
|
|
440
446
|
|
|
441
|
-
const
|
|
447
|
+
const inputConverter = this.class._convertInput
|
|
442
448
|
const extraction = columns.map((c,i) => {
|
|
443
|
-
const element = elements?.[c] || {}
|
|
444
449
|
const extract = `value->>'$[${i}]'`
|
|
445
|
-
const
|
|
446
|
-
|
|
450
|
+
const element = elements?.[c]
|
|
451
|
+
const converter = element?.[inputConverter]
|
|
452
|
+
return converter?.(extract,element) || extract
|
|
447
453
|
})
|
|
448
454
|
|
|
455
|
+
this.columns = columns.map(c => this.quote(c))
|
|
449
456
|
this.entries = [[JSON.stringify(INSERT.rows)]]
|
|
450
457
|
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${
|
|
451
458
|
this.columns
|
package/lib/cqn4sql.js
CHANGED
|
@@ -310,8 +310,11 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
310
310
|
const col = columns[i]
|
|
311
311
|
|
|
312
312
|
if (isCalculatedOnRead(col.$refLinks?.[col.$refLinks.length - 1].definition)) {
|
|
313
|
-
const
|
|
314
|
-
transformedColumns.
|
|
313
|
+
const name = getName(col)
|
|
314
|
+
if (!transformedColumns.some(inserted => getName(inserted) === name)) {
|
|
315
|
+
const calcElement = resolveCalculatedElement(col)
|
|
316
|
+
transformedColumns.push(calcElement)
|
|
317
|
+
}
|
|
315
318
|
} else if (col.expand) {
|
|
316
319
|
if (col.ref?.length > 1 && col.ref[0] === '$self' && !col.$refLinks[0].definition.kind) {
|
|
317
320
|
const dollarSelfReplacement = calculateDollarSelfColumn(col)
|
|
@@ -433,7 +436,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
433
436
|
|
|
434
437
|
if (col.$refLinks.some(link => link.definition._target?.['@cds.persistence.skip'] === true)) return
|
|
435
438
|
|
|
436
|
-
const getName = col => col.as || col.ref?.at(-1)
|
|
437
439
|
const flatColumns = getFlatColumnsFor(col, { baseName, columnAlias, tableAlias })
|
|
438
440
|
flatColumns.forEach(flatColumn => {
|
|
439
441
|
const name = getName(flatColumn)
|
|
@@ -480,7 +482,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
480
482
|
|
|
481
483
|
function handleEmptyColumns(columns) {
|
|
482
484
|
if (columns.some(c => c.$refLinks?.[c.$refLinks.length - 1].definition.type === 'cds.Composition')) return
|
|
483
|
-
throw new
|
|
485
|
+
throw new Error('Queries must have at least one non-virtual column')
|
|
484
486
|
}
|
|
485
487
|
}
|
|
486
488
|
|
|
@@ -509,7 +511,9 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
509
511
|
res = { xpr: getTransformedTokenStream(value.xpr, baseLink) }
|
|
510
512
|
} else if (val) {
|
|
511
513
|
res = { val }
|
|
512
|
-
} else if (func)
|
|
514
|
+
} else if (func) {
|
|
515
|
+
res = { args: getTransformedTokenStream(value.args, baseLink), func: value.func }
|
|
516
|
+
}
|
|
513
517
|
if (!omitAlias) res.as = column.as || column.name || column.flatName
|
|
514
518
|
return res
|
|
515
519
|
}
|
|
@@ -664,7 +668,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
664
668
|
// everything after the wildcard, is a potential replacement
|
|
665
669
|
// in the wildcard expansion
|
|
666
670
|
const replace = []
|
|
667
|
-
// we need to
|
|
671
|
+
// we need to make the refs absolute
|
|
668
672
|
col[prop].slice(wildcardIndex + 1).forEach(c => {
|
|
669
673
|
const fakeColumn = { ...c }
|
|
670
674
|
if (fakeColumn.ref) {
|
|
@@ -852,7 +856,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
852
856
|
* the result.
|
|
853
857
|
*/
|
|
854
858
|
if (inOrderBy && flatColumns.length > 1)
|
|
855
|
-
|
|
859
|
+
throw new Error(`"${getFullName(leaf)}" can't be used in order by as it expands to multiple fields`)
|
|
856
860
|
if (col.nulls) flatColumns[0].nulls = col.nulls
|
|
857
861
|
if (col.sort) flatColumns[0].sort = col.sort
|
|
858
862
|
res.push(...flatColumns)
|
|
@@ -910,21 +914,23 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
910
914
|
*/
|
|
911
915
|
function getColumnsForWildcard(exclude = [], replace = []) {
|
|
912
916
|
const wildcardColumns = []
|
|
913
|
-
Object.keys(inferred.$combinedElements)
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
917
|
+
Object.keys(inferred.$combinedElements)
|
|
918
|
+
.filter(k => !exclude.includes(k))
|
|
919
|
+
.forEach(k => {
|
|
920
|
+
const { index, tableAlias } = inferred.$combinedElements[k][0]
|
|
921
|
+
const element = tableAlias.elements[k]
|
|
922
|
+
// ignore FK for odata csn / ignore blobs from wildcard expansion
|
|
923
|
+
if (isManagedAssocInFlatMode(element) || (element['@Core.MediaType'] && !element['@Core.IsURL'])) return
|
|
924
|
+
// for wildcard on subquery in from, just reference the elements
|
|
925
|
+
if (tableAlias.SELECT && !element.elements && !element.target) {
|
|
926
|
+
wildcardColumns.push(index ? { ref: [index, k] } : { ref: [k] })
|
|
927
|
+
} else if (isCalculatedOnRead(element)) {
|
|
928
|
+
wildcardColumns.push(resolveCalculatedElement(replace.find(r => r.as === k) || element))
|
|
929
|
+
} else {
|
|
930
|
+
const flatColumns = getFlatColumnsFor(element, { tableAlias: index }, [], { exclude, replace }, true)
|
|
931
|
+
wildcardColumns.push(...flatColumns)
|
|
932
|
+
}
|
|
933
|
+
})
|
|
928
934
|
return wildcardColumns
|
|
929
935
|
|
|
930
936
|
/**
|
|
@@ -1143,7 +1149,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1143
1149
|
const { ref, $refLinks } = tokenStream[i + 1]
|
|
1144
1150
|
if (!ref) continue
|
|
1145
1151
|
if (ref[0] in { $self: true, $projection: true })
|
|
1146
|
-
|
|
1152
|
+
throw new Error(`Unexpected "${ref[0]}" following "exists", remove it or add a table alias instead`)
|
|
1147
1153
|
const firstStepIsTableAlias = ref.length > 1 && ref[0] in inferred.sources
|
|
1148
1154
|
for (let j = 0; j < ref.length; j += 1) {
|
|
1149
1155
|
let current, next
|
|
@@ -1176,6 +1182,22 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1176
1182
|
|
|
1177
1183
|
const as = getNextAvailableTableAlias(getLastStringSegment(next.alias))
|
|
1178
1184
|
next.alias = as
|
|
1185
|
+
if (next.definition.value) {
|
|
1186
|
+
throw new Error(
|
|
1187
|
+
`Calculated elements cannot be used in “exists” predicates in: “exists ${tokenStream[i + 1].ref
|
|
1188
|
+
.map(idOnly)
|
|
1189
|
+
.join('.')}”`,
|
|
1190
|
+
)
|
|
1191
|
+
}
|
|
1192
|
+
if (!next.definition.target) {
|
|
1193
|
+
throw new Error(
|
|
1194
|
+
`Expecting path “${tokenStream[i + 1].ref
|
|
1195
|
+
.map(idOnly)
|
|
1196
|
+
.join('.')}” following “EXISTS” predicate to end with association/composition, found “${
|
|
1197
|
+
next.definition.type
|
|
1198
|
+
}”`,
|
|
1199
|
+
)
|
|
1200
|
+
}
|
|
1179
1201
|
whereExistsSubSelects.push(getWhereExistsSubquery(current, next, step.where, true))
|
|
1180
1202
|
}
|
|
1181
1203
|
|
|
@@ -1244,7 +1266,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1244
1266
|
rhs === 'null'
|
|
1245
1267
|
) {
|
|
1246
1268
|
if (notSupportedOps.some(([firstOp]) => firstOp === next))
|
|
1247
|
-
|
|
1269
|
+
throw new Error(`The operator "${next}" is not supported for structure comparison`)
|
|
1248
1270
|
const newTokens = expandComparison(token, ops, rhs, $baseLink)
|
|
1249
1271
|
const needXpr = Boolean(tokenStream[i - 1] || tokenStream[indexRhs + 1])
|
|
1250
1272
|
transformedTokenStream.push(...(needXpr ? [asXpr(newTokens)] : newTokens))
|
|
@@ -1269,8 +1291,15 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1269
1291
|
transformedTokenStream.push(...getTransformedTokenStream(dollarSelfReplacement))
|
|
1270
1292
|
continue
|
|
1271
1293
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1294
|
+
// if we have e.g. a calculated element like `books.authorLastName,`
|
|
1295
|
+
// we have effectively a ref ['books', 'author', 'lastName']
|
|
1296
|
+
// in that case, we have a baseLink `books` which we need to resolve the following steps
|
|
1297
|
+
// however, the correct table alias has been assigned to the `author` step
|
|
1298
|
+
// hence we need to ignore the alias of the `$baseLink`
|
|
1299
|
+
const refHasOwnAssoc =
|
|
1300
|
+
token.isJoinRelevant && [...token.$refLinks].reverse().find(l => l.definition.isAssociation)
|
|
1301
|
+
const tableAlias = getQuerySourceName(token, refHasOwnAssoc || $baseLink)
|
|
1302
|
+
if ((!$baseLink || refHasOwnAssoc) && token.isJoinRelevant) {
|
|
1274
1303
|
result.ref = [tableAlias, getFullName(token.$refLinks[token.$refLinks.length - 1].definition)]
|
|
1275
1304
|
} else if (tableAlias) {
|
|
1276
1305
|
result.ref = [tableAlias, token.flatName]
|
|
@@ -1327,7 +1356,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1327
1356
|
// --> calculate error message or exit early? See test "proper error if structures cannot be compared / too many elements on lhs"
|
|
1328
1357
|
if (flatRhs.length !== flatLhs.length)
|
|
1329
1358
|
// make sure we can compare both structures
|
|
1330
|
-
|
|
1359
|
+
throw new Error(
|
|
1331
1360
|
`Can't compare "${definition.name}" with "${
|
|
1332
1361
|
value.$refLinks[value.$refLinks.length - 1].definition.name
|
|
1333
1362
|
}": the operands must have the same structure`,
|
|
@@ -1355,13 +1384,13 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1355
1384
|
// if we still have elements in flatRhs -> those were not found in lhs
|
|
1356
1385
|
const lhsPath = token.ref.join('.') // original path of the comparison, used in error message
|
|
1357
1386
|
flatRhs.forEach(t => pathNotFoundErr.push(`Path "${t._csnPath.slice(1).join('.')}" not found in "${lhsPath}"`))
|
|
1358
|
-
|
|
1387
|
+
throw new Error(`Can't compare "${lhsPath}" with "${rhsPath}": ${pathNotFoundErr.join(', ')}`)
|
|
1359
1388
|
}
|
|
1360
1389
|
} else {
|
|
1361
1390
|
// compare with value
|
|
1362
1391
|
const flatLhs = flattenWithBaseName(token)
|
|
1363
1392
|
if (flatLhs.length > 1 && value.val !== null && value !== 'null')
|
|
1364
|
-
|
|
1393
|
+
throw new Error(`Can't compare structure "${token.ref.join('.')}" with value "${value.val}"`)
|
|
1365
1394
|
const boolOp = notEqOps.some(([f, s]) => operator[0] === f && operator[1] === s) ? 'or' : 'and'
|
|
1366
1395
|
flatLhs.forEach((column, i) => {
|
|
1367
1396
|
result.push(column, ...operator, value)
|
|
@@ -1397,10 +1426,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1397
1426
|
rejectStructInExpression()
|
|
1398
1427
|
|
|
1399
1428
|
function rejectAssocInExpression() {
|
|
1400
|
-
throw new Error(
|
|
1429
|
+
throw new Error("An association can't be used as a value in an expression")
|
|
1401
1430
|
}
|
|
1402
1431
|
function rejectStructInExpression() {
|
|
1403
|
-
throw new Error(
|
|
1432
|
+
throw new Error("A structured element can't be used as a value in an expression")
|
|
1404
1433
|
}
|
|
1405
1434
|
}
|
|
1406
1435
|
|
|
@@ -1946,7 +1975,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1946
1975
|
if ($baseLink) {
|
|
1947
1976
|
return getBaseLinkAlias($baseLink)
|
|
1948
1977
|
}
|
|
1949
|
-
|
|
1950
1978
|
if (node.isJoinRelevant) {
|
|
1951
1979
|
return getJoinRelevantAlias(node)
|
|
1952
1980
|
}
|
|
@@ -2048,6 +2076,6 @@ function setElementOnColumns(col, element) {
|
|
|
2048
2076
|
writable: true,
|
|
2049
2077
|
})
|
|
2050
2078
|
}
|
|
2051
|
-
|
|
2079
|
+
const getName = col => col.as || col.ref?.at(-1)
|
|
2052
2080
|
const idOnly = ref => ref.id || ref
|
|
2053
2081
|
const is_regexp = x => x?.constructor?.name === 'RegExp' // NOTE: x instanceof RegExp doesn't work in repl
|
package/lib/deep-queries.js
CHANGED
|
@@ -17,38 +17,29 @@ const handledDeep = Symbol('handledDeep')
|
|
|
17
17
|
*/
|
|
18
18
|
async function onDeep(req, next) {
|
|
19
19
|
const { query } = req
|
|
20
|
+
if (handledDeep in query) return next()
|
|
21
|
+
|
|
20
22
|
// REVISIT: req.target does not match the query.INSERT target for path insert
|
|
21
23
|
// const target = query.sources[Object.keys(query.sources)[0]]
|
|
22
|
-
if (!this.model?.definitions[_target_name4(req.query)])
|
|
23
|
-
|
|
24
|
-
}
|
|
24
|
+
if (!this.model?.definitions[_target_name4(req.query)]) return next()
|
|
25
|
+
|
|
25
26
|
const { target } = this.infer(query)
|
|
26
27
|
if (!hasDeep(query, target)) return next()
|
|
27
|
-
const beforeData = query.INSERT ? [] : await this.run(getExpandForDeep(query, target, true))
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
29
|
+
const beforeData = query.INSERT ? [] : await this.run(getExpandForDeep(query, target, true))
|
|
30
|
+
if (query.UPDATE && !beforeData.length) return 0
|
|
32
31
|
|
|
33
32
|
const queries = getDeepQueries(query, beforeData, target)
|
|
34
|
-
const res = await Promise.all(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}),
|
|
40
|
-
)
|
|
33
|
+
const res = await Promise.all(queries.map(query => {
|
|
34
|
+
if (query.INSERT) return this.onINSERT({ query })
|
|
35
|
+
if (query.UPDATE) return this.onUPDATE({ query })
|
|
36
|
+
if (query.DELETE) return this.onSIMPLE({ query })
|
|
37
|
+
}))
|
|
41
38
|
return res[0] ?? 0 // TODO what todo with multiple result responses?
|
|
42
39
|
}
|
|
43
40
|
|
|
44
|
-
const hasDeep = (
|
|
45
|
-
|
|
46
|
-
if (query.DELETE) {
|
|
47
|
-
for (let c in target?.compositions) return true
|
|
48
|
-
return false
|
|
49
|
-
}
|
|
50
|
-
const data =
|
|
51
|
-
query.INSERT?.entries || (query.UPDATE?.data && [query.UPDATE.data]) || (query.UPDATE?.with && [query.UPDATE.with])
|
|
41
|
+
const hasDeep = (q, target) => {
|
|
42
|
+
const data = q.INSERT?.entries || (q.UPDATE?.data && [q.UPDATE.data]) || (q.UPDATE?.with && [q.UPDATE.with])
|
|
52
43
|
if (data)
|
|
53
44
|
for (const c in target.compositions) {
|
|
54
45
|
for (const row of data) if (row[c] !== undefined) return true
|
|
@@ -110,7 +101,7 @@ const _calculateExpandColumns = (target, data, expandColumns = [], elementMap =
|
|
|
110
101
|
const seen = elementMap.get(fqn)
|
|
111
102
|
if (seen && seen >= DEEP_DELETE_MAX_RECURSION_DEPTH) {
|
|
112
103
|
// recursion -> abort
|
|
113
|
-
return
|
|
104
|
+
return expandColumns
|
|
114
105
|
}
|
|
115
106
|
|
|
116
107
|
let expandColumn = expandColumns.find(expandColumn => expandColumn.ref[0] === composition.name)
|
|
@@ -145,6 +136,7 @@ const _calculateExpandColumns = (target, data, expandColumns = [], elementMap =
|
|
|
145
136
|
_calculateExpandColumns(composition._target, compositionData, expandColumn.expand, newElementMap)
|
|
146
137
|
}
|
|
147
138
|
}
|
|
139
|
+
return expandColumns
|
|
148
140
|
}
|
|
149
141
|
|
|
150
142
|
/**
|
|
@@ -152,18 +144,9 @@ const _calculateExpandColumns = (target, data, expandColumns = [], elementMap =
|
|
|
152
144
|
* @param {import('@sap/cds/apis/csn').Definition} target
|
|
153
145
|
*/
|
|
154
146
|
const getExpandForDeep = (query, target) => {
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
/** @type {import("@sap/cds/apis/ql").SELECT<unknown>} */
|
|
160
|
-
const cqn = SELECT.from(from)
|
|
161
|
-
if (where) cqn.SELECT.where = where
|
|
162
|
-
|
|
163
|
-
const columns = []
|
|
164
|
-
_calculateExpandColumns(target, data, columns)
|
|
165
|
-
cqn.columns(columns)
|
|
166
|
-
return cqn
|
|
147
|
+
const { entity, data = null, where } = query.UPDATE
|
|
148
|
+
const columns = _calculateExpandColumns(target, data)
|
|
149
|
+
return SELECT(columns).from(entity).where(where)
|
|
167
150
|
}
|
|
168
151
|
|
|
169
152
|
/**
|
package/lib/fill-in-keys.js
CHANGED
|
@@ -62,7 +62,7 @@ module.exports = async function fill_in_keys(req, next) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// REVISIT no input processing for INPUT with rows/values
|
|
65
|
-
if (
|
|
65
|
+
if (!(req.query.INSERT?.rows || req.query.INSERT?.values)) {
|
|
66
66
|
if (Array.isArray(req.data)) {
|
|
67
67
|
for (const d of req.data) {
|
|
68
68
|
generateUUIDandPropagateKeys(req.target, d, req.event)
|
package/lib/infer/index.js
CHANGED
|
@@ -23,12 +23,12 @@ for (const each in cdsTypes) cdsTypes[`cds.${each}`] = cdsTypes[each]
|
|
|
23
23
|
* @returns {import('./cqn').Query} = q with .target and .elements
|
|
24
24
|
*/
|
|
25
25
|
function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
26
|
-
if (!model)
|
|
26
|
+
if (!model) throw new Error('Please specify a model')
|
|
27
27
|
const inferred = typeof originalQuery === 'string' ? cds.parse.cql(originalQuery) : cds.ql.clone(originalQuery)
|
|
28
28
|
|
|
29
29
|
// REVISIT: The more edge use cases we support, thes less optimized are we for the 90+% use cases
|
|
30
30
|
// e.g. there's a lot of overhead for infer( SELECT.from(Books) )
|
|
31
|
-
if (originalQuery.SET)
|
|
31
|
+
if (originalQuery.SET) throw new Error('”UNION” based queries are not supported')
|
|
32
32
|
const _ =
|
|
33
33
|
inferred.SELECT ||
|
|
34
34
|
inferred.INSERT ||
|
|
@@ -97,16 +97,16 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
97
97
|
if (ref) {
|
|
98
98
|
const first = ref[0].id || ref[0]
|
|
99
99
|
let target = getDefinition(first, model)
|
|
100
|
-
if (!target)
|
|
100
|
+
if (!target) throw new Error(`"${first}" not found in the definitions of your model`)
|
|
101
101
|
if (ref.length > 1) {
|
|
102
102
|
target = from.ref.slice(1).reduce((d, r) => {
|
|
103
103
|
const next = d.elements[r.id || r]?.elements ? d.elements[r.id || r] : d.elements[r.id || r]?._target
|
|
104
|
-
if (!next)
|
|
104
|
+
if (!next) throw new Error(`No association “${r.id || r}” in ${d.kind} “${d.name}”`)
|
|
105
105
|
return next
|
|
106
106
|
}, target)
|
|
107
107
|
}
|
|
108
108
|
if (target.kind !== 'entity' && !target._isAssociation)
|
|
109
|
-
throw new Error(
|
|
109
|
+
throw new Error('Query source must be a an entity or an association')
|
|
110
110
|
|
|
111
111
|
attachRefLinksToArg(from) // REVISIT: remove
|
|
112
112
|
const alias =
|
|
@@ -154,8 +154,9 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
154
154
|
* @returns {void} This function does not return a value; it mutates the 'arg' object directly.
|
|
155
155
|
*/
|
|
156
156
|
function attachRefLinksToArg(arg, $baseLink = null, expandOrExists = false) {
|
|
157
|
-
const { ref, xpr } = arg
|
|
157
|
+
const { ref, xpr, args } = arg
|
|
158
158
|
if (xpr) xpr.forEach(t => attachRefLinksToArg(t, $baseLink, expandOrExists))
|
|
159
|
+
if (args) args.forEach(arg => attachRefLinksToArg(arg, $baseLink, expandOrExists))
|
|
159
160
|
if (!ref) return
|
|
160
161
|
init$refLinks(arg)
|
|
161
162
|
ref.forEach((step, i) => {
|
|
@@ -172,9 +173,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
172
173
|
const nextStep = ref[1]?.id || ref[1]
|
|
173
174
|
// no unmanaged assoc in infix filter path
|
|
174
175
|
if (!expandOrExists && e.on)
|
|
175
|
-
throw new Error(
|
|
176
|
-
`"${e.name}" in path "${arg.ref.map(idOnly).join('.')}" must not be an unmanaged association`,
|
|
177
|
-
)
|
|
176
|
+
throw new Error(`"${e.name}" in path "${arg.ref.map(idOnly).join('.')}" must not be an unmanaged association`)
|
|
178
177
|
// no non-fk traversal in infix filter
|
|
179
178
|
if (!expandOrExists && nextStep && !(nextStep in e.foreignKeys))
|
|
180
179
|
throw new Error(`Only foreign keys of "${e.name}" can be accessed in infix filter`)
|
|
@@ -288,8 +287,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
288
287
|
wildcardSelect = true
|
|
289
288
|
} else if (col.val !== undefined || col.xpr || col.SELECT || col.func || col.param) {
|
|
290
289
|
const as = col.as || col.func || col.val
|
|
291
|
-
if (as === undefined)
|
|
292
|
-
if (queryElements[as])
|
|
290
|
+
if (as === undefined) cds.error`Expecting expression to have an alias name`
|
|
291
|
+
if (queryElements[as]) cds.error`Duplicate definition of element “${as}”`
|
|
293
292
|
if (col.xpr || col.SELECT) {
|
|
294
293
|
queryElements[as] = getElementForXprOrSubquery(col)
|
|
295
294
|
} else if (col.func) {
|
|
@@ -313,7 +312,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
313
312
|
} else if (col.expand) {
|
|
314
313
|
inferQueryElement(col)
|
|
315
314
|
} else {
|
|
316
|
-
|
|
315
|
+
cds.error`Not supported: ${JSON.stringify(col)}`
|
|
317
316
|
}
|
|
318
317
|
})
|
|
319
318
|
|
|
@@ -338,34 +337,43 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
338
337
|
inferQueryElement(token, false, $baseLink)
|
|
339
338
|
})
|
|
340
339
|
}
|
|
341
|
-
|
|
340
|
+
|
|
341
|
+
// walk over all paths in other query properties
|
|
342
|
+
if (where)
|
|
343
|
+
walkTokenStream(where)
|
|
344
|
+
if (groupBy)
|
|
345
|
+
groupBy.forEach(token => inferQueryElement(token, false))
|
|
346
|
+
if (having)
|
|
347
|
+
walkTokenStream(having)
|
|
348
|
+
if (_.with) // consider UPDATE.with
|
|
349
|
+
Object.values(_.with).forEach(val => inferQueryElement(val, false))
|
|
350
|
+
|
|
351
|
+
return queryElements
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Recursively drill down into a tokenStream (`where` or `having`) and pass
|
|
355
|
+
* on the information whether the next token is resolved within an `exists` predicates.
|
|
356
|
+
* If such a token has an infix filter, it is not join relevant, because the filter
|
|
357
|
+
* condition is applied to the generated `exists <subquery>` condition.
|
|
358
|
+
*
|
|
359
|
+
* @param {array} tokenStream
|
|
360
|
+
*/
|
|
361
|
+
function walkTokenStream(tokenStream) {
|
|
342
362
|
let skipJoins
|
|
343
|
-
const
|
|
344
|
-
if (
|
|
363
|
+
const processToken = t => {
|
|
364
|
+
if (t === 'exists') {
|
|
345
365
|
// no joins for infix filters along `exists <path>`
|
|
346
366
|
skipJoins = true
|
|
347
|
-
} else if (
|
|
367
|
+
} else if (t.xpr) {
|
|
348
368
|
// don't miss an exists within an expression
|
|
349
|
-
|
|
369
|
+
t.xpr.forEach(processToken)
|
|
350
370
|
} else {
|
|
351
|
-
inferQueryElement(
|
|
371
|
+
inferQueryElement(t, false, null, { inExists: skipJoins, inExpr: true })
|
|
352
372
|
skipJoins = false
|
|
353
373
|
}
|
|
354
374
|
}
|
|
355
|
-
|
|
375
|
+
tokenStream.forEach(processToken)
|
|
356
376
|
}
|
|
357
|
-
if (groupBy)
|
|
358
|
-
// link $refLinks
|
|
359
|
-
groupBy.forEach(token => inferQueryElement(token, false))
|
|
360
|
-
if (having)
|
|
361
|
-
// link $refLinks
|
|
362
|
-
having.forEach(token => inferQueryElement(token, false))
|
|
363
|
-
if (_.with)
|
|
364
|
-
// consider UPDATE.with
|
|
365
|
-
Object.values(_.with).forEach(val => inferQueryElement(val, false))
|
|
366
|
-
|
|
367
|
-
return queryElements
|
|
368
|
-
|
|
369
377
|
/**
|
|
370
378
|
* Processes references starting with `$self`, which are intended to target other query elements.
|
|
371
379
|
* These `$self` paths must be handled after processing the "regular" columns since they are dependent on other query elements.
|
|
@@ -504,7 +512,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
504
512
|
.join('.')}" must not be an unmanaged association`,
|
|
505
513
|
)
|
|
506
514
|
// no non-fk traversal in infix filter
|
|
507
|
-
if (nextStep && !(nextStep in element.foreignKeys))
|
|
515
|
+
if (nextStep && element.foreignKeys && !(nextStep in element.foreignKeys))
|
|
508
516
|
throw new Error(`Only foreign keys of "${element.name}" can be accessed in infix filter`)
|
|
509
517
|
}
|
|
510
518
|
const resolvableIn = definition.target ? definition._target : target
|
|
@@ -536,10 +544,10 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
536
544
|
const element = elements?.[id]
|
|
537
545
|
|
|
538
546
|
if (firstStepIsSelf && element?.isAssociation) {
|
|
539
|
-
throw
|
|
547
|
+
throw new Error(
|
|
540
548
|
`Paths starting with “$self” must not contain steps of type “cds.Association”: ref: [ ${column.ref.map(
|
|
541
549
|
idOnly,
|
|
542
|
-
)} ]`,
|
|
550
|
+
).join(', ')} ]`,
|
|
543
551
|
)
|
|
544
552
|
}
|
|
545
553
|
|
|
@@ -579,7 +587,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
579
587
|
if (step.where) {
|
|
580
588
|
const danglingFilter = !(column.ref[i + 1] || column.expand || column.inline || inExists)
|
|
581
589
|
if (!column.$refLinks[i].definition.target || danglingFilter)
|
|
582
|
-
throw new Error(
|
|
590
|
+
throw new Error('A filter can only be provided when navigating along associations')
|
|
583
591
|
if (!column.expand) Object.defineProperty(column, 'isJoinRelevant', { value: true })
|
|
584
592
|
// books[exists genre[code='A']].title --> column is join relevant but inner exists filter is not
|
|
585
593
|
let skipJoinsForFilter = inExists
|
|
@@ -664,7 +672,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
664
672
|
: column
|
|
665
673
|
if (isColumnJoinRelevant(colWithBase)) {
|
|
666
674
|
if (originalQuery.UPDATE)
|
|
667
|
-
throw
|
|
675
|
+
throw new Error(
|
|
668
676
|
'Path expressions for UPDATE statements are not supported. Use “where exists” with infix filters instead.',
|
|
669
677
|
)
|
|
670
678
|
Object.defineProperty(column, 'isJoinRelevant', { value: true })
|
|
@@ -672,7 +680,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
672
680
|
}
|
|
673
681
|
}
|
|
674
682
|
if (leafArt.value && !leafArt.value.stored) {
|
|
675
|
-
|
|
683
|
+
linkCalculatedElement(column, $baseLink, baseColumn)
|
|
676
684
|
}
|
|
677
685
|
|
|
678
686
|
/**
|
|
@@ -752,6 +760,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
752
760
|
columns: expand.filter(c => !c.inline),
|
|
753
761
|
},
|
|
754
762
|
}
|
|
763
|
+
if (col.excluding) expandSubquery.SELECT.excluding = col.excluding
|
|
755
764
|
if (col.as) expandSubquery.SELECT.as = col.as
|
|
756
765
|
const inferredExpandSubquery = infer(expandSubquery, model)
|
|
757
766
|
const res = $leafLink.definition.is2one
|
|
@@ -798,10 +807,10 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
798
807
|
// if the `elt` from a `$self.elt` path is found in the `$combinedElements` -> hint to remove `$self`
|
|
799
808
|
if (step in $combinedElements)
|
|
800
809
|
err.push(` did you mean ${$combinedElements[step].map(ta => `"${ta.index || ta.as}.${step}"`).join(',')}?`)
|
|
801
|
-
throw new Error(err)
|
|
810
|
+
throw new Error(err.join(','))
|
|
802
811
|
}
|
|
803
812
|
}
|
|
804
|
-
function
|
|
813
|
+
function linkCalculatedElement(column, baseLink, baseColumn) {
|
|
805
814
|
const calcElement = column.$refLinks?.[column.$refLinks.length - 1].definition || column
|
|
806
815
|
if (alreadySeenCalcElements.has(calcElement)) return
|
|
807
816
|
else alreadySeenCalcElements.add(calcElement)
|
|
@@ -809,35 +818,54 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
809
818
|
if (ref || xpr) {
|
|
810
819
|
baseLink = baseLink || { definition: calcElement.parent, target: calcElement.parent }
|
|
811
820
|
attachRefLinksToArg(calcElement.value, baseLink, true)
|
|
812
|
-
const basePath =
|
|
821
|
+
const basePath =
|
|
822
|
+
column.$refLinks?.length > 1
|
|
823
|
+
? { $refLinks: column.$refLinks.slice(0, -1), ref: column.ref.slice(0, -1) }
|
|
824
|
+
: { $refLinks: [], ref: [] }
|
|
813
825
|
if (baseColumn) {
|
|
814
826
|
basePath.$refLinks.push(...baseColumn.$refLinks)
|
|
815
827
|
basePath.ref.push(...baseColumn.ref)
|
|
816
828
|
}
|
|
817
|
-
// column is now fully linked, now we need to find out if we need to merge it into the join tree
|
|
818
|
-
// for that, we calculate all paths from a calc element and merge them into the join tree
|
|
819
829
|
mergePathsIntoJoinTree(calcElement.value, basePath)
|
|
820
830
|
}
|
|
821
831
|
if (func)
|
|
822
|
-
calcElement.value.args?.forEach(arg =>
|
|
823
|
-
inferQueryElement(
|
|
824
|
-
|
|
825
|
-
|
|
832
|
+
calcElement.value.args?.forEach(arg => {
|
|
833
|
+
inferQueryElement(
|
|
834
|
+
arg,
|
|
835
|
+
false,
|
|
836
|
+
{ definition: calcElement.parent, target: calcElement.parent },
|
|
837
|
+
{ inCalcElement: true },
|
|
838
|
+
)
|
|
839
|
+
const basePath =
|
|
840
|
+
column.$refLinks?.length > 1
|
|
841
|
+
? { $refLinks: column.$refLinks.slice(0, -1), ref: column.ref.slice(0, -1) }
|
|
842
|
+
: { $refLinks: [], ref: [] }
|
|
843
|
+
mergePathsIntoJoinTree(arg, basePath)
|
|
844
|
+
}) // {func}.args are optional
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Calculates all paths from a given ref and merges them into the join tree.
|
|
848
|
+
* Recursively walks into refs of calculated elements.
|
|
849
|
+
*
|
|
850
|
+
* @param {object} arg with a ref and sibling $refLinks
|
|
851
|
+
* @param {object} basePath with a ref and sibling $refLinks, used for recursion
|
|
852
|
+
*/
|
|
853
|
+
function mergePathsIntoJoinTree(arg, basePath = null) {
|
|
826
854
|
basePath = basePath || { $refLinks: [], ref: [] }
|
|
827
|
-
if (
|
|
828
|
-
|
|
855
|
+
if (arg.ref) {
|
|
856
|
+
arg.$refLinks.forEach((link, i) => {
|
|
829
857
|
const { definition } = link
|
|
830
858
|
if (!definition.value) {
|
|
831
859
|
basePath.$refLinks.push(link)
|
|
832
|
-
basePath.ref.push(
|
|
860
|
+
basePath.ref.push(arg.ref[i])
|
|
833
861
|
}
|
|
834
862
|
})
|
|
835
|
-
const leafOfCalculatedElementRef =
|
|
863
|
+
const leafOfCalculatedElementRef = arg.$refLinks[arg.$refLinks.length - 1].definition
|
|
836
864
|
if (leafOfCalculatedElementRef.value) mergePathsIntoJoinTree(leafOfCalculatedElementRef.value, basePath)
|
|
837
865
|
|
|
838
|
-
mergePathIfNecessary(basePath,
|
|
839
|
-
} else if (
|
|
840
|
-
|
|
866
|
+
mergePathIfNecessary(basePath, arg)
|
|
867
|
+
} else if (arg.xpr) {
|
|
868
|
+
arg.xpr.forEach(step => {
|
|
841
869
|
if (step.ref) {
|
|
842
870
|
const subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
|
|
843
871
|
step.$refLinks.forEach((link, i) => {
|
|
@@ -923,14 +951,17 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
923
951
|
const exclude = _.excluding ? x => _.excluding.includes(x) : () => false
|
|
924
952
|
|
|
925
953
|
if (Object.keys(queryElements).length === 0 && aliases.length === 1) {
|
|
954
|
+
const { elements } = sources[aliases[0]]
|
|
926
955
|
// only one query source and no overwritten columns
|
|
927
|
-
Object.
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
956
|
+
Object.keys(elements)
|
|
957
|
+
.filter(k => !exclude(k))
|
|
958
|
+
.forEach(k => {
|
|
959
|
+
const element = sources[aliases[0]].elements[k]
|
|
960
|
+
if (element.type !== 'cds.LargeBinary') queryElements[k] = element
|
|
961
|
+
if (element.value) {
|
|
962
|
+
linkCalculatedElement(element)
|
|
963
|
+
}
|
|
964
|
+
})
|
|
934
965
|
return
|
|
935
966
|
}
|
|
936
967
|
|
|
@@ -943,6 +974,9 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
943
974
|
if (exclude(name) || name in queryElements) return true
|
|
944
975
|
const element = tableAliases[0].tableAlias.elements[name]
|
|
945
976
|
if (element.type !== 'cds.LargeBinary') queryElements[name] = element
|
|
977
|
+
if (element.value) {
|
|
978
|
+
linkCalculatedElement(element)
|
|
979
|
+
}
|
|
946
980
|
})
|
|
947
981
|
|
|
948
982
|
if (Object.keys(ambiguousElements).length > 0) throwAmbiguousWildcardError()
|
package/lib/infer/join-tree.js
CHANGED
|
@@ -181,7 +181,10 @@ class JoinTree {
|
|
|
181
181
|
if (next) {
|
|
182
182
|
// step already seen before
|
|
183
183
|
node = next
|
|
184
|
-
|
|
184
|
+
// re-set $refLink to equal the one which got already merged
|
|
185
|
+
col.$refLinks[i].alias = node.$refLink.alias
|
|
186
|
+
col.$refLinks[i].definition = node.$refLink.definition
|
|
187
|
+
col.$refLinks[i].target = node.$refLink.target
|
|
185
188
|
} else {
|
|
186
189
|
if (col.expand && !col.ref[i + 1]) {
|
|
187
190
|
node.$refLink.onlyForeignKeyAccess = false
|
package/package.json
CHANGED