@cap-js/db-service 2.8.1 → 2.9.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 +30 -0
- package/lib/SQLService.js +5 -8
- package/lib/cqn2sql.js +127 -44
- package/lib/cqn4sql.js +314 -34
- package/lib/infer/index.js +84 -32
- package/lib/infer/join-tree.js +8 -6
- package/lib/infer/pseudos.js +12 -11
- package/lib/search.js +1 -1
- package/lib/utils.js +29 -0
- package/package.json +2 -2
package/lib/cqn4sql.js
CHANGED
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
defineProperty,
|
|
14
14
|
getModelUtils,
|
|
15
15
|
hasOwnSkip,
|
|
16
|
+
isRuntimeView,
|
|
16
17
|
} = require('./utils')
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -173,8 +174,114 @@ function cqn4sql(originalQuery, model) {
|
|
|
173
174
|
}
|
|
174
175
|
}
|
|
175
176
|
|
|
177
|
+
if (cds.env.features.runtime_views) processRuntimeViews(transformedQuery, model)
|
|
178
|
+
|
|
176
179
|
return transformedQuery
|
|
177
180
|
|
|
181
|
+
/**
|
|
182
|
+
* If the target entity is annotated with persistence skip and has an underlying db entity,
|
|
183
|
+
* we treat it as a runtime view and transform it into a CTE.
|
|
184
|
+
*
|
|
185
|
+
* @param {object} transformedQuery - The query object to be transformed.
|
|
186
|
+
* @param {string} model - The data model used for inference and transformation.
|
|
187
|
+
*/
|
|
188
|
+
function processRuntimeViews(transformedQuery, model) {
|
|
189
|
+
const currentDef = transformedQuery._target
|
|
190
|
+
|
|
191
|
+
if (hasOwnSkip(currentDef)) {
|
|
192
|
+
if (!isRuntimeView(currentDef)) throw new Error(`${currentDef.name} is not a runtime view`)
|
|
193
|
+
|
|
194
|
+
addWith(currentDef, transformedQuery, model)
|
|
195
|
+
updateRefsWithRTVAlias(transformedQuery._with, transformedQuery)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Recursively call cqn4sql for all nested runtime views to calculate cte and
|
|
201
|
+
* add it as a with clause to the transformed query.
|
|
202
|
+
* Alias the runtime view with a unique alias and update all references to the runtime view to point to the alias.
|
|
203
|
+
*
|
|
204
|
+
* @param {object} rootDefinition - The root definition of the query. This is used to recursively process nested runtime views.
|
|
205
|
+
* @param {object} transformedQuery - The query object to be transformed.
|
|
206
|
+
* @param {string} model - The data model used for infer and cqn4sql.
|
|
207
|
+
*/
|
|
208
|
+
function addWith(rootDefinition, transformedQuery, model) {
|
|
209
|
+
if (!rootDefinition?.query) return
|
|
210
|
+
|
|
211
|
+
// early exit if already processed
|
|
212
|
+
if (transformedQuery._with?.some(w => w._source === rootDefinition)) return
|
|
213
|
+
|
|
214
|
+
const q = cds.ql.clone(rootDefinition.query)
|
|
215
|
+
if (q.SELECT) {
|
|
216
|
+
if (!q.SELECT.columns) q.SELECT.columns = ['*']
|
|
217
|
+
if (q.SELECT.columns.includes('*')) {
|
|
218
|
+
// cache element names for faster lookup
|
|
219
|
+
const existingColumns = new Set(
|
|
220
|
+
q.SELECT.columns
|
|
221
|
+
.map(col => col.as || col.ref?.at(-1))
|
|
222
|
+
.filter(Boolean)
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
for (let el of rootDefinition.elements) {
|
|
226
|
+
if (el.type === 'cds.LargeBinary' && !existingColumns.has(el.name)) {
|
|
227
|
+
q.SELECT.columns.push({ ref: [el.name] })
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const inferredDQ = infer(q, model)
|
|
233
|
+
inferredDQ._with = transformedQuery._with
|
|
234
|
+
const transformedDQ = cqn4sql(inferredDQ, model)
|
|
235
|
+
|
|
236
|
+
if (q.SELECT?.from?.args) {
|
|
237
|
+
for (const arg of q.SELECT.from.args) {
|
|
238
|
+
addWith(arg.$refLinks.at(-1).definition, inferredDQ, model)
|
|
239
|
+
arg.as ??= arg.ref.at(-1).split('.').at(-1) // apply @sap/cds-compiler default alias
|
|
240
|
+
updateRefsWithRTVAlias(inferredDQ._with, transformedDQ, arg.ref)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const newWiths = transformedDQ._with || []
|
|
245
|
+
const rootDefinitionName = rootDefinition.name
|
|
246
|
+
|
|
247
|
+
defineProperty(transformedDQ, '_source', rootDefinition)
|
|
248
|
+
const alias = `RTV_${getImplicitAlias(rootDefinitionName)}`
|
|
249
|
+
transformedDQ.as = transformedDQ.joinTree.addNextAvailableTableAlias(alias, newWiths, rootDefinitionName)
|
|
250
|
+
|
|
251
|
+
// update SELECT.from with runtime view alias
|
|
252
|
+
if (hasOwnSkip(transformedDQ._target)) updateRefsWithRTVAlias(transformedDQ._with, transformedDQ)
|
|
253
|
+
|
|
254
|
+
if (transformedDQ._with) delete transformedDQ._with
|
|
255
|
+
newWiths.push(transformedDQ)
|
|
256
|
+
|
|
257
|
+
// propagate with clauses
|
|
258
|
+
if (!transformedQuery._with) transformedQuery._with = newWiths
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function updateRefsWithRTVAlias(_with, query, ref) {
|
|
262
|
+
if (!_with?.length) return
|
|
263
|
+
|
|
264
|
+
const _updateRef = (ref) => {
|
|
265
|
+
const refAlias = ref[0]
|
|
266
|
+
if (/RTV_$/.test(refAlias)) return
|
|
267
|
+
for (const w of _with) {
|
|
268
|
+
const aliasValue = w.joinTree._queryAliases.get(refAlias)
|
|
269
|
+
if (aliasValue) {
|
|
270
|
+
ref[0] = aliasValue
|
|
271
|
+
if (query.joinTree?._queryAliases) {
|
|
272
|
+
query.joinTree._queryAliases.set(refAlias, aliasValue)
|
|
273
|
+
}
|
|
274
|
+
break
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (ref) return _updateRef(ref)
|
|
280
|
+
|
|
281
|
+
if (query.SELECT.from.args) for (const arg of query.SELECT.from.args) _updateRef(arg.ref)
|
|
282
|
+
else if (query.SELECT.from.ref) _updateRef(query.SELECT.from.ref)
|
|
283
|
+
}
|
|
284
|
+
|
|
178
285
|
function transformSelectQuery(queryProp, transformedFrom, transformedWhere, transformedQuery) {
|
|
179
286
|
const { columns, having, groupBy, orderBy, limit } = queryProp
|
|
180
287
|
|
|
@@ -314,7 +421,9 @@ function cqn4sql(originalQuery, model) {
|
|
|
314
421
|
),
|
|
315
422
|
)
|
|
316
423
|
|
|
317
|
-
const
|
|
424
|
+
const def = getDefinition(nextAssoc.$refLink.definition.target)
|
|
425
|
+
const id = def.name
|
|
426
|
+
if (hasOwnSkip(def) && isRuntimeView(def)) addWith(model.definitions[id], transformedQuery, model)
|
|
318
427
|
const { args } = nextAssoc
|
|
319
428
|
const arg = {
|
|
320
429
|
ref: [args ? { id, args } : id],
|
|
@@ -352,7 +461,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
352
461
|
for (let i = 0; i < columns.length; i++) {
|
|
353
462
|
const col = columns[i]
|
|
354
463
|
|
|
355
|
-
if (isCalculatedOnRead(col.$refLinks?.
|
|
464
|
+
if (isCalculatedOnRead(col.$refLinks?.at(-1).definition) && !col.$refLinks?.at(-1).target?.SELECT) {
|
|
356
465
|
const name = getName(col)
|
|
357
466
|
if (!transformedColumns.some(inserted => getName(inserted) === name)) {
|
|
358
467
|
const calcElement = resolveCalculatedElement(col)
|
|
@@ -473,7 +582,13 @@ function cqn4sql(originalQuery, model) {
|
|
|
473
582
|
const refNavigation = col.ref.slice(col.$refLinks[0].definition.kind !== 'element' ? 1 : 0).join('_')
|
|
474
583
|
if (!columnAlias && col.flatName && col.flatName !== refNavigation) columnAlias = refNavigation
|
|
475
584
|
|
|
476
|
-
if (
|
|
585
|
+
if (
|
|
586
|
+
col.$refLinks.some(link => {
|
|
587
|
+
const def = getDefinition(link.definition.target)
|
|
588
|
+
return hasOwnSkip(def) && !isRuntimeView(def)
|
|
589
|
+
})
|
|
590
|
+
)
|
|
591
|
+
return
|
|
477
592
|
|
|
478
593
|
const flatColumns = getFlatColumnsFor(col, { baseName, columnAlias, tableAlias })
|
|
479
594
|
flatColumns.forEach(flatColumn => {
|
|
@@ -560,6 +675,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
560
675
|
res = { args: getTransformedFunctionArgs(value.args, $baseLink), func: value.func }
|
|
561
676
|
}
|
|
562
677
|
if (!omitAlias) res.as = column.as || column.name || column.flatName
|
|
678
|
+
setElementOnColumns(res, column.element || column)
|
|
563
679
|
return res
|
|
564
680
|
}
|
|
565
681
|
|
|
@@ -643,8 +759,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
643
759
|
nameParts.push(nestedProjection.as ? nestedProjection.as : nestedProjection.ref.map(idOnly).join('_'))
|
|
644
760
|
const name = nameParts.join('_')
|
|
645
761
|
if (nestedProjection.ref) {
|
|
646
|
-
const augmentedInlineCol =
|
|
647
|
-
augmentedInlineCol.ref = col.ref ? [...col.ref, ...nestedProjection.ref] : nestedProjection.ref
|
|
762
|
+
const augmentedInlineCol = augmentInlineRefWithParent(nestedProjection, col)
|
|
648
763
|
if (
|
|
649
764
|
col.as ||
|
|
650
765
|
nestedProjection.as ||
|
|
@@ -653,19 +768,6 @@ function cqn4sql(originalQuery, model) {
|
|
|
653
768
|
) {
|
|
654
769
|
augmentedInlineCol.as = nameParts.join('_')
|
|
655
770
|
}
|
|
656
|
-
Object.defineProperties(augmentedInlineCol, {
|
|
657
|
-
$refLinks: { value: [...nestedProjection.$refLinks], writable: true },
|
|
658
|
-
isJoinRelevant: {
|
|
659
|
-
value: nestedProjection.isJoinRelevant,
|
|
660
|
-
writable: true,
|
|
661
|
-
},
|
|
662
|
-
})
|
|
663
|
-
// if the expand is not anonymous, we must prepend the expand columns path
|
|
664
|
-
// to make sure the full path is resolvable
|
|
665
|
-
if (col.ref) {
|
|
666
|
-
augmentedInlineCol.$refLinks.unshift(...col.$refLinks)
|
|
667
|
-
augmentedInlineCol.isJoinRelevant = augmentedInlineCol.isJoinRelevant || col.isJoinRelevant
|
|
668
|
-
}
|
|
669
771
|
const flatColumns = getTransformedColumns([augmentedInlineCol])
|
|
670
772
|
flatColumns.forEach(flatColumn => {
|
|
671
773
|
const flatColumnName = flatColumn.as || flatColumn.ref[flatColumn.ref.length - 1]
|
|
@@ -682,6 +784,10 @@ function cqn4sql(originalQuery, model) {
|
|
|
682
784
|
if (!res.some(c => (c.as || c.ref.slice(1).map(idOnly).join('_')) === name)) {
|
|
683
785
|
const rewrittenColumn = { ...nestedProjection }
|
|
684
786
|
rewrittenColumn.as = name
|
|
787
|
+
// For xpr, we need to transform refs inside to include the struct prefix
|
|
788
|
+
if (nestedProjection.xpr && col.ref) {
|
|
789
|
+
rewrittenColumn.xpr = augmentInlineXprRefs(nestedProjection.xpr, col)
|
|
790
|
+
}
|
|
685
791
|
rewrittenColumns.push(rewrittenColumn)
|
|
686
792
|
}
|
|
687
793
|
}
|
|
@@ -690,6 +796,53 @@ function cqn4sql(originalQuery, model) {
|
|
|
690
796
|
})
|
|
691
797
|
|
|
692
798
|
return res
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Augment a ref column with the parent column's path and $refLinks.
|
|
802
|
+
*/
|
|
803
|
+
function augmentInlineRefWithParent(refCol, parentCol) {
|
|
804
|
+
const augmented = { ...refCol }
|
|
805
|
+
augmented.ref = parentCol.ref ? [...parentCol.ref, ...refCol.ref] : refCol.ref
|
|
806
|
+
Object.defineProperties(augmented, {
|
|
807
|
+
$refLinks: { value: [...refCol.$refLinks], writable: true },
|
|
808
|
+
isJoinRelevant: { value: refCol.isJoinRelevant, writable: true },
|
|
809
|
+
})
|
|
810
|
+
if (parentCol.ref) {
|
|
811
|
+
augmented.$refLinks.unshift(...parentCol.$refLinks)
|
|
812
|
+
augmented.isJoinRelevant = augmented.isJoinRelevant || parentCol.isJoinRelevant
|
|
813
|
+
}
|
|
814
|
+
return augmented
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Augment refs inside an xpr with the parent column's path and $refLinks,
|
|
819
|
+
* then transform them to flat refs.
|
|
820
|
+
*/
|
|
821
|
+
function augmentInlineXprRefs(xpr, parentCol) {
|
|
822
|
+
return xpr.map(token => {
|
|
823
|
+
if (typeof token === 'string' || token.val !== undefined) {
|
|
824
|
+
return token
|
|
825
|
+
}
|
|
826
|
+
if (token.ref && token.$refLinks) {
|
|
827
|
+
const augmented = augmentInlineRefWithParent(token, parentCol)
|
|
828
|
+
// Transform this single ref column to get the flat version
|
|
829
|
+
const transformed = getTransformedColumns([augmented])
|
|
830
|
+
if (transformed.length === 1) {
|
|
831
|
+
return transformed[0]
|
|
832
|
+
}
|
|
833
|
+
return augmented
|
|
834
|
+
}
|
|
835
|
+
if (token.xpr) {
|
|
836
|
+
return { ...token, xpr: augmentInlineXprRefs(token.xpr, parentCol) }
|
|
837
|
+
}
|
|
838
|
+
if (token.func && token.args) {
|
|
839
|
+
return { ...token, args: token.args.map(arg =>
|
|
840
|
+
arg.ref ? augmentInlineXprRefs([arg], parentCol)[0] : arg
|
|
841
|
+
)}
|
|
842
|
+
}
|
|
843
|
+
return token
|
|
844
|
+
})
|
|
845
|
+
}
|
|
693
846
|
}
|
|
694
847
|
|
|
695
848
|
/**
|
|
@@ -747,6 +900,9 @@ function cqn4sql(originalQuery, model) {
|
|
|
747
900
|
|
|
748
901
|
if (baseRefLinks.at(-1).definition.kind === 'entity') {
|
|
749
902
|
res.push(...getColumnsForWildcard(exclude, replace, col.as))
|
|
903
|
+
} else if (baseRefLinks.at(-1).definition.target) {
|
|
904
|
+
// Wildcard on association - need to include FK columns and join-relevant target columns
|
|
905
|
+
res.push(...expandAssociationWildcard(col, baseRef, baseRefLinks, exclude, replace))
|
|
750
906
|
} else
|
|
751
907
|
res.push(
|
|
752
908
|
...getFlatColumnsFor(col, { columnAlias: col.as, tableAlias: getTableAlias(col) }, [], {
|
|
@@ -757,6 +913,117 @@ function cqn4sql(originalQuery, model) {
|
|
|
757
913
|
return res
|
|
758
914
|
}
|
|
759
915
|
|
|
916
|
+
/**
|
|
917
|
+
* Expands a wildcard on an association into:
|
|
918
|
+
* 1. FK columns from the source table
|
|
919
|
+
* 2. Non-FK columns from the target via join
|
|
920
|
+
*/
|
|
921
|
+
function expandAssociationWildcard(col, baseRef, baseRefLinks, exclude, replace) {
|
|
922
|
+
const res = []
|
|
923
|
+
const assocDef = baseRefLinks.at(-1).definition
|
|
924
|
+
const targetDef = getDefinition(assocDef.target)
|
|
925
|
+
const columnAlias = col.as || baseRef.map(idOnly).join('_')
|
|
926
|
+
const sourceTableAlias = getTableAlias(col)
|
|
927
|
+
|
|
928
|
+
// Get the join alias for this association (set during join tree merge)
|
|
929
|
+
const joinAlias = baseRefLinks.at(-1).alias
|
|
930
|
+
|
|
931
|
+
// Collect FK element names
|
|
932
|
+
const fkNames = new Set()
|
|
933
|
+
if (assocDef.keys) {
|
|
934
|
+
for (const k of assocDef.keys) {
|
|
935
|
+
fkNames.add(k.ref[0])
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// First, add FK columns from source table
|
|
940
|
+
// These are accessed via the source table alias, not the join
|
|
941
|
+
const fkColumns = getFlatColumnsFor(col, { tableAlias: sourceTableAlias }, [], {
|
|
942
|
+
exclude,
|
|
943
|
+
replace,
|
|
944
|
+
})
|
|
945
|
+
res.push(...fkColumns.filter(fk => !col.excluding?.some(e => targetDef.elements[e] === fk.element)))
|
|
946
|
+
|
|
947
|
+
// Then, add non-FK columns from target via join
|
|
948
|
+
if (targetDef?.elements) {
|
|
949
|
+
for (const [elemName, elemDef] of Object.entries(targetDef.elements)) {
|
|
950
|
+
// Skip FK elements (already included above), virtual, blobs, and unmanaged assocs
|
|
951
|
+
if (fkNames.has(elemName)) continue
|
|
952
|
+
if (elemDef.virtual) continue
|
|
953
|
+
if (elemDef.type === 'cds.LargeBinary') continue
|
|
954
|
+
if (elemDef.on && !elemDef.keys) continue // unmanaged association
|
|
955
|
+
|
|
956
|
+
// Check exclusions
|
|
957
|
+
const fullName = `${columnAlias}_${elemName}`
|
|
958
|
+
if (exclude.some(e => (e.ref?.at(-1) || e.as || e) === elemName)) continue
|
|
959
|
+
if (exclude.some(e => (e.ref?.at(-1) || e.as || e) === fullName)) continue
|
|
960
|
+
|
|
961
|
+
// Check for replacement
|
|
962
|
+
const replacement = replace.find(r => (r.ref?.at(-1) || r.as) === elemName)
|
|
963
|
+
if (replacement) {
|
|
964
|
+
// Handle replacement - create augmented column
|
|
965
|
+
const augmented = { ...replacement }
|
|
966
|
+
augmented.as = fullName
|
|
967
|
+
res.push(...getTransformedColumns([augmented]))
|
|
968
|
+
continue
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Create column referencing the join alias
|
|
972
|
+
if (elemDef.elements) {
|
|
973
|
+
// Structured element - need to flatten it
|
|
974
|
+
const structCols = getFlatColumnsFor(
|
|
975
|
+
elemDef,
|
|
976
|
+
{ baseName: elemName, columnAlias: fullName, tableAlias: joinAlias },
|
|
977
|
+
[],
|
|
978
|
+
{ exclude, replace },
|
|
979
|
+
true,
|
|
980
|
+
)
|
|
981
|
+
res.push(...structCols)
|
|
982
|
+
} else if (elemDef.keys) {
|
|
983
|
+
// Association element - flatten its foreign keys
|
|
984
|
+
// The FK column name is: assocName_keyName (e.g., 'head_id')
|
|
985
|
+
for (const k of elemDef.keys) {
|
|
986
|
+
const keyName = k.as || k.ref.join('_')
|
|
987
|
+
const fkName = `${elemName}_${keyName}` // e.g., 'head_id'
|
|
988
|
+
const fkFullName = `${columnAlias}_${fkName}` // e.g., 'department_head_id'
|
|
989
|
+
|
|
990
|
+
// Check if this FK is excluded
|
|
991
|
+
if (exclude.some(e => (e.ref?.at(-1) || e.as || e) === fkName)) continue
|
|
992
|
+
if (exclude.some(e => (e.ref?.at(-1) || e.as || e) === fkFullName)) continue
|
|
993
|
+
|
|
994
|
+
const flatColumn = {
|
|
995
|
+
ref: [joinAlias, fkName],
|
|
996
|
+
as: fkFullName,
|
|
997
|
+
}
|
|
998
|
+
const fkElement = getElementForRef(k.ref, getDefinition(elemDef.target))
|
|
999
|
+
setElementOnColumns(flatColumn, fkElement)
|
|
1000
|
+
res.push(flatColumn)
|
|
1001
|
+
}
|
|
1002
|
+
} else if (elemDef.value) {
|
|
1003
|
+
// Calculated element - resolve it
|
|
1004
|
+
const calcElement = resolveCalculatedElement({ $refLinks: [{ definition: elemDef }] }, true)
|
|
1005
|
+
if (calcElement.as) {
|
|
1006
|
+
calcElement.as = fullName
|
|
1007
|
+
} else {
|
|
1008
|
+
calcElement.as = fullName
|
|
1009
|
+
}
|
|
1010
|
+
res.push(calcElement)
|
|
1011
|
+
}
|
|
1012
|
+
else {
|
|
1013
|
+
// Scalar element
|
|
1014
|
+
const flatColumn = {
|
|
1015
|
+
ref: [joinAlias, elemName],
|
|
1016
|
+
as: fullName,
|
|
1017
|
+
}
|
|
1018
|
+
setElementOnColumns(flatColumn, elemDef)
|
|
1019
|
+
res.push(flatColumn)
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return res
|
|
1025
|
+
}
|
|
1026
|
+
|
|
760
1027
|
/**
|
|
761
1028
|
* This function converts a column with an `expand` property into a subquery.
|
|
762
1029
|
*
|
|
@@ -972,7 +1239,12 @@ function cqn4sql(originalQuery, model) {
|
|
|
972
1239
|
} else if (pseudos.elements[col.ref?.[0]]) {
|
|
973
1240
|
res.push({ ...col })
|
|
974
1241
|
} else if (col.ref) {
|
|
975
|
-
if (
|
|
1242
|
+
if (
|
|
1243
|
+
col.$refLinks.some(link => {
|
|
1244
|
+
const def = getDefinition(link.definition.target)
|
|
1245
|
+
return hasOwnSkip(def) && !isRuntimeView(def)
|
|
1246
|
+
})
|
|
1247
|
+
)
|
|
976
1248
|
continue
|
|
977
1249
|
if (col.ref.length > 1 && col.ref[0] === '$self' && !col.$refLinks[0].definition.kind) {
|
|
978
1250
|
const dollarSelfReplacement = calculateDollarSelfColumn(col)
|
|
@@ -1070,10 +1342,18 @@ function cqn4sql(originalQuery, model) {
|
|
|
1070
1342
|
outerQueries.push(inferred)
|
|
1071
1343
|
defineProperty(q, 'outerQueries', outerQueries)
|
|
1072
1344
|
}
|
|
1345
|
+
|
|
1073
1346
|
const target = cds.infer.target(inferred) // REVISIT: we should reliably use inferred._target instead
|
|
1074
1347
|
if (isLocalized(target)) q.SELECT.localized = true
|
|
1075
1348
|
if (q.SELECT.from.ref && !q.SELECT.from.as) assignUniqueSubqueryAlias()
|
|
1076
|
-
|
|
1349
|
+
if (cds.env.features.runtime_views) q._with = transformedQuery._with
|
|
1350
|
+
const _q = cqn4sql(q, model)
|
|
1351
|
+
if (cds.env.features.runtime_views && _q._with) {
|
|
1352
|
+
if (!transformedQuery._with) transformedQuery._with = _q._with
|
|
1353
|
+
delete _q._with
|
|
1354
|
+
}
|
|
1355
|
+
return _q
|
|
1356
|
+
|
|
1077
1357
|
|
|
1078
1358
|
function assignUniqueSubqueryAlias() {
|
|
1079
1359
|
if (q.SELECT.from.uniqueSubqueryAlias) return
|
|
@@ -1213,7 +1493,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1213
1493
|
columnAlias = column.as || column.ref.slice(0, -1).map(idOnly).join('_')
|
|
1214
1494
|
} else baseName = getFullName(column.$refLinks[column.$refLinks.length - 1].definition)
|
|
1215
1495
|
|
|
1216
|
-
if(column.element && !isAssocOrStruct(column.element)) {
|
|
1496
|
+
if (column.element && !isAssocOrStruct(column.element)) {
|
|
1217
1497
|
columnAlias = column.as || leafAssocIndex === -1 ? columnAlias : column.ref.slice(leafAssocIndex - 1).map(idOnly).join('_')
|
|
1218
1498
|
const res = { ref: [tableAlias, calculateElementName(column)], as: columnAlias }
|
|
1219
1499
|
setElementOnColumns(res, column.element)
|
|
@@ -1503,7 +1783,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1503
1783
|
throw new Error(`The operator "${next}" can only be used with scalar operands`)
|
|
1504
1784
|
|
|
1505
1785
|
const newTokens = expandComparison(token, ops, rhs, $baseLink)
|
|
1506
|
-
if(newTokens.length === 0)
|
|
1786
|
+
if (newTokens.length === 0)
|
|
1507
1787
|
throw new Error(`Can't compare two empty structures`)
|
|
1508
1788
|
|
|
1509
1789
|
const needXpr = Boolean(tokenStream[i - 1] || tokenStream[indexRhs + 1])
|
|
@@ -1538,9 +1818,9 @@ function cqn4sql(originalQuery, model) {
|
|
|
1538
1818
|
const lastAssoc =
|
|
1539
1819
|
token.isJoinRelevant && [...token.$refLinks].reverse().find(l => l.definition.isAssociation)
|
|
1540
1820
|
const tableAlias = getTableAlias(token, (!lastAssoc?.onlyForeignKeyAccess && lastAssoc) || $baseLink)
|
|
1541
|
-
if(isAssocOrStruct(definition)) {
|
|
1542
|
-
const flat =
|
|
1543
|
-
if(flat.length === 0)
|
|
1821
|
+
if (isAssocOrStruct(definition)) {
|
|
1822
|
+
const flat = getFlatColumnsFor(token, { tableAlias: $baseLink?.alias || tableAlias })
|
|
1823
|
+
if (flat.length === 0)
|
|
1544
1824
|
throw new Error(`Structured element “${getFullName(definition)}” expands to nothing and can't be used in expressions`)
|
|
1545
1825
|
else if (flat.length > 1 && context.prop !== 'list') // only acceptable in `list`
|
|
1546
1826
|
throw new Error(`Structured element “${getFullName(definition)}” expands to multiple fields and can't be used in expressions`)
|
|
@@ -1673,7 +1953,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1673
1953
|
|
|
1674
1954
|
function assertNoStructInXpr(token, context) {
|
|
1675
1955
|
const definition = token.$refLinks?.at(-1).definition
|
|
1676
|
-
if(!definition) return
|
|
1956
|
+
if (!definition) return
|
|
1677
1957
|
const rejectStructs = context && (context.prop in { where: 1, having: 1 })
|
|
1678
1958
|
// unmanaged is always forbidden
|
|
1679
1959
|
// expanding a ref in a `where`/`having` context
|
|
@@ -1785,7 +2065,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1785
2065
|
|
|
1786
2066
|
// OData variant w/o mentioning key
|
|
1787
2067
|
if (refReverse[0].where?.length === 1 && refReverse[0].where[0].val) {
|
|
1788
|
-
filterConditions.push(getTransformedTokenStream(refReverse[0].where,{ $baseLink: $refLinksReverse[0] }))
|
|
2068
|
+
filterConditions.push(getTransformedTokenStream(refReverse[0].where, { $baseLink: $refLinksReverse[0] }))
|
|
1789
2069
|
}
|
|
1790
2070
|
|
|
1791
2071
|
if (existingWhere.length > 0) filterConditions.push(existingWhere)
|
|
@@ -2231,7 +2511,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2231
2511
|
return SELECT
|
|
2232
2512
|
}
|
|
2233
2513
|
|
|
2234
|
-
/**
|
|
2514
|
+
/**
|
|
2235
2515
|
* For a given search term calculate a search expression which can be used in a where clause.
|
|
2236
2516
|
* The search function is pushed to a subquery and the primary key(s) of the entity is/are used to match
|
|
2237
2517
|
* the search results of the subquery.
|
|
@@ -2259,13 +2539,13 @@ function cqn4sql(originalQuery, model) {
|
|
|
2259
2539
|
// for aggregated queries / search on subqueries we do not do a subquery search
|
|
2260
2540
|
if (inferred.SELECT.groupBy || entity.SELECT)
|
|
2261
2541
|
return searchFunc
|
|
2262
|
-
|
|
2542
|
+
|
|
2263
2543
|
const matchColumns = getPrimaryKey(entity)
|
|
2264
2544
|
if (matchColumns.length === 0 || searchIn.every(r => r.ref.length === 1)) // keyless or not deep, fallback to old behavior
|
|
2265
2545
|
return searchFunc
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
return { xpr: [
|
|
2546
|
+
|
|
2547
|
+
const subquery = SELECT.from(entity).columns(...matchColumns).where(searchFunc)
|
|
2548
|
+
return { xpr: [matchColumns.length === 1 ? matchColumns[0] : { list: matchColumns }, 'in', subquery] }
|
|
2269
2549
|
}
|
|
2270
2550
|
|
|
2271
2551
|
/**
|
|
@@ -2282,7 +2562,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2282
2562
|
if (!node || !node.$refLinks || !node.ref) {
|
|
2283
2563
|
throw new Error('Invalid node')
|
|
2284
2564
|
}
|
|
2285
|
-
if(node.$refLinks[0].$main) {
|
|
2565
|
+
if (node.$refLinks[0].$main) {
|
|
2286
2566
|
if (node.isJoinRelevant) {
|
|
2287
2567
|
return getJoinRelevantAlias(node)
|
|
2288
2568
|
}
|
|
@@ -2425,7 +2705,7 @@ function assignQueryModifiers(SELECT, modifiers) {
|
|
|
2425
2705
|
else SELECT.having.push('and', ...val)
|
|
2426
2706
|
} else if (key === 'where') {
|
|
2427
2707
|
// ignore OData shortcut variant: `… bookshop.Orders:items[2]`
|
|
2428
|
-
if(!val || val.length === 1 && val[0].val) continue
|
|
2708
|
+
if (!val || val.length === 1 && val[0].val) continue
|
|
2429
2709
|
if (!SELECT.where) SELECT.where = val
|
|
2430
2710
|
// infix filter comes first in resulting where
|
|
2431
2711
|
else SELECT.where = [...(hasLogicalOr(val) ? [asXpr(val)] : val), 'and', ...(hasLogicalOr(SELECT.where) ? [asXpr(SELECT.where)] : SELECT.where)]
|