@cap-js/sqlite 0.2.0 → 1.0.1

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.
@@ -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