@cap-js/db-service 1.0.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 +9 -0
- package/LICENSE +201 -0
- package/README.md +5 -0
- package/index.js +4 -0
- package/lib/InsertResults.js +95 -0
- package/lib/SQLService.js +288 -0
- package/lib/common/DatabaseService.js +144 -0
- package/lib/cql-functions.js +148 -0
- package/lib/cqn2sql.js +605 -0
- package/lib/cqn4sql.js +1846 -0
- package/lib/deep-queries.js +244 -0
- package/lib/fill-in-keys.js +65 -0
- package/lib/infer/index.js +922 -0
- package/lib/infer/join-tree.js +188 -0
- package/lib/infer/pseudos.js +23 -0
- package/package.json +30 -0
package/lib/cqn4sql.js
ADDED
|
@@ -0,0 +1,1846 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const cds = require('@sap/cds/lib')
|
|
4
|
+
const { computeColumnsToBeSearched } = require('@sap/cds/libx/_runtime/cds-services/services/utils/columns.js')
|
|
5
|
+
|
|
6
|
+
const infer = require('./infer')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* For operators of <eqOps>, this is replaced by comparing all leaf elements with null, combined with and.
|
|
10
|
+
* If there are at least two leaf elements and if there are tokens before or after the recognized pattern, we enclose the resulting condition in parens (...)
|
|
11
|
+
*/
|
|
12
|
+
const eqOps = [['is'], ['='] /* ['=='] */]
|
|
13
|
+
/**
|
|
14
|
+
* For operators of <notEqOps>, do the same but use or instead of and.
|
|
15
|
+
* This ensures that not struc == <value> is the same as struc != <value>.
|
|
16
|
+
*/
|
|
17
|
+
const notEqOps = [['is', 'not'], ['<>'], ['!=']]
|
|
18
|
+
/**
|
|
19
|
+
* not supported in comparison w/ struct because of unclear semantics
|
|
20
|
+
*/
|
|
21
|
+
const notSupportedOps = [['>'], ['<'], ['>='], ['<=']]
|
|
22
|
+
|
|
23
|
+
const allOps = eqOps.concat(eqOps).concat(notEqOps).concat(notSupportedOps)
|
|
24
|
+
|
|
25
|
+
const { pseudos } = require('./infer/pseudos')
|
|
26
|
+
/**
|
|
27
|
+
* Transforms a CDL style query into SQL-Like CQN:
|
|
28
|
+
* - transform association paths in `from` to `WHERE exists` subqueries
|
|
29
|
+
* - transforms columns into their flat representation.
|
|
30
|
+
* 1. Flatten managed associations to their foreign
|
|
31
|
+
* 2. Flatten structures to their leafs
|
|
32
|
+
* 3. Replace join-relevant ref paths (i.e. non-fk accesses in association paths) with the correct join alias
|
|
33
|
+
* - transforms `expand` columns into special, normalized subqueries
|
|
34
|
+
* - transform `where` clause.
|
|
35
|
+
* That is the flattening of all `ref`s and the expansion of `where exists` predicates
|
|
36
|
+
* - rewrites `from` clause:
|
|
37
|
+
* Each join relevant association path traversal is translated to a join condition.
|
|
38
|
+
*
|
|
39
|
+
* `cqn4sql` is applied recursively to all queries found in `from`, `columns` and `where`
|
|
40
|
+
* of a query.
|
|
41
|
+
*
|
|
42
|
+
* @param {object} originalQuery
|
|
43
|
+
* @param {object} model
|
|
44
|
+
* @returns {object} transformedQuery the transformed query
|
|
45
|
+
*/
|
|
46
|
+
function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
47
|
+
const inferred = infer(originalQuery, model)
|
|
48
|
+
if (originalQuery.SELECT?.from.args && !originalQuery.joinTree) return inferred
|
|
49
|
+
|
|
50
|
+
let transformedQuery = cds.ql.clone(inferred)
|
|
51
|
+
const kind = inferred.cmd || Object.keys(inferred)[0]
|
|
52
|
+
|
|
53
|
+
if (inferred.INSERT || inferred.UPSERT) {
|
|
54
|
+
transformedQuery = transformQueryForInsertUpsert(kind)
|
|
55
|
+
} else {
|
|
56
|
+
const queryProp = inferred[kind]
|
|
57
|
+
if (!inferred.STREAM?.from && inferred.STREAM?.into) {
|
|
58
|
+
transformedQuery = transformStreamQuery()
|
|
59
|
+
} else {
|
|
60
|
+
const { entity, where } = queryProp
|
|
61
|
+
const from = queryProp.from
|
|
62
|
+
|
|
63
|
+
const transformedProp = { __proto__: queryProp } // IMPORTANT: don't lose anything you might not know of
|
|
64
|
+
|
|
65
|
+
// Transform the existing where, prepend table aliases, and so on...
|
|
66
|
+
if (where) {
|
|
67
|
+
transformedProp.where = getTransformedTokenStream(where)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Transform the from clause: association path steps turn into `WHERE EXISTS` subqueries.
|
|
71
|
+
// The already transformed `where` clause is then glued together with the resulting subqueries.
|
|
72
|
+
const { transformedWhere, transformedFrom } = getTransformedFrom(from || entity, transformedProp.where)
|
|
73
|
+
|
|
74
|
+
if (inferred.SELECT) {
|
|
75
|
+
transformedQuery = transformSelectQuery(queryProp, transformedFrom, transformedWhere, transformedQuery)
|
|
76
|
+
} else {
|
|
77
|
+
if (from) {
|
|
78
|
+
transformedProp.from = transformedFrom
|
|
79
|
+
} else {
|
|
80
|
+
transformedProp.entity = transformedFrom
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (transformedWhere?.length > 0) {
|
|
84
|
+
transformedProp.where = transformedWhere
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
transformedQuery[kind] = transformedProp
|
|
88
|
+
|
|
89
|
+
if (inferred.UPDATE?.with) {
|
|
90
|
+
Object.entries(inferred.UPDATE.with).forEach(([key, val]) => {
|
|
91
|
+
const transformed = getTransformedTokenStream([val])
|
|
92
|
+
inferred.UPDATE.with[key] = transformed[0]
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (inferred.joinTree && !inferred.joinTree.isInitial) {
|
|
98
|
+
transformedQuery[kind].from = translateAssocsToJoins(transformedQuery[kind].from)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return transformedQuery
|
|
104
|
+
|
|
105
|
+
function transformSelectQuery(queryProp, transformedFrom, transformedWhere, transformedQuery) {
|
|
106
|
+
const { columns, having, groupBy, orderBy, limit } = queryProp
|
|
107
|
+
|
|
108
|
+
// Trivial replacement -> no transformations needed
|
|
109
|
+
if (limit) {
|
|
110
|
+
transformedQuery.SELECT.limit = limit
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
transformedQuery.SELECT.from = transformedFrom
|
|
114
|
+
|
|
115
|
+
if (transformedWhere?.length > 0) {
|
|
116
|
+
transformedQuery.SELECT.where = transformedWhere
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (columns) {
|
|
120
|
+
transformedQuery.SELECT.columns = getTransformedColumns(columns)
|
|
121
|
+
} else {
|
|
122
|
+
transformedQuery.SELECT.columns = getColumnsForWildcard()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Like the WHERE clause, aliases from the SELECT list are not accessible for `group by`/`having` (in most DB's)
|
|
126
|
+
if (having) {
|
|
127
|
+
transformedQuery.SELECT.having = getTransformedTokenStream(having)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (groupBy) {
|
|
131
|
+
const transformedGroupBy = getTransformedOrderByGroupBy(groupBy)
|
|
132
|
+
if (transformedGroupBy.length) {
|
|
133
|
+
transformedQuery.SELECT.groupBy = transformedGroupBy
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Since all the expressions in the SELECT part of the query have been computed,
|
|
138
|
+
// one can reference aliases of the queries columns in the orderBy clause.
|
|
139
|
+
if (orderBy) {
|
|
140
|
+
const transformedOrderBy = getTransformedOrderByGroupBy(orderBy, true)
|
|
141
|
+
if (transformedOrderBy.length) {
|
|
142
|
+
transformedQuery.SELECT.orderBy = transformedOrderBy
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (inferred.SELECT.search) {
|
|
147
|
+
// Search target can be a navigation, in that case use _target to get the correct entity
|
|
148
|
+
const where = transformSearchToWhere(inferred.SELECT.search, transformedFrom)
|
|
149
|
+
if (where) {
|
|
150
|
+
transformedQuery.SELECT.where = where
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return transformedQuery
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Transforms a query object for INSERT or UPSERT operations by modifying the `into` clause.
|
|
158
|
+
*
|
|
159
|
+
* @param {string} kind - The type of operation: "INSERT" or "UPSERT".
|
|
160
|
+
*
|
|
161
|
+
* @returns {Object} - The transformed query with updated `into` clause.
|
|
162
|
+
*/
|
|
163
|
+
function transformQueryForInsertUpsert(kind) {
|
|
164
|
+
const { as } = transformedQuery[kind].into
|
|
165
|
+
transformedQuery[kind].into = { ref: [inferred.target.name] }
|
|
166
|
+
if (as) transformedQuery[kind].into.as = as
|
|
167
|
+
return transformedQuery
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Transforms a stream query, replacing the `where` and `into` clauses after processing.
|
|
172
|
+
*
|
|
173
|
+
* @param {Object} inferred - The inferred object containing the STREAM query.
|
|
174
|
+
* @param {Object} transformedQuery - The query object to be transformed.
|
|
175
|
+
*
|
|
176
|
+
* @returns {Object} - The transformed query with updated STREAM clauses.
|
|
177
|
+
*/
|
|
178
|
+
function transformStreamQuery() {
|
|
179
|
+
const { into, where } = inferred.STREAM
|
|
180
|
+
const transformedProp = { __proto__: inferred.STREAM }
|
|
181
|
+
if (where) {
|
|
182
|
+
transformedProp.where = getTransformedTokenStream(where)
|
|
183
|
+
}
|
|
184
|
+
const { transformedWhere, transformedFrom } = getTransformedFrom(into, transformedProp.where)
|
|
185
|
+
if (transformedWhere?.length > 0) {
|
|
186
|
+
transformedProp.where = transformedWhere
|
|
187
|
+
}
|
|
188
|
+
transformedProp.into = transformedFrom
|
|
189
|
+
transformedQuery.STREAM = transformedProp
|
|
190
|
+
return transformedQuery
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Transforms a search expression to a WHERE clause for a SELECT operation.
|
|
195
|
+
*
|
|
196
|
+
* @param {Object} search - The search expression which shall be applied to the searchable columns on the query source.
|
|
197
|
+
* @param {Object} from - The FROM clause of the CQN statement.
|
|
198
|
+
*
|
|
199
|
+
* @returns {(Object|Array|undefined)} - If the target of the query contains searchable elements, the function returns an array that represents the WHERE clause.
|
|
200
|
+
* 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.
|
|
201
|
+
* If the SELECT query does not contain a WHERE clause, the returned array solely consists of the 'contains' clause.
|
|
202
|
+
* If the target entity of the query does not contain searchable elements, the function returns null.
|
|
203
|
+
*
|
|
204
|
+
*/
|
|
205
|
+
function transformSearchToWhere(search, from) {
|
|
206
|
+
const entity = from.$refLinks[0].definition._target || from.$refLinks[0].definition
|
|
207
|
+
const searchIn = computeColumnsToBeSearched(inferred, entity, from.as)
|
|
208
|
+
if (searchIn.length > 0) {
|
|
209
|
+
const xpr = search
|
|
210
|
+
const contains = {
|
|
211
|
+
func: 'search',
|
|
212
|
+
args: [
|
|
213
|
+
searchIn.length > 1 ? { list: searchIn } : { ...searchIn[0] },
|
|
214
|
+
xpr.length === 1 && 'val' in xpr[0] ? xpr[0] : { xpr },
|
|
215
|
+
],
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (transformedQuery.SELECT.where) {
|
|
219
|
+
return [asXpr(transformedQuery.SELECT.where), 'and', contains]
|
|
220
|
+
} else {
|
|
221
|
+
return [contains]
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
return null
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Rewrites the from clause based on the `query.joinTree`.
|
|
230
|
+
*
|
|
231
|
+
* For each join relevant node in the join tree, the respective join is generated.
|
|
232
|
+
* Each join relevant node in the join tree has an unique table alias which is the query source for the respective
|
|
233
|
+
* path traversals. Hence, all join relevant `ref`s must be rewritten to point to the generated join aliases. However,
|
|
234
|
+
* this is done in the @function getFlatColumnsFor().
|
|
235
|
+
*
|
|
236
|
+
* @returns {CQN.from}
|
|
237
|
+
*/
|
|
238
|
+
function translateAssocsToJoins() {
|
|
239
|
+
let from
|
|
240
|
+
/**
|
|
241
|
+
* remember already seen aliases, do not create a join for them again
|
|
242
|
+
*/
|
|
243
|
+
const alreadySeen = new Map()
|
|
244
|
+
inferred.joinTree._roots.forEach(r => {
|
|
245
|
+
const args = r.queryArtifact.SELECT
|
|
246
|
+
? [{ SELECT: transformSubquery(r.queryArtifact).SELECT, as: r.alias }]
|
|
247
|
+
: [{ ref: [localized(r.queryArtifact)], as: r.alias }]
|
|
248
|
+
from = { join: 'left', args, on: [] }
|
|
249
|
+
r.children.forEach(c => {
|
|
250
|
+
from = joinForBranch(from, c)
|
|
251
|
+
from = { join: 'left', args: [from], on: [] }
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
return from.args.length > 1 ? from : from.args[0]
|
|
255
|
+
|
|
256
|
+
function joinForBranch(lhs, node) {
|
|
257
|
+
const nextAssoc = inferred.joinTree.findNextAssoc(node)
|
|
258
|
+
if (!nextAssoc || alreadySeen.has(nextAssoc.$refLink.alias)) return lhs.args.length > 1 ? lhs : lhs.args[0]
|
|
259
|
+
|
|
260
|
+
lhs.on.push(
|
|
261
|
+
...onCondFor(
|
|
262
|
+
nextAssoc.$refLink,
|
|
263
|
+
node.parent.$refLink || /** tree roots do not have $refLink */ {
|
|
264
|
+
alias: node.parent.alias,
|
|
265
|
+
definition: node.parent.queryArtifact,
|
|
266
|
+
target: node.parent.queryArtifact,
|
|
267
|
+
},
|
|
268
|
+
/** flip source and target in on condition */ true,
|
|
269
|
+
),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
const arg = {
|
|
273
|
+
ref: [localized(model.definitions[nextAssoc.$refLink.definition.target])],
|
|
274
|
+
as: nextAssoc.$refLink.alias,
|
|
275
|
+
}
|
|
276
|
+
lhs.args.push(arg)
|
|
277
|
+
alreadySeen.set(nextAssoc.$refLink.alias, true)
|
|
278
|
+
if (nextAssoc.where) {
|
|
279
|
+
const filter = getTransformedTokenStream(nextAssoc.where, nextAssoc.$refLink)
|
|
280
|
+
lhs.on = [
|
|
281
|
+
...(hasLogicalOr(lhs.on) ? [asXpr(lhs.on)] : lhs.on),
|
|
282
|
+
'and',
|
|
283
|
+
...(hasLogicalOr(filter) ? [asXpr(filter)] : filter),
|
|
284
|
+
]
|
|
285
|
+
}
|
|
286
|
+
if (node.children) {
|
|
287
|
+
node.children.forEach(c => {
|
|
288
|
+
lhs = { join: 'left', args: [lhs], on: [] }
|
|
289
|
+
lhs = joinForBranch(lhs, c)
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
return lhs.args.length > 1 ? lhs : lhs.args[0]
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Walks over a list of columns (ref's, xpr, subqueries, val), applies flattening on structured types and expands wildcards.
|
|
298
|
+
*
|
|
299
|
+
* @param {object[]} columns
|
|
300
|
+
* @returns {object[]} the transformed representation of the input. Expanded and flattened.
|
|
301
|
+
*/
|
|
302
|
+
function getTransformedColumns(columns) {
|
|
303
|
+
const transformedColumns = []
|
|
304
|
+
|
|
305
|
+
for (let i = 0; i < columns.length; i++) {
|
|
306
|
+
const col = columns[i]
|
|
307
|
+
|
|
308
|
+
if (col.expand) {
|
|
309
|
+
handleExpand(col)
|
|
310
|
+
} else if (col.inline) {
|
|
311
|
+
handleInline(col)
|
|
312
|
+
} else if (col.ref) {
|
|
313
|
+
handleRef(col)
|
|
314
|
+
} else if (col === '*') {
|
|
315
|
+
handleWildcard(columns)
|
|
316
|
+
} else {
|
|
317
|
+
handleDefault(col)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (transformedColumns.length === 0 && columns.length) {
|
|
322
|
+
handleEmptyColumns(columns)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return transformedColumns
|
|
326
|
+
|
|
327
|
+
function handleExpand(col) {
|
|
328
|
+
const { $refLinks } = col
|
|
329
|
+
const last = $refLinks?.[$refLinks.length - 1]
|
|
330
|
+
if (last && !last.skipExpand && last.definition.isAssociation) {
|
|
331
|
+
const expandedSubqueryColumn = expandColumn(col)
|
|
332
|
+
transformedColumns.push(expandedSubqueryColumn)
|
|
333
|
+
} else if (!last?.skipExpand) {
|
|
334
|
+
const expandCols = nestedProjectionOnStructure(col, 'expand')
|
|
335
|
+
transformedColumns.push(...expandCols)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function handleInline(col) {
|
|
340
|
+
const inlineCols = nestedProjectionOnStructure(col)
|
|
341
|
+
transformedColumns.push(...inlineCols)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function handleRef(col) {
|
|
345
|
+
if (pseudos.elements[col.ref[0]] || col.param) {
|
|
346
|
+
transformedColumns.push({ ...col })
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const tableAlias = getQuerySourceName(col)
|
|
351
|
+
// re-adjust usage of implicit alias in subquery
|
|
352
|
+
if (col.$refLinks[0].definition.kind === 'entity' && col.ref[0] !== tableAlias) {
|
|
353
|
+
col.ref[0] = tableAlias
|
|
354
|
+
}
|
|
355
|
+
const leaf = col.$refLinks[col.$refLinks.length - 1].definition
|
|
356
|
+
if (leaf.virtual === true) return
|
|
357
|
+
|
|
358
|
+
let baseName
|
|
359
|
+
if (col.ref.length >= 2) {
|
|
360
|
+
baseName = col.ref.slice(col.ref[0] === tableAlias ? 1 : 0, col.ref.length - 1).join('_')
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let columnAlias = col.as || (col.isJoinRelevant ? col.flatName : null)
|
|
364
|
+
const refNavigation = col.ref.slice(col.ref[0] === tableAlias ? 1 : 0).join('_')
|
|
365
|
+
if (!columnAlias && col.flatName && col.flatName !== refNavigation) columnAlias = refNavigation
|
|
366
|
+
|
|
367
|
+
if (col.$refLinks.some(link => link.definition._target?.['@cds.persistence.skip'] === true)) return
|
|
368
|
+
|
|
369
|
+
const flatColumns = getFlatColumnsFor(col, { baseName, columnAlias, tableAlias })
|
|
370
|
+
flatColumns.forEach(flatColumn => {
|
|
371
|
+
const { as } = flatColumn
|
|
372
|
+
if (!(as && transformedColumns.some(inserted => inserted?.as === as))) transformedColumns.push(flatColumn)
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function handleWildcard(columns) {
|
|
377
|
+
const wildcardIndex = columns.indexOf('*')
|
|
378
|
+
const ignoreInWildcardExpansion = columns.slice(0, wildcardIndex)
|
|
379
|
+
const { excluding } = inferred.SELECT
|
|
380
|
+
if (excluding) ignoreInWildcardExpansion.push(...excluding)
|
|
381
|
+
|
|
382
|
+
const wildcardColumns = getColumnsForWildcard(ignoreInWildcardExpansion, columns.slice(wildcardIndex + 1))
|
|
383
|
+
transformedColumns.push(...wildcardColumns)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function handleDefault(col) {
|
|
387
|
+
let transformedColumn = getTransformedColumn(col)
|
|
388
|
+
if (col.as) transformedColumn.as = col.as
|
|
389
|
+
|
|
390
|
+
const replaceWith = transformedColumns.findIndex(t => (t.as || t.ref[t.ref.length - 1]) === transformedColumn.as)
|
|
391
|
+
if (replaceWith === -1) transformedColumns.push(transformedColumn)
|
|
392
|
+
else transformedColumns.splice(replaceWith, 1, transformedColumn)
|
|
393
|
+
|
|
394
|
+
Object.defineProperty(transformedColumn, 'element', { value: originalQuery.elements[col.as] })
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function getTransformedColumn(col) {
|
|
398
|
+
if (col.SELECT) {
|
|
399
|
+
if (isLocalized(inferred.target)) col.SELECT.localized = true
|
|
400
|
+
if (!col.SELECT.from.as) {
|
|
401
|
+
const uniqueSubqueryAlias = inferred.joinTree.addNextAvailableTableAlias(
|
|
402
|
+
getLastStringSegment(col.SELECT.from.ref[col.SELECT.from.ref.length - 1]),
|
|
403
|
+
originalQuery.outerQueries,
|
|
404
|
+
)
|
|
405
|
+
Object.defineProperty(col.SELECT.from, 'uniqueSubqueryAlias', { value: uniqueSubqueryAlias })
|
|
406
|
+
}
|
|
407
|
+
return transformSubquery(col)
|
|
408
|
+
} else if (col.xpr) {
|
|
409
|
+
return { xpr: getTransformedTokenStream(col.xpr) }
|
|
410
|
+
} else if (col.func) {
|
|
411
|
+
return {
|
|
412
|
+
func: col.func,
|
|
413
|
+
args: col.args && getTransformedTokenStream(col.args),
|
|
414
|
+
as: col.func,
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
return copy(col)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function handleEmptyColumns(columns) {
|
|
422
|
+
if (columns.some(c => c.$refLinks?.[c.$refLinks.length - 1].definition.type === 'cds.Composition')) return
|
|
423
|
+
throw new cds.error('Queries must have at least one non-virtual column')
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Calculates the columns for a nested projection on a structure.
|
|
429
|
+
*
|
|
430
|
+
* @param {object} col
|
|
431
|
+
* @param {'inline'|'expand'} prop the property on which to operate. Default is `inline`.
|
|
432
|
+
* @returns a list of flat columns.
|
|
433
|
+
*/
|
|
434
|
+
function nestedProjectionOnStructure(col, prop = 'inline') {
|
|
435
|
+
const res = []
|
|
436
|
+
|
|
437
|
+
col[prop].forEach((nestedProjection, i) => {
|
|
438
|
+
let rewrittenColumns = []
|
|
439
|
+
if (nestedProjection === '*') {
|
|
440
|
+
res.push(...expandNestedProjectionWildcard(col, i, prop))
|
|
441
|
+
} else {
|
|
442
|
+
const nameParts = col.as ? [col.as] : [col.ref.map(idOnly).join('_')]
|
|
443
|
+
nameParts.push(nestedProjection.as ? nestedProjection.as : nestedProjection.ref.map(idOnly).join('_'))
|
|
444
|
+
const name = nameParts.join('_')
|
|
445
|
+
if (nestedProjection.ref) {
|
|
446
|
+
const augmentedInlineCol = { ...nestedProjection }
|
|
447
|
+
augmentedInlineCol.ref = col.ref ? [...col.ref, ...nestedProjection.ref] : nestedProjection.ref
|
|
448
|
+
if (col.as || nestedProjection.as || nestedProjection.isJoinRelevant) {
|
|
449
|
+
augmentedInlineCol.as = nameParts.join('_')
|
|
450
|
+
}
|
|
451
|
+
Object.defineProperties(augmentedInlineCol, {
|
|
452
|
+
$refLinks: { value: [...nestedProjection.$refLinks], writable: true },
|
|
453
|
+
isJoinRelevant: {
|
|
454
|
+
value: nestedProjection.isJoinRelevant,
|
|
455
|
+
writable: true,
|
|
456
|
+
},
|
|
457
|
+
})
|
|
458
|
+
// if the expand is not anonymous, we must prepend the expand columns path
|
|
459
|
+
// to make sure the full path is resolvable
|
|
460
|
+
if (col.ref) {
|
|
461
|
+
augmentedInlineCol.$refLinks.unshift(...col.$refLinks)
|
|
462
|
+
augmentedInlineCol.isJoinRelevant = augmentedInlineCol.isJoinRelevant || col.isJoinRelevant
|
|
463
|
+
}
|
|
464
|
+
const flatColumns = getTransformedColumns([augmentedInlineCol])
|
|
465
|
+
flatColumns.forEach(flatColumn => {
|
|
466
|
+
const flatColumnName = flatColumn.as || flatColumn.ref[flatColumn.ref.length - 1]
|
|
467
|
+
if (!res.some(c => (c.as || c.ref.slice(1).map(idOnly).join('_')) === flatColumnName)) {
|
|
468
|
+
const rewrittenColumn = { ...flatColumn }
|
|
469
|
+
if (nestedProjection.as) rewrittenColumn.as = flatColumnName
|
|
470
|
+
rewrittenColumns.push(rewrittenColumn)
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
} else {
|
|
474
|
+
// func, xpr, val..
|
|
475
|
+
// we need to check if the column was already added
|
|
476
|
+
// in the wildcard expansion
|
|
477
|
+
if (!res.some(c => (c.as || c.ref.slice(1).map(idOnly).join('_')) === name)) {
|
|
478
|
+
const rewrittenColumn = { ...nestedProjection }
|
|
479
|
+
rewrittenColumn.as = name
|
|
480
|
+
rewrittenColumns.push(rewrittenColumn)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
res.push(...rewrittenColumns)
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
return res
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Expand the wildcard of the given column into all leaf elements.
|
|
492
|
+
* Respect smart wildcard rules and excluding clause.
|
|
493
|
+
*
|
|
494
|
+
* Every column before the wildcardIndex is excluded from the wildcard expansion.
|
|
495
|
+
* Columns after the wildcardIndex overwrite columns within the wildcard expansion in place.
|
|
496
|
+
*
|
|
497
|
+
* @TODO use this also for `expand` wildcards on structures.
|
|
498
|
+
*
|
|
499
|
+
* @param {csn.Column} col
|
|
500
|
+
* @param {integer} wildcardIndex
|
|
501
|
+
* @returns an array of columns which represents the expanded wildcard
|
|
502
|
+
*/
|
|
503
|
+
function expandNestedProjectionWildcard(col, wildcardIndex, prop = 'inline') {
|
|
504
|
+
const res = []
|
|
505
|
+
// everything before the wildcard is inserted before the wildcard
|
|
506
|
+
// and ignored from the wildcard expansion
|
|
507
|
+
const exclude = col[prop].slice(0, wildcardIndex)
|
|
508
|
+
// everything after the wildcard, is a potential replacement
|
|
509
|
+
// in the wildcard expansion
|
|
510
|
+
const replace = []
|
|
511
|
+
// we need to absolutefy the refs
|
|
512
|
+
col[prop].slice(wildcardIndex + 1).forEach(c => {
|
|
513
|
+
const fakeColumn = { ...c }
|
|
514
|
+
if (fakeColumn.ref) {
|
|
515
|
+
fakeColumn.ref = [...col.ref, ...fakeColumn.ref]
|
|
516
|
+
fakeColumn.$refLinks = [...col.$refLinks, ...c.$refLinks]
|
|
517
|
+
}
|
|
518
|
+
replace.push(fakeColumn)
|
|
519
|
+
})
|
|
520
|
+
// respect excluding clause
|
|
521
|
+
if (col.excluding) {
|
|
522
|
+
// fake the ref since excluding only has strings
|
|
523
|
+
col.excluding.forEach(c => {
|
|
524
|
+
const fakeColumn = {
|
|
525
|
+
ref: [...col.ref, c],
|
|
526
|
+
}
|
|
527
|
+
exclude.push(fakeColumn)
|
|
528
|
+
})
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (col.$refLinks[col.$refLinks.length - 1].definition.kind === 'entity')
|
|
532
|
+
res.push(...getColumnsForWildcard(exclude, replace))
|
|
533
|
+
else
|
|
534
|
+
res.push(
|
|
535
|
+
...getFlatColumnsFor(col, { columnAlias: col.as, tableAlias: getQuerySourceName(col) }, [], {
|
|
536
|
+
exclude,
|
|
537
|
+
replace,
|
|
538
|
+
}),
|
|
539
|
+
)
|
|
540
|
+
return res
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* This function converts a column with an `expand` property into a subquery.
|
|
545
|
+
*
|
|
546
|
+
* It operates by using the following steps:
|
|
547
|
+
*
|
|
548
|
+
* 1. It creates an intermediate SQL query, selecting `from <effective query source>:...<column>.ref { ...<column>.expand }`.
|
|
549
|
+
* For example, from the query `SELECT from Authors { books { title } }`, it generates:
|
|
550
|
+
* - `SELECT from Authors:books as books {title}`
|
|
551
|
+
*
|
|
552
|
+
* 2. It then adds the properties `expand: true` and `one: <expand assoc>.is2one` to the intermediate SQL query.
|
|
553
|
+
*
|
|
554
|
+
* 3. It applies `cqn4sql` to the intermediate query (ensuring the aliases of the outer query are maintained).
|
|
555
|
+
* For example, `cqn4sql(…)` is used to create the following query:
|
|
556
|
+
* - `SELECT from Books as books {books.title} where exists ( SELECT 1 from Authors as Authors where Authors.ID = books.author_ID )`
|
|
557
|
+
*
|
|
558
|
+
* 4. It then replaces the `exists <subquery>` with the where condition of the `<subquery>` and correlates it with the effective query source.
|
|
559
|
+
* For example, this query is created:
|
|
560
|
+
* - `SELECT from Books as books { books.title } where Authors.ID = books.author_ID`
|
|
561
|
+
*
|
|
562
|
+
* 5. Lastly, it replaces the `expand` column of the original query with the transformed subquery.
|
|
563
|
+
* For example, the query becomes:
|
|
564
|
+
* - `SELECT from Authors { (SELECT from Books as books { books.title } where Authors.ID = books.author_ID) as books }`
|
|
565
|
+
*
|
|
566
|
+
* @param {CSN.column} column - The column with the 'expand' property to be transformed into a subquery.
|
|
567
|
+
*
|
|
568
|
+
* @returns {Object} Returns a subquery correlated with the enclosing query, with added properties `expand:true` and `one:true|false`.
|
|
569
|
+
*/
|
|
570
|
+
function expandColumn(column) {
|
|
571
|
+
let outerAlias
|
|
572
|
+
let subqueryFromRef
|
|
573
|
+
if (column.isJoinRelevant) {
|
|
574
|
+
// all n-1 steps of the expand column are already transformed into joins
|
|
575
|
+
// find the last join relevant association. That is the n-1 assoc in the ref path.
|
|
576
|
+
// slice the ref array beginning from the n-1 assoc in the ref and take that as the postfix for the subqueries from ref.
|
|
577
|
+
;[...column.$refLinks]
|
|
578
|
+
.reverse()
|
|
579
|
+
.slice(1)
|
|
580
|
+
.find((link, i) => {
|
|
581
|
+
if (link.definition.isAssociation) {
|
|
582
|
+
subqueryFromRef = [link.definition.target, ...column.ref.slice(-(i + 1), column.ref.length)]
|
|
583
|
+
// alias of last join relevant association is also the correlation alias for the subquery
|
|
584
|
+
outerAlias = link.alias
|
|
585
|
+
return true
|
|
586
|
+
}
|
|
587
|
+
})
|
|
588
|
+
} else {
|
|
589
|
+
outerAlias = transformedQuery.SELECT.from.as
|
|
590
|
+
subqueryFromRef = [
|
|
591
|
+
...transformedQuery.SELECT.from.ref,
|
|
592
|
+
...(column.$refLinks[0].definition.kind === 'entity' ? column.ref.slice(1) : column.ref),
|
|
593
|
+
]
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// this is the alias of the column which holds the correlated subquery
|
|
597
|
+
const columnAlias =
|
|
598
|
+
column.as ||
|
|
599
|
+
(column.$refLinks[0].definition.kind === 'entity'
|
|
600
|
+
? column.ref.slice(1).map(idOnly).join('_') // omit explicit table alias from name of column
|
|
601
|
+
: column.ref.map(idOnly).join('_'))
|
|
602
|
+
|
|
603
|
+
// we need to respect the aliases of the outer query, so the columnAlias might not be suitable
|
|
604
|
+
// as table alias for the correlated subquery
|
|
605
|
+
const uniqueSubqueryAlias = getNextAvailableTableAlias(columnAlias, originalQuery.outerQueries)
|
|
606
|
+
|
|
607
|
+
// `SELECT from Authors { books.genre as genreOfBooks { name } } becomes `SELECT from Books:genre as genreOfBooks`
|
|
608
|
+
const from = { ref: subqueryFromRef, as: uniqueSubqueryAlias }
|
|
609
|
+
const subqueryBase = Object.fromEntries(
|
|
610
|
+
// preserve all props on subquery (`limit`, `order by`, …) but `expand` and `ref`
|
|
611
|
+
Object.entries(column).filter(([key]) => !(key in { ref: true, expand: true })),
|
|
612
|
+
)
|
|
613
|
+
const subquery = {
|
|
614
|
+
SELECT: {
|
|
615
|
+
...subqueryBase,
|
|
616
|
+
from,
|
|
617
|
+
columns: JSON.parse(JSON.stringify(column.expand)),
|
|
618
|
+
expand: true,
|
|
619
|
+
one: column.$refLinks[column.$refLinks.length - 1].definition.is2one,
|
|
620
|
+
},
|
|
621
|
+
}
|
|
622
|
+
if (isLocalized(inferred.target)) subquery.SELECT.localized = true
|
|
623
|
+
const expanded = transformSubquery(subquery)
|
|
624
|
+
const correlated = _correlate({ ...expanded, as: columnAlias }, outerAlias)
|
|
625
|
+
Object.defineProperty(correlated, 'elements', { value: subquery.elements })
|
|
626
|
+
return correlated
|
|
627
|
+
|
|
628
|
+
function _correlate(subq, outer) {
|
|
629
|
+
const subqueryFollowingExists = (a, indexOfExists) => a[indexOfExists + 1]
|
|
630
|
+
let {
|
|
631
|
+
SELECT: { where },
|
|
632
|
+
} = subq
|
|
633
|
+
let recent = where
|
|
634
|
+
let i = where.indexOf('exists')
|
|
635
|
+
while (i !== -1) {
|
|
636
|
+
where = subqueryFollowingExists((recent = where), i).SELECT.where
|
|
637
|
+
i = where.indexOf('exists')
|
|
638
|
+
}
|
|
639
|
+
const existsIndex = recent.indexOf('exists')
|
|
640
|
+
recent.splice(
|
|
641
|
+
existsIndex,
|
|
642
|
+
2,
|
|
643
|
+
...where.map(x => {
|
|
644
|
+
return replaceAliasWithSubqueryAlias(x)
|
|
645
|
+
}),
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
function replaceAliasWithSubqueryAlias(x) {
|
|
649
|
+
const existsSubqueryAlias = recent[existsIndex + 1].SELECT.from.as
|
|
650
|
+
if (existsSubqueryAlias === x.ref?.[0]) return { ref: [outer, ...x.ref.slice(1)] }
|
|
651
|
+
if (x.xpr) x.xpr = x.xpr.map(replaceAliasWithSubqueryAlias)
|
|
652
|
+
return x
|
|
653
|
+
}
|
|
654
|
+
return subq
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function getTransformedOrderByGroupBy(columns, inOrderBy = false) {
|
|
659
|
+
const res = []
|
|
660
|
+
for (let i = 0; i < columns.length; i++) {
|
|
661
|
+
const col = columns[i]
|
|
662
|
+
if (col.isJoinRelevant) {
|
|
663
|
+
const tableAlias$refLink = getQuerySourceName(col)
|
|
664
|
+
const transformedColumn = {
|
|
665
|
+
ref: [tableAlias$refLink, getFullName(col.$refLinks[col.$refLinks.length - 1].definition)],
|
|
666
|
+
}
|
|
667
|
+
if (col.sort) transformedColumn.sort = col.sort
|
|
668
|
+
if (col.nulls) transformedColumn.nulls = col.nulls
|
|
669
|
+
res.push(transformedColumn)
|
|
670
|
+
} else if (pseudos.elements[col.ref?.[0]]) {
|
|
671
|
+
res.push({ ...col })
|
|
672
|
+
} else if (col.ref) {
|
|
673
|
+
if (col.$refLinks.some(link => link.definition._target?.['@cds.persistence.skip'] === true)) continue
|
|
674
|
+
const { target } = col.$refLinks[0]
|
|
675
|
+
const tableAlias = target.SELECT ? null : getQuerySourceName(col) // do not prepend TA if orderBy column addresses element of query
|
|
676
|
+
const leaf = col.$refLinks[col.$refLinks.length - 1].definition
|
|
677
|
+
if (leaf.virtual === true) continue // already in getFlatColumnForElement
|
|
678
|
+
let baseName
|
|
679
|
+
if (col.ref.length >= 2) {
|
|
680
|
+
// leaf might be intermediate structure
|
|
681
|
+
baseName = col.ref.slice(col.ref[0] === tableAlias ? 1 : 0, col.ref.length - 1).join('_')
|
|
682
|
+
}
|
|
683
|
+
const flatColumns = getFlatColumnsFor(col, { baseName, tableAlias })
|
|
684
|
+
/**
|
|
685
|
+
* We can't guarantee that the element order will NOT change in the future.
|
|
686
|
+
* We claim that the element order doesn't matter, hence we can't allow elements
|
|
687
|
+
* in the order by clause which expand to more than one column, as the order impacts
|
|
688
|
+
* the result.
|
|
689
|
+
*/
|
|
690
|
+
if (inOrderBy && flatColumns.length > 1)
|
|
691
|
+
cds.error(`"${getFullName(leaf)}" can't be used in order by as it expands to multiple fields`)
|
|
692
|
+
if (col.nulls) flatColumns[0].nulls = col.nulls
|
|
693
|
+
if (col.sort) flatColumns[0].sort = col.sort
|
|
694
|
+
res.push(...flatColumns)
|
|
695
|
+
} else {
|
|
696
|
+
let transformedColumn
|
|
697
|
+
if (col.SELECT) transformedColumn = transformSubquery(col)
|
|
698
|
+
else if (col.xpr) transformedColumn = { xpr: getTransformedTokenStream(col.xpr) }
|
|
699
|
+
else if (col.func) transformedColumn = { args: getTransformedTokenStream(col.args), func: col.func }
|
|
700
|
+
// val
|
|
701
|
+
else transformedColumn = copy(col)
|
|
702
|
+
if (col.sort) transformedColumn.sort = col.sort
|
|
703
|
+
if (col.nulls) transformedColumn.nulls = col.nulls
|
|
704
|
+
res.push(transformedColumn)
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return res
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Transforms a subquery.
|
|
712
|
+
*
|
|
713
|
+
* If the current query contains outer queries (is itself a subquery),
|
|
714
|
+
* it appends the current inferred query.
|
|
715
|
+
* Otherwise, it initializes the `outerQueries` array and adds the inferred query.
|
|
716
|
+
* The `outerQueries` property makes sure
|
|
717
|
+
* that the table aliases of the outer queries are accessible within the scope of the subquery.
|
|
718
|
+
* Lastly, it recursively calls cqn4sql on the subquery.
|
|
719
|
+
*
|
|
720
|
+
* @param {object} q - The query to be transformed. This should be a subquery object.
|
|
721
|
+
* @returns {object} - The cqn4sql transformed subquery.
|
|
722
|
+
*/
|
|
723
|
+
function transformSubquery(q) {
|
|
724
|
+
if (q.outerQueries) q.outerQueries.push(inferred)
|
|
725
|
+
else {
|
|
726
|
+
const outerQueries = inferred.outerQueries || []
|
|
727
|
+
outerQueries.push(inferred)
|
|
728
|
+
Object.defineProperty(q, 'outerQueries', { value: outerQueries })
|
|
729
|
+
}
|
|
730
|
+
return cqn4sql(q, model)
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* This function converts a wildcard into explicit columns.
|
|
735
|
+
*
|
|
736
|
+
* Based on the query's `$combinedElements` attribute, the function computes the flat column representations
|
|
737
|
+
* and returns them. Additionally, it prepends the respective table alias to each column. Columns specified
|
|
738
|
+
* in the `excluding` clause are ignored during this transformation.
|
|
739
|
+
*
|
|
740
|
+
* Furthermore, foreign keys (FK) for OData CSN and blobs are excluded from the wildcard expansion.
|
|
741
|
+
*
|
|
742
|
+
* @param {Array} exclude - An optional list of columns to be excluded during the wildcard expansion.
|
|
743
|
+
* @param {Array} replace - An optional list of columns to replace during the wildcard expansion.
|
|
744
|
+
*
|
|
745
|
+
* @returns {Array} Returns an array of explicit columns derived from the wildcard.
|
|
746
|
+
*/
|
|
747
|
+
function getColumnsForWildcard(exclude = [], replace = []) {
|
|
748
|
+
const wildcardColumns = []
|
|
749
|
+
Object.keys(inferred.$combinedElements).forEach(k => {
|
|
750
|
+
const { index, tableAlias } = inferred.$combinedElements[k][0]
|
|
751
|
+
const element = tableAlias.elements[k]
|
|
752
|
+
// ignore FK for odata csn / ignore blobs from wildcard expansion
|
|
753
|
+
if (isODataFlatForeignKey(element) || (element['@Core.MediaType'] && !element['@Core.IsURL'])) return
|
|
754
|
+
// for wildcard on subquery in from, just reference the elements
|
|
755
|
+
if (tableAlias.SELECT && !element.elements && !element.target) {
|
|
756
|
+
wildcardColumns.push(index ? { ref: [index, k] } : { ref: [k] })
|
|
757
|
+
} else {
|
|
758
|
+
const flatColumns = getFlatColumnsFor(element, { tableAlias: index }, [], { exclude, replace }, true)
|
|
759
|
+
wildcardColumns.push(...flatColumns)
|
|
760
|
+
}
|
|
761
|
+
})
|
|
762
|
+
return wildcardColumns
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* HACK for odata csn input - foreign keys are already part of the elements in this csn flavor
|
|
766
|
+
* not excluding them from the wildcard columns would cause duplicate columns upon foreign key expansion
|
|
767
|
+
* @param {CSN.element} e
|
|
768
|
+
* @returns {boolean} true if the element is a flat foreign key generated by the compiler
|
|
769
|
+
*/
|
|
770
|
+
function isODataFlatForeignKey(e) {
|
|
771
|
+
return Boolean(e['@odata.foreignKey4'] || e._foreignKey4)
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Resolve `ref` within `def` and return the element
|
|
777
|
+
*
|
|
778
|
+
* @param {string[]} ref
|
|
779
|
+
* @param {CSN.Artifact} def
|
|
780
|
+
* @returns {CSN.Element}
|
|
781
|
+
*/
|
|
782
|
+
function getElementForRef(ref, def) {
|
|
783
|
+
return ref.reduce((prev, res) => {
|
|
784
|
+
return prev?.elements?.[res] || prev?._target?.elements[res]
|
|
785
|
+
}, def)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Recursively expands a structured element into flat columns, representing all leaf paths.
|
|
790
|
+
* This function transforms complex structured elements into simple column representations.
|
|
791
|
+
*
|
|
792
|
+
* For each element, the function checks if it's a structure, an association or a scalar,
|
|
793
|
+
* and proceeds accordingly. If the element is a structure, it recursively fetches flat columns for all sub-elements.
|
|
794
|
+
* If it's an association, it fetches flat columns for it's foreign keys.
|
|
795
|
+
* If it's a scalar, it creates a flat column for it.
|
|
796
|
+
*
|
|
797
|
+
* Columns excluded in a wildcard expansion or replaced by other columns are also handled accordingly.
|
|
798
|
+
*
|
|
799
|
+
* @param {object} column - The structured element which needs to be expanded.
|
|
800
|
+
* @param {string} baseName - The prefixes of the column reference (joined with '_'). Optional.
|
|
801
|
+
* @param {string} columnAlias - The explicit alias which the user has defined for the column.
|
|
802
|
+
* For instance `{ struct.foo as bar}` will be transformed into
|
|
803
|
+
* `{ struct_foo_leaf1 as bar_foo_leaf1, struct_foo_leaf2 as bar_foo_leaf2 }`.
|
|
804
|
+
* @param {string} tableAlias - The table alias to prepend to the column name. Optional.
|
|
805
|
+
* @param {Array} csnPath - An array containing CSN paths. Optional.
|
|
806
|
+
* @param {Array} exclude - An array of columns to be excluded from the flat structure. Optional.
|
|
807
|
+
* @param {Array} replace - An array of columns to be replaced in the flat structure. Optional.
|
|
808
|
+
*
|
|
809
|
+
* @returns {object[]} Returns an array of flat column(s) for the given element.
|
|
810
|
+
*/
|
|
811
|
+
function getFlatColumnsFor(column, names, csnPath = [], excludeAndReplace, isWildcard = false) {
|
|
812
|
+
if (!column) return column
|
|
813
|
+
if (column.val || column.func || column.SELECT) return [column]
|
|
814
|
+
|
|
815
|
+
let { baseName, columnAlias, tableAlias } = names
|
|
816
|
+
const { exclude, replace } = excludeAndReplace || {}
|
|
817
|
+
const { $refLinks, flatName, isJoinRelevant } = column
|
|
818
|
+
let leafAssoc
|
|
819
|
+
let element = $refLinks ? $refLinks[$refLinks.length - 1].definition : column
|
|
820
|
+
if (isWildcard && element.type === 'cds.LargeBinary') return []
|
|
821
|
+
if (element.on) return [] // unmanaged doesn't make it into columns
|
|
822
|
+
else if (element.virtual === true) return []
|
|
823
|
+
else if (!isJoinRelevant && flatName) baseName = flatName
|
|
824
|
+
else if (isJoinRelevant) {
|
|
825
|
+
const leaf = column.$refLinks[column.$refLinks.length - 1]
|
|
826
|
+
leafAssoc = [...column.$refLinks].reverse().find(link => link.definition.isAssociation)
|
|
827
|
+
const { foreignKeys } = leafAssoc.definition
|
|
828
|
+
if (foreignKeys && leaf.alias in foreignKeys) {
|
|
829
|
+
element = leafAssoc.definition
|
|
830
|
+
baseName = getFullName(leafAssoc.definition)
|
|
831
|
+
columnAlias = column.ref.slice(0, -1).map(idOnly).join('_')
|
|
832
|
+
} else baseName = getFullName(column.$refLinks[column.$refLinks.length - 1].definition)
|
|
833
|
+
} else baseName = baseName ? `${baseName}_${element.name}` : getFullName(element)
|
|
834
|
+
|
|
835
|
+
// now we have the name of the to be expanded column
|
|
836
|
+
// it could be a structure, an association or a scalar
|
|
837
|
+
// check if the column shall be skipped
|
|
838
|
+
// e.g. for wildcard elements which have been overwritten before
|
|
839
|
+
if (exclude && getReplacement(exclude)) return []
|
|
840
|
+
const replacedBy = getReplacement(replace)
|
|
841
|
+
if (replacedBy) {
|
|
842
|
+
// the replacement alias is the baseName of the flat structure
|
|
843
|
+
// e.g. `office.{ *, address.city as floor }`
|
|
844
|
+
// for the `ref: [ office, floor ]` we find the replacement
|
|
845
|
+
// `ref: [ office, address, city]` so the `baseName` of the replacement
|
|
846
|
+
if (replacedBy.as) replacedBy.as = baseName
|
|
847
|
+
// we might have a new base ref
|
|
848
|
+
if (replacedBy.ref && replacedBy.ref.length > 1)
|
|
849
|
+
baseName = getFullName(replacedBy.$refLinks?.[replacedBy.$refLinks.length - 2].definition)
|
|
850
|
+
if (replacedBy.isJoinRelevant)
|
|
851
|
+
// we need to provide the correct table alias
|
|
852
|
+
tableAlias = getQuerySourceName(replacedBy)
|
|
853
|
+
|
|
854
|
+
return getFlatColumnsFor(replacedBy, { baseName, columnAlias: replacedBy.as, tableAlias }, csnPath)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
csnPath.push(element.name)
|
|
858
|
+
|
|
859
|
+
if (element.keys) {
|
|
860
|
+
const flatColumns = []
|
|
861
|
+
element.keys.forEach(fk => {
|
|
862
|
+
const fkElement = getElementForRef(fk.ref, element._target)
|
|
863
|
+
let fkBaseName
|
|
864
|
+
if (!leafAssoc || leafAssoc.onlyForeignKeyAccess)
|
|
865
|
+
fkBaseName = `${baseName}_${fk.as || fk.ref[fk.ref.length - 1]}`
|
|
866
|
+
// e.g. if foreign key is accessed via infix filter - use join alias to access key in target
|
|
867
|
+
else fkBaseName = fk.ref[fk.ref.length - 1]
|
|
868
|
+
const fkPath = [...csnPath, fk.ref[fk.ref.length - 1]]
|
|
869
|
+
if (fkElement.elements) {
|
|
870
|
+
// structured key
|
|
871
|
+
Object.values(fkElement.elements).forEach(e => {
|
|
872
|
+
let alias
|
|
873
|
+
if (columnAlias) {
|
|
874
|
+
const fkName = fk.as
|
|
875
|
+
? `${fk.as}_${e.name}` // foreign key might also be re-named: `assoc { id as foo }`
|
|
876
|
+
: `${fk.ref.join('_')}_${e.name}`
|
|
877
|
+
alias = `${columnAlias}_${fkName}`
|
|
878
|
+
}
|
|
879
|
+
flatColumns.push(
|
|
880
|
+
...getFlatColumnsFor(
|
|
881
|
+
e,
|
|
882
|
+
{ baseName: fkBaseName, columnAlias: alias, tableAlias },
|
|
883
|
+
[...fkPath],
|
|
884
|
+
excludeAndReplace,
|
|
885
|
+
isWildcard,
|
|
886
|
+
),
|
|
887
|
+
)
|
|
888
|
+
})
|
|
889
|
+
} else if (fkElement.isAssociation) {
|
|
890
|
+
// assoc as key
|
|
891
|
+
flatColumns.push(
|
|
892
|
+
...getFlatColumnsFor(
|
|
893
|
+
fkElement,
|
|
894
|
+
{ baseName, columnAlias, tableAlias },
|
|
895
|
+
csnPath,
|
|
896
|
+
excludeAndReplace,
|
|
897
|
+
isWildcard,
|
|
898
|
+
),
|
|
899
|
+
)
|
|
900
|
+
} else {
|
|
901
|
+
// leaf reached
|
|
902
|
+
let flatColumn
|
|
903
|
+
if (columnAlias) flatColumn = { ref: [fkBaseName], as: `${columnAlias}_${fk.ref.join('_')}` }
|
|
904
|
+
else flatColumn = { ref: [fkBaseName] }
|
|
905
|
+
if (tableAlias) flatColumn.ref.unshift(tableAlias)
|
|
906
|
+
Object.defineProperty(flatColumn, 'element', { value: fkElement })
|
|
907
|
+
Object.defineProperty(flatColumn, '_csnPath', { value: csnPath })
|
|
908
|
+
flatColumns.push(flatColumn)
|
|
909
|
+
}
|
|
910
|
+
})
|
|
911
|
+
return flatColumns
|
|
912
|
+
} else if (element.elements) {
|
|
913
|
+
const flatRefs = []
|
|
914
|
+
Object.values(element.elements).forEach(e => {
|
|
915
|
+
const alias = columnAlias ? `${columnAlias}_${e.name}` : null
|
|
916
|
+
flatRefs.push(
|
|
917
|
+
...getFlatColumnsFor(
|
|
918
|
+
e,
|
|
919
|
+
{ baseName, columnAlias: alias, tableAlias },
|
|
920
|
+
[...csnPath],
|
|
921
|
+
excludeAndReplace,
|
|
922
|
+
isWildcard,
|
|
923
|
+
),
|
|
924
|
+
)
|
|
925
|
+
})
|
|
926
|
+
return flatRefs
|
|
927
|
+
}
|
|
928
|
+
const flatRef = tableAlias ? { ref: [tableAlias, baseName] } : { ref: [baseName] }
|
|
929
|
+
if (column.cast) {
|
|
930
|
+
flatRef.cast = column.cast
|
|
931
|
+
if (!columnAlias)
|
|
932
|
+
// provide an explicit alias
|
|
933
|
+
columnAlias = baseName
|
|
934
|
+
}
|
|
935
|
+
if (column.sort) flatRef.sort = column.sort
|
|
936
|
+
if (columnAlias) flatRef.as = columnAlias
|
|
937
|
+
Object.defineProperty(flatRef, 'element', { value: element })
|
|
938
|
+
Object.defineProperty(flatRef, '_csnPath', { value: csnPath })
|
|
939
|
+
return [flatRef]
|
|
940
|
+
|
|
941
|
+
function getReplacement(from) {
|
|
942
|
+
return from?.find(replacement => {
|
|
943
|
+
const nameOfExcludedColumn = replacement.as || replacement.ref?.[replacement.ref.length - 1] || replacement
|
|
944
|
+
return nameOfExcludedColumn === element.name
|
|
945
|
+
})
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Transforms a CQN token stream (e.g. `where`, `xpr` or `having`) into a SQL like expression.
|
|
951
|
+
*
|
|
952
|
+
* Expand `exists <assoc>` into `WHERE EXISTS` subqueries, apply flattening to `ref`s.
|
|
953
|
+
* Recursively apply `cqn4sql` to query expressions found in the token stream.
|
|
954
|
+
*
|
|
955
|
+
* @param {object[]} tokenStream - The token stream to transform. Each token in the stream is an
|
|
956
|
+
* object representing a CQN construct such as a column, an operator,
|
|
957
|
+
* or a subquery.
|
|
958
|
+
* @param {object} [$baseLink=null] - The context in which the `ref`s in the token stream are resolvable.
|
|
959
|
+
* It serves as the reference point for resolving associations in
|
|
960
|
+
* statements like `{…} WHERE exists assoc[exists anotherAssoc]`.
|
|
961
|
+
* Here, the $baseLink for `anotherAssoc` would be `assoc`.
|
|
962
|
+
* @returns {object[]} - The transformed token stream.
|
|
963
|
+
*/
|
|
964
|
+
function getTransformedTokenStream(tokenStream, $baseLink = null) {
|
|
965
|
+
const transformedWhere = []
|
|
966
|
+
for (let i = 0; i < tokenStream.length; i++) {
|
|
967
|
+
const token = tokenStream[i]
|
|
968
|
+
if (token === 'exists') {
|
|
969
|
+
transformedWhere.push(token)
|
|
970
|
+
const whereExistsSubSelects = []
|
|
971
|
+
const { ref, $refLinks } = tokenStream[i + 1]
|
|
972
|
+
if (!ref) continue
|
|
973
|
+
if (ref[0] in { $self: true, $projection: true })
|
|
974
|
+
cds.error(`Unexpected "${ref[0]}" following "exists", remove it or add a table alias instead`)
|
|
975
|
+
const firstStepIsTableAlias = ref.length > 1 && ref[0] in inferred.sources
|
|
976
|
+
for (let j = 0; j < ref.length; j += 1) {
|
|
977
|
+
let current, next
|
|
978
|
+
const step = ref[j]
|
|
979
|
+
const id = step.id || step
|
|
980
|
+
if (j === 0) {
|
|
981
|
+
if (firstStepIsTableAlias) continue
|
|
982
|
+
current = $baseLink || {
|
|
983
|
+
definition: $refLinks[0].target,
|
|
984
|
+
target: $refLinks[0].target,
|
|
985
|
+
// if the first step of a where is not a table alias,
|
|
986
|
+
// the table alias is the query source where the current ref step
|
|
987
|
+
// originates from. As no table alias is specified, there must be
|
|
988
|
+
// only one table alias for the given ref step
|
|
989
|
+
alias: inferred.$combinedElements[id][0].index,
|
|
990
|
+
}
|
|
991
|
+
next = $refLinks[0]
|
|
992
|
+
} else {
|
|
993
|
+
current = $refLinks[j - 1]
|
|
994
|
+
next = $refLinks[j]
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (isStructured(next.definition)) {
|
|
998
|
+
// find next association / entity in the ref because this is actually our real nextStep
|
|
999
|
+
const nextAssocIndex =
|
|
1000
|
+
2 + $refLinks.slice(j + 2).findIndex(rl => rl.definition.isAssociation || rl.definition.kind === 'entity')
|
|
1001
|
+
next = $refLinks[nextAssocIndex]
|
|
1002
|
+
j = nextAssocIndex
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const as = getNextAvailableTableAlias(getLastStringSegment(next.alias))
|
|
1006
|
+
next.alias = as
|
|
1007
|
+
whereExistsSubSelects.push(getWhereExistsSubquery(current, next, step.where, true))
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const whereExists = { SELECT: whereExistsSubqueries(whereExistsSubSelects) }
|
|
1011
|
+
transformedWhere[i + 1] = whereExists
|
|
1012
|
+
// skip newly created subquery from being iterated
|
|
1013
|
+
i += 1
|
|
1014
|
+
} else if (token.list) {
|
|
1015
|
+
if (token.list.length === 0) {
|
|
1016
|
+
// replace `[not] in <empty list>` to harmonize behavior across dbs
|
|
1017
|
+
const precedingTwoTokens = tokenStream.slice(i - 2, i)
|
|
1018
|
+
const firstPrecedingToken =
|
|
1019
|
+
typeof precedingTwoTokens[0] === 'string' ? precedingTwoTokens[0].toLowerCase() : ''
|
|
1020
|
+
const secondPrecedingToken =
|
|
1021
|
+
typeof precedingTwoTokens[1] === 'string' ? precedingTwoTokens[1].toLowerCase() : ''
|
|
1022
|
+
|
|
1023
|
+
if (firstPrecedingToken === 'not') {
|
|
1024
|
+
transformedWhere.splice(i - 2, 2, 'is', 'not', 'null')
|
|
1025
|
+
} else if (secondPrecedingToken === 'in') {
|
|
1026
|
+
transformedWhere.splice(i - 1, 1, '=', { val: null })
|
|
1027
|
+
} else {
|
|
1028
|
+
transformedWhere.push({ list: [] })
|
|
1029
|
+
}
|
|
1030
|
+
} else {
|
|
1031
|
+
transformedWhere.push({ list: getTransformedTokenStream(token.list) })
|
|
1032
|
+
}
|
|
1033
|
+
} else if (tokenStream.length === 1 && token.val && $baseLink) {
|
|
1034
|
+
// infix filter - OData variant w/o mentioning key --> flatten out and compare each leaf to token.val
|
|
1035
|
+
const def = $baseLink.definition._target || $baseLink.definition
|
|
1036
|
+
const keys = def.keys // use key aspect on entity
|
|
1037
|
+
const keyValComparisons = []
|
|
1038
|
+
const flatKeys = []
|
|
1039
|
+
Object.values(keys)
|
|
1040
|
+
// up__ID already part of inner where exists, no need to add it explicitly here
|
|
1041
|
+
.filter(k => k !== backlinkFor($baseLink.definition)?.[0])
|
|
1042
|
+
.forEach(v => {
|
|
1043
|
+
flatKeys.push(...getFlatColumnsFor(v, { tableAlias: $baseLink.alias }))
|
|
1044
|
+
})
|
|
1045
|
+
if (flatKeys.length > 1)
|
|
1046
|
+
throw new Error('Filters can only be applied to managed associations which result in a single foreign key')
|
|
1047
|
+
flatKeys.forEach(c => keyValComparisons.push([...[c, '=', token]]))
|
|
1048
|
+
keyValComparisons.forEach((kv, j) =>
|
|
1049
|
+
transformedWhere.push(...kv) && keyValComparisons[j + 1] ? transformedWhere.push('and') : null,
|
|
1050
|
+
)
|
|
1051
|
+
} else if (token.ref && token.param) {
|
|
1052
|
+
transformedWhere.push({ ...token })
|
|
1053
|
+
} else if (pseudos.elements[token.ref?.[0]]) {
|
|
1054
|
+
transformedWhere.push({ ...token })
|
|
1055
|
+
} else {
|
|
1056
|
+
// expand `struct = null | struct2`
|
|
1057
|
+
const { definition } = token.$refLinks?.[token.$refLinks.length - 1] || {}
|
|
1058
|
+
const next = tokenStream[i + 1]
|
|
1059
|
+
if (allOps.some(([firstOp]) => firstOp === next) && (definition?.elements || definition?.keys)) {
|
|
1060
|
+
const ops = [next]
|
|
1061
|
+
let indexRhs = i + 2
|
|
1062
|
+
let rhs = tokenStream[i + 2] // either another operator (i.e. `not like` et. al.) or the operand, i.e. the val | null
|
|
1063
|
+
if (allOps.some(([, secondOp]) => secondOp === rhs)) {
|
|
1064
|
+
ops.push(rhs)
|
|
1065
|
+
rhs = tokenStream[i + 3]
|
|
1066
|
+
indexRhs += 1
|
|
1067
|
+
}
|
|
1068
|
+
if (
|
|
1069
|
+
isAssocOrStruct(rhs.$refLinks?.[rhs.$refLinks.length - 1].definition) ||
|
|
1070
|
+
rhs.val !== undefined ||
|
|
1071
|
+
/* unary operator `is null` parsed as string */
|
|
1072
|
+
rhs === 'null'
|
|
1073
|
+
) {
|
|
1074
|
+
if (notSupportedOps.some(([firstOp]) => firstOp === next))
|
|
1075
|
+
cds.error(`The operator "${next}" is not supported for structure comparison`)
|
|
1076
|
+
const newTokens = expandComparison(token, ops, rhs)
|
|
1077
|
+
const needXpr = Boolean(tokenStream[i - 1] || tokenStream[indexRhs + 1])
|
|
1078
|
+
transformedWhere.push(...(needXpr ? [asXpr(newTokens)] : newTokens))
|
|
1079
|
+
i = indexRhs // jump to next relevant index
|
|
1080
|
+
}
|
|
1081
|
+
} else {
|
|
1082
|
+
// reject associations in expression, except if we are in an infix filter -> $baseLink is set
|
|
1083
|
+
assertNoStructInXpr(token, $baseLink)
|
|
1084
|
+
|
|
1085
|
+
let result = is_regexp(token?.val) ? token : copy(token) // REVISIT: too expensive! //
|
|
1086
|
+
if (token.ref) {
|
|
1087
|
+
const tableAlias = getQuerySourceName(token, $baseLink)
|
|
1088
|
+
if (!$baseLink && token.isJoinRelevant) {
|
|
1089
|
+
result.ref = [tableAlias, getFullName(token.$refLinks[token.$refLinks.length - 1].definition)]
|
|
1090
|
+
} else if (tableAlias) {
|
|
1091
|
+
result.ref = [tableAlias, token.flatName]
|
|
1092
|
+
} else {
|
|
1093
|
+
// if there is no table alias, we might select from an anonymous subquery
|
|
1094
|
+
result.ref = [token.flatName]
|
|
1095
|
+
}
|
|
1096
|
+
} else if (token.SELECT) {
|
|
1097
|
+
result = transformSubquery(token)
|
|
1098
|
+
} else if (token.xpr) {
|
|
1099
|
+
result.xpr = getTransformedTokenStream(token.xpr, $baseLink)
|
|
1100
|
+
} else if (token.func && token.args) {
|
|
1101
|
+
result.args = token.args.map(t => {
|
|
1102
|
+
if (!t.val)
|
|
1103
|
+
// this must not be touched
|
|
1104
|
+
return getTransformedTokenStream([t], $baseLink)[0]
|
|
1105
|
+
return t
|
|
1106
|
+
})
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
transformedWhere.push(result)
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return transformedWhere
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Expand the given definition and compare all leafs to `val`.
|
|
1118
|
+
*
|
|
1119
|
+
* @param {object} token with $refLinks
|
|
1120
|
+
* @param {string} operator one of allOps
|
|
1121
|
+
* @param {object} value either `null` or a column (with `ref` and `$refLinks`)
|
|
1122
|
+
* @returns {array}
|
|
1123
|
+
*/
|
|
1124
|
+
function expandComparison(token, operator, value) {
|
|
1125
|
+
const { definition } = token.$refLinks[token.$refLinks.length - 1]
|
|
1126
|
+
let flatRhs
|
|
1127
|
+
const result = []
|
|
1128
|
+
if (value.$refLinks) {
|
|
1129
|
+
// structural comparison
|
|
1130
|
+
flatRhs = flattenWithBaseName(value)
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (flatRhs) {
|
|
1134
|
+
const flatLhs = flattenWithBaseName(token)
|
|
1135
|
+
|
|
1136
|
+
//Revisit: Early exit here? We kndow we cant compare the structs, however we do not know exactly why
|
|
1137
|
+
// --> calculate error message or exit early? See test "proper error if structures cannot be compared / too many elements on lhs"
|
|
1138
|
+
if (flatRhs.length !== flatLhs.length)
|
|
1139
|
+
// make sure we can compare both structures
|
|
1140
|
+
cds.error(
|
|
1141
|
+
`Can't compare "${definition.name}" with "${
|
|
1142
|
+
value.$refLinks[value.$refLinks.length - 1].definition.name
|
|
1143
|
+
}": the operands must have the same structure`,
|
|
1144
|
+
)
|
|
1145
|
+
const pathNotFoundErr = []
|
|
1146
|
+
const boolOp = notEqOps.some(([f, s]) => operator[0] === f && operator[1] === s) ? 'or' : 'and'
|
|
1147
|
+
const rhsPath = value.ref.join('.') // original path of the comparison, used in error message
|
|
1148
|
+
while (flatLhs.length > 0) {
|
|
1149
|
+
// retrieve and remove one flat element from LHS and search for it in RHS (remove it there too)
|
|
1150
|
+
const { ref, _csnPath: lhs_csnPath } = flatLhs.shift()
|
|
1151
|
+
const indexOfElementOnRhs = flatRhs.findIndex(rhs => {
|
|
1152
|
+
const { _csnPath: rhs_csnPath } = rhs
|
|
1153
|
+
// all following steps must also be part of lhs
|
|
1154
|
+
return lhs_csnPath.slice(1).every((val, i) => val === rhs_csnPath[i + 1]) // first step is name of struct -> ignore
|
|
1155
|
+
})
|
|
1156
|
+
if (indexOfElementOnRhs === -1) {
|
|
1157
|
+
pathNotFoundErr.push(`Path "${lhs_csnPath.slice(1).join('.')}" not found in "${rhsPath}"`)
|
|
1158
|
+
continue
|
|
1159
|
+
}
|
|
1160
|
+
const rhs = flatRhs.splice(indexOfElementOnRhs, 1)[0] // remove the element also from RHS
|
|
1161
|
+
result.push({ ref }, ...operator, rhs)
|
|
1162
|
+
if (flatLhs.length > 0) result.push(boolOp)
|
|
1163
|
+
}
|
|
1164
|
+
if (flatRhs.length) {
|
|
1165
|
+
// if we still have elements in flatRhs -> those were not found in lhs
|
|
1166
|
+
const lhsPath = token.ref.join('.') // original path of the comparison, used in error message
|
|
1167
|
+
flatRhs.forEach(t => pathNotFoundErr.push(`Path "${t._csnPath.slice(1).join('.')}" not found in "${lhsPath}"`))
|
|
1168
|
+
cds.error(`Can't compare "${lhsPath}" with "${rhsPath}": ${pathNotFoundErr.join(', ')}`)
|
|
1169
|
+
}
|
|
1170
|
+
} else {
|
|
1171
|
+
// compare with value
|
|
1172
|
+
const flatLhs = flattenWithBaseName(token)
|
|
1173
|
+
if (flatLhs.length > 1 && value.val !== null && value !== 'null')
|
|
1174
|
+
cds.error(`Can't compare structure "${token.ref.join('.')}" with value "${value.val}"`)
|
|
1175
|
+
const boolOp = notEqOps.some(([f, s]) => operator[0] === f && operator[1] === s) ? 'or' : 'and'
|
|
1176
|
+
flatLhs.forEach((column, i) => {
|
|
1177
|
+
result.push(column, ...operator, value)
|
|
1178
|
+
if (flatLhs[i + 1]) result.push(boolOp)
|
|
1179
|
+
})
|
|
1180
|
+
}
|
|
1181
|
+
return result
|
|
1182
|
+
|
|
1183
|
+
function flattenWithBaseName(def) {
|
|
1184
|
+
if (!def.$refLinks) return def
|
|
1185
|
+
const leaf = def.$refLinks[def.$refLinks.length - 1]
|
|
1186
|
+
const first = def.$refLinks[0]
|
|
1187
|
+
const tableAlias = getQuerySourceName(def, def.ref.length > 1 && first.definition.isAssociation ? first : null)
|
|
1188
|
+
if (leaf.definition.parent.kind !== 'entity')
|
|
1189
|
+
// we need the base name
|
|
1190
|
+
return getFlatColumnsFor(leaf.definition, {
|
|
1191
|
+
baseName: def.ref.slice(0, def.ref.length - 1).join('_'),
|
|
1192
|
+
tableAlias,
|
|
1193
|
+
})
|
|
1194
|
+
return getFlatColumnsFor(leaf.definition, { tableAlias })
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function assertNoStructInXpr(token, inInfixFilter = false) {
|
|
1199
|
+
if (!inInfixFilter && token.$refLinks?.[token.$refLinks.length - 1].definition.target)
|
|
1200
|
+
// revisit: let this through if not requested otherwise
|
|
1201
|
+
rejectAssocInExpression()
|
|
1202
|
+
if (isStructured(token.$refLinks?.[token.$refLinks.length - 1].definition))
|
|
1203
|
+
// revisit: let this through if not requested otherwise
|
|
1204
|
+
rejectStructInExpression()
|
|
1205
|
+
|
|
1206
|
+
function rejectAssocInExpression() {
|
|
1207
|
+
throw new Error(/An association can't be used as a value in an expression/)
|
|
1208
|
+
}
|
|
1209
|
+
function rejectStructInExpression() {
|
|
1210
|
+
throw new Error(/A structured element can't be used as a value in an expression/)
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Recursively walks over all `from` args. Association steps in the `ref`s
|
|
1216
|
+
* are transformed into `WHERE exists` subqueries. The given `from.ref`s
|
|
1217
|
+
* are always of length == 1 after processing.
|
|
1218
|
+
*
|
|
1219
|
+
* The steps in a `ref` are processed in reversed order. This is the main difference
|
|
1220
|
+
* to the `WHERE exists` expansion in the @function getTransformedTokenStream().
|
|
1221
|
+
*
|
|
1222
|
+
* @param {object} from
|
|
1223
|
+
* @param {object[]?} existingWhere custom where condition which is appended to the filter
|
|
1224
|
+
* conditions of the resulting `WHERE exists` subquery
|
|
1225
|
+
*/
|
|
1226
|
+
function getTransformedFrom(from, existingWhere = []) {
|
|
1227
|
+
const transformedWhere = []
|
|
1228
|
+
let transformedFrom = copy(from) // REVISIT: too expensive!
|
|
1229
|
+
if (from.$refLinks)
|
|
1230
|
+
Object.defineProperty(transformedFrom, '$refLinks', { value: [...from.$refLinks], writable: true })
|
|
1231
|
+
if (from.args) {
|
|
1232
|
+
transformedFrom.args = []
|
|
1233
|
+
from.args.forEach(arg => {
|
|
1234
|
+
if (arg.SELECT) {
|
|
1235
|
+
const { whereExists: e, transformedFrom: f } = getTransformedFrom(arg.SELECT.from, arg.SELECT.where)
|
|
1236
|
+
const transformedArg = { SELECT: { from: f, where: e } }
|
|
1237
|
+
transformedFrom.args.push(transformedArg)
|
|
1238
|
+
} else {
|
|
1239
|
+
const { transformedFrom: f } = getTransformedFrom(arg)
|
|
1240
|
+
transformedFrom.args.push(f)
|
|
1241
|
+
}
|
|
1242
|
+
})
|
|
1243
|
+
return { transformedFrom }
|
|
1244
|
+
} else if (from.SELECT) {
|
|
1245
|
+
transformedFrom = transformSubquery(from)
|
|
1246
|
+
if (from.as)
|
|
1247
|
+
// preserve explicit TA
|
|
1248
|
+
transformedFrom.as = from.as
|
|
1249
|
+
return { transformedFrom }
|
|
1250
|
+
} else {
|
|
1251
|
+
return _transformFrom()
|
|
1252
|
+
}
|
|
1253
|
+
function _transformFrom() {
|
|
1254
|
+
if (typeof from === 'string') {
|
|
1255
|
+
// normalize to `ref`, i.e. for `UPDATE.entity('bookshop.Books')`
|
|
1256
|
+
return { transformedFrom: { ref: [from], as: getLastStringSegment(from) } }
|
|
1257
|
+
}
|
|
1258
|
+
transformedFrom.as =
|
|
1259
|
+
from.uniqueSubqueryAlias ||
|
|
1260
|
+
from.as ||
|
|
1261
|
+
getLastStringSegment(transformedFrom.$refLinks[transformedFrom.$refLinks.length - 1].definition.name)
|
|
1262
|
+
const whereExistsSubSelects = []
|
|
1263
|
+
const filterConditions = []
|
|
1264
|
+
const refReverse = [...from.ref].reverse()
|
|
1265
|
+
const $refLinksReverse = [...transformedFrom.$refLinks].reverse()
|
|
1266
|
+
for (let i = 0; i < refReverse.length; i += 1) {
|
|
1267
|
+
const stepLink = $refLinksReverse[i]
|
|
1268
|
+
|
|
1269
|
+
let nextStepLink = $refLinksReverse[i + 1]
|
|
1270
|
+
const nextStep = refReverse[i + 1] // only because we want the filter condition
|
|
1271
|
+
|
|
1272
|
+
if (stepLink.definition.target && nextStepLink) {
|
|
1273
|
+
const { where } = nextStep
|
|
1274
|
+
if (isStructured(nextStepLink.definition)) {
|
|
1275
|
+
// find next association / entity in the ref because this is actually our real nextStep
|
|
1276
|
+
const nextStepIndex =
|
|
1277
|
+
2 +
|
|
1278
|
+
$refLinksReverse
|
|
1279
|
+
.slice(i + 2)
|
|
1280
|
+
.findIndex(rl => rl.definition.isAssociation || rl.definition.kind === 'entity')
|
|
1281
|
+
nextStepLink = $refLinksReverse[nextStepIndex]
|
|
1282
|
+
}
|
|
1283
|
+
let as = getLastStringSegment(nextStepLink.alias)
|
|
1284
|
+
/**
|
|
1285
|
+
* for an `expand` subquery, we do not need to add
|
|
1286
|
+
* the table alias of the `expand` host to the join tree
|
|
1287
|
+
* --> This is an artificial query, which will later be correlated
|
|
1288
|
+
* with the main query alias. see @function expandColumn()
|
|
1289
|
+
*/
|
|
1290
|
+
if (!(originalQuery.SELECT?.expand === true)) {
|
|
1291
|
+
as = getNextAvailableTableAlias(as)
|
|
1292
|
+
}
|
|
1293
|
+
nextStepLink.alias = as
|
|
1294
|
+
whereExistsSubSelects.push(getWhereExistsSubquery(stepLink, nextStepLink, where))
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// only append infix filter to outer where if it is the leaf of the from ref
|
|
1299
|
+
if (refReverse[0].where)
|
|
1300
|
+
filterConditions.push(getTransformedTokenStream(refReverse[0].where, $refLinksReverse[0]))
|
|
1301
|
+
|
|
1302
|
+
if (existingWhere.length > 0) filterConditions.push(existingWhere)
|
|
1303
|
+
if (whereExistsSubSelects.length > 0) {
|
|
1304
|
+
const { definition: leafAssoc, alias } = transformedFrom.$refLinks[transformedFrom.$refLinks.length - 1]
|
|
1305
|
+
Object.assign(transformedFrom, {
|
|
1306
|
+
ref: [leafAssoc.target],
|
|
1307
|
+
as: alias,
|
|
1308
|
+
})
|
|
1309
|
+
transformedWhere.push(...['exists', { SELECT: whereExistsSubqueries(whereExistsSubSelects) }])
|
|
1310
|
+
filterConditions.forEach(f => {
|
|
1311
|
+
transformedWhere.push('and')
|
|
1312
|
+
if (filterConditions.length > 1) transformedWhere.push(asXpr(f))
|
|
1313
|
+
else if (f.length > 3) transformedWhere.push(asXpr(f))
|
|
1314
|
+
else transformedWhere.push(...f)
|
|
1315
|
+
})
|
|
1316
|
+
} else {
|
|
1317
|
+
if (filterConditions.length > 0) {
|
|
1318
|
+
filterConditions.reverse().forEach((f, index) => {
|
|
1319
|
+
if (filterConditions.length > 1) transformedWhere.push(asXpr(f))
|
|
1320
|
+
else transformedWhere.push(...f)
|
|
1321
|
+
if (filterConditions[index + 1] !== undefined) transformedWhere.push('and')
|
|
1322
|
+
})
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// adjust ref & $refLinks after associations have turned into where exists subqueries
|
|
1327
|
+
transformedFrom.$refLinks.splice(0, transformedFrom.$refLinks.length - 1)
|
|
1328
|
+
transformedFrom.ref = [localized(transformedFrom.$refLinks[0].target)]
|
|
1329
|
+
|
|
1330
|
+
return { transformedWhere, transformedFrom }
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function whereExistsSubqueries(whereExistsSubSelects) {
|
|
1335
|
+
if (whereExistsSubSelects.length === 1) return whereExistsSubSelects[0]
|
|
1336
|
+
whereExistsSubSelects.reduce((prev, cur) => {
|
|
1337
|
+
if (prev.where) {
|
|
1338
|
+
prev.where.push('and', 'exists', { SELECT: cur })
|
|
1339
|
+
return cur
|
|
1340
|
+
} else {
|
|
1341
|
+
prev = cur
|
|
1342
|
+
}
|
|
1343
|
+
return prev
|
|
1344
|
+
}, {})
|
|
1345
|
+
return whereExistsSubSelects[0]
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function getNextAvailableTableAlias(id) {
|
|
1349
|
+
return inferred.joinTree.addNextAvailableTableAlias(id, inferred.outerQueries)
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function asXpr(thing) {
|
|
1353
|
+
return { xpr: thing }
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* @param {CSN.Element} elt
|
|
1358
|
+
* @returns {boolean}
|
|
1359
|
+
*/
|
|
1360
|
+
function isStructured(elt) {
|
|
1361
|
+
return Boolean(elt?.elements && elt.kind === 'element')
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* @param {CSN.Element} elt
|
|
1366
|
+
* @returns {boolean}
|
|
1367
|
+
*/
|
|
1368
|
+
function isAssocOrStruct(elt) {
|
|
1369
|
+
return elt?.keys || (elt?.elements && elt.kind === 'element')
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* Calculates which elements are the backlinks of a $self comparison in a
|
|
1374
|
+
* given on-condition. That are the managed associations in the target of the
|
|
1375
|
+
* given association.
|
|
1376
|
+
*
|
|
1377
|
+
* @param {CSN.Association} assoc with on-condition
|
|
1378
|
+
* @returns {[CSN.Association] | null} all assocs which are compared to `$self`
|
|
1379
|
+
*/
|
|
1380
|
+
function backlinkFor(assoc) {
|
|
1381
|
+
if (!assoc.on) return null
|
|
1382
|
+
const target = model.definitions[assoc.target]
|
|
1383
|
+
// technically we could have multiple backlinks
|
|
1384
|
+
const backlinks = []
|
|
1385
|
+
for (let i = 0; i < assoc.on.length; i += 3) {
|
|
1386
|
+
const lhs = assoc.on[i]
|
|
1387
|
+
const rhs = assoc.on[i + 2]
|
|
1388
|
+
if (lhs?.ref?.length === 1 && lhs.ref[0] === '$self') backlinks.push(rhs)
|
|
1389
|
+
else if (rhs?.ref?.length === 1 && rhs.ref[0] === '$self') backlinks.push(lhs)
|
|
1390
|
+
}
|
|
1391
|
+
return backlinks.map(each =>
|
|
1392
|
+
getElementForRef(
|
|
1393
|
+
each.ref.slice(1),
|
|
1394
|
+
each.ref[0] in { $self: true, $projection: true } ? getParentEntity(assoc) : target,
|
|
1395
|
+
),
|
|
1396
|
+
)
|
|
1397
|
+
|
|
1398
|
+
function getParentEntity(element) {
|
|
1399
|
+
if (element.kind === 'entity') return element
|
|
1400
|
+
else return getParentEntity(element.parent)
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/**
|
|
1405
|
+
* Calculates the on-condition for the given (un-)managed association.
|
|
1406
|
+
*
|
|
1407
|
+
* @param {$refLink} assocRefLink with on-condition
|
|
1408
|
+
* @param {$refLink} targetSideRefLink the reflink which has the target alias of the association
|
|
1409
|
+
* @returns {[CSN.Association] | null} all assocs which are compared to `$self`
|
|
1410
|
+
*/
|
|
1411
|
+
function onCondFor(assocRefLink, targetSideRefLink, inWhereOrJoin) {
|
|
1412
|
+
const { on, keys } = assocRefLink.definition
|
|
1413
|
+
const target = model.definitions[assocRefLink.definition.target]
|
|
1414
|
+
let res
|
|
1415
|
+
// technically we could have multiple backlinks
|
|
1416
|
+
if (keys) {
|
|
1417
|
+
const fkPkPairs = getParentKeyForeignKeyPairs(assocRefLink.definition, targetSideRefLink, true)
|
|
1418
|
+
const transformedOn = []
|
|
1419
|
+
fkPkPairs.forEach((pair, i) => {
|
|
1420
|
+
const { sourceSide, targetSide } = pair
|
|
1421
|
+
sourceSide.ref.unshift(assocRefLink.alias)
|
|
1422
|
+
transformedOn.push(sourceSide, '=', targetSide)
|
|
1423
|
+
if (fkPkPairs[i + 1]) transformedOn.push('and')
|
|
1424
|
+
})
|
|
1425
|
+
res = transformedOn
|
|
1426
|
+
} else if (on) {
|
|
1427
|
+
res = calculateOnCondition(on)
|
|
1428
|
+
}
|
|
1429
|
+
return res
|
|
1430
|
+
|
|
1431
|
+
/**
|
|
1432
|
+
* For an unmanaged association, calculate the proper on-condition.
|
|
1433
|
+
* For a `$self = assoc.<backlink>` comparison, the three tokens are replaced
|
|
1434
|
+
* by the on-condition of the <backlink>.
|
|
1435
|
+
*
|
|
1436
|
+
*
|
|
1437
|
+
* @param {on} tokenStream the on condition of the unmanaged association
|
|
1438
|
+
* @returns the final on-condition for the unmanaged association
|
|
1439
|
+
*/
|
|
1440
|
+
function calculateOnCondition(tokenStream) {
|
|
1441
|
+
const result = copy(tokenStream) // REVISIT: too expensive!
|
|
1442
|
+
for (let i = 0; i < result.length; i += 1) {
|
|
1443
|
+
const lhs = result[i]
|
|
1444
|
+
if (lhs.xpr) {
|
|
1445
|
+
const xpr = calculateOnCondition(lhs.xpr)
|
|
1446
|
+
result[i] = asXpr(xpr)
|
|
1447
|
+
continue
|
|
1448
|
+
}
|
|
1449
|
+
const rhs = result[i + 2]
|
|
1450
|
+
let backlink
|
|
1451
|
+
if (rhs?.ref && lhs?.ref) {
|
|
1452
|
+
if (lhs?.ref?.length === 1 && lhs.ref[0] === '$self')
|
|
1453
|
+
backlink = getElementForRef(
|
|
1454
|
+
rhs.ref.slice(1),
|
|
1455
|
+
rhs.ref[0] in { $self: true, $projection: true } ? getParentEntity(assocRefLink.definition) : target,
|
|
1456
|
+
)
|
|
1457
|
+
else if (rhs?.ref?.length === 1 && rhs.ref[0] === '$self')
|
|
1458
|
+
backlink = getElementForRef(
|
|
1459
|
+
lhs.ref.slice(1),
|
|
1460
|
+
lhs.ref[0] in { $self: true, $projection: true } ? getParentEntity(assocRefLink.definition) : target,
|
|
1461
|
+
)
|
|
1462
|
+
else {
|
|
1463
|
+
// if we have refs on each side of the comparison, we might need to perform tuple expansion
|
|
1464
|
+
// or flatten the structures
|
|
1465
|
+
// REVISIT: this whole section needs a refactoring, it is too complex and some edge cases may still be not considered...
|
|
1466
|
+
const refLinkFaker = thing => {
|
|
1467
|
+
const { ref } = thing
|
|
1468
|
+
const assocHost = getParentEntity(assocRefLink.definition)
|
|
1469
|
+
Object.defineProperty(thing, '$refLinks', {
|
|
1470
|
+
value: [],
|
|
1471
|
+
writable: true,
|
|
1472
|
+
})
|
|
1473
|
+
ref.reduce((prev, res, i) => {
|
|
1474
|
+
if (res === '$self')
|
|
1475
|
+
// next is resolvable in entity
|
|
1476
|
+
return prev
|
|
1477
|
+
const definition = prev?.elements?.[res] || prev?._target?.elements[res] || pseudos.elements[res]
|
|
1478
|
+
const target = getParentEntity(definition)
|
|
1479
|
+
thing.$refLinks[i] = { definition, target, alias: definition.name }
|
|
1480
|
+
return prev?.elements?.[res] || prev?._target?.elements[res] || pseudos.elements[res]
|
|
1481
|
+
}, assocHost)
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// comparison in on condition needs to be expanded...
|
|
1485
|
+
// re-use existing algorithm for that
|
|
1486
|
+
// we need to fake some $refLinks for that to work though...
|
|
1487
|
+
lhs?.ref && refLinkFaker(lhs)
|
|
1488
|
+
rhs?.ref && refLinkFaker(rhs)
|
|
1489
|
+
const lhsLeafArt = lhs.ref && lhs.$refLinks[lhs.$refLinks.length - 1].definition
|
|
1490
|
+
const rhsLeafArt = rhs.ref && rhs.$refLinks[rhs.$refLinks.length - 1].definition
|
|
1491
|
+
if (lhsLeafArt?.target || rhsLeafArt?.target) {
|
|
1492
|
+
if (rhs.$refLinks[0].definition !== assocRefLink.definition) {
|
|
1493
|
+
rhs.ref.unshift(targetSideRefLink.alias)
|
|
1494
|
+
rhs.$refLinks.unshift(targetSideRefLink)
|
|
1495
|
+
}
|
|
1496
|
+
if (lhs.$refLinks[0].definition !== assocRefLink.definition) {
|
|
1497
|
+
lhs.ref.unshift(targetSideRefLink.alias)
|
|
1498
|
+
lhs.$refLinks.unshift(targetSideRefLink)
|
|
1499
|
+
}
|
|
1500
|
+
const expandedComparison = getTransformedTokenStream([lhs, result[i + 1], rhs])
|
|
1501
|
+
const res = tokenStream[i + 3] ? [asXpr(expandedComparison)] : expandedComparison
|
|
1502
|
+
result.splice(i, 3, ...res)
|
|
1503
|
+
i += res.length
|
|
1504
|
+
continue
|
|
1505
|
+
}
|
|
1506
|
+
// naive assumption: if first step is the association itself, all following ref steps must be resolvable
|
|
1507
|
+
// within target `assoc.assoc.fk` -> `assoc.assoc_fk`
|
|
1508
|
+
else if (
|
|
1509
|
+
lhs.$refLinks[0].definition ===
|
|
1510
|
+
getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]
|
|
1511
|
+
)
|
|
1512
|
+
result[i].ref = [result[i].ref[0], lhs.ref.slice(1).join('_')]
|
|
1513
|
+
// naive assumption: if the path starts with an association which is not the association from
|
|
1514
|
+
// which the on-condition originates, it must be a foreign key and hence resolvable in the source
|
|
1515
|
+
else if (lhs.$refLinks[0].definition.target) result[i].ref = [result[i].ref.join('_')]
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
if (backlink) {
|
|
1519
|
+
const wrapInXpr = result[i + 3] || result[i - 1] // if we have a complex on-condition, wrap each part in xpr
|
|
1520
|
+
let backlinkOnCondition = []
|
|
1521
|
+
if (backlink.on) {
|
|
1522
|
+
// unmanaged backlink -> prepend correct aliases
|
|
1523
|
+
backlinkOnCondition = backlink.on.map(t => {
|
|
1524
|
+
if (t.ref?.length > 1 && t.ref[0] === (backlink.name || targetSideRefLink.definition.name)) {
|
|
1525
|
+
return { ref: [targetSideRefLink.alias, ...t.ref.slice(1)] }
|
|
1526
|
+
} else if (t.ref) {
|
|
1527
|
+
if (t.ref.length > 1 && !(t.ref[0] in pseudos.elements))
|
|
1528
|
+
return { ref: [assocRefLink.alias, ...t.ref.slice(1)] }
|
|
1529
|
+
else return { ref: [assocRefLink.alias, ...t.ref] }
|
|
1530
|
+
} else {
|
|
1531
|
+
return t
|
|
1532
|
+
}
|
|
1533
|
+
})
|
|
1534
|
+
} else if (backlink.keys) {
|
|
1535
|
+
// managed backlink -> calculate fk-pk pairs
|
|
1536
|
+
const fkPkPairs = getParentKeyForeignKeyPairs(backlink, targetSideRefLink)
|
|
1537
|
+
fkPkPairs.forEach((pair, j) => {
|
|
1538
|
+
const { sourceSide, targetSide } = pair
|
|
1539
|
+
sourceSide.ref.unshift(assocRefLink.alias)
|
|
1540
|
+
backlinkOnCondition.push(sourceSide, '=', targetSide)
|
|
1541
|
+
if (!inWhereOrJoin) backlinkOnCondition.reverse()
|
|
1542
|
+
if (fkPkPairs[j + 1]) backlinkOnCondition.push('and')
|
|
1543
|
+
})
|
|
1544
|
+
}
|
|
1545
|
+
result.splice(i, 3, ...(wrapInXpr ? [asXpr(backlinkOnCondition)] : backlinkOnCondition))
|
|
1546
|
+
i += wrapInXpr ? 1 : backlinkOnCondition.length // skip inserted tokens
|
|
1547
|
+
} else if (lhs.ref) {
|
|
1548
|
+
if (lhs.ref[0] === '$self') result[i].ref.splice(0, 1, targetSideRefLink.alias)
|
|
1549
|
+
else if (lhs.ref.length > 1) {
|
|
1550
|
+
if (
|
|
1551
|
+
!(lhs.ref[0] in pseudos.elements) &&
|
|
1552
|
+
lhs.ref[0] !== assocRefLink.alias &&
|
|
1553
|
+
lhs.ref[0] !== targetSideRefLink.alias
|
|
1554
|
+
) {
|
|
1555
|
+
// we need to find correct table alias for the structured access
|
|
1556
|
+
const { definition } = lhs.$refLinks[0]
|
|
1557
|
+
if (definition === assocRefLink.definition) {
|
|
1558
|
+
// first step is the association itself -> use it's name as it becomes the table alias
|
|
1559
|
+
result[i].ref.splice(0, 1, assocRefLink.alias)
|
|
1560
|
+
} else if (
|
|
1561
|
+
Object.values(
|
|
1562
|
+
targetSideRefLink.definition.elements || targetSideRefLink.definition._target.elements,
|
|
1563
|
+
).some(e => e === definition)
|
|
1564
|
+
) {
|
|
1565
|
+
// first step is association which refers to its foreign key by dot notation
|
|
1566
|
+
result[i].ref = [targetSideRefLink.alias, lhs.ref.join('_')]
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
} else if (lhs.ref.length === 1) result[i].ref.unshift(targetSideRefLink.alias)
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
return result
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Recursively calculates the containing entity for a given element.
|
|
1576
|
+
*
|
|
1577
|
+
* @param {CSN.element} element
|
|
1578
|
+
* @returns {CSN.definition} the entity containing the given element
|
|
1579
|
+
*/
|
|
1580
|
+
function getParentEntity(element) {
|
|
1581
|
+
if (!element.kind)
|
|
1582
|
+
// pseudo element
|
|
1583
|
+
return element
|
|
1584
|
+
if (element.kind === 'entity') return element
|
|
1585
|
+
else return model.definitions[localized(getParentEntity(element.parent))]
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/**
|
|
1590
|
+
* For a given managed association, calculate the foreign key - parent key tuples.
|
|
1591
|
+
*
|
|
1592
|
+
* @param {CDS.Association} assoc the association for which the on condition shall be calculated
|
|
1593
|
+
* @param {object} targetSideRefLink the reflink which has the target alias of the (backlink) association
|
|
1594
|
+
* @param {boolean} flipSourceAndTarget target and source side are flipped in the where exists subquery
|
|
1595
|
+
* @returns {[{sourceSide: {ref: []}, targetSide: {ref:[]}}]} array of source side - target side reference tuples, i.e. the foreign keys and parent keys.
|
|
1596
|
+
*/
|
|
1597
|
+
function getParentKeyForeignKeyPairs(assoc, targetSideRefLink, flipSourceAndTarget = false) {
|
|
1598
|
+
const res = []
|
|
1599
|
+
const backlink = backlinkFor(assoc)?.[0]
|
|
1600
|
+
const { keys, _target } = backlink || assoc
|
|
1601
|
+
if (keys) {
|
|
1602
|
+
keys.forEach(fk => {
|
|
1603
|
+
const { ref, as } = fk
|
|
1604
|
+
const elem = getElementForRef(ref, _target) // find the element (the target element of the foreign key) in the target of the (backlink) association
|
|
1605
|
+
const flatParentKeys = getFlatColumnsFor(elem, { baseName: ref.slice(0, ref.length - 1).join('_') }) // it might be a structured element, so expand it into the full parent key tuple
|
|
1606
|
+
const flatAssociationName = getFullName(backlink || assoc) // get the name of the (backlink) association
|
|
1607
|
+
const flatForeignKeys = getFlatColumnsFor(elem, { baseName: flatAssociationName, columnAlias: as }) // the name of the (backlink) association is the base of the foreign key tuple, also respect aliased fk.
|
|
1608
|
+
|
|
1609
|
+
for (let i = 0; i < flatForeignKeys.length; i++) {
|
|
1610
|
+
if (flipSourceAndTarget) {
|
|
1611
|
+
// `where exists <assoc>` expansion
|
|
1612
|
+
// a backlink will cause the foreign key to be on the source side
|
|
1613
|
+
const refInTarget = backlink ? flatParentKeys[i] : flatForeignKeys[i]
|
|
1614
|
+
const refInSource = backlink ? flatForeignKeys[i] : flatParentKeys[i]
|
|
1615
|
+
res.push({
|
|
1616
|
+
sourceSide: refInSource,
|
|
1617
|
+
targetSide: {
|
|
1618
|
+
ref: [
|
|
1619
|
+
targetSideRefLink.alias,
|
|
1620
|
+
refInTarget.as ? `${flatAssociationName}_${refInTarget.as}` : refInTarget.ref[0],
|
|
1621
|
+
],
|
|
1622
|
+
},
|
|
1623
|
+
})
|
|
1624
|
+
} else {
|
|
1625
|
+
// `select from <assoc>` to `where exists` expansion
|
|
1626
|
+
// a backlink will cause the foreign key to be on the target side
|
|
1627
|
+
const refInTarget = backlink ? flatForeignKeys[i] : flatParentKeys[i]
|
|
1628
|
+
const refInSource = backlink ? flatParentKeys[i] : flatForeignKeys[i]
|
|
1629
|
+
res.push({
|
|
1630
|
+
sourceSide: {
|
|
1631
|
+
ref: [refInSource.as ? `${flatAssociationName}_${refInSource.as}` : refInSource.ref[0]],
|
|
1632
|
+
},
|
|
1633
|
+
targetSide: { ref: [targetSideRefLink.alias, ...refInTarget.ref] },
|
|
1634
|
+
})
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
})
|
|
1638
|
+
}
|
|
1639
|
+
return res
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
/**
|
|
1643
|
+
* Constructs a where exists subquery for a given association - i.e. calculates foreign key / parent key
|
|
1644
|
+
* relations for the association.
|
|
1645
|
+
*
|
|
1646
|
+
* @param {$refLink} current step of the association path
|
|
1647
|
+
* @param {$refLink} next step of the association path
|
|
1648
|
+
* @param {object[]} customWhere infix filter which must be part of the where exists subquery on condition
|
|
1649
|
+
* @param {boolean} inWhere whether or not the path is part of the queries where clause
|
|
1650
|
+
* -> if it is, target and source side are flipped in the where exists subquery
|
|
1651
|
+
* @returns {CQN.SELECT}
|
|
1652
|
+
*/
|
|
1653
|
+
function getWhereExistsSubquery(current, next, customWhere = null, inWhere = false) {
|
|
1654
|
+
const { definition } = current
|
|
1655
|
+
const { definition: nextDefinition } = next
|
|
1656
|
+
const on = []
|
|
1657
|
+
const fkSource = inWhere ? nextDefinition : definition
|
|
1658
|
+
// TODO: use onCondFor()
|
|
1659
|
+
if (fkSource.keys) {
|
|
1660
|
+
const pkFkPairs = getParentKeyForeignKeyPairs(fkSource, current, inWhere)
|
|
1661
|
+
pkFkPairs.forEach((pkFkPair, i) => {
|
|
1662
|
+
const { targetSide, sourceSide } = pkFkPair
|
|
1663
|
+
sourceSide.ref.unshift(next.alias)
|
|
1664
|
+
if (i > 0) on.push('and')
|
|
1665
|
+
on.push(sourceSide, '=', targetSide)
|
|
1666
|
+
})
|
|
1667
|
+
} else {
|
|
1668
|
+
const unmanagedOn = onCondFor(inWhere ? next : current, inWhere ? current : next, inWhere)
|
|
1669
|
+
on.push(...(customWhere && hasLogicalOr(unmanagedOn) ? [asXpr(unmanagedOn)] : unmanagedOn))
|
|
1670
|
+
}
|
|
1671
|
+
// infix filter conditions are wrapped in `xpr` when added to the on-condition
|
|
1672
|
+
if (customWhere) {
|
|
1673
|
+
const filter = getTransformedTokenStream(customWhere, next)
|
|
1674
|
+
on.push(...['and', ...(hasLogicalOr(filter) ? [asXpr(filter)] : filter)])
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
const SELECT = {
|
|
1678
|
+
from: {
|
|
1679
|
+
ref: [localized(assocTarget(nextDefinition) || nextDefinition)],
|
|
1680
|
+
as: next.alias,
|
|
1681
|
+
},
|
|
1682
|
+
columns: [
|
|
1683
|
+
{
|
|
1684
|
+
val: 1,
|
|
1685
|
+
// as: 'dummy'
|
|
1686
|
+
},
|
|
1687
|
+
],
|
|
1688
|
+
where: on,
|
|
1689
|
+
}
|
|
1690
|
+
return SELECT
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* If the query is `localized`, return the name of the `localized` entity for the `definition`.
|
|
1695
|
+
* If there is no `localized` entity for the `definition`, return the name of the `definition`
|
|
1696
|
+
*
|
|
1697
|
+
* @param {CSN.definition} definition
|
|
1698
|
+
* @returns the name of the localized entity for the given `definition` or `definition.name`
|
|
1699
|
+
*/
|
|
1700
|
+
function localized(definition) {
|
|
1701
|
+
if (!isLocalized(definition)) return definition.name
|
|
1702
|
+
const view = model.definitions[`localized.${definition.name}`]
|
|
1703
|
+
return view?.name || definition.name
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
/**
|
|
1707
|
+
* If a given query is required to be translated, the query has
|
|
1708
|
+
* the `.localized` property set to `true`. If that is the case,
|
|
1709
|
+
* and the definition has not set the `@cds.localized` annotation
|
|
1710
|
+
* to `false`, the given definition must be translated.
|
|
1711
|
+
*
|
|
1712
|
+
* @returns true if the given definition shall be localized
|
|
1713
|
+
*/
|
|
1714
|
+
function isLocalized(definition) {
|
|
1715
|
+
return inferred.SELECT?.localized && definition['@cds.localized'] !== false
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/**
|
|
1719
|
+
* Get the csn definition of the target of a given association
|
|
1720
|
+
*
|
|
1721
|
+
* @param assoc
|
|
1722
|
+
* @returns the csn definition of the association target or null if it is not an association
|
|
1723
|
+
*/
|
|
1724
|
+
function assocTarget(assoc) {
|
|
1725
|
+
return model.definitions[assoc.target] || null
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
/**
|
|
1729
|
+
* Calculate the flat name for a deeply nested element:
|
|
1730
|
+
* @example `entity E { struct: { foo: String} }` => `getFullName(foo)` => `struct_foo`
|
|
1731
|
+
*
|
|
1732
|
+
* @param {CSN.element} node an element
|
|
1733
|
+
* @param {object} name the last part of the name, e.g. the name of the deeply nested element
|
|
1734
|
+
* @returns the flat name of the element
|
|
1735
|
+
*/
|
|
1736
|
+
function getFullName(node, name = node.name) {
|
|
1737
|
+
if (node.parent.kind === 'entity') return name
|
|
1738
|
+
|
|
1739
|
+
return getFullName(node.parent, `${node.parent.name}_${name}`)
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
/**
|
|
1743
|
+
* Calculates the name of the source which can be used to address the given node.
|
|
1744
|
+
*
|
|
1745
|
+
* @param {object} node a csn object with a `ref` and `$refLinks`
|
|
1746
|
+
* @param {object} $baseLink optional base `$refLink`, e.g. for infix filters.
|
|
1747
|
+
* For an infix filter, we must explicitly pass the TA name
|
|
1748
|
+
* because the first step of the ref might not be part of
|
|
1749
|
+
* the combined elements of the query
|
|
1750
|
+
* @returns the source name which can be used to address the node
|
|
1751
|
+
*/
|
|
1752
|
+
function getQuerySourceName(node, $baseLink = null) {
|
|
1753
|
+
if (!node || !node.$refLinks || !node.ref) {
|
|
1754
|
+
throw new Error('Invalid node')
|
|
1755
|
+
}
|
|
1756
|
+
if ($baseLink) {
|
|
1757
|
+
return getBaseLinkAlias($baseLink)
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
if (node.isJoinRelevant) {
|
|
1761
|
+
return getJoinRelevantAlias(node)
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
return getSelectOrEntityAlias(node) || getCombinedElementAlias(node)
|
|
1765
|
+
function getBaseLinkAlias($baseLink) {
|
|
1766
|
+
return $baseLink.alias
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
function getJoinRelevantAlias(node) {
|
|
1770
|
+
return [...node.$refLinks]
|
|
1771
|
+
.reverse()
|
|
1772
|
+
.find($refLink => $refLink.definition.isAssociation && !$refLink.onlyForeignKeyAccess).alias
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
function getSelectOrEntityAlias(node) {
|
|
1776
|
+
let firstRefLink = node.$refLinks[0].definition
|
|
1777
|
+
if (firstRefLink.SELECT || firstRefLink.kind === 'entity') {
|
|
1778
|
+
const firstStep = node.ref[0]
|
|
1779
|
+
/**
|
|
1780
|
+
* If the node.ref refers to an implicit alias which is later on changed by cqn4sql,
|
|
1781
|
+
* we need to replace the usage of the implicit alias, with the correct, auto-generated table alias.
|
|
1782
|
+
*
|
|
1783
|
+
* This is the case if the following holds true:
|
|
1784
|
+
* - the original query has NO explicit alias
|
|
1785
|
+
* - ref[0] equals the implicit alias of the query (i.e. from.ref[ from.length - 1 ].split('.').pop())
|
|
1786
|
+
* - but differs from the explicit alias, assigned by cqn4sql (i.e. <subquery>.from.uniqueSubqueryAlias)
|
|
1787
|
+
*/
|
|
1788
|
+
if (
|
|
1789
|
+
originalQuery.SELECT?.from.uniqueSubqueryAlias &&
|
|
1790
|
+
!originalQuery.SELECT?.from.as &&
|
|
1791
|
+
firstStep === getLastStringSegment(transformedQuery.SELECT.from.ref[0])
|
|
1792
|
+
) {
|
|
1793
|
+
return originalQuery.SELECT?.from.uniqueSubqueryAlias
|
|
1794
|
+
}
|
|
1795
|
+
return node.ref[0]
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function getCombinedElementAlias(node) {
|
|
1800
|
+
return getLastStringSegment(inferred.$combinedElements[node.ref[0].id || node.ref[0]][0].index)
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
module.exports = Object.assign(cqn4sql, {
|
|
1806
|
+
// for own tests only:
|
|
1807
|
+
eqOps,
|
|
1808
|
+
notEqOps,
|
|
1809
|
+
notSupportedOps,
|
|
1810
|
+
})
|
|
1811
|
+
|
|
1812
|
+
function copy(obj) {
|
|
1813
|
+
const walk = function (par, prop) {
|
|
1814
|
+
const val = prop ? par[prop] : par
|
|
1815
|
+
|
|
1816
|
+
// If value is native return
|
|
1817
|
+
if (typeof val !== 'object' || val == null || val instanceof RegExp || val instanceof Date || val instanceof Buffer)
|
|
1818
|
+
return val
|
|
1819
|
+
|
|
1820
|
+
const ret = Array.isArray(val) ? [] : {}
|
|
1821
|
+
Object.keys(val).forEach(k => {
|
|
1822
|
+
ret[k] = walk(val, k)
|
|
1823
|
+
})
|
|
1824
|
+
return ret
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
return walk(obj)
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
function hasLogicalOr(tokenStream) {
|
|
1831
|
+
return tokenStream.some(t => t in { OR: true, or: true })
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
/**
|
|
1835
|
+
* Returns the last segment of a string after the last dot.
|
|
1836
|
+
*
|
|
1837
|
+
* @param {string} str - The input string.
|
|
1838
|
+
* @returns {string} The last segment of the string after the last dot. If there is no dot in the string, the function returns the original string.
|
|
1839
|
+
*/
|
|
1840
|
+
function getLastStringSegment(str) {
|
|
1841
|
+
const index = str.lastIndexOf('.')
|
|
1842
|
+
return index != -1 ? str.substring(index + 1) : str
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
const idOnly = ref => ref.id || ref
|
|
1846
|
+
const is_regexp = x => x?.constructor?.name === 'RegExp' // NOTE: x instanceof RegExp doesn't work in repl
|