@cap-js/db-service 1.2.0 → 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 CHANGED
@@ -4,6 +4,26 @@
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
+
21
+ ## Version 1.2.1 - 2023-09-08
22
+
23
+ ### Fixed
24
+
25
+ - Association expansion in infix filters. #213
26
+
7
27
  ## Version 1.2.0 - 2023-09-06
8
28
 
9
29
  ### Added
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 {import('@sap/cds/apis/services').Request} param0
16
- * @param {next} param1
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', 'DELETE'], require('./fill-in-keys')) // REVISIT should be replaced by correct input processing eventually
26
- this.on(['INSERT', 'UPSERT', 'UPDATE', 'DELETE'], require('./deep-queries').onDeep)
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', 'CREATE ENTITY', 'DROP ENTITY'], this.onSIMPLE)
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
- else
85
- try {
86
- await this.send('ROLLBACK')
87
- } finally {
88
- this.release()
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
- return this.pool.release(this.dbc)
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
@@ -98,7 +98,7 @@ const StandardFunctions = {
98
98
  * @param {string} y
99
99
  * @returns {string}
100
100
  */
101
- matchesPattern: (x, y) => `${x} regexp ${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,23 +233,33 @@ 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
- let col = `'$."${name}"',${this.output_converter4(x.element, this.quote(name))}`
239
+ let col = `'${name}',${this.output_converter4(x.element, this.quote(name))}`
239
240
  if (x.SELECT?.count) {
240
241
  // Return both the sub select and the count for @odata.count
241
242
  const qc = cds.ql.clone(x, { columns: [{ func: 'count' }], one: 1, limit: 0, orderBy: 0 })
242
- col += `, '$."${name}@odata.count"',${this.expr(qc)}`
243
+ return [col, `'${name}@odata.count',${this.expr(qc)}`]
243
244
  }
244
245
  return col
245
- })
246
+ }).flat()
246
247
 
247
248
  // Prevent SQLite from hitting function argument limit of 100
248
- let obj = "'{}'"
249
- for (let i = 0; i < cols.length; i += 48) {
250
- obj = `json_insert(${obj},${cols.slice(i, i + 48)})`
251
- }
252
- return `SELECT ${SELECT.one || SELECT.expand === 'root' ? obj : `json_group_array(${obj})`} as _json_ FROM (${sql})`
249
+ let obj = ''
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})`
253
263
  }
254
264
 
255
265
  /**
@@ -431,20 +441,18 @@ class CQN2SQLRenderer {
431
441
  const entity = this.name(q.target?.name || INSERT.into.ref[0])
432
442
  const alias = INSERT.into.as
433
443
  const elements = q.elements || q.target?.elements
434
- if (!INSERT.columns && !elements) {
435
- throw cds.error`Cannot insert rows without columns or elements`
436
- }
437
- let columns = INSERT.columns || (elements && ObjectKeys(elements).filter(c => !elements[c].virtual && !elements[c].isAssociation))
438
- this.columns = columns.map(c => this.quote(c))
444
+ const columns = INSERT.columns
445
+ || cds.error`Cannot insert rows without columns or elements`
439
446
 
440
- const inputConverterKey = this.class._convertInput
447
+ const inputConverter = this.class._convertInput
441
448
  const extraction = columns.map((c,i) => {
442
- const element = elements?.[c] || {}
443
449
  const extract = `value->>'$[${i}]'`
444
- const converter = element[inputConverterKey] || (e => e)
445
- return converter(extract, element)
450
+ const element = elements?.[c]
451
+ const converter = element?.[inputConverter]
452
+ return converter?.(extract,element) || extract
446
453
  })
447
454
 
455
+ this.columns = columns.map(c => this.quote(c))
448
456
  this.entries = [[JSON.stringify(INSERT.rows)]]
449
457
  return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${
450
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 calcElement = resolveCalculatedElement(col)
314
- transformedColumns.push(calcElement)
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 cds.error('Queries must have at least one non-virtual column')
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) res = { args: getTransformedTokenStream(value.args, baseLink), func: value.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
  }
@@ -548,7 +552,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
548
552
  dollarSelfColumn.ref = [...referencedColumn.ref, ...dollarSelfColumn.ref.slice(2)]
549
553
  Object.defineProperties(dollarSelfColumn, {
550
554
  flatName: {
551
- value: referencedColumn.$refLinks[0].definition.kind === 'entity' ? dollarSelfColumn.ref.slice(1).join('_') : dollarSelfColumn.ref.join('_'),
555
+ value:
556
+ referencedColumn.$refLinks[0].definition.kind === 'entity'
557
+ ? dollarSelfColumn.ref.slice(1).join('_')
558
+ : dollarSelfColumn.ref.join('_'),
552
559
  },
553
560
  isJoinRelevant: {
554
561
  value: referencedColumn.isJoinRelevant,
@@ -661,7 +668,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
661
668
  // everything after the wildcard, is a potential replacement
662
669
  // in the wildcard expansion
663
670
  const replace = []
664
- // we need to absolutefy the refs
671
+ // we need to make the refs absolute
665
672
  col[prop].slice(wildcardIndex + 1).forEach(c => {
666
673
  const fakeColumn = { ...c }
667
674
  if (fakeColumn.ref) {
@@ -849,7 +856,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
849
856
  * the result.
850
857
  */
851
858
  if (inOrderBy && flatColumns.length > 1)
852
- cds.error(`"${getFullName(leaf)}" can't be used in order by as it expands to multiple fields`)
859
+ throw new Error(`"${getFullName(leaf)}" can't be used in order by as it expands to multiple fields`)
853
860
  if (col.nulls) flatColumns[0].nulls = col.nulls
854
861
  if (col.sort) flatColumns[0].sort = col.sort
855
862
  res.push(...flatColumns)
@@ -907,21 +914,23 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
907
914
  */
908
915
  function getColumnsForWildcard(exclude = [], replace = []) {
909
916
  const wildcardColumns = []
910
- Object.keys(inferred.$combinedElements).forEach(k => {
911
- const { index, tableAlias } = inferred.$combinedElements[k][0]
912
- const element = tableAlias.elements[k]
913
- // ignore FK for odata csn / ignore blobs from wildcard expansion
914
- if (isManagedAssocInFlatMode(element) || (element['@Core.MediaType'] && !element['@Core.IsURL'])) return
915
- // for wildcard on subquery in from, just reference the elements
916
- if (tableAlias.SELECT && !element.elements && !element.target) {
917
- wildcardColumns.push(index ? { ref: [index, k] } : { ref: [k] })
918
- } else if (isCalculatedOnRead(element)) {
919
- wildcardColumns.push(resolveCalculatedElement(element))
920
- } else {
921
- const flatColumns = getFlatColumnsFor(element, { tableAlias: index }, [], { exclude, replace }, true)
922
- wildcardColumns.push(...flatColumns)
923
- }
924
- })
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
+ })
925
934
  return wildcardColumns
