@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/CHANGELOG.md +24 -0
- package/lib/InsertResults.js +2 -1
- package/lib/SQLService.js +64 -52
- package/lib/common/DatabaseService.js +84 -130
- package/lib/common/generic-pool.js +34 -0
- package/lib/common/session-context.js +32 -0
- package/lib/cql-functions.js +14 -1
- package/lib/cqn2sql.js +218 -173
- package/lib/cqn4sql.js +132 -59
- package/lib/deep-queries.js +4 -3
- package/lib/fill-in-keys.js +5 -4
- package/lib/infer/index.js +41 -19
- package/lib/infer/join-tree.js +2 -2
- package/package.json +2 -5
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
|
-
|
|
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
|
-
|
|
397
|
+
setElementOnColumns(expandedSubqueryColumn, col.element)
|
|
398
|
+
res.push(expandedSubqueryColumn)
|
|
350
399
|
} else if (!last?.skipExpand) {
|
|
351
400
|
const expandCols = nestedProjectionOnStructure(col, 'expand')
|
|
352
|
-
|
|
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
|
|
389
|
-
if (!
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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 (
|
|
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 (
|
|
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
|
-
*
|
|
875
|
-
* not excluding
|
|
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
|
|
934
|
+
* @returns {boolean} true if the element is a managed association and the model is flat
|
|
878
935
|
*/
|
|
879
|
-
function
|
|
880
|
-
return
|
|
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
|
-
|
|
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(
|
|
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 = [
|
|
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
|
package/lib/deep-queries.js
CHANGED
|
@@ -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)
|
package/lib/fill-in-keys.js
CHANGED
|
@@ -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
|
}
|
package/lib/infer/index.js
CHANGED
|
@@ -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 (!
|
|
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
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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)
|
|
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)
|
|
845
|
-
|
|
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
|
}
|
package/lib/infer/join-tree.js
CHANGED
|
@@ -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
|
|
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
|
-
"
|
|
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"
|