@cap-js/db-service 1.0.0 → 1.1.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 +19 -1
- package/index.js +16 -2
- package/lib/InsertResults.js +20 -3
- package/lib/SQLService.js +112 -28
- package/lib/common/DatabaseService.js +55 -4
- package/lib/common/factory.d.ts +5 -0
- package/lib/converters.d.ts +24 -0
- package/lib/cql-functions.js +192 -4
- package/lib/cqn2sql.js +270 -5
- package/lib/cqn4sql.js +172 -38
- package/lib/deep-queries.js +27 -0
- package/lib/fill-in-keys.js +10 -0
- package/lib/infer/cqn.d.ts +45 -0
- package/lib/infer/index.js +178 -43
- package/lib/infer/join-tree.js +62 -17
- package/package.json +18 -6
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 = {}
|
|
@@ -275,7 +282,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
275
282
|
inferElementsFromWildCard(aliases)
|
|
276
283
|
} else {
|
|
277
284
|
let wildcardSelect = false
|
|
278
|
-
const
|
|
285
|
+
const dollarSelfRefs = []
|
|
279
286
|
columns.forEach(col => {
|
|
280
287
|
if (col === '*') {
|
|
281
288
|
wildcardSelect = true
|
|
@@ -294,24 +301,24 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
294
301
|
}
|
|
295
302
|
setElementOnColumns(col, queryElements[as])
|
|
296
303
|
} else if (col.ref) {
|
|
297
|
-
|
|
304
|
+
const firstStepIsTableAlias =
|
|
305
|
+
(col.ref.length > 1 && col.ref[0] in sources) ||
|
|
306
|
+
// nested projection on table alias
|
|
307
|
+
(col.ref.length === 1 && col.ref[0] in sources && col.inline)
|
|
308
|
+
const firstStepIsSelf =
|
|
309
|
+
!firstStepIsTableAlias && col.ref.length > 1 && ['$self', '$projection'].includes(col.ref[0])
|
|
310
|
+
// we must handle $self references after the query elements have been calculated
|
|
311
|
+
if (firstStepIsSelf) dollarSelfRefs.push(col)
|
|
312
|
+
else handleRef(col)
|
|
298
313
|
} else if (col.expand) {
|
|
299
314
|
inferQueryElement(col)
|
|
300
315
|
} else {
|
|
301
316
|
throw cds.error`Not supported: ${JSON.stringify(col)}`
|
|
302
317
|
}
|
|
303
318
|
})
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (col.cast)
|
|
308
|
-
// final type overwritten -> element not visible anymore
|
|
309
|
-
setElementOnColumns(col, getElementForCast(col))
|
|
310
|
-
else if ((col.ref.length === 1) & (col.ref[0] === '$user'))
|
|
311
|
-
// shortcut to $user.id
|
|
312
|
-
setElementOnColumns(col, queryElements[col.as || '$user'])
|
|
313
|
-
else setElementOnColumns(col, definition)
|
|
314
|
-
})
|
|
319
|
+
|
|
320
|
+
if (dollarSelfRefs.length) inferDollarSelfRefs(dollarSelfRefs)
|
|
321
|
+
|
|
315
322
|
if (wildcardSelect) inferElementsFromWildCard(aliases)
|
|
316
323
|
}
|
|
317
324
|
if (orderBy) {
|
|
@@ -359,6 +366,54 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
359
366
|
|
|
360
367
|
return queryElements
|
|
361
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Processes references starting with `$self`, which are intended to target other query elements.
|
|
371
|
+
* These `$self` paths must be handled after processing the "regular" columns since they are dependent on other query elements.
|
|
372
|
+
*
|
|
373
|
+
* This function checks for `$self` references that may target other `$self` columns, and delays their processing.
|
|
374
|
+
* `$self` references not targeting other `$self` references are handled by the generic `handleRef` function immediately.
|
|
375
|
+
*
|
|
376
|
+
* @param {array} dollarSelfColumns - An array of column objects containing `$self` references.
|
|
377
|
+
*/
|
|
378
|
+
function inferDollarSelfRefs(dollarSelfColumns) {
|
|
379
|
+
do {
|
|
380
|
+
const unprocessedColumns = []
|
|
381
|
+
|
|
382
|
+
for (const currentDollarSelfColumn of dollarSelfColumns) {
|
|
383
|
+
const { ref } = currentDollarSelfColumn
|
|
384
|
+
const stepToFind = ref[1]
|
|
385
|
+
|
|
386
|
+
const referencesOtherDollarSelfColumn = dollarSelfColumns.find(
|
|
387
|
+
otherDollarSelfCol =>
|
|
388
|
+
otherDollarSelfCol !== currentDollarSelfColumn &&
|
|
389
|
+
(otherDollarSelfCol.as
|
|
390
|
+
? stepToFind === otherDollarSelfCol.as
|
|
391
|
+
: stepToFind === otherDollarSelfCol.ref?.[otherDollarSelfCol.ref.length - 1]),
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
if (referencesOtherDollarSelfColumn) {
|
|
395
|
+
unprocessedColumns.push(currentDollarSelfColumn)
|
|
396
|
+
} else {
|
|
397
|
+
handleRef(currentDollarSelfColumn)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
dollarSelfColumns = unprocessedColumns
|
|
402
|
+
} while (dollarSelfColumns.length > 0)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function handleRef(col) {
|
|
406
|
+
inferQueryElement(col)
|
|
407
|
+
const { definition } = col.$refLinks[col.$refLinks.length - 1]
|
|
408
|
+
if (col.cast)
|
|
409
|
+
// final type overwritten -> element not visible anymore
|
|
410
|
+
setElementOnColumns(col, getElementForCast(col))
|
|
411
|
+
else if ((col.ref.length === 1) & (col.ref[0] === '$user'))
|
|
412
|
+
// shortcut to $user.id
|
|
413
|
+
setElementOnColumns(col, queryElements[col.as || '$user'])
|
|
414
|
+
else setElementOnColumns(col, definition)
|
|
415
|
+
}
|
|
416
|
+
|
|
362
417
|
/**
|
|
363
418
|
* This function is responsible for inferring a query element based on a provided column.
|
|
364
419
|
* It initializes and attaches a non-enumerable `$refLinks` property to the column,
|
|
@@ -396,7 +451,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
396
451
|
*/
|
|
397
452
|
|
|
398
453
|
function inferQueryElement(column, insertIntoQueryElements = true, $baseLink = null, context) {
|
|
399
|
-
const { inExists, inExpr, inNestedProjection } = context || {}
|
|
454
|
+
const { inExists, inExpr, inNestedProjection, inCalcElement } = context || {}
|
|
400
455
|
if (column.param) return // parameter references are only resolved into values on execution e.g. :val, :1 or ?
|
|
401
456
|
if (column.args) column.args.forEach(arg => inferQueryElement(arg, false, $baseLink, context)) // e.g. function in expression
|
|
402
457
|
if (column.list) column.list.forEach(arg => inferQueryElement(arg, false, $baseLink, context))
|
|
@@ -438,7 +493,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
438
493
|
const elements = definition.elements || definition._target?.elements
|
|
439
494
|
if (elements && id in elements) {
|
|
440
495
|
const element = elements[id]
|
|
441
|
-
if (!inExists && !inNestedProjection && element.target) {
|
|
496
|
+
if (!inExists && !inNestedProjection && !inCalcElement && element.target) {
|
|
442
497
|
// only fk access in infix filter
|
|
443
498
|
const nextStep = column.ref[1]?.id || column.ref[1]
|
|
444
499
|
// no unmanaged assoc in infix filter path
|
|
@@ -452,7 +507,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
452
507
|
if (nextStep && !(nextStep in element.foreignKeys))
|
|
453
508
|
throw new Error(`Only foreign keys of "${element.name}" can be accessed in infix filter`)
|
|
454
509
|
}
|
|
455
|
-
|
|
510
|
+
const resolvableIn = definition.target ? definition._target : target
|
|
511
|
+
column.$refLinks.push({ definition: elements[id], target: resolvableIn })
|
|
456
512
|
} else {
|
|
457
513
|
stepNotFoundInPredecessor(id, definition.name)
|
|
458
514
|
}
|
|
@@ -477,14 +533,25 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
477
533
|
} else {
|
|
478
534
|
const { definition } = column.$refLinks[i - 1]
|
|
479
535
|
const elements = definition.elements || definition._target?.elements
|
|
480
|
-
|
|
481
|
-
|
|
536
|
+
const element = elements?.[id]
|
|
537
|
+
|
|
538
|
+
if (firstStepIsSelf && element?.isAssociation) {
|
|
539
|
+
throw cds.error(
|
|
540
|
+
`Paths starting with “$self” must not contain steps of type “cds.Association”: ref: [ ${column.ref.map(
|
|
541
|
+
idOnly,
|
|
542
|
+
)} ]`,
|
|
543
|
+
)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const target = definition._target || column.$refLinks[i - 1].target
|
|
547
|
+
if (element) {
|
|
548
|
+
const $refLink = { definition: elements[id], target }
|
|
482
549
|
column.$refLinks.push($refLink)
|
|
483
550
|
} else if (firstStepIsSelf) {
|
|
484
551
|
stepNotFoundInColumnList(id)
|
|
485
552
|
} else if (column.ref[0] === '$user' && pseudoPath) {
|
|
486
553
|
// `$user.some.unknown.element` -> no error
|
|
487
|
-
column.$refLinks.push({ definition: {}, target
|
|
554
|
+
column.$refLinks.push({ definition: {}, target })
|
|
488
555
|
} else if (id === '$dummy') {
|
|
489
556
|
// `some.known.element.$dummy` -> no error; used by cds.ql to simulate joins
|
|
490
557
|
column.$refLinks.push({ definition: { name: '$dummy', parent: column.$refLinks[i - 1].target } })
|
|
@@ -504,7 +571,9 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
504
571
|
: null
|
|
505
572
|
if (foreignKeyAlias) nameSegments.push(foreignKeyAlias)
|
|
506
573
|
else if (skipAliasedFkSegmentsOfNameStack[0] === id) skipAliasedFkSegmentsOfNameStack.shift()
|
|
507
|
-
else
|
|
574
|
+
else {
|
|
575
|
+
nameSegments.push(firstStepIsSelf && i === 1 ? element.__proto__.name : id)
|
|
576
|
+
}
|
|
508
577
|
}
|
|
509
578
|
|
|
510
579
|
if (step.where) {
|
|
@@ -567,7 +636,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
567
636
|
}
|
|
568
637
|
if (queryElements[elementName] !== undefined)
|
|
569
638
|
throw new Error(`Duplicate definition of element “${elementName}”`)
|
|
570
|
-
|
|
639
|
+
const element = getCopyWithAnnos(column, leafArt)
|
|
640
|
+
queryElements[elementName] = element
|
|
571
641
|
}
|
|
572
642
|
}
|
|
573
643
|
}
|
|
@@ -584,19 +654,27 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
584
654
|
return
|
|
585
655
|
}
|
|
586
656
|
}
|
|
587
|
-
const
|
|
657
|
+
const leafArt = column.$refLinks[column.$refLinks.length - 1].definition
|
|
658
|
+
const virtual = (leafArt.virtual || !isPersisted) && !inExpr
|
|
588
659
|
// check if we need to merge the column `ref` into the join tree of the query
|
|
589
|
-
if (!inExists && !virtual && isColumnJoinRelevant(column)) {
|
|
660
|
+
if (!inExists && !virtual && !inCalcElement && isColumnJoinRelevant(column, firstStepIsSelf)) {
|
|
661
|
+
if (originalQuery.UPDATE)
|
|
662
|
+
throw cds.error(
|
|
663
|
+
'Path expressions for UPDATE statements are not supported. Use “where exists” with infix filters instead.',
|
|
664
|
+
)
|
|
590
665
|
Object.defineProperty(column, 'isJoinRelevant', { value: true })
|
|
591
|
-
joinTree.mergeColumn(column)
|
|
666
|
+
joinTree.mergeColumn(column, $baseLink)
|
|
667
|
+
}
|
|
668
|
+
if (leafArt.value && !leafArt.value.stored) {
|
|
669
|
+
resolveCalculatedElement(leafArt, column)
|
|
592
670
|
}
|
|
593
671
|
|
|
594
672
|
/**
|
|
595
673
|
* Resolves and processes the inline attribute of a column in a database query.
|
|
596
674
|
*
|
|
597
|
-
* @param {
|
|
675
|
+
* @param {object} col - The column object with properties: `inline` and `$refLinks`.
|
|
598
676
|
* @param {string} [namePrefix=col.as || col.flatName] - Prefix for naming new columns. Defaults to `col.as` or `col.flatName`.
|
|
599
|
-
* @returns {
|
|
677
|
+
* @returns {object} - An object with resolved and processed inline column definitions.
|
|
600
678
|
*
|
|
601
679
|
* Procedure:
|
|
602
680
|
* 1. Iterate through `inline` array. For each `inlineCol`:
|
|
@@ -650,8 +728,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
650
728
|
/**
|
|
651
729
|
* Resolves a query column which has an `expand` property.
|
|
652
730
|
*
|
|
653
|
-
* @param {
|
|
654
|
-
* @returns {
|
|
731
|
+
* @param {object} col - The column object with properties: `expand` and `$refLinks`.
|
|
732
|
+
* @returns {object} - A `cds.struct` object with expanded column definitions.
|
|
655
733
|
*
|
|
656
734
|
* Procedure:
|
|
657
735
|
* - if `$leafLink` is an association, constructs an `expandSubquery` and infers a new query structure.
|
|
@@ -717,6 +795,58 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
717
795
|
throw new Error(err)
|
|
718
796
|
}
|
|
719
797
|
}
|
|
798
|
+
function resolveCalculatedElement(calcElement) {
|
|
799
|
+
if (alreadySeenCalcElements.has(calcElement)) return
|
|
800
|
+
else alreadySeenCalcElements.add(calcElement)
|
|
801
|
+
const { ref, xpr, func } = calcElement.value
|
|
802
|
+
if (ref || xpr) {
|
|
803
|
+
attachRefLinksToArg(calcElement.value, { definition: calcElement.parent, target: calcElement.parent }, true)
|
|
804
|
+
// column is now fully linked, now we need to find out if we need to merge it into the join tree
|
|
805
|
+
// for that, we calculate all paths from a calc element and merge them into the join tree
|
|
806
|
+
mergePathsIntoJoinTree(calcElement.value)
|
|
807
|
+
}
|
|
808
|
+
if (func) calcElement.value.args?.forEach(arg => inferQueryElement(arg, false)) // {func}.args are optional
|
|
809
|
+
function mergePathsIntoJoinTree(e, basePath = null) {
|
|
810
|
+
basePath = basePath || { $refLinks: [], ref: [] }
|
|
811
|
+
if (e.ref) {
|
|
812
|
+
e.$refLinks.forEach((link, i) => {
|
|
813
|
+
const { definition } = link
|
|
814
|
+
if (!definition.value) {
|
|
815
|
+
basePath.$refLinks.push(link)
|
|
816
|
+
basePath.ref.push(e.ref[i])
|
|
817
|
+
}
|
|
818
|
+
})
|
|
819
|
+
const leafOfCalculatedElementRef = e.$refLinks[e.$refLinks.length - 1].definition
|
|
820
|
+
if (leafOfCalculatedElementRef.value) mergePathsIntoJoinTree(leafOfCalculatedElementRef.value, basePath)
|
|
821
|
+
|
|
822
|
+
mergePathIfNecessary(basePath, e)
|
|
823
|
+
} else if (e.xpr) {
|
|
824
|
+
e.xpr.forEach(step => {
|
|
825
|
+
if (step.ref) {
|
|
826
|
+
const subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
|
|
827
|
+
step.$refLinks.forEach((link, i) => {
|
|
828
|
+
const { definition } = link
|
|
829
|
+
if (definition.value) {
|
|
830
|
+
mergePathsIntoJoinTree(definition.value)
|
|
831
|
+
} else {
|
|
832
|
+
subPath.$refLinks.push(link)
|
|
833
|
+
subPath.ref.push(step.ref[i])
|
|
834
|
+
}
|
|
835
|
+
})
|
|
836
|
+
mergePathIfNecessary(subPath, step)
|
|
837
|
+
}
|
|
838
|
+
})
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function mergePathIfNecessary(p, step) {
|
|
842
|
+
const calcElementIsJoinRelevant = isColumnJoinRelevant(p)
|
|
843
|
+
if (calcElementIsJoinRelevant) {
|
|
844
|
+
if (!calcElement.value.isColumnJoinRelevant) Object.defineProperty(step, 'isJoinRelevant', { value: true })
|
|
845
|
+
joinTree.mergeColumn(p)
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
720
850
|
|
|
721
851
|
/**
|
|
722
852
|
* Checks whether or not the `ref` of the given column is join relevant.
|
|
@@ -760,7 +890,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
760
890
|
|
|
761
891
|
if (!assoc) return false
|
|
762
892
|
if (fkAccess) return false
|
|
763
|
-
|
|
893
|
+
return true
|
|
764
894
|
}
|
|
765
895
|
|
|
766
896
|
/**
|
|
@@ -768,15 +898,20 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
768
898
|
* if there is not already an element with the same name present.
|
|
769
899
|
*/
|
|
770
900
|
function inferElementsFromWildCard() {
|
|
901
|
+
const exclude = _.excluding ? x => _.excluding.includes(x) : () => false
|
|
902
|
+
|
|
771
903
|
if (Object.keys(queryElements).length === 0 && aliases.length === 1) {
|
|
772
904
|
// only one query source and no overwritten columns
|
|
773
905
|
Object.entries(sources[aliases[0]].elements).forEach(([name, element]) => {
|
|
774
|
-
if (element.type !== 'cds.LargeBinary') queryElements[name] = element
|
|
906
|
+
if (!exclude(name) && element.type !== 'cds.LargeBinary') queryElements[name] = element
|
|
907
|
+
if (element.value) {
|
|
908
|
+
// we might have join relevant calculated elements
|
|
909
|
+
resolveCalculatedElement(element)
|
|
910
|
+
}
|
|
775
911
|
})
|
|
776
912
|
return
|
|
777
913
|
}
|
|
778
914
|
|
|
779
|
-
const exclude = _.excluding ? x => _.excluding.includes(x) : () => false
|
|
780
915
|
const ambiguousElements = {}
|
|
781
916
|
Object.entries($combinedElements).forEach(([name, tableAliases]) => {
|
|
782
917
|
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,7 +151,7 @@ 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
157
|
mergeColumn(col) {
|
|
@@ -145,8 +189,9 @@ 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
|
package/package.json
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/db-service",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.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"
|
|
@@ -19,12 +27,16 @@
|
|
|
19
27
|
},
|
|
20
28
|
"scripts": {
|
|
21
29
|
"prettier": "npx prettier --write .",
|
|
22
|
-
"test": "npx jest --silent",
|
|
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",
|
|
23
32
|
"lint": "npx eslint . && npx prettier --check . "
|
|
24
33
|
},
|
|
25
|
-
"dependencies": {},
|
|
26
34
|
"peerDependencies": {
|
|
27
|
-
"@sap/cds": ">=7"
|
|
35
|
+
"@sap/cds": ">=7.1.1"
|
|
28
36
|
},
|
|
29
|
-
"license": "SEE LICENSE"
|
|
37
|
+
"license": "SEE LICENSE",
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@typescript-eslint/eslint-plugin": "^6.2.0",
|
|
40
|
+
"typescript": "^5.1.6"
|
|
41
|
+
}
|
|
30
42
|
}
|