@cap-js/db-service 2.8.2 → 2.10.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 +35 -0
- package/lib/InsertResults.js +6 -3
- package/lib/SQLService.js +88 -50
- package/lib/cql-functions.js +1 -1
- package/lib/cqn2pql.js +116 -0
- package/lib/cqn2sql.js +131 -48
- package/lib/cqn4sql.js +588 -180
- package/lib/infer/index.js +77 -16
- package/lib/infer/join-tree.js +8 -6
- package/lib/utils.js +29 -0
- package/package.json +2 -2
package/lib/cqn4sql.js
CHANGED
|
@@ -9,10 +9,11 @@ const {
|
|
|
9
9
|
prettyPrintRef,
|
|
10
10
|
isCalculatedOnRead,
|
|
11
11
|
isCalculatedElement,
|
|
12
|
-
getImplicitAlias,
|
|
12
|
+
getImplicitAlias: _getImplicitAlias,
|
|
13
13
|
defineProperty,
|
|
14
14
|
getModelUtils,
|
|
15
15
|
hasOwnSkip,
|
|
16
|
+
isRuntimeView,
|
|
16
17
|
} = require('./utils')
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -53,7 +54,8 @@ const { pseudos } = require('./infer/pseudos')
|
|
|
53
54
|
* @param {object} model
|
|
54
55
|
* @returns {object} transformedQuery the transformed query
|
|
55
56
|
*/
|
|
56
|
-
function cqn4sql(originalQuery, model) {
|
|
57
|
+
function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
|
|
58
|
+
const getImplicitAlias = str => _getImplicitAlias(str, useTechnicalAlias)
|
|
57
59
|
let inferred = typeof originalQuery === 'string' ? cds.parse.cql(originalQuery) : cds.ql.clone(originalQuery)
|
|
58
60
|
const hasCustomJoins =
|
|
59
61
|
originalQuery.SELECT?.from.args && (!originalQuery.joinTree || originalQuery.joinTree.isInitial)
|
|
@@ -80,7 +82,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
80
82
|
if (inferred.UPDATE?.entity.ref?.at(-1).id) {
|
|
81
83
|
assignQueryModifiers(inferred.UPDATE, inferred.UPDATE.entity.ref.at(-1))
|
|
82
84
|
}
|
|
83
|
-
inferred = infer(inferred, model)
|
|
85
|
+
inferred = infer(inferred, model, useTechnicalAlias)
|
|
84
86
|
const { getLocalizedName, isLocalized, getDefinition } = getModelUtils(model, originalQuery) // TODO: pass model to getModelUtils
|
|
85
87
|
// if the query has custom joins we don't want to transform it
|
|
86
88
|
// TODO: move all the way to the top of this function once cds.infer supports joins as well
|
|
@@ -133,7 +135,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
133
135
|
// match primary keys of the target entity with the subquery
|
|
134
136
|
primaryKey.list.forEach(k => subquery.SELECT.columns.push({ ref: k.ref.slice(1) }))
|
|
135
137
|
|
|
136
|
-
const transformedSubquery = cqn4sql(subquery, model)
|
|
138
|
+
const transformedSubquery = cqn4sql(subquery, model, useTechnicalAlias)
|
|
137
139
|
|
|
138
140
|
// replace where condition of original query with the transformed subquery
|
|
139
141
|
// correlate UPDATE / DELETE query with subquery by primary key matches
|
|
@@ -173,8 +175,114 @@ function cqn4sql(originalQuery, model) {
|
|
|
173
175
|
}
|
|
174
176
|
}
|
|
175
177
|
|
|
178
|
+
if (cds.env.features.runtime_views) processRuntimeViews(transformedQuery, model)
|
|
179
|
+
|
|
176
180
|
return transformedQuery
|
|
177
181
|
|
|
182
|
+
/**
|
|
183
|
+
* If the target entity is annotated with persistence skip and has an underlying db entity,
|
|
184
|
+
* we treat it as a runtime view and transform it into a CTE.
|
|
185
|
+
*
|
|
186
|
+
* @param {object} transformedQuery - The query object to be transformed.
|
|
187
|
+
* @param {string} model - The data model used for inference and transformation.
|
|
188
|
+
*/
|
|
189
|
+
function processRuntimeViews(transformedQuery, model) {
|
|
190
|
+
const currentDef = transformedQuery._target
|
|
191
|
+
|
|
192
|
+
if (hasOwnSkip(currentDef)) {
|
|
193
|
+
if (!isRuntimeView(currentDef)) throw new Error(`${currentDef.name} is not a runtime view`)
|
|
194
|
+
|
|
195
|
+
addWith(currentDef, transformedQuery, model)
|
|
196
|
+
updateRefsWithRTVAlias(transformedQuery._with, transformedQuery)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Recursively call cqn4sql for all nested runtime views to calculate cte and
|
|
202
|
+
* add it as a with clause to the transformed query.
|
|
203
|
+
* Alias the runtime view with a unique alias and update all references to the runtime view to point to the alias.
|
|
204
|
+
*
|
|
205
|
+
* @param {object} rootDefinition - The root definition of the query. This is used to recursively process nested runtime views.
|
|
206
|
+
* @param {object} transformedQuery - The query object to be transformed.
|
|
207
|
+
* @param {string} model - The data model used for infer and cqn4sql.
|
|
208
|
+
*/
|
|
209
|
+
function addWith(rootDefinition, transformedQuery, model) {
|
|
210
|
+
if (!rootDefinition?.query) return
|
|
211
|
+
|
|
212
|
+
// early exit if already processed
|
|
213
|
+
if (transformedQuery._with?.some(w => w._source === rootDefinition)) return
|
|
214
|
+
|
|
215
|
+
const q = cds.ql.clone(rootDefinition.query)
|
|
216
|
+
if (q.SELECT) {
|
|
217
|
+
if (!q.SELECT.columns) q.SELECT.columns = ['*']
|
|
218
|
+
if (q.SELECT.columns.includes('*')) {
|
|
219
|
+
// cache element names for faster lookup
|
|
220
|
+
const existingColumns = new Set(
|
|
221
|
+
q.SELECT.columns
|
|
222
|
+
.map(col => col.as || col.ref?.at(-1))
|
|
223
|
+
.filter(Boolean)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
for (let el of rootDefinition.elements) {
|
|
227
|
+
if (el.type === 'cds.LargeBinary' && !existingColumns.has(el.name)) {
|
|
228
|
+
q.SELECT.columns.push({ ref: [el.name] })
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const inferredDQ = infer(q, model, useTechnicalAlias)
|
|
234
|
+
inferredDQ._with = transformedQuery._with
|
|
235
|
+
const transformedDQ = cqn4sql(inferredDQ, model, useTechnicalAlias)
|
|
236
|
+
|
|
237
|
+
if (q.SELECT?.from?.args) {
|
|
238
|
+
for (const arg of q.SELECT.from.args) {
|
|
239
|
+
addWith(arg.$refLinks.at(-1).definition, inferredDQ, model)
|
|
240
|
+
arg.as ??= arg.ref.at(-1).split('.').at(-1) // apply @sap/cds-compiler default alias
|
|
241
|
+
updateRefsWithRTVAlias(inferredDQ._with, transformedDQ, arg.ref)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const newWiths = transformedDQ._with || []
|
|
246
|
+
const rootDefinitionName = rootDefinition.name
|
|
247
|
+
|
|
248
|
+
defineProperty(transformedDQ, '_source', rootDefinition)
|
|
249
|
+
const alias = `RTV_${getImplicitAlias(rootDefinitionName)}`
|
|
250
|
+
transformedDQ.as = transformedDQ.joinTree.addNextAvailableTableAlias(alias, newWiths, rootDefinitionName)
|
|
251
|
+
|
|
252
|
+
// update SELECT.from with runtime view alias
|
|
253
|
+
if (hasOwnSkip(transformedDQ._target)) updateRefsWithRTVAlias(transformedDQ._with, transformedDQ)
|
|
254
|
+
|
|
255
|
+
if (transformedDQ._with) delete transformedDQ._with
|
|
256
|
+
newWiths.push(transformedDQ)
|
|
257
|
+
|
|
258
|
+
// propagate with clauses
|
|
259
|
+
if (!transformedQuery._with) transformedQuery._with = newWiths
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function updateRefsWithRTVAlias(_with, query, ref) {
|
|
263
|
+
if (!_with?.length) return
|
|
264
|
+
|
|
265
|
+
const _updateRef = (ref) => {
|
|
266
|
+
const refAlias = ref[0]
|
|
267
|
+
if (/RTV_$/.test(refAlias)) return
|
|
268
|
+
for (const w of _with) {
|
|
269
|
+
const aliasValue = w.joinTree._queryAliases.get(refAlias)
|
|
270
|
+
if (aliasValue) {
|
|
271
|
+
ref[0] = aliasValue
|
|
272
|
+
if (query.joinTree?._queryAliases) {
|
|
273
|
+
query.joinTree._queryAliases.set(refAlias, aliasValue)
|
|
274
|
+
}
|
|
275
|
+
break
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (ref) return _updateRef(ref)
|
|
281
|
+
|
|
282
|
+
if (query.SELECT.from.args) for (const arg of query.SELECT.from.args) _updateRef(arg.ref)
|
|
283
|
+
else if (query.SELECT.from.ref) _updateRef(query.SELECT.from.ref)
|
|
284
|
+
}
|
|
285
|
+
|
|
178
286
|
function transformSelectQuery(queryProp, transformedFrom, transformedWhere, transformedQuery) {
|
|
179
287
|
const { columns, having, groupBy, orderBy, limit } = queryProp
|
|
180
288
|
|
|
@@ -314,7 +422,9 @@ function cqn4sql(originalQuery, model) {
|
|
|
314
422
|
),
|
|
315
423
|
)
|
|
316
424
|
|
|
317
|
-
const
|
|
425
|
+
const def = getDefinition(nextAssoc.$refLink.definition.target)
|
|
426
|
+
const id = def.name
|
|
427
|
+
if (hasOwnSkip(def) && isRuntimeView(def)) addWith(model.definitions[id], transformedQuery, model)
|
|
318
428
|
const { args } = nextAssoc
|
|
319
429
|
const arg = {
|
|
320
430
|
ref: [args ? { id, args } : id],
|
|
@@ -352,7 +462,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
352
462
|
for (let i = 0; i < columns.length; i++) {
|
|
353
463
|
const col = columns[i]
|
|
354
464
|
|
|
355
|
-
if (isCalculatedOnRead(col.$refLinks?.
|
|
465
|
+
if (isCalculatedOnRead(col.$refLinks?.at(-1).definition) && !col.$refLinks?.at(-1).target?.SELECT) {
|
|
356
466
|
const name = getName(col)
|
|
357
467
|
if (!transformedColumns.some(inserted => getName(inserted) === name)) {
|
|
358
468
|
const calcElement = resolveCalculatedElement(col)
|
|
@@ -473,7 +583,13 @@ function cqn4sql(originalQuery, model) {
|
|
|
473
583
|
const refNavigation = col.ref.slice(col.$refLinks[0].definition.kind !== 'element' ? 1 : 0).join('_')
|
|
474
584
|
if (!columnAlias && col.flatName && col.flatName !== refNavigation) columnAlias = refNavigation
|
|
475
585
|
|
|
476
|
-
if (
|
|
586
|
+
if (
|
|
587
|
+
col.$refLinks.some(link => {
|
|
588
|
+
const def = getDefinition(link.definition.target)
|
|
589
|
+
return hasOwnSkip(def) && !isRuntimeView(def)
|
|
590
|
+
})
|
|
591
|
+
)
|
|
592
|
+
return
|
|
477
593
|
|
|
478
594
|
const flatColumns = getFlatColumnsFor(col, { baseName, columnAlias, tableAlias })
|
|
479
595
|
flatColumns.forEach(flatColumn => {
|
|
@@ -507,6 +623,11 @@ function cqn4sql(originalQuery, model) {
|
|
|
507
623
|
|
|
508
624
|
function getTransformedColumn(col) {
|
|
509
625
|
let ret
|
|
626
|
+
if (col !== null && typeof col === 'object' && '#' in col) {
|
|
627
|
+
ret = resolveEnumToken(col, [], -1)
|
|
628
|
+
// cast is already resolved inside resolveEnumToken; do not overwrite it here
|
|
629
|
+
return ret
|
|
630
|
+
}
|
|
510
631
|
if (col.func) {
|
|
511
632
|
ret = {
|
|
512
633
|
func: col.func,
|
|
@@ -519,7 +640,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
519
640
|
ret.xpr = getTransformedTokenStream(col.xpr)
|
|
520
641
|
}
|
|
521
642
|
if (ret) {
|
|
522
|
-
if (col.cast) ret.cast = col.cast
|
|
643
|
+
if (col.cast) ret.cast = resolveEnumCastType(col.cast)
|
|
523
644
|
return ret
|
|
524
645
|
}
|
|
525
646
|
return copy(col)
|
|
@@ -560,6 +681,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
560
681
|
res = { args: getTransformedFunctionArgs(value.args, $baseLink), func: value.func }
|
|
561
682
|
}
|
|
562
683
|
if (!omitAlias) res.as = column.as || column.name || column.flatName
|
|
684
|
+
setElementOnColumns(res, column.element || column)
|
|
563
685
|
return res
|
|
564
686
|
}
|
|
565
687
|
|
|
@@ -610,10 +732,11 @@ function cqn4sql(originalQuery, model) {
|
|
|
610
732
|
},
|
|
611
733
|
})
|
|
612
734
|
} else {
|
|
613
|
-
// target column is `val` or `
|
|
735
|
+
// target column is `val`, `xpr`, or `func` — destructure and throw away the ref with the $self
|
|
614
736
|
// eslint-disable-next-line no-unused-vars
|
|
615
|
-
const { xpr, val, ref, as: _as, ...rest } = referencedColumn
|
|
737
|
+
const { xpr, val, func, args, ref, as: _as, ...rest } = referencedColumn
|
|
616
738
|
if (xpr) rest.xpr = xpr
|
|
739
|
+
else if (func) { rest.func = func; rest.args = args }
|
|
617
740
|
else rest.val = val
|
|
618
741
|
dollarSelfColumn = { ...rest } // reassign dummyColumn without 'ref'
|
|
619
742
|
if (!omitAlias) dollarSelfColumn.as = as
|
|
@@ -643,8 +766,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
643
766
|
nameParts.push(nestedProjection.as ? nestedProjection.as : nestedProjection.ref.map(idOnly).join('_'))
|
|
644
767
|
const name = nameParts.join('_')
|
|
645
768
|
if (nestedProjection.ref) {
|
|
646
|
-
const augmentedInlineCol =
|
|
647
|
-
augmentedInlineCol.ref = col.ref ? [...col.ref, ...nestedProjection.ref] : nestedProjection.ref
|
|
769
|
+
const augmentedInlineCol = augmentInlineRefWithParent(nestedProjection, col)
|
|
648
770
|
if (
|
|
649
771
|
col.as ||
|
|
650
772
|
nestedProjection.as ||
|
|
@@ -653,19 +775,6 @@ function cqn4sql(originalQuery, model) {
|
|
|
653
775
|
) {
|
|
654
776
|
augmentedInlineCol.as = nameParts.join('_')
|
|
655
777
|
}
|
|
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
778
|
const flatColumns = getTransformedColumns([augmentedInlineCol])
|
|
670
779
|
flatColumns.forEach(flatColumn => {
|
|
671
780
|
const flatColumnName = flatColumn.as || flatColumn.ref[flatColumn.ref.length - 1]
|
|
@@ -682,6 +791,10 @@ function cqn4sql(originalQuery, model) {
|
|
|
682
791
|
if (!res.some(c => (c.as || c.ref.slice(1).map(idOnly).join('_')) === name)) {
|
|
683
792
|
const rewrittenColumn = { ...nestedProjection }
|
|
684
793
|
rewrittenColumn.as = name
|
|
794
|
+
// For xpr, we need to transform refs inside to include the struct prefix
|
|
795
|
+
if (nestedProjection.xpr && col.ref) {
|
|
796
|
+
rewrittenColumn.xpr = augmentInlineXprRefs(nestedProjection.xpr, col)
|
|
797
|
+
}
|
|
685
798
|
rewrittenColumns.push(rewrittenColumn)
|
|
686
799
|
}
|
|
687
800
|
}
|
|
@@ -690,6 +803,55 @@ function cqn4sql(originalQuery, model) {
|
|
|
690
803
|
})
|
|
691
804
|
|
|
692
805
|
return res
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Augment a ref column with the parent column's path and $refLinks.
|
|
809
|
+
*/
|
|
810
|
+
function augmentInlineRefWithParent(refCol, parentCol) {
|
|
811
|
+
const augmented = { ...refCol }
|
|
812
|
+
augmented.ref = parentCol.ref ? [...parentCol.ref, ...refCol.ref] : refCol.ref
|
|
813
|
+
Object.defineProperties(augmented, {
|
|
814
|
+
$refLinks: { value: [...refCol.$refLinks], writable: true },
|
|
815
|
+
isJoinRelevant: { value: refCol.isJoinRelevant, writable: true },
|
|
816
|
+
})
|
|
817
|
+
if (parentCol.ref) {
|
|
818
|
+
augmented.$refLinks.unshift(...parentCol.$refLinks)
|
|
819
|
+
augmented.isJoinRelevant = augmented.isJoinRelevant || parentCol.isJoinRelevant
|
|
820
|
+
}
|
|
821
|
+
return augmented
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Augment refs inside an xpr with the parent column's path and $refLinks,
|
|
826
|
+
* then transform them to flat refs.
|
|
827
|
+
*/
|
|
828
|
+
function augmentInlineXprRefs(xpr, parentCol) {
|
|
829
|
+
return xpr.map(token => {
|
|
830
|
+
if (typeof token === 'string' || token.val !== undefined) {
|
|
831
|
+
return token
|
|
832
|
+
}
|
|
833
|
+
if (token.ref && token.$refLinks) {
|
|
834
|
+
const augmented = augmentInlineRefWithParent(token, parentCol)
|
|
835
|
+
// Transform this single ref column to get the flat version
|
|
836
|
+
const transformed = getTransformedColumns([augmented])
|
|
837
|
+
if (transformed.length === 1) {
|
|
838
|
+
return transformed[0]
|
|
839
|
+
}
|
|
840
|
+
return augmented
|
|
841
|
+
}
|
|
842
|
+
if (token.xpr) {
|
|
843
|
+
return { ...token, xpr: augmentInlineXprRefs(token.xpr, parentCol) }
|
|
844
|
+
}
|
|
845
|
+
if (token.func && token.args) {
|
|
846
|
+
return {
|
|
847
|
+
...token, args: token.args.map(arg =>
|
|
848
|
+
arg.ref ? augmentInlineXprRefs([arg], parentCol)[0] : arg
|
|
849
|
+
)
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return token
|
|
853
|
+
})
|
|
854
|
+
}
|
|
693
855
|
}
|
|
694
856
|
|
|
695
857
|
/**
|
|
@@ -747,6 +909,9 @@ function cqn4sql(originalQuery, model) {
|
|
|
747
909
|
|
|
748
910
|
if (baseRefLinks.at(-1).definition.kind === 'entity') {
|
|
749
911
|
res.push(...getColumnsForWildcard(exclude, replace, col.as))
|
|
912
|
+
} else if (baseRefLinks.at(-1).definition.target) {
|
|
913
|
+
// Wildcard on association - need to include FK columns and join-relevant target columns
|
|
914
|
+
res.push(...expandAssociationWildcard(col, baseRef, baseRefLinks, exclude, replace))
|
|
750
915
|
} else
|
|
751
916
|
res.push(
|
|
752
917
|
...getFlatColumnsFor(col, { columnAlias: col.as, tableAlias: getTableAlias(col) }, [], {
|
|
@@ -757,6 +922,117 @@ function cqn4sql(originalQuery, model) {
|
|
|
757
922
|
return res
|
|
758
923
|
}
|
|
759
924
|
|
|
925
|
+
/**
|
|
926
|
+
* Expands a wildcard on an association into:
|
|
927
|
+
* 1. FK columns from the source table
|
|
928
|
+
* 2. Non-FK columns from the target via join
|
|
929
|
+
*/
|
|
930
|
+
function expandAssociationWildcard(col, baseRef, baseRefLinks, exclude, replace) {
|
|
931
|
+
const res = []
|
|
932
|
+
const assocDef = baseRefLinks.at(-1).definition
|
|
933
|
+
const targetDef = getDefinition(assocDef.target)
|
|
934
|
+
const columnAlias = col.as || baseRef.map(idOnly).join('_')
|
|
935
|
+
const sourceTableAlias = getTableAlias(col)
|
|
936
|
+
|
|
937
|
+
// Get the join alias for this association (set during join tree merge)
|
|
938
|
+
const joinAlias = baseRefLinks.at(-1).alias
|
|
939
|
+
|
|
940
|
+
// Collect FK element names
|
|
941
|
+
const fkNames = new Set()
|
|
942
|
+
if (assocDef.keys) {
|
|
943
|
+
for (const k of assocDef.keys) {
|
|
944
|
+
fkNames.add(k.ref[0])
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// First, add FK columns from source table
|
|
949
|
+
// These are accessed via the source table alias, not the join
|
|
950
|
+
const fkColumns = getFlatColumnsFor(col, { tableAlias: sourceTableAlias }, [], {
|
|
951
|
+
exclude,
|
|
952
|
+
replace,
|
|
953
|
+
})
|
|
954
|
+
res.push(...fkColumns.filter(fk => !col.excluding?.some(e => targetDef.elements[e] === fk.element)))
|
|
955
|
+
|
|
956
|
+
// Then, add non-FK columns from target via join
|
|
957
|
+
if (targetDef?.elements) {
|
|
958
|
+
for (const [elemName, elemDef] of Object.entries(targetDef.elements)) {
|
|
959
|
+
// Skip FK elements (already included above), virtual, blobs, and unmanaged assocs
|
|
960
|
+
if (fkNames.has(elemName)) continue
|
|
961
|
+
if (elemDef.virtual) continue
|
|
962
|
+
if (elemDef.type === 'cds.LargeBinary') continue
|
|
963
|
+
if (elemDef.on && !elemDef.keys) continue // unmanaged association
|
|
964
|
+
|
|
965
|
+
// Check exclusions
|
|
966
|
+
const fullName = `${columnAlias}_${elemName}`
|
|
967
|
+
if (exclude.some(e => (e.ref?.at(-1) || e.as || e) === elemName)) continue
|
|
968
|
+
if (exclude.some(e => (e.ref?.at(-1) || e.as || e) === fullName)) continue
|
|
969
|
+
|
|
970
|
+
// Check for replacement
|
|
971
|
+
const replacement = replace.find(r => (r.ref?.at(-1) || r.as) === elemName)
|
|
972
|
+
if (replacement) {
|
|
973
|
+
// Handle replacement - create augmented column
|
|
974
|
+
const augmented = { ...replacement }
|
|
975
|
+
augmented.as = fullName
|
|
976
|
+
res.push(...getTransformedColumns([augmented]))
|
|
977
|
+
continue
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Create column referencing the join alias
|
|
981
|
+
if (elemDef.elements) {
|
|
982
|
+
// Structured element - need to flatten it
|
|
983
|
+
const structCols = getFlatColumnsFor(
|
|
984
|
+
elemDef,
|
|
985
|
+
{ baseName: elemName, columnAlias: fullName, tableAlias: joinAlias },
|
|
986
|
+
[],
|
|
987
|
+
{ exclude, replace },
|
|
988
|
+
true,
|
|
989
|
+
)
|
|
990
|
+
res.push(...structCols)
|
|
991
|
+
} else if (elemDef.keys) {
|
|
992
|
+
// Association element - flatten its foreign keys
|
|
993
|
+
// The FK column name is: assocName_keyName (e.g., 'head_id')
|
|
994
|
+
for (const k of elemDef.keys) {
|
|
995
|
+
const keyName = k.as || k.ref.join('_')
|
|
996
|
+
const fkName = `${elemName}_${keyName}` // e.g., 'head_id'
|
|
997
|
+
const fkFullName = `${columnAlias}_${fkName}` // e.g., 'department_head_id'
|
|
998
|
+
|
|
999
|
+
// Check if this FK is excluded
|
|
1000
|
+
if (exclude.some(e => (e.ref?.at(-1) || e.as || e) === fkName)) continue
|
|
1001
|
+
if (exclude.some(e => (e.ref?.at(-1) || e.as || e) === fkFullName)) continue
|
|
1002
|
+
|
|
1003
|
+
const flatColumn = {
|
|
1004
|
+
ref: [joinAlias, fkName],
|
|
1005
|
+
as: fkFullName,
|
|
1006
|
+
}
|
|
1007
|
+
const fkElement = getElementForRef(k.ref, getDefinition(elemDef.target))
|
|
1008
|
+
setElementOnColumns(flatColumn, fkElement)
|
|
1009
|
+
res.push(flatColumn)
|
|
1010
|
+
}
|
|
1011
|
+
} else if (elemDef.value) {
|
|
1012
|
+
// Calculated element - resolve it
|
|
1013
|
+
const calcElement = resolveCalculatedElement({ $refLinks: [{ definition: elemDef }] }, true)
|
|
1014
|
+
if (calcElement.as) {
|
|
1015
|
+
calcElement.as = fullName
|
|
1016
|
+
} else {
|
|
1017
|
+
calcElement.as = fullName
|
|
1018
|
+
}
|
|
1019
|
+
res.push(calcElement)
|
|
1020
|
+
}
|
|
1021
|
+
else {
|
|
1022
|
+
// Scalar element
|
|
1023
|
+
const flatColumn = {
|
|
1024
|
+
ref: [joinAlias, elemName],
|
|
1025
|
+
as: fullName,
|
|
1026
|
+
}
|
|
1027
|
+
setElementOnColumns(flatColumn, elemDef)
|
|
1028
|
+
res.push(flatColumn)
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
return res
|
|
1034
|
+
}
|
|
1035
|
+
|
|
760
1036
|
/**
|
|
761
1037
|
* This function converts a column with an `expand` property into a subquery.
|
|
762
1038
|
*
|
|
@@ -972,7 +1248,12 @@ function cqn4sql(originalQuery, model) {
|
|
|
972
1248
|
} else if (pseudos.elements[col.ref?.[0]]) {
|
|
973
1249
|
res.push({ ...col })
|
|
974
1250
|
} else if (col.ref) {
|
|
975
|
-
if (
|
|
1251
|
+
if (
|
|
1252
|
+
col.$refLinks.some(link => {
|
|
1253
|
+
const def = getDefinition(link.definition.target)
|
|
1254
|
+
return hasOwnSkip(def) && !isRuntimeView(def)
|
|
1255
|
+
})
|
|
1256
|
+
)
|
|
976
1257
|
continue
|
|
977
1258
|
if (col.ref.length > 1 && col.ref[0] === '$self' && !col.$refLinks[0].definition.kind) {
|
|
978
1259
|
const dollarSelfReplacement = calculateDollarSelfColumn(col)
|
|
@@ -1070,10 +1351,18 @@ function cqn4sql(originalQuery, model) {
|
|
|
1070
1351
|
outerQueries.push(inferred)
|
|
1071
1352
|
defineProperty(q, 'outerQueries', outerQueries)
|
|
1072
1353
|
}
|
|
1354
|
+
|
|
1073
1355
|
const target = cds.infer.target(inferred) // REVISIT: we should reliably use inferred._target instead
|
|
1074
1356
|
if (isLocalized(target)) q.SELECT.localized = true
|
|
1075
1357
|
if (q.SELECT.from.ref && !q.SELECT.from.as) assignUniqueSubqueryAlias()
|
|
1076
|
-
|
|
1358
|
+
if (cds.env.features.runtime_views) q._with = transformedQuery._with
|
|
1359
|
+
const _q = cqn4sql(q, model, useTechnicalAlias)
|
|
1360
|
+
if (cds.env.features.runtime_views && _q._with) {
|
|
1361
|
+
if (!transformedQuery._with) transformedQuery._with = _q._with
|
|
1362
|
+
delete _q._with
|
|
1363
|
+
}
|
|
1364
|
+
return _q
|
|
1365
|
+
|
|
1077
1366
|
|
|
1078
1367
|
function assignUniqueSubqueryAlias() {
|
|
1079
1368
|
if (q.SELECT.from.uniqueSubqueryAlias) return
|
|
@@ -1108,8 +1397,8 @@ function cqn4sql(originalQuery, model) {
|
|
|
1108
1397
|
if (!exclude.includes(k)) {
|
|
1109
1398
|
const { index, tableAlias } = inferred.$combinedElements[k][0]
|
|
1110
1399
|
const element = tableAlias.elements[k]
|
|
1111
|
-
// ignore FK for odata csn / ignore blobs from wildcard expansion
|
|
1112
|
-
if (isManagedAssocInFlatMode(element) || element.type === 'cds.LargeBinary') continue
|
|
1400
|
+
// ignore FK for odata csn (but not for subquery sources where FK is not a separate element) / ignore blobs from wildcard expansion
|
|
1401
|
+
if ((!tableAlias.SELECT && isManagedAssocInFlatMode(element)) || element.type === 'cds.LargeBinary') continue
|
|
1113
1402
|
// for wildcard on subquery in from, just reference the elements
|
|
1114
1403
|
if (tableAlias.SELECT && !element.elements && !element.target) {
|
|
1115
1404
|
wildcardColumns.push(index ? { ref: [index, k] } : { ref: [k] })
|
|
@@ -1157,35 +1446,23 @@ function cqn4sql(originalQuery, model) {
|
|
|
1157
1446
|
|
|
1158
1447
|
/**
|
|
1159
1448
|
* Recursively expands a structured element into flat columns, representing all leaf paths.
|
|
1160
|
-
* This function transforms complex structured elements into simple column representations.
|
|
1161
|
-
*
|
|
1162
|
-
* For each element, the function checks if it's a structure, an association or a scalar,
|
|
1163
|
-
* and proceeds accordingly. If the element is a structure, it recursively fetches flat columns for all sub-elements.
|
|
1164
|
-
* If it's an association, it fetches flat columns for it's foreign keys.
|
|
1165
|
-
* If it's a scalar, it creates a flat column for it.
|
|
1166
1449
|
*
|
|
1167
|
-
*
|
|
1450
|
+
* Structures → flat sub-element columns. Associations → flat foreign key columns.
|
|
1451
|
+
* Scalars → single column reference.
|
|
1168
1452
|
*
|
|
1169
|
-
* @param {object} column - The
|
|
1453
|
+
* @param {object} column - The element to expand (may be a ref with $refLinks, or a raw element definition).
|
|
1170
1454
|
* @param {{
|
|
1171
|
-
*
|
|
1172
|
-
*
|
|
1173
|
-
*
|
|
1174
|
-
* }} names -
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
1177
|
-
*
|
|
1178
|
-
*
|
|
1179
|
-
*
|
|
1180
|
-
* @param {
|
|
1181
|
-
*
|
|
1182
|
-
* `{ struct_foo_leaf1 as bar_foo_leaf1, struct_foo_leaf2 as bar_foo_leaf2 }`.
|
|
1183
|
-
* @param {string} tableAlias - The table alias to prepend to the column name. Optional.
|
|
1184
|
-
* @param {Array} csnPath - An array containing CSN paths. Optional.
|
|
1185
|
-
* @param {Array} exclude - An array of columns to be excluded from the flat structure. Optional.
|
|
1186
|
-
* @param {Array} replace - An array of columns to be replaced in the flat structure. Optional.
|
|
1187
|
-
*
|
|
1188
|
-
* @returns {object[]} Returns an array of flat column(s) for the given element.
|
|
1455
|
+
* baseName?: string,
|
|
1456
|
+
* columnAlias?: string,
|
|
1457
|
+
* tableAlias?: string
|
|
1458
|
+
* }} [names] - Naming context:
|
|
1459
|
+
* - `baseName` — accumulated underscore-joined prefix for the flat column ref (e.g. `'address'` → `'address_street'`).
|
|
1460
|
+
* - `columnAlias` — explicit alias for the output column. Defaults to `column.as` when omitted.
|
|
1461
|
+
* - `tableAlias` — table alias prepended to the column ref.
|
|
1462
|
+
* @param {string[]} [csnPath=[]] - Accumulated CSN element path (used for `_csnPath` metadata on leaf columns).
|
|
1463
|
+
* @param {{ exclude?: Array, replace?: Array }} [excludeAndReplace] - Columns to exclude or replace during wildcard expansion.
|
|
1464
|
+
* @param {boolean} [isWildcard=false] - Whether this expansion originates from a wildcard; filters out LargeBinary.
|
|
1465
|
+
* @returns {object[]} Flat column(s) for the given element.
|
|
1189
1466
|
*/
|
|
1190
1467
|
function getFlatColumnsFor(column, names, csnPath = [], excludeAndReplace, isWildcard = false) {
|
|
1191
1468
|
if (!column) return column
|
|
@@ -1198,28 +1475,13 @@ function cqn4sql(originalQuery, model) {
|
|
|
1198
1475
|
let firstNonJoinRelevantAssoc, stepAfterAssoc
|
|
1199
1476
|
let element = $refLinks ? $refLinks[$refLinks.length - 1].definition : column
|
|
1200
1477
|
if (isWildcard && element.type === 'cds.LargeBinary') return []
|
|
1201
|
-
if (element.on && !element.keys)
|
|
1202
|
-
|
|
1203
|
-
else if (element.virtual === true) return []
|
|
1204
|
-
else if (!isJoinRelevant && flatName) baseName = flatName
|
|
1205
|
-
else if (isJoinRelevant) {
|
|
1206
|
-
const leafAssocIndex = column.$refLinks.findIndex(link => link.definition.isAssociation && link.onlyForeignKeyAccess)
|
|
1207
|
-
firstNonJoinRelevantAssoc = column.$refLinks[leafAssocIndex] || [...column.$refLinks].reverse().find(link => link.definition.isAssociation)
|
|
1208
|
-
stepAfterAssoc = column.$refLinks.at(leafAssocIndex + 1) || column.$refLinks.at(-1)
|
|
1209
|
-
let elements = firstNonJoinRelevantAssoc.definition.elements || firstNonJoinRelevantAssoc.definition.foreignKeys
|
|
1210
|
-
if (elements && stepAfterAssoc.definition.name in elements) {
|
|
1211
|
-
element = firstNonJoinRelevantAssoc.definition
|
|
1212
|
-
baseName = getFullName(firstNonJoinRelevantAssoc.definition)
|
|
1213
|
-
columnAlias = column.as || column.ref.slice(0, -1).map(idOnly).join('_')
|
|
1214
|
-
} else baseName = getFullName(column.$refLinks[column.$refLinks.length - 1].definition)
|
|
1215
|
-
|
|
1216
|
-
if(column.element && !isAssocOrStruct(column.element)) {
|
|
1217
|
-
columnAlias = column.as || leafAssocIndex === -1 ? columnAlias : column.ref.slice(leafAssocIndex - 1).map(idOnly).join('_')
|
|
1218
|
-
const res = { ref: [tableAlias, calculateElementName(column)], as: columnAlias }
|
|
1219
|
-
setElementOnColumns(res, column.element)
|
|
1220
|
-
return [res]
|
|
1221
|
-
}
|
|
1478
|
+
if (element.on && !element.keys) return [] // unmanaged doesn't make it into columns
|
|
1479
|
+
if (element.virtual === true) return []
|
|
1222
1480
|
|
|
1481
|
+
if (!isJoinRelevant && flatName) baseName = flatName
|
|
1482
|
+
else if (isJoinRelevant) {
|
|
1483
|
+
const earlyResult = resolveJoinRelevantNames()
|
|
1484
|
+
if (earlyResult) return earlyResult
|
|
1223
1485
|
} else if (!baseName && structsAreUnfoldedAlready) {
|
|
1224
1486
|
baseName = element.name // name is already fully constructed
|
|
1225
1487
|
} else {
|
|
@@ -1250,108 +1512,41 @@ function cqn4sql(originalQuery, model) {
|
|
|
1250
1512
|
return getFlatColumnsFor(replacedBy, { baseName, columnAlias: replacedBy.as, tableAlias }, csnPath)
|
|
1251
1513
|
}
|
|
1252
1514
|
|
|
1253
|
-
csnPath
|
|
1515
|
+
csnPath = [...csnPath, element.name]
|
|
1254
1516
|
|
|
1255
|
-
if (element.keys)
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
const flattenThisForeignKey =
|
|
1261
|
-
!$refLinks || // the association is passed as element, not as ref --> flatten full foreign key
|
|
1262
|
-
element === $refLinks.at(-1).definition || // the association is the leaf of the ref --> flatten full foreign key
|
|
1263
|
-
keyElement === stepAfterAssoc.definition // the foreign key is the leaf of the ref --> only flatten this specific foreign key
|
|
1264
|
-
if (flattenThisForeignKey) {
|
|
1265
|
-
const fkElement = getElementForRef(k.ref, getDefinition(element.target))
|
|
1266
|
-
let fkBaseName
|
|
1267
|
-
if (!firstNonJoinRelevantAssoc || firstNonJoinRelevantAssoc.onlyForeignKeyAccess) fkBaseName = `${baseName}_${k.as || k.ref.at(-1)}`
|
|
1268
|
-
// e.g. if foreign key is accessed via infix filter - use join alias to access key in target
|
|
1269
|
-
else fkBaseName = k.ref.at(-1)
|
|
1270
|
-
const fkPath = [...csnPath, k.ref.at(-1)]
|
|
1271
|
-
if (fkElement.elements) {
|
|
1272
|
-
// structured key
|
|
1273
|
-
for (const e of Object.values(fkElement.elements)) {
|
|
1274
|
-
let alias
|
|
1275
|
-
if (columnAlias) {
|
|
1276
|
-
const fkName = k.as
|
|
1277
|
-
? `${k.as}_${e.name}` // foreign key might also be re-named: `assoc { id as foo }`
|
|
1278
|
-
: `${k.ref.join('_')}_${e.name}`
|
|
1279
|
-
alias = `${columnAlias}_${fkName}`
|
|
1280
|
-
}
|
|
1281
|
-
flatColumns.push(
|
|
1282
|
-
...getFlatColumnsFor(
|
|
1283
|
-
e,
|
|
1284
|
-
{ baseName: fkBaseName, columnAlias: alias, tableAlias },
|
|
1285
|
-
[...fkPath],
|
|
1286
|
-
excludeAndReplace,
|
|
1287
|
-
isWildcard,
|
|
1288
|
-
),
|
|
1289
|
-
)
|
|
1290
|
-
}
|
|
1291
|
-
} else if (fkElement.isAssociation) {
|
|
1292
|
-
// assoc as key
|
|
1293
|
-
flatColumns.push(
|
|
1294
|
-
...getFlatColumnsFor(
|
|
1295
|
-
fkElement,
|
|
1296
|
-
{ baseName, columnAlias, tableAlias },
|
|
1297
|
-
csnPath,
|
|
1298
|
-
excludeAndReplace,
|
|
1299
|
-
isWildcard,
|
|
1300
|
-
),
|
|
1301
|
-
)
|
|
1302
|
-
} else {
|
|
1303
|
-
// leaf reached
|
|
1304
|
-
let flatColumn
|
|
1305
|
-
if (columnAlias) {
|
|
1306
|
-
// if the column has an explicit alias AND the original ref
|
|
1307
|
-
// directly resolves to the foreign key, we must not append the fk name to the column alias
|
|
1308
|
-
// e.g. `assoc.fk as FOO` => columns.alias = FOO
|
|
1309
|
-
// `assoc as FOO` => columns.alias = FOO_fk
|
|
1310
|
-
let columnAliasWithFlatFk
|
|
1311
|
-
if (!(column.as && fkElement === column.$refLinks?.at(-1).definition))
|
|
1312
|
-
columnAliasWithFlatFk = `${columnAlias}_${k.as || k.ref.join('_')}`
|
|
1313
|
-
flatColumn = { ref: [fkBaseName], as: columnAliasWithFlatFk || columnAlias }
|
|
1314
|
-
} else flatColumn = { ref: [fkBaseName] }
|
|
1315
|
-
if (tableAlias) flatColumn.ref.unshift(tableAlias)
|
|
1316
|
-
|
|
1317
|
-
// in a flat model, we must assign the foreign key rather than the key in the target
|
|
1318
|
-
const flatForeignKey = getDefinition(element.parent.name)?.elements[fkBaseName]
|
|
1319
|
-
|
|
1320
|
-
setElementOnColumns(flatColumn, flatForeignKey || fkElement)
|
|
1321
|
-
defineProperty(flatColumn, '_csnPath', csnPath)
|
|
1322
|
-
flatColumns.push(flatColumn)
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
return flatColumns
|
|
1327
|
-
} else if (element.elements && element.type !== 'cds.Map') {
|
|
1517
|
+
if (element.keys) return flattenForeignKeys()
|
|
1518
|
+
if (element.elements && element.type !== 'cds.Map') return flattenStructElements()
|
|
1519
|
+
return buildScalarColumn()
|
|
1520
|
+
|
|
1521
|
+
function flattenStructElements() {
|
|
1328
1522
|
const flatRefs = []
|
|
1329
|
-
Object.values(element.elements)
|
|
1523
|
+
for (const e of Object.values(element.elements)) {
|
|
1330
1524
|
const alias = columnAlias ? `${columnAlias}_${e.name}` : null
|
|
1331
1525
|
flatRefs.push(
|
|
1332
1526
|
...getFlatColumnsFor(
|
|
1333
1527
|
e,
|
|
1334
1528
|
{ baseName, columnAlias: alias, tableAlias },
|
|
1335
|
-
|
|
1529
|
+
csnPath,
|
|
1336
1530
|
excludeAndReplace,
|
|
1337
1531
|
isWildcard,
|
|
1338
1532
|
),
|
|
1339
1533
|
)
|
|
1340
|
-
}
|
|
1534
|
+
}
|
|
1341
1535
|
return flatRefs
|
|
1342
1536
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
flatRef
|
|
1346
|
-
if (
|
|
1347
|
-
|
|
1348
|
-
columnAlias = baseName
|
|
1537
|
+
|
|
1538
|
+
function buildScalarColumn() {
|
|
1539
|
+
const flatRef = tableAlias ? { ref: [tableAlias, baseName] } : { ref: [baseName] }
|
|
1540
|
+
if (column.cast) {
|
|
1541
|
+
flatRef.cast = column.cast
|
|
1542
|
+
if (!columnAlias) columnAlias = baseName
|
|
1543
|
+
}
|
|
1544
|
+
if (column.sort) flatRef.sort = column.sort
|
|
1545
|
+
if (columnAlias) flatRef.as = columnAlias
|
|
1546
|
+
setElementOnColumns(flatRef, element)
|
|
1547
|
+
defineProperty(flatRef, '_csnPath', csnPath)
|
|
1548
|
+
return [flatRef]
|
|
1349
1549
|
}
|
|
1350
|
-
if (column.sort) flatRef.sort = column.sort
|
|
1351
|
-
if (columnAlias) flatRef.as = columnAlias
|
|
1352
|
-
setElementOnColumns(flatRef, element)
|
|
1353
|
-
defineProperty(flatRef, '_csnPath', csnPath)
|
|
1354
|
-
return [flatRef]
|
|
1355
1550
|
|
|
1356
1551
|
function getReplacement(from) {
|
|
1357
1552
|
return from?.find(replacement => {
|
|
@@ -1359,6 +1554,101 @@ function cqn4sql(originalQuery, model) {
|
|
|
1359
1554
|
return nameOfExcludedColumn === element.name
|
|
1360
1555
|
})
|
|
1361
1556
|
}
|
|
1557
|
+
|
|
1558
|
+
function resolveJoinRelevantNames() {
|
|
1559
|
+
const leafAssocIndex = column.$refLinks.findIndex(
|
|
1560
|
+
link => link.definition.isAssociation && link.onlyForeignKeyAccess,
|
|
1561
|
+
)
|
|
1562
|
+
firstNonJoinRelevantAssoc =
|
|
1563
|
+
column.$refLinks[leafAssocIndex] || [...column.$refLinks].reverse().find(link => link.definition.isAssociation)
|
|
1564
|
+
stepAfterAssoc = column.$refLinks.at(leafAssocIndex + 1) || column.$refLinks.at(-1)
|
|
1565
|
+
const targetElements = firstNonJoinRelevantAssoc.definition.elements || firstNonJoinRelevantAssoc.definition.foreignKeys
|
|
1566
|
+
if (targetElements && stepAfterAssoc.definition.name in targetElements) {
|
|
1567
|
+
element = firstNonJoinRelevantAssoc.definition
|
|
1568
|
+
baseName = getFullName(firstNonJoinRelevantAssoc.definition)
|
|
1569
|
+
columnAlias = column.as || column.ref.slice(0, -1).map(idOnly).join('_')
|
|
1570
|
+
} else {
|
|
1571
|
+
baseName = getFullName(column.$refLinks.at(-1).definition)
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
if (column.element && !isAssocOrStruct(column.element)) {
|
|
1575
|
+
columnAlias =
|
|
1576
|
+
column.as || (leafAssocIndex === -1 ? columnAlias : column.ref.slice(leafAssocIndex - 1).map(idOnly).join('_'))
|
|
1577
|
+
const res = { ref: [tableAlias, calculateElementName(column)], as: columnAlias }
|
|
1578
|
+
setElementOnColumns(res, column.element)
|
|
1579
|
+
return [res]
|
|
1580
|
+
}
|
|
1581
|
+
return null
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
function flattenForeignKeys() {
|
|
1585
|
+
const flatColumns = []
|
|
1586
|
+
for (const k of element.keys) {
|
|
1587
|
+
const fkElement = getElementForRef(k.ref, getDefinition(element.target))
|
|
1588
|
+
// if only one part of a foreign key is requested, only flatten the partial key
|
|
1589
|
+
const shouldFlatten =
|
|
1590
|
+
!$refLinks || // the association is passed as element, not as ref --> flatten full foreign key
|
|
1591
|
+
element === $refLinks.at(-1).definition || // the association is the leaf of the ref --> flatten full foreign key
|
|
1592
|
+
fkElement === stepAfterAssoc.definition // the foreign key is the leaf of the ref --> only flatten this specific foreign key
|
|
1593
|
+
if (!shouldFlatten) continue
|
|
1594
|
+
|
|
1595
|
+
// e.g. if foreign key is accessed via infix filter - use join alias to access key in target
|
|
1596
|
+
const fkBaseName = !firstNonJoinRelevantAssoc || firstNonJoinRelevantAssoc.onlyForeignKeyAccess
|
|
1597
|
+
? `${baseName}_${k.as || k.ref.at(-1)}`
|
|
1598
|
+
: k.ref.at(-1)
|
|
1599
|
+
const fkPath = [...csnPath, k.ref.at(-1)]
|
|
1600
|
+
|
|
1601
|
+
if (fkElement.elements) {
|
|
1602
|
+
// structured key
|
|
1603
|
+
for (const e of Object.values(fkElement.elements)) {
|
|
1604
|
+
let alias
|
|
1605
|
+
if (columnAlias) {
|
|
1606
|
+
const fkName = k.as
|
|
1607
|
+
? `${k.as}_${e.name}` // foreign key might also be re-named: `assoc { id as foo }`
|
|
1608
|
+
: `${k.ref.join('_')}_${e.name}`
|
|
1609
|
+
alias = `${columnAlias}_${fkName}`
|
|
1610
|
+
}
|
|
1611
|
+
flatColumns.push(
|
|
1612
|
+
...getFlatColumnsFor(
|
|
1613
|
+
e,
|
|
1614
|
+
{ baseName: fkBaseName, columnAlias: alias, tableAlias },
|
|
1615
|
+
fkPath,
|
|
1616
|
+
excludeAndReplace,
|
|
1617
|
+
isWildcard,
|
|
1618
|
+
),
|
|
1619
|
+
)
|
|
1620
|
+
}
|
|
1621
|
+
} else if (fkElement.isAssociation) {
|
|
1622
|
+
// assoc as key
|
|
1623
|
+
flatColumns.push(
|
|
1624
|
+
...getFlatColumnsFor(fkElement, { baseName, columnAlias, tableAlias }, csnPath, excludeAndReplace, isWildcard),
|
|
1625
|
+
)
|
|
1626
|
+
} else {
|
|
1627
|
+
// leaf reached
|
|
1628
|
+
let flatColumn
|
|
1629
|
+
if (columnAlias) {
|
|
1630
|
+
// if the column has an explicit alias AND the original ref
|
|
1631
|
+
// directly resolves to the foreign key, we must not append the fk name to the column alias
|
|
1632
|
+
// e.g. `assoc.fk as FOO` => columns.alias = FOO
|
|
1633
|
+
// `assoc as FOO` => columns.alias = FOO_fk
|
|
1634
|
+
let fkAlias = columnAlias
|
|
1635
|
+
if (!(column.as && fkElement === column.$refLinks?.at(-1).definition))
|
|
1636
|
+
fkAlias = `${columnAlias}_${k.as || k.ref.join('_')}`
|
|
1637
|
+
flatColumn = { ref: [fkBaseName], as: fkAlias }
|
|
1638
|
+
} else {
|
|
1639
|
+
flatColumn = { ref: [fkBaseName] }
|
|
1640
|
+
}
|
|
1641
|
+
if (tableAlias) flatColumn.ref.unshift(tableAlias)
|
|
1642
|
+
|
|
1643
|
+
// in a flat model, we must assign the foreign key rather than the key in the target
|
|
1644
|
+
const flatForeignKey = getDefinition(element.parent.name)?.elements[fkBaseName]
|
|
1645
|
+
setElementOnColumns(flatColumn, flatForeignKey || fkElement)
|
|
1646
|
+
defineProperty(flatColumn, '_csnPath', csnPath)
|
|
1647
|
+
flatColumns.push(flatColumn)
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
return flatColumns
|
|
1651
|
+
}
|
|
1362
1652
|
}
|
|
1363
1653
|
|
|
1364
1654
|
/**
|
|
@@ -1446,6 +1736,9 @@ function cqn4sql(originalQuery, model) {
|
|
|
1446
1736
|
transformedTokenStream[i + 1] = whereExists
|
|
1447
1737
|
// skip newly created subquery from being iterated
|
|
1448
1738
|
i += 1
|
|
1739
|
+
} else if (token !== null && typeof token === 'object' && '#' in token) {
|
|
1740
|
+
// Enum token: resolve to its value
|
|
1741
|
+
transformedTokenStream.push(resolveEnumToken(token, tokenStream, i))
|
|
1449
1742
|
} else if (token.list) {
|
|
1450
1743
|
if (token.list.length === 0) {
|
|
1451
1744
|
// replace `[not] in <empty list>` to harmonize behavior across dbs
|
|
@@ -1463,8 +1756,13 @@ function cqn4sql(originalQuery, model) {
|
|
|
1463
1756
|
transformedTokenStream.push({ list: [] })
|
|
1464
1757
|
}
|
|
1465
1758
|
} else {
|
|
1466
|
-
|
|
1467
|
-
|
|
1759
|
+
let { list } = token
|
|
1760
|
+
// Resolve enum tokens in list items using context from the parent token stream
|
|
1761
|
+
if (list.some(e => e !== null && typeof e === 'object' && '#' in e)) {
|
|
1762
|
+
const enumDef = findEnumDefinition(tokenStream, i)
|
|
1763
|
+
list = list.map(item => (item !== null && typeof item === 'object' && '#' in item) ? resolveEnumToken(item, tokenStream, i, enumDef) : item)
|
|
1764
|
+
}
|
|
1765
|
+
if (list.every(e => 'val' in e))
|
|
1468
1766
|
// no need for transformation
|
|
1469
1767
|
transformedTokenStream.push({ list })
|
|
1470
1768
|
else transformedTokenStream.push({ list: getTransformedTokenStream(list, { $baseLink, prop: 'list' }) })
|
|
@@ -1496,14 +1794,13 @@ function cqn4sql(originalQuery, model) {
|
|
|
1496
1794
|
ops.push(rhs)
|
|
1497
1795
|
rhs = tokenStream[i + 3]
|
|
1498
1796
|
indexRhs += 1
|
|
1499
|
-
rhsDef = rhs?.$refLinks?.at(-1)?.definition
|
|
1500
1797
|
}
|
|
1501
1798
|
|
|
1502
1799
|
if (notSupportedOps.some(([firstOp]) => firstOp === next))
|
|
1503
1800
|
throw new Error(`The operator "${next}" can only be used with scalar operands`)
|
|
1504
1801
|
|
|
1505
1802
|
const newTokens = expandComparison(token, ops, rhs, $baseLink)
|
|
1506
|
-
if(newTokens.length === 0)
|
|
1803
|
+
if (newTokens.length === 0)
|
|
1507
1804
|
throw new Error(`Can't compare two empty structures`)
|
|
1508
1805
|
|
|
1509
1806
|
const needXpr = Boolean(tokenStream[i - 1] || tokenStream[indexRhs + 1])
|
|
@@ -1538,9 +1835,9 @@ function cqn4sql(originalQuery, model) {
|
|
|
1538
1835
|
const lastAssoc =
|
|
1539
1836
|
token.isJoinRelevant && [...token.$refLinks].reverse().find(l => l.definition.isAssociation)
|
|
1540
1837
|
const tableAlias = getTableAlias(token, (!lastAssoc?.onlyForeignKeyAccess && lastAssoc) || $baseLink)
|
|
1541
|
-
if(isAssocOrStruct(definition)) {
|
|
1542
|
-
const flat =
|
|
1543
|
-
if(flat.length === 0)
|
|
1838
|
+
if (isAssocOrStruct(definition)) {
|
|
1839
|
+
const flat = getFlatColumnsFor(token, { tableAlias: $baseLink?.alias || tableAlias })
|
|
1840
|
+
if (flat.length === 0)
|
|
1544
1841
|
throw new Error(`Structured element “${getFullName(definition)}” expands to nothing and can't be used in expressions`)
|
|
1545
1842
|
else if (flat.length > 1 && context.prop !== 'list') // only acceptable in `list`
|
|
1546
1843
|
throw new Error(`Structured element “${getFullName(definition)}” expands to multiple fields and can't be used in expressions`)
|
|
@@ -1566,6 +1863,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1566
1863
|
}
|
|
1567
1864
|
}
|
|
1568
1865
|
|
|
1866
|
+
if (result.cast) result.cast = resolveEnumCastType(result.cast)
|
|
1569
1867
|
transformedTokenStream.push(result)
|
|
1570
1868
|
}
|
|
1571
1869
|
}
|
|
@@ -1673,7 +1971,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1673
1971
|
|
|
1674
1972
|
function assertNoStructInXpr(token, context) {
|
|
1675
1973
|
const definition = token.$refLinks?.at(-1).definition
|
|
1676
|
-
if(!definition) return
|
|
1974
|
+
if (!definition) return
|
|
1677
1975
|
const rejectStructs = context && (context.prop in { where: 1, having: 1 })
|
|
1678
1976
|
// unmanaged is always forbidden
|
|
1679
1977
|
// expanding a ref in a `where`/`having` context
|
|
@@ -1785,7 +2083,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1785
2083
|
|
|
1786
2084
|
// OData variant w/o mentioning key
|
|
1787
2085
|
if (refReverse[0].where?.length === 1 && refReverse[0].where[0].val) {
|
|
1788
|
-
filterConditions.push(getTransformedTokenStream(refReverse[0].where,{ $baseLink: $refLinksReverse[0] }))
|
|
2086
|
+
filterConditions.push(getTransformedTokenStream(refReverse[0].where, { $baseLink: $refLinksReverse[0] }))
|
|
1789
2087
|
}
|
|
1790
2088
|
|
|
1791
2089
|
if (existingWhere.length > 0) filterConditions.push(existingWhere)
|
|
@@ -2231,7 +2529,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2231
2529
|
return SELECT
|
|
2232
2530
|
}
|
|
2233
2531
|
|
|
2234
|
-
/**
|
|
2532
|
+
/**
|
|
2235
2533
|
* For a given search term calculate a search expression which can be used in a where clause.
|
|
2236
2534
|
* The search function is pushed to a subquery and the primary key(s) of the entity is/are used to match
|
|
2237
2535
|
* the search results of the subquery.
|
|
@@ -2259,13 +2557,13 @@ function cqn4sql(originalQuery, model) {
|
|
|
2259
2557
|
// for aggregated queries / search on subqueries we do not do a subquery search
|
|
2260
2558
|
if (inferred.SELECT.groupBy || entity.SELECT)
|
|
2261
2559
|
return searchFunc
|
|
2262
|
-
|
|
2560
|
+
|
|
2263
2561
|
const matchColumns = getPrimaryKey(entity)
|
|
2264
2562
|
if (matchColumns.length === 0 || searchIn.every(r => r.ref.length === 1)) // keyless or not deep, fallback to old behavior
|
|
2265
2563
|
return searchFunc
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
return { xpr: [
|
|
2564
|
+
|
|
2565
|
+
const subquery = SELECT.from(entity).columns(...matchColumns).where(searchFunc)
|
|
2566
|
+
return { xpr: [matchColumns.length === 1 ? matchColumns[0] : { list: matchColumns }, 'in', subquery] }
|
|
2269
2567
|
}
|
|
2270
2568
|
|
|
2271
2569
|
/**
|
|
@@ -2282,7 +2580,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
2282
2580
|
if (!node || !node.$refLinks || !node.ref) {
|
|
2283
2581
|
throw new Error('Invalid node')
|
|
2284
2582
|
}
|
|
2285
|
-
if(node.$refLinks[0].$main) {
|
|
2583
|
+
if (node.$refLinks[0].$main) {
|
|
2286
2584
|
if (node.isJoinRelevant) {
|
|
2287
2585
|
return getJoinRelevantAlias(node)
|
|
2288
2586
|
}
|
|
@@ -2354,6 +2652,116 @@ function cqn4sql(originalQuery, model) {
|
|
|
2354
2652
|
}
|
|
2355
2653
|
return result
|
|
2356
2654
|
}
|
|
2655
|
+
|
|
2656
|
+
/**
|
|
2657
|
+
* Resolves an enum token to a value literal.
|
|
2658
|
+
*
|
|
2659
|
+
* If the token already has a `val`, it is used directly.
|
|
2660
|
+
* Otherwise, the enum value is resolved by looking up the symbol
|
|
2661
|
+
* in the enum definition found from the surrounding context.
|
|
2662
|
+
*
|
|
2663
|
+
* @param {object} token - The enum token with a `#` property.
|
|
2664
|
+
* @param {object[]} tokenStream - The surrounding token stream for context discovery.
|
|
2665
|
+
* @param {number} index - The index of the enum token in the token stream.
|
|
2666
|
+
* @param {object} [enumDef] - An already-discovered enum definition (optimization for lists).
|
|
2667
|
+
* @returns {object} A value token `{ val: resolvedValue }`.
|
|
2668
|
+
*/
|
|
2669
|
+
function resolveEnumToken(token, tokenStream, index, enumDef) {
|
|
2670
|
+
if ('val' in token) {
|
|
2671
|
+
const result = { val: token.val }
|
|
2672
|
+
if (token.cast) result.cast = resolveEnumCastType(token.cast)
|
|
2673
|
+
return result
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
// Check if the token itself has a cast with an enum type
|
|
2677
|
+
if (!enumDef && token.cast?.type) {
|
|
2678
|
+
const typeDef = model.definitions[token.cast.type]
|
|
2679
|
+
if (typeDef?.enum) enumDef = typeDef.enum
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
if (!enumDef) enumDef = findEnumDefinition(tokenStream, index)
|
|
2683
|
+
if (!enumDef) {
|
|
2684
|
+
throw new Error(`Can't resolve enum value "#${token['#']}"`)
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
const entry = enumDef[token['#']]
|
|
2688
|
+
if (!entry) {
|
|
2689
|
+
throw new Error(`Unknown enum symbol "#${token['#']}"`)
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
const result = { val: 'val' in entry ? entry.val : token['#'] }
|
|
2693
|
+
if (token.cast) result.cast = resolveEnumCastType(token.cast)
|
|
2694
|
+
return result
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
/**
|
|
2698
|
+
* If `cast.type` refers to a user-defined enum type, resolves it to the
|
|
2699
|
+
* underlying scalar CDS built-in type so that the SQL builder (`cqn2sql`)
|
|
2700
|
+
* can render a valid SQL type name.
|
|
2701
|
+
*
|
|
2702
|
+
* Example: `{ type: 'enums.Priority' }` → `{ type: 'cds.Integer' }`
|
|
2703
|
+
*
|
|
2704
|
+
* Non-enum types (including CDS built-ins) are returned unchanged.
|
|
2705
|
+
*
|
|
2706
|
+
* @param {object} cast - The cast descriptor with a `type` property.
|
|
2707
|
+
* @returns {object} The cast descriptor with the resolved type.
|
|
2708
|
+
*/
|
|
2709
|
+
function resolveEnumCastType(cast) {
|
|
2710
|
+
if (!cast?.type) return cast
|
|
2711
|
+
let def = model.definitions[cast.type]
|
|
2712
|
+
while (def?.enum) {
|
|
2713
|
+
const baseType = def.type
|
|
2714
|
+
if (!baseType) return cast // no base type declared – leave as-is
|
|
2715
|
+
if (cds.builtin.types[baseType]) return { ...cast, type: baseType }
|
|
2716
|
+
def = model.definitions[baseType]
|
|
2717
|
+
}
|
|
2718
|
+
return cast
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
/**
|
|
2722
|
+
* Scans the token stream around the given index to find an element
|
|
2723
|
+
* definition that has an `enum` property, which can be used to resolve
|
|
2724
|
+
* enum symbols to their values.
|
|
2725
|
+
*
|
|
2726
|
+
* @param {object[]} tokenStream - The token stream to scan.
|
|
2727
|
+
* @param {number} index - The index of the enum token.
|
|
2728
|
+
* @returns {object|null} The enum definition object, or null if not found.
|
|
2729
|
+
*/
|
|
2730
|
+
function findEnumDefinition(tokenStream, index) {
|
|
2731
|
+
// Scan backward
|
|
2732
|
+
for (let j = index - 1; j >= 0; j--) {
|
|
2733
|
+
const t = tokenStream[j]
|
|
2734
|
+
if (typeof t === 'string') continue // operators, keywords
|
|
2735
|
+
if (t !== null && typeof t === 'object' && '#' in t) continue // other enum tokens
|
|
2736
|
+
if ('val' in t && !t.ref) continue // plain value literals
|
|
2737
|
+
|
|
2738
|
+
const def = t.$refLinks?.at(-1)?.definition
|
|
2739
|
+
if (def?.enum) return def.enum
|
|
2740
|
+
if (t.cast?.type) {
|
|
2741
|
+
const typeDef = model.definitions[t.cast.type]
|
|
2742
|
+
if (typeDef?.enum) return typeDef.enum
|
|
2743
|
+
}
|
|
2744
|
+
if (def) break // found a ref without enum type, stop
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
// Scan forward
|
|
2748
|
+
for (let j = index + 1; j < tokenStream.length; j++) {
|
|
2749
|
+
const t = tokenStream[j]
|
|
2750
|
+
if (typeof t === 'string') continue
|
|
2751
|
+
if (t !== null && typeof t === 'object' && '#' in t) continue
|
|
2752
|
+
if ('val' in t && !t.ref) continue
|
|
2753
|
+
|
|
2754
|
+
const def = t.$refLinks?.at(-1)?.definition
|
|
2755
|
+
if (def?.enum) return def.enum
|
|
2756
|
+
if (t.cast?.type) {
|
|
2757
|
+
const typeDef = model.definitions[t.cast.type]
|
|
2758
|
+
if (typeDef?.enum) return typeDef.enum
|
|
2759
|
+
}
|
|
2760
|
+
if (def) break
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
return null
|
|
2764
|
+
}
|
|
2357
2765
|
}
|
|
2358
2766
|
|
|
2359
2767
|
function calculateElementName(token) {
|
|
@@ -2425,7 +2833,7 @@ function assignQueryModifiers(SELECT, modifiers) {
|
|
|
2425
2833
|
else SELECT.having.push('and', ...val)
|
|
2426
2834
|
} else if (key === 'where') {
|
|
2427
2835
|
// ignore OData shortcut variant: `… bookshop.Orders:items[2]`
|
|
2428
|
-
if(!val || val.length === 1 && val[0].val) continue
|
|
2836
|
+
if (!val || val.length === 1 && val[0].val) continue
|
|
2429
2837
|
if (!SELECT.where) SELECT.where = val
|
|
2430
2838
|
// infix filter comes first in resulting where
|
|
2431
2839
|
else SELECT.where = [...(hasLogicalOr(val) ? [asXpr(val)] : val), 'and', ...(hasLogicalOr(SELECT.where) ? [asXpr(SELECT.where)] : SELECT.where)]
|