@cap-js/db-service 1.0.1 → 1.1.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/lib/cqn4sql.js CHANGED
@@ -70,13 +70,14 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
70
70
  // Transform the from clause: association path steps turn into `WHERE EXISTS` subqueries.
71
71
  // The already transformed `where` clause is then glued together with the resulting subqueries.
72
72
  const { transformedWhere, transformedFrom } = getTransformedFrom(from || entity, transformedProp.where)
73
+ const queryNeedsJoins = inferred.joinTree && !inferred.joinTree.isInitial
73
74
 
74
75
  if (inferred.SELECT) {
75
76
  transformedQuery = transformSelectQuery(queryProp, transformedFrom, transformedWhere, transformedQuery)
76
77
  } else {
77
78
  if (from) {
78
79
  transformedProp.from = transformedFrom
79
- } else {
80
+ } else if (!queryNeedsJoins) {
80
81
  transformedProp.entity = transformedFrom
81
82
  }
82
83
 
@@ -94,7 +95,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
94
95
  }
95
96
  }
96
97
 
97
- if (inferred.joinTree && !inferred.joinTree.isInitial) {
98
+ if (queryNeedsJoins) {
98
99
  transformedQuery[kind].from = translateAssocsToJoins(transformedQuery[kind].from)
99
100
  }
100
101
  }
@@ -119,7 +120,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
119
120
  if (columns) {
120
121
  transformedQuery.SELECT.columns = getTransformedColumns(columns)
121
122
  } else {
122
- transformedQuery.SELECT.columns = getColumnsForWildcard()
123
+ transformedQuery.SELECT.columns = getColumnsForWildcard(originalQuery.SELECT?.excluding)
123
124
  }
124
125
 
125
126
  // Like the WHERE clause, aliases from the SELECT list are not accessible for `group by`/`having` (in most DB's)
@@ -158,7 +159,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
158
159
  *
159
160
  * @param {string} kind - The type of operation: "INSERT" or "UPSERT".
160
161
  *
161
- * @returns {Object} - The transformed query with updated `into` clause.
162
+ * @returns {object} - The transformed query with updated `into` clause.
162
163
  */
