@cap-js/db-service 1.1.0 → 1.2.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.
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,10 @@ 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:
552
+ referencedColumn.$refLinks[0].definition.kind === 'entity'
553
+ ? dollarSelfColumn.ref.slice(1).join('_')
554
+ : dollarSelfColumn.ref.join('_'),
503
555
  },
504
556
  isJoinRelevant: {
505
557
  value: referencedColumn.isJoinRelevant,
@@ -544,7 +596,12 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
544
596
  if (nestedProjection.ref) {
545
597
  const augmentedInlineCol = { ...nestedProjection }
546
598
  augmentedInlineCol.ref = col.ref ? [...col.ref, ...nestedProjection.ref] : nestedProjection.ref
547
- if (col.as || nestedProjection.as || nestedProjection.isJoinRelevant) {
599
+ if (
600
+ col.as ||
601
+ nestedProjection.as ||
602
+ nestedProjection.$refLinks[nestedProjection.$refLinks.length - 1].definition.value ||
603
+ nestedProjection.isJoinRelevant
604
+ ) {
548
605
  augmentedInlineCol.as = nameParts.join('_')
549
606
  }
550
607
  Object.defineProperties(augmentedInlineCol, {
@@ -857,7 +914,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
857
914
  const { index, tableAlias } = inferred.$combinedElements[k][0]
858
915
  const element = tableAlias.elements[k]
859
916
  // ignore FK for odata csn / ignore blobs from wildcard expansion
860
- if (isODataFlatForeignKey(element) || (element['@Core.MediaType'] && !element['@Core.IsURL'])) return
917
+ if (isManagedAssocInFlatMode(element) || (element['@Core.MediaType'] && !element['@Core.IsURL'])) return
861
918
  // for wildcard on subquery in from, just reference the elements
862
919
  if (tableAlias.SELECT && !element.elements && !element.target) {
863
920
  wildcardColumns.push(index ? { ref: [index, k] } : { ref: [k] })
@@ -871,13 +928,13 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
871
928
  return wildcardColumns
872
929
 
873
930
  /**
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
931
+ * foreign keys are already part of the elements in a flat model
932
+ * not excluding the associations from the wildcard columns would cause duplicate columns upon foreign key expansion
876
933
  * @param {CSN.element} e
877
- * @returns {boolean} true if the element is a flat foreign key generated by the compiler
934
+ * @returns {boolean} true if the element is a managed association and the model is flat
878
935
  */
879
- function isODataFlatForeignKey(e) {
880
- return Boolean(e['@odata.foreignKey4'] || e._foreignKey4)
936
+ function isManagedAssocInFlatMode(e) {
937
+ return model.meta.transformation === 'odata' && e.isAssociation && e.keys
881
938
  }
882
939
  }
883
940
 
@@ -960,6 +1017,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
960
1017
  // we need to provide the correct table alias
961
1018
  tableAlias = getQuerySourceName(replacedBy)
962
1019
 
1020
+ if (replacedBy.expand) return [{ as: baseName }]
1021
+
963
1022
  return getFlatColumnsFor(replacedBy, { baseName, columnAlias: replacedBy.as, tableAlias }, csnPath)
964
1023
  }
965
1024
 
@@ -1013,7 +1072,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1013
1072
  else flatColumn = { ref: [fkBaseName] }
1014
1073
  if (tableAlias) flatColumn.ref.unshift(tableAlias)
1015
1074
 
1016
- setElementOnColumns(flatColumn, fkElement)
1075
+ // in a flat model, we must assign the foreign key rather than the key in the target
1076
+ const flatForeignKey = model.definitions[element.parent.name]?.elements[fkBaseName]
1077
+
1078
+ setElementOnColumns(flatColumn, flatForeignKey || fkElement)
1017
1079
  Object.defineProperty(flatColumn, '_csnPath', { value: csnPath, writable: true })
1018
1080
  flatColumns.push(flatColumn)
1019
1081
  }
@@ -1183,7 +1245,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1183
1245
  ) {
1184
1246
  if (notSupportedOps.some(([firstOp]) => firstOp === next))
1185
1247
  cds.error(`The operator "${next}" is not supported for structure comparison`)
1186
- const newTokens = expandComparison(token, ops, rhs)
1248
+ const newTokens = expandComparison(token, ops, rhs, $baseLink)
1187
1249
  const needXpr = Boolean(tokenStream[i - 1] || tokenStream[indexRhs + 1])
1188
1250
  transformedTokenStream.push(...(needXpr ? [asXpr(newTokens)] : newTokens))
1189
1251
  i = indexRhs // jump to next relevant index
@@ -1195,6 +1257,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1195
1257
  let result = is_regexp(token?.val) ? token : copy(token) // REVISIT: too expensive! //
1196
1258
  if (token.ref) {
1197
1259
  const { definition } = token.$refLinks[token.$refLinks.length - 1]
1260
+ // Add definition to result
1261
+ setElementOnColumns(result, definition)
1198
1262
  if (isCalculatedOnRead(definition)) {
1199
1263
  const calculatedElement = resolveCalculatedElement(token, true, $baseLink)
1200
1264
  transformedTokenStream.push(calculatedElement)
@@ -1240,9 +1304,14 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1240
1304
  * @param {object} token with $refLinks
1241
1305
  * @param {string} operator one of allOps
1242
1306
  * @param {object} value either `null` or a column (with `ref` and `$refLinks`)
1307
+ * @param {object} $baseLink optional base `$refLink`, e.g. for infix filters of scoped queries.
1308
+ * In the following example, we must pass `bookshop:Reproduce` as $baseLink for `author`:
1309
+ *
1310
+ * `DELETE.from('bookshop.Reproduce[author = null]:accessGroup')`
1311
+ * ^^^^^^
1243
1312
  * @returns {array}
1244
1313
  */
1245
- function expandComparison(token, operator, value) {
1314
+ function expandComparison(token, operator, value, $baseLink = null) {
1246
1315
  const { definition } = token.$refLinks[token.$refLinks.length - 1]
1247
1316
  let flatRhs
1248
1317
  const result = []
@@ -1305,7 +1374,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1305
1374
  if (!def.$refLinks) return def
1306
1375
  const leaf = def.$refLinks[def.$refLinks.length - 1]
1307
1376
  const first = def.$refLinks[0]
1308
- const tableAlias = getQuerySourceName(def, def.ref.length > 1 && first.definition.isAssociation ? first : null)
1377
+ const tableAlias = getQuerySourceName(
1378
+ def,
1379
+ def.ref.length > 1 && first.definition.isAssociation ? first : $baseLink,
1380
+ )
1309
1381
  if (leaf.definition.parent.kind !== 'entity')
1310
1382
  // we need the base name
1311
1383
  return getFlatColumnsFor(leaf.definition, {
@@ -1563,6 +1635,34 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1563
1635
  continue
1564
1636
  }
1565
1637
  const rhs = result[i + 2]
1638
+ if (rhs?.ref || lhs.ref) {
1639
+ // if we have refs on each side of the comparison, we might need to perform tuple expansion
1640
+ // or flatten the structures
1641
+ const refLinkFaker = thing => {
1642
+ const { ref } = thing
1643
+ const assocHost = getParentEntity(assocRefLink.definition)
1644
+ Object.defineProperty(thing, '$refLinks', {
1645
+ value: [],
1646
+ writable: true,
1647
+ })
1648
+ ref.reduce((prev, res, i) => {
1649
+ if (res === '$self')
1650
+ // next is resolvable in entity
1651
+ return prev
1652
+ const definition = prev?.elements?.[res] || prev?._target?.elements[res] || pseudos.elements[res]
1653
+ const target = getParentEntity(definition)
1654
+ thing.$refLinks[i] = { definition, target, alias: definition.name }
1655
+ return prev?.elements?.[res] || prev?._target?.elements[res] || pseudos.elements[res]
1656
+ }, assocHost)
1657
+ }
1658
+
1659
+ // comparison in on condition needs to be expanded...
1660
+ // re-use existing algorithm for that
1661
+ // we need to fake some $refLinks for that to work though...
1662
+ lhs?.ref && !lhs.$refLinks && refLinkFaker(lhs)
1663
+ rhs?.ref && !rhs.$refLinks && refLinkFaker(rhs)
1664
+ }
1665
+
1566
1666
  let backlink
1567
1667
  if (rhs?.ref && lhs?.ref) {
1568
1668
  if (lhs?.ref?.length === 1 && lhs.ref[0] === '$self')
@@ -1576,32 +1676,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1576
1676
  lhs.ref[0] in { $self: true, $projection: true } ? getParentEntity(assocRefLink.definition) : target,
1577
1677
  )
1578
1678
  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
1679
  const lhsLeafArt = lhs.ref && lhs.$refLinks[lhs.$refLinks.length - 1].definition
1606
1680
  const rhsLeafArt = rhs.ref && rhs.$refLinks[rhs.$refLinks.length - 1].definition
1607
1681
  if (lhsLeafArt?.target || rhsLeafArt?.target) {
@@ -1625,7 +1699,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1625
1699
  lhs.$refLinks[0].definition ===
1626
1700
  getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]
1627
1701
  )
1628
- result[i].ref = [result[i].ref[0], lhs.ref.slice(1).join('_')]
1702
+ result[i].ref = [assocRefLink.alias, lhs.ref.slice(1).join('_')]
1629
1703
  // naive assumption: if the path starts with an association which is not the association from
1630
1704
  // which the on-condition originates, it must be a foreign key and hence resolvable in the source
1631
1705
  else if (lhs.$refLinks[0].definition.target) result[i].ref = [result[i].ref.join('_')]
@@ -1876,7 +1950,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1876
1950
  if (node.isJoinRelevant) {
1877
1951
  return getJoinRelevantAlias(node)
1878
1952
  }
1879
-
1880
1953
  return getSelectOrEntityAlias(node) || getCombinedElementAlias(node)
1881
1954
  function getBaseLinkAlias($baseLink) {
1882
1955
  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.1",
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"