@cap-js/db-service 1.1.0 → 1.2.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
@@ -318,7 +318,14 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
318
318
  transformedColumns.push(...getTransformedColumns([dollarSelfReplacement]))
319
319
  continue
320
320
  }
321
- handleExpand(col)
321
+ transformedColumns.push(() => {
322
+ const expandResult = handleExpand(col)
323
+ if (expandResult.length > 1) {
324
+ return expandResult
325
+ } else {
326
+ return expandResult[0]
327
+ }
328
+ })
322
329
  } else if (col.inline) {
323
330
  handleInline(col)
324
331
  } else if (col.ref) {
@@ -330,10 +337,34 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
330
337
  handleRef(col)
331
338
  } else if (col === '*') {
332
339
  handleWildcard(columns)
340
+ } else if (col.SELECT) {
341
+ handleSubquery(col)
333
342
  } else {
334
343
  handleDefault(col)
335
344
  }
336
345
  }
346
+ // subqueries are processed in the end
347
+ for (let i = 0; i < transformedColumns.length; i++) {
348
+ const c = transformedColumns[i]
349
+ if (typeof c === 'function') {
350
+ const res = c() || [] // target of expand / subquery could also be skipped -> no result
351
+ if (res.length !== undefined) {
352
+ transformedColumns.splice(i, 1, ...res)
353
+ i += res.length - 1
354
+ } else {
355
+ const replaceWith = res.as
356
+ ? transformedColumns.findIndex(t => (t.as || t.ref?.[t.ref.length - 1]) === res.as)
357
+ : -1
358
+ if (replaceWith === -1) transformedColumns.splice(i, 1, res)
359
+ else {
360
+ transformedColumns.splice(replaceWith, 1, res)
361
+ transformedColumns.splice(i, 1)
362
+ // When removing an element, the next element moves to the current index
363
+ i--
364
+ }
365
+ }
366
+ }
367
+ }
337
368
 
338
369
  if (transformedColumns.length === 0 && columns.length) {
339
370
  handleEmptyColumns(columns)
@@ -341,16 +372,35 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
341
372
 
342
373
  return transformedColumns
343
374
 
375
+ function handleSubquery(col) {
376
+ if (isLocalized(inferred.target)) col.SELECT.localized = true
377
+ if (!col.SELECT.from.as) {
378
+ const uniqueSubqueryAlias = inferred.joinTree.addNextAvailableTableAlias(
379
+ getLastStringSegment(col.SELECT.from.ref[col.SELECT.from.ref.length - 1]),
380
+ originalQuery.outerQueries,
381
+ )
382
+ Object.defineProperty(col.SELECT.from, 'uniqueSubqueryAlias', { value: uniqueSubqueryAlias })
383
+ }
384
+ transformedColumns.push(() => {
385
+ const res = transformSubquery(col)
386
+ if (col.as) res.as = col.as
387
+ return res
388
+ })
389
+ }
390
+
344
391
  function handleExpand(col) {
345
392
  const { $refLinks } = col
393
+ const res = []
346
394
  const last = $refLinks?.[$refLinks.length - 1]
347
395
  if (last && !last.skipExpand && last.definition.isAssociation) {
348
396
  const expandedSubqueryColumn = expandColumn(col)
349
- transformedColumns.push(expandedSubqueryColumn)
397
+ setElementOnColumns(expandedSubqueryColumn, col.element)
398
+ res.push(expandedSubqueryColumn)
350
399
  } else if (!last?.skipExpand) {
351
400
  const expandCols = nestedProjectionOnStructure(col, 'expand')
352
- transformedColumns.push(...expandCols)
401
+ res.push(...expandCols)
353
402
  }
403
+ return res
354
404
  }
355
405
 
356
406
  function handleInline(col) {
@@ -383,10 +433,11 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
383
433
 
384
434
  if (col.$refLinks.some(link => link.definition._target?.['@cds.persistence.skip'] === true)) return
385
435
 
436
+ const getName = col => col.as || col.ref?.at(-1)
386
437
  const flatColumns = getFlatColumnsFor(col, { baseName, columnAlias, tableAlias })
387
438
  flatColumns.forEach(flatColumn => {
388
- const { as } = flatColumn
389
- if (!(as && transformedColumns.some(inserted => inserted?.as === as))) transformedColumns.push(flatColumn)
439
+ const name = getName(flatColumn)
440
+ if (!transformedColumns.some(inserted => getName(inserted) === name)) transformedColumns.push(flatColumn)
390
441
  })
391
442
  }
392
443
 
@@ -404,7 +455,9 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
404
455
  let transformedColumn = getTransformedColumn(col)
405
456
  if (col.as) transformedColumn.as = col.as
406
457
 
407
- const replaceWith = transformedColumns.findIndex(t => (t.as || t.ref[t.ref.length - 1]) === transformedColumn.as)
458
+ const replaceWith = transformedColumns.findIndex(
459
+ t => (t.as || t.ref?.[t.ref.length - 1]) === transformedColumn.as,
460
+ )
408
461
  if (replaceWith === -1) transformedColumns.push(transformedColumn)
409
462
  else transformedColumns.splice(replaceWith, 1, transformedColumn)
410
463
 
@@ -412,17 +465,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
412
465
  }
