@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/cqn4sql.js
CHANGED
|
@@ -70,13 +70,14 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
70
70
|
// Transform the from clause: association path steps turn into `WHERE EXISTS` subqueries.
|
|
71
71
|
// The already transformed `where` clause is then glued together with the resulting subqueries.
|
|
72
72
|
const { transformedWhere, transformedFrom } = getTransformedFrom(from || entity, transformedProp.where)
|
|
73
|
+
const queryNeedsJoins = inferred.joinTree && !inferred.joinTree.isInitial
|
|
73
74
|
|
|
74
75
|
if (inferred.SELECT) {
|
|
75
76
|
transformedQuery = transformSelectQuery(queryProp, transformedFrom, transformedWhere, transformedQuery)
|
|
76
77
|
} else {
|
|
77
78
|
if (from) {
|
|
78
79
|
transformedProp.from = transformedFrom
|
|
79
|
-
} else {
|
|
80
|
+
} else if (!queryNeedsJoins) {
|
|
80
81
|
transformedProp.entity = transformedFrom
|
|
81
82
|
}
|
|
82
83
|
|
|
@@ -94,7 +95,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
if (
|
|
98
|
+
if (queryNeedsJoins) {
|
|
98
99
|
transformedQuery[kind].from = translateAssocsToJoins(transformedQuery[kind].from)
|
|
99
100
|
}
|
|
100
101
|
}
|
|
@@ -119,7 +120,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
119
120
|
if (columns) {
|
|
120
121
|
transformedQuery.SELECT.columns = getTransformedColumns(columns)
|
|
121
122
|
} else {
|
|
122
|
-
transformedQuery.SELECT.columns = getColumnsForWildcard()
|
|
123
|
+
transformedQuery.SELECT.columns = getColumnsForWildcard(originalQuery.SELECT?.excluding)
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
// Like the WHERE clause, aliases from the SELECT list are not accessible for `group by`/`having` (in most DB's)
|
|
@@ -158,7 +159,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
158
159
|
*
|
|
159
160
|
* @param {string} kind - The type of operation: "INSERT" or "UPSERT".
|
|
160
161
|
*
|
|
161
|
-
* @returns {
|
|
162
|
+
* @returns {object} - The transformed query with updated `into` clause.
|
|
162
163
|
*/
|
|
163
164
|
function transformQueryForInsertUpsert(kind) {
|
|
164
165
|
const { as } = transformedQuery[kind].into
|
|
@@ -170,10 +171,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
170
171
|
/**
|
|
171
172
|
* Transforms a stream query, replacing the `where` and `into` clauses after processing.
|
|
172
173
|
*
|
|
173
|
-
* @param {
|
|
174
|
-
* @param {
|
|
174
|
+
* @param {object} inferred - The inferred object containing the STREAM query.
|
|
175
|
+
* @param {object} transformedQuery - The query object to be transformed.
|
|
175
176
|
*
|
|
176
|
-
* @returns {
|
|
177
|
+
* @returns {object} - The transformed query with updated STREAM clauses.
|
|
177
178
|
*/
|
|
178
179
|
function transformStreamQuery() {
|
|
179
180
|
const { into, where } = inferred.STREAM
|
|
@@ -193,8 +194,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
193
194
|
/**
|
|
194
195
|
* Transforms a search expression to a WHERE clause for a SELECT operation.
|
|
195
196
|
*
|
|
196
|
-
* @param {
|
|
197
|
-
* @param {
|
|
197
|
+
* @param {object} search - The search expression which shall be applied to the searchable columns on the query source.
|
|
198
|
+
* @param {object} from - The FROM clause of the CQN statement.
|
|
198
199
|
*
|
|
199
200
|
* @returns {(Object|Array|undefined)} - If the target of the query contains searchable elements, the function returns an array that represents the WHERE clause.
|
|
200
201
|
* If the SELECT query already contains a WHERE clause, this array includes the existing clause and appends an AND condition with the new 'contains' clause.
|
|
@@ -293,6 +294,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
293
294
|
}
|
|
294
295
|
}
|
|
295
296
|
|
|
297
|
+
function isCalculatedOnRead(def) {
|
|
298
|
+
return def?.value && !def.value.stored
|
|
299
|
+
}
|
|
300
|
+
|
|
296
301
|
/**
|
|
297
302
|
* Walks over a list of columns (ref's, xpr, subqueries, val), applies flattening on structured types and expands wildcards.
|
|
298
303
|
*
|
|
@@ -301,15 +306,27 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
301
306
|
*/
|
|
302
307
|
function getTransformedColumns(columns) {
|
|
303
308
|
const transformedColumns = []
|
|
304
|
-
|
|
305
309
|
for (let i = 0; i < columns.length; i++) {
|
|
306
310
|
const col = columns[i]
|
|
307
311
|
|
|
308
|
-
if (col.
|
|
312
|
+
if (isCalculatedOnRead(col.$refLinks?.[col.$refLinks.length - 1].definition)) {
|
|
313
|
+
const calcElement = resolveCalculatedElement(col)
|
|
314
|
+
transformedColumns.push(calcElement)
|
|
315
|
+
} else if (col.expand) {
|
|
316
|
+
if (col.ref?.length > 1 && col.ref[0] === '$self' && !col.$refLinks[0].definition.kind) {
|
|
317
|
+
const dollarSelfReplacement = calculateDollarSelfColumn(col)
|
|
318
|
+
transformedColumns.push(...getTransformedColumns([dollarSelfReplacement]))
|
|
319
|
+
continue
|
|
320
|
+
}
|
|
309
321
|
handleExpand(col)
|
|
310
322
|
} else if (col.inline) {
|
|
311
323
|
handleInline(col)
|
|
312
324
|
} else if (col.ref) {
|
|
325
|
+
if (col.ref.length > 1 && col.ref[0] === '$self' && !col.$refLinks[0].definition.kind) {
|
|
326
|
+
const dollarSelfReplacement = calculateDollarSelfColumn(col)
|
|
327
|
+
transformedColumns.push(...getTransformedColumns([dollarSelfReplacement]))
|
|
328
|
+
continue
|
|
329
|
+
}
|
|
313
330
|
handleRef(col)
|
|
314
331
|
} else if (col === '*') {
|
|
315
332
|
handleWildcard(columns)
|
|
@@ -391,7 +408,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
391
408
|
if (replaceWith === -1) transformedColumns.push(transformedColumn)
|
|
392
409
|
else transformedColumns.splice(replaceWith, 1, transformedColumn)
|
|
393
410
|
|
|
394
|
-
|
|
411
|
+
setElementOnColumns(transformedColumn, originalQuery.elements[col.as])
|
|
395
412
|
}
|
|
396
413
|
|
|
397
414
|
function getTransformedColumn(col) {
|
|
@@ -424,6 +441,88 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
424
441
|
}
|
|
425
442
|
}
|
|
426
443
|
|
|
444
|
+
function resolveCalculatedElement(column, omitAlias = false, baseLink = null) {
|
|
445
|
+
let value
|
|
446
|
+
|
|
447
|
+
if (column.$refLinks) {
|
|
448
|
+
const { $refLinks } = column
|
|
449
|
+
value = $refLinks[$refLinks.length - 1].definition.value
|
|
450
|
+
baseLink = [...column.$refLinks].reverse().find(link => link.definition.isAssociation) || baseLink
|
|
451
|
+
} else {
|
|
452
|
+
value = column.value
|
|
453
|
+
}
|
|
454
|
+
const { ref, val, xpr, func } = value
|
|
455
|
+
|
|
456
|
+
let res
|
|
457
|
+
if (ref) {
|
|
458
|
+
res = getTransformedTokenStream([value], baseLink)[0]
|
|
459
|
+
} else if (xpr) {
|
|
460
|
+
res = { xpr: getTransformedTokenStream(value.xpr, baseLink) }
|
|
461
|
+
} else if (val) {
|
|
462
|
+
res = { val }
|
|
463
|
+
} else if (func) res = { args: getTransformedTokenStream(value.args), func: value.func }
|
|
464
|
+
if (!omitAlias) res.as = column.as || column.name || column.flatName
|
|
465
|
+
return res
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* This function resolves a `ref` starting with a `$self`.
|
|
470
|
+
* Such a path targets another element of the query by it's implicit, or explicit alias.
|
|
471
|
+
*
|
|
472
|
+
* A `$self` reference may also target another `$self` path. In this case, this function
|
|
473
|
+
* recursively resolves the tail of the `$self` references (`$selfPath.ref.slice(2)`) onto it's
|
|
474
|
+
* new base.
|
|
475
|
+
*
|
|
476
|
+
* @param {object} col with a ref like `[ '$self', <target column>, <optional further path navigation> ]`
|
|
477
|
+
* @param {boolean} omitAlias if we replace a $self reference in an aggregation or a token stream, we must not add an "as" to the result
|
|
478
|
+
*/
|
|
479
|
+
function calculateDollarSelfColumn(col, omitAlias = false) {
|
|
480
|
+
const dummyColumn = buildDummyColumnForDollarSelf({ ...col }, col.$refLinks)
|
|
481
|
+
|
|
482
|
+
return dummyColumn
|
|
483
|
+
|
|
484
|
+
function buildDummyColumnForDollarSelf(dollarSelfColumn, $refLinks) {
|
|
485
|
+
const { ref, as } = dollarSelfColumn
|
|
486
|
+
const stepToFind = ref[1]
|
|
487
|
+
let referencedColumn = inferred.SELECT.columns.find(
|
|
488
|
+
otherColumn =>
|
|
489
|
+
otherColumn !== dollarSelfColumn &&
|
|
490
|
+
(otherColumn.as
|
|
491
|
+
? stepToFind === otherColumn.as
|
|
492
|
+
: stepToFind === otherColumn.ref?.[otherColumn.ref.length - 1]),
|
|
493
|
+
)
|
|
494
|
+
if (referencedColumn.ref?.[0] === '$self') {
|
|
495
|
+
referencedColumn = buildDummyColumnForDollarSelf({ ...referencedColumn }, referencedColumn.$refLinks)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (referencedColumn.ref) {
|
|
499
|
+
dollarSelfColumn.ref = [...referencedColumn.ref, ...dollarSelfColumn.ref.slice(2)]
|
|
500
|
+
Object.defineProperties(dollarSelfColumn, {
|
|
501
|
+
flatName: {
|
|
502
|
+
value: dollarSelfColumn.ref.join('_'),
|
|
503
|
+
},
|
|
504
|
+
isJoinRelevant: {
|
|
505
|
+
value: referencedColumn.isJoinRelevant,
|
|
506
|
+
},
|
|
507
|
+
$refLinks: {
|
|
508
|
+
value: [...referencedColumn.$refLinks, ...$refLinks.slice(2)],
|
|
509
|
+
},
|
|
510
|
+
})
|
|
511
|
+
} else {
|
|
512
|
+
// target column is `val` or `xpr`, destructure and throw away the ref with the $self
|
|
513
|
+
// eslint-disable-next-line no-unused-vars
|
|
514
|
+
const { xpr, val, ref, as: _as, ...rest } = referencedColumn
|
|
515
|
+
if (xpr) rest.xpr = xpr
|
|
516
|
+
else rest.val = val
|
|
517
|
+
dollarSelfColumn = { ...rest } // reassign dummyColumn without 'ref'
|
|
518
|
+
if (!omitAlias) dollarSelfColumn.as = as
|
|
519
|
+
}
|
|
520
|
+
return dollarSelfColumn.ref?.[0] === '$self'
|
|
521
|
+
? buildDummyColumnForDollarSelf({ ...dollarSelfColumn }, $refLinks)
|
|
522
|
+
: dollarSelfColumn
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
427
526
|
/**
|
|
428
527
|
* Calculates the columns for a nested projection on a structure.
|
|
429
528
|
*
|
|
@@ -565,7 +664,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
565
664
|
*
|
|
566
665
|
* @param {CSN.column} column - The column with the 'expand' property to be transformed into a subquery.
|
|
567
666
|
*
|
|
568
|
-
* @returns {
|
|
667
|
+
* @returns {object} Returns a subquery correlated with the enclosing query, with added properties `expand:true` and `one:true|false`.
|
|
569
668
|
*/
|
|
570
669
|
function expandColumn(column) {
|
|
571
670
|
let outerAlias
|
|
@@ -622,7 +721,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
622
721
|
if (isLocalized(inferred.target)) subquery.SELECT.localized = true
|
|
623
722
|
const expanded = transformSubquery(subquery)
|
|
624
723
|
const correlated = _correlate({ ...expanded, as: columnAlias }, outerAlias)
|
|
625
|
-
Object.defineProperty(correlated, 'elements', { value: subquery.elements })
|
|
724
|
+
Object.defineProperty(correlated, 'elements', { value: subquery.elements, writable: true })
|
|
626
725
|
return correlated
|
|
627
726
|
|
|
628
727
|
function _correlate(subq, outer) {
|
|
@@ -659,7 +758,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
659
758
|
const res = []
|
|
660
759
|
for (let i = 0; i < columns.length; i++) {
|
|
661
760
|
const col = columns[i]
|
|
662
|
-
if (col.
|
|
761
|
+
if (isCalculatedOnRead(col.$refLinks?.[col.$refLinks.length - 1].definition)) {
|
|
762
|
+
const calcElement = resolveCalculatedElement(col, true)
|
|
763
|
+
res.push(calcElement)
|
|
764
|
+
} else if (col.isJoinRelevant) {
|
|
663
765
|
const tableAlias$refLink = getQuerySourceName(col)
|
|
664
766
|
const transformedColumn = {
|
|
665
767
|
ref: [tableAlias$refLink, getFullName(col.$refLinks[col.$refLinks.length - 1].definition)],
|
|
@@ -671,6 +773,11 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
671
773
|
res.push({ ...col })
|
|
672
774
|
} else if (col.ref) {
|
|
673
775
|
if (col.$refLinks.some(link => link.definition._target?.['@cds.persistence.skip'] === true)) continue
|
|
776
|
+
if (col.ref.length > 1 && col.ref[0] === '$self' && !col.$refLinks[0].definition.kind) {
|
|
777
|
+
const dollarSelfReplacement = calculateDollarSelfColumn(col)
|
|
778
|
+
res.push(...getTransformedOrderByGroupBy([dollarSelfReplacement], inOrderBy))
|
|
779
|
+
continue
|
|
780
|
+
}
|
|
674
781
|
const { target } = col.$refLinks[0]
|
|
675
782
|
const tableAlias = target.SELECT ? null : getQuerySourceName(col) // do not prepend TA if orderBy column addresses element of query
|
|
676
783
|
const leaf = col.$refLinks[col.$refLinks.length - 1].definition
|
|
@@ -754,6 +861,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
754
861
|
// for wildcard on subquery in from, just reference the elements
|
|
755
862
|
if (tableAlias.SELECT && !element.elements && !element.target) {
|
|
756
863
|
wildcardColumns.push(index ? { ref: [index, k] } : { ref: [k] })
|
|
864
|
+
} else if (isCalculatedOnRead(element)) {
|
|
865
|
+
wildcardColumns.push(resolveCalculatedElement(element))
|
|
757
866
|
} else {
|
|
758
867
|
const flatColumns = getFlatColumnsFor(element, { tableAlias: index }, [], { exclude, replace }, true)
|
|
759
868
|
wildcardColumns.push(...flatColumns)
|
|
@@ -903,8 +1012,9 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
903
1012
|
if (columnAlias) flatColumn = { ref: [fkBaseName], as: `${columnAlias}_${fk.ref.join('_')}` }
|
|
904
1013
|
else flatColumn = { ref: [fkBaseName] }
|
|
905
1014
|
if (tableAlias) flatColumn.ref.unshift(tableAlias)
|
|
906
|
-
|
|
907
|
-
|
|
1015
|
+
|
|
1016
|
+
setElementOnColumns(flatColumn, fkElement)
|
|
1017
|
+
Object.defineProperty(flatColumn, '_csnPath', { value: csnPath, writable: true })
|
|
908
1018
|
flatColumns.push(flatColumn)
|
|
909
1019
|
}
|
|
910
1020
|
})
|
|
@@ -934,8 +1044,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
934
1044
|
}
|
|
935
1045
|
if (column.sort) flatRef.sort = column.sort
|
|
936
1046
|
if (columnAlias) flatRef.as = columnAlias
|
|
937
|
-
|
|
938
|
-
Object.defineProperty(flatRef, '_csnPath', { value: csnPath })
|
|
1047
|
+
setElementOnColumns(flatRef, element)
|
|
1048
|
+
Object.defineProperty(flatRef, '_csnPath', { value: csnPath, writable: true })
|
|
939
1049
|
return [flatRef]
|
|
940
1050
|
|
|
941
1051
|
function getReplacement(from) {
|
|
@@ -962,11 +1072,11 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
962
1072
|
* @returns {object[]} - The transformed token stream.
|
|
963
1073
|
*/
|
|
964
1074
|
function getTransformedTokenStream(tokenStream, $baseLink = null) {
|
|
965
|
-
const
|
|
1075
|
+
const transformedTokenStream = []
|
|
966
1076
|
for (let i = 0; i < tokenStream.length; i++) {
|
|
967
1077
|
const token = tokenStream[i]
|
|
968
1078
|
if (token === 'exists') {
|
|
969
|
-
|
|
1079
|
+
transformedTokenStream.push(token)
|
|
970
1080
|
const whereExistsSubSelects = []
|
|
971
1081
|
const { ref, $refLinks } = tokenStream[i + 1]
|
|
972
1082
|
if (!ref) continue
|
|
@@ -1008,7 +1118,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1008
1118
|
}
|
|
1009
1119
|
|
|
1010
1120
|
const whereExists = { SELECT: whereExistsSubqueries(whereExistsSubSelects) }
|
|
1011
|
-
|
|
1121
|
+
transformedTokenStream[i + 1] = whereExists
|
|
1012
1122
|
// skip newly created subquery from being iterated
|
|
1013
1123
|
i += 1
|
|
1014
1124
|
} else if (token.list) {
|
|
@@ -1021,14 +1131,14 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1021
1131
|
typeof precedingTwoTokens[1] === 'string' ? precedingTwoTokens[1].toLowerCase() : ''
|
|
1022
1132
|
|
|
1023
1133
|
if (firstPrecedingToken === 'not') {
|
|
1024
|
-
|
|
1134
|
+
transformedTokenStream.splice(i - 2, 2, 'is', 'not', 'null')
|
|
1025
1135
|
} else if (secondPrecedingToken === 'in') {
|
|
1026
|
-
|
|
1136
|
+
transformedTokenStream.splice(i - 1, 1, '=', { val: null })
|
|
1027
1137
|
} else {
|
|
1028
|
-
|
|
1138
|
+
transformedTokenStream.push({ list: [] })
|
|
1029
1139
|
}
|
|
1030
1140
|
} else {
|
|
1031
|
-
|
|
1141
|
+
transformedTokenStream.push({ list: getTransformedTokenStream(token.list) })
|
|
1032
1142
|
}
|
|
1033
1143
|
} else if (tokenStream.length === 1 && token.val && $baseLink) {
|
|
1034
1144
|
// infix filter - OData variant w/o mentioning key --> flatten out and compare each leaf to token.val
|
|
@@ -1046,12 +1156,12 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1046
1156
|
throw new Error('Filters can only be applied to managed associations which result in a single foreign key')
|
|
1047
1157
|
flatKeys.forEach(c => keyValComparisons.push([...[c, '=', token]]))
|
|
1048
1158
|
keyValComparisons.forEach((kv, j) =>
|
|
1049
|
-
|
|
1159
|
+
transformedTokenStream.push(...kv) && keyValComparisons[j + 1] ? transformedTokenStream.push('and') : null,
|
|
1050
1160
|
)
|
|
1051
1161
|
} else if (token.ref && token.param) {
|
|
1052
|
-
|
|
1162
|
+
transformedTokenStream.push({ ...token })
|
|
1053
1163
|
} else if (pseudos.elements[token.ref?.[0]]) {
|
|
1054
|
-
|
|
1164
|
+
transformedTokenStream.push({ ...token })
|
|
1055
1165
|
} else {
|
|
1056
1166
|
// expand `struct = null | struct2`
|
|
1057
1167
|
const { definition } = token.$refLinks?.[token.$refLinks.length - 1] || {}
|
|
@@ -1075,7 +1185,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1075
1185
|
cds.error(`The operator "${next}" is not supported for structure comparison`)
|
|
1076
1186
|
const newTokens = expandComparison(token, ops, rhs)
|
|
1077
1187
|
const needXpr = Boolean(tokenStream[i - 1] || tokenStream[indexRhs + 1])
|
|
1078
|
-
|
|
1188
|
+
transformedTokenStream.push(...(needXpr ? [asXpr(newTokens)] : newTokens))
|
|
1079
1189
|
i = indexRhs // jump to next relevant index
|
|
1080
1190
|
}
|
|
1081
1191
|
} else {
|
|
@@ -1084,6 +1194,17 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1084
1194
|
|
|
1085
1195
|
let result = is_regexp(token?.val) ? token : copy(token) // REVISIT: too expensive! //
|
|
1086
1196
|
if (token.ref) {
|
|
1197
|
+
const { definition } = token.$refLinks[token.$refLinks.length - 1]
|
|
1198
|
+
if (isCalculatedOnRead(definition)) {
|
|
1199
|
+
const calculatedElement = resolveCalculatedElement(token, true, $baseLink)
|
|
1200
|
+
transformedTokenStream.push(calculatedElement)
|
|
1201
|
+
continue
|
|
1202
|
+
}
|
|
1203
|
+
if (token.ref.length > 1 && token.ref[0] === '$self' && !token.$refLinks[0].definition.kind) {
|
|
1204
|
+
const dollarSelfReplacement = [calculateDollarSelfColumn(token, true)]
|
|
1205
|
+
transformedTokenStream.push(...getTransformedTokenStream(dollarSelfReplacement))
|
|
1206
|
+
continue
|
|
1207
|
+
}
|
|
1087
1208
|
const tableAlias = getQuerySourceName(token, $baseLink)
|
|
1088
1209
|
if (!$baseLink && token.isJoinRelevant) {
|
|
1089
1210
|
result.ref = [tableAlias, getFullName(token.$refLinks[token.$refLinks.length - 1].definition)]
|
|
@@ -1106,11 +1227,11 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1106
1227
|
})
|
|
1107
1228
|
}
|
|
1108
1229
|
|
|
1109
|
-
|
|
1230
|
+
transformedTokenStream.push(result)
|
|
1110
1231
|
}
|
|
1111
1232
|
}
|
|
1112
1233
|
}
|
|
1113
|
-
return
|
|
1234
|
+
return transformedTokenStream
|
|
1114
1235
|
}
|
|
1115
1236
|
|
|
1116
1237
|
/**
|
|
@@ -1394,11 +1515,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1394
1515
|
each.ref[0] in { $self: true, $projection: true } ? getParentEntity(assoc) : target,
|
|
1395
1516
|
),
|
|
1396
1517
|
)
|
|
1397
|
-
|
|
1398
|
-
function getParentEntity(element) {
|
|
1399
|
-
if (element.kind === 'entity') return element
|
|
1400
|
-
else return getParentEntity(element.parent)
|
|
1401
|
-
}
|
|
1402
1518
|
}
|
|
1403
1519
|
|
|
1404
1520
|
/**
|
|
@@ -1797,7 +1913,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1797
1913
|
}
|
|
1798
1914
|
|
|
1799
1915
|
function getCombinedElementAlias(node) {
|
|
1800
|
-
return getLastStringSegment(inferred.$combinedElements[node.ref[0].id || node.ref[0]][0].index)
|
|
1916
|
+
return getLastStringSegment(inferred.$combinedElements[node.ref[0].id || node.ref[0]]?.[0].index)
|
|
1801
1917
|
}
|
|
1802
1918
|
}
|
|
1803
1919
|
}
|
|
@@ -1842,5 +1958,23 @@ function getLastStringSegment(str) {
|
|
|
1842
1958
|
return index != -1 ? str.substring(index + 1) : str
|
|
1843
1959
|
}
|
|
1844
1960
|
|
|
1961
|
+
function getParentEntity(element) {
|
|
1962
|
+
if (element.kind === 'entity') return element
|
|
1963
|
+
else return getParentEntity(element.parent)
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
/**
|
|
1967
|
+
* Assigns the given `element` as non-enumerable property 'element' onto `col`.
|
|
1968
|
+
*
|
|
1969
|
+
* @param {object} col
|
|
1970
|
+
* @param {csn.Element} element
|
|
1971
|
+
*/
|
|
1972
|
+
function setElementOnColumns(col, element) {
|
|
1973
|
+
Object.defineProperty(col, 'element', {
|
|
1974
|
+
value: element,
|
|
1975
|
+
writable: true,
|
|
1976
|
+
})
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1845
1979
|
const idOnly = ref => ref.id || ref
|
|
1846
1980
|
const is_regexp = x => x?.constructor?.name === 'RegExp' // NOTE: x instanceof RegExp doesn't work in repl
|
package/lib/deep-queries.js
CHANGED
|
@@ -4,6 +4,17 @@ const { _target_name4 } = require('./SQLService')
|
|
|
4
4
|
|
|
5
5
|
const handledDeep = Symbol('handledDeep')
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* @callback nextCallback
|
|
9
|
+
* @param {Error|undefined} error
|
|
10
|
+
* @returns {Promise<unknown>}
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {import('@sap/cds/apis/services').Request} req
|
|
15
|
+
* @param {nextCallback} next
|
|
16
|
+
* @returns {Promise<number>}
|
|
17
|
+
*/
|
|
7
18
|
async function onDeep(req, next) {
|
|
8
19
|
const { query } = req
|
|
9
20
|
// REVISIT: req.target does not match the query.INSERT target for path insert
|
|
@@ -136,11 +147,16 @@ const _calculateExpandColumns = (target, data, expandColumns = [], elementMap =
|
|
|
136
147
|
}
|
|
137
148
|
}
|
|
138
149
|
|
|
150
|
+
/**
|
|
151
|
+
* @param {import('@sap/cds/apis/cqn').Query} query
|
|
152
|
+
* @param {import('@sap/cds/apis/csn').Definition} target
|
|
153
|
+
*/
|
|
139
154
|
const getExpandForDeep = (query, target) => {
|
|
140
155
|
const from = query.DELETE?.from || query.UPDATE?.entity
|
|
141
156
|
const data = query.UPDATE?.data || null
|
|
142
157
|
const where = query.DELETE?.where || query.UPDATE?.where
|
|
143
158
|
|
|
159
|
+
/** @type {import("@sap/cds/apis/ql").SELECT<unknown>} */
|
|
144
160
|
const cqn = SELECT.from(from)
|
|
145
161
|
if (where) cqn.SELECT.where = where
|
|
146
162
|
|
|
@@ -150,6 +166,12 @@ const getExpandForDeep = (query, target) => {
|
|
|
150
166
|
return cqn
|
|
151
167
|
}
|
|
152
168
|
|
|
169
|
+
/**
|
|
170
|
+
* @param {import('@sap/cds/apis/cqn').Query} query
|
|
171
|
+
* @param {unknown[]} dbData
|
|
172
|
+
* @param {import('@sap/cds/apis/csn').Definition} target
|
|
173
|
+
* @returns
|
|
174
|
+
*/
|
|
153
175
|
const getDeepQueries = (query, dbData, target) => {
|
|
154
176
|
let queryData
|
|
155
177
|
if (query.INSERT) {
|
|
@@ -174,6 +196,11 @@ const _hasManagedElements = target => {
|
|
|
174
196
|
return Object.keys(target.elements).filter(elementName => target.elements[elementName]['@cds.on.update']).length > 0
|
|
175
197
|
}
|
|
176
198
|
|
|
199
|
+
/**
|
|
200
|
+
* @param {unknown[]} diff
|
|
201
|
+
* @param {import('@sap/cds/apis/csn').Definition} target
|
|
202
|
+
* @returns {import('@sap/cds/apis/cqn').Query[]}
|
|
203
|
+
*/
|
|
177
204
|
const _getDeepQueries = (diff, target) => {
|
|
178
205
|
const queries = []
|
|
179
206
|
|
package/lib/fill-in-keys.js
CHANGED
|
@@ -41,6 +41,16 @@ const generateUUIDandPropagateKeys = (target, data, event) => {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* @callback nextCallback
|
|
46
|
+
* @param {Error|undefined} error
|
|
47
|
+
* @returns {Promise<unknown>}
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {import('@sap/cds/apis/services').Request} req
|
|
52
|
+
* @param {nextCallback} next
|
|
53
|
+
*/
|
|
44
54
|
module.exports = async function fill_in_keys(req, next) {
|
|
45
55
|
// REVISIT dummy handler until we have input processing
|
|
46
56
|
if (!req.target || !this.model || req.target._unresolved) return next()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as cqn from '@sap/cds/apis/cqn'
|
|
2
|
+
import * as csn from '@sap/cds/apis/csn'
|
|
3
|
+
|
|
4
|
+
type linkedQuery = {
|
|
5
|
+
target: csn.Definition
|
|
6
|
+
elements: elements
|
|
7
|
+
}
|
|
8
|
+
export type SELECT = cqn.SELECT & linkedQuery
|
|
9
|
+
export type INSERT = cqn.INSERT & linkedQuery
|
|
10
|
+
export type UPSERT = cqn.UPSERT & linkedQuery
|
|
11
|
+
export type UPDATE = cqn.UPDATE & linkedQuery
|
|
12
|
+
export type STREAM = {
|
|
13
|
+
STREAM: { into: cqn.name | ref; data: ReadableStream<Buffer>; from: cqn.source; column?: ref; where: predicate }
|
|
14
|
+
} & linkedQuery
|
|
15
|
+
export type DELETE = cqn.DELETE & linkedQuery
|
|
16
|
+
export type CREATE = cqn.CREATE & linkedQuery
|
|
17
|
+
export type DROP = cqn.DROP & linkedQuery
|
|
18
|
+
|
|
19
|
+
export type Query = SELECT | INSERT | UPSERT | UPDATE | DELETE | CREATE | DROP
|
|
20
|
+
|
|
21
|
+
export type element = csn.Element & {
|
|
22
|
+
key?: boolean
|
|
23
|
+
virtual?: boolean
|
|
24
|
+
unique?: boolean
|
|
25
|
+
notNull?: boolean
|
|
26
|
+
}
|
|
27
|
+
export type elements = {
|
|
28
|
+
[name: string]: element
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type col = cqn.column_expr & { element: element }
|
|
32
|
+
|
|
33
|
+
export type list = {
|
|
34
|
+
list: cqn.expr[]
|
|
35
|
+
}
|
|
36
|
+
// Passthrough
|
|
37
|
+
export type source = cqn.source
|
|
38
|
+
export type ref = cqn.ref
|
|
39
|
+
export type val = cqn.val
|
|
40
|
+
export type xpr = cqn.xpr
|
|
41
|
+
export type expr = cqn.expr
|
|
42
|
+
export type func = cqn.function_call
|
|
43
|
+
export type predicate = cqn.predicate
|
|
44
|
+
export type ordering_term = cqn.ordering_term
|
|
45
|
+
export type limit = { rows: val; offset: val }
|