@cap-js/db-service 1.0.1 → 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
@@ -70,13 +70,14 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
70
70
  // Transform the from clause: association path steps turn into `WHERE EXISTS` subqueries.
71
71
  // The already transformed `where` clause is then glued together with the resulting subqueries.
72
72
  const { transformedWhere, transformedFrom } = getTransformedFrom(from || entity, transformedProp.where)
73
+ const queryNeedsJoins = inferred.joinTree && !inferred.joinTree.isInitial
73
74
 
74
75
  if (inferred.SELECT) {
75
76
  transformedQuery = transformSelectQuery(queryProp, transformedFrom, transformedWhere, transformedQuery)
76
77
  } else {
77
78
  if (from) {
78
79
  transformedProp.from = transformedFrom
79
- } else {
80
+ } else if (!queryNeedsJoins) {
80
81
  transformedProp.entity = transformedFrom
81
82
  }
82
83
 
@@ -94,7 +95,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
94
95
  }
95
96
  }
96
97
 
97
- if (inferred.joinTree && !inferred.joinTree.isInitial) {
98
+ if (queryNeedsJoins) {
98
99
  transformedQuery[kind].from = translateAssocsToJoins(transformedQuery[kind].from)
99
100
  }
100
101
  }
@@ -119,7 +120,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
119
120
  if (columns) {
120
121
  transformedQuery.SELECT.columns = getTransformedColumns(columns)
121
122
  } else {
122
- transformedQuery.SELECT.columns = getColumnsForWildcard()
123
+ transformedQuery.SELECT.columns = getColumnsForWildcard(originalQuery.SELECT?.excluding)
123
124
  }
124
125
 
125
126
  // Like the WHERE clause, aliases from the SELECT list are not accessible for `group by`/`having` (in most DB's)
@@ -158,7 +159,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
158
159
  *
159
160
  * @param {string} kind - The type of operation: "INSERT" or "UPSERT".
160
161
  *
161
- * @returns {Object} - The transformed query with updated `into` clause.
162
+ * @returns {object} - The transformed query with updated `into` clause.
162
163
  */