413
466
 
414
467
  function getTransformedColumn(col) {
415
- if (col.SELECT) {
416
- if (isLocalized(inferred.target)) col.SELECT.localized = true
417
- if (!col.SELECT.from.as) {
418
- const uniqueSubqueryAlias = inferred.joinTree.addNextAvailableTableAlias(
419
- getLastStringSegment(col.SELECT.from.ref[col.SELECT.from.ref.length - 1]),
420
- originalQuery.outerQueries,
421
- )
422
- Object.defineProperty(col.SELECT.from, 'uniqueSubqueryAlias', { value: uniqueSubqueryAlias })
423
- }
424
- return transformSubquery(col)
425
- } else if (col.xpr) {
468
+ if (col.xpr) {
426
469
  return { xpr: getTransformedTokenStream(col.xpr) }
427
470
  } else if (col.func) {
428
471
  return {
@@ -447,7 +490,13 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
447
490
  if (column.$refLinks) {
448
491
  const { $refLinks } = column
449
492
  value = $refLinks[$refLinks.length - 1].definition.value
450
- baseLink = [...column.$refLinks].reverse().find(link => link.definition.isAssociation) || baseLink
493
+ if (column.$refLinks.length > 1) {
494
+ baseLink =
495
+ [...$refLinks].reverse().find($refLink => $refLink.definition.isAssociation) ||
496
+ // if there is no association in the path, the table alias is the base link
497
+ // TA might refer to subquery -> we need to propagate the alias to all paths of the calc element
498
+ column.$refLinks[0]
499
+ }
451
500
  } else {
452
501
  value = column.value
453
502
  }
@@ -460,7 +509,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
460
509
  res = { xpr: getTransformedTokenStream(value.xpr, baseLink) }
461
510
  } else if (val) {
462
511
  res = { val }
463
- } else if (func) res = { args: getTransformedTokenStream(value.args), func: value.func }
512
+ } else if (func) res = { args: getTransformedTokenStream(value.args, baseLink), func: value.func }
464
513
  if (!omitAlias) res.as = column.as || column.name || column.flatName
465
514
  return res
466
515
  }
@@ -499,7 +548,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
499
548
  dollarSelfColumn.ref = [...referencedColumn.ref, ...dollarSelfColumn.ref.slice(2)]
500
549
  Object.defineProperties(dollarSelfColumn, {
501
550
  flatName: {
502
- value: dollarSelfColumn.ref.join('_'),
551
+ value: referencedColumn.$refLinks[0].definition.kind === 'entity' ? dollarSelfColumn.ref.slice(1).join('_') : dollarSelfColumn.ref.join('_'),
503
552
  },
504
553
  isJoinRelevant: {
505
554
  value: referencedColumn.isJoinRelevant,
@@ -544,7 +593,12 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
544
593
  if (nestedProjection.ref) {
545
594
  const augmentedInlineCol = { ...nestedProjection }
546
595
  augmentedInlineCol.ref = col.ref ? [...col.ref, ...nestedProjection.ref] : nestedProjection.ref
547
- if (col.as || nestedProjection.as || nestedProjection.isJoinRelevant) {
596
+ if (
597
+ col.as ||
598
+ nestedProjection.as ||
599
+ nestedProjection.$refLinks[nestedProjection.$refLinks.length - 1].definition.value ||
600
+ nestedProjection.isJoinRelevant
601
+ ) {
548
602
  augmentedInlineCol.as = nameParts.join('_')
549
603
  }
550
604
  Object.defineProperties(augmentedInlineCol, {
@@ -857,7 +911,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
857
911
  const { index, tableAlias } = inferred.$combinedElements[k][0]
858
912
  const element = tableAlias.elements[k]
859
913
  // ignore FK for odata csn / ignore blobs from wildcard expansion
860
- if (isODataFlatForeignKey(element) || (element['@Core.MediaType'] && !element['@Core.IsURL'])) return
914
+ if (isManagedAssocInFlatMode(element) || (element['@Core.MediaType'] && !element['@Core.IsURL'])) return
861
915
  // for wildcard on subquery in from, just reference the elements
862
916
  if (tableAlias.SELECT && !element.elements && !element.target) {
863
917
  wildcardColumns.push(index ? { ref: [index, k] } : { ref: [k] })
@@ -871,13 +925,13 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
871
925
  return wildcardColumns
872
926
 
873
927
  /**
874
- * HACK for odata csn input - foreign keys are already part of the elements in this csn flavor
875
- * not excluding them from the wildcard columns would cause duplicate columns upon foreign key expansion
928
+ * foreign keys are already part of the elements in a flat model
929
+ * not excluding the associations from the wildcard columns would cause duplicate columns upon foreign key expansion
876
930
  * @param {CSN.element} e
877
- * @returns {boolean} true if the element is a flat foreign key generated by the compiler
931
+ * @returns {boolean} true if the element is a managed association and the model is flat
878
932
  */
879
- function isODataFlatForeignKey(e) {
880
- return Boolean(e['@odata.foreignKey4'] || e._foreignKey4)
933
+ function isManagedAssocInFlatMode(e) {
934
+ return model.meta.transformation === 'odata' && e.isAssociation && e.keys
881
935
  }
882
936
  }
883
937
 
@@ -960,6 +1014,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
960
1014
  // we need to provide the correct table alias
961
1015
  tableAlias = getQuerySourceName(replacedBy)
962
1016
 
1017
+ if (replacedBy.expand) return [{ as: baseName }]
1018
+
963
1019
  return getFlatColumnsFor(replacedBy, { baseName, columnAlias: replacedBy.as, tableAlias }, csnPath)
964
1020
  }
965
1021
 
@@ -1013,7 +1069,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1013
1069
  else flatColumn = { ref: [fkBaseName] }
1014
1070
  if (tableAlias) flatColumn.ref.unshift(tableAlias)
1015
1071
 
1016
- setElementOnColumns(flatColumn, fkElement)
1072
+ // in a flat model, we must assign the foreign key rather than the key in the target
1073
+ const flatForeignKey = model.definitions[element.parent.name]?.elements[fkBaseName]
1074
+
1075
+ setElementOnColumns(flatColumn, flatForeignKey || fkElement)
1017
1076
  Object.defineProperty(flatColumn, '_csnPath', { value: csnPath, writable: true })
1018
1077
  flatColumns.push(flatColumn)
1019
1078
  }
@@ -1195,6 +1254,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1195
1254
  let result = is_regexp(token?.val) ? token : copy(token) // REVISIT: too expensive! //
1196
1255
  if (token.ref) {
1197
1256
  const { definition } = token.$refLinks[token.$refLinks.length - 1]
1257
+ // Add definition to result
1258
+ setElementOnColumns(result, definition)
1198
1259
  if (isCalculatedOnRead(definition)) {
1199
1260
  const calculatedElement = resolveCalculatedElement(token, true, $baseLink)
1200
1261
  transformedTokenStream.push(calculatedElement)
@@ -1563,6 +1624,34 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1563
1624
  continue
1564
1625
  }
1565
1626
  const rhs = result[i + 2]
1627
+ if (rhs?.ref || lhs.ref) {
1628
+ // if we have refs on each side of the comparison, we might need to perform tuple expansion
1629
+ // or flatten the structures
1630
+ const refLinkFaker = thing => {
1631
+ const { ref } = thing
1632
+ const assocHost = getParentEntity(assocRefLink.definition)
1633
+ Object.defineProperty(thing, '$refLinks', {
1634
+ value: [],
1635
+ writable: true,
1636
+ })
1637
+ ref.reduce((prev, res, i) => {
1638
+ if (res === '$self')
1639
+ // next is resolvable in entity
1640
+ return prev
1641
+ const definition = prev?.elements?.[res] || prev?._target?.elements[res] || pseudos.elements[res]
1642
+ const target = getParentEntity(definition)
1643
+ thing.$refLinks[i] = { definition, target, alias: definition.name }
1644
+ return prev?.elements?.[res] || prev?._target?.elements[res] || pseudos.elements[res]
1645
+ }, assocHost)
1646
+ }
1647
+
1648
+ // comparison in on condition needs to be expanded...
1649
+ // re-use existing algorithm for that
1650
+ // we need to fake some $refLinks for that to work though...
1651
+ lhs?.ref && !lhs.$refLinks && refLinkFaker(lhs)
1652
+ rhs?.ref && !rhs.$refLinks && refLinkFaker(rhs)
1653
+ }
1654
+
1566
1655
  let backlink
1567
1656
  if (rhs?.ref && lhs?.ref) {
1568
1657
  if (lhs?.ref?.length === 1 && lhs.ref[0] === '$self')
@@ -1576,32 +1665,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1576
1665
  lhs.ref[0] in { $self: true, $projection: true } ? getParentEntity(assocRefLink.definition) : target,
1577
1666
  )
1578
1667
  else {
1579
- // if we have refs on each side of the comparison, we might need to perform tuple expansion
1580
- // or flatten the structures
1581
- // REVISIT: this whole section needs a refactoring, it is too complex and some edge cases may still be not considered...
1582
- const refLinkFaker = thing => {
1583
- const { ref } = thing
1584
- const assocHost = getParentEntity(assocRefLink.definition)
1585
- Object.defineProperty(thing, '$refLinks', {
1586
- value: [],
1587
- writable: true,
1588
- })
1589
- ref.reduce((prev, res, i) => {
1590
- if (res === '$self')
1591
- // next is resolvable in entity
1592
- return prev
1593
- const definition = prev?.elements?.[res] || prev?._target?.elements[res] || pseudos.elements[res]
1594
- const target = getParentEntity(definition)
1595
- thing.$refLinks[i] = { definition, target, alias: definition.name }
1596
- return prev?.elements?.[res] || prev?._target?.elements[res] || pseudos.elements[res]
1597
- }, assocHost)
1598
- }
1599
-
1600
- // comparison in on condition needs to be expanded...
1601
- // re-use existing algorithm for that
1602
- // we need to fake some $refLinks for that to work though...
1603
- lhs?.ref && refLinkFaker(lhs)
1604
- rhs?.ref && refLinkFaker(rhs)
1605
1668
  const lhsLeafArt = lhs.ref && lhs.$refLinks[lhs.$refLinks.length - 1].definition
1606
1669
  const rhsLeafArt = rhs.ref && rhs.$refLinks[rhs.$refLinks.length - 1].definition
1607
1670
  if (lhsLeafArt?.target || rhsLeafArt?.target) {
@@ -1625,7 +1688,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1625
1688
  lhs.$refLinks[0].definition ===
1626
1689
  getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]
1627
1690
  )
1628
- result[i].ref = [result[i].ref[0], lhs.ref.slice(1).join('_')]
1691
+ result[i].ref = [assocRefLink.alias, lhs.ref.slice(1).join('_')]
1629
1692
  // naive assumption: if the path starts with an association which is not the association from
1630
1693
  // which the on-condition originates, it must be a foreign key and hence resolvable in the source
1631
1694
  else if (lhs.$refLinks[0].definition.target) result[i].ref = [result[i].ref.join('_')]
@@ -1876,7 +1939,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1876
1939
  if (node.isJoinRelevant) {
1877
1940
  return getJoinRelevantAlias(node)
1878
1941
  }
1879
-
1880
1942
  return getSelectOrEntityAlias(node) || getCombinedElementAlias(node)
1881
1943
  function getBaseLinkAlias($baseLink) {
1882
1944
  return $baseLink.alias
@@ -189,7 +189,7 @@ const getDeepQueries = (query, dbData, target) => {
189
189
  diff = [diff]
190
190
  }
191
191
 
192
- return _getDeepQueries(diff, target)
192
+ return _getDeepQueries(diff, target, true)
193
193
  }
194
194
 
195
195
  const _hasManagedElements = target => {
@@ -199,9 +199,10 @@ const _hasManagedElements = target => {
199
199
  /**
200
200
  * @param {unknown[]} diff
201
201
  * @param {import('@sap/cds/apis/csn').Definition} target
202
+ * @param {boolean} [root=false]
202
203
  * @returns {import('@sap/cds/apis/cqn').Query[]}
203
204
  */
204
- const _getDeepQueries = (diff, target) => {
205
+ const _getDeepQueries = (diff, target, root = false) => {
205
206
  const queries = []
206
207
 
207
208
  for (const diffEntry of diff) {
@@ -240,7 +241,7 @@ const _getDeepQueries = (diff, target) => {
240
241
  queries.push(INSERT.into(target).entries(diffEntry))
241
242
  } else if (op === 'delete') {
242
243
  queries.push(DELETE.from(target).where(diffEntry))
243
- } else if (op === 'update' || (op === undefined && subQueries.length && _hasManagedElements(target))) {
244
+ } else if (op === 'update' || (op === undefined && (root || subQueries.length) && _hasManagedElements(target))) {
244
245
  // TODO do we need the where here?
245
246
  const keys = target.keys
246
247
  const cqn = UPDATE(target).with(diffEntry)
@@ -23,6 +23,11 @@ const generateUUIDandPropagateKeys = (target, data, event) => {
23
23
  }
24
24
 
25
25
  if (elements[element].is2one || elements[element].is2many) {
26
+ // propagate own foreign keys to propagate further to sub data
27
+ propagateForeignKeys(element, data, elements[element]._foreignKeys, elements[element].isComposition, {
28
+ deleteAssocs: true,
29
+ })
30
+
26
31
  let subData = data[element]
27
32
  if (subData) {
28
33
  if (!Array.isArray(subData)) {
@@ -33,10 +38,6 @@ const generateUUIDandPropagateKeys = (target, data, event) => {
33
38
  generateUUIDandPropagateKeys(elements[element]._target, sub, 'CREATE')
34
39
  }
35
40
  }
36
-
37
- propagateForeignKeys(element, data, elements[element]._foreignKeys, elements[element].isComposition, {
38
- deleteAssocs: true,
39
- })
40
41
  }
41
42
  }
42
43
  }
@@ -451,7 +451,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
451
451
  */
452
452
 
453
453
  function inferQueryElement(column, insertIntoQueryElements = true, $baseLink = null, context) {
454
- const { inExists, inExpr, inNestedProjection, inCalcElement } = context || {}
454
+ const { inExists, inExpr, inNestedProjection, inCalcElement, baseColumn } = context || {}
455
455
  if (column.param) return // parameter references are only resolved into values on execution e.g. :val, :1 or ?
456
456
  if (column.args) column.args.forEach(arg => inferQueryElement(arg, false, $baseLink, context)) // e.g. function in expression
457
457
  if (column.list) column.list.forEach(arg => inferQueryElement(arg, false, $baseLink, context))
@@ -493,11 +493,11 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
493
493
  const elements = definition.elements || definition._target?.elements
494
494
  if (elements && id in elements) {
495
495
  const element = elements[id]
496
- if (!inExists && !inNestedProjection && !inCalcElement && element.target) {
496
+ if (!inNestedProjection && !inCalcElement && element.target) {
497
497
  // only fk access in infix filter
498
498
  const nextStep = column.ref[1]?.id || column.ref[1]
499
499
  // no unmanaged assoc in infix filter path
500
- if (element.on)
500
+ if (!inExists && element.on)
501
501
  throw new Error(
502
502
  `"${element.name}" in path "${column.ref
503
503
  .map(idOnly)
@@ -577,7 +577,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
577
577
  }
578
578
 
579
579
  if (step.where) {
580
- const danglingFilter = !(column.ref[i + 1] || column.expand || inExists)
580
+ const danglingFilter = !(column.ref[i + 1] || column.expand || column.inline || inExists)
581
581
  if (!column.$refLinks[i].definition.target || danglingFilter)
582
582
  throw new Error(/A filter can only be provided when navigating along associations/)
583
583
  if (!column.expand) Object.defineProperty(column, 'isJoinRelevant', { value: true })
@@ -657,16 +657,22 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
657
657
  const leafArt = column.$refLinks[column.$refLinks.length - 1].definition
658
658
  const virtual = (leafArt.virtual || !isPersisted) && !inExpr
659
659
  // check if we need to merge the column `ref` into the join tree of the query
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
- )
665
- Object.defineProperty(column, 'isJoinRelevant', { value: true })
666
- joinTree.mergeColumn(column, $baseLink)
660
+ if (!inExists && !virtual && !inCalcElement) {
661
+ // for a ref inside an `inline` we need to consider the column `ref` which has the `inline` prop
662
+ const colWithBase = baseColumn
663
+ ? { ref: [...baseColumn.ref, ...column.ref], $refLinks: [...baseColumn.$refLinks, ...column.$refLinks] }
664
+ : column
665
+ if (isColumnJoinRelevant(colWithBase)) {
666
+ if (originalQuery.UPDATE)
667
+ throw cds.error(
668
+ 'Path expressions for UPDATE statements are not supported. Use “where exists” with infix filters instead.',
669
+ )
670
+ Object.defineProperty(column, 'isJoinRelevant', { value: true })
671
+ joinTree.mergeColumn(colWithBase, originalQuery.outerQueries)
672
+ }
667
673
  }
668
674
  if (leafArt.value && !leafArt.value.stored) {
669
- resolveCalculatedElement(leafArt, column)
675
+ resolveCalculatedElement(column, $baseLink, baseColumn)
670
676
  }
671
677
 
672
678
  /**
@@ -689,7 +695,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
689
695
  const $leafLink = $refLinks[$refLinks.length - 1]
690
696
  let elements = {}
691
697
  inline.forEach(inlineCol => {
692
- inferQueryElement(inlineCol, false, $leafLink, { inExpr: true, inNestedProjection: true })
698
+ inferQueryElement(inlineCol, false, $leafLink, { inExpr: true, inNestedProjection: true, baseColumn: col })
693
699
  if (inlineCol === '*') {
694
700
  const wildCardElements = {}
695
701
  // either the `.elements´ of the struct or the `.elements` of the assoc target
@@ -795,17 +801,27 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
795
801
  throw new Error(err)
796
802
  }
797
803
  }
798
- function resolveCalculatedElement(calcElement) {
804
+ function resolveCalculatedElement(column, baseLink, baseColumn) {
805
+ const calcElement = column.$refLinks?.[column.$refLinks.length - 1].definition || column
799
806
  if (alreadySeenCalcElements.has(calcElement)) return
800
807
  else alreadySeenCalcElements.add(calcElement)
801
808
  const { ref, xpr, func } = calcElement.value
802
809
  if (ref || xpr) {
803
- attachRefLinksToArg(calcElement.value, { definition: calcElement.parent, target: calcElement.parent }, true)
810
+ baseLink = baseLink || { definition: calcElement.parent, target: calcElement.parent }
811
+ attachRefLinksToArg(calcElement.value, baseLink, true)
812
+ const basePath = { $refLinks: [], ref: [] }
813
+ if (baseColumn) {
814
+ basePath.$refLinks.push(...baseColumn.$refLinks)
815
+ basePath.ref.push(...baseColumn.ref)
816
+ }
804
817
  // column is now fully linked, now we need to find out if we need to merge it into the join tree
805
818
  // for that, we calculate all paths from a calc element and merge them into the join tree
806
- mergePathsIntoJoinTree(calcElement.value)
819
+ mergePathsIntoJoinTree(calcElement.value, basePath)
807
820
  }
808
- if (func) calcElement.value.args?.forEach(arg => inferQueryElement(arg, false)) // {func}.args are optional
821
+ if (func)
822
+ calcElement.value.args?.forEach(arg =>
823
+ inferQueryElement(arg, false, { definition: calcElement.parent, target: calcElement.parent }),
824
+ ) // {func}.args are optional
809
825
  function mergePathsIntoJoinTree(e, basePath = null) {
810
826
  basePath = basePath || { $refLinks: [], ref: [] }
811
827
  if (e.ref) {
@@ -841,8 +857,14 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
841
857
  function mergePathIfNecessary(p, step) {
842
858
  const calcElementIsJoinRelevant = isColumnJoinRelevant(p)
843
859
  if (calcElementIsJoinRelevant) {
844
- if (!calcElement.value.isColumnJoinRelevant) Object.defineProperty(step, 'isJoinRelevant', { value: true })
845
- joinTree.mergeColumn(p)
860
+ if (!calcElement.value.isColumnJoinRelevant)
861
+ Object.defineProperty(step, 'isJoinRelevant', { value: true, writable: true })
862
+ joinTree.mergeColumn(p, originalQuery.outerQueries)
863
+ } else {
864
+ // we need to explicitly set the value to false in this case,
865
+ // e.g. `SELECT from booksCalc.Books { ID, author.{name }, author {name } }`
866
+ // --> for the inline column, the name is join relevant, while for the expand, it is not
867
+ Object.defineProperty(step, 'isJoinRelevant', { value: false, writable: true })
846
868
  }
847
869
  }
848
870
  }
@@ -154,7 +154,7 @@ class JoinTree {
154
154
  * @param {object} col - The column object to be merged into the existing join tree. This object should have the properties $refLinks and ref.
155
155
  * @returns {boolean} - Always returns true, indicating the column has been successfully merged into the join tree.
156
156
  */
157
- mergeColumn(col) {
157
+ mergeColumn(col, outerQueries = null) {
158
158
  if (this.isInitial) this.isInitial = false
159
159
  const head = col.$refLinks[0]
160
160
  let node = this._roots.get(head.alias)
@@ -196,7 +196,7 @@ class JoinTree {
196
196
  } else {
197
197
  child.$refLink.onlyForeignKeyAccess = true
198
198
  }
199
- child.$refLink.alias = this.addNextAvailableTableAlias($refLink.alias)
199
+ child.$refLink.alias = this.addNextAvailableTableAlias($refLink.alias, outerQueries)
200
200
  }
201
201
 
202
202
  const foreignKeys = node.$refLink?.definition.foreignKeys
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "CDS base database service",
5
5
  "homepage": "https://github.com/cap-js/cds-dbs/tree/main/db-service#cds-base-database-service",
6
6
  "repository": {
@@ -26,10 +26,7 @@
26
26
  "npm": ">=8"
27
27
  },
28
28
  "scripts": {
29
- "prettier": "npx prettier --write .",
30
- "test": "npm run build && npx jest --silent",
31
- "build": "tsc && find lib/ -type f -name '*.d.ts' -exec cp '{}' 'dist/{}' ';' && cp ts.eslintrc.cjs dist/.eslintrc.cjs && npx eslint ./dist --ext .d.ts",
32
- "lint": "npx eslint . && npx prettier --check . "
29
+ "test": "jest --silent"
33
30
  },
34
31
  "peerDependencies": {
35
32
  "@sap/cds": ">=7.1.1"