163
164
  function transformQueryForInsertUpsert(kind) {
164
165
  const { as } = transformedQuery[kind].into
@@ -170,10 +171,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
170
171
  /**
171
172
  * Transforms a stream query, replacing the `where` and `into` clauses after processing.
172
173
  *
173
- * @param {Object} inferred - The inferred object containing the STREAM query.
174
- * @param {Object} transformedQuery - The query object to be transformed.
174
+ * @param {object} inferred - The inferred object containing the STREAM query.
175
+ * @param {object} transformedQuery - The query object to be transformed.
175
176
  *
176
- * @returns {Object} - The transformed query with updated STREAM clauses.
177
+ * @returns {object} - The transformed query with updated STREAM clauses.
177
178
  */
178
179
  function transformStreamQuery() {
179
180
  const { into, where } = inferred.STREAM
@@ -193,8 +194,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
193
194
  /**
194
195
  * Transforms a search expression to a WHERE clause for a SELECT operation.
195
196
  *
196
- * @param {Object} search - The search expression which shall be applied to the searchable columns on the query source.
197
- * @param {Object} from - The FROM clause of the CQN statement.
197
+ * @param {object} search - The search expression which shall be applied to the searchable columns on the query source.
198
+ * @param {object} from - The FROM clause of the CQN statement.
198
199
  *
199
200
  * @returns {(Object|Array|undefined)} - If the target of the query contains searchable elements, the function returns an array that represents the WHERE clause.
200
201
  * If the SELECT query already contains a WHERE clause, this array includes the existing clause and appends an AND condition with the new 'contains' clause.
@@ -293,6 +294,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
293
294
  }
294
295
  }
295
296
 
297
+ function isCalculatedOnRead(def) {
298
+ return def?.value && !def.value.stored
299
+ }
300
+
296
301
  /**
297
302
  * Walks over a list of columns (ref's, xpr, subqueries, val), applies flattening on structured types and expands wildcards.
298
303
  *
@@ -304,7 +309,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
304
309
  for (let i = 0; i < columns.length; i++) {
305
310
  const col = columns[i]
306
311
 
307
- if (col.expand) {
312
+ if (isCalculatedOnRead(col.$refLinks?.[col.$refLinks.length - 1].definition)) {
313
+ const calcElement = resolveCalculatedElement(col)
314
+ transformedColumns.push(calcElement)
315
+ } else if (col.expand) {
308
316
  if (col.ref?.length > 1 && col.ref[0] === '$self' && !col.$refLinks[0].definition.kind) {
309
317
  const dollarSelfReplacement = calculateDollarSelfColumn(col)
310
318
  transformedColumns.push(...getTransformedColumns([dollarSelfReplacement]))
@@ -433,6 +441,30 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
433
441
  }
434
442
  }
435
443
 
444
+ function resolveCalculatedElement(column, omitAlias = false, baseLink = null) {
445
+ let value
446
+
447
+ if (column.$refLinks) {
448
+ const { $refLinks } = column
449
+ value = $refLinks[$refLinks.length - 1].definition.value
450
+ baseLink = [...column.$refLinks].reverse().find(link => link.definition.isAssociation) || baseLink
451
+ } else {
452
+ value = column.value
453
+ }
454
+ const { ref, val, xpr, func } = value
455
+
456
+ let res
457
+ if (ref) {
458
+ res = getTransformedTokenStream([value], baseLink)[0]
459
+ } else if (xpr) {
460
+ res = { xpr: getTransformedTokenStream(value.xpr, baseLink) }
461
+ } else if (val) {
462
+ res = { val }
463
+ } else if (func) res = { args: getTransformedTokenStream(value.args), func: value.func }
464
+ if (!omitAlias) res.as = column.as || column.name || column.flatName
465
+ return res
466
+ }
467
+
436
468
  /**
437
469
  * This function resolves a `ref` starting with a `$self`.
438
470
  * Such a path targets another element of the query by it's implicit, or explicit alias.
@@ -632,7 +664,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
632
664
  *
633
665
  * @param {CSN.column} column - The column with the 'expand' property to be transformed into a subquery.
634
666
  *
635
- * @returns {Object} Returns a subquery correlated with the enclosing query, with added properties `expand:true` and `one:true|false`.
667
+ * @returns {object} Returns a subquery correlated with the enclosing query, with added properties `expand:true` and `one:true|false`.
636
668
  */
637
669
  function expandColumn(column) {
638
670
  let outerAlias
@@ -726,7 +758,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
726
758
  const res = []
727
759
  for (let i = 0; i < columns.length; i++) {
728
760
  const col = columns[i]
729
- if (col.isJoinRelevant) {
761
+ if (isCalculatedOnRead(col.$refLinks?.[col.$refLinks.length - 1].definition)) {
762
+ const calcElement = resolveCalculatedElement(col, true)
763
+ res.push(calcElement)
764
+ } else if (col.isJoinRelevant) {
730
765
  const tableAlias$refLink = getQuerySourceName(col)
731
766
  const transformedColumn = {
732
767
  ref: [tableAlias$refLink, getFullName(col.$refLinks[col.$refLinks.length - 1].definition)],
@@ -826,6 +861,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
826
861
  // for wildcard on subquery in from, just reference the elements
827
862
  if (tableAlias.SELECT && !element.elements && !element.target) {
828
863
  wildcardColumns.push(index ? { ref: [index, k] } : { ref: [k] })
864
+ } else if (isCalculatedOnRead(element)) {
865
+ wildcardColumns.push(resolveCalculatedElement(element))
829
866
  } else {
830
867
  const flatColumns = getFlatColumnsFor(element, { tableAlias: index }, [], { exclude, replace }, true)
831
868
  wildcardColumns.push(...flatColumns)
@@ -1157,6 +1194,12 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1157
1194
 
1158
1195
  let result = is_regexp(token?.val) ? token : copy(token) // REVISIT: too expensive! //
1159
1196
  if (token.ref) {
1197
+ const { definition } = token.$refLinks[token.$refLinks.length - 1]
1198
+ if (isCalculatedOnRead(definition)) {
1199
+ const calculatedElement = resolveCalculatedElement(token, true, $baseLink)
1200
+ transformedTokenStream.push(calculatedElement)
1201
+ continue
1202
+ }
1160
1203
  if (token.ref.length > 1 && token.ref[0] === '$self' && !token.$refLinks[0].definition.kind) {
1161
1204
  const dollarSelfReplacement = [calculateDollarSelfColumn(token, true)]
1162
1205
  transformedTokenStream.push(...getTransformedTokenStream(dollarSelfReplacement))
@@ -1472,11 +1515,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1472
1515
  each.ref[0] in { $self: true, $projection: true } ? getParentEntity(assoc) : target,
1473
1516
  ),
1474
1517
  )
1475
-
1476
- function getParentEntity(element) {
1477
- if (element.kind === 'entity') return element
1478
- else return getParentEntity(element.parent)
1479
- }
1480
1518
  }
1481
1519
 
1482
1520
  /**
@@ -1920,6 +1958,11 @@ function getLastStringSegment(str) {
1920
1958
  return index != -1 ? str.substring(index + 1) : str
1921
1959
  }
1922
1960
 
1961
+ function getParentEntity(element) {
1962
+ if (element.kind === 'entity') return element
1963
+ else return getParentEntity(element.parent)
1964
+ }
1965
+
1923
1966
  /**
1924
1967
  * Assigns the given `element` as non-enumerable property 'element' onto `col`.
1925
1968
  *
@@ -4,6 +4,17 @@ const { _target_name4 } = require('./SQLService')
4
4
 
5
5
  const handledDeep = Symbol('handledDeep')
6
6
 
7
+ /**
8
+ * @callback nextCallback
9
+ * @param {Error|undefined} error
10
+ * @returns {Promise<unknown>}
11
+ */
12
+
13
+ /**
14
+ * @param {import('@sap/cds/apis/services').Request} req
15
+ * @param {nextCallback} next
16
+ * @returns {Promise<number>}
17
+ */
7
18
  async function onDeep(req, next) {
8
19
  const { query } = req
9
20
  // REVISIT: req.target does not match the query.INSERT target for path insert
@@ -136,11 +147,16 @@ const _calculateExpandColumns = (target, data, expandColumns = [], elementMap =
136
147
  }
137
148
  }
138
149
 
150
+ /**
151
+ * @param {import('@sap/cds/apis/cqn').Query} query
152
+ * @param {import('@sap/cds/apis/csn').Definition} target
153
+ */
139
154
  const getExpandForDeep = (query, target) => {
140
155
  const from = query.DELETE?.from || query.UPDATE?.entity
141
156
  const data = query.UPDATE?.data || null
142
157
  const where = query.DELETE?.where || query.UPDATE?.where
143
158
 
159
+ /** @type {import("@sap/cds/apis/ql").SELECT<unknown>} */
144
160
  const cqn = SELECT.from(from)
145
161
  if (where) cqn.SELECT.where = where
146
162
 
@@ -150,6 +166,12 @@ const getExpandForDeep = (query, target) => {
150
166
  return cqn
151
167
  }
152
168
 
169
+ /**
170
+ * @param {import('@sap/cds/apis/cqn').Query} query
171
+ * @param {unknown[]} dbData
172
+ * @param {import('@sap/cds/apis/csn').Definition} target
173
+ * @returns
174
+ */
153
175
  const getDeepQueries = (query, dbData, target) => {
154
176
  let queryData
155
177
  if (query.INSERT) {
@@ -174,6 +196,11 @@ const _hasManagedElements = target => {
174
196
  return Object.keys(target.elements).filter(elementName => target.elements[elementName]['@cds.on.update']).length > 0
175
197
  }
176
198
 
199
+ /**
200
+ * @param {unknown[]} diff
201
+ * @param {import('@sap/cds/apis/csn').Definition} target
202
+ * @returns {import('@sap/cds/apis/cqn').Query[]}
203
+ */
177
204
  const _getDeepQueries = (diff, target) => {
178
205
  const queries = []
179
206
 
@@ -41,6 +41,16 @@ const generateUUIDandPropagateKeys = (target, data, event) => {
41
41
  }
42
42
  }
43
43
 
44
+ /**
45
+ * @callback nextCallback
46
+ * @param {Error|undefined} error
47
+ * @returns {Promise<unknown>}
48
+ */
49
+
50
+ /**
51
+ * @param {import('@sap/cds/apis/services').Request} req
52
+ * @param {nextCallback} next
53
+ */
44
54
  module.exports = async function fill_in_keys(req, next) {
45
55
  // REVISIT dummy handler until we have input processing
46
56
  if (!req.target || !this.model || req.target._unresolved) return next()
@@ -0,0 +1,45 @@
1
+ import * as cqn from '@sap/cds/apis/cqn'
2
+ import * as csn from '@sap/cds/apis/csn'
3
+
4
+ type linkedQuery = {
5
+ target: csn.Definition
6
+ elements: elements
7
+ }
8
+ export type SELECT = cqn.SELECT & linkedQuery
9
+ export type INSERT = cqn.INSERT & linkedQuery
10
+ export type UPSERT = cqn.UPSERT & linkedQuery
11
+ export type UPDATE = cqn.UPDATE & linkedQuery
12
+ export type STREAM = {
13
+ STREAM: { into: cqn.name | ref; data: ReadableStream<Buffer>; from: cqn.source; column?: ref; where: predicate }
14
+ } & linkedQuery
15
+ export type DELETE = cqn.DELETE & linkedQuery
16
+ export type CREATE = cqn.CREATE & linkedQuery
17
+ export type DROP = cqn.DROP & linkedQuery
18
+
19
+ export type Query = SELECT | INSERT | UPSERT | UPDATE | DELETE | CREATE | DROP
20
+
21
+ export type element = csn.Element & {
22
+ key?: boolean
23
+ virtual?: boolean
24
+ unique?: boolean
25
+ notNull?: boolean
26
+ }
27
+ export type elements = {
28
+ [name: string]: element
29
+ }
30
+
31
+ export type col = cqn.column_expr & { element: element }
32
+
33
+ export type list = {
34
+ list: cqn.expr[]
35
+ }
36
+ // Passthrough
37
+ export type source = cqn.source
38
+ export type ref = cqn.ref
39
+ export type val = cqn.val
40
+ export type xpr = cqn.xpr
41
+ export type expr = cqn.expr
42
+ export type func = cqn.function_call
43
+ export type predicate = cqn.predicate
44
+ export type ordering_term = cqn.ordering_term
45
+ export type limit = { rows: val; offset: val }
@@ -4,7 +4,6 @@ const cds = require('@sap/cds/lib')
4
4
 
5
5
  const JoinTree = require('./join-tree')
6
6
  const { pseudos } = require('./pseudos')
7
- // REVISIT: we should always return cds.linked elements
8
7
  const cdsTypes = cds.linked({
9
8
  definitions: {
10
9
  Timestamp: { type: 'cds.Timestamp' },
@@ -18,11 +17,10 @@ const cdsTypes = cds.linked({
18
17
  },
19
18
  }).definitions
20
19
  for (const each in cdsTypes) cdsTypes[`cds.${each}`] = cdsTypes[each]
21
-
22
20
  /**
23
- * @param {CQN|CQL} originalQuery
24
- * @param {CSN} [model]
25
- * @returns {InferredCQN} = q with .target and .elements
21
+ * @param {import('@sap/cds/apis/cqn').Query|string} originalQuery
22
+ * @param {import('@sap/cds/apis/csn').CSN} [model]
23
+ * @returns {import('./cqn').Query} = q with .target and .elements
26
24
  */
27
25
  function infer(originalQuery, model = cds.context?.model || cds.model) {
28
26
  if (!model) cds.error('Please specify a model')
@@ -40,6 +38,10 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
40
38
  inferred.CREATE ||
41
39
  inferred.DROP ||
42
40
  inferred.STREAM
41
+
42
+ // cache for already processed calculated elements
43
+ const alreadySeenCalcElements = new Set()
44
+
43
45
  const sources = inferTarget(_.from || _.into || _.entity, {})
44
46
  const joinTree = new JoinTree(sources)
45
47
  const aliases = Object.keys(sources)
@@ -134,11 +136,11 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
134
136
  * next 'ref' step should be looked up.
135
137
  *
136
138
  *
137
- * @param {Object} arg - The argument object that will be augmented with additional properties.
139
+ * @param {object} arg - The argument object that will be augmented with additional properties.
138
140
  * It must contain a 'ref' property, which is an array representing the steps to be processed.
139
141
  * Optionally, it can also contain an 'xpr' property, which is also processed recursively.
140
142
  *
141
- * @param {Object} $baseLink - Optional parameter. It represents the environment in which the first 'ref' step should be
143
+ * @param {object} $baseLink - Optional parameter. It represents the environment in which the first 'ref' step should be
142
144
  * resolved. It's needed for infix filter / expand columns. It must contain a 'definition'
143
145
  * property, which is an object representing the base environment.
144
146
  *
@@ -177,7 +179,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
177
179
  if (!expandOrExists && nextStep && !(nextStep in e.foreignKeys))
178
180
  throw new Error(`Only foreign keys of "${e.name}" can be accessed in infix filter`)
179
181
  }
180
- arg.$refLinks.push({ definition: e, target: e._target || e })
182
+ arg.$refLinks.push({ definition: e, target: definition })
181
183
  // filter paths are flattened
182
184
  // REVISIT: too much augmentation -> better remove flatName..
183
185
  Object.defineProperty(arg, 'flatName', { value: ref.join('_'), writable: true })
@@ -188,7 +190,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
188
190
  }
189
191
  } else {
190
192
  const recent = arg.$refLinks[i - 1]
191
- const { elements } = recent.target
193
+ const { elements } = recent.definition._target || recent.definition
192
194
  const e = elements[id]
193
195
  if (!e) throw new Error(`"${id}" not found in the elements of "${arg.$refLinks[i - 1].definition.name}"`)
194
196
  arg.$refLinks.push({ definition: e, target: e._target || e })
@@ -216,6 +218,11 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
216
218
  } else throw new Error('A filter can only be provided when navigating along associations')
217
219
  }
218
220
  })
221
+ const { definition, target } = arg.$refLinks[arg.$refLinks.length - 1]
222
+ if (definition.value) {
223
+ // nested calculated element
224
+ attachRefLinksToArg(definition.value, { definition: definition.parent, target }, true)
225
+ }
219
226
  }
220
227
 
221
228
  /**
@@ -227,7 +234,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
227
234
  * Each entry in the `$combinedElements` dictionary maps from the element name
228
235
  * to an array of objects containing the index and table alias where the element can be found.
229
236
  *
230
- * @returns {Object} The `$combinedElements` dictionary, which maps element names to an array of objects
237
+ * @returns {object} The `$combinedElements` dictionary, which maps element names to an array of objects
231
238
  * containing the index and table alias where the element can be found.
232
239
  */
233
240
  function inferCombinedElements() {
@@ -264,9 +271,9 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
264
271
  * Also walks over other `ref`s in the query, validates them, and attaches `$refLinks`.
265
272
  * This includes handling `where`, infix filters within column `refs`, or other `csn` paths.
266
273
  *
267
- * @param {Object} $combinedElements The `$combinedElements` dictionary of the query, which maps element names
274
+ * @param {object} $combinedElements The `$combinedElements` dictionary of the query, which maps element names
268
275
  * to an array of objects containing the index and table alias where the element can be found.
269
- * @returns {Object} The inferred `elements` dictionary of the query, which maps element names to their corresponding definitions.
276
+ * @returns {object} The inferred `elements` dictionary of the query, which maps element names to their corresponding definitions.
270
277
  */
271
278
  function inferQueryElements($combinedElements) {
272
279
  let queryElements = {}
@@ -444,7 +451,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
444
451
  */
445
452
 
446
453
  function inferQueryElement(column, insertIntoQueryElements = true, $baseLink = null, context) {
447
- const { inExists, inExpr, inNestedProjection } = context || {}
454
+ const { inExists, inExpr, inNestedProjection, inCalcElement } = context || {}
448
455
  if (column.param) return // parameter references are only resolved into values on execution e.g. :val, :1 or ?
449
456
  if (column.args) column.args.forEach(arg => inferQueryElement(arg, false, $baseLink, context)) // e.g. function in expression
450
457
  if (column.list) column.list.forEach(arg => inferQueryElement(arg, false, $baseLink, context))
@@ -486,7 +493,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
486
493
  const elements = definition.elements || definition._target?.elements
487
494
  if (elements && id in elements) {
488
495
  const element = elements[id]
489
- if (!inExists && !inNestedProjection && element.target) {
496
+ if (!inExists && !inNestedProjection && !inCalcElement && element.target) {
490
497
  // only fk access in infix filter
491
498
  const nextStep = column.ref[1]?.id || column.ref[1]
492
499
  // no unmanaged assoc in infix filter path
@@ -500,7 +507,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
500
507
  if (nextStep && !(nextStep in element.foreignKeys))
501
508
  throw new Error(`Only foreign keys of "${element.name}" can be accessed in infix filter`)
502
509
  }
503
- column.$refLinks.push({ definition: elements[id], target })
510
+ const resolvableIn = definition.target ? definition._target : target
511
+ column.$refLinks.push({ definition: elements[id], target: resolvableIn })
504
512
  } else {
505
513
  stepNotFoundInPredecessor(id, definition.name)
506
514
  }
@@ -535,14 +543,15 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
535
543
  )
536
544
  }
537
545
 
546
+ const target = definition._target || column.$refLinks[i - 1].target
538
547
  if (element) {
539
- const $refLink = { definition: elements[id], target: column.$refLinks[i - 1].target }
548
+ const $refLink = { definition: elements[id], target }
540
549
  column.$refLinks.push($refLink)
541
550
  } else if (firstStepIsSelf) {
542
551
  stepNotFoundInColumnList(id)
543
552
  } else if (column.ref[0] === '$user' && pseudoPath) {
544
553
  // `$user.some.unknown.element` -> no error
545
- column.$refLinks.push({ definition: {}, target: column.$refLinks[i - 1].target })
554
+ column.$refLinks.push({ definition: {}, target })
546
555
  } else if (id === '$dummy') {
547
556
  // `some.known.element.$dummy` -> no error; used by cds.ql to simulate joins
548
557
  column.$refLinks.push({ definition: { name: '$dummy', parent: column.$refLinks[i - 1].target } })
@@ -627,7 +636,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
627
636
  }
628
637
  if (queryElements[elementName] !== undefined)
629
638
  throw new Error(`Duplicate definition of element “${elementName}”`)
630
- queryElements[elementName] = getCopyWithAnnos(column, leafArt)
639
+ const element = getCopyWithAnnos(column, leafArt)
640
+ queryElements[elementName] = element
631
641
  }
632
642
  }
633
643
  }
@@ -644,19 +654,27 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
644
654
  return
645
655
  }
646
656
  }
647
- const virtual = (column.$refLinks[column.$refLinks.length - 1].definition.virtual || !isPersisted) && !inExpr
657
+ const leafArt = column.$refLinks[column.$refLinks.length - 1].definition
658
+ const virtual = (leafArt.virtual || !isPersisted) && !inExpr
648
659
  // check if we need to merge the column `ref` into the join tree of the query
649
- if (!inExists && !virtual && isColumnJoinRelevant(column, firstStepIsSelf)) {
660
+ if (!inExists && !virtual && !inCalcElement && isColumnJoinRelevant(column, firstStepIsSelf)) {
661
+ if (originalQuery.UPDATE)
662
+ throw cds.error(
663
+ 'Path expressions for UPDATE statements are not supported. Use “where exists” with infix filters instead.',
664
+ )
650
665
  Object.defineProperty(column, 'isJoinRelevant', { value: true })
651
- joinTree.mergeColumn(column)
666
+ joinTree.mergeColumn(column, $baseLink)
667
+ }
668
+ if (leafArt.value && !leafArt.value.stored) {
669
+ resolveCalculatedElement(leafArt, column)
652
670
  }
653
671
 
654
672
  /**
655
673
  * Resolves and processes the inline attribute of a column in a database query.
656
674
  *
657
- * @param {Object} col - The column object with properties: `inline` and `$refLinks`.
675
+ * @param {object} col - The column object with properties: `inline` and `$refLinks`.
658
676
  * @param {string} [namePrefix=col.as || col.flatName] - Prefix for naming new columns. Defaults to `col.as` or `col.flatName`.
659
- * @returns {Object} - An object with resolved and processed inline column definitions.
677
+ * @returns {object} - An object with resolved and processed inline column definitions.
660
678
  *
661
679
  * Procedure:
662
680
  * 1. Iterate through `inline` array. For each `inlineCol`:
@@ -710,8 +728,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
710
728
  /**
711
729
  * Resolves a query column which has an `expand` property.
712
730
  *
713
- * @param {Object} col - The column object with properties: `expand` and `$refLinks`.
714
- * @returns {Object} - A `cds.struct` object with expanded column definitions.
731
+ * @param {object} col - The column object with properties: `expand` and `$refLinks`.
732
+ * @returns {object} - A `cds.struct` object with expanded column definitions.
715
733
  *
716
734
  * Procedure:
717
735
  * - if `$leafLink` is an association, constructs an `expandSubquery` and infers a new query structure.
@@ -777,6 +795,58 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
777
795
  throw new Error(err)
778
796
  }
779
797
  }
798
+ function resolveCalculatedElement(calcElement) {
799
+ if (alreadySeenCalcElements.has(calcElement)) return
800
+ else alreadySeenCalcElements.add(calcElement)
801
+ const { ref, xpr, func } = calcElement.value
802
+ if (ref || xpr) {
803
+ attachRefLinksToArg(calcElement.value, { definition: calcElement.parent, target: calcElement.parent }, true)
804
+ // column is now fully linked, now we need to find out if we need to merge it into the join tree
805
+ // for that, we calculate all paths from a calc element and merge them into the join tree
806
+ mergePathsIntoJoinTree(calcElement.value)
807
+ }
808
+ if (func) calcElement.value.args?.forEach(arg => inferQueryElement(arg, false)) // {func}.args are optional
809
+ function mergePathsIntoJoinTree(e, basePath = null) {
810
+ basePath = basePath || { $refLinks: [], ref: [] }
811
+ if (e.ref) {
812
+ e.$refLinks.forEach((link, i) => {
813
+ const { definition } = link
814
+ if (!definition.value) {
815
+ basePath.$refLinks.push(link)
816
+ basePath.ref.push(e.ref[i])
817
+ }
818
+ })
819
+ const leafOfCalculatedElementRef = e.$refLinks[e.$refLinks.length - 1].definition
820
+ if (leafOfCalculatedElementRef.value) mergePathsIntoJoinTree(leafOfCalculatedElementRef.value, basePath)
821
+
822
+ mergePathIfNecessary(basePath, e)
823
+ } else if (e.xpr) {
824
+ e.xpr.forEach(step => {
825
+ if (step.ref) {
826
+ const subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
827
+ step.$refLinks.forEach((link, i) => {
828
+ const { definition } = link
829
+ if (definition.value) {
830
+ mergePathsIntoJoinTree(definition.value)
831
+ } else {
832
+ subPath.$refLinks.push(link)
833
+ subPath.ref.push(step.ref[i])
834
+ }
835
+ })
836
+ mergePathIfNecessary(subPath, step)
837
+ }
838
+ })
839
+ }
840
+
841
+ function mergePathIfNecessary(p, step) {
842
+ const calcElementIsJoinRelevant = isColumnJoinRelevant(p)
843
+ if (calcElementIsJoinRelevant) {
844
+ if (!calcElement.value.isColumnJoinRelevant) Object.defineProperty(step, 'isJoinRelevant', { value: true })
845
+ joinTree.mergeColumn(p)
846
+ }
847
+ }
848
+ }
849
+ }
780
850
 
781
851
  /**
782
852
  * Checks whether or not the `ref` of the given column is join relevant.
@@ -828,15 +898,20 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
828
898
  * if there is not already an element with the same name present.
829
899
  */
830
900
  function inferElementsFromWildCard() {
901
+ const exclude = _.excluding ? x => _.excluding.includes(x) : () => false
902
+
831
903
  if (Object.keys(queryElements).length === 0 && aliases.length === 1) {
832
904
  // only one query source and no overwritten columns
833
905
  Object.entries(sources[aliases[0]].elements).forEach(([name, element]) => {
834
- if (element.type !== 'cds.LargeBinary') queryElements[name] = element
906
+ if (!exclude(name) && element.type !== 'cds.LargeBinary') queryElements[name] = element
907
+ if (element.value) {
908
+ // we might have join relevant calculated elements
909
+ resolveCalculatedElement(element)
910
+ }
835
911
  })
836
912
  return
837
913
  }
838
914
 
839
- const exclude = _.excluding ? x => _.excluding.includes(x) : () => false
840
915
  const ambiguousElements = {}
841
916
  Object.entries($combinedElements).forEach(([name, tableAliases]) => {
842
917
  if (Object.keys(tableAliases).length > 1) {