926
935
 
927
936
  /**
@@ -1140,7 +1149,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1140
1149
  const { ref, $refLinks } = tokenStream[i + 1]
1141
1150
  if (!ref) continue
1142
1151
  if (ref[0] in { $self: true, $projection: true })
1143
- cds.error(`Unexpected "${ref[0]}" following "exists", remove it or add a table alias instead`)
1152
+ throw new Error(`Unexpected "${ref[0]}" following "exists", remove it or add a table alias instead`)
1144
1153
  const firstStepIsTableAlias = ref.length > 1 && ref[0] in inferred.sources
1145
1154
  for (let j = 0; j < ref.length; j += 1) {
1146
1155
  let current, next
@@ -1173,6 +1182,22 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1173
1182
 
1174
1183
  const as = getNextAvailableTableAlias(getLastStringSegment(next.alias))
1175
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
+ }
1176
1201
  whereExistsSubSelects.push(getWhereExistsSubquery(current, next, step.where, true))
1177
1202
  }
1178
1203
 
@@ -1241,8 +1266,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1241
1266
  rhs === 'null'
1242
1267
  ) {
1243
1268
  if (notSupportedOps.some(([firstOp]) => firstOp === next))
1244
- cds.error(`The operator "${next}" is not supported for structure comparison`)
1245
- const newTokens = expandComparison(token, ops, rhs)
1269
+ throw new Error(`The operator "${next}" is not supported for structure comparison`)
1270
+ const newTokens = expandComparison(token, ops, rhs, $baseLink)
1246
1271
  const needXpr = Boolean(tokenStream[i - 1] || tokenStream[indexRhs + 1])
1247
1272
  transformedTokenStream.push(...(needXpr ? [asXpr(newTokens)] : newTokens))
1248
1273
  i = indexRhs // jump to next relevant index
@@ -1266,8 +1291,15 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1266
1291
  transformedTokenStream.push(...getTransformedTokenStream(dollarSelfReplacement))
1267
1292
  continue
1268
1293
  }
1269
- const tableAlias = getQuerySourceName(token, $baseLink)
1270
- if (!$baseLink && token.isJoinRelevant) {
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) {
1271
1303
  result.ref = [tableAlias, getFullName(token.$refLinks[token.$refLinks.length - 1].definition)]
1272
1304
  } else if (tableAlias) {
1273
1305
  result.ref = [tableAlias, token.flatName]
@@ -1301,9 +1333,14 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1301
1333
  * @param {object} token with $refLinks
1302
1334
  * @param {string} operator one of allOps
1303
1335
  * @param {object} value either `null` or a column (with `ref` and `$refLinks`)
1336
+ * @param {object} $baseLink optional base `$refLink`, e.g. for infix filters of scoped queries.
1337
+ * In the following example, we must pass `bookshop:Reproduce` as $baseLink for `author`:
1338
+ *
1339
+ * `DELETE.from('bookshop.Reproduce[author = null]:accessGroup')`
1340
+ * ^^^^^^
1304
1341
  * @returns {array}
1305
1342
  */
