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