@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/CHANGELOG.md +29 -2
- package/index.js +16 -2
- package/lib/InsertResults.js +22 -4
- package/lib/SQLService.js +169 -73
- package/lib/common/DatabaseService.js +93 -88
- package/lib/common/factory.d.ts +5 -0
- package/lib/common/generic-pool.js +34 -0
- package/lib/common/session-context.js +32 -0
- package/lib/converters.d.ts +24 -0
- package/lib/cql-functions.js +205 -5
- package/lib/cqn2sql.js +463 -154
- package/lib/cqn4sql.js +176 -71
- package/lib/deep-queries.js +31 -3
- package/lib/fill-in-keys.js +15 -4
- package/lib/infer/cqn.d.ts +45 -0
- package/lib/infer/index.js +128 -31
- package/lib/infer/join-tree.js +64 -19
- package/package.json +17 -8
package/lib/infer/index.js
CHANGED
|
@@ -4,7 +4,6 @@ const cds = require('@sap/cds/lib')
|
|
|
4
4
|
|
|
5
5
|
const JoinTree = require('./join-tree')
|
|
6
6
|
const { pseudos } = require('./pseudos')
|
|
7
|
-
// REVISIT: we should always return cds.linked elements
|
|
8
7
|
const cdsTypes = cds.linked({
|
|
9
8
|
definitions: {
|
|
10
9
|
Timestamp: { type: 'cds.Timestamp' },
|
|
@@ -18,11 +17,10 @@ const cdsTypes = cds.linked({
|
|
|
18
17
|
},
|
|
19
18
|
}).definitions
|
|
20
19
|
for (const each in cdsTypes) cdsTypes[`cds.${each}`] = cdsTypes[each]
|
|
21
|
-
|
|
22
20
|
/**
|
|
23
|
-
* @param {
|
|
24
|
-
* @param {CSN} [model]
|
|
25
|
-
* @returns {
|
|
21
|
+
* @param {import('@sap/cds/apis/cqn').Query|string} originalQuery
|
|
22
|
+
* @param {import('@sap/cds/apis/csn').CSN} [model]
|
|
23
|
+
* @returns {import('./cqn').Query} = q with .target and .elements
|
|
26
24
|
*/
|
|
27
25
|
function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
28
26
|
if (!model) cds.error('Please specify a model')
|
|
@@ -40,6 +38,10 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
40
38
|
inferred.CREATE ||
|
|
41
39
|
inferred.DROP ||
|
|
42
40
|
inferred.STREAM
|
|
41
|
+
|
|
42
|
+
// cache for already processed calculated elements
|
|
43
|
+
const alreadySeenCalcElements = new Set()
|
|
44
|
+
|
|
43
45
|
const sources = inferTarget(_.from || _.into || _.entity, {})
|
|
44
46
|
const joinTree = new JoinTree(sources)
|
|
45
47
|
const aliases = Object.keys(sources)
|
|
@@ -134,11 +136,11 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
134
136
|
* next 'ref' step should be looked up.
|
|
135
137
|
*
|
|
136
138
|
*
|
|
137
|
-
* @param {
|
|
139
|
+
* @param {object} arg - The argument object that will be augmented with additional properties.
|
|
138
140
|
* It must contain a 'ref' property, which is an array representing the steps to be processed.
|
|
139
141
|
* Optionally, it can also contain an 'xpr' property, which is also processed recursively.
|
|
140
142
|
*
|
|
141
|
-
* @param {
|
|
143
|
+
* @param {object} $baseLink - Optional parameter. It represents the environment in which the first 'ref' step should be
|
|
142
144
|
* resolved. It's needed for infix filter / expand columns. It must contain a 'definition'
|
|
143
145
|
* property, which is an object representing the base environment.
|
|
144
146
|
*
|
|
@@ -177,7 +179,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
177
179
|
if (!expandOrExists && nextStep && !(nextStep in e.foreignKeys))
|
|
178
180
|
throw new Error(`Only foreign keys of "${e.name}" can be accessed in infix filter`)
|
|
179
181
|
}
|
|
180
|
-
arg.$refLinks.push({ definition: e, target:
|
|
182
|
+
arg.$refLinks.push({ definition: e, target: definition })
|
|
181
183
|
// filter paths are flattened
|
|
182
184
|
// REVISIT: too much augmentation -> better remove flatName..
|
|
183
185
|
Object.defineProperty(arg, 'flatName', { value: ref.join('_'), writable: true })
|
|
@@ -188,7 +190,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
188
190
|
}
|
|
189
191
|
} else {
|
|
190
192
|
const recent = arg.$refLinks[i - 1]
|
|
191
|
-
const { elements } = recent.
|
|
193
|
+
const { elements } = recent.definition._target || recent.definition
|
|
192
194
|
const e = elements[id]
|
|
193
195
|
if (!e) throw new Error(`"${id}" not found in the elements of "${arg.$refLinks[i - 1].definition.name}"`)
|
|
194
196
|
arg.$refLinks.push({ definition: e, target: e._target || e })
|
|
@@ -216,6 +218,11 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
216
218
|
} else throw new Error('A filter can only be provided when navigating along associations')
|
|
217
219
|
}
|
|
218
220
|
})
|
|
221
|
+
const { definition, target } = arg.$refLinks[arg.$refLinks.length - 1]
|
|
222
|
+
if (definition.value) {
|
|
223
|
+
// nested calculated element
|
|
224
|
+
attachRefLinksToArg(definition.value, { definition: definition.parent, target }, true)
|
|
225
|
+
}
|
|
219
226
|
}
|
|
220
227
|
|
|
221
228
|
/**
|
|
@@ -227,7 +234,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
227
234
|
* Each entry in the `$combinedElements` dictionary maps from the element name
|
|
228
235
|
* to an array of objects containing the index and table alias where the element can be found.
|
|
229
236
|
*
|
|
230
|
-
* @returns {
|
|
237
|
+
* @returns {object} The `$combinedElements` dictionary, which maps element names to an array of objects
|
|
231
238
|
* containing the index and table alias where the element can be found.
|
|
232
239
|
*/
|
|
233
240
|
function inferCombinedElements() {
|
|
@@ -264,9 +271,9 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
264
271
|
* Also walks over other `ref`s in the query, validates them, and attaches `$refLinks`.
|
|
265
272
|
* This includes handling `where`, infix filters within column `refs`, or other `csn` paths.
|
|
266
273
|
*
|
|
267
|
-
* @param {
|
|
274
|
+
* @param {object} $combinedElements The `$combinedElements` dictionary of the query, which maps element names
|
|
268
275
|
* to an array of objects containing the index and table alias where the element can be found.
|
|
269
|
-
* @returns {
|
|
276
|
+
* @returns {object} The inferred `elements` dictionary of the query, which maps element names to their corresponding definitions.
|
|
270
277
|
*/
|
|
271
278
|
function inferQueryElements($combinedElements) {
|
|
272
279
|
let queryElements = {}
|
|
@@ -444,7 +451,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
444
451
|
*/
|
|
445
452
|
|
|
446
453
|
function inferQueryElement(column, insertIntoQueryElements = true, $baseLink = null, context) {
|
|
447
|
-
const { inExists, inExpr, inNestedProjection } = context || {}
|
|
454
|
+
const { inExists, inExpr, inNestedProjection, inCalcElement, baseColumn } = context || {}
|
|
448
455
|
if (column.param) return // parameter references are only resolved into values on execution e.g. :val, :1 or ?
|
|
449
456
|
if (column.args) column.args.forEach(arg => inferQueryElement(arg, false, $baseLink, context)) // e.g. function in expression
|
|
450
457
|
if (column.list) column.list.forEach(arg => inferQueryElement(arg, false, $baseLink, context))
|
|
@@ -486,11 +493,11 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
486
493
|
const elements = definition.elements || definition._target?.elements
|
|
487
494
|
if (elements && id in elements) {
|
|
488
495
|
const element = elements[id]
|
|
489
|
-
if (!
|
|
496
|
+
if (!inNestedProjection && !inCalcElement && element.target) {
|
|
490
497
|
// only fk access in infix filter
|
|
491
498
|
const nextStep = column.ref[1]?.id || column.ref[1]
|
|
492
499
|
// no unmanaged assoc in infix filter path
|
|
493
|
-
if (element.on)
|
|
500
|
+
if (!inExists && element.on)
|
|
494
501
|
throw new Error(
|
|
495
502
|
`"${element.name}" in path "${column.ref
|
|
496
503
|
.map(idOnly)
|
|
@@ -500,7 +507,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
500
507
|
if (nextStep && !(nextStep in element.foreignKeys))
|
|
501
508
|
throw new Error(`Only foreign keys of "${element.name}" can be accessed in infix filter`)
|
|
502
509
|
}
|
|
503
|
-
|
|
510
|
+
const resolvableIn = definition.target ? definition._target : target
|
|
511
|
+
column.$refLinks.push({ definition: elements[id], target: resolvableIn })
|
|
504
512
|
} else {
|
|
505
513
|
stepNotFoundInPredecessor(id, definition.name)
|
|
506
514
|
}
|
|
@@ -535,14 +543,15 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
535
543
|
)
|
|
536
544
|
}
|
|
537
545
|
|
|
546
|
+
const target = definition._target || column.$refLinks[i - 1].target
|
|
538
547
|
if (element) {
|
|
539
|
-
const $refLink = { definition: elements[id], target
|
|
548
|
+
const $refLink = { definition: elements[id], target }
|
|
540
549
|
column.$refLinks.push($refLink)
|
|
541
550
|
} else if (firstStepIsSelf) {
|
|
542
551
|
stepNotFoundInColumnList(id)
|
|
543
552
|
} else if (column.ref[0] === '$user' && pseudoPath) {
|
|
544
553
|
// `$user.some.unknown.element` -> no error
|
|
545
|
-
column.$refLinks.push({ definition: {}, target
|
|
554
|
+
column.$refLinks.push({ definition: {}, target })
|
|
546
555
|
} else if (id === '$dummy') {
|
|
547
556
|
// `some.known.element.$dummy` -> no error; used by cds.ql to simulate joins
|
|
548
557
|
column.$refLinks.push({ definition: { name: '$dummy', parent: column.$refLinks[i - 1].target } })
|
|
@@ -568,7 +577,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
568
577
|
}
|
|
569
578
|
|
|
570
579
|
if (step.where) {
|
|
571
|
-
const danglingFilter = !(column.ref[i + 1] || column.expand || inExists)
|
|
580
|
+
const danglingFilter = !(column.ref[i + 1] || column.expand || column.inline || inExists)
|
|
572
581
|
if (!column.$refLinks[i].definition.target || danglingFilter)
|
|
573
582
|
throw new Error(/A filter can only be provided when navigating along associations/)
|
|
574
583
|
if (!column.expand) Object.defineProperty(column, 'isJoinRelevant', { value: true })
|
|
@@ -627,7 +636,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
627
636
|
}
|
|
628
637
|
if (queryElements[elementName] !== undefined)
|
|
629
638
|
throw new Error(`Duplicate definition of element “${elementName}”`)
|
|
630
|
-
|
|
639
|
+
const element = getCopyWithAnnos(column, leafArt)
|
|
640
|
+
queryElements[elementName] = element
|
|
631
641
|
}
|
|
632
642
|
}
|
|
633
643
|
}
|
|
@@ -644,19 +654,33 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
644
654
|
return
|
|
645
655
|
}
|
|
646
656
|
}
|
|
647
|
-
const
|
|
657
|
+
const leafArt = column.$refLinks[column.$refLinks.length - 1].definition
|
|
658
|
+
const virtual = (leafArt.virtual || !isPersisted) && !inExpr
|
|
648
659
|
// check if we need to merge the column `ref` into the join tree of the query
|
|
649
|
-
if (!inExists && !virtual &&
|
|
650
|
-
|
|
651
|
-
|
|
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
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (leafArt.value && !leafArt.value.stored) {
|
|
675
|
+
resolveCalculatedElement(column, $baseLink, baseColumn)
|
|
652
676
|
}
|
|
653
677
|
|
|
654
678
|
/**
|
|
655
679
|
* Resolves and processes the inline attribute of a column in a database query.
|
|
656
680
|
*
|
|
657
|
-
* @param {
|
|
681
|
+
* @param {object} col - The column object with properties: `inline` and `$refLinks`.
|
|
658
682
|
* @param {string} [namePrefix=col.as || col.flatName] - Prefix for naming new columns. Defaults to `col.as` or `col.flatName`.
|
|
659
|
-
* @returns {
|
|
683
|
+
* @returns {object} - An object with resolved and processed inline column definitions.
|
|
660
684
|
*
|
|
661
685
|
* Procedure:
|
|
662
686
|
* 1. Iterate through `inline` array. For each `inlineCol`:
|
|
@@ -671,7 +695,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
671
695
|
const $leafLink = $refLinks[$refLinks.length - 1]
|
|
672
696
|
let elements = {}
|
|
673
697
|
inline.forEach(inlineCol => {
|
|
674
|
-
inferQueryElement(inlineCol, false, $leafLink, { inExpr: true, inNestedProjection: true })
|
|
698
|
+
inferQueryElement(inlineCol, false, $leafLink, { inExpr: true, inNestedProjection: true, baseColumn: col })
|
|
675
699
|
if (inlineCol === '*') {
|
|
676
700
|
const wildCardElements = {}
|
|
677
701
|
// either the `.elements´ of the struct or the `.elements` of the assoc target
|
|
@@ -710,8 +734,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
710
734
|
/**
|
|
711
735
|
* Resolves a query column which has an `expand` property.
|
|
712
736
|
*
|
|
713
|
-
* @param {
|
|
714
|
-
* @returns {
|
|
737
|
+
* @param {object} col - The column object with properties: `expand` and `$refLinks`.
|
|
738
|
+
* @returns {object} - A `cds.struct` object with expanded column definitions.
|
|
715
739
|
*
|
|
716
740
|
* Procedure:
|
|
717
741
|
* - if `$leafLink` is an association, constructs an `expandSubquery` and infers a new query structure.
|
|
@@ -777,6 +801,74 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
777
801
|
throw new Error(err)
|
|
778
802
|
}
|
|
779
803
|
}
|
|
804
|
+
function resolveCalculatedElement(column, baseLink, baseColumn) {
|
|
805
|
+
const calcElement = column.$refLinks?.[column.$refLinks.length - 1].definition || column
|
|
806
|
+
if (alreadySeenCalcElements.has(calcElement)) return
|
|
807
|
+
else alreadySeenCalcElements.add(calcElement)
|
|
808
|
+
const { ref, xpr, func } = calcElement.value
|
|
809
|
+
if (ref || xpr) {
|
|
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
|
+
}
|
|
817
|
+
// column is now fully linked, now we need to find out if we need to merge it into the join tree
|
|
818
|
+
// for that, we calculate all paths from a calc element and merge them into the join tree
|
|
819
|
+
mergePathsIntoJoinTree(calcElement.value, basePath)
|
|
820
|
+
}
|
|
821
|
+
if (func)
|
|
822
|
+
calcElement.value.args?.forEach(arg =>
|
|
823
|
+
inferQueryElement(arg, false, { definition: calcElement.parent, target: calcElement.parent }),
|
|
824
|
+
) // {func}.args are optional
|
|
825
|
+
function mergePathsIntoJoinTree(e, basePath = null) {
|
|
826
|
+
basePath = basePath || { $refLinks: [], ref: [] }
|
|
827
|
+
if (e.ref) {
|
|
828
|
+
e.$refLinks.forEach((link, i) => {
|
|
829
|
+
const { definition } = link
|
|
830
|
+
if (!definition.value) {
|
|
831
|
+
basePath.$refLinks.push(link)
|
|
832
|
+
basePath.ref.push(e.ref[i])
|
|
833
|
+
}
|
|
834
|
+
})
|
|
835
|
+
const leafOfCalculatedElementRef = e.$refLinks[e.$refLinks.length - 1].definition
|
|
836
|
+
if (leafOfCalculatedElementRef.value) mergePathsIntoJoinTree(leafOfCalculatedElementRef.value, basePath)
|
|
837
|
+
|
|
838
|
+
mergePathIfNecessary(basePath, e)
|
|
839
|
+
} else if (e.xpr) {
|
|
840
|
+
e.xpr.forEach(step => {
|
|
841
|
+
if (step.ref) {
|
|
842
|
+
const subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
|
|
843
|
+
step.$refLinks.forEach((link, i) => {
|
|
844
|
+
const { definition } = link
|
|
845
|
+
if (definition.value) {
|
|
846
|
+
mergePathsIntoJoinTree(definition.value)
|
|
847
|
+
} else {
|
|
848
|
+
subPath.$refLinks.push(link)
|
|
849
|
+
subPath.ref.push(step.ref[i])
|
|
850
|
+
}
|
|
851
|
+
})
|
|
852
|
+
mergePathIfNecessary(subPath, step)
|
|
853
|
+
}
|
|
854
|
+
})
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function mergePathIfNecessary(p, step) {
|
|
858
|
+
const calcElementIsJoinRelevant = isColumnJoinRelevant(p)
|
|
859
|
+
if (calcElementIsJoinRelevant) {
|
|
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 })
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
780
872
|
|
|
781
873
|
/**
|
|
782
874
|
* Checks whether or not the `ref` of the given column is join relevant.
|
|
@@ -828,15 +920,20 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
828
920
|
* if there is not already an element with the same name present.
|
|
829
921
|
*/
|
|
830
922
|
function inferElementsFromWildCard() {
|
|
923
|
+
const exclude = _.excluding ? x => _.excluding.includes(x) : () => false
|
|
924
|
+
|
|
831
925
|
if (Object.keys(queryElements).length === 0 && aliases.length === 1) {
|
|
832
926
|
// only one query source and no overwritten columns
|
|
833
927
|
Object.entries(sources[aliases[0]].elements).forEach(([name, element]) => {
|
|
834
|
-
if (element.type !== 'cds.LargeBinary') queryElements[name] = element
|
|
928
|
+
if (!exclude(name) && element.type !== 'cds.LargeBinary') queryElements[name] = element
|
|
929
|
+
if (element.value) {
|
|
930
|
+
// we might have join relevant calculated elements
|
|
931
|
+
resolveCalculatedElement(element)
|
|
932
|
+
}
|
|
835
933
|
})
|
|
836
934
|
return
|
|
837
935
|
}
|
|
838
936
|
|
|
839
|
-
const exclude = _.excluding ? x => _.excluding.includes(x) : () => false
|
|
840
937
|
const ambiguousElements = {}
|
|
841
938
|
Object.entries($combinedElements).forEach(([name, tableAliases]) => {
|
|
842
939
|
if (Object.keys(tableAliases).length > 1) {
|
package/lib/infer/join-tree.js
CHANGED
|
@@ -1,50 +1,92 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
// REVISIT: define following unknown types
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {unknown} $refLink
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {unknown} parent
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {unknown} where
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {unknown} children
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {unknown} queryArtifact
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {string} alias
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Map<alias,Root>} _roots
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {Object.<string, unknown>} sources
|
|
35
|
+
*/
|
|
36
|
+
|
|
3
37
|
/**
|
|
4
38
|
* A class representing a Node in the join tree.
|
|
5
|
-
*
|
|
6
|
-
* @property {$refLink} - A reference link to this node.
|
|
7
|
-
* @property {parent} - The parent Node of this node.
|
|
8
|
-
* @property {where} - An optional condition to be applied to this node.
|
|
9
|
-
* @property {children} - A Map of children nodes belonging to this node.
|
|
10
39
|
*/
|
|
11
40
|
class Node {
|
|
41
|
+
/**
|
|
42
|
+
* @param {$refLink} $refLink
|
|
43
|
+
* @param {parent} parent
|
|
44
|
+
* @param {where} where
|
|
45
|
+
*/
|
|
12
46
|
constructor($refLink, parent, where = null) {
|
|
47
|
+
/** @type {$refLink} - A reference link to this node. */
|
|
13
48
|
this.$refLink = $refLink
|
|
49
|
+
/** @type {parent} - The parent Node of this node. */
|
|
14
50
|
this.parent = parent
|
|
51
|
+
/** @type {where} - An optional condition to be applied to this node. */
|
|
15
52
|
this.where = where
|
|
53
|
+
/** @type {children} - A Map of children nodes belonging to this node. */
|
|
16
54
|
this.children = new Map()
|
|
17
55
|
}
|
|
18
56
|
}
|
|
19
57
|
|
|
20
58
|
/**
|
|
21
59
|
* A class representing the root of the join tree.
|
|
22
|
-
*
|
|
23
|
-
* @property {queryArtifact} - The artifact used to make the query.
|
|
24
|
-
* @property {alias} - The alias of the artifact.
|
|
25
|
-
* @property {parent} - The parent Node of this root, null for the root Node.
|
|
26
|
-
* @property {children} - A Map of children nodes belonging to this root.
|
|
27
60
|
*/
|
|
28
61
|
class Root {
|
|
62
|
+
/**
|
|
63
|
+
* @param {[alias, queryArtifact]} querySource
|
|
64
|
+
*/
|
|
29
65
|
constructor(querySource) {
|
|
30
66
|
const [alias, queryArtifact] = querySource
|
|
67
|
+
/** @type {queryArtifact} - The artifact used to make the query. */
|
|
31
68
|
this.queryArtifact = queryArtifact
|
|
69
|
+
/** @type {alias} - The alias of the artifact. */
|
|
32
70
|
this.alias = alias
|
|
71
|
+
/** @type {parent} - The parent Node of this root, null for the root Node. */
|
|
33
72
|
this.parent = null
|
|
73
|
+
/** @type {children} - A Map of children nodes belonging to this root. */
|
|
34
74
|
this.children = new Map()
|
|
35
75
|
}
|
|
36
76
|
}
|
|
37
77
|
|
|
38
78
|
/**
|
|
39
79
|
* A class representing a Join Tree.
|
|
40
|
-
*
|
|
41
|
-
* @property {_roots} - A Map of root nodes.
|
|
42
|
-
* @property {isInitial} - A boolean indicating if the join tree is in its initial state.
|
|
43
|
-
* @property {_queryAliases} - A Map of query aliases, which is used during the association to join translation.
|
|
44
80
|
*/
|
|
45
81
|
class JoinTree {
|
|
82
|
+
/**
|
|
83
|
+
*
|
|
84
|
+
* @param {sources} sources
|
|
85
|
+
*/
|
|
46
86
|
constructor(sources) {
|
|
87
|
+
/** @type {_roots} - A Map of root nodes. */
|
|
47
88
|
this._roots = new Map()
|
|
89
|
+
/** @type {boolean} - A boolean indicating if the join tree is in its initial state. */
|
|
48
90
|
this.isInitial = true
|
|
49
91
|
/**
|
|
50
92
|
* A map that holds query aliases which are used during the
|
|
@@ -53,6 +95,7 @@ class JoinTree {
|
|
|
53
95
|
*
|
|
54
96
|
* The table aliases are treated case insensitive. The index of each
|
|
55
97
|
* table alias entry, is the capitalized version of the alias.
|
|
98
|
+
* @type {Map<string, string>}
|
|
56
99
|
*/
|
|
57
100
|
this._queryAliases = new Map()
|
|
58
101
|
Object.entries(sources).forEach(entry => {
|
|
@@ -82,6 +125,7 @@ class JoinTree {
|
|
|
82
125
|
* Calculates and adds the next available table alias to the alias map.
|
|
83
126
|
*
|
|
84
127
|
* @param {string} alias - The original alias name.
|
|
128
|
+
* @param {unknown[]} outerQueries - An array of outer queries.
|
|
85
129
|
* @returns {string} - The next unambiguous table alias.
|
|
86
130
|
*/
|
|
87
131
|
addNextAvailableTableAlias(alias, outerQueries) {
|
|
@@ -107,10 +151,10 @@ class JoinTree {
|
|
|
107
151
|
* For each step, it checks whether it has been seen before. If so, it resets the $refLink to point to the already merged $refLink.
|
|
108
152
|
* If not, it creates a new Node and ensures proper aliasing and foreign key access.
|
|
109
153
|
*
|
|
110
|
-
* @param {
|
|
154
|
+
* @param {object} col - The column object to be merged into the existing join tree. This object should have the properties $refLinks and ref.
|
|
111
155
|
* @returns {boolean} - Always returns true, indicating the column has been successfully merged into the join tree.
|
|
112
156
|
*/
|
|
113
|
-
mergeColumn(col) {
|
|
157
|
+
mergeColumn(col, outerQueries = null) {
|
|
114
158
|
if (this.isInitial) this.isInitial = false
|
|
115
159
|
const head = col.$refLinks[0]
|
|
116
160
|
let node = this._roots.get(head.alias)
|
|
@@ -145,13 +189,14 @@ class JoinTree {
|
|
|
145
189
|
}
|
|
146
190
|
const child = new Node($refLink, node, where)
|
|
147
191
|
if (child.$refLink.definition.isAssociation) {
|
|
148
|
-
if (child.where) {
|
|
149
|
-
// always join relevant
|
|
192
|
+
if (child.where || col.inline) {
|
|
193
|
+
// filter is always join relevant
|
|
194
|
+
// if the column ends up in an `inline` -> each assoc step is join relevant
|
|
150
195
|
child.$refLink.onlyForeignKeyAccess = false
|
|
151
196
|
} else {
|
|
152
197
|
child.$refLink.onlyForeignKeyAccess = true
|
|
153
198
|
}
|
|
154
|
-
child.$refLink.alias = this.addNextAvailableTableAlias($refLink.alias)
|
|
199
|
+
child.$refLink.alias = this.addNextAvailableTableAlias($refLink.alias, outerQueries)
|
|
155
200
|
}
|
|
156
201
|
|
|
157
202
|
const foreignKeys = node.$refLink?.definition.foreignKeys
|
package/package.json
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/db-service",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "CDS base database service",
|
|
5
|
-
"homepage": "https://
|
|
5
|
+
"homepage": "https://github.com/cap-js/cds-dbs/tree/main/db-service#cds-base-database-service",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/cap-js/cds-dbs"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/cap-js/cds-dbs/issues"
|
|
12
|
+
},
|
|
6
13
|
"keywords": [
|
|
7
14
|
"CAP",
|
|
8
15
|
"CDS"
|
|
9
16
|
],
|
|
10
17
|
"author": "SAP SE (https://www.sap.com)",
|
|
11
18
|
"main": "index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
12
20
|
"files": [
|
|
13
21
|
"lib",
|
|
14
22
|
"CHANGELOG.md"
|
|
@@ -18,13 +26,14 @@
|
|
|
18
26
|
"npm": ">=8"
|
|
19
27
|
},
|
|
20
28
|
"scripts": {
|
|
21
|
-
"
|
|
22
|
-
"test": "npx jest --silent",
|
|
23
|
-
"lint": "npx eslint . && npx prettier --check . "
|
|
29
|
+
"test": "jest --silent"
|
|
24
30
|
},
|
|
25
|
-
"dependencies": {},
|
|
26
31
|
"peerDependencies": {
|
|
27
|
-
"@sap/cds": ">=7"
|
|
32
|
+
"@sap/cds": ">=7.1.1"
|
|
28
33
|
},
|
|
29
|
-
"license": "SEE LICENSE"
|
|
34
|
+
"license": "SEE LICENSE",
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@typescript-eslint/eslint-plugin": "^6.2.0",
|
|
37
|
+
"typescript": "^5.1.6"
|
|
38
|
+
}
|
|
30
39
|
}
|