163
164
  function transformQueryForInsertUpsert(kind) {
164
165
  const { as } = transformedQuery[kind].into
@@ -170,10 +171,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
170
171
  /**
171
172
  * Transforms a stream query, replacing the `where` and `into` clauses after processing.
172
173
  *
173
- * @param {Object} inferred - The inferred object containing the STREAM query.
174
- * @param {Object} transformedQuery - The query object to be transformed.
174
+ * @param {object} inferred - The inferred object containing the STREAM query.
175
+ * @param {object} transformedQuery - The query object to be transformed.
175
176
  *
176
- * @returns {Object} - The transformed query with updated STREAM clauses.
177
+ * @returns {object} - The transformed query with updated STREAM clauses.
177
178
  */
178
179
  function transformStreamQuery() {
179
180
  const { into, where } = inferred.STREAM
@@ -193,8 +194,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
193
194
  /**
194
195
  * Transforms a search expression to a WHERE clause for a SELECT operation.
195
196
  *
196
- * @param {Object} search - The search expression which shall be applied to the searchable columns on the query source.
197
- * @param {Object} from - The FROM clause of the CQN statement.
197
+ * @param {object} search - The search expression which shall be applied to the searchable columns on the query source.
198
+ * @param {object} from - The FROM clause of the CQN statement.
198
199
  *
199
200
  * @returns {(Object|Array|undefined)} - If the target of the query contains searchable elements, the function returns an array that represents the WHERE clause.
200
201
  * If the SELECT query already contains a WHERE clause, this array includes the existing clause and appends an AND condition with the new 'contains' clause.
@@ -293,6 +294,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
293
294
  }
294
295
  }
295
296
 
297
+ function isCalculatedOnRead(def) {
298
+ return def?.value && !def.value.stored
299
+ }
300
+
296
301
  /**
297
302
  * Walks over a list of columns (ref's, xpr, subqueries, val), applies flattening on structured types and expands wildcards.
298
303
  *
@@ -304,13 +309,23 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
304
309
  for (let i = 0; i < columns.length; i++) {
305
310
  const col = columns[i]
306
311
 
307
- if (col.expand) {
312
+ if (isCalculatedOnRead(col.$refLinks?.[col.$refLinks.length - 1].definition)) {
313
+ const calcElement = resolveCalculatedElement(col)
314
+ transformedColumns.push(calcElement)
315
+ } else if (col.expand) {
308
316
  if (col.ref?.length > 1 && col.ref[0] === '$self' && !col.$refLinks[0].definition.kind) {
309
317
  const dollarSelfReplacement = calculateDollarSelfColumn(col)
310
318
  transformedColumns.push(...getTransformedColumns([dollarSelfReplacement]))
311
319
  continue
312
320
  }
313
- 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
+ })
314
329
  } else if (col.inline) {
315
330
  handleInline(col)
316
331
  } else if (col.ref) {
@@ -322,10 +337,34 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
322
337
  handleRef(col)
323
338
  } else if (col === '*') {
324
339
  handleWildcard(columns)
340
+ } else if (col.SELECT) {
341
+ handleSubquery(col)
325
342
  } else {
326
343
  handleDefault(col)
327
344
  }
328
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
+ }
329
368
 
330
369
  if (transformedColumns.length === 0 && columns.length) {
331
370
  handleEmptyColumns(columns)
@@ -333,16 +372,35 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
333
372
 
334
373
  return transformedColumns
335
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
+
336
391
  function handleExpand(col) {
337
392
  const { $refLinks } = col
393
+ const res = []
338
394
  const last = $refLinks?.[$refLinks.length - 1]
339
395
  if (last && !last.skipExpand && last.definition.isAssociation) {
340
396
  const expandedSubqueryColumn = expandColumn(col)
341
- transformedColumns.push(expandedSubqueryColumn)
397
+ setElementOnColumns(expandedSubqueryColumn, col.element)
398
+ res.push(expandedSubqueryColumn)
342
399
  } else if (!last?.skipExpand) {
343
400
  const expandCols = nestedProjectionOnStructure(col, 'expand')
344
- transformedColumns.push(...expandCols)
401
+ res.push(...expandCols)
345
402
  }
403
+ return res
346
404
  }
347
405
 
348
406
  function handleInline(col) {
@@ -375,10 +433,11 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
375
433
 
376
434
  if (col.$refLinks.some(link => link.definition._target?.['@cds.persistence.skip'] === true)) return
377
435
 
436
+ const getName = col => col.as || col.ref?.at(-1)
378
437
  const flatColumns = getFlatColumnsFor(col, { baseName, columnAlias, tableAlias })
379
438
  flatColumns.forEach(flatColumn => {
380
- const { as } = flatColumn
381
- 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)
382
441
  })
383
442
  }
384
443
 
@@ -396,7 +455,9 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
396
455
  let transformedColumn = getTransformedColumn(col)
397
456
  if (col.as) transformedColumn.as = col.as
398
457
 
399
- 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
+ )
400
461
  if (replaceWith === -1) transformedColumns.push(transformedColumn)
401
462
  else transformedColumns.splice(replaceWith, 1, transformedColumn)
402
463
 
@@ -404,17 +465,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
404
465
  }
405
466
 
406
467
  function getTransformedColumn(col) {
407
- if (col.SELECT) {
408
- if (isLocalized(inferred.target)) col.SELECT.localized = true
409
- if (!col.SELECT.from.as) {
410
- const uniqueSubqueryAlias = inferred.joinTree.addNextAvailableTableAlias(
411
- getLastStringSegment(col.SELECT.from.ref[col.SELECT.from.ref.length - 1]),
412
- originalQuery.outerQueries,
413
- )
414
- Object.defineProperty(col.SELECT.from, 'uniqueSubqueryAlias', { value: uniqueSubqueryAlias })
415
- }
416
- return transformSubquery(col)
417
- } else if (col.xpr) {
468
+ if (col.xpr) {
418
469
  return { xpr: getTransformedTokenStream(col.xpr) }
419
470
  } else if (col.func) {
420
471
  return {
@@ -433,6 +484,36 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
433
484
  }
434
485
  }
435
486
 
487
+ function resolveCalculatedElement(column, omitAlias = false, baseLink = null) {
488
+ let value
489
+
490
+ if (column.$refLinks) {
491
+ const { $refLinks } = column
492
+ value = $refLinks[$refLinks.length - 1].definition.value
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
+ }
500
+ } else {
501
+ value = column.value
502
+ }
503
+ const { ref, val, xpr, func } = value
504
+
505
+ let res
506
+ if (ref) {
507
+ res = getTransformedTokenStream([value], baseLink)[0]
508
+ } else if (xpr) {
509
+ res = { xpr: getTransformedTokenStream(value.xpr, baseLink) }
510
+ } else if (val) {
511
+ res = { val }
512
+ } else if (func) res = { args: getTransformedTokenStream(value.args, baseLink), func: value.func }
513
+ if (!omitAlias) res.as = column.as || column.name || column.flatName
514
+ return res
515
+ }
516
+
436
517
  /**
437
518
  * This function resolves a `ref` starting with a `$self`.
438
519
  * Such a path targets another element of the query by it's implicit, or explicit alias.
@@ -467,7 +548,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
467
548
  dollarSelfColumn.ref = [...referencedColumn.ref, ...dollarSelfColumn.ref.slice(2)]
468
549
  Object.defineProperties(dollarSelfColumn, {
469
550
  flatName: {
470
- value: dollarSelfColumn.ref.join('_'),
551
+ value: referencedColumn.$refLinks[0].definition.kind === 'entity' ? dollarSelfColumn.ref.slice(1).join('_') : dollarSelfColumn.ref.join('_'),
471
552
  },
472
553
  isJoinRelevant: {
473
554
  value: referencedColumn.isJoinRelevant,
@@ -512,7 +593,12 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
512
593
  if (nestedProjection.ref) {
513
594
  const augmentedInlineCol = { ...nestedProjection }
514
595
  augmentedInlineCol.ref = col.ref ? [...col.ref, ...nestedProjection.ref] : nestedProjection.ref
515
- 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
+ ) {
516
602
  augmentedInlineCol.as = nameParts.join('_')
517
603
  }
518
604
  Object.defineProperties(augmentedInlineCol, {
@@ -632,7 +718,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
632
718
  *
633
719
  * @param {CSN.column} column - The column with the 'expand' property to be transformed into a subquery.
634
720
  *
635
- * @returns {Object} Returns a subquery correlated with the enclosing query, with added properties `expand:true` and `one:true|false`.
721
+ * @returns {object} Returns a subquery correlated with the enclosing query, with added properties `expand:true` and `one:true|false`.
636
722
  */
637
723
  function expandColumn(column) {
638
724
  let outerAlias
@@ -726,7 +812,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
726
812
  const res = []
727
813
  for (let i = 0; i < columns.length; i++) {
728
814
  const col = columns[i]
729
- if (col.isJoinRelevant) {
815
+ if (isCalculatedOnRead(col.$refLinks?.[col.$refLinks.length - 1].definition)) {
816
+ const calcElement = resolveCalculatedElement(col, true)
817
+ res.push(calcElement)
818
+ } else if (col.isJoinRelevant) {
730
819
  const tableAlias$refLink = getQuerySourceName(col)
731
820
  const transformedColumn = {
732
821
  ref: [tableAlias$refLink, getFullName(col.$refLinks[col.$refLinks.length - 1].definition)],
@@ -822,10 +911,12 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
822
911
  const { index, tableAlias } = inferred.$combinedElements[k][0]
823
912
  const element = tableAlias.elements[k]
824
913
  // ignore FK for odata csn / ignore blobs from wildcard expansion
825
- if (isODataFlatForeignKey(element) || (element['@Core.MediaType'] && !element['@Core.IsURL'])) return
914
+ if (isManagedAssocInFlatMode(element) || (element['@Core.MediaType'] && !element['@Core.IsURL'])) return
826
915
  // for wildcard on subquery in from, just reference the elements
827
916
  if (tableAlias.SELECT && !element.elements && !element.target) {
828
917
  wildcardColumns.push(index ? { ref: [index, k] } : { ref: [k] })
918
+ } else if (isCalculatedOnRead(element)) {
919
+ wildcardColumns.push(resolveCalculatedElement(element))
829
920
  } else {
830
921
  const flatColumns = getFlatColumnsFor(element, { tableAlias: index }, [], { exclude, replace }, true)
831
922
  wildcardColumns.push(...flatColumns)
@@ -834,13 +925,13 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
834
925
  return wildcardColumns
835
926
 
836
927
  /**
837
- * HACK for odata csn input - foreign keys are already part of the elements in this csn flavor
838
- * 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
839
930
  * @param {CSN.element} e
840
- * @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
841
932
  */
842
- function isODataFlatForeignKey(e) {
843
- return Boolean(e['@odata.foreignKey4'] || e._foreignKey4)
933
+ function isManagedAssocInFlatMode(e) {
934
+ return model.meta.transformation === 'odata' && e.isAssociation && e.keys
844
935
  }
845
936
  }
846
937
 
@@ -923,6 +1014,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
923
1014
  // we need to provide the correct table alias
924
1015
  tableAlias = getQuerySourceName(replacedBy)
925
1016
 
1017
+ if (replacedBy.expand) return [{ as: baseName }]
1018
+
926
1019
  return getFlatColumnsFor(replacedBy, { baseName, columnAlias: replacedBy.as, tableAlias }, csnPath)
927
1020
  }
928
1021
 
@@ -976,7 +1069,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
976
1069
  else flatColumn = { ref: [fkBaseName] }
977
1070
  if (tableAlias) flatColumn.ref.unshift(tableAlias)
978
1071
 
979
- 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)
980
1076
  Object.defineProperty(flatColumn, '_csnPath', { value: csnPath, writable: true })
981
1077
  flatColumns.push(flatColumn)
982
1078
  }
@@ -1157,6 +1253,14 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1157
1253
 
1158
1254
  let result = is_regexp(token?.val) ? token : copy(token) // REVISIT: too expensive! //
1159
1255
  if (token.ref) {
1256
+ const { definition } = token.$refLinks[token.$refLinks.length - 1]
1257
+ // Add definition to result
1258
+ setElementOnColumns(result, definition)
1259
+ if (isCalculatedOnRead(definition)) {
1260
+ const calculatedElement = resolveCalculatedElement(token, true, $baseLink)
1261
+ transformedTokenStream.push(calculatedElement)
1262
+ continue
1263
+ }
1160
1264
  if (token.ref.length > 1 && token.ref[0] === '$self' && !token.$refLinks[0].definition.kind) {
1161
1265
  const dollarSelfReplacement = [calculateDollarSelfColumn(token, true)]
1162
1266
  transformedTokenStream.push(...getTransformedTokenStream(dollarSelfReplacement))
@@ -1472,11 +1576,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1472
1576
  each.ref[0] in { $self: true, $projection: true } ? getParentEntity(assoc) : target,
1473
1577
  ),
1474
1578
  )
1475
-
1476
- function getParentEntity(element) {
1477
- if (element.kind === 'entity') return element
1478
- else return getParentEntity(element.parent)
1479
- }
1480
1579
  }
1481
1580
 
1482
1581
  /**
@@ -1525,6 +1624,34 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1525
1624
  continue
1526
1625
  }
1527
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
+
1528
1655
  let backlink
1529
1656
  if (rhs?.ref && lhs?.ref) {
1530
1657
  if (lhs?.ref?.length === 1 && lhs.ref[0] === '$self')
@@ -1538,32 +1665,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1538
1665
  lhs.ref[0] in { $self: true, $projection: true } ? getParentEntity(assocRefLink.definition) : target,
1539
1666
  )
1540
1667
  else {
1541
- // if we have refs on each side of the comparison, we might need to perform tuple expansion
1542
- // or flatten the structures
1543
- // REVISIT: this whole section needs a refactoring, it is too complex and some edge cases may still be not considered...
1544
- const refLinkFaker = thing => {
1545
- const { ref } = thing
1546
- const assocHost = getParentEntity(assocRefLink.definition)
1547
- Object.defineProperty(thing, '$refLinks', {
1548
- value: [],
1549
- writable: true,
1550
- })
1551
- ref.reduce((prev, res, i) => {
1552
- if (res === '$self')
1553
- // next is resolvable in entity
1554
- return prev
1555
- const definition = prev?.elements?.[res] || prev?._target?.elements[res] || pseudos.elements[res]
1556
- const target = getParentEntity(definition)
1557
- thing.$refLinks[i] = { definition, target, alias: definition.name }
1558
- return prev?.elements?.[res] || prev?._target?.elements[res] || pseudos.elements[res]
1559
- }, assocHost)
1560
- }
1561
-
1562
- // comparison in on condition needs to be expanded...
1563
- // re-use existing algorithm for that
1564
- // we need to fake some $refLinks for that to work though...
1565
- lhs?.ref && refLinkFaker(lhs)
1566
- rhs?.ref && refLinkFaker(rhs)
1567
1668
  const lhsLeafArt = lhs.ref && lhs.$refLinks[lhs.$refLinks.length - 1].definition
1568
1669
  const rhsLeafArt = rhs.ref && rhs.$refLinks[rhs.$refLinks.length - 1].definition
1569
1670
  if (lhsLeafArt?.target || rhsLeafArt?.target) {
@@ -1587,7 +1688,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1587
1688
  lhs.$refLinks[0].definition ===
1588
1689
  getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]
1589
1690
  )
1590
- result[i].ref = [result[i].ref[0], lhs.ref.slice(1).join('_')]
1691
+ result[i].ref = [assocRefLink.alias, lhs.ref.slice(1).join('_')]
1591
1692
  // naive assumption: if the path starts with an association which is not the association from
1592
1693
  // which the on-condition originates, it must be a foreign key and hence resolvable in the source
1593
1694
  else if (lhs.$refLinks[0].definition.target) result[i].ref = [result[i].ref.join('_')]
@@ -1838,7 +1939,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
1838
1939
  if (node.isJoinRelevant) {
1839
1940
  return getJoinRelevantAlias(node)
1840
1941
  }
1841
-
1842
1942
  return getSelectOrEntityAlias(node) || getCombinedElementAlias(node)
1843
1943
  function getBaseLinkAlias($baseLink) {
1844
1944
  return $baseLink.alias
@@ -1920,6 +2020,11 @@ function getLastStringSegment(str) {
1920
2020
  return index != -1 ? str.substring(index + 1) : str
1921
2021
  }
1922
2022
 
2023
+ function getParentEntity(element) {
2024
+ if (element.kind === 'entity') return element
2025
+ else return getParentEntity(element.parent)
2026
+ }
2027
+
1923
2028
  /**
1924
2029
  * Assigns the given `element` as non-enumerable property 'element' onto `col`.
1925
2030
  *
@@ -4,6 +4,17 @@ const { _target_name4 } = require('./SQLService')
4
4
 
5
5
  const handledDeep = Symbol('handledDeep')
6
6
 
7
+ /**
8
+ * @callback nextCallback
9
+ * @param {Error|undefined} error
10
+ * @returns {Promise<unknown>}
11
+ */
12
+
13
+ /**
14
+ * @param {import('@sap/cds/apis/services').Request} req
15
+ * @param {nextCallback} next
16
+ * @returns {Promise<number>}
17
+ */
7
18
  async function onDeep(req, next) {
8
19
  const { query } = req
9
20
  // REVISIT: req.target does not match the query.INSERT target for path insert
@@ -136,11 +147,16 @@ const _calculateExpandColumns = (target, data, expandColumns = [], elementMap =
136
147
  }
137
148
  }
138
149
 
150
+ /**
151
+ * @param {import('@sap/cds/apis/cqn').Query} query
152
+ * @param {import('@sap/cds/apis/csn').Definition} target
153
+ */
139
154
  const getExpandForDeep = (query, target) => {
140
155
  const from = query.DELETE?.from || query.UPDATE?.entity
141
156
  const data = query.UPDATE?.data || null
142
157
  const where = query.DELETE?.where || query.UPDATE?.where
143
158
 
159
+ /** @type {import("@sap/cds/apis/ql").SELECT<unknown>} */
144
160
  const cqn = SELECT.from(from)
145
161
  if (where) cqn.SELECT.where = where
146
162
 
@@ -150,6 +166,12 @@ const getExpandForDeep = (query, target) => {
150
166
  return cqn
151
167
  }
152
168
 
169
+ /**
170
+ * @param {import('@sap/cds/apis/cqn').Query} query
171
+ * @param {unknown[]} dbData
172
+ * @param {import('@sap/cds/apis/csn').Definition} target
173
+ * @returns
174
+ */
153
175
  const getDeepQueries = (query, dbData, target) => {
154
176
  let queryData
155
177
  if (query.INSERT) {
@@ -167,14 +189,20 @@ const getDeepQueries = (query, dbData, target) => {
167
189
  diff = [diff]
168
190
  }
169
191
 
170
- return _getDeepQueries(diff, target)
192
+ return _getDeepQueries(diff, target, true)
171
193
  }
172
194
 
173
195
  const _hasManagedElements = target => {
174
196
  return Object.keys(target.elements).filter(elementName => target.elements[elementName]['@cds.on.update']).length > 0
175
197
  }
176
198
 
177
- const _getDeepQueries = (diff, target) => {
199
+ /**
200
+ * @param {unknown[]} diff
201
+ * @param {import('@sap/cds/apis/csn').Definition} target
202
+ * @param {boolean} [root=false]
203
+ * @returns {import('@sap/cds/apis/cqn').Query[]}
204
+ */
205
+ const _getDeepQueries = (diff, target, root = false) => {
178
206
  const queries = []
179
207
 
180
208
  for (const diffEntry of diff) {
@@ -213,7 +241,7 @@ const _getDeepQueries = (diff, target) => {
213
241
  queries.push(INSERT.into(target).entries(diffEntry))
214
242
  } else if (op === 'delete') {
215
243
  queries.push(DELETE.from(target).where(diffEntry))
216
- } else if (op === 'update' || (op === undefined && subQueries.length && _hasManagedElements(target))) {
244
+ } else if (op === 'update' || (op === undefined && (root || subQueries.length) && _hasManagedElements(target))) {
217
245
  // TODO do we need the where here?
218
246
  const keys = target.keys
219
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,14 +38,20 @@ 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
  }
43
44
 
45
+ /**
46
+ * @callback nextCallback
47
+ * @param {Error|undefined} error
48
+ * @returns {Promise<unknown>}
49
+ */
50
+
51
+ /**
52
+ * @param {import('@sap/cds/apis/services').Request} req
53
+ * @param {nextCallback} next
54
+ */
44
55
  module.exports = async function fill_in_keys(req, next) {
45
56
  // REVISIT dummy handler until we have input processing
46
57
  if (!req.target || !this.model || req.target._unresolved) return next()
@@ -0,0 +1,45 @@
1
+ import * as cqn from '@sap/cds/apis/cqn'
2
+ import * as csn from '@sap/cds/apis/csn'
3
+
4
+ type linkedQuery = {
5
+ target: csn.Definition
6
+ elements: elements
7
+ }
8
+ export type SELECT = cqn.SELECT & linkedQuery
9
+ export type INSERT = cqn.INSERT & linkedQuery
10
+ export type UPSERT = cqn.UPSERT & linkedQuery
11
+ export type UPDATE = cqn.UPDATE & linkedQuery
12
+ export type STREAM = {
13
+ STREAM: { into: cqn.name | ref; data: ReadableStream<Buffer>; from: cqn.source; column?: ref; where: predicate }
14
+ } & linkedQuery
15
+ export type DELETE = cqn.DELETE & linkedQuery
16
+ export type CREATE = cqn.CREATE & linkedQuery
17
+ export type DROP = cqn.DROP & linkedQuery
18
+
19
+ export type Query = SELECT | INSERT | UPSERT | UPDATE | DELETE | CREATE | DROP
20
+
21
+ export type element = csn.Element & {
22
+ key?: boolean
23
+ virtual?: boolean
24
+ unique?: boolean
25
+ notNull?: boolean
26
+ }
27
+ export type elements = {
28
+ [name: string]: element
29
+ }
30
+
31
+ export type col = cqn.column_expr & { element: element }
32
+
33
+ export type list = {
34
+ list: cqn.expr[]
35
+ }
36
+ // Passthrough
37
+ export type source = cqn.source
38
+ export type ref = cqn.ref
39
+ export type val = cqn.val
40
+ export type xpr = cqn.xpr
41
+ export type expr = cqn.expr
42
+ export type func = cqn.function_call
43
+ export type predicate = cqn.predicate
44
+ export type ordering_term = cqn.ordering_term
45
+ export type limit = { rows: val; offset: val }