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