@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/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
|
*
|
|
@@ -304,13 +309,23 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
304
309
|
for (let i = 0; i < columns.length; i++) {
|
|
305
310
|
const col = columns[i]
|
|
306
311
|
|
|
307
|
-
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) {
|
|
308
316
|
if (col.ref?.length > 1 && col.ref[0] === '$self' && !col.$refLinks[0].definition.kind) {
|
|
309
317
|
const dollarSelfReplacement = calculateDollarSelfColumn(col)
|
|
310
318
|
transformedColumns.push(...getTransformedColumns([dollarSelfReplacement]))
|
|
311
319
|
continue
|
|
312
320
|
}
|
|
313
|
-
|
|
321
|
+
transformedColumns.push(() => {
|
|
322
|
+
const expandResult = handleExpand(col)
|
|
323
|
+
if (expandResult.length > 1) {
|
|
324
|
+
return expandResult
|
|
325
|
+
} else {
|
|
326
|
+
return expandResult[0]
|
|
327
|
+
}
|
|
328
|
+
})
|
|
314
329
|
} else if (col.inline) {
|
|
315
330
|
handleInline(col)
|
|
316
331
|
} else if (col.ref) {
|
|
@@ -322,10 +337,34 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
322
337
|
handleRef(col)
|
|
323
338
|
} else if (col === '*') {
|
|
324
339
|
handleWildcard(columns)
|
|
340
|
+
} else if (col.SELECT) {
|
|
341
|
+
handleSubquery(col)
|
|
325
342
|
} else {
|
|
326
343
|
handleDefault(col)
|
|
327
344
|
}
|
|
328
345
|
}
|
|
346
|
+
// subqueries are processed in the end
|
|
347
|
+
for (let i = 0; i < transformedColumns.length; i++) {
|
|
348
|
+
const c = transformedColumns[i]
|
|
349
|
+
if (typeof c === 'function') {
|
|
350
|
+
const res = c() || [] // target of expand / subquery could also be skipped -> no result
|
|
351
|
+
if (res.length !== undefined) {
|
|
352
|
+
transformedColumns.splice(i, 1, ...res)
|
|
353
|
+
i += res.length - 1
|
|
354
|
+
} else {
|
|
355
|
+
const replaceWith = res.as
|
|
356
|
+
? transformedColumns.findIndex(t => (t.as || t.ref?.[t.ref.length - 1]) === res.as)
|
|
357
|
+
: -1
|
|
358
|
+
if (replaceWith === -1) transformedColumns.splice(i, 1, res)
|
|
359
|
+
else {
|
|
360
|
+
transformedColumns.splice(replaceWith, 1, res)
|
|
361
|
+
transformedColumns.splice(i, 1)
|
|
362
|
+
// When removing an element, the next element moves to the current index
|
|
363
|
+
i--
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
329
368
|
|
|
330
369
|
if (transformedColumns.length === 0 && columns.length) {
|
|
331
370
|
handleEmptyColumns(columns)
|
|
@@ -333,16 +372,35 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
333
372
|
|
|
334
373
|
return transformedColumns
|
|
335
374
|
|
|
375
|
+
function handleSubquery(col) {
|
|
376
|
+
if (isLocalized(inferred.target)) col.SELECT.localized = true
|
|
377
|
+
if (!col.SELECT.from.as) {
|
|
378
|
+
const uniqueSubqueryAlias = inferred.joinTree.addNextAvailableTableAlias(
|
|
379
|
+
getLastStringSegment(col.SELECT.from.ref[col.SELECT.from.ref.length - 1]),
|
|
380
|
+
originalQuery.outerQueries,
|
|
381
|
+
)
|
|
382
|
+
Object.defineProperty(col.SELECT.from, 'uniqueSubqueryAlias', { value: uniqueSubqueryAlias })
|
|
383
|
+
}
|
|
384
|
+
transformedColumns.push(() => {
|
|
385
|
+
const res = transformSubquery(col)
|
|
386
|
+
if (col.as) res.as = col.as
|
|
387
|
+
return res
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
|
|
336
391
|
function handleExpand(col) {
|
|
337
392
|
const { $refLinks } = col
|
|
393
|
+
const res = []
|
|
338
394
|
const last = $refLinks?.[$refLinks.length - 1]
|
|
339
395
|
if (last && !last.skipExpand && last.definition.isAssociation) {
|
|
340
396
|
const expandedSubqueryColumn = expandColumn(col)
|
|
341
|
-
|
|
397
|
+
setElementOnColumns(expandedSubqueryColumn, col.element)
|
|
398
|
+
res.push(expandedSubqueryColumn)
|
|
342
399
|
} else if (!last?.skipExpand) {
|
|
343
400
|
const expandCols = nestedProjectionOnStructure(col, 'expand')
|
|
344
|
-
|
|
401
|
+
res.push(...expandCols)
|
|
345
402
|
}
|
|
403
|
+
return res
|
|
346
404
|
}
|
|
347
405
|
|
|
348
406
|
function handleInline(col) {
|
|
@@ -375,10 +433,11 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
375
433
|
|
|
376
434
|
if (col.$refLinks.some(link => link.definition._target?.['@cds.persistence.skip'] === true)) return
|
|
377
435
|
|
|
436
|
+
const getName = col => col.as || col.ref?.at(-1)
|
|
378
437
|
const flatColumns = getFlatColumnsFor(col, { baseName, columnAlias, tableAlias })
|
|
379
438
|
flatColumns.forEach(flatColumn => {
|
|
380
|
-
const
|
|
381
|
-
if (!
|
|
439
|
+
const name = getName(flatColumn)
|
|
440
|
+
if (!transformedColumns.some(inserted => getName(inserted) === name)) transformedColumns.push(flatColumn)
|
|
382
441
|
})
|
|
383
442
|
}
|
|
384
443
|
|
|
@@ -396,7 +455,9 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
396
455
|
let transformedColumn = getTransformedColumn(col)
|
|
397
456
|
if (col.as) transformedColumn.as = col.as
|
|
398
457
|
|
|
399
|
-
const replaceWith = transformedColumns.findIndex(
|
|
458
|
+
const replaceWith = transformedColumns.findIndex(
|
|
459
|
+
t => (t.as || t.ref?.[t.ref.length - 1]) === transformedColumn.as,
|
|
460
|
+
)
|
|
400
461
|
if (replaceWith === -1) transformedColumns.push(transformedColumn)
|
|
401
462
|
else transformedColumns.splice(replaceWith, 1, transformedColumn)
|
|
402
463
|
|
|
@@ -404,17 +465,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
404
465
|
}
|
|
405
466
|
|
|
406
467
|
function getTransformedColumn(col) {
|
|
407
|
-
if (col.
|
|
408
|
-
if (isLocalized(inferred.target)) col.SELECT.localized = true
|
|
409
|
-
if (!col.SELECT.from.as) {
|
|
410
|
-
const uniqueSubqueryAlias = inferred.joinTree.addNextAvailableTableAlias(
|
|
411
|
-
getLastStringSegment(col.SELECT.from.ref[col.SELECT.from.ref.length - 1]),
|
|
412
|
-
originalQuery.outerQueries,
|
|
413
|
-
)
|
|
414
|
-
Object.defineProperty(col.SELECT.from, 'uniqueSubqueryAlias', { value: uniqueSubqueryAlias })
|
|
415
|
-
}
|
|
416
|
-
return transformSubquery(col)
|
|
417
|
-
} else if (col.xpr) {
|
|
468
|
+
if (col.xpr) {
|
|
418
469
|
return { xpr: getTransformedTokenStream(col.xpr) }
|
|
419
470
|
} else if (col.func) {
|
|
420
471
|
return {
|
|
@@ -433,6 +484,36 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
433
484
|
}
|
|
434
485
|
}
|
|
435
486
|
|
|
487
|
+
function resolveCalculatedElement(column, omitAlias = false, baseLink = null) {
|
|
488
|
+
let value
|
|
489
|
+
|
|
490
|
+
if (column.$refLinks) {
|
|
491
|
+
const { $refLinks } = column
|
|
492
|
+
value = $refLinks[$refLinks.length - 1].definition.value
|
|
493
|
+
if (column.$refLinks.length > 1) {
|
|
494
|
+
baseLink =
|
|
495
|
+
[...$refLinks].reverse().find($refLink => $refLink.definition.isAssociation) ||
|
|
496
|
+
// if there is no association in the path, the table alias is the base link
|
|
497
|
+
// TA might refer to subquery -> we need to propagate the alias to all paths of the calc element
|
|
498
|
+
column.$refLinks[0]
|
|
499
|
+
}
|
|
500
|
+
} else {
|
|
501
|
+
value = column.value
|
|
502
|
+
}
|
|
503
|
+
const { ref, val, xpr, func } = value
|
|
504
|
+
|
|
505
|
+
let res
|
|
506
|
+
if (ref) {
|
|
507
|
+
res = getTransformedTokenStream([value], baseLink)[0]
|
|
508
|
+
} else if (xpr) {
|
|
509
|
+
res = { xpr: getTransformedTokenStream(value.xpr, baseLink) }
|
|
510
|
+
} else if (val) {
|
|
511
|
+
res = { val }
|
|
512
|
+
} else if (func) res = { args: getTransformedTokenStream(value.args, baseLink), func: value.func }
|
|
513
|
+
if (!omitAlias) res.as = column.as || column.name || column.flatName
|
|
514
|
+
return res
|
|
515
|
+
}
|
|
516
|
+
|
|
436
517
|
/**
|
|
437
518
|
* This function resolves a `ref` starting with a `$self`.
|
|
438
519
|
* Such a path targets another element of the query by it's implicit, or explicit alias.
|
|
@@ -467,7 +548,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
467
548
|
dollarSelfColumn.ref = [...referencedColumn.ref, ...dollarSelfColumn.ref.slice(2)]
|
|
468
549
|
Object.defineProperties(dollarSelfColumn, {
|
|
469
550
|
flatName: {
|
|
470
|
-
value: dollarSelfColumn.ref.join('_'),
|
|
551
|
+
value: referencedColumn.$refLinks[0].definition.kind === 'entity' ? dollarSelfColumn.ref.slice(1).join('_') : dollarSelfColumn.ref.join('_'),
|
|
471
552
|
},
|
|
472
553
|
isJoinRelevant: {
|
|
473
554
|
value: referencedColumn.isJoinRelevant,
|
|
@@ -512,7 +593,12 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
512
593
|
if (nestedProjection.ref) {
|
|
513
594
|
const augmentedInlineCol = { ...nestedProjection }
|
|
514
595
|
augmentedInlineCol.ref = col.ref ? [...col.ref, ...nestedProjection.ref] : nestedProjection.ref
|
|
515
|
-
if (
|
|
596
|
+
if (
|
|
597
|
+
col.as ||
|
|
598
|
+
nestedProjection.as ||
|
|
599
|
+
nestedProjection.$refLinks[nestedProjection.$refLinks.length - 1].definition.value ||
|
|
600
|
+
nestedProjection.isJoinRelevant
|
|
601
|
+
) {
|
|
516
602
|
augmentedInlineCol.as = nameParts.join('_')
|
|
517
603
|
}
|
|
518
604
|
Object.defineProperties(augmentedInlineCol, {
|
|
@@ -632,7 +718,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
632
718
|
*
|
|
633
719
|
* @param {CSN.column} column - The column with the 'expand' property to be transformed into a subquery.
|
|
634
720
|
*
|
|
635
|
-
* @returns {
|
|
721
|
+
* @returns {object} Returns a subquery correlated with the enclosing query, with added properties `expand:true` and `one:true|false`.
|
|
636
722
|
*/
|
|
637
723
|
function expandColumn(column) {
|
|
638
724
|
let outerAlias
|
|
@@ -726,7 +812,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
726
812
|
const res = []
|
|
727
813
|
for (let i = 0; i < columns.length; i++) {
|
|
728
814
|
const col = columns[i]
|
|
729
|
-
if (col.
|
|
815
|
+
if (isCalculatedOnRead(col.$refLinks?.[col.$refLinks.length - 1].definition)) {
|
|
816
|
+
const calcElement = resolveCalculatedElement(col, true)
|
|
817
|
+
res.push(calcElement)
|
|
818
|
+
} else if (col.isJoinRelevant) {
|
|
730
819
|
const tableAlias$refLink = getQuerySourceName(col)
|
|
731
820
|
const transformedColumn = {
|
|
732
821
|
ref: [tableAlias$refLink, getFullName(col.$refLinks[col.$refLinks.length - 1].definition)],
|
|
@@ -822,10 +911,12 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
822
911
|
const { index, tableAlias } = inferred.$combinedElements[k][0]
|
|
823
912
|
const element = tableAlias.elements[k]
|
|
824
913
|
// ignore FK for odata csn / ignore blobs from wildcard expansion
|
|
825
|
-
if (
|
|
914
|
+
if (isManagedAssocInFlatMode(element) || (element['@Core.MediaType'] && !element['@Core.IsURL'])) return
|
|
826
915
|
// for wildcard on subquery in from, just reference the elements
|
|
827
916
|
if (tableAlias.SELECT && !element.elements && !element.target) {
|
|
828
917
|
wildcardColumns.push(index ? { ref: [index, k] } : { ref: [k] })
|
|
918
|
+
} else if (isCalculatedOnRead(element)) {
|
|
919
|
+
wildcardColumns.push(resolveCalculatedElement(element))
|
|
829
920
|
} else {
|
|
830
921
|
const flatColumns = getFlatColumnsFor(element, { tableAlias: index }, [], { exclude, replace }, true)
|
|
831
922
|
wildcardColumns.push(...flatColumns)
|
|
@@ -834,13 +925,13 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
834
925
|
return wildcardColumns
|
|
835
926
|
|
|
836
927
|
/**
|
|
837
|
-
*
|
|
838
|
-
* not excluding
|
|
928
|
+
* foreign keys are already part of the elements in a flat model
|
|
929
|
+
* not excluding the associations from the wildcard columns would cause duplicate columns upon foreign key expansion
|
|
839
930
|
* @param {CSN.element} e
|
|
840
|
-
* @returns {boolean} true if the element is a
|
|
931
|
+
* @returns {boolean} true if the element is a managed association and the model is flat
|
|
841
932
|
*/
|
|
842
|
-
function
|
|
843
|
-
return
|
|
933
|
+
function isManagedAssocInFlatMode(e) {
|
|
934
|
+
return model.meta.transformation === 'odata' && e.isAssociation && e.keys
|
|
844
935
|
}
|
|
845
936
|
}
|
|
846
937
|
|
|
@@ -923,6 +1014,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
923
1014
|
// we need to provide the correct table alias
|
|
924
1015
|
tableAlias = getQuerySourceName(replacedBy)
|
|
925
1016
|
|
|
1017
|
+
if (replacedBy.expand) return [{ as: baseName }]
|
|
1018
|
+
|
|
926
1019
|
return getFlatColumnsFor(replacedBy, { baseName, columnAlias: replacedBy.as, tableAlias }, csnPath)
|
|
927
1020
|
}
|
|
928
1021
|
|
|
@@ -976,7 +1069,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
976
1069
|
else flatColumn = { ref: [fkBaseName] }
|
|
977
1070
|
if (tableAlias) flatColumn.ref.unshift(tableAlias)
|
|
978
1071
|
|
|
979
|
-
|
|
1072
|
+
// in a flat model, we must assign the foreign key rather than the key in the target
|
|
1073
|
+
const flatForeignKey = model.definitions[element.parent.name]?.elements[fkBaseName]
|
|
1074
|
+
|
|
1075
|
+
setElementOnColumns(flatColumn, flatForeignKey || fkElement)
|
|
980
1076
|
Object.defineProperty(flatColumn, '_csnPath', { value: csnPath, writable: true })
|
|
981
1077
|
flatColumns.push(flatColumn)
|
|
982
1078
|
}
|
|
@@ -1157,6 +1253,14 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1157
1253
|
|
|
1158
1254
|
let result = is_regexp(token?.val) ? token : copy(token) // REVISIT: too expensive! //
|
|
1159
1255
|
if (token.ref) {
|
|
1256
|
+
const { definition } = token.$refLinks[token.$refLinks.length - 1]
|
|
1257
|
+
// Add definition to result
|
|
1258
|
+
setElementOnColumns(result, definition)
|
|
1259
|
+
if (isCalculatedOnRead(definition)) {
|
|
1260
|
+
const calculatedElement = resolveCalculatedElement(token, true, $baseLink)
|
|
1261
|
+
transformedTokenStream.push(calculatedElement)
|
|
1262
|
+
continue
|
|
1263
|
+
}
|
|
1160
1264
|
if (token.ref.length > 1 && token.ref[0] === '$self' && !token.$refLinks[0].definition.kind) {
|
|
1161
1265
|
const dollarSelfReplacement = [calculateDollarSelfColumn(token, true)]
|
|
1162
1266
|
transformedTokenStream.push(...getTransformedTokenStream(dollarSelfReplacement))
|
|
@@ -1472,11 +1576,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1472
1576
|
each.ref[0] in { $self: true, $projection: true } ? getParentEntity(assoc) : target,
|
|
1473
1577
|
),
|
|
1474
1578
|
)
|
|
1475
|
-
|
|
1476
|
-
function getParentEntity(element) {
|
|
1477
|
-
if (element.kind === 'entity') return element
|
|
1478
|
-
else return getParentEntity(element.parent)
|
|
1479
|
-
}
|
|
1480
1579
|
}
|
|
1481
1580
|
|
|
1482
1581
|
/**
|
|
@@ -1525,6 +1624,34 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1525
1624
|
continue
|
|
1526
1625
|
}
|
|
1527
1626
|
const rhs = result[i + 2]
|
|
1627
|
+
if (rhs?.ref || lhs.ref) {
|
|
1628
|
+
// if we have refs on each side of the comparison, we might need to perform tuple expansion
|
|
1629
|
+
// or flatten the structures
|
|
1630
|
+
const refLinkFaker = thing => {
|
|
1631
|
+
const { ref } = thing
|
|
1632
|
+
const assocHost = getParentEntity(assocRefLink.definition)
|
|
1633
|
+
Object.defineProperty(thing, '$refLinks', {
|
|
1634
|
+
value: [],
|
|
1635
|
+
writable: true,
|
|
1636
|
+
})
|
|
1637
|
+
ref.reduce((prev, res, i) => {
|
|
1638
|
+
if (res === '$self')
|
|
1639
|
+
// next is resolvable in entity
|
|
1640
|
+
return prev
|
|
1641
|
+
const definition = prev?.elements?.[res] || prev?._target?.elements[res] || pseudos.elements[res]
|
|
1642
|
+
const target = getParentEntity(definition)
|
|
1643
|
+
thing.$refLinks[i] = { definition, target, alias: definition.name }
|
|
1644
|
+
return prev?.elements?.[res] || prev?._target?.elements[res] || pseudos.elements[res]
|
|
1645
|
+
}, assocHost)
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// comparison in on condition needs to be expanded...
|
|
1649
|
+
// re-use existing algorithm for that
|
|
1650
|
+
// we need to fake some $refLinks for that to work though...
|
|
1651
|
+
lhs?.ref && !lhs.$refLinks && refLinkFaker(lhs)
|
|
1652
|
+
rhs?.ref && !rhs.$refLinks && refLinkFaker(rhs)
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1528
1655
|
let backlink
|
|
1529
1656
|
if (rhs?.ref && lhs?.ref) {
|
|
1530
1657
|
if (lhs?.ref?.length === 1 && lhs.ref[0] === '$self')
|
|
@@ -1538,32 +1665,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1538
1665
|
lhs.ref[0] in { $self: true, $projection: true } ? getParentEntity(assocRefLink.definition) : target,
|
|
1539
1666
|
)
|
|
1540
1667
|
else {
|
|
1541
|
-
// if we have refs on each side of the comparison, we might need to perform tuple expansion
|
|
1542
|
-
// or flatten the structures
|
|
1543
|
-
// REVISIT: this whole section needs a refactoring, it is too complex and some edge cases may still be not considered...
|
|
1544
|
-
const refLinkFaker = thing => {
|
|
1545
|
-
const { ref } = thing
|
|
1546
|
-
const assocHost = getParentEntity(assocRefLink.definition)
|
|
1547
|
-
Object.defineProperty(thing, '$refLinks', {
|
|
1548
|
-
value: [],
|
|
1549
|
-
writable: true,
|
|
1550
|
-
})
|
|
1551
|
-
ref.reduce((prev, res, i) => {
|
|
1552
|
-
if (res === '$self')
|
|
1553
|
-
// next is resolvable in entity
|
|
1554
|
-
return prev
|
|
1555
|
-
const definition = prev?.elements?.[res] || prev?._target?.elements[res] || pseudos.elements[res]
|
|
1556
|
-
const target = getParentEntity(definition)
|
|
1557
|
-
thing.$refLinks[i] = { definition, target, alias: definition.name }
|
|
1558
|
-
return prev?.elements?.[res] || prev?._target?.elements[res] || pseudos.elements[res]
|
|
1559
|
-
}, assocHost)
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
// comparison in on condition needs to be expanded...
|
|
1563
|
-
// re-use existing algorithm for that
|
|
1564
|
-
// we need to fake some $refLinks for that to work though...
|
|
1565
|
-
lhs?.ref && refLinkFaker(lhs)
|
|
1566
|
-
rhs?.ref && refLinkFaker(rhs)
|
|
1567
1668
|
const lhsLeafArt = lhs.ref && lhs.$refLinks[lhs.$refLinks.length - 1].definition
|
|
1568
1669
|
const rhsLeafArt = rhs.ref && rhs.$refLinks[rhs.$refLinks.length - 1].definition
|
|
1569
1670
|
if (lhsLeafArt?.target || rhsLeafArt?.target) {
|
|
@@ -1587,7 +1688,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1587
1688
|
lhs.$refLinks[0].definition ===
|
|
1588
1689
|
getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]
|
|
1589
1690
|
)
|
|
1590
|
-
result[i].ref = [
|
|
1691
|
+
result[i].ref = [assocRefLink.alias, lhs.ref.slice(1).join('_')]
|
|
1591
1692
|
// naive assumption: if the path starts with an association which is not the association from
|
|
1592
1693
|
// which the on-condition originates, it must be a foreign key and hence resolvable in the source
|
|
1593
1694
|
else if (lhs.$refLinks[0].definition.target) result[i].ref = [result[i].ref.join('_')]
|
|
@@ -1838,7 +1939,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1838
1939
|
if (node.isJoinRelevant) {
|
|
1839
1940
|
return getJoinRelevantAlias(node)
|
|
1840
1941
|
}
|
|
1841
|
-
|
|
1842
1942
|
return getSelectOrEntityAlias(node) || getCombinedElementAlias(node)
|
|
1843
1943
|
function getBaseLinkAlias($baseLink) {
|
|
1844
1944
|
return $baseLink.alias
|
|
@@ -1920,6 +2020,11 @@ function getLastStringSegment(str) {
|
|
|
1920
2020
|
return index != -1 ? str.substring(index + 1) : str
|
|
1921
2021
|
}
|
|
1922
2022
|
|
|
2023
|
+
function getParentEntity(element) {
|
|
2024
|
+
if (element.kind === 'entity') return element
|
|
2025
|
+
else return getParentEntity(element.parent)
|
|
2026
|
+
}
|
|
2027
|
+
|
|
1923
2028
|
/**
|
|
1924
2029
|
* Assigns the given `element` as non-enumerable property 'element' onto `col`.
|
|
1925
2030
|
*
|
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) {
|
|
@@ -167,14 +189,20 @@ const getDeepQueries = (query, dbData, target) => {
|
|
|
167
189
|
diff = [diff]
|
|
168
190
|
}
|
|
169
191
|
|
|
170
|
-
return _getDeepQueries(diff, target)
|
|
192
|
+
return _getDeepQueries(diff, target, true)
|
|
171
193
|
}
|
|
172
194
|
|
|
173
195
|
const _hasManagedElements = target => {
|
|
174
196
|
return Object.keys(target.elements).filter(elementName => target.elements[elementName]['@cds.on.update']).length > 0
|
|
175
197
|
}
|
|
176
198
|
|
|
177
|
-
|
|
199
|
+
/**
|
|
200
|
+
* @param {unknown[]} diff
|
|
201
|
+
* @param {import('@sap/cds/apis/csn').Definition} target
|
|
202
|
+
* @param {boolean} [root=false]
|
|
203
|
+
* @returns {import('@sap/cds/apis/cqn').Query[]}
|
|
204
|
+
*/
|
|
205
|
+
const _getDeepQueries = (diff, target, root = false) => {
|
|
178
206
|
const queries = []
|
|
179
207
|
|
|
180
208
|
for (const diffEntry of diff) {
|
|
@@ -213,7 +241,7 @@ const _getDeepQueries = (diff, target) => {
|
|
|
213
241
|
queries.push(INSERT.into(target).entries(diffEntry))
|
|
214
242
|
} else if (op === 'delete') {
|
|
215
243
|
queries.push(DELETE.from(target).where(diffEntry))
|
|
216
|
-
} else if (op === 'update' || (op === undefined && subQueries.length && _hasManagedElements(target))) {
|
|
244
|
+
} else if (op === 'update' || (op === undefined && (root || subQueries.length) && _hasManagedElements(target))) {
|
|
217
245
|
// TODO do we need the where here?
|
|
218
246
|
const keys = target.keys
|
|
219
247
|
const cqn = UPDATE(target).with(diffEntry)
|
package/lib/fill-in-keys.js
CHANGED
|
@@ -23,6 +23,11 @@ const generateUUIDandPropagateKeys = (target, data, event) => {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
if (elements[element].is2one || elements[element].is2many) {
|
|
26
|
+
// propagate own foreign keys to propagate further to sub data
|
|
27
|
+
propagateForeignKeys(element, data, elements[element]._foreignKeys, elements[element].isComposition, {
|
|
28
|
+
deleteAssocs: true,
|
|
29
|
+
})
|
|
30
|
+
|
|
26
31
|
let subData = data[element]
|
|
27
32
|
if (subData) {
|
|
28
33
|
if (!Array.isArray(subData)) {
|
|
@@ -33,14 +38,20 @@ const generateUUIDandPropagateKeys = (target, data, event) => {
|
|
|
33
38
|
generateUUIDandPropagateKeys(elements[element]._target, sub, 'CREATE')
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
|
-
|
|
37
|
-
propagateForeignKeys(element, data, elements[element]._foreignKeys, elements[element].isComposition, {
|
|
38
|
-
deleteAssocs: true,
|
|
39
|
-
})
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
/**
|
|
46
|
+
* @callback nextCallback
|
|
47
|
+
* @param {Error|undefined} error
|
|
48
|
+
* @returns {Promise<unknown>}
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {import('@sap/cds/apis/services').Request} req
|
|
53
|
+
* @param {nextCallback} next
|
|
54
|
+
*/
|
|
44
55
|
module.exports = async function fill_in_keys(req, next) {
|
|
45
56
|
// REVISIT dummy handler until we have input processing
|
|
46
57
|
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 }
|