@cap-js/db-service 1.16.0 → 1.16.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.
- package/CHANGELOG.md +11 -0
- package/lib/common/DatabaseService.js +7 -6
- package/lib/common/generic-pool.js +7 -9
- package/lib/cqn2sql.js +20 -21
- package/lib/cqn4sql.js +89 -68
- package/lib/infer/index.js +618 -731
- package/package.json +1 -1
package/lib/infer/index.js
CHANGED
|
@@ -42,6 +42,8 @@ function infer(originalQuery, model) {
|
|
|
42
42
|
// cache for already processed calculated elements
|
|
43
43
|
const alreadySeenCalcElements = new Set()
|
|
44
44
|
|
|
45
|
+
let $combinedElements
|
|
46
|
+
|
|
45
47
|
const sources = inferTarget(_.from || _.into || _.entity, {})
|
|
46
48
|
const joinTree = new JoinTree(sources)
|
|
47
49
|
const aliases = Object.keys(sources)
|
|
@@ -62,21 +64,21 @@ function infer(originalQuery, model) {
|
|
|
62
64
|
},
|
|
63
65
|
})
|
|
64
66
|
if (originalQuery.SELECT || originalQuery.DELETE || originalQuery.UPDATE) {
|
|
65
|
-
|
|
67
|
+
$combinedElements = inferCombinedElements()
|
|
66
68
|
/**
|
|
67
69
|
* TODO: this function is currently only called on DELETE's
|
|
68
70
|
* because it correctly set's up the $refLink's in the
|
|
69
71
|
* where clause: This functionality should be pulled out
|
|
70
72
|
* of ´inferQueryElement()` as this is a subtle side effect
|
|
71
73
|
*/
|
|
72
|
-
const elements = inferQueryElements(
|
|
74
|
+
const elements = inferQueryElements()
|
|
73
75
|
Object.defineProperties(inferred, {
|
|
74
|
-
$combinedElements: { value: $combinedElements, writable: true },
|
|
75
|
-
elements: { value: elements, writable: true },
|
|
76
|
-
joinTree: { value: joinTree, writable: true }, // REVISIT: eliminate
|
|
76
|
+
$combinedElements: { value: $combinedElements, writable: true, configurable: true },
|
|
77
|
+
elements: { value: elements, writable: true, configurable: true },
|
|
78
|
+
joinTree: { value: joinTree, writable: true, configurable: true }, // REVISIT: eliminate
|
|
77
79
|
})
|
|
78
80
|
// also enrich original query -> writable because it may be inferred again
|
|
79
|
-
Object.defineProperty(originalQuery, 'elements', { value: elements, writable: true })
|
|
81
|
+
Object.defineProperty(originalQuery, 'elements', { value: elements, writable: true, configurable: true })
|
|
80
82
|
}
|
|
81
83
|
return inferred
|
|
82
84
|
|
|
@@ -112,7 +114,7 @@ function infer(originalQuery, model) {
|
|
|
112
114
|
if (target.kind !== 'entity' && !target.isAssociation)
|
|
113
115
|
throw new Error('Query source must be a an entity or an association')
|
|
114
116
|
|
|
115
|
-
|
|
117
|
+
inferArg(from, null, null, { inFrom: true })
|
|
116
118
|
const alias =
|
|
117
119
|
from.uniqueSubqueryAlias ||
|
|
118
120
|
from.as ||
|
|
@@ -139,116 +141,6 @@ function infer(originalQuery, model) {
|
|
|
139
141
|
return querySources
|
|
140
142
|
}
|
|
141
143
|
|
|
142
|
-
// REVISIT: this helper is doing by far too much, with too many side effects
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* This function recursively traverses through all 'ref' steps of the 'arg' object and enriches it by attaching
|
|
146
|
-
* additional information. For each 'ref' step, it adds the corresponding definition and the target in which the
|
|
147
|
-
* next 'ref' step should be looked up.
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* @param {object} arg - The argument object that will be augmented with additional properties.
|
|
151
|
-
* It must contain a 'ref' property, which is an array representing the steps to be processed.
|
|
152
|
-
* Optionally, it can also contain an 'xpr' property, which is also processed recursively.
|
|
153
|
-
*
|
|
154
|
-
* @param {object} $baseLink - Optional parameter. It represents the environment in which the first 'ref' step should be
|
|
155
|
-
* resolved. It's needed for infix filter / expand columns. It must contain a 'definition'
|
|
156
|
-
* property, which is an object representing the base environment.
|
|
157
|
-
*
|
|
158
|
-
* @param {boolean} expandOrExists - Optional parameter, defaults to false. It indicates whether the 'arg' is part of a
|
|
159
|
-
* 'column.expand' or preceded by an 'exists'. When true, unmanaged association paths
|
|
160
|
-
* are allowed -> $baseLink is an `expand` or `assoc` preceded by `exists`.
|
|
161
|
-
*
|
|
162
|
-
* @throws Will throw an error if a 'ref' step cannot be found in the current environment or if a 'ref' step
|
|
163
|
-
* represents an unmanaged association in the case of infix filters and 'expandOrExists' is false.
|
|
164
|
-
*
|
|
165
|
-
* @returns {void} This function does not return a value; it mutates the 'arg' object directly.
|
|
166
|
-
*/
|
|
167
|
-
function attachRefLinksToArg(arg, $baseLink = null, expandOrExists = false) {
|
|
168
|
-
const { ref, xpr, args, list } = arg
|
|
169
|
-
if (xpr) xpr.forEach(t => attachRefLinksToArg(t, $baseLink, expandOrExists))
|
|
170
|
-
if (args) applyToFunctionArgs(args, attachRefLinksToArg, [$baseLink, expandOrExists])
|
|
171
|
-
if (list) list.forEach(arg => attachRefLinksToArg(arg, $baseLink, expandOrExists))
|
|
172
|
-
if (!ref) return
|
|
173
|
-
init$refLinks(arg)
|
|
174
|
-
let i = 0
|
|
175
|
-
let pseudoPath = false
|
|
176
|
-
for (const step of ref) {
|
|
177
|
-
const id = step.id || step
|
|
178
|
-
if (i === 0) {
|
|
179
|
-
if (id in pseudos.elements) {
|
|
180
|
-
// pseudo path
|
|
181
|
-
arg.$refLinks.push({ definition: pseudos.elements[id], target: pseudos })
|
|
182
|
-
pseudoPath = true // only first path step must be well defined
|
|
183
|
-
} else if ($baseLink) {
|
|
184
|
-
// infix filter never have table alias
|
|
185
|
-
// we need to search for first step in ´model.definitions[infixAlias]`
|
|
186
|
-
const { definition } = $baseLink
|
|
187
|
-
const elements = getDefinition(definition.target)?.elements || definition.elements
|
|
188
|
-
const e = elements?.[id] || cds.error`"${id}" not found in the elements of "${definition.name}"`
|
|
189
|
-
if (e.target) {
|
|
190
|
-
// only fk access in infix filter
|
|
191
|
-
const nextStep = ref[1]?.id || ref[1]
|
|
192
|
-
if (isNonForeignKeyNavigation(e, nextStep)) {
|
|
193
|
-
if (expandOrExists) {
|
|
194
|
-
Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
|
|
195
|
-
} else {
|
|
196
|
-
rejectNonFkNavigation(e, e.on ? $baseLink.definition.name : nextStep)
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
arg.$refLinks.push({ definition: e, target: definition })
|
|
201
|
-
// filter paths are flattened
|
|
202
|
-
// REVISIT: too much augmentation -> better remove flatName..
|
|
203
|
-
Object.defineProperty(arg, 'flatName', { value: ref.join('_'), writable: true })
|
|
204
|
-
} else {
|
|
205
|
-
// must be in model.definitions
|
|
206
|
-
const definition = getDefinition(id) || cds.error`"${id}" not found in the definitions of your model`
|
|
207
|
-
arg.$refLinks[0] = { definition, target: definition }
|
|
208
|
-
}
|
|
209
|
-
} else if (arg.ref[0] === '$user' && pseudoPath) {
|
|
210
|
-
// `$user.some.unknown.element` -> no error
|
|
211
|
-
arg.$refLinks.push({ definition: {}, target: pseudos })
|
|
212
|
-
} else {
|
|
213
|
-
const recent = arg.$refLinks[i - 1]
|
|
214
|
-
const { elements } = getDefinition(recent.definition.target) || recent.definition
|
|
215
|
-
const e = elements[id]
|
|
216
|
-
const notFoundIn = pseudoPath ? arg.ref[i - 1] : getFullPathForLinkedArg(arg)
|
|
217
|
-
if (!e) throw new Error(`"${id}" not found in the elements of "${notFoundIn}"`)
|
|
218
|
-
arg.$refLinks.push({ definition: e, target: getDefinition(e.target) || e })
|
|
219
|
-
}
|
|
220
|
-
arg.$refLinks[i].alias = !ref[i + 1] && arg.as ? arg.as : id.split('.').pop()
|
|
221
|
-
|
|
222
|
-
// link refs in where
|
|
223
|
-
if (step.where) {
|
|
224
|
-
// REVISIT: why do we need to walk through these so early?
|
|
225
|
-
if (arg.$refLinks[i].definition.kind === 'entity' || getDefinition(arg.$refLinks[i].definition.target)) {
|
|
226
|
-
let existsPredicate = false
|
|
227
|
-
const walkTokenStream = token => {
|
|
228
|
-
if (token === 'exists') {
|
|
229
|
-
// no joins for infix filters along `exists <path>`
|
|
230
|
-
existsPredicate = true
|
|
231
|
-
} else if (token.xpr) {
|
|
232
|
-
// don't miss an exists within an expression
|
|
233
|
-
token.xpr.forEach(walkTokenStream)
|
|
234
|
-
} else {
|
|
235
|
-
attachRefLinksToArg(token, arg.$refLinks[i], existsPredicate || expandOrExists)
|
|
236
|
-
existsPredicate = false
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
step.where.forEach(walkTokenStream)
|
|
240
|
-
} else throw new Error('A filter can only be provided when navigating along associations')
|
|
241
|
-
}
|
|
242
|
-
i += 1
|
|
243
|
-
}
|
|
244
|
-
if ($baseLink?.pathExpressionInsideFilter) Object.defineProperty(arg, 'join', { value: 'inner' })
|
|
245
|
-
const { definition, target } = arg.$refLinks[arg.$refLinks.length - 1]
|
|
246
|
-
if (definition.value) {
|
|
247
|
-
// nested calculated element
|
|
248
|
-
attachRefLinksToArg(definition.value, { definition: definition.parent, target }, true)
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
144
|
/**
|
|
253
145
|
* Calculates the `$combinedElements` based on the provided queries `sources`.
|
|
254
146
|
* The `$combinedElements` of a query consist of all accessible elements across all
|
|
@@ -299,11 +191,11 @@ function infer(originalQuery, model) {
|
|
|
299
191
|
* to an array of objects containing the index and table alias where the element can be found.
|
|
300
192
|
* @returns {object} The inferred `elements` dictionary of the query, which maps element names to their corresponding definitions.
|
|
301
193
|
*/
|
|
302
|
-
function inferQueryElements(
|
|
194
|
+
function inferQueryElements() {
|
|
303
195
|
let queryElements = {}
|
|
304
196
|
const { columns, where, groupBy, having, orderBy } = _
|
|
305
197
|
if (!columns) {
|
|
306
|
-
inferElementsFromWildCard(
|
|
198
|
+
inferElementsFromWildCard(queryElements)
|
|
307
199
|
} else {
|
|
308
200
|
let wildcardSelect = false
|
|
309
201
|
const dollarSelfRefs = []
|
|
@@ -315,12 +207,12 @@ function infer(originalQuery, model) {
|
|
|
315
207
|
if (as === undefined) cds.error`Expecting expression to have an alias name`
|
|
316
208
|
if (queryElements[as]) cds.error`Duplicate definition of element “${as}”`
|
|
317
209
|
if (col.xpr || col.SELECT) {
|
|
318
|
-
queryElements[as] = getElementForXprOrSubquery(col)
|
|
210
|
+
queryElements[as] = getElementForXprOrSubquery(col, queryElements)
|
|
319
211
|
}
|
|
320
212
|
if (col.func) {
|
|
321
213
|
if (col.args) {
|
|
322
214
|
// {func}.args are optional
|
|
323
|
-
applyToFunctionArgs(col.args,
|
|
215
|
+
applyToFunctionArgs(col.args, inferArg, [false])
|
|
324
216
|
}
|
|
325
217
|
queryElements[as] = getElementForCast(col)
|
|
326
218
|
}
|
|
@@ -340,7 +232,7 @@ function infer(originalQuery, model) {
|
|
|
340
232
|
if (firstStepIsSelf) dollarSelfRefs.push(col)
|
|
341
233
|
else handleRef(col)
|
|
342
234
|
} else if (col.expand) {
|
|
343
|
-
|
|
235
|
+
inferArg(col, queryElements, null)
|
|
344
236
|
} else {
|
|
345
237
|
cds.error`Not supported: ${JSON.stringify(col)}`
|
|
346
238
|
}
|
|
@@ -348,7 +240,7 @@ function infer(originalQuery, model) {
|
|
|
348
240
|
|
|
349
241
|
if (dollarSelfRefs.length) inferDollarSelfRefs(dollarSelfRefs)
|
|
350
242
|
|
|
351
|
-
if (wildcardSelect) inferElementsFromWildCard(
|
|
243
|
+
if (wildcardSelect) inferElementsFromWildCard(queryElements)
|
|
352
244
|
}
|
|
353
245
|
if (orderBy) {
|
|
354
246
|
// link $refLinks -> special name resolution rules for orderBy
|
|
@@ -376,7 +268,7 @@ function infer(originalQuery, model) {
|
|
|
376
268
|
$baseLink = null
|
|
377
269
|
}
|
|
378
270
|
|
|
379
|
-
|
|
271
|
+
inferArg(token, queryElements, $baseLink, { inQueryModifier: true })
|
|
380
272
|
if (token.isJoinRelevant && rejectJoinRelevantPath) {
|
|
381
273
|
// reverse the array, find the last association and calculate the index of the association in non-reversed order
|
|
382
274
|
const assocIndex =
|
|
@@ -390,12 +282,12 @@ function infer(originalQuery, model) {
|
|
|
390
282
|
}
|
|
391
283
|
|
|
392
284
|
// walk over all paths in other query properties
|
|
393
|
-
if (where) walkTokenStream(where)
|
|
394
|
-
if (groupBy) groupBy
|
|
285
|
+
if (where) walkTokenStream(where, true)
|
|
286
|
+
if (groupBy) walkTokenStream(groupBy)
|
|
395
287
|
if (having) walkTokenStream(having)
|
|
396
288
|
if (_.with)
|
|
397
289
|
// consider UPDATE.with
|
|
398
|
-
Object.values(_.with).forEach(val =>
|
|
290
|
+
Object.values(_.with).forEach(val => inferArg(val, queryElements, null, { inExpr: true }))
|
|
399
291
|
|
|
400
292
|
return queryElements
|
|
401
293
|
|
|
@@ -407,7 +299,7 @@ function infer(originalQuery, model) {
|
|
|
407
299
|
*
|
|
408
300
|
* @param {array} tokenStream
|
|
409
301
|
*/
|
|
410
|
-
function walkTokenStream(tokenStream) {
|
|
302
|
+
function walkTokenStream(tokenStream, inExpr = false) {
|
|
411
303
|
let skipJoins
|
|
412
304
|
const processToken = t => {
|
|
413
305
|
if (t === 'exists') {
|
|
@@ -417,7 +309,7 @@ function infer(originalQuery, model) {
|
|
|
417
309
|
// don't miss an exists within an expression
|
|
418
310
|
t.xpr.forEach(processToken)
|
|
419
311
|
} else {
|
|
420
|
-
|
|
312
|
+
inferArg(t, queryElements, null, { inExists: skipJoins, inExpr, inQueryModifier: true })
|
|
421
313
|
skipJoins = false
|
|
422
314
|
}
|
|
423
315
|
}
|
|
@@ -460,7 +352,7 @@ function infer(originalQuery, model) {
|
|
|
460
352
|
}
|
|
461
353
|
|
|
462
354
|
function handleRef(col) {
|
|
463
|
-
|
|
355
|
+
inferArg(col, queryElements)
|
|
464
356
|
const { definition } = col.$refLinks[col.$refLinks.length - 1]
|
|
465
357
|
if (col.cast)
|
|
466
358
|
// final type overwritten -> element not visible anymore
|
|
@@ -470,148 +362,91 @@ function infer(originalQuery, model) {
|
|
|
470
362
|
setElementOnColumns(col, queryElements[col.as || '$user'])
|
|
471
363
|
else setElementOnColumns(col, definition)
|
|
472
364
|
}
|
|
365
|
+
}
|
|
473
366
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
function inferQueryElement(column, insertIntoQueryElements = true, $baseLink = null, context) {
|
|
511
|
-
const { inExists, inExpr, inCalcElement, baseColumn, inInfixFilter } = context || {}
|
|
512
|
-
if (column.param || column.SELECT) return // parameter references are only resolved into values on execution e.g. :val, :1 or ?
|
|
513
|
-
if (column.args) {
|
|
514
|
-
applyToFunctionArgs(column.args, inferQueryElement, [false, $baseLink, context])
|
|
515
|
-
}
|
|
516
|
-
if (column.list) column.list.forEach(arg => inferQueryElement(arg, false, $baseLink, context))
|
|
517
|
-
if (column.xpr)
|
|
518
|
-
column.xpr.forEach(token => inferQueryElement(token, false, $baseLink, { ...context, inExpr: true })) // e.g. function in expression
|
|
519
|
-
|
|
520
|
-
if (!column.ref) {
|
|
521
|
-
if (column.expand) queryElements[column.as] = resolveExpand(column)
|
|
522
|
-
return
|
|
523
|
-
}
|
|
367
|
+
/**
|
|
368
|
+
* This function is responsible for inferring a query element based on a provided column.
|
|
369
|
+
* It initializes and attaches a non-enumerable `$refLinks` property to the column,
|
|
370
|
+
* which stores an array of objects that represent the corresponding artifact of the ref step.
|
|
371
|
+
* Each object in the `$refLinks` array corresponds to the same index position in the `column.ref` array.
|
|
372
|
+
* Based on the leaf artifact (last object in the `$refLinks` array), the query element is inferred.
|
|
373
|
+
*
|
|
374
|
+
* @param {object} arg - The column object that contains the properties to infer a query element.
|
|
375
|
+
* @param {boolean} [queryElements=true] - Determines whether the inferred element should be inserted into the queries elements.
|
|
376
|
+
* For instance, it's set to false when walking over the where clause.
|
|
377
|
+
* @param {object} [$baseLink=null] - A base reference link, usually it's an object with a definition and a target.
|
|
378
|
+
* Used for infix filters, exists <assoc> and nested projections.
|
|
379
|
+
* @param {object} [context={}] - Contextual information for element inference.
|
|
380
|
+
* @param {boolean} [context.inExists=false] - Flag to control the creation of joins for non-association path traversals.
|
|
381
|
+
* for `exists <assoc>` paths we do not need to create joins for path expressions as they are part of the semi-joined subquery.
|
|
382
|
+
* @param {boolean} [context.inExpr=false] - Flag to signal whether the element is part of an expression.
|
|
383
|
+
* Used to ignore non-persisted elements.
|
|
384
|
+
* @param {boolean} [context.inNestedProjection=false] - Flag to signal whether the element is part of a nested projection.
|
|
385
|
+
*
|
|
386
|
+
* Note:
|
|
387
|
+
* - `inExists` is used to specify cases where no joins should be created for non-association path traversals.
|
|
388
|
+
* It is primarily used for infix filters in `exists assoc[parent.foo='bar']`, where it becomes part of a semi-join.
|
|
389
|
+
* - Columns with a `param` property are parameter references resolved into values only at execution time.
|
|
390
|
+
* - Columns with an `args` property are function calls in expressions.
|
|
391
|
+
* - Columns with a `list` property represent a list of values (e.g., for the IN operator).
|
|
392
|
+
* - Columns with a `SELECT` property represent subqueries.
|
|
393
|
+
*
|
|
394
|
+
* @throws {Error} If an unmanaged association is found in an infix filter path, an error is thrown.
|
|
395
|
+
* @throws {Error} If a non-foreign key traversal is found in an infix filter, an error is thrown.
|
|
396
|
+
* @throws {Error} If a first step is not found in the combined elements, an error is thrown.
|
|
397
|
+
* @throws {Error} If a filter is provided while navigating along non-associations, an error is thrown.
|
|
398
|
+
* @throws {Error} If the same element name is inferred more than once, an error is thrown.
|
|
399
|
+
*
|
|
400
|
+
* @returns {void}
|
|
401
|
+
*/
|
|
524
402
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
!firstStepIsTableAlias && column.ref.length > 1 && ['$self', '$projection'].includes(column.ref[0])
|
|
532
|
-
const expandOnTableAlias = column.ref.length === 1 && column.ref[0] in sources && (column.expand || column.inline)
|
|
533
|
-
const nameSegments = []
|
|
534
|
-
// if a (segment) of a (structured) foreign key is renamed, we must not include
|
|
535
|
-
// the aliased ref segments into the name of the final foreign key which is e.g. used in
|
|
536
|
-
// on conditions of joins
|
|
537
|
-
const skipAliasedFkSegmentsOfNameStack = []
|
|
538
|
-
let pseudoPath = false
|
|
539
|
-
column.ref.forEach((step, i) => {
|
|
540
|
-
const id = step.id || step
|
|
541
|
-
if (i === 0) {
|
|
542
|
-
if (id in pseudos.elements) {
|
|
543
|
-
// pseudo path
|
|
544
|
-
column.$refLinks.push({ definition: pseudos.elements[id], target: pseudos })
|
|
545
|
-
pseudoPath = true // only first path step must be well defined
|
|
546
|
-
nameSegments.push(id)
|
|
547
|
-
} else if ($baseLink) {
|
|
548
|
-
const { definition, target } = $baseLink
|
|
549
|
-
const elements = getDefinition(definition.target)?.elements || definition.elements
|
|
550
|
-
if (elements && id in elements) {
|
|
551
|
-
const element = elements[id]
|
|
552
|
-
if (inInfixFilter) {
|
|
553
|
-
const nextStep = column.ref[1]?.id || column.ref[1]
|
|
554
|
-
if (isNonForeignKeyNavigation(element, nextStep)) {
|
|
555
|
-
if (inExists) {
|
|
556
|
-
Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
|
|
557
|
-
} else {
|
|
558
|
-
rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
const resolvableIn = getDefinition(definition.target) || target
|
|
563
|
-
const $refLink = { definition: elements[id], target: resolvableIn }
|
|
564
|
-
column.$refLinks.push($refLink)
|
|
565
|
-
} else {
|
|
566
|
-
stepNotFoundInPredecessor(id, definition.name)
|
|
567
|
-
}
|
|
568
|
-
nameSegments.push(id)
|
|
569
|
-
} else if (firstStepIsTableAlias) {
|
|
570
|
-
column.$refLinks.push({
|
|
571
|
-
definition: getDefinitionFromSources(sources, id),
|
|
572
|
-
target: getDefinitionFromSources(sources, id),
|
|
573
|
-
})
|
|
574
|
-
} else if (firstStepIsSelf) {
|
|
575
|
-
column.$refLinks.push({ definition: { elements: queryElements }, target: { elements: queryElements } })
|
|
576
|
-
} else if (column.ref.length > 1 && inferred.outerQueries?.find(outer => id in outer.sources)) {
|
|
577
|
-
// outer query accessed via alias
|
|
578
|
-
const outerAlias = inferred.outerQueries.find(outer => id in outer.sources)
|
|
579
|
-
column.$refLinks.push({
|
|
580
|
-
definition: getDefinitionFromSources(outerAlias.sources, id),
|
|
581
|
-
target: getDefinitionFromSources(outerAlias.sources, id),
|
|
582
|
-
})
|
|
583
|
-
} else if (id in $combinedElements) {
|
|
584
|
-
if ($combinedElements[id].length > 1) stepIsAmbiguous(id) // exit
|
|
585
|
-
const definition = $combinedElements[id][0].tableAlias.elements[id]
|
|
586
|
-
const $refLink = { definition, target: $combinedElements[id][0].tableAlias }
|
|
587
|
-
column.$refLinks.push($refLink)
|
|
588
|
-
nameSegments.push(id)
|
|
589
|
-
} else if (expandOnTableAlias) {
|
|
590
|
-
// expand on table alias
|
|
591
|
-
column.$refLinks.push({
|
|
592
|
-
definition: getDefinitionFromSources(sources, id),
|
|
593
|
-
target: getDefinitionFromSources(sources, id),
|
|
594
|
-
})
|
|
595
|
-
} else {
|
|
596
|
-
stepNotFoundInCombinedElements(id) // REVISIT: fails with {__proto__:elements)
|
|
597
|
-
}
|
|
598
|
-
} else {
|
|
599
|
-
const { definition } = column.$refLinks[i - 1]
|
|
600
|
-
const elements = getDefinition(definition.target)?.elements || definition.elements //> go for assoc._target first, instead of assoc as struct
|
|
601
|
-
const element = elements?.[id]
|
|
403
|
+
function inferArg(arg, queryElements = null, $baseLink = null, context = {}) {
|
|
404
|
+
const { inExists, inExpr, inCalcElement, baseColumn, inInfixFilter, inQueryModifier, inFrom } = context
|
|
405
|
+
if (arg.param || arg.SELECT) return // parameter references are only resolved into values on execution e.g. :val, :1 or ?
|
|
406
|
+
if (arg.args) applyToFunctionArgs(arg.args, inferArg, [null, $baseLink, context])
|
|
407
|
+
if (arg.list) arg.list.forEach(arg => inferArg(arg, null, $baseLink, context))
|
|
408
|
+
if (arg.xpr) arg.xpr.forEach(token => inferArg(token, queryElements, $baseLink, { ...context, inExpr: true })) // e.g. function in expression
|
|
602
409
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
.join(', ')} ]`,
|
|
608
|
-
)
|
|
609
|
-
}
|
|
410
|
+
if (!arg.ref) {
|
|
411
|
+
if (arg.expand && queryElements) queryElements[arg.as] = resolveExpand(arg)
|
|
412
|
+
return
|
|
413
|
+
}
|
|
610
414
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
415
|
+
// initialize $refLinks
|
|
416
|
+
Object.defineProperty(arg, '$refLinks', {
|
|
417
|
+
value: [],
|
|
418
|
+
writable: true,
|
|
419
|
+
})
|
|
420
|
+
// if any path step points to an artifact with `@cds.persistence.skip`
|
|
421
|
+
// we must ignore the element from the queries elements
|
|
422
|
+
let isPersisted = true
|
|
423
|
+
let firstStepIsTableAlias, firstStepIsSelf, expandOnTableAlias
|
|
424
|
+
if (!inFrom) {
|
|
425
|
+
firstStepIsTableAlias = arg.ref.length > 1 && arg.ref[0] in sources
|
|
426
|
+
firstStepIsSelf = !firstStepIsTableAlias && arg.ref.length > 1 && ['$self', '$projection'].includes(arg.ref[0])
|
|
427
|
+
expandOnTableAlias = arg.ref.length === 1 && arg.ref[0] in sources && (arg.expand || arg.inline)
|
|
428
|
+
}
|
|
429
|
+
const nameSegments = []
|
|
430
|
+
// if a (segment) of a (structured) foreign key is renamed, we must not include
|
|
431
|
+
// the aliased ref segments into the name of the final foreign key which is e.g. used in
|
|
432
|
+
// on conditions of joins
|
|
433
|
+
const skipAliasedFkSegmentsOfNameStack = []
|
|
434
|
+
let pseudoPath = false
|
|
435
|
+
arg.ref.forEach((step, i) => {
|
|
436
|
+
const id = step.id || step
|
|
437
|
+
if (i === 0) {
|
|
438
|
+
if (id in pseudos.elements) {
|
|
439
|
+
// pseudo path
|
|
440
|
+
arg.$refLinks.push({ definition: pseudos.elements[id], target: pseudos })
|
|
441
|
+
pseudoPath = true // only first path step must be well defined
|
|
442
|
+
nameSegments.push(id)
|
|
443
|
+
} else if ($baseLink) {
|
|
444
|
+
const { definition, target } = $baseLink
|
|
445
|
+
const elements = getDefinition(definition.target)?.elements || definition.elements
|
|
446
|
+
if (elements && id in elements) {
|
|
447
|
+
const element = elements[id]
|
|
448
|
+
if (inInfixFilter) {
|
|
449
|
+
const nextStep = arg.ref[1]?.id || arg.ref[1]
|
|
615
450
|
if (isNonForeignKeyNavigation(element, nextStep)) {
|
|
616
451
|
if (inExists) {
|
|
617
452
|
Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
|
|
@@ -620,529 +455,589 @@ function infer(originalQuery, model) {
|
|
|
620
455
|
}
|
|
621
456
|
}
|
|
622
457
|
}
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
stepNotFoundInColumnList(id)
|
|
627
|
-
} else if (column.ref[0] === '$user' && pseudoPath) {
|
|
628
|
-
// `$user.some.unknown.element` -> no error
|
|
629
|
-
column.$refLinks.push({ definition: {}, target })
|
|
630
|
-
} else if (id === '$dummy') {
|
|
631
|
-
// `some.known.element.$dummy` -> no error; used by cds.ql to simulate joins
|
|
632
|
-
column.$refLinks.push({ definition: { name: '$dummy', parent: column.$refLinks[i - 1].target } })
|
|
633
|
-
Object.defineProperty(column, 'isJoinRelevant', { value: true })
|
|
458
|
+
const resolvableIn = getDefinition(definition.target) || target
|
|
459
|
+
const $refLink = { definition: elements[id], target: resolvableIn }
|
|
460
|
+
arg.$refLinks.push($refLink)
|
|
634
461
|
} else {
|
|
635
|
-
|
|
636
|
-
stepNotFoundInPredecessor(id, notFoundIn)
|
|
637
|
-
}
|
|
638
|
-
const foreignKeyAlias = Array.isArray(definition.keys)
|
|
639
|
-
? definition.keys.find(k => {
|
|
640
|
-
if (k.ref.every((step, j) => column.ref[i + j] === step)) {
|
|
641
|
-
skipAliasedFkSegmentsOfNameStack.push(...k.ref.slice(1))
|
|
642
|
-
return true
|
|
643
|
-
}
|
|
644
|
-
return false
|
|
645
|
-
})?.as
|
|
646
|
-
: null
|
|
647
|
-
if (foreignKeyAlias) nameSegments.push(foreignKeyAlias)
|
|
648
|
-
else if (skipAliasedFkSegmentsOfNameStack[0] === id) skipAliasedFkSegmentsOfNameStack.shift()
|
|
649
|
-
else {
|
|
650
|
-
nameSegments.push(firstStepIsSelf && i === 1 ? element.__proto__.name : id)
|
|
462
|
+
stepNotFoundInPredecessor(id, definition.name)
|
|
651
463
|
}
|
|
464
|
+
nameSegments.push(id)
|
|
465
|
+
} else if (inFrom) {
|
|
466
|
+
const definition = getDefinition(id) || cds.error`"${id}" not found in the definitions of your model`
|
|
467
|
+
arg.$refLinks.push({ definition, target: definition })
|
|
468
|
+
} else if (firstStepIsTableAlias) {
|
|
469
|
+
arg.$refLinks.push({
|
|
470
|
+
definition: getDefinitionFromSources(sources, id),
|
|
471
|
+
target: getDefinitionFromSources(sources, id),
|
|
472
|
+
})
|
|
473
|
+
} else if (firstStepIsSelf) {
|
|
474
|
+
arg.$refLinks.push({ definition: { elements: queryElements }, target: { elements: queryElements } })
|
|
475
|
+
} else if (arg.ref.length > 1 && inferred.outerQueries?.find(outer => id in outer.sources)) {
|
|
476
|
+
// outer query accessed via alias
|
|
477
|
+
const outerAlias = inferred.outerQueries.find(outer => id in outer.sources)
|
|
478
|
+
arg.$refLinks.push({
|
|
479
|
+
definition: getDefinitionFromSources(outerAlias.sources, id),
|
|
480
|
+
target: getDefinitionFromSources(outerAlias.sources, id),
|
|
481
|
+
})
|
|
482
|
+
} else if (id in $combinedElements) {
|
|
483
|
+
if ($combinedElements[id].length > 1) stepIsAmbiguous(id) // exit
|
|
484
|
+
const definition = $combinedElements[id][0].tableAlias.elements[id]
|
|
485
|
+
const $refLink = { definition, target: $combinedElements[id][0].tableAlias }
|
|
486
|
+
arg.$refLinks.push($refLink)
|
|
487
|
+
nameSegments.push(id)
|
|
488
|
+
} else if (expandOnTableAlias) {
|
|
489
|
+
// expand on table alias
|
|
490
|
+
arg.$refLinks.push({
|
|
491
|
+
definition: getDefinitionFromSources(sources, id),
|
|
492
|
+
target: getDefinitionFromSources(sources, id),
|
|
493
|
+
})
|
|
494
|
+
} else {
|
|
495
|
+
stepNotFoundInCombinedElements(id) // REVISIT: fails with {__proto__:elements)
|
|
652
496
|
}
|
|
497
|
+
} else {
|
|
498
|
+
const { definition } = arg.$refLinks[i - 1]
|
|
499
|
+
const elements = getDefinition(definition.target)?.elements || definition.elements //> go for assoc._target first, instead of assoc as struct
|
|
500
|
+
const element = elements?.[id]
|
|
653
501
|
|
|
654
|
-
if (
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
step.where.forEach(token => {
|
|
661
|
-
if (token === 'exists') {
|
|
662
|
-
// books[exists genre[code='A']].title --> column is join relevant but inner exists filter is not
|
|
663
|
-
skipJoinsForFilter = true
|
|
664
|
-
} else if (token.ref || token.xpr) {
|
|
665
|
-
inferQueryElement(token, false, column.$refLinks[i], {
|
|
666
|
-
inExists: skipJoinsForFilter || inExists,
|
|
667
|
-
inExpr: !!token.xpr,
|
|
668
|
-
inInfixFilter: true,
|
|
669
|
-
})
|
|
670
|
-
} else if (token.func) {
|
|
671
|
-
if (token.args) {
|
|
672
|
-
applyToFunctionArgs(token.args, inferQueryElement, [
|
|
673
|
-
false,
|
|
674
|
-
column.$refLinks[i],
|
|
675
|
-
{ inExists: skipJoinsForFilter || inExists, inExpr: true, inInfixFilter: true },
|
|
676
|
-
])
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
})
|
|
502
|
+
if (firstStepIsSelf && element?.isAssociation) {
|
|
503
|
+
throw new Error(
|
|
504
|
+
`Paths starting with “$self” must not contain steps of type “cds.Association”: ref: [ ${arg.ref
|
|
505
|
+
.map(idOnly)
|
|
506
|
+
.join(', ')} ]`,
|
|
507
|
+
)
|
|
680
508
|
}
|
|
681
509
|
|
|
682
|
-
|
|
683
|
-
if (
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (column.cast) {
|
|
690
|
-
const base = getElementForCast(column)
|
|
691
|
-
if (insertIntoQueryElements) queryElements[column.as || flatName] = getCopyWithAnnos(column, base)
|
|
692
|
-
} else if (column.expand) {
|
|
693
|
-
const elements = resolveExpand(column)
|
|
694
|
-
let elementName
|
|
695
|
-
// expand on table alias
|
|
696
|
-
if (column.$refLinks.length === 1 && column.$refLinks[0].definition.kind === 'entity')
|
|
697
|
-
elementName = column.$refLinks[0].alias
|
|
698
|
-
else elementName = column.as || flatName
|
|
699
|
-
if (insertIntoQueryElements) queryElements[elementName] = elements
|
|
700
|
-
} else if (column.inline && insertIntoQueryElements) {
|
|
701
|
-
const elements = resolveInline(column)
|
|
702
|
-
queryElements = { ...queryElements, ...elements }
|
|
703
|
-
} else {
|
|
704
|
-
// shortcut for `ref: ['$user']` -> `ref: ['$user', 'id']`
|
|
705
|
-
const leafArt =
|
|
706
|
-
i === 0 && id === '$user' ? column.$refLinks[i].definition.elements.id : column.$refLinks[i].definition
|
|
707
|
-
// infer element based on leaf artifact of path
|
|
708
|
-
if (insertIntoQueryElements) {
|
|
709
|
-
let elementName
|
|
710
|
-
if (column.as) {
|
|
711
|
-
elementName = column.as
|
|
510
|
+
const target = getDefinition(definition.target) || arg.$refLinks[i - 1].target
|
|
511
|
+
if (element) {
|
|
512
|
+
if ($baseLink && inInfixFilter) {
|
|
513
|
+
const nextStep = arg.ref[i + 1]?.id || arg.ref[i + 1]
|
|
514
|
+
if (isNonForeignKeyNavigation(element, nextStep)) {
|
|
515
|
+
if (inExists) {
|
|
516
|
+
Object.defineProperty($baseLink, 'pathExpressionInsideFilter', { value: true })
|
|
712
517
|
} else {
|
|
713
|
-
|
|
714
|
-
// the inferred name of the element equals the flat version of the user-written ref.
|
|
715
|
-
const refNavigation = column.ref
|
|
716
|
-
.slice(firstStepIsSelf || firstStepIsTableAlias ? 1 : 0)
|
|
717
|
-
.map(idOnly)
|
|
718
|
-
.join('_')
|
|
719
|
-
if (refNavigation !== flatName) elementName = refNavigation
|
|
720
|
-
else elementName = flatName
|
|
518
|
+
rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
|
|
721
519
|
}
|
|
722
|
-
if (queryElements[elementName] !== undefined)
|
|
723
|
-
throw new Error(`Duplicate definition of element “${elementName}”`)
|
|
724
|
-
const element = getCopyWithAnnos(column, leafArt)
|
|
725
|
-
queryElements[elementName] = element
|
|
726
520
|
}
|
|
727
521
|
}
|
|
522
|
+
const $refLink = { definition: elements[id], target }
|
|
523
|
+
arg.$refLinks.push($refLink)
|
|
524
|
+
} else if (firstStepIsSelf) {
|
|
525
|
+
stepNotFoundInColumnList(id)
|
|
526
|
+
} else if (arg.ref[0] === '$user' && pseudoPath) {
|
|
527
|
+
// `$user.some.unknown.element` -> no error
|
|
528
|
+
arg.$refLinks.push({ definition: {}, target })
|
|
529
|
+
} else if (id === '$dummy') {
|
|
530
|
+
// `some.known.element.$dummy` -> no error; used by cds.ql to simulate joins
|
|
531
|
+
arg.$refLinks.push({ definition: { name: '$dummy', parent: arg.$refLinks[i - 1].target } })
|
|
532
|
+
Object.defineProperty(arg, 'isJoinRelevant', { value: true })
|
|
533
|
+
} else {
|
|
534
|
+
const notFoundIn = pseudoPath ? arg.ref[i - 1] : getFullPathForLinkedArg(arg)
|
|
535
|
+
stepNotFoundInPredecessor(id, notFoundIn)
|
|
728
536
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
if (
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
}
|
|
743
|
-
const leafArt = column.$refLinks[column.$refLinks.length - 1].definition
|
|
744
|
-
const virtual = (leafArt.virtual || !isPersisted) && !inExpr
|
|
745
|
-
// check if we need to merge the column `ref` into the join tree of the query
|
|
746
|
-
if (!inExists && !virtual && !inCalcElement) {
|
|
747
|
-
// for a ref inside an `inline` we need to consider the column `ref` which has the `inline` prop
|
|
748
|
-
const colWithBase = baseColumn
|
|
749
|
-
? { ref: [...baseColumn.ref, ...column.ref], $refLinks: [...baseColumn.$refLinks, ...column.$refLinks] }
|
|
750
|
-
: column
|
|
751
|
-
if (isColumnJoinRelevant(colWithBase)) {
|
|
752
|
-
Object.defineProperty(column, 'isJoinRelevant', { value: true })
|
|
753
|
-
joinTree.mergeColumn(colWithBase, originalQuery.outerQueries)
|
|
537
|
+
const foreignKeyAlias = Array.isArray(definition.keys)
|
|
538
|
+
? definition.keys.find(k => {
|
|
539
|
+
if (k.ref.every((step, j) => arg.ref[i + j] === step)) {
|
|
540
|
+
skipAliasedFkSegmentsOfNameStack.push(...k.ref.slice(1))
|
|
541
|
+
return true
|
|
542
|
+
}
|
|
543
|
+
return false
|
|
544
|
+
})?.as
|
|
545
|
+
: null
|
|
546
|
+
if (foreignKeyAlias) nameSegments.push(foreignKeyAlias)
|
|
547
|
+
else if (skipAliasedFkSegmentsOfNameStack[0] === id) skipAliasedFkSegmentsOfNameStack.shift()
|
|
548
|
+
else {
|
|
549
|
+
nameSegments.push(firstStepIsSelf && i === 1 ? element.__proto__.name : id)
|
|
754
550
|
}
|
|
755
551
|
}
|
|
756
|
-
if (isCalculatedOnRead(leafArt)) {
|
|
757
|
-
linkCalculatedElement(column, $baseLink, baseColumn)
|
|
758
|
-
}
|
|
759
552
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const $leafLink = $refLinks[$refLinks.length - 1]
|
|
778
|
-
if (!$leafLink.definition.target && !$leafLink.definition.elements) {
|
|
779
|
-
throw new Error(
|
|
780
|
-
`Unexpected “inline” on “${col.ref.map(idOnly)}”; can only be used after a reference to a structure, association or table alias`,
|
|
781
|
-
)
|
|
782
|
-
}
|
|
783
|
-
let elements = {}
|
|
784
|
-
inline.forEach(inlineCol => {
|
|
785
|
-
inferQueryElement(inlineCol, false, $leafLink, { inExpr: true, baseColumn: col })
|
|
786
|
-
if (inlineCol === '*') {
|
|
787
|
-
const wildCardElements = {}
|
|
788
|
-
// either the `.elements´ of the struct or the `.elements` of the assoc target
|
|
789
|
-
const leafLinkElements =
|
|
790
|
-
getDefinition($leafLink.definition.target)?.elements || $leafLink.definition.elements
|
|
791
|
-
Object.entries(leafLinkElements).forEach(([k, v]) => {
|
|
792
|
-
const name = namePrefix ? `${namePrefix}_${k}` : k
|
|
793
|
-
// if overwritten/excluded omit from wildcard elements
|
|
794
|
-
// in elements the names are already flat so consider the prefix
|
|
795
|
-
// in excluding, the elements are addressed without the prefix
|
|
796
|
-
if (!(name in elements || col.excluding?.includes(k))) wildCardElements[name] = v
|
|
553
|
+
if (step.where) {
|
|
554
|
+
const danglingFilter = !(arg.ref[i + 1] || arg.expand || arg.inline || inExists)
|
|
555
|
+
const definition = arg.$refLinks[i].definition
|
|
556
|
+
if ((!definition.target && definition.kind !== 'entity') || (!inFrom && danglingFilter))
|
|
557
|
+
throw new Error('A filter can only be provided when navigating along associations')
|
|
558
|
+
if (!inFrom && !arg.expand) Object.defineProperty(arg, 'isJoinRelevant', { value: true })
|
|
559
|
+
let skipJoinsForFilter = false
|
|
560
|
+
step.where.forEach(token => {
|
|
561
|
+
if (token === 'exists') {
|
|
562
|
+
// books[exists genre[code='A']].title --> column is join relevant but inner exists filter is not
|
|
563
|
+
skipJoinsForFilter = true
|
|
564
|
+
} else if (token.ref || token.xpr || token.list) {
|
|
565
|
+
inferArg(token, false, arg.$refLinks[i], {
|
|
566
|
+
inExists: skipJoinsForFilter || inExists,
|
|
567
|
+
inExpr: !!token.xpr,
|
|
568
|
+
inInfixFilter: true,
|
|
569
|
+
inFrom,
|
|
797
570
|
})
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
const inlineElements = resolveInline(inlineCol, name)
|
|
806
|
-
elements = { ...elements, ...inlineElements }
|
|
807
|
-
} else if (inlineCol.expand) {
|
|
808
|
-
const expandElements = resolveExpand(inlineCol)
|
|
809
|
-
elements = { ...elements, [name]: expandElements }
|
|
810
|
-
} else if (inlineCol.val) {
|
|
811
|
-
elements[name] = { ...getCdsTypeForVal(inlineCol.val) }
|
|
812
|
-
} else if (inlineCol.func) {
|
|
813
|
-
elements[name] = {}
|
|
814
|
-
} else {
|
|
815
|
-
elements[name] = inlineCol.$refLinks[inlineCol.$refLinks.length - 1].definition
|
|
571
|
+
} else if (token.func) {
|
|
572
|
+
if (token.args) {
|
|
573
|
+
applyToFunctionArgs(token.args, inferArg, [
|
|
574
|
+
false,
|
|
575
|
+
arg.$refLinks[i],
|
|
576
|
+
{ inExists: skipJoinsForFilter || inExists, inExpr: true, inInfixFilter: true, inFrom },
|
|
577
|
+
])
|
|
816
578
|
}
|
|
817
579
|
}
|
|
818
580
|
})
|
|
819
|
-
return elements
|
|
820
581
|
}
|
|
821
582
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
const inferredExpandSubquery = infer(expandSubquery, model)
|
|
852
|
-
const res = $leafLink.definition.is2one
|
|
853
|
-
? new cds.struct({ elements: inferredExpandSubquery.elements })
|
|
854
|
-
: new cds.array({ items: new cds.struct({ elements: inferredExpandSubquery.elements }) })
|
|
855
|
-
return Object.defineProperty(res, '$assocExpand', { value: true })
|
|
856
|
-
} else if ($leafLink.definition.elements) {
|
|
857
|
-
let elements = {}
|
|
858
|
-
expand.forEach(e => {
|
|
859
|
-
if (e === '*') {
|
|
860
|
-
elements = { ...elements, ...$leafLink.definition.elements }
|
|
583
|
+
arg.$refLinks[i].alias = !arg.ref[i + 1] && arg.as ? arg.as : id.split('.').pop()
|
|
584
|
+
if (getDefinition(arg.$refLinks[i].definition.target)?.['@cds.persistence.skip'] === true) isPersisted = false
|
|
585
|
+
if (!arg.ref[i + 1]) {
|
|
586
|
+
const flatName = nameSegments.join('_')
|
|
587
|
+
Object.defineProperty(arg, 'flatName', { value: flatName, writable: true })
|
|
588
|
+
// if column is casted, we overwrite it's origin with the new type
|
|
589
|
+
if (arg.cast) {
|
|
590
|
+
const base = getElementForCast(arg)
|
|
591
|
+
if (insertIntoQueryElements()) queryElements[arg.as || flatName] = getCopyWithAnnos(arg, base)
|
|
592
|
+
} else if (arg.expand) {
|
|
593
|
+
const elements = resolveExpand(arg)
|
|
594
|
+
let elementName
|
|
595
|
+
// expand on table alias
|
|
596
|
+
if (arg.$refLinks.length === 1 && arg.$refLinks[0].definition.kind === 'entity')
|
|
597
|
+
elementName = arg.$refLinks[0].alias
|
|
598
|
+
else elementName = arg.as || flatName
|
|
599
|
+
if (queryElements) queryElements[elementName] = elements
|
|
600
|
+
} else if (arg.inline && queryElements) {
|
|
601
|
+
const elements = resolveInline(arg)
|
|
602
|
+
Object.assign(queryElements, elements)
|
|
603
|
+
} else {
|
|
604
|
+
// shortcut for `ref: ['$user']` -> `ref: ['$user', 'id']`
|
|
605
|
+
const leafArt =
|
|
606
|
+
i === 0 && id === '$user' ? arg.$refLinks[i].definition.elements.id : arg.$refLinks[i].definition
|
|
607
|
+
// infer element based on leaf artifact of path
|
|
608
|
+
if (insertIntoQueryElements()) {
|
|
609
|
+
let elementName
|
|
610
|
+
if (arg.as) {
|
|
611
|
+
elementName = arg.as
|
|
861
612
|
} else {
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
613
|
+
// if the navigation the user has written differs from the final flat ref - e.g. for renamed foreign keys -
|
|
614
|
+
// the inferred name of the element equals the flat version of the user-written ref.
|
|
615
|
+
const refNavigation = arg.ref
|
|
616
|
+
.slice(firstStepIsSelf || firstStepIsTableAlias ? 1 : 0)
|
|
617
|
+
.map(idOnly)
|
|
618
|
+
.join('_')
|
|
619
|
+
if (refNavigation !== flatName) elementName = refNavigation
|
|
620
|
+
else elementName = flatName
|
|
866
621
|
}
|
|
867
|
-
|
|
868
|
-
|
|
622
|
+
if (queryElements[elementName] !== undefined)
|
|
623
|
+
throw new Error(`Duplicate definition of element “${elementName}”`)
|
|
624
|
+
const element = getCopyWithAnnos(arg, leafArt)
|
|
625
|
+
queryElements[elementName] = element
|
|
626
|
+
}
|
|
869
627
|
}
|
|
870
628
|
}
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
// we need inner joins for the path expressions inside filter expressions after exists predicate
|
|
632
|
+
if ($baseLink?.pathExpressionInsideFilter) Object.defineProperty(arg, 'join', { value: 'inner' })
|
|
871
633
|
|
|
872
|
-
|
|
873
|
-
|
|
634
|
+
// ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
|
|
635
|
+
if (arg.expand) {
|
|
636
|
+
const { $refLinks } = arg
|
|
637
|
+
const skip = $refLinks.some(link => getDefinition(link.definition.target)?.['@cds.persistence.skip'] === true)
|
|
638
|
+
if (skip) {
|
|
639
|
+
$refLinks[$refLinks.length - 1].skipExpand = true
|
|
640
|
+
return
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const leafArt = arg.$refLinks[arg.$refLinks.length - 1].definition
|
|
644
|
+
const virtual = (leafArt.virtual || !isPersisted) && !inExpr
|
|
645
|
+
// check if we need to merge the column `ref` into the join tree of the query
|
|
646
|
+
if (!inFrom && !inExists && !virtual && !inCalcElement) {
|
|
647
|
+
// for a ref inside an `inline` we need to consider the column `ref` which has the `inline` prop
|
|
648
|
+
const colWithBase = baseColumn
|
|
649
|
+
? { ref: [...baseColumn.ref, ...arg.ref], $refLinks: [...baseColumn.$refLinks, ...arg.$refLinks] }
|
|
650
|
+
: arg
|
|
651
|
+
if (isColumnJoinRelevant(colWithBase)) {
|
|
652
|
+
Object.defineProperty(arg, 'isJoinRelevant', { value: true })
|
|
653
|
+
joinTree.mergeColumn(colWithBase, originalQuery.outerQueries)
|
|
874
654
|
}
|
|
655
|
+
}
|
|
656
|
+
if (isCalculatedOnRead(leafArt)) {
|
|
657
|
+
linkCalculatedElement(arg, $baseLink, baseColumn, context)
|
|
658
|
+
}
|
|
875
659
|
|
|
876
|
-
|
|
660
|
+
function insertIntoQueryElements() {
|
|
661
|
+
return queryElements && !inExpr && !inInfixFilter && !inQueryModifier
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Resolves and processes the inline attribute of a column in a database query.
|
|
666
|
+
*
|
|
667
|
+
* @param {object} col - The column object with properties: `inline` and `$refLinks`.
|
|
668
|
+
* @param {string} [namePrefix=col.as || col.flatName] - Prefix for naming new columns. Defaults to `col.as` or `col.flatName`.
|
|
669
|
+
* @returns {object} - An object with resolved and processed inline column definitions.
|
|
670
|
+
*
|
|
671
|
+
* Procedure:
|
|
672
|
+
* 1. Iterate through `inline` array. For each `inlineCol`:
|
|
673
|
+
* a. If `inlineCol` equals '*', wildcard elements are processed and added to the `elements` object.
|
|
674
|
+
* b. If `inlineCol` has inline or expand attributes, corresponding functions are called recursively and the resulting elements are added to the `elements` object.
|
|
675
|
+
* c. If `inlineCol` has val or func attributes, new elements are created and added to the `elements` object.
|
|
676
|
+
* d. Otherwise, the corresponding `$refLinks` definition is added to the `elements` object.
|
|
677
|
+
* 2. Returns the `elements` object.
|
|
678
|
+
*/
|
|
679
|
+
function resolveInline(col, namePrefix = col.as || col.flatName) {
|
|
680
|
+
const { inline, $refLinks } = col
|
|
681
|
+
const $leafLink = $refLinks[$refLinks.length - 1]
|
|
682
|
+
if (!$leafLink.definition.target && !$leafLink.definition.elements) {
|
|
877
683
|
throw new Error(
|
|
878
|
-
`
|
|
879
|
-
.map(ta => `"${ta.index}.${step}"`)
|
|
880
|
-
.join(', ')} instead`,
|
|
684
|
+
`Unexpected “inline” on “${col.ref.map(idOnly)}”; can only be used after a reference to a structure, association or table alias`,
|
|
881
685
|
)
|
|
882
686
|
}
|
|
687
|
+
let elements = {}
|
|
688
|
+
inline.forEach(inlineCol => {
|
|
689
|
+
inferArg(inlineCol, null, $leafLink, { inExpr: true, baseColumn: col })
|
|
690
|
+
if (inlineCol === '*') {
|
|
691
|
+
const wildCardElements = {}
|
|
692
|
+
// either the `.elements´ of the struct or the `.elements` of the assoc target
|
|
693
|
+
const leafLinkElements = getDefinition($leafLink.definition.target)?.elements || $leafLink.definition.elements
|
|
694
|
+
Object.entries(leafLinkElements).forEach(([k, v]) => {
|
|
695
|
+
const name = namePrefix ? `${namePrefix}_${k}` : k
|
|
696
|
+
// if overwritten/excluded omit from wildcard elements
|
|
697
|
+
// in elements the names are already flat so consider the prefix
|
|
698
|
+
// in excluding, the elements are addressed without the prefix
|
|
699
|
+
if (!(name in elements || col.excluding?.includes(k))) wildCardElements[name] = v
|
|
700
|
+
})
|
|
701
|
+
elements = { ...elements, ...wildCardElements }
|
|
702
|
+
} else {
|
|
703
|
+
const nameParts = namePrefix ? [namePrefix] : []
|
|
704
|
+
if (inlineCol.as) nameParts.push(inlineCol.as)
|
|
705
|
+
else nameParts.push(...inlineCol.ref.map(idOnly))
|
|
706
|
+
const name = nameParts.join('_')
|
|
707
|
+
if (inlineCol.inline) {
|
|
708
|
+
const inlineElements = resolveInline(inlineCol, name)
|
|
709
|
+
elements = { ...elements, ...inlineElements }
|
|
710
|
+
} else if (inlineCol.expand) {
|
|
711
|
+
const expandElements = resolveExpand(inlineCol)
|
|
712
|
+
elements = { ...elements, [name]: expandElements }
|
|
713
|
+
} else if (inlineCol.val) {
|
|
714
|
+
elements[name] = { ...getCdsTypeForVal(inlineCol.val) }
|
|
715
|
+
} else if (inlineCol.func) {
|
|
716
|
+
elements[name] = {}
|
|
717
|
+
} else {
|
|
718
|
+
elements[name] = inlineCol.$refLinks[inlineCol.$refLinks.length - 1].definition
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
})
|
|
722
|
+
return elements
|
|
723
|
+
}
|
|
883
724
|
|
|
884
|
-
|
|
725
|
+
/**
|
|
726
|
+
* Resolves a query column which has an `expand` property.
|
|
727
|
+
*
|
|
728
|
+
* @param {object} col - The column object with properties: `expand` and `$refLinks`.
|
|
729
|
+
* @returns {object} - A `cds.struct` object with expanded column definitions.
|
|
730
|
+
*
|
|
731
|
+
* Procedure:
|
|
732
|
+
* - if `$leafLink` is an association, constructs an `expandSubquery` and infers a new query structure.
|
|
733
|
+
* Returns a new `cds.struct` if the association has a target cardinality === 1 or a `cds.array` for to many relations.
|
|
734
|
+
* - else constructs an `elements` object based on the refs `expand` found in the expand and returns a new `cds.struct` with these `elements`.
|
|
735
|
+
*/
|
|
736
|
+
function resolveExpand(col) {
|
|
737
|
+
const { expand, $refLinks } = col
|
|
738
|
+
const $leafLink = $refLinks?.[$refLinks.length - 1] || inferred.SELECT.from.$refLinks.at(-1) // fallback to anonymous expand
|
|
739
|
+
if (!$leafLink.definition.target && !$leafLink.definition.elements) {
|
|
885
740
|
throw new Error(
|
|
886
|
-
`
|
|
887
|
-
.map(s => s.definition)
|
|
888
|
-
.map(def => `"${def.name || /* subquery */ def.as}"`)
|
|
889
|
-
.join(', ')}`,
|
|
741
|
+
`Unexpected “expand” on “${col.ref.map(idOnly)}”; can only be used after a reference to a structure, association or table alias`,
|
|
890
742
|
)
|
|
891
743
|
}
|
|
744
|
+
const target = getDefinition($leafLink.definition.target)
|
|
745
|
+
if (target) {
|
|
746
|
+
const expandSubquery = {
|
|
747
|
+
SELECT: {
|
|
748
|
+
from: target.name,
|
|
749
|
+
columns: expand.filter(c => !c.inline),
|
|
750
|
+
},
|
|
751
|
+
}
|
|
752
|
+
if (col.excluding) expandSubquery.SELECT.excluding = col.excluding
|
|
753
|
+
if (col.as) expandSubquery.SELECT.as = col.as
|
|
754
|
+
const inferredExpandSubquery = infer(expandSubquery, model)
|
|
755
|
+
const res = $leafLink.definition.is2one
|
|
756
|
+
? new cds.struct({ elements: inferredExpandSubquery.elements })
|
|
757
|
+
: new cds.array({ items: new cds.struct({ elements: inferredExpandSubquery.elements }) })
|
|
758
|
+
return Object.defineProperty(res, '$assocExpand', { value: true })
|
|
759
|
+
} else if ($leafLink.definition.elements) {
|
|
760
|
+
let elements = {}
|
|
761
|
+
expand.forEach(e => {
|
|
762
|
+
if (e === '*') {
|
|
763
|
+
elements = { ...elements, ...$leafLink.definition.elements }
|
|
764
|
+
} else {
|
|
765
|
+
inferArg(e, false, $leafLink, { inExpr: true })
|
|
766
|
+
if (e.expand) elements[e.as || e.flatName] = resolveExpand(e)
|
|
767
|
+
if (e.inline) elements = { ...elements, ...resolveInline(e) }
|
|
768
|
+
else elements[e.as || e.flatName] = e.$refLinks ? e.$refLinks[e.$refLinks.length - 1].definition : e
|
|
769
|
+
}
|
|
770
|
+
})
|
|
771
|
+
return new cds.struct({ elements })
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function stepNotFoundInPredecessor(step, def) {
|
|
776
|
+
throw new Error(`"${step}" not found in "${def}"`)
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function stepIsAmbiguous(step) {
|
|
780
|
+
throw new Error(
|
|
781
|
+
`ambiguous reference to "${step}", write ${Object.values($combinedElements[step])
|
|
782
|
+
.map(ta => `"${ta.index}.${step}"`)
|
|
783
|
+
.join(', ')} instead`,
|
|
784
|
+
)
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function stepNotFoundInCombinedElements(step) {
|
|
788
|
+
throw new Error(
|
|
789
|
+
`"${step}" not found in the elements of ${Object.values(sources)
|
|
790
|
+
.map(s => s.definition)
|
|
791
|
+
.map(def => `"${def.name || /* subquery */ def.as}"`)
|
|
792
|
+
.join(', ')}`,
|
|
793
|
+
)
|
|
794
|
+
}
|
|
892
795
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
796
|
+
function stepNotFoundInColumnList(step) {
|
|
797
|
+
const err = [`"${step}" not found in the columns list of query`]
|
|
798
|
+
// if the `elt` from a `$self.elt` path is found in the `$combinedElements` -> hint to remove `$self`
|
|
799
|
+
if (step in $combinedElements)
|
|
800
|
+
err.push(` did you mean ${$combinedElements[step].map(ta => `"${ta.index || ta.as}.${step}"`).join(',')}?`)
|
|
801
|
+
throw new Error(err.join(','))
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
function linkCalculatedElement(column, baseLink, baseColumn, context = {}) {
|
|
805
|
+
const calcElement = column.$refLinks?.[column.$refLinks.length - 1].definition || column
|
|
806
|
+
if (alreadySeenCalcElements.has(calcElement)) return
|
|
807
|
+
else alreadySeenCalcElements.add(calcElement)
|
|
808
|
+
const { ref, xpr } = calcElement.value
|
|
809
|
+
if (ref || xpr) {
|
|
810
|
+
baseLink = { definition: calcElement.parent, target: calcElement.parent }
|
|
811
|
+
inferArg(calcElement.value, null, baseLink, { inCalcElement: true, ...context })
|
|
812
|
+
const basePath =
|
|
813
|
+
column.$refLinks?.length > 1
|
|
814
|
+
? { $refLinks: column.$refLinks.slice(0, -1), ref: column.ref.slice(0, -1) }
|
|
815
|
+
: { $refLinks: [], ref: [] }
|
|
816
|
+
if (baseColumn) {
|
|
817
|
+
basePath.$refLinks.push(...baseColumn.$refLinks)
|
|
818
|
+
basePath.ref.push(...baseColumn.ref)
|
|
899
819
|
}
|
|
820
|
+
mergePathsIntoJoinTree(calcElement.value, basePath)
|
|
900
821
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
const { ref, xpr } = calcElement.value
|
|
906
|
-
if (ref || xpr) {
|
|
907
|
-
baseLink = baseLink || { definition: calcElement.parent, target: calcElement.parent }
|
|
908
|
-
attachRefLinksToArg(calcElement.value, baseLink, true)
|
|
822
|
+
|
|
823
|
+
if (calcElement.value.args) {
|
|
824
|
+
const processArgument = (arg, calcElement, column) => {
|
|
825
|
+
inferArg(arg, null, { definition: calcElement.parent, target: calcElement.parent }, { inCalcElement: true })
|
|
909
826
|
const basePath =
|
|
910
827
|
column.$refLinks?.length > 1
|
|
911
828
|
? { $refLinks: column.$refLinks.slice(0, -1), ref: column.ref.slice(0, -1) }
|
|
912
829
|
: { $refLinks: [], ref: [] }
|
|
913
|
-
|
|
914
|
-
basePath.$refLinks.push(...baseColumn.$refLinks)
|
|
915
|
-
basePath.ref.push(...baseColumn.ref)
|
|
916
|
-
}
|
|
917
|
-
mergePathsIntoJoinTree(calcElement.value, basePath)
|
|
830
|
+
mergePathsIntoJoinTree(arg, basePath)
|
|
918
831
|
}
|
|
919
832
|
|
|
920
833
|
if (calcElement.value.args) {
|
|
921
|
-
|
|
922
|
-
inferQueryElement(
|
|
923
|
-
arg,
|
|
924
|
-
false,
|
|
925
|
-
{ definition: calcElement.parent, target: calcElement.parent },
|
|
926
|
-
{ inCalcElement: true },
|
|
927
|
-
)
|
|
928
|
-
const basePath =
|
|
929
|
-
column.$refLinks?.length > 1
|
|
930
|
-
? { $refLinks: column.$refLinks.slice(0, -1), ref: column.ref.slice(0, -1) }
|
|
931
|
-
: { $refLinks: [], ref: [] }
|
|
932
|
-
mergePathsIntoJoinTree(arg, basePath)
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
if (calcElement.value.args) {
|
|
936
|
-
applyToFunctionArgs(calcElement.value.args, processArgument, [calcElement, column])
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
/**
|
|
941
|
-
* Calculates all paths from a given ref and merges them into the join tree.
|
|
942
|
-
* Recursively walks into refs of calculated elements.
|
|
943
|
-
*
|
|
944
|
-
* @param {object} arg with a ref and sibling $refLinks
|
|
945
|
-
* @param {object} basePath with a ref and sibling $refLinks, used for recursion
|
|
946
|
-
*/
|
|
947
|
-
function mergePathsIntoJoinTree(arg, basePath = null) {
|
|
948
|
-
basePath = basePath || { $refLinks: [], ref: [] }
|
|
949
|
-
if (arg.ref) {
|
|
950
|
-
arg.$refLinks.forEach((link, i) => {
|
|
951
|
-
const { definition } = link
|
|
952
|
-
if (!definition.value) {
|
|
953
|
-
basePath.$refLinks.push(link)
|
|
954
|
-
basePath.ref.push(arg.ref[i])
|
|
955
|
-
}
|
|
956
|
-
})
|
|
957
|
-
const leafOfCalculatedElementRef = arg.$refLinks[arg.$refLinks.length - 1].definition
|
|
958
|
-
if (leafOfCalculatedElementRef.value) mergePathsIntoJoinTree(leafOfCalculatedElementRef.value, basePath)
|
|
959
|
-
|
|
960
|
-
mergePathIfNecessary(basePath, arg)
|
|
961
|
-
} else if (arg.xpr || arg.args) {
|
|
962
|
-
const prop = arg.xpr ? 'xpr' : 'args'
|
|
963
|
-
arg[prop].forEach(step => {
|
|
964
|
-
const subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
|
|
965
|
-
if (step.ref) {
|
|
966
|
-
step.$refLinks.forEach((link, i) => {
|
|
967
|
-
const { definition } = link
|
|
968
|
-
if (definition.value) {
|
|
969
|
-
mergePathsIntoJoinTree(definition.value, subPath)
|
|
970
|
-
} else {
|
|
971
|
-
subPath.$refLinks.push(link)
|
|
972
|
-
subPath.ref.push(step.ref[i])
|
|
973
|
-
}
|
|
974
|
-
})
|
|
975
|
-
mergePathIfNecessary(subPath, step)
|
|
976
|
-
} else if (step.args || step.xpr) {
|
|
977
|
-
const nestedProp = step.xpr ? 'xpr' : 'args'
|
|
978
|
-
step[nestedProp].forEach(a => {
|
|
979
|
-
mergePathsIntoJoinTree(a, subPath)
|
|
980
|
-
})
|
|
981
|
-
}
|
|
982
|
-
})
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
function mergePathIfNecessary(p, step) {
|
|
986
|
-
const calcElementIsJoinRelevant = isColumnJoinRelevant(p)
|
|
987
|
-
if (calcElementIsJoinRelevant) {
|
|
988
|
-
if (!calcElement.value.isColumnJoinRelevant)
|
|
989
|
-
Object.defineProperty(step, 'isJoinRelevant', { value: true, writable: true })
|
|
990
|
-
joinTree.mergeColumn(p, originalQuery.outerQueries)
|
|
991
|
-
} else {
|
|
992
|
-
// we need to explicitly set the value to false in this case,
|
|
993
|
-
// e.g. `SELECT from booksCalc.Books { ID, author.{name }, author {name } }`
|
|
994
|
-
// --> for the inline column, the name is join relevant, while for the expand, it is not
|
|
995
|
-
Object.defineProperty(step, 'isJoinRelevant', { value: false, writable: true })
|
|
996
|
-
}
|
|
997
|
-
}
|
|
834
|
+
applyToFunctionArgs(calcElement.value.args, processArgument, [calcElement, column])
|
|
998
835
|
}
|
|
999
836
|
}
|
|
1000
837
|
|
|
1001
838
|
/**
|
|
1002
|
-
*
|
|
1003
|
-
*
|
|
1004
|
-
* - the association is unmanaged
|
|
1005
|
-
* - a non-foreign key access is performed
|
|
1006
|
-
* - an infix filter is applied at the association
|
|
839
|
+
* Calculates all paths from a given ref and merges them into the join tree.
|
|
840
|
+
* Recursively walks into refs of calculated elements.
|
|
1007
841
|
*
|
|
1008
|
-
* @param {object}
|
|
1009
|
-
* @
|
|
842
|
+
* @param {object} arg with a ref and sibling $refLinks
|
|
843
|
+
* @param {object} basePath with a ref and sibling $refLinks, used for recursion
|
|
1010
844
|
*/
|
|
1011
|
-
function
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
if (column.expand && assoc) return true
|
|
1020
|
-
// if unmanaged assoc is exposed, ignore it
|
|
1021
|
-
return false
|
|
845
|
+
function mergePathsIntoJoinTree(arg, basePath = null) {
|
|
846
|
+
basePath = basePath || { $refLinks: [], ref: [] }
|
|
847
|
+
if (arg.ref) {
|
|
848
|
+
arg.$refLinks.forEach((link, i) => {
|
|
849
|
+
const { definition } = link
|
|
850
|
+
if (!definition.value) {
|
|
851
|
+
basePath.$refLinks.push(link)
|
|
852
|
+
basePath.ref.push(arg.ref[i])
|
|
1022
853
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
if (
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
if (
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
854
|
+
})
|
|
855
|
+
const leafOfCalculatedElementRef = arg.$refLinks[arg.$refLinks.length - 1].definition
|
|
856
|
+
if (leafOfCalculatedElementRef.value) mergePathsIntoJoinTree(leafOfCalculatedElementRef.value, basePath)
|
|
857
|
+
|
|
858
|
+
mergePathIfNecessary(basePath, arg)
|
|
859
|
+
} else if (arg.xpr || arg.args) {
|
|
860
|
+
const prop = arg.xpr ? 'xpr' : 'args'
|
|
861
|
+
arg[prop].forEach(step => {
|
|
862
|
+
const subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
|
|
863
|
+
if (step.ref) {
|
|
864
|
+
step.$refLinks.forEach((link, i) => {
|
|
865
|
+
const { definition } = link
|
|
866
|
+
if (definition.value) {
|
|
867
|
+
mergePathsIntoJoinTree(definition.value, subPath)
|
|
868
|
+
} else {
|
|
869
|
+
subPath.$refLinks.push(link)
|
|
870
|
+
subPath.ref.push(step.ref[i])
|
|
871
|
+
}
|
|
872
|
+
})
|
|
873
|
+
mergePathIfNecessary(subPath, step)
|
|
874
|
+
} else if (step.args || step.xpr) {
|
|
875
|
+
const nestedProp = step.xpr ? 'xpr' : 'args'
|
|
876
|
+
step[nestedProp].forEach(a => {
|
|
877
|
+
mergePathsIntoJoinTree(a, subPath)
|
|
878
|
+
})
|
|
1039
879
|
}
|
|
1040
|
-
}
|
|
880
|
+
})
|
|
1041
881
|
}
|
|
1042
882
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
if (Object.keys(queryElements).length === 0 && aliases.length === 1) {
|
|
1056
|
-
const { elements } = getDefinitionFromSources(sources, aliases[0])
|
|
1057
|
-
// only one query source and no overwritten columns
|
|
1058
|
-
for (const k of Object.keys(elements)) {
|
|
1059
|
-
if (!exclude(k)) {
|
|
1060
|
-
const element = elements[k]
|
|
1061
|
-
if (element.type !== 'cds.LargeBinary') {
|
|
1062
|
-
queryElements[k] = element
|
|
1063
|
-
}
|
|
1064
|
-
if (isCalculatedOnRead(element)) {
|
|
1065
|
-
linkCalculatedElement(element)
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
883
|
+
function mergePathIfNecessary(p, step) {
|
|
884
|
+
const calcElementIsJoinRelevant = isColumnJoinRelevant(p)
|
|
885
|
+
if (calcElementIsJoinRelevant) {
|
|
886
|
+
if (!calcElement.value.isJoinRelevant)
|
|
887
|
+
Object.defineProperty(step, 'isJoinRelevant', { value: true, writable: true, })
|
|
888
|
+
joinTree.mergeColumn(p, originalQuery.outerQueries)
|
|
889
|
+
} else {
|
|
890
|
+
// we need to explicitly set the value to false in this case,
|
|
891
|
+
// e.g. `SELECT from booksCalc.Books { ID, author.{name }, author {name } }`
|
|
892
|
+
// --> for the inline column, the name is join relevant, while for the expand, it is not
|
|
893
|
+
Object.defineProperty(step, 'isJoinRelevant', { value: false, writable: true })
|
|
1068
894
|
}
|
|
1069
|
-
return
|
|
1070
895
|
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
1071
898
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
899
|
+
/**
|
|
900
|
+
* Checks whether or not the `ref` of the given column is join relevant.
|
|
901
|
+
* A `ref` is considered join relevant if it includes an association traversal and:
|
|
902
|
+
* - the association is unmanaged
|
|
903
|
+
* - a non-foreign key access is performed
|
|
904
|
+
* - an infix filter is applied at the association
|
|
905
|
+
*
|
|
906
|
+
* @param {object} column the column with the `ref` to check for join relevance
|
|
907
|
+
* @returns {boolean} true if the column ref needs to be merged into a join tree
|
|
908
|
+
*/
|
|
909
|
+
function isColumnJoinRelevant(column) {
|
|
910
|
+
let fkAccess = false
|
|
911
|
+
let assoc = null
|
|
912
|
+
for (let i = 0; i < column.ref.length; i++) {
|
|
913
|
+
const ref = column.ref[i]
|
|
914
|
+
const link = column.$refLinks[i]
|
|
915
|
+
if (link.definition.on && link.definition.isAssociation) {
|
|
916
|
+
if (!column.ref[i + 1]) {
|
|
917
|
+
if (column.expand && assoc) return true
|
|
918
|
+
// if unmanaged assoc is exposed, ignore it
|
|
919
|
+
return false
|
|
1077
920
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
921
|
+
return true
|
|
922
|
+
}
|
|
923
|
+
if (assoc) {
|
|
924
|
+
// foreign key access without filters never join relevant
|
|
925
|
+
if (assoc.keys?.some(key => key.ref.every((step, j) => column.ref[i + j] === step))) return false
|
|
926
|
+
// <assoc>.<anotherAssoc>.<…> is join relevant as <anotherAssoc> is not fk of <assoc>
|
|
927
|
+
return true
|
|
928
|
+
}
|
|
929
|
+
if (link.definition.target && link.definition.keys) {
|
|
930
|
+
if (column.ref[i + 1] || assoc) fkAccess = false
|
|
931
|
+
else fkAccess = true
|
|
932
|
+
assoc = link.definition
|
|
933
|
+
if (ref.where) {
|
|
934
|
+
// always join relevant except for expand assoc
|
|
935
|
+
if (column.expand && !column.ref[i + 1]) return false
|
|
936
|
+
return true
|
|
1083
937
|
}
|
|
1084
|
-
})
|
|
1085
|
-
|
|
1086
|
-
if (Object.keys(ambiguousElements).length > 0) throwAmbiguousWildcardError()
|
|
1087
|
-
|
|
1088
|
-
function throwAmbiguousWildcardError() {
|
|
1089
|
-
const err = []
|
|
1090
|
-
err.push('Ambiguous wildcard elements:')
|
|
1091
|
-
Object.keys(ambiguousElements).forEach(name => {
|
|
1092
|
-
const tableAliasNames = Object.values(ambiguousElements[name]).map(v => v.index)
|
|
1093
|
-
err.push(
|
|
1094
|
-
` select "${name}" explicitly with ${tableAliasNames
|
|
1095
|
-
.map(taName => `"${taName}.${name}"`)
|
|
1096
|
-
.join(', ')}`,
|
|
1097
|
-
)
|
|
1098
|
-
})
|
|
1099
|
-
throw new Error(err.join('\n'))
|
|
1100
938
|
}
|
|
1101
939
|
}
|
|
1102
940
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
941
|
+
if (!assoc) return false
|
|
942
|
+
if (fkAccess) return false
|
|
943
|
+
return true
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Iterates over all `$combinedElements` of the `query` and puts them into the `query`s `elements`,
|
|
948
|
+
* if there is not already an element with the same name present.
|
|
949
|
+
*/
|
|
950
|
+
function inferElementsFromWildCard(queryElements) {
|
|
951
|
+
const exclude = _.excluding ? x => _.excluding.includes(x) : () => false
|
|
952
|
+
|
|
953
|
+
if (Object.keys(queryElements).length === 0 && aliases.length === 1) {
|
|
954
|
+
const { elements } = getDefinitionFromSources(sources, aliases[0])
|
|
955
|
+
// only one query source and no overwritten columns
|
|
956
|
+
for (const k of Object.keys(elements)) {
|
|
957
|
+
if (!exclude(k)) {
|
|
958
|
+
const element = elements[k]
|
|
959
|
+
if (element.type !== 'cds.LargeBinary') {
|
|
960
|
+
queryElements[k] = element
|
|
961
|
+
}
|
|
962
|
+
if (isCalculatedOnRead(element)) {
|
|
963
|
+
linkCalculatedElement(element)
|
|
964
|
+
}
|
|
1120
965
|
}
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
if (col.key) base.key = col.key // > preserve key on column
|
|
1124
|
-
return getCopyWithAnnos(col, base)
|
|
966
|
+
}
|
|
967
|
+
return
|
|
1125
968
|
}
|
|
1126
969
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
970
|
+
const ambiguousElements = {}
|
|
971
|
+
Object.entries($combinedElements).forEach(([name, tableAliases]) => {
|
|
972
|
+
if (Object.keys(tableAliases).length > 1) {
|
|
973
|
+
ambiguousElements[name] = tableAliases
|
|
974
|
+
return ambiguousElements[name]
|
|
975
|
+
}
|
|
976
|
+
if (exclude(name) || name in queryElements) return true
|
|
977
|
+
const element = tableAliases[0].tableAlias.elements[name]
|
|
978
|
+
if (element.type !== 'cds.LargeBinary') queryElements[name] = element
|
|
979
|
+
if (isCalculatedOnRead(element)) {
|
|
980
|
+
linkCalculatedElement(element)
|
|
981
|
+
}
|
|
982
|
+
})
|
|
983
|
+
|
|
984
|
+
if (Object.keys(ambiguousElements).length > 0) throwAmbiguousWildcardError()
|
|
985
|
+
|
|
986
|
+
function throwAmbiguousWildcardError() {
|
|
987
|
+
const err = []
|
|
988
|
+
err.push('Ambiguous wildcard elements:')
|
|
989
|
+
Object.keys(ambiguousElements).forEach(name => {
|
|
990
|
+
const tableAliasNames = Object.values(ambiguousElements[name]).map(v => v.index)
|
|
991
|
+
err.push(
|
|
992
|
+
` select "${name}" explicitly with ${tableAliasNames.map(taName => `"${taName}.${name}"`).join(', ')}`,
|
|
993
|
+
)
|
|
994
|
+
})
|
|
995
|
+
throw new Error(err.join('\n'))
|
|
1143
996
|
}
|
|
1144
997
|
}
|
|
1145
998
|
|
|
999
|
+
/**
|
|
1000
|
+
* Returns a new object which is the inferred element for the given `col`.
|
|
1001
|
+
* A cast type (via cast function) on the column gets preserved.
|
|
1002
|
+
*
|
|
1003
|
+
* @param {object} col
|
|
1004
|
+
* @returns object
|
|
1005
|
+
*/
|
|
1006
|
+
function getElementForXprOrSubquery(col, queryElements) {
|
|
1007
|
+
const { xpr } = col
|
|
1008
|
+
let skipJoins = false
|
|
1009
|
+
xpr?.forEach(token => {
|
|
1010
|
+
if (token === 'exists') {
|
|
1011
|
+
// no joins for infix filters along `exists <path>`
|
|
1012
|
+
skipJoins = true
|
|
1013
|
+
} else {
|
|
1014
|
+
inferArg(token, queryElements, null, { inExists: skipJoins, inExpr: true })
|
|
1015
|
+
skipJoins = false
|
|
1016
|
+
}
|
|
1017
|
+
})
|
|
1018
|
+
const base = getElementForCast(col.cast ? col : xpr?.[0] || col)
|
|
1019
|
+
if (col.key) base.key = col.key // > preserve key on column
|
|
1020
|
+
return getCopyWithAnnos(col, base)
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Returns an object with the cast-type defined in the cast of the `thing`.
|
|
1025
|
+
* If no cast property is present, it just returns an empty object.
|
|
1026
|
+
* The type of the cast is mapped to the `cds` type if possible.
|
|
1027
|
+
*
|
|
1028
|
+
* @param {object} thing with the cast property
|
|
1029
|
+
* @returns {object}
|
|
1030
|
+
*/
|
|
1031
|
+
function getElementForCast(thing) {
|
|
1032
|
+
const { cast, $refLinks } = thing
|
|
1033
|
+
if (!cast) return {}
|
|
1034
|
+
if ($refLinks?.[$refLinks.length - 1].definition.elements)
|
|
1035
|
+
// no cast on structure
|
|
1036
|
+
cds.error`Structured elements can't be cast to a different type`
|
|
1037
|
+
thing.cast = cdsTypes[cast.type] || cast
|
|
1038
|
+
return thing.cast
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1146
1041
|
/**
|
|
1147
1042
|
* return a new object based on @param base
|
|
1148
1043
|
* with all annotations found in @param from
|
|
@@ -1165,14 +1060,6 @@ function infer(originalQuery, model) {
|
|
|
1165
1060
|
return Object.setPrototypeOf(result, base)
|
|
1166
1061
|
}
|
|
1167
1062
|
|
|
1168
|
-
// REVISIT: functions without return are by nature side-effect functions -> bad
|
|
1169
|
-
function init$refLinks(arg) {
|
|
1170
|
-
Object.defineProperty(arg, '$refLinks', {
|
|
1171
|
-
value: [],
|
|
1172
|
-
writable: true,
|
|
1173
|
-
})
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
1063
|
function getCdsTypeForVal(val) {
|
|
1177
1064
|
// REVISIT: JS null should have a type for proper DB layer conversion logic
|
|
1178
1065
|
// if(val === null) return {type:'cds.String'}
|