@cap-js/db-service 2.8.2 → 2.10.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/CHANGELOG.md +35 -0
- package/lib/InsertResults.js +6 -3
- package/lib/SQLService.js +88 -50
- package/lib/cql-functions.js +1 -1
- package/lib/cqn2pql.js +116 -0
- package/lib/cqn2sql.js +131 -48
- package/lib/cqn4sql.js +588 -180
- package/lib/infer/index.js +77 -16
- package/lib/infer/join-tree.js +8 -6
- package/lib/utils.js +29 -0
- package/package.json +2 -2
package/lib/infer/index.js
CHANGED
|
@@ -4,14 +4,14 @@ const cds = require('@sap/cds')
|
|
|
4
4
|
|
|
5
5
|
const JoinTree = require('./join-tree')
|
|
6
6
|
const { pseudos } = require('./pseudos')
|
|
7
|
-
const { isCalculatedOnRead, getImplicitAlias, getModelUtils, defineProperty, hasOwnSkip } = require('../utils')
|
|
7
|
+
const { isCalculatedOnRead, getImplicitAlias, getModelUtils, defineProperty, hasOwnSkip, isRuntimeView } = require('../utils')
|
|
8
8
|
const cdsTypes = cds.builtin.types
|
|
9
9
|
/**
|
|
10
10
|
* @param {import('@sap/cds/apis/cqn').Query|string} originalQuery
|
|
11
11
|
* @param {import('@sap/cds/apis/csn').CSN} [model]
|
|
12
12
|
* @returns {import('./cqn').Query} = q with .target and .elements
|
|
13
13
|
*/
|
|
14
|
-
function infer(originalQuery, model) {
|
|
14
|
+
function infer(originalQuery, model, useTechnicalAlias = true) {
|
|
15
15
|
if (!model) throw new Error('Please specify a model')
|
|
16
16
|
const inferred = originalQuery
|
|
17
17
|
|
|
@@ -34,7 +34,7 @@ function infer(originalQuery, model) {
|
|
|
34
34
|
|
|
35
35
|
let $combinedElements
|
|
36
36
|
|
|
37
|
-
const sources = inferTarget(_.into || _.from || _.entity, {}) // IMPORTANT: _.into has to go before _.from for INSERT.into().from(SELECT)
|
|
37
|
+
const sources = inferTarget(_.into || _.from || _.entity, {}, useTechnicalAlias) // IMPORTANT: _.into has to go before _.from for INSERT.into().from(SELECT)
|
|
38
38
|
const aliases = Object.keys(sources)
|
|
39
39
|
const target = aliases.length === 1 ? getDefinitionFromSources(sources, aliases[0]) : originalQuery
|
|
40
40
|
Object.defineProperties(inferred, {
|
|
@@ -80,7 +80,7 @@ function infer(originalQuery, model) {
|
|
|
80
80
|
* Each key is a query source alias, and its value is the corresponding CSN Definition.
|
|
81
81
|
* @returns {object} The updated `querySources` object with inferred sources from the `from` clause.
|
|
82
82
|
*/
|
|
83
|
-
function inferTarget(from, querySources, useTechnicalAlias
|
|
83
|
+
function inferTarget(from, querySources, useTechnicalAlias) {
|
|
84
84
|
const { ref } = from
|
|
85
85
|
// Given a from clause `Root:parent[$main.name = name].parent as Foo`
|
|
86
86
|
// we need to first resolve until to the last step of the from.ref
|
|
@@ -191,6 +191,7 @@ function infer(originalQuery, model) {
|
|
|
191
191
|
const dollarSelfRefs = []
|
|
192
192
|
columns.forEach(col => {
|
|
193
193
|
if (col === '*') {
|
|
194
|
+
if (wildcardSelect) throw new Error('Duplicate wildcard "*" in column list')
|
|
194
195
|
wildcardSelect = true
|
|
195
196
|
} else if (col.val !== undefined || col.xpr || col.SELECT || col.func || col.param) {
|
|
196
197
|
const as = col.as || col.func || col.val
|
|
@@ -459,10 +460,10 @@ function infer(originalQuery, model) {
|
|
|
459
460
|
const element = elements[id]
|
|
460
461
|
if (inInfixFilter) {
|
|
461
462
|
const nextStep = arg.ref[1]?.id || arg.ref[1]
|
|
462
|
-
if (isNonForeignKeyNavigation(element, nextStep)) {
|
|
463
|
+
if (isNonForeignKeyNavigation(element, nextStep) || arg.ref[0]?.where) {
|
|
463
464
|
if (inExists) {
|
|
464
465
|
defineProperty($baseLink, 'pathExpressionInsideFilter', true)
|
|
465
|
-
} else {
|
|
466
|
+
} else if (!inFrom) {
|
|
466
467
|
rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
|
|
467
468
|
}
|
|
468
469
|
}
|
|
@@ -523,10 +524,10 @@ function infer(originalQuery, model) {
|
|
|
523
524
|
if (element) {
|
|
524
525
|
if ($baseLink && inInfixFilter) {
|
|
525
526
|
const nextStep = arg.ref[i + 1]?.id || arg.ref[i + 1]
|
|
526
|
-
if (isNonForeignKeyNavigation(element, nextStep)) {
|
|
527
|
+
if (isNonForeignKeyNavigation(element, nextStep) || arg.ref[i-1]?.where) {
|
|
527
528
|
if (inExists) {
|
|
528
529
|
defineProperty($baseLink, 'pathExpressionInsideFilter', true)
|
|
529
|
-
} else {
|
|
530
|
+
} else if (!inFrom) {
|
|
530
531
|
rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
|
|
531
532
|
}
|
|
532
533
|
}
|
|
@@ -565,7 +566,7 @@ function infer(originalQuery, model) {
|
|
|
565
566
|
if (step.where) {
|
|
566
567
|
const danglingFilter = !(arg.ref[i + 1] || arg.expand || arg.inline || inExists)
|
|
567
568
|
const definition = arg.$refLinks[i].definition
|
|
568
|
-
if ((!definition.target && definition.kind !== 'entity') || (!inFrom && danglingFilter))
|
|
569
|
+
if ((!definition.target && definition.kind !== 'entity') || (!inFrom && !inCalcElement && danglingFilter))
|
|
569
570
|
throw new Error('A filter can only be provided when navigating along associations')
|
|
570
571
|
if (!inFrom && !arg.expand)defineProperty(arg, 'isJoinRelevant', true)
|
|
571
572
|
let skipJoinsForFilter = false
|
|
@@ -574,9 +575,11 @@ function infer(originalQuery, model) {
|
|
|
574
575
|
// books[exists genre[code='A']].title --> column is join relevant but inner exists filter is not
|
|
575
576
|
skipJoinsForFilter = true
|
|
576
577
|
} else if (token.ref || token.xpr || token.list) {
|
|
578
|
+
// For scoped queries (non-dangling filters in FROM), treat filter contents as EXISTS context
|
|
579
|
+
// because they will become part of an EXISTS subquery
|
|
577
580
|
inferArg(token, false, arg.$refLinks[i], {
|
|
578
581
|
...context,
|
|
579
|
-
inExists: skipJoinsForFilter || inExists,
|
|
582
|
+
inExists: skipJoinsForFilter || inExists || (inFrom && !danglingFilter),
|
|
580
583
|
inXpr: !!token.xpr,
|
|
581
584
|
inInfixFilter: true,
|
|
582
585
|
inFrom,
|
|
@@ -586,7 +589,7 @@ function infer(originalQuery, model) {
|
|
|
586
589
|
applyToFunctionArgs(token.args, inferArg, [
|
|
587
590
|
false,
|
|
588
591
|
arg.$refLinks[i],
|
|
589
|
-
{ inExists: skipJoinsForFilter || inExists, inXpr: true, inInfixFilter: true, inFrom },
|
|
592
|
+
{ inExists: skipJoinsForFilter || inExists || (inFrom && !danglingFilter), inXpr: true, inInfixFilter: true, inFrom },
|
|
590
593
|
])
|
|
591
594
|
}
|
|
592
595
|
}
|
|
@@ -595,7 +598,8 @@ function infer(originalQuery, model) {
|
|
|
595
598
|
|
|
596
599
|
if(!arg.$refLinks[i].$main)
|
|
597
600
|
arg.$refLinks[i].alias = !arg.ref[i + 1] && arg.as ? arg.as : id.split('.').pop()
|
|
598
|
-
|
|
601
|
+
const def = getDefinition(arg.$refLinks[i].definition.target)
|
|
602
|
+
if (hasOwnSkip(def) && !isRuntimeView(def)) isPersisted = false
|
|
599
603
|
if (!arg.ref[i + 1]) {
|
|
600
604
|
const flatName = nameSegments.join('_')
|
|
601
605
|
defineProperty(arg, 'flatName', flatName)
|
|
@@ -655,7 +659,11 @@ function infer(originalQuery, model) {
|
|
|
655
659
|
// ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
|
|
656
660
|
if (arg.expand) {
|
|
657
661
|
const { $refLinks } = arg
|
|
658
|
-
|
|
662
|
+
|
|
663
|
+
const skip = $refLinks.some(link => {
|
|
664
|
+
const def = getDefinition(link.definition.target)
|
|
665
|
+
return hasOwnSkip(def) && !isRuntimeView(def)
|
|
666
|
+
})
|
|
659
667
|
if (skip) {
|
|
660
668
|
$refLinks[$refLinks.length - 1].skipExpand = true
|
|
661
669
|
return
|
|
@@ -708,24 +716,65 @@ function infer(originalQuery, model) {
|
|
|
708
716
|
)
|
|
709
717
|
}
|
|
710
718
|
let elements = {}
|
|
719
|
+
let seenWildcard = false
|
|
711
720
|
inline.forEach(inlineCol => {
|
|
712
721
|
inferArg(inlineCol, null, $leafLink, { inXpr: true, baseColumn: col })
|
|
713
722
|
if (inlineCol === '*') {
|
|
723
|
+
if (seenWildcard) throw new Error(`Duplicate wildcard "*" in inline of "${col.as || col.ref.map(idOnly).join('_')}"`)
|
|
724
|
+
seenWildcard = true
|
|
714
725
|
const wildCardElements = {}
|
|
715
726
|
// either the `.elements´ of the struct or the `.elements` of the assoc target
|
|
716
|
-
const
|
|
727
|
+
const targetDef = getDefinition($leafLink.definition.target)
|
|
728
|
+
const leafLinkElements = targetDef?.elements || $leafLink.definition.elements
|
|
729
|
+
const isAssociation = !!$leafLink.definition.target
|
|
730
|
+
|
|
731
|
+
const deferredCalcElements = []
|
|
717
732
|
Object.entries(leafLinkElements).forEach(([k, v]) => {
|
|
718
733
|
const name = namePrefix ? `${namePrefix}_${k}` : k
|
|
719
734
|
// if overwritten/excluded omit from wildcard elements
|
|
720
735
|
// in elements the names are already flat so consider the prefix
|
|
721
736
|
// in excluding, the elements are addressed without the prefix
|
|
722
|
-
if (!(name in elements || col.excluding?.includes(k)))
|
|
737
|
+
if (!(name in elements || col.excluding?.includes(k))) {
|
|
738
|
+
wildCardElements[name] = v
|
|
739
|
+
|
|
740
|
+
if(v.value) {
|
|
741
|
+
// defer linkCalculatedElement calls until after all association joins are registered
|
|
742
|
+
// so that the join tree order is correct
|
|
743
|
+
deferredCalcElements.push({ k, v })
|
|
744
|
+
}
|
|
745
|
+
else if (isAssociation && !v.virtual && v.type !== 'cds.LargeBinary' && !(v.on && !v.keys)) {
|
|
746
|
+
// Check if this element is a foreign key (FK elements don't need join)
|
|
747
|
+
const isFK = $leafLink.definition.keys?.some(key => key.ref[0] === k)
|
|
748
|
+
if (!isFK) {
|
|
749
|
+
// Create a fake column with ref [<inlined assoc>, <element name>] and proper $refLinks
|
|
750
|
+
const fakeCol = {
|
|
751
|
+
ref: [...col.ref, k],
|
|
752
|
+
}
|
|
753
|
+
// Copy $refLinks and add new link for the target element with proper alias
|
|
754
|
+
const fakeRefLinks = [
|
|
755
|
+
...$refLinks,
|
|
756
|
+
{ definition: v, target: targetDef, alias: k }
|
|
757
|
+
]
|
|
758
|
+
defineProperty(fakeCol, '$refLinks', fakeRefLinks)
|
|
759
|
+
defineProperty(fakeCol, 'isJoinRelevant', true)
|
|
760
|
+
// Merge into join tree
|
|
761
|
+
inferred.joinTree.mergeColumn(fakeCol, originalQuery.outerQueries)
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
723
765
|
})
|
|
766
|
+
// link calculated elements after association joins are registered in the join tree
|
|
767
|
+
for (const { k, v } of deferredCalcElements) {
|
|
768
|
+
linkCalculatedElement(
|
|
769
|
+
{ ref: [k], $refLinks: [{ definition: v, target: targetDef }] },
|
|
770
|
+
$leafLink,
|
|
771
|
+
)
|
|
772
|
+
}
|
|
724
773
|
elements = { ...elements, ...wildCardElements }
|
|
725
774
|
} else {
|
|
726
775
|
const nameParts = namePrefix ? [namePrefix] : []
|
|
727
776
|
if (inlineCol.as) nameParts.push(inlineCol.as)
|
|
728
|
-
else nameParts.push(...inlineCol.ref.map(idOnly))
|
|
777
|
+
else if (inlineCol.ref) nameParts.push(...inlineCol.ref.map(idOnly))
|
|
729
778
|
const name = nameParts.join('_')
|
|
730
779
|
if (inlineCol.inline) {
|
|
731
780
|
const inlineElements = resolveInline(inlineCol, name)
|
|
@@ -737,6 +786,8 @@ function infer(originalQuery, model) {
|
|
|
737
786
|
elements[name] = getCdsTypeForVal(inlineCol.val)
|
|
738
787
|
} else if (inlineCol.func) {
|
|
739
788
|
elements[name] = {}
|
|
789
|
+
} else if (inlineCol.xpr) {
|
|
790
|
+
elements[name] = {}
|
|
740
791
|
} else {
|
|
741
792
|
elements[name] = inlineCol.$refLinks[inlineCol.$refLinks.length - 1].definition
|
|
742
793
|
}
|
|
@@ -764,6 +815,16 @@ function infer(originalQuery, model) {
|
|
|
764
815
|
`Unexpected “expand” on “${col.ref.map(idOnly)}”; can only be used after a reference to a structure, association or table alias`,
|
|
765
816
|
)
|
|
766
817
|
}
|
|
818
|
+
// Check for duplicate wildcards before creating the subquery
|
|
819
|
+
let seenWildcard = false
|
|
820
|
+
for (const e of expand) {
|
|
821
|
+
if (e === '*') {
|
|
822
|
+
if (seenWildcard) {
|
|
823
|
+
throw new Error(`Duplicate wildcard "*" in expand of "${col.as || col.ref.map(idOnly).join('_')}"`)
|
|
824
|
+
}
|
|
825
|
+
seenWildcard = true
|
|
826
|
+
}
|
|
827
|
+
}
|
|
767
828
|
const target = getDefinition($leafLink.definition.target)
|
|
768
829
|
if (target) {
|
|
769
830
|
const expandSubquery = {
|
package/lib/infer/join-tree.js
CHANGED
|
@@ -133,20 +133,22 @@ class JoinTree {
|
|
|
133
133
|
*
|
|
134
134
|
* @param {string} alias - The original alias name.
|
|
135
135
|
* @param {unknown[]} outerQueries - An array of outer queries.
|
|
136
|
+
* @param {string} key - The key to be used for storing the alias in the map. If not provided, the upper-case version of the alias will be used as the key.
|
|
136
137
|
* @returns {string} - The next unambiguous table alias.
|
|
137
138
|
*/
|
|
138
|
-
addNextAvailableTableAlias(alias, outerQueries) {
|
|
139
|
+
addNextAvailableTableAlias(alias, outerQueries, key) {
|
|
139
140
|
const upperAlias = alias.toUpperCase()
|
|
140
|
-
if (this._queryAliases.get(upperAlias) || outerQueries?.some(outer => outerHasAlias(outer))) {
|
|
141
|
+
if (this._queryAliases.get(upperAlias) || outerQueries?.some(outer => outerHasAlias(outer, key))) {
|
|
141
142
|
let j = 2
|
|
142
|
-
while (this._queryAliases.get(upperAlias + j) || outerQueries?.some(outer => outerHasAlias(outer, j))) j += 1
|
|
143
|
+
while (this._queryAliases.get(upperAlias + j) || outerQueries?.some(outer => outerHasAlias(outer, key, j))) j += 1
|
|
143
144
|
alias += j
|
|
144
145
|
}
|
|
145
|
-
this._queryAliases.set(alias.toUpperCase(), alias)
|
|
146
|
+
this._queryAliases.set(key || alias.toUpperCase(), alias)
|
|
146
147
|
return alias
|
|
147
148
|
|
|
148
|
-
function outerHasAlias(outer, number) {
|
|
149
|
-
|
|
149
|
+
function outerHasAlias(outer, searchInValues = false, number) {
|
|
150
|
+
const currAlias = number ? upperAlias + number : upperAlias
|
|
151
|
+
return searchInValues ? Array.from(outer.joinTree._queryAliases.values()).includes(currAlias) : outer.joinTree._queryAliases.get(currAlias)
|
|
150
152
|
}
|
|
151
153
|
}
|
|
152
154
|
|
package/lib/utils.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const cds = require('@sap/cds')
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Formats a ref array into a string representation.
|
|
5
7
|
* If the first step is an entity, the separator is a colon, otherwise a dot.
|
|
@@ -27,6 +29,25 @@ function hasOwnSkip(definition) {
|
|
|
27
29
|
)
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
function isRuntimeView(definition) {
|
|
33
|
+
if (!definition || !cds.env.features.runtime_views) return false
|
|
34
|
+
if (definition['_isRuntimeView']) return true
|
|
35
|
+
if (!definition['@cds.persistence.skip']) {
|
|
36
|
+
Object.defineProperty(definition, '_isRuntimeView', {
|
|
37
|
+
value: true,
|
|
38
|
+
writable: false,
|
|
39
|
+
configurable: true,
|
|
40
|
+
enumerable: false
|
|
41
|
+
})
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
// views with "as select from" variant are also runtime views, even if they are annotated with persistence skip
|
|
45
|
+
if (definition.query && !definition.query._target) return true
|
|
46
|
+
if (definition.query) return isRuntimeView(definition.query._target)
|
|
47
|
+
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
|
|
30
51
|
/**
|
|
31
52
|
* Determines if a definition is calculated on read.
|
|
32
53
|
* - Stored calculated elements are not unfolded
|
|
@@ -136,6 +157,12 @@ function getModelUtils(model, query) {
|
|
|
136
157
|
}
|
|
137
158
|
}
|
|
138
159
|
|
|
160
|
+
function resolveTable(target) {
|
|
161
|
+
if (target.query?._target && !Object.prototype.hasOwnProperty.call(target, '@cds.persistence.table'))
|
|
162
|
+
return resolveTable(target.query._target)
|
|
163
|
+
return target
|
|
164
|
+
}
|
|
165
|
+
|
|
139
166
|
// export the function to be used in other modules
|
|
140
167
|
module.exports = {
|
|
141
168
|
prettyPrintRef,
|
|
@@ -145,4 +172,6 @@ module.exports = {
|
|
|
145
172
|
defineProperty,
|
|
146
173
|
getModelUtils,
|
|
147
174
|
hasOwnSkip,
|
|
175
|
+
isRuntimeView,
|
|
176
|
+
resolveTable
|
|
148
177
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/db-service",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.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": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"generic-pool": "^3.9.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@sap/cds": ">=9.
|
|
30
|
+
"@sap/cds": ">=9.8"
|
|
31
31
|
},
|
|
32
32
|
"license": "Apache-2.0"
|
|
33
33
|
}
|