1306
- function expandComparison(token, operator, value) {
1343
+ function expandComparison(token, operator, value, $baseLink = null) {
1307
1344
  const { definition } = token.$refLinks[token.$refLinks.length - 1]
1308
1345
  let flatRhs
1309
1346
  const result = []
@@ -1319,7 +1356,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1319
1356
  // --> calculate error message or exit early? See test "proper error if structures cannot be compared / too many elements on lhs"
1320
1357
  if (flatRhs.length !== flatLhs.length)
1321
1358
  // make sure we can compare both structures
1322
- cds.error(
1359
+ throw new Error(
1323
1360
  `Can't compare "${definition.name}" with "${
1324
1361
  value.$refLinks[value.$refLinks.length - 1].definition.name
1325
1362
  }": the operands must have the same structure`,
@@ -1347,13 +1384,13 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1347
1384
  // if we still have elements in flatRhs -> those were not found in lhs
1348
1385
  const lhsPath = token.ref.join('.') // original path of the comparison, used in error message
1349
1386
  flatRhs.forEach(t => pathNotFoundErr.push(`Path "${t._csnPath.slice(1).join('.')}" not found in "${lhsPath}"`))
1350
- cds.error(`Can't compare "${lhsPath}" with "${rhsPath}": ${pathNotFoundErr.join(', ')}`)
1387
+ throw new Error(`Can't compare "${lhsPath}" with "${rhsPath}": ${pathNotFoundErr.join(', ')}`)
1351
1388
  }
1352
1389
  } else {
1353
1390
  // compare with value
1354
1391
  const flatLhs = flattenWithBaseName(token)
1355
1392
  if (flatLhs.length > 1 && value.val !== null && value !== 'null')
1356
- cds.error(`Can't compare structure "${token.ref.join('.')}" with value "${value.val}"`)
1393
+ throw new Error(`Can't compare structure "${token.ref.join('.')}" with value "${value.val}"`)
1357
1394
  const boolOp = notEqOps.some(([f, s]) => operator[0] === f && operator[1] === s) ? 'or' : 'and'
1358
1395
  flatLhs.forEach((column, i) => {
1359
1396
  result.push(column, ...operator, value)
@@ -1366,7 +1403,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1366
1403
  if (!def.$refLinks) return def
1367
1404
  const leaf = def.$refLinks[def.$refLinks.length - 1]
1368
1405
  const first = def.$refLinks[0]
1369
- const tableAlias = getQuerySourceName(def, def.ref.length > 1 && first.definition.isAssociation ? first : null)
1406
+ const tableAlias = getQuerySourceName(
1407
+ def,
1408
+ def.ref.length > 1 && first.definition.isAssociation ? first : $baseLink,
1409
+ )
1370
1410
  if (leaf.definition.parent.kind !== 'entity')
1371
1411
  // we need the base name
1372
1412
  return getFlatColumnsFor(leaf.definition, {
@@ -1386,10 +1426,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1386
1426
  rejectStructInExpression()
1387
1427
 
1388
1428
  function rejectAssocInExpression() {
1389
- throw new Error(/An association can't be used as a value in an expression/)
1429
+ throw new Error("An association can't be used as a value in an expression")
1390
1430
  }
1391
1431
  function rejectStructInExpression() {
1392
- throw new Error(/A structured element can't be used as a value in an expression/)
1432
+ throw new Error("A structured element can't be used as a value in an expression")
1393
1433
  }
1394
1434
  }
1395
1435
 
@@ -1935,7 +1975,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1935
1975
  if ($baseLink) {
1936
1976
  return getBaseLinkAlias($baseLink)
1937
1977
  }
1938
-
1939
1978
  if (node.isJoinRelevant) {
1940
1979
  return getJoinRelevantAlias(node)
1941
1980
  }
@@ -2037,6 +2076,6 @@ function setElementOnColumns(col, element) {
2037
2076
  writable: true,
2038
2077
  })
2039
2078
  }
2040
-
2079
+ const getName = col => col.as || col.ref?.at(-1)
2041
2080
  const idOnly = ref => ref.id || ref
2042
2081
  const is_regexp = x => x?.constructor?.name === 'RegExp' // NOTE: x instanceof RegExp doesn't work in repl
@@ -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
- return next()
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
- if (query.UPDATE && !beforeData.length) {
30
- return 0
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
- queries.map(query => {
36
- if (query.INSERT) return this.onINSERT({ query })
37
- if (query.UPDATE) return this.onUPDATE({ query })
38
- if (query.DELETE) return this.onSIMPLE({ query })
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 = (query, target) => {
45
- if (handledDeep in query) return
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 from = query.DELETE?.from || query.UPDATE?.entity
156
- const data = query.UPDATE?.data || null
157
- const where = query.DELETE?.where || query.UPDATE?.where
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
  /**
@@ -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 (req.event !== 'DELETE' && !(req.query.INSERT?.rows || req.query.INSERT?.values)) {
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)
@@ -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) cds.error('Please specify a 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) cds.error('”UNION” based queries are not supported')
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) cds.error(`"${first}" not found in the definitions of your model`)
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) cds.error(`No association "${r.id || r}" in ${d.kind} "${d.name}": ${d}`)
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(/Query source must be a an entity or an association/)
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) throw cds.error`Expecting expression to have an alias name`
292
- if (queryElements[as]) throw cds.error`Duplicate definition of element “${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
- throw cds.error`Not supported: ${JSON.stringify(col)}`
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
- if (where) {
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 walkTokenStream = token => {
344
- if (token === 'exists') {
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 (token.xpr) {
367
+ } else if (t.xpr) {
348
368
  // don't miss an exists within an expression
349
- token.xpr.forEach(walkTokenStream)
369
+ t.xpr.forEach(processToken)
350
370
  } else {
351
- inferQueryElement(token, false, null, { inExists: skipJoins, inExpr: true })
371
+ inferQueryElement(t, false, null, { inExists: skipJoins, inExpr: true })
352
372
  skipJoins = false
353
373
  }
354
374
  }
355
- where.forEach(walkTokenStream)
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 cds.error(
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(/A filter can only be provided when navigating along associations/)
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 cds.error(
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
- resolveCalculatedElement(column, $baseLink, baseColumn)
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 resolveCalculatedElement(column, baseLink, baseColumn) {
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 = { $refLinks: [], ref: [] }
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(arg, false, { definition: calcElement.parent, target: calcElement.parent }),
824
- ) // {func}.args are optional
825
- function mergePathsIntoJoinTree(e, basePath = null) {
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 (e.ref) {
828
- e.$refLinks.forEach((link, i) => {
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(e.ref[i])
860
+ basePath.ref.push(arg.ref[i])
833
861
  }
834
862
  })
835
- const leafOfCalculatedElementRef = e.$refLinks[e.$refLinks.length - 1].definition
863
+ const leafOfCalculatedElementRef = arg.$refLinks[arg.$refLinks.length - 1].definition
836
864
  if (leafOfCalculatedElementRef.value) mergePathsIntoJoinTree(leafOfCalculatedElementRef.value, basePath)
837
865
 
838
- mergePathIfNecessary(basePath, e)
839
- } else if (e.xpr) {
840
- e.xpr.forEach(step => {
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.entries(sources[aliases[0]].elements).forEach(([name, element]) => {
928
- if (!exclude(name) && element.type !== 'cds.LargeBinary') queryElements[name] = element
929
- if (element.value) {
930
- // we might have join relevant calculated elements
931
- resolveCalculatedElement(element)
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()
@@ -181,7 +181,10 @@ class JoinTree {
181
181
  if (next) {
182
182
  // step already seen before
183
183
  node = next
184
- col.$refLinks[i] = node.$refLink // re-set $refLink to point to already merged $refLink
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "1.2.0",
3
+ "version": "1.3.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": {