@cap-js/db-service 1.16.0 → 1.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -42,6 +42,8 @@ function infer(originalQuery, model) {
42
42
  // cache for already processed calculated elements
43
43
  const alreadySeenCalcElements = new Set()
44
44
 
45
+ let $combinedElements
46
+
45
47
  const sources = inferTarget(_.from || _.into || _.entity, {})
46
48
  const joinTree = new JoinTree(sources)
47
49
  const aliases = Object.keys(sources)
@@ -62,21 +64,21 @@ function infer(originalQuery, model) {
62
64
  },
63
65
  })
64
66
  if (originalQuery.SELECT || originalQuery.DELETE || originalQuery.UPDATE) {
65
- const $combinedElements = inferCombinedElements()
67
+ $combinedElements = inferCombinedElements()
66
68
  /**
67
69
  * TODO: this function is currently only called on DELETE's
68
70
  * because it correctly set's up the $refLink's in the
69
71
  * where clause: This functionality should be pulled out
70
72
  * of ´inferQueryElement()` as this is a subtle side effect
71
73
  */
72
- const elements = inferQueryElements($combinedElements)
74
+ const elements = inferQueryElements()
73
75
  Object.defineProperties(inferred, {
74
- $combinedElements: { value: $combinedElements, writable: true },
75
- elements: { value: elements, writable: true },
76
- joinTree: { value: joinTree, writable: true }, // REVISIT: eliminate
76
+ $combinedElements: { value: $combinedElements, writable: true, configurable: true },
77
+ elements: { value: elements, writable: true, configurable: true },
78
+ joinTree: { value: joinTree, writable: true, configurable: true }, // REVISIT: eliminate
77
79
  })
78
80
  // also enrich original query -> writable because it may be inferred again
79
- Object.defineProperty(originalQuery, 'elements', { value: elements, writable: true })
81
+ Object.defineProperty(originalQuery, 'elements', { value: elements, writable: true, configurable: true })
80
82
  }
81
83
  return inferred
82
84
 
@@ -112,7 +114,7 @@ function infer(originalQuery, model) {
112
114
  if (target.kind !== 'entity' && !target.isAssociation)
113
115
  throw new Error('Query source must be a an entity or an association')
114
116
 
115
- attachRefLinksToArg(from) // REVISIT: remove
117
+ inferArg(from, null, null, { inFrom: true })
116
118
  const alias =
117
119
  from.uniqueSubqueryAlias ||
118
120
  from.as ||
@@ -139,116 +141,6 @@ function infer(originalQuery, model) {
139
141
  return querySources
140
142
  }
141
143
 
142
- // REVISIT: this helper is doing by far too much, with too many side effects
143
-
144
- /**
145
- * This function recursively traverses through all 'ref' steps of the 'arg' object and enriches it by attaching
146
- * additional information. For each 'ref' step, it adds the corresponding definition and the target in which the
147
- * next 'ref' step should be looked up.
148
- *
149
- *
150
- * @param {object} arg - The argument object that will be augmented with additional properties.
151
- * It must contain a 'ref' property, which is an array representing the steps to be processed.
152
- * Optionally, it can also contain an 'xpr' property, which is also processed recursively.
153
- *
154
- * @param {object} $baseLink - Optional parameter. It represents the environment in which the first 'ref' step should be
155
- * resolved. It's needed for infix filter / expand columns. It must contain a 'definition'
156
- * property, which is an object representing the base environment.
157
- *
158
- * @param {boolean} expandOrExists - Optional parameter, defaults to false. It indicates whether the 'arg' is part of a
159
- * 'column.expand' or preceded by an 'exists'. When true, unmanaged association paths
160
- * are allowed -> $baseLink is an `expand` or `assoc` preceded by `exists`.
161
- *
162
- * @throws Will throw an error if a 'ref' step cannot be found in the current environment or if a 'ref' step
163
- * represents an unmanaged association in the case of infix filters and 'expandOrExists' is false.
164
- *
165
- * @returns {void} This function does not return a value; it mutates the 'arg' object directly.
166
- */
167
- function attachRefLinksToArg(arg, $baseLink = null, expandOrExists = false) {
168
- const { ref, xpr, args, list } = arg
169
- if (xpr) xpr.forEach(t => attachRefLinksToArg(t, $baseLink, expandOrExists))
170
- if (args) applyToFunctionArgs(args, attachRefLinksToArg, [$baseLink, expandOrExists])
171
- if (list) list.forEach(arg => attachRefLinksToArg(arg, $baseLink, expandOrExists))
172
- if (!ref) return
173
- init$refLinks(arg)
174
- let i = 0
175
- let pseudoPath = false
176
- for (const step of ref) {
177
- const id = step.id || step
178
- if (i === 0) {
179
- if (id in pseudos.elements) {
180
- // pseudo path
181
- arg.$refLinks.push({ definition: pseudos.elements[id], target: pseudos })
182
- pseudoPath = true // only first path step must be well defined
183
- } else if ($baseLink) {
184
- // infix filter never have table alias
185
- // we need to search for first step in ´model.definitions[infixAlias]`
186
- const { definition } = $baseLink
187
- const elements = getDefinition(definition.target)?.elements || definition.elements
188
- const e = elements?.[id] || cds.error`"${id}" not found in the elements of "${definition.name}"`
189
- if (e.target) {
190
- // only fk access in infix filter
191
- const nextStep = ref[1]?.id || ref[1]
192
- if (isNonForeignKeyNavigation(e, nextStep)) {
193
- if (expandOrExists) {
194
- Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
195
- } else {
196
- rejectNonFkNavigation(e, e.on ? $baseLink.definition.name : nextStep)
197
- }
198
- }
199
- }
200
- arg.$refLinks.push({ definition: e, target: definition })
201
- // filter paths are flattened
202
- // REVISIT: too much augmentation -> better remove flatName..
203
- Object.defineProperty(arg, 'flatName', { value: ref.join('_'), writable: true })
204
- } else {
205
- // must be in model.definitions
206
- const definition = getDefinition(id) || cds.error`"${id}" not found in the definitions of your model`
207
- arg.$refLinks[0] = { definition, target: definition }
208
- }
209
- } else if (arg.ref[0] === '$user' && pseudoPath) {
210
- // `$user.some.unknown.element` -> no error
211
- arg.$refLinks.push({ definition: {}, target: pseudos })
212
- } else {
213
- const recent = arg.$refLinks[i - 1]
214
- const { elements } = getDefinition(recent.definition.target) || recent.definition
215
- const e = elements[id]
216
- const notFoundIn = pseudoPath ? arg.ref[i - 1] : getFullPathForLinkedArg(arg)
217
- if (!e) throw new Error(`"${id}" not found in the elements of "${notFoundIn}"`)
218
- arg.$refLinks.push({ definition: e, target: getDefinition(e.target) || e })
219
- }
220
- arg.$refLinks[i].alias = !ref[i + 1] && arg.as ? arg.as : id.split('.').pop()
221
-
222
- // link refs in where
223
- if (step.where) {
224
- // REVISIT: why do we need to walk through these so early?
225
- if (arg.$refLinks[i].definition.kind === 'entity' || getDefinition(arg.$refLinks[i].definition.target)) {
226
- let existsPredicate = false
227
- const walkTokenStream = token => {
228
- if (token === 'exists') {
229
- // no joins for infix filters along `exists <path>`
230
- existsPredicate = true
231
- } else if (token.xpr) {
232
- // don't miss an exists within an expression
233
- token.xpr.forEach(walkTokenStream)
234
- } else {
235
- attachRefLinksToArg(token, arg.$refLinks[i], existsPredicate || expandOrExists)
236
- existsPredicate = false
237
- }
238
- }
239
- step.where.forEach(walkTokenStream)
240
- } else throw new Error('A filter can only be provided when navigating along associations')
241
- }
242
- i += 1
243
- }
244
- if ($baseLink?.pathExpressionInsideFilter) Object.defineProperty(arg, 'join', { value: 'inner' })
245
- const { definition, target } = arg.$refLinks[arg.$refLinks.length - 1]
246
- if (definition.value) {
247
- // nested calculated element
248
- attachRefLinksToArg(definition.value, { definition: definition.parent, target }, true)
249
- }
250
- }
251
-
252
144
  /**
253
145
  * Calculates the `$combinedElements` based on the provided queries `sources`.
254
146
  * The `$combinedElements` of a query consist of all accessible elements across all
@@ -299,11 +191,11 @@ function infer(originalQuery, model) {
299
191
  * to an array of objects containing the index and table alias where the element can be found.
300
192
  * @returns {object} The inferred `elements` dictionary of the query, which maps element names to their corresponding definitions.
301
193
  */
302
- function inferQueryElements($combinedElements) {
194
+ function inferQueryElements() {
303
195
  let queryElements = {}
304
196
  const { columns, where, groupBy, having, orderBy } = _
305
197
  if (!columns) {
306
- inferElementsFromWildCard(aliases)
198
+ inferElementsFromWildCard(queryElements)
307
199
  } else {
308
200
  let wildcardSelect = false
309
201
  const dollarSelfRefs = []
@@ -315,12 +207,12 @@ function infer(originalQuery, model) {
315
207
  if (as === undefined) cds.error`Expecting expression to have an alias name`
316
208
  if (queryElements[as]) cds.error`Duplicate definition of element “${as}”`
317
209
  if (col.xpr || col.SELECT) {
318
- queryElements[as] = getElementForXprOrSubquery(col)
210
+ queryElements[as] = getElementForXprOrSubquery(col, queryElements)
319
211
  }
320
212
  if (col.func) {
321
213
  if (col.args) {
322
214
  // {func}.args are optional
323
- applyToFunctionArgs(col.args, inferQueryElement, [false])
215
+ applyToFunctionArgs(col.args, inferArg, [false])
324
216
  }
325
217
  queryElements[as] = getElementForCast(col)
326
218
  }
@@ -340,7 +232,7 @@ function infer(originalQuery, model) {
340
232
  if (firstStepIsSelf) dollarSelfRefs.push(col)
341
233
  else handleRef(col)
342
234
  } else if (col.expand) {
343
- inferQueryElement(col)
235
+ inferArg(col, queryElements, null)
344
236
  } else {
345
237
  cds.error`Not supported: ${JSON.stringify(col)}`
346
238
  }
@@ -348,7 +240,7 @@ function infer(originalQuery, model) {
348
240
 
349
241
  if (dollarSelfRefs.length) inferDollarSelfRefs(dollarSelfRefs)
350
242
 
351
- if (wildcardSelect) inferElementsFromWildCard(aliases)
243
+ if (wildcardSelect) inferElementsFromWildCard(queryElements)
352
244
  }
353
245
  if (orderBy) {
354
246
  // link $refLinks -> special name resolution rules for orderBy
@@ -376,7 +268,7 @@ function infer(originalQuery, model) {
376
268
  $baseLink = null
377
269
  }
378
270
 
379
- inferQueryElement(token, false, $baseLink)
271
+ inferArg(token, queryElements, $baseLink, { inQueryModifier: true })
380
272
  if (token.isJoinRelevant && rejectJoinRelevantPath) {
381
273
  // reverse the array, find the last association and calculate the index of the association in non-reversed order
382
274
  const assocIndex =
@@ -390,12 +282,12 @@ function infer(originalQuery, model) {
390
282
  }
391
283
 
392
284
  // walk over all paths in other query properties
393
- if (where) walkTokenStream(where)
394
- if (groupBy) groupBy.forEach(token => inferQueryElement(token, false))
285
+ if (where) walkTokenStream(where, true)
286
+ if (groupBy) walkTokenStream(groupBy)
395
287
  if (having) walkTokenStream(having)
396
288
  if (_.with)
397
289
  // consider UPDATE.with
398
- Object.values(_.with).forEach(val => inferQueryElement(val, false))
290
+ Object.values(_.with).forEach(val => inferArg(val, queryElements, null, { inExpr: true }))
399
291
 
400
292
  return queryElements
401
293
 
@@ -407,7 +299,7 @@ function infer(originalQuery, model) {
407
299
  *
408
300
  * @param {array} tokenStream
409
301
  */
410
- function walkTokenStream(tokenStream) {
302
+ function walkTokenStream(tokenStream, inExpr = false) {
411
303
  let skipJoins
412
304
  const processToken = t => {
413
305
  if (t === 'exists') {
@@ -417,7 +309,7 @@ function infer(originalQuery, model) {
417
309
  // don't miss an exists within an expression
418
310
  t.xpr.forEach(processToken)
419
311
  } else {
420
- inferQueryElement(t, false, null, { inExists: skipJoins, inExpr: true })
312
+ inferArg(t, queryElements, null, { inExists: skipJoins, inExpr, inQueryModifier: true })
421
313
  skipJoins = false
422
314
  }
423
315
  }
@@ -460,7 +352,7 @@ function infer(originalQuery, model) {
460
352
  }
461
353
 
462
354
  function handleRef(col) {
463
- inferQueryElement(col)
355
+ inferArg(col, queryElements)
464
356
  const { definition } = col.$refLinks[col.$refLinks.length - 1]
465
357
  if (col.cast)
466
358
  // final type overwritten -> element not visible anymore
@@ -470,148 +362,91 @@ function infer(originalQuery, model) {
470
362
  setElementOnColumns(col, queryElements[col.as || '$user'])
471
363
  else setElementOnColumns(col, definition)
472
364
  }
365
+ }
473
366
 
474
- /**
475
- * This function is responsible for inferring a query element based on a provided column.
476
- * It initializes and attaches a non-enumerable `$refLinks` property to the column,
477
- * which stores an array of objects that represent the corresponding artifact of the ref step.
478
- * Each object in the `$refLinks` array corresponds to the same index position in the `column.ref` array.
479
- * Based on the leaf artifact (last object in the `$refLinks` array), the query element is inferred.
480
- *
481
- * @param {object} column - The column object that contains the properties to infer a query element.
482
- * @param {boolean} [insertIntoQueryElements=true] - Determines whether the inferred element should be inserted into the queries elements.
483
- * For instance, it's set to false when walking over the where clause.
484
- * @param {object} [$baseLink=null] - A base reference link, usually it's an object with a definition and a target.
485
- * Used for infix filters, exists <assoc> and nested projections.
486
- * @param {object} [context={}] - Contextual information for element inference.
487
- * @param {boolean} [context.inExists=false] - Flag to control the creation of joins for non-association path traversals.
488
- * for `exists <assoc>` paths we do not need to create joins for path expressions as they are part of the semi-joined subquery.
489
- * @param {boolean} [context.inExpr=false] - Flag to signal whether the element is part of an expression.
490
- * Used to ignore non-persisted elements.
491
- * @param {boolean} [context.inNestedProjection=false] - Flag to signal whether the element is part of a nested projection.
492
- *
493
- * Note:
494
- * - `inExists` is used to specify cases where no joins should be created for non-association path traversals.
495
- * It is primarily used for infix filters in `exists assoc[parent.foo='bar']`, where it becomes part of a semi-join.
496
- * - Columns with a `param` property are parameter references resolved into values only at execution time.
497
- * - Columns with an `args` property are function calls in expressions.
498
- * - Columns with a `list` property represent a list of values (e.g., for the IN operator).
499
- * - Columns with a `SELECT` property represent subqueries.
500
- *
501
- * @throws {Error} If an unmanaged association is found in an infix filter path, an error is thrown.
502
- * @throws {Error} If a non-foreign key traversal is found in an infix filter, an error is thrown.
503
- * @throws {Error} If a first step is not found in the combined elements, an error is thrown.
504
- * @throws {Error} If a filter is provided while navigating along non-associations, an error is thrown.
505
- * @throws {Error} If the same element name is inferred more than once, an error is thrown.
506
- *
507
- * @returns {void}
508
- */
509
-
510
- function inferQueryElement(column, insertIntoQueryElements = true, $baseLink = null, context) {
511
- const { inExists, inExpr, inCalcElement, baseColumn, inInfixFilter } = context || {}
512
- if (column.param || column.SELECT) return // parameter references are only resolved into values on execution e.g. :val, :1 or ?
513
- if (column.args) {
514
- applyToFunctionArgs(column.args, inferQueryElement, [false, $baseLink, context])
515
- }
516
- if (column.list) column.list.forEach(arg => inferQueryElement(arg, false, $baseLink, context))
517
- if (column.xpr)
518
- column.xpr.forEach(token => inferQueryElement(token, false, $baseLink, { ...context, inExpr: true })) // e.g. function in expression
519
-
520
- if (!column.ref) {
521
- if (column.expand) queryElements[column.as] = resolveExpand(column)
522
- return
523
- }
367
+ /**
368
+ * This function is responsible for inferring a query element based on a provided column.
369
+ * It initializes and attaches a non-enumerable `$refLinks` property to the column,
370
+ * which stores an array of objects that represent the corresponding artifact of the ref step.
371
+ * Each object in the `$refLinks` array corresponds to the same index position in the `column.ref` array.
372
+ * Based on the leaf artifact (last object in the `$refLinks` array), the query element is inferred.
373
+ *
374
+ * @param {object} arg - The column object that contains the properties to infer a query element.
375
+ * @param {boolean} [queryElements=true] - Determines whether the inferred element should be inserted into the queries elements.
376
+ * For instance, it's set to false when walking over the where clause.
377
+ * @param {object} [$baseLink=null] - A base reference link, usually it's an object with a definition and a target.
378
+ * Used for infix filters, exists <assoc> and nested projections.
379
+ * @param {object} [context={}] - Contextual information for element inference.
380
+ * @param {boolean} [context.inExists=false] - Flag to control the creation of joins for non-association path traversals.
381
+ * for `exists <assoc>` paths we do not need to create joins for path expressions as they are part of the semi-joined subquery.
382
+ * @param {boolean} [context.inExpr=false] - Flag to signal whether the element is part of an expression.
383
+ * Used to ignore non-persisted elements.
384
+ * @param {boolean} [context.inNestedProjection=false] - Flag to signal whether the element is part of a nested projection.
385
+ *
386
+ * Note:
387
+ * - `inExists` is used to specify cases where no joins should be created for non-association path traversals.
388
+ * It is primarily used for infix filters in `exists assoc[parent.foo='bar']`, where it becomes part of a semi-join.
389
+ * - Columns with a `param` property are parameter references resolved into values only at execution time.
390
+ * - Columns with an `args` property are function calls in expressions.
391
+ * - Columns with a `list` property represent a list of values (e.g., for the IN operator).
392
+ * - Columns with a `SELECT` property represent subqueries.
393
+ *
394
+ * @throws {Error} If an unmanaged association is found in an infix filter path, an error is thrown.
395
+ * @throws {Error} If a non-foreign key traversal is found in an infix filter, an error is thrown.
396
+ * @throws {Error} If a first step is not found in the combined elements, an error is thrown.
397
+ * @throws {Error} If a filter is provided while navigating along non-associations, an error is thrown.
398
+ * @throws {Error} If the same element name is inferred more than once, an error is thrown.
399
+ *
400
+ * @returns {void}
401
+ */
524
402
 
525
- init$refLinks(column)
526
- // if any path step points to an artifact with `@cds.persistence.skip`
527
- // we must ignore the element from the queries elements
528
- let isPersisted = true
529
- const firstStepIsTableAlias = column.ref.length > 1 && column.ref[0] in sources
530
- const firstStepIsSelf =
531
- !firstStepIsTableAlias && column.ref.length > 1 && ['$self', '$projection'].includes(column.ref[0])
532
- const expandOnTableAlias = column.ref.length === 1 && column.ref[0] in sources && (column.expand || column.inline)
533
- const nameSegments = []
534
- // if a (segment) of a (structured) foreign key is renamed, we must not include
535
- // the aliased ref segments into the name of the final foreign key which is e.g. used in
536
- // on conditions of joins
537
- const skipAliasedFkSegmentsOfNameStack = []
538
- let pseudoPath = false
539
- column.ref.forEach((step, i) => {
540
- const id = step.id || step
541
- if (i === 0) {
542
- if (id in pseudos.elements) {
543
- // pseudo path
544
- column.$refLinks.push({ definition: pseudos.elements[id], target: pseudos })
545
- pseudoPath = true // only first path step must be well defined
546
- nameSegments.push(id)
547
- } else if ($baseLink) {
548
- const { definition, target } = $baseLink
549
- const elements = getDefinition(definition.target)?.elements || definition.elements
550
- if (elements && id in elements) {
551
- const element = elements[id]
552
- if (inInfixFilter) {
553
- const nextStep = column.ref[1]?.id || column.ref[1]
554
- if (isNonForeignKeyNavigation(element, nextStep)) {
555
- if (inExists) {
556
- Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
557
- } else {
558
- rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
559
- }
560
- }
561
- }
562
- const resolvableIn = getDefinition(definition.target) || target
563
- const $refLink = { definition: elements[id], target: resolvableIn }
564
- column.$refLinks.push($refLink)
565
- } else {
566
- stepNotFoundInPredecessor(id, definition.name)
567
- }
568
- nameSegments.push(id)
569
- } else if (firstStepIsTableAlias) {
570
- column.$refLinks.push({
571
- definition: getDefinitionFromSources(sources, id),
572
- target: getDefinitionFromSources(sources, id),
573
- })
574
- } else if (firstStepIsSelf) {
575
- column.$refLinks.push({ definition: { elements: queryElements }, target: { elements: queryElements } })
576
- } else if (column.ref.length > 1 && inferred.outerQueries?.find(outer => id in outer.sources)) {
577
- // outer query accessed via alias
578
- const outerAlias = inferred.outerQueries.find(outer => id in outer.sources)
579
- column.$refLinks.push({
580
- definition: getDefinitionFromSources(outerAlias.sources, id),
581
- target: getDefinitionFromSources(outerAlias.sources, id),
582
- })
583
- } else if (id in $combinedElements) {
584
- if ($combinedElements[id].length > 1) stepIsAmbiguous(id) // exit
585
- const definition = $combinedElements[id][0].tableAlias.elements[id]
586
- const $refLink = { definition, target: $combinedElements[id][0].tableAlias }
587
- column.$refLinks.push($refLink)
588
- nameSegments.push(id)
589
- } else if (expandOnTableAlias) {
590
- // expand on table alias
591
- column.$refLinks.push({
592
- definition: getDefinitionFromSources(sources, id),
593
- target: getDefinitionFromSources(sources, id),
594
- })
595
- } else {
596
- stepNotFoundInCombinedElements(id) // REVISIT: fails with {__proto__:elements)
597
- }
598
- } else {
599
- const { definition } = column.$refLinks[i - 1]
600
- const elements = getDefinition(definition.target)?.elements || definition.elements //> go for assoc._target first, instead of assoc as struct
601
- const element = elements?.[id]
403
+ function inferArg(arg, queryElements = null, $baseLink = null, context = {}) {
404
+ const { inExists, inExpr, inCalcElement, baseColumn, inInfixFilter, inQueryModifier, inFrom } = context
405
+ if (arg.param || arg.SELECT) return // parameter references are only resolved into values on execution e.g. :val, :1 or ?
406
+ if (arg.args) applyToFunctionArgs(arg.args, inferArg, [null, $baseLink, context])
407
+ if (arg.list) arg.list.forEach(arg => inferArg(arg, null, $baseLink, context))
408
+ if (arg.xpr) arg.xpr.forEach(token => inferArg(token, queryElements, $baseLink, { ...context, inExpr: true })) // e.g. function in expression
602
409
 
603
- if (firstStepIsSelf && element?.isAssociation) {
604
- throw new Error(
605
- `Paths starting with “$self” must not contain steps of type “cds.Association”: ref: [ ${column.ref
606
- .map(idOnly)
607
- .join(', ')} ]`,
608
- )
609
- }
410
+ if (!arg.ref) {
411
+ if (arg.expand && queryElements) queryElements[arg.as] = resolveExpand(arg)
412
+ return
413
+ }
610
414
 
611
- const target = getDefinition(definition.target) || column.$refLinks[i - 1].target
612
- if (element) {
613
- if ($baseLink && inInfixFilter) {
614
- const nextStep = column.ref[i + 1]?.id || column.ref[i + 1]
415
+ // initialize $refLinks
416
+ Object.defineProperty(arg, '$refLinks', {
417
+ value: [],
418
+ writable: true,
419
+ })
420
+ // if any path step points to an artifact with `@cds.persistence.skip`
421
+ // we must ignore the element from the queries elements
422
+ let isPersisted = true
423
+ let firstStepIsTableAlias, firstStepIsSelf, expandOnTableAlias
424
+ if (!inFrom) {
425
+ firstStepIsTableAlias = arg.ref.length > 1 && arg.ref[0] in sources
426
+ firstStepIsSelf = !firstStepIsTableAlias && arg.ref.length > 1 && ['$self', '$projection'].includes(arg.ref[0])
427
+ expandOnTableAlias = arg.ref.length === 1 && arg.ref[0] in sources && (arg.expand || arg.inline)
428
+ }
429
+ const nameSegments = []
430
+ // if a (segment) of a (structured) foreign key is renamed, we must not include
431
+ // the aliased ref segments into the name of the final foreign key which is e.g. used in
432
+ // on conditions of joins
433
+ const skipAliasedFkSegmentsOfNameStack = []
434
+ let pseudoPath = false
435
+ arg.ref.forEach((step, i) => {
436
+ const id = step.id || step
437
+ if (i === 0) {
438
+ if (id in pseudos.elements) {
439
+ // pseudo path
440
+ arg.$refLinks.push({ definition: pseudos.elements[id], target: pseudos })
441
+ pseudoPath = true // only first path step must be well defined
442
+ nameSegments.push(id)
443
+ } else if ($baseLink) {
444
+ const { definition, target } = $baseLink
445
+ const elements = getDefinition(definition.target)?.elements || definition.elements
446
+ if (elements && id in elements) {
447
+ const element = elements[id]
448
+ if (inInfixFilter) {
449
+ const nextStep = arg.ref[1]?.id || arg.ref[1]
615
450
  if (isNonForeignKeyNavigation(element, nextStep)) {
616
451
  if (inExists) {
617
452
  Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
@@ -620,529 +455,589 @@ function infer(originalQuery, model) {
620
455
  }
621
456
  }
622
457
  }
623
- const $refLink = { definition: elements[id], target }
624
- column.$refLinks.push($refLink)
625
- } else if (firstStepIsSelf) {
626
- stepNotFoundInColumnList(id)
627
- } else if (column.ref[0] === '$user' && pseudoPath) {
628
- // `$user.some.unknown.element` -> no error
629
- column.$refLinks.push({ definition: {}, target })
630
- } else if (id === '$dummy') {
631
- // `some.known.element.$dummy` -> no error; used by cds.ql to simulate joins
632
- column.$refLinks.push({ definition: { name: '$dummy', parent: column.$refLinks[i - 1].target } })
633
- Object.defineProperty(column, 'isJoinRelevant', { value: true })
458
+ const resolvableIn = getDefinition(definition.target) || target
459
+ const $refLink = { definition: elements[id], target: resolvableIn }
460
+ arg.$refLinks.push($refLink)
634
461
  } else {
635
- const notFoundIn = pseudoPath ? column.ref[i - 1] : getFullPathForLinkedArg(column)
636
- stepNotFoundInPredecessor(id, notFoundIn)
637
- }
638
- const foreignKeyAlias = Array.isArray(definition.keys)
639
- ? definition.keys.find(k => {
640
- if (k.ref.every((step, j) => column.ref[i + j] === step)) {
641
- skipAliasedFkSegmentsOfNameStack.push(...k.ref.slice(1))
642
- return true
643
- }
644
- return false
645
- })?.as
646
- : null
647
- if (foreignKeyAlias) nameSegments.push(foreignKeyAlias)
648
- else if (skipAliasedFkSegmentsOfNameStack[0] === id) skipAliasedFkSegmentsOfNameStack.shift()
649
- else {
650
- nameSegments.push(firstStepIsSelf && i === 1 ? element.__proto__.name : id)
462
+ stepNotFoundInPredecessor(id, definition.name)
651
463
  }
464
+ nameSegments.push(id)
465
+ } else if (inFrom) {
466
+ const definition = getDefinition(id) || cds.error`"${id}" not found in the definitions of your model`
467
+ arg.$refLinks.push({ definition, target: definition })
468
+ } else if (firstStepIsTableAlias) {
469
+ arg.$refLinks.push({
470
+ definition: getDefinitionFromSources(sources, id),
471
+ target: getDefinitionFromSources(sources, id),
472
+ })
473
+ } else if (firstStepIsSelf) {
474
+ arg.$refLinks.push({ definition: { elements: queryElements }, target: { elements: queryElements } })
475
+ } else if (arg.ref.length > 1 && inferred.outerQueries?.find(outer => id in outer.sources)) {
476
+ // outer query accessed via alias
477
+ const outerAlias = inferred.outerQueries.find(outer => id in outer.sources)
478
+ arg.$refLinks.push({
479
+ definition: getDefinitionFromSources(outerAlias.sources, id),
480
+ target: getDefinitionFromSources(outerAlias.sources, id),
481
+ })
482
+ } else if (id in $combinedElements) {
483
+ if ($combinedElements[id].length > 1) stepIsAmbiguous(id) // exit
484
+ const definition = $combinedElements[id][0].tableAlias.elements[id]
485
+ const $refLink = { definition, target: $combinedElements[id][0].tableAlias }
486
+ arg.$refLinks.push($refLink)
487
+ nameSegments.push(id)
488
+ } else if (expandOnTableAlias) {
489
+ // expand on table alias
490
+ arg.$refLinks.push({
491
+ definition: getDefinitionFromSources(sources, id),
492
+ target: getDefinitionFromSources(sources, id),
493
+ })
494
+ } else {
495
+ stepNotFoundInCombinedElements(id) // REVISIT: fails with {__proto__:elements)
652
496
  }
497
+ } else {
498
+ const { definition } = arg.$refLinks[i - 1]
499
+ const elements = getDefinition(definition.target)?.elements || definition.elements //> go for assoc._target first, instead of assoc as struct
500
+ const element = elements?.[id]
653
501
 
654
- if (step.where) {
655
- const danglingFilter = !(column.ref[i + 1] || column.expand || column.inline || inExists)
656
- if (!column.$refLinks[i].definition.target || danglingFilter)
657
- throw new Error('A filter can only be provided when navigating along associations')
658
- if (!column.expand) Object.defineProperty(column, 'isJoinRelevant', { value: true })
659
- let skipJoinsForFilter = false
660
- step.where.forEach(token => {
661
- if (token === 'exists') {
662
- // books[exists genre[code='A']].title --> column is join relevant but inner exists filter is not
663
- skipJoinsForFilter = true
664
- } else if (token.ref || token.xpr) {
665
- inferQueryElement(token, false, column.$refLinks[i], {
666
- inExists: skipJoinsForFilter || inExists,
667
- inExpr: !!token.xpr,
668
- inInfixFilter: true,
669
- })
670
- } else if (token.func) {
671
- if (token.args) {
672
- applyToFunctionArgs(token.args, inferQueryElement, [
673
- false,
674
- column.$refLinks[i],
675
- { inExists: skipJoinsForFilter || inExists, inExpr: true, inInfixFilter: true },
676
- ])
677
- }
678
- }
679
- })
502
+ if (firstStepIsSelf && element?.isAssociation) {
503
+ throw new Error(
504
+ `Paths starting with “$self” must not contain steps of type “cds.Association”: ref: [ ${arg.ref
505
+ .map(idOnly)
506
+ .join(', ')} ]`,
507
+ )
680
508
  }
681
509
 
682
- column.$refLinks[i].alias = !column.ref[i + 1] && column.as ? column.as : id.split('.').pop()
683
- if (getDefinition(column.$refLinks[i].definition.target)?.['@cds.persistence.skip'] === true)
684
- isPersisted = false
685
- if (!column.ref[i + 1]) {
686
- const flatName = nameSegments.join('_')
687
- Object.defineProperty(column, 'flatName', { value: flatName, writable: true })
688
- // if column is casted, we overwrite it's origin with the new type
689
- if (column.cast) {
690
- const base = getElementForCast(column)
691
- if (insertIntoQueryElements) queryElements[column.as || flatName] = getCopyWithAnnos(column, base)
692
- } else if (column.expand) {
693
- const elements = resolveExpand(column)
694
- let elementName
695
- // expand on table alias
696
- if (column.$refLinks.length === 1 && column.$refLinks[0].definition.kind === 'entity')
697
- elementName = column.$refLinks[0].alias
698
- else elementName = column.as || flatName
699
- if (insertIntoQueryElements) queryElements[elementName] = elements
700
- } else if (column.inline && insertIntoQueryElements) {
701
- const elements = resolveInline(column)
702
- queryElements = { ...queryElements, ...elements }
703
- } else {
704
- // shortcut for `ref: ['$user']` -> `ref: ['$user', 'id']`
705
- const leafArt =
706
- i === 0 && id === '$user' ? column.$refLinks[i].definition.elements.id : column.$refLinks[i].definition
707
- // infer element based on leaf artifact of path
708
- if (insertIntoQueryElements) {
709
- let elementName
710
- if (column.as) {
711
- elementName = column.as
510
+ const target = getDefinition(definition.target) || arg.$refLinks[i - 1].target
511
+ if (element) {
512
+ if ($baseLink && inInfixFilter) {
513
+ const nextStep = arg.ref[i + 1]?.id || arg.ref[i + 1]
514
+ if (isNonForeignKeyNavigation(element, nextStep)) {
515
+ if (inExists) {
516
+ Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
712
517
  } else {
713
- // if the navigation the user has written differs from the final flat ref - e.g. for renamed foreign keys -
714
- // the inferred name of the element equals the flat version of the user-written ref.
715
- const refNavigation = column.ref
716
- .slice(firstStepIsSelf || firstStepIsTableAlias ? 1 : 0)
717
- .map(idOnly)
718
- .join('_')
719
- if (refNavigation !== flatName) elementName = refNavigation
720
- else elementName = flatName
518
+ rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
721
519
  }
722
- if (queryElements[elementName] !== undefined)
723
- throw new Error(`Duplicate definition of element “${elementName}”`)
724
- const element = getCopyWithAnnos(column, leafArt)
725
- queryElements[elementName] = element
726
520
  }
727
521
  }
522
+ const $refLink = { definition: elements[id], target }
523
+ arg.$refLinks.push($refLink)
524
+ } else if (firstStepIsSelf) {
525
+ stepNotFoundInColumnList(id)
526
+ } else if (arg.ref[0] === '$user' && pseudoPath) {
527
+ // `$user.some.unknown.element` -> no error
528
+ arg.$refLinks.push({ definition: {}, target })
529
+ } else if (id === '$dummy') {
530
+ // `some.known.element.$dummy` -> no error; used by cds.ql to simulate joins
531
+ arg.$refLinks.push({ definition: { name: '$dummy', parent: arg.$refLinks[i - 1].target } })
532
+ Object.defineProperty(arg, 'isJoinRelevant', { value: true })
533
+ } else {
534
+ const notFoundIn = pseudoPath ? arg.ref[i - 1] : getFullPathForLinkedArg(arg)
535
+ stepNotFoundInPredecessor(id, notFoundIn)
728
536
  }
729
- })
730
-
731
- // we need inner joins for the path expressions inside filter expressions after exists predicate
732
- if ($baseLink?.pathExpressionInsideFilter) Object.defineProperty(column, 'join', { value: 'inner' })
733
-
734
- // ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
735
- if (column.expand) {
736
- const { $refLinks } = column
737
- const skip = $refLinks.some(link => getDefinition(link.definition.target)?.['@cds.persistence.skip'] === true)
738
- if (skip) {
739
- $refLinks[$refLinks.length - 1].skipExpand = true
740
- return
741
- }
742
- }
743
- const leafArt = column.$refLinks[column.$refLinks.length - 1].definition
744
- const virtual = (leafArt.virtual || !isPersisted) && !inExpr
745
- // check if we need to merge the column `ref` into the join tree of the query
746
- if (!inExists && !virtual && !inCalcElement) {
747
- // for a ref inside an `inline` we need to consider the column `ref` which has the `inline` prop
748
- const colWithBase = baseColumn
749
- ? { ref: [...baseColumn.ref, ...column.ref], $refLinks: [...baseColumn.$refLinks, ...column.$refLinks] }
750
- : column
751
- if (isColumnJoinRelevant(colWithBase)) {
752
- Object.defineProperty(column, 'isJoinRelevant', { value: true })
753
- joinTree.mergeColumn(colWithBase, originalQuery.outerQueries)
537
+ const foreignKeyAlias = Array.isArray(definition.keys)
538
+ ? definition.keys.find(k => {
539
+ if (k.ref.every((step, j) => arg.ref[i + j] === step)) {
540
+ skipAliasedFkSegmentsOfNameStack.push(...k.ref.slice(1))
541
+ return true
542
+ }
543
+ return false
544
+ })?.as
545
+ : null
546
+ if (foreignKeyAlias) nameSegments.push(foreignKeyAlias)
547
+ else if (skipAliasedFkSegmentsOfNameStack[0] === id) skipAliasedFkSegmentsOfNameStack.shift()
548
+ else {
549
+ nameSegments.push(firstStepIsSelf && i === 1 ? element.__proto__.name : id)
754
550
  }
755
551
  }
756
- if (isCalculatedOnRead(leafArt)) {
757
- linkCalculatedElement(column, $baseLink, baseColumn)
758
- }
759
552
 
760
- /**
761
- * Resolves and processes the inline attribute of a column in a database query.
762
- *
763
- * @param {object} col - The column object with properties: `inline` and `$refLinks`.
764
- * @param {string} [namePrefix=col.as || col.flatName] - Prefix for naming new columns. Defaults to `col.as` or `col.flatName`.
765
- * @returns {object} - An object with resolved and processed inline column definitions.
766
- *
767
- * Procedure:
768
- * 1. Iterate through `inline` array. For each `inlineCol`:
769
- * a. If `inlineCol` equals '*', wildcard elements are processed and added to the `elements` object.
770
- * b. If `inlineCol` has inline or expand attributes, corresponding functions are called recursively and the resulting elements are added to the `elements` object.
771
- * c. If `inlineCol` has val or func attributes, new elements are created and added to the `elements` object.
772
- * d. Otherwise, the corresponding `$refLinks` definition is added to the `elements` object.
773
- * 2. Returns the `elements` object.
774
- */
775
- function resolveInline(col, namePrefix = col.as || col.flatName) {
776
- const { inline, $refLinks } = col
777
- const $leafLink = $refLinks[$refLinks.length - 1]
778
- if (!$leafLink.definition.target && !$leafLink.definition.elements) {
779
- throw new Error(
780
- `Unexpected “inline” on “${col.ref.map(idOnly)}”; can only be used after a reference to a structure, association or table alias`,
781
- )
782
- }
783
- let elements = {}
784
- inline.forEach(inlineCol => {
785
- inferQueryElement(inlineCol, false, $leafLink, { inExpr: true, baseColumn: col })
786
- if (inlineCol === '*') {
787
- const wildCardElements = {}
788
- // either the `.elements´ of the struct or the `.elements` of the assoc target
789
- const leafLinkElements =
790
- getDefinition($leafLink.definition.target)?.elements || $leafLink.definition.elements
791
- Object.entries(leafLinkElements).forEach(([k, v]) => {
792
- const name = namePrefix ? `${namePrefix}_${k}` : k
793
- // if overwritten/excluded omit from wildcard elements
794
- // in elements the names are already flat so consider the prefix
795
- // in excluding, the elements are addressed without the prefix
796
- if (!(name in elements || col.excluding?.includes(k))) wildCardElements[name] = v
553
+ if (step.where) {
554
+ const danglingFilter = !(arg.ref[i + 1] || arg.expand || arg.inline || inExists)
555
+ const definition = arg.$refLinks[i].definition
556
+ if ((!definition.target && definition.kind !== 'entity') || (!inFrom && danglingFilter))
557
+ throw new Error('A filter can only be provided when navigating along associations')
558
+ if (!inFrom && !arg.expand) Object.defineProperty(arg, 'isJoinRelevant', { value: true })
559
+ let skipJoinsForFilter = false
560
+ step.where.forEach(token => {
561
+ if (token === 'exists') {
562
+ // books[exists genre[code='A']].title --> column is join relevant but inner exists filter is not
563
+ skipJoinsForFilter = true
564
+ } else if (token.ref || token.xpr || token.list) {
565
+ inferArg(token, false, arg.$refLinks[i], {
566
+ inExists: skipJoinsForFilter || inExists,
567
+ inExpr: !!token.xpr,
568
+ inInfixFilter: true,
569
+ inFrom,
797
570
  })
798
- elements = { ...elements, ...wildCardElements }
799
- } else {
800
- const nameParts = namePrefix ? [namePrefix] : []
801
- if (inlineCol.as) nameParts.push(inlineCol.as)
802
- else nameParts.push(...inlineCol.ref.map(idOnly))
803
- const name = nameParts.join('_')
804
- if (inlineCol.inline) {
805
- const inlineElements = resolveInline(inlineCol, name)
806
- elements = { ...elements, ...inlineElements }
807
- } else if (inlineCol.expand) {
808
- const expandElements = resolveExpand(inlineCol)
809
- elements = { ...elements, [name]: expandElements }
810
- } else if (inlineCol.val) {
811
- elements[name] = { ...getCdsTypeForVal(inlineCol.val) }
812
- } else if (inlineCol.func) {
813
- elements[name] = {}
814
- } else {
815
- elements[name] = inlineCol.$refLinks[inlineCol.$refLinks.length - 1].definition
571
+ } else if (token.func) {
572
+ if (token.args) {
573
+ applyToFunctionArgs(token.args, inferArg, [
574
+ false,
575
+ arg.$refLinks[i],
576
+ { inExists: skipJoinsForFilter || inExists, inExpr: true, inInfixFilter: true, inFrom },
577
+ ])
816
578
  }
817
579
  }
818
580
  })
819
- return elements
820
581
  }
821
582
 
822
- /**
823
- * Resolves a query column which has an `expand` property.
824
- *
825
- * @param {object} col - The column object with properties: `expand` and `$refLinks`.
826
- * @returns {object} - A `cds.struct` object with expanded column definitions.
827
- *
828
- * Procedure:
829
- * - if `$leafLink` is an association, constructs an `expandSubquery` and infers a new query structure.
830
- * Returns a new `cds.struct` if the association has a target cardinality === 1 or a `cds.array` for to many relations.
831
- * - else constructs an `elements` object based on the refs `expand` found in the expand and returns a new `cds.struct` with these `elements`.
832
- */
833
- function resolveExpand(col) {
834
- const { expand, $refLinks } = col
835
- const $leafLink = $refLinks?.[$refLinks.length - 1] || inferred.SELECT.from.$refLinks.at(-1) // fallback to anonymous expand
836
- if (!$leafLink.definition.target && !$leafLink.definition.elements) {
837
- throw new Error(
838
- `Unexpected “expand” on “${col.ref.map(idOnly)}”; can only be used after a reference to a structure, association or table alias`,
839
- )
840
- }
841
- const target = getDefinition($leafLink.definition.target)
842
- if (target) {
843
- const expandSubquery = {
844
- SELECT: {
845
- from: target.name,
846
- columns: expand.filter(c => !c.inline),
847
- },
848
- }
849
- if (col.excluding) expandSubquery.SELECT.excluding = col.excluding
850
- if (col.as) expandSubquery.SELECT.as = col.as
851
- const inferredExpandSubquery = infer(expandSubquery, model)
852
- const res = $leafLink.definition.is2one
853
- ? new cds.struct({ elements: inferredExpandSubquery.elements })
854
- : new cds.array({ items: new cds.struct({ elements: inferredExpandSubquery.elements }) })
855
- return Object.defineProperty(res, '$assocExpand', { value: true })
856
- } else if ($leafLink.definition.elements) {
857
- let elements = {}
858
- expand.forEach(e => {
859
- if (e === '*') {
860
- elements = { ...elements, ...$leafLink.definition.elements }
583
+ arg.$refLinks[i].alias = !arg.ref[i + 1] && arg.as ? arg.as : id.split('.').pop()
584
+ if (getDefinition(arg.$refLinks[i].definition.target)?.['@cds.persistence.skip'] === true) isPersisted = false
585
+ if (!arg.ref[i + 1]) {
586
+ const flatName = nameSegments.join('_')
587
+ Object.defineProperty(arg, 'flatName', { value: flatName, writable: true })
588
+ // if column is casted, we overwrite it's origin with the new type
589
+ if (arg.cast) {
590
+ const base = getElementForCast(arg)
591
+ if (insertIntoQueryElements()) queryElements[arg.as || flatName] = getCopyWithAnnos(arg, base)
592
+ } else if (arg.expand) {
593
+ const elements = resolveExpand(arg)
594
+ let elementName
595
+ // expand on table alias
596
+ if (arg.$refLinks.length === 1 && arg.$refLinks[0].definition.kind === 'entity')
597
+ elementName = arg.$refLinks[0].alias
598
+ else elementName = arg.as || flatName
599
+ if (queryElements) queryElements[elementName] = elements
600
+ } else if (arg.inline && queryElements) {
601
+ const elements = resolveInline(arg)
602
+ Object.assign(queryElements, elements)
603
+ } else {
604
+ // shortcut for `ref: ['$user']` -> `ref: ['$user', 'id']`
605
+ const leafArt =
606
+ i === 0 && id === '$user' ? arg.$refLinks[i].definition.elements.id : arg.$refLinks[i].definition
607
+ // infer element based on leaf artifact of path
608
+ if (insertIntoQueryElements()) {
609
+ let elementName
610
+ if (arg.as) {
611
+ elementName = arg.as
861
612
  } else {
862
- inferQueryElement(e, false, $leafLink, { inExpr: true })
863
- if (e.expand) elements[e.as || e.flatName] = resolveExpand(e)
864
- if (e.inline) elements = { ...elements, ...resolveInline(e) }
865
- else elements[e.as || e.flatName] = e.$refLinks ? e.$refLinks[e.$refLinks.length - 1].definition : e
613
+ // if the navigation the user has written differs from the final flat ref - e.g. for renamed foreign keys -
614
+ // the inferred name of the element equals the flat version of the user-written ref.
615
+ const refNavigation = arg.ref
616
+ .slice(firstStepIsSelf || firstStepIsTableAlias ? 1 : 0)
617
+ .map(idOnly)
618
+ .join('_')
619
+ if (refNavigation !== flatName) elementName = refNavigation
620
+ else elementName = flatName
866
621
  }
867
- })
868
- return new cds.struct({ elements })
622
+ if (queryElements[elementName] !== undefined)
623
+ throw new Error(`Duplicate definition of element “${elementName}”`)
624
+ const element = getCopyWithAnnos(arg, leafArt)
625
+ queryElements[elementName] = element
626
+ }
869
627
  }
870
628
  }
629
+ })
630
+
631
+ // we need inner joins for the path expressions inside filter expressions after exists predicate
632
+ if ($baseLink?.pathExpressionInsideFilter) Object.defineProperty(arg, 'join', { value: 'inner' })
871
633
 
872
- function stepNotFoundInPredecessor(step, def) {
873
- throw new Error(`"${step}" not found in "${def}"`)
634
+ // ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
635
+ if (arg.expand) {
636
+ const { $refLinks } = arg
637
+ const skip = $refLinks.some(link => getDefinition(link.definition.target)?.['@cds.persistence.skip'] === true)
638
+ if (skip) {
639
+ $refLinks[$refLinks.length - 1].skipExpand = true
640
+ return
641
+ }
642
+ }
643
+ const leafArt = arg.$refLinks[arg.$refLinks.length - 1].definition
644
+ const virtual = (leafArt.virtual || !isPersisted) && !inExpr
645
+ // check if we need to merge the column `ref` into the join tree of the query
646
+ if (!inFrom && !inExists && !virtual && !inCalcElement) {
647
+ // for a ref inside an `inline` we need to consider the column `ref` which has the `inline` prop
648
+ const colWithBase = baseColumn
649
+ ? { ref: [...baseColumn.ref, ...arg.ref], $refLinks: [...baseColumn.$refLinks, ...arg.$refLinks] }
650
+ : arg
651
+ if (isColumnJoinRelevant(colWithBase)) {
652
+ Object.defineProperty(arg, 'isJoinRelevant', { value: true })
653
+ joinTree.mergeColumn(colWithBase, originalQuery.outerQueries)
874
654
  }
655
+ }
656
+ if (isCalculatedOnRead(leafArt)) {
657
+ linkCalculatedElement(arg, $baseLink, baseColumn, context)
658
+ }
875
659
 
876
- function stepIsAmbiguous(step) {
660
+ function insertIntoQueryElements() {
661
+ return queryElements && !inExpr && !inInfixFilter && !inQueryModifier
662
+ }
663
+
664
+ /**
665
+ * Resolves and processes the inline attribute of a column in a database query.
666
+ *
667
+ * @param {object} col - The column object with properties: `inline` and `$refLinks`.
668
+ * @param {string} [namePrefix=col.as || col.flatName] - Prefix for naming new columns. Defaults to `col.as` or `col.flatName`.
669
+ * @returns {object} - An object with resolved and processed inline column definitions.
670
+ *
671
+ * Procedure:
672
+ * 1. Iterate through `inline` array. For each `inlineCol`:
673
+ * a. If `inlineCol` equals '*', wildcard elements are processed and added to the `elements` object.
674
+ * b. If `inlineCol` has inline or expand attributes, corresponding functions are called recursively and the resulting elements are added to the `elements` object.
675
+ * c. If `inlineCol` has val or func attributes, new elements are created and added to the `elements` object.
676
+ * d. Otherwise, the corresponding `$refLinks` definition is added to the `elements` object.
677
+ * 2. Returns the `elements` object.
678
+ */
679
+ function resolveInline(col, namePrefix = col.as || col.flatName) {
680
+ const { inline, $refLinks } = col
681
+ const $leafLink = $refLinks[$refLinks.length - 1]
682
+ if (!$leafLink.definition.target && !$leafLink.definition.elements) {
877
683
  throw new Error(
878
- `ambiguous reference to "${step}", write ${Object.values($combinedElements[step])
879
- .map(ta => `"${ta.index}.${step}"`)
880
- .join(', ')} instead`,
684
+ `Unexpected “inline” on “${col.ref.map(idOnly)}”; can only be used after a reference to a structure, association or table alias`,
881
685
  )
882
686
  }
687
+ let elements = {}
688
+ inline.forEach(inlineCol => {
689
+ inferArg(inlineCol, null, $leafLink, { inExpr: true, baseColumn: col })
690
+ if (inlineCol === '*') {
691
+ const wildCardElements = {}
692
+ // either the `.elements´ of the struct or the `.elements` of the assoc target
693
+ const leafLinkElements = getDefinition($leafLink.definition.target)?.elements || $leafLink.definition.elements
694
+ Object.entries(leafLinkElements).forEach(([k, v]) => {
695
+ const name = namePrefix ? `${namePrefix}_${k}` : k
696
+ // if overwritten/excluded omit from wildcard elements
697
+ // in elements the names are already flat so consider the prefix
698
+ // in excluding, the elements are addressed without the prefix
699
+ if (!(name in elements || col.excluding?.includes(k))) wildCardElements[name] = v
700
+ })
701
+ elements = { ...elements, ...wildCardElements }
702
+ } else {
703
+ const nameParts = namePrefix ? [namePrefix] : []
704
+ if (inlineCol.as) nameParts.push(inlineCol.as)
705
+ else nameParts.push(...inlineCol.ref.map(idOnly))
706
+ const name = nameParts.join('_')
707
+ if (inlineCol.inline) {
708
+ const inlineElements = resolveInline(inlineCol, name)
709
+ elements = { ...elements, ...inlineElements }
710
+ } else if (inlineCol.expand) {
711
+ const expandElements = resolveExpand(inlineCol)
712
+ elements = { ...elements, [name]: expandElements }
713
+ } else if (inlineCol.val) {
714
+ elements[name] = { ...getCdsTypeForVal(inlineCol.val) }
715
+ } else if (inlineCol.func) {
716
+ elements[name] = {}
717
+ } else {
718
+ elements[name] = inlineCol.$refLinks[inlineCol.$refLinks.length - 1].definition
719
+ }
720
+ }
721
+ })
722
+ return elements
723
+ }
883
724
 
884
- function stepNotFoundInCombinedElements(step) {
725
+ /**
726
+ * Resolves a query column which has an `expand` property.
727
+ *
728
+ * @param {object} col - The column object with properties: `expand` and `$refLinks`.
729
+ * @returns {object} - A `cds.struct` object with expanded column definitions.
730
+ *
731
+ * Procedure:
732
+ * - if `$leafLink` is an association, constructs an `expandSubquery` and infers a new query structure.
733
+ * Returns a new `cds.struct` if the association has a target cardinality === 1 or a `cds.array` for to many relations.
734
+ * - else constructs an `elements` object based on the refs `expand` found in the expand and returns a new `cds.struct` with these `elements`.
735
+ */
736
+ function resolveExpand(col) {
737
+ const { expand, $refLinks } = col
738
+ const $leafLink = $refLinks?.[$refLinks.length - 1] || inferred.SELECT.from.$refLinks.at(-1) // fallback to anonymous expand
739
+ if (!$leafLink.definition.target && !$leafLink.definition.elements) {
885
740
  throw new Error(
886
- `"${step}" not found in the elements of ${Object.values(sources)
887
- .map(s => s.definition)
888
- .map(def => `"${def.name || /* subquery */ def.as}"`)
889
- .join(', ')}`,
741
+ `Unexpected “expand” on “${col.ref.map(idOnly)}”; can only be used after a reference to a structure, association or table alias`,
890
742
  )
891
743
  }
744
+ const target = getDefinition($leafLink.definition.target)
745
+ if (target) {
746
+ const expandSubquery = {
747
+ SELECT: {
748
+ from: target.name,
749
+ columns: expand.filter(c => !c.inline),
750
+ },
751
+ }
752
+ if (col.excluding) expandSubquery.SELECT.excluding = col.excluding
753
+ if (col.as) expandSubquery.SELECT.as = col.as
754
+ const inferredExpandSubquery = infer(expandSubquery, model)
755
+ const res = $leafLink.definition.is2one
756
+ ? new cds.struct({ elements: inferredExpandSubquery.elements })
757
+ : new cds.array({ items: new cds.struct({ elements: inferredExpandSubquery.elements }) })
758
+ return Object.defineProperty(res, '$assocExpand', { value: true })
759
+ } else if ($leafLink.definition.elements) {
760
+ let elements = {}
761
+ expand.forEach(e => {
762
+ if (e === '*') {
763
+ elements = { ...elements, ...$leafLink.definition.elements }
764
+ } else {
765
+ inferArg(e, false, $leafLink, { inExpr: true })
766
+ if (e.expand) elements[e.as || e.flatName] = resolveExpand(e)
767
+ if (e.inline) elements = { ...elements, ...resolveInline(e) }
768
+ else elements[e.as || e.flatName] = e.$refLinks ? e.$refLinks[e.$refLinks.length - 1].definition : e
769
+ }
770
+ })
771
+ return new cds.struct({ elements })
772
+ }
773
+ }
774
+
775
+ function stepNotFoundInPredecessor(step, def) {
776
+ throw new Error(`"${step}" not found in "${def}"`)
777
+ }
778
+
779
+ function stepIsAmbiguous(step) {
780
+ throw new Error(
781
+ `ambiguous reference to "${step}", write ${Object.values($combinedElements[step])
782
+ .map(ta => `"${ta.index}.${step}"`)
783
+ .join(', ')} instead`,
784
+ )
785
+ }
786
+
787
+ function stepNotFoundInCombinedElements(step) {
788
+ throw new Error(
789
+ `"${step}" not found in the elements of ${Object.values(sources)
790
+ .map(s => s.definition)
791
+ .map(def => `"${def.name || /* subquery */ def.as}"`)
792
+ .join(', ')}`,
793
+ )
794
+ }
892
795
 
893
- function stepNotFoundInColumnList(step) {
894
- const err = [`"${step}" not found in the columns list of query`]
895
- // if the `elt` from a `$self.elt` path is found in the `$combinedElements` -> hint to remove `$self`
896
- if (step in $combinedElements)
897
- err.push(` did you mean ${$combinedElements[step].map(ta => `"${ta.index || ta.as}.${step}"`).join(',')}?`)
898
- throw new Error(err.join(','))
796
+ function stepNotFoundInColumnList(step) {
797
+ const err = [`"${step}" not found in the columns list of query`]
798
+ // if the `elt` from a `$self.elt` path is found in the `$combinedElements` -> hint to remove `$self`
799
+ if (step in $combinedElements)
800
+ err.push(` did you mean ${$combinedElements[step].map(ta => `"${ta.index || ta.as}.${step}"`).join(',')}?`)
801
+ throw new Error(err.join(','))
802
+ }
803
+ }
804
+ function linkCalculatedElement(column, baseLink, baseColumn, context = {}) {
805
+ const calcElement = column.$refLinks?.[column.$refLinks.length - 1].definition || column
806
+ if (alreadySeenCalcElements.has(calcElement)) return
807
+ else alreadySeenCalcElements.add(calcElement)
808
+ const { ref, xpr } = calcElement.value
809
+ if (ref || xpr) {
810
+ baseLink = { definition: calcElement.parent, target: calcElement.parent }
811
+ inferArg(calcElement.value, null, baseLink, { inCalcElement: true, ...context })
812
+ const basePath =
813
+ column.$refLinks?.length > 1
814
+ ? { $refLinks: column.$refLinks.slice(0, -1), ref: column.ref.slice(0, -1) }
815
+ : { $refLinks: [], ref: [] }
816
+ if (baseColumn) {
817
+ basePath.$refLinks.push(...baseColumn.$refLinks)
818
+ basePath.ref.push(...baseColumn.ref)
899
819
  }
820
+ mergePathsIntoJoinTree(calcElement.value, basePath)
900
821
  }
901
- function linkCalculatedElement(column, baseLink, baseColumn) {
902
- const calcElement = column.$refLinks?.[column.$refLinks.length - 1].definition || column
903
- if (alreadySeenCalcElements.has(calcElement)) return
904
- else alreadySeenCalcElements.add(calcElement)
905
- const { ref, xpr } = calcElement.value
906
- if (ref || xpr) {
907
- baseLink = baseLink || { definition: calcElement.parent, target: calcElement.parent }
908
- attachRefLinksToArg(calcElement.value, baseLink, true)
822
+
823
+ if (calcElement.value.args) {
824
+ const processArgument = (arg, calcElement, column) => {
825
+ inferArg(arg, null, { definition: calcElement.parent, target: calcElement.parent }, { inCalcElement: true })
909
826
  const basePath =
910
827
  column.$refLinks?.length > 1
911
828
  ? { $refLinks: column.$refLinks.slice(0, -1), ref: column.ref.slice(0, -1) }
912
829
  : { $refLinks: [], ref: [] }
913
- if (baseColumn) {
914
- basePath.$refLinks.push(...baseColumn.$refLinks)
915
- basePath.ref.push(...baseColumn.ref)
916
- }
917
- mergePathsIntoJoinTree(calcElement.value, basePath)
830
+ mergePathsIntoJoinTree(arg, basePath)
918
831
  }
919
832
 
920
833
  if (calcElement.value.args) {
921
- const processArgument = (arg, calcElement, column) => {
922
- inferQueryElement(
923
- arg,
924
- false,
925
- { definition: calcElement.parent, target: calcElement.parent },
926
- { inCalcElement: true },
927
- )
928
- const basePath =
929
- column.$refLinks?.length > 1
930
- ? { $refLinks: column.$refLinks.slice(0, -1), ref: column.ref.slice(0, -1) }
931
- : { $refLinks: [], ref: [] }
932
- mergePathsIntoJoinTree(arg, basePath)
933
- }
934
-
935
- if (calcElement.value.args) {
936
- applyToFunctionArgs(calcElement.value.args, processArgument, [calcElement, column])
937
- }
938
- }
939
-
940
- /**
941
- * Calculates all paths from a given ref and merges them into the join tree.
942
- * Recursively walks into refs of calculated elements.
943
- *
944
- * @param {object} arg with a ref and sibling $refLinks
945
- * @param {object} basePath with a ref and sibling $refLinks, used for recursion
946
- */
947
- function mergePathsIntoJoinTree(arg, basePath = null) {
948
- basePath = basePath || { $refLinks: [], ref: [] }
949
- if (arg.ref) {
950
- arg.$refLinks.forEach((link, i) => {
951
- const { definition } = link
952
- if (!definition.value) {
953
- basePath.$refLinks.push(link)
954
- basePath.ref.push(arg.ref[i])
955
- }
956
- })
957
- const leafOfCalculatedElementRef = arg.$refLinks[arg.$refLinks.length - 1].definition
958
- if (leafOfCalculatedElementRef.value) mergePathsIntoJoinTree(leafOfCalculatedElementRef.value, basePath)
959
-
960
- mergePathIfNecessary(basePath, arg)
961
- } else if (arg.xpr || arg.args) {
962
- const prop = arg.xpr ? 'xpr' : 'args'
963
- arg[prop].forEach(step => {
964
- const subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
965
- if (step.ref) {
966
- step.$refLinks.forEach((link, i) => {
967
- const { definition } = link
968
- if (definition.value) {
969
- mergePathsIntoJoinTree(definition.value, subPath)
970
- } else {
971
- subPath.$refLinks.push(link)
972
- subPath.ref.push(step.ref[i])
973
- }
974
- })
975
- mergePathIfNecessary(subPath, step)
976
- } else if (step.args || step.xpr) {
977
- const nestedProp = step.xpr ? 'xpr' : 'args'
978
- step[nestedProp].forEach(a => {
979
- mergePathsIntoJoinTree(a, subPath)
980
- })
981
- }
982
- })
983
- }
984
-
985
- function mergePathIfNecessary(p, step) {
986
- const calcElementIsJoinRelevant = isColumnJoinRelevant(p)
987
- if (calcElementIsJoinRelevant) {
988
- if (!calcElement.value.isColumnJoinRelevant)
989
- Object.defineProperty(step, 'isJoinRelevant', { value: true, writable: true })
990
- joinTree.mergeColumn(p, originalQuery.outerQueries)
991
- } else {
992
- // we need to explicitly set the value to false in this case,
993
- // e.g. `SELECT from booksCalc.Books { ID, author.{name }, author {name } }`
994
- // --> for the inline column, the name is join relevant, while for the expand, it is not
995
- Object.defineProperty(step, 'isJoinRelevant', { value: false, writable: true })
996
- }
997
- }
834
+ applyToFunctionArgs(calcElement.value.args, processArgument, [calcElement, column])
998
835
  }
999
836
  }
1000
837
 
1001
838
  /**
1002
- * Checks whether or not the `ref` of the given column is join relevant.
1003
- * A `ref` is considered join relevant if it includes an association traversal and:
1004
- * - the association is unmanaged
1005
- * - a non-foreign key access is performed
1006
- * - an infix filter is applied at the association
839
+ * Calculates all paths from a given ref and merges them into the join tree.
840
+ * Recursively walks into refs of calculated elements.
1007
841
  *
1008
- * @param {object} column the column with the `ref` to check for join relevance
1009
- * @returns {boolean} true if the column ref needs to be merged into a join tree
842
+ * @param {object} arg with a ref and sibling $refLinks
843
+ * @param {object} basePath with a ref and sibling $refLinks, used for recursion
1010
844
  */
1011
- function isColumnJoinRelevant(column) {
1012
- let fkAccess = false
1013
- let assoc = null
1014
- for (let i = 0; i < column.ref.length; i++) {
1015
- const ref = column.ref[i]
1016
- const link = column.$refLinks[i]
1017
- if (link.definition.on && link.definition.isAssociation) {
1018
- if (!column.ref[i + 1]) {
1019
- if (column.expand && assoc) return true
1020
- // if unmanaged assoc is exposed, ignore it
1021
- return false
845
+ function mergePathsIntoJoinTree(arg, basePath = null) {
846
+ basePath = basePath || { $refLinks: [], ref: [] }
847
+ if (arg.ref) {
848
+ arg.$refLinks.forEach((link, i) => {
849
+ const { definition } = link
850
+ if (!definition.value) {
851
+ basePath.$refLinks.push(link)
852
+ basePath.ref.push(arg.ref[i])
1022
853
  }
1023
- return true
1024
- }
1025
- if (assoc) {
1026
- // foreign key access without filters never join relevant
1027
- if (assoc.keys?.some(key => key.ref.every((step, j) => column.ref[i + j] === step))) return false
1028
- // <assoc>.<anotherAssoc>.<…> is join relevant as <anotherAssoc> is not fk of <assoc>
1029
- return true
1030
- }
1031
- if (link.definition.target && link.definition.keys) {
1032
- if (column.ref[i + 1] || assoc) fkAccess = false
1033
- else fkAccess = true
1034
- assoc = link.definition
1035
- if (ref.where) {
1036
- // always join relevant except for expand assoc
1037
- if (column.expand && !column.ref[i + 1]) return false
1038
- return true
854
+ })
855
+ const leafOfCalculatedElementRef = arg.$refLinks[arg.$refLinks.length - 1].definition
856
+ if (leafOfCalculatedElementRef.value) mergePathsIntoJoinTree(leafOfCalculatedElementRef.value, basePath)
857
+
858
+ mergePathIfNecessary(basePath, arg)
859
+ } else if (arg.xpr || arg.args) {
860
+ const prop = arg.xpr ? 'xpr' : 'args'
861
+ arg[prop].forEach(step => {
862
+ const subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
863
+ if (step.ref) {
864
+ step.$refLinks.forEach((link, i) => {
865
+ const { definition } = link
866
+ if (definition.value) {
867
+ mergePathsIntoJoinTree(definition.value, subPath)
868
+ } else {
869
+ subPath.$refLinks.push(link)
870
+ subPath.ref.push(step.ref[i])
871
+ }
872
+ })
873
+ mergePathIfNecessary(subPath, step)
874
+ } else if (step.args || step.xpr) {
875
+ const nestedProp = step.xpr ? 'xpr' : 'args'
876
+ step[nestedProp].forEach(a => {
877
+ mergePathsIntoJoinTree(a, subPath)
878
+ })
1039
879
  }
1040
- }
880
+ })
1041
881
  }
1042
882
 
1043
- if (!assoc) return false
1044
- if (fkAccess) return false
1045
- return true
1046
- }
1047
-
1048
- /**
1049
- * Iterates over all `$combinedElements` of the `query` and puts them into the `query`s `elements`,
1050
- * if there is not already an element with the same name present.
1051
- */
1052
- function inferElementsFromWildCard() {
1053
- const exclude = _.excluding ? x => _.excluding.includes(x) : () => false
1054
-
1055
- if (Object.keys(queryElements).length === 0 && aliases.length === 1) {
1056
- const { elements } = getDefinitionFromSources(sources, aliases[0])
1057
- // only one query source and no overwritten columns
1058
- for (const k of Object.keys(elements)) {
1059
- if (!exclude(k)) {
1060
- const element = elements[k]
1061
- if (element.type !== 'cds.LargeBinary') {
1062
- queryElements[k] = element
1063
- }
1064
- if (isCalculatedOnRead(element)) {
1065
- linkCalculatedElement(element)
1066
- }
1067
- }
883
+ function mergePathIfNecessary(p, step) {
884
+ const calcElementIsJoinRelevant = isColumnJoinRelevant(p)
885
+ if (calcElementIsJoinRelevant) {
886
+ if (!calcElement.value.isJoinRelevant)
887
+ Object.defineProperty(step, 'isJoinRelevant', { value: true, writable: true, })
888
+ joinTree.mergeColumn(p, originalQuery.outerQueries)
889
+ } else {
890
+ // we need to explicitly set the value to false in this case,
891
+ // e.g. `SELECT from booksCalc.Books { ID, author.{name }, author {name } }`
892
+ // --> for the inline column, the name is join relevant, while for the expand, it is not
893
+ Object.defineProperty(step, 'isJoinRelevant', { value: false, writable: true })
1068
894
  }
1069
- return
1070
895
  }
896
+ }
897
+ }
1071
898
 
1072
- const ambiguousElements = {}
1073
- Object.entries($combinedElements).forEach(([name, tableAliases]) => {
1074
- if (Object.keys(tableAliases).length > 1) {
1075
- ambiguousElements[name] = tableAliases
1076
- return ambiguousElements[name]
899
+ /**
900
+ * Checks whether or not the `ref` of the given column is join relevant.
901
+ * A `ref` is considered join relevant if it includes an association traversal and:
902
+ * - the association is unmanaged
903
+ * - a non-foreign key access is performed
904
+ * - an infix filter is applied at the association
905
+ *
906
+ * @param {object} column the column with the `ref` to check for join relevance
907
+ * @returns {boolean} true if the column ref needs to be merged into a join tree
908
+ */
909
+ function isColumnJoinRelevant(column) {
910
+ let fkAccess = false
911
+ let assoc = null
912
+ for (let i = 0; i < column.ref.length; i++) {
913
+ const ref = column.ref[i]
914
+ const link = column.$refLinks[i]
915
+ if (link.definition.on && link.definition.isAssociation) {
916
+ if (!column.ref[i + 1]) {
917
+ if (column.expand && assoc) return true
918
+ // if unmanaged assoc is exposed, ignore it
919
+ return false
1077
920
  }
1078
- if (exclude(name) || name in queryElements) return true
1079
- const element = tableAliases[0].tableAlias.elements[name]
1080
- if (element.type !== 'cds.LargeBinary') queryElements[name] = element
1081
- if (isCalculatedOnRead(element)) {
1082
- linkCalculatedElement(element)
921
+ return true
922
+ }
923
+ if (assoc) {
924
+ // foreign key access without filters never join relevant
925
+ if (assoc.keys?.some(key => key.ref.every((step, j) => column.ref[i + j] === step))) return false
926
+ // <assoc>.<anotherAssoc>.<…> is join relevant as <anotherAssoc> is not fk of <assoc>
927
+ return true
928
+ }
929
+ if (link.definition.target && link.definition.keys) {
930
+ if (column.ref[i + 1] || assoc) fkAccess = false
931
+ else fkAccess = true
932
+ assoc = link.definition
933
+ if (ref.where) {
934
+ // always join relevant except for expand assoc
935
+ if (column.expand && !column.ref[i + 1]) return false
936
+ return true
1083
937
  }
1084
- })
1085
-
1086
- if (Object.keys(ambiguousElements).length > 0) throwAmbiguousWildcardError()
1087
-
1088
- function throwAmbiguousWildcardError() {
1089
- const err = []
1090
- err.push('Ambiguous wildcard elements:')
1091
- Object.keys(ambiguousElements).forEach(name => {
1092
- const tableAliasNames = Object.values(ambiguousElements[name]).map(v => v.index)
1093
- err.push(
1094
- ` select "${name}" explicitly with ${tableAliasNames
1095
- .map(taName => `"${taName}.${name}"`)
1096
- .join(', ')}`,
1097
- )
1098
- })
1099
- throw new Error(err.join('\n'))
1100
938
  }
1101
939
  }
1102
940
 
1103
- /**
1104
- * Returns a new object which is the inferred element for the given `col`.
1105
- * A cast type (via cast function) on the column gets preserved.
1106
- *
1107
- * @param {object} col
1108
- * @returns object
1109
- */
1110
- function getElementForXprOrSubquery(col) {
1111
- const { xpr } = col
1112
- let skipJoins = false
1113
- xpr?.forEach(token => {
1114
- if (token === 'exists') {
1115
- // no joins for infix filters along `exists <path>`
1116
- skipJoins = true
1117
- } else {
1118
- inferQueryElement(token, false, null, { inExists: skipJoins, inExpr: true })
1119
- skipJoins = false
941
+ if (!assoc) return false
942
+ if (fkAccess) return false
943
+ return true
944
+ }
945
+
946
+ /**
947
+ * Iterates over all `$combinedElements` of the `query` and puts them into the `query`s `elements`,
948
+ * if there is not already an element with the same name present.
949
+ */
950
+ function inferElementsFromWildCard(queryElements) {
951
+ const exclude = _.excluding ? x => _.excluding.includes(x) : () => false
952
+
953
+ if (Object.keys(queryElements).length === 0 && aliases.length === 1) {
954
+ const { elements } = getDefinitionFromSources(sources, aliases[0])
955
+ // only one query source and no overwritten columns
956
+ for (const k of Object.keys(elements)) {
957
+ if (!exclude(k)) {
958
+ const element = elements[k]
959
+ if (element.type !== 'cds.LargeBinary') {
960
+ queryElements[k] = element
961
+ }
962
+ if (isCalculatedOnRead(element)) {
963
+ linkCalculatedElement(element)
964
+ }
1120
965
  }
1121
- })
1122
- const base = getElementForCast(col.cast ? col : xpr?.[0] || col)
1123
- if (col.key) base.key = col.key // > preserve key on column
1124
- return getCopyWithAnnos(col, base)
966
+ }
967
+ return
1125
968
  }
1126
969
 
1127
- /**
1128
- * Returns an object with the cast-type defined in the cast of the `thing`.
1129
- * If no cast property is present, it just returns an empty object.
1130
- * The type of the cast is mapped to the `cds` type if possible.
1131
- *
1132
- * @param {object} thing with the cast property
1133
- * @returns {object}
1134
- */
1135
- function getElementForCast(thing) {
1136
- const { cast, $refLinks } = thing
1137
- if (!cast) return {}
1138
- if ($refLinks?.[$refLinks.length - 1].definition.elements)
1139
- // no cast on structure
1140
- cds.error`Structured elements can't be cast to a different type`
1141
- thing.cast = cdsTypes[cast.type] || cast
1142
- return thing.cast
970
+ const ambiguousElements = {}
971
+ Object.entries($combinedElements).forEach(([name, tableAliases]) => {
972
+ if (Object.keys(tableAliases).length > 1) {
973
+ ambiguousElements[name] = tableAliases
974
+ return ambiguousElements[name]
975
+ }
976
+ if (exclude(name) || name in queryElements) return true
977
+ const element = tableAliases[0].tableAlias.elements[name]
978
+ if (element.type !== 'cds.LargeBinary') queryElements[name] = element
979
+ if (isCalculatedOnRead(element)) {
980
+ linkCalculatedElement(element)
981
+ }
982
+ })
983
+
984
+ if (Object.keys(ambiguousElements).length > 0) throwAmbiguousWildcardError()
985
+
986
+ function throwAmbiguousWildcardError() {
987
+ const err = []
988
+ err.push('Ambiguous wildcard elements:')
989
+ Object.keys(ambiguousElements).forEach(name => {
990
+ const tableAliasNames = Object.values(ambiguousElements[name]).map(v => v.index)
991
+ err.push(
992
+ ` select "${name}" explicitly with ${tableAliasNames.map(taName => `"${taName}.${name}"`).join(', ')}`,
993
+ )
994
+ })
995
+ throw new Error(err.join('\n'))
1143
996
  }
1144
997
  }
1145
998
 
999
+ /**
1000
+ * Returns a new object which is the inferred element for the given `col`.
1001
+ * A cast type (via cast function) on the column gets preserved.
1002
+ *
1003
+ * @param {object} col
1004
+ * @returns object
1005
+ */
1006
+ function getElementForXprOrSubquery(col, queryElements) {
1007
+ const { xpr } = col
1008
+ let skipJoins = false
1009
+ xpr?.forEach(token => {
1010
+ if (token === 'exists') {
1011
+ // no joins for infix filters along `exists <path>`
1012
+ skipJoins = true
1013
+ } else {
1014
+ inferArg(token, queryElements, null, { inExists: skipJoins, inExpr: true })
1015
+ skipJoins = false
1016
+ }
1017
+ })
1018
+ const base = getElementForCast(col.cast ? col : xpr?.[0] || col)
1019
+ if (col.key) base.key = col.key // > preserve key on column
1020
+ return getCopyWithAnnos(col, base)
1021
+ }
1022
+
1023
+ /**
1024
+ * Returns an object with the cast-type defined in the cast of the `thing`.
1025
+ * If no cast property is present, it just returns an empty object.
1026
+ * The type of the cast is mapped to the `cds` type if possible.
1027
+ *
1028
+ * @param {object} thing with the cast property
1029
+ * @returns {object}
1030
+ */
1031
+ function getElementForCast(thing) {
1032
+ const { cast, $refLinks } = thing
1033
+ if (!cast) return {}
1034
+ if ($refLinks?.[$refLinks.length - 1].definition.elements)
1035
+ // no cast on structure
1036
+ cds.error`Structured elements can't be cast to a different type`
1037
+ thing.cast = cdsTypes[cast.type] || cast
1038
+ return thing.cast
1039
+ }
1040
+
1146
1041
  /**
1147
1042
  * return a new object based on @param base
1148
1043
  * with all annotations found in @param from
@@ -1165,14 +1060,6 @@ function infer(originalQuery, model) {
1165
1060
  return Object.setPrototypeOf(result, base)
1166
1061
  }
1167
1062
 
1168
- // REVISIT: functions without return are by nature side-effect functions -> bad
1169
- function init$refLinks(arg) {
1170
- Object.defineProperty(arg, '$refLinks', {
1171
- value: [],
1172
- writable: true,
1173
- })
1174
- }
1175
-
1176
1063
  function getCdsTypeForVal(val) {
1177
1064
  // REVISIT: JS null should have a type for proper DB layer conversion logic
1178
1065
  // if(val === null) return {type:'cds.String'}