@sap/cds-dk 10.0.3 → 10.0.5

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/bin/debug.js +6 -2
  3. package/bin/up.js +1 -1
  4. package/bin/watch.js +1 -0
  5. package/lib/build/plugins/helm/files/templates/_deployment_helpers.tpl +2 -0
  6. package/lib/build/plugins/nodejs/index.js +6 -3
  7. package/lib/build/util.js +1 -1
  8. package/lib/init/template/ams/files/containerize.yaml.hbs +1 -0
  9. package/lib/init/template/approuter/files/values.yaml.hbs +9 -1
  10. package/lib/init/template/data/as-csv.js +4 -2
  11. package/lib/init/template/data/as-json.js +4 -1
  12. package/lib/init/template/github-actions/files/workflows/cf.yaml.hbs +6 -6
  13. package/lib/init/template/ias/files/values.yaml.hbs +4 -4
  14. package/lib/init/template/portal/files/values.yaml.hbs +12 -0
  15. package/lib/init/template/sample/files/app/browse/fiori-service.cds +2 -0
  16. package/lib/init/template/typer/files/xsconfig.json +1 -1
  17. package/lib/init/template/typescript/files/package.json +1 -1
  18. package/lib/lint/eslintHelp.json +1 -1
  19. package/lib/watch/esm-resolve-registration.js +27 -0
  20. package/lib/watch/vite-watch.js +1 -1
  21. package/node_modules/@cap-js/db-service/CHANGELOG.md +768 -0
  22. package/node_modules/@cap-js/db-service/LICENSE +201 -0
  23. package/node_modules/@cap-js/db-service/README.md +26 -0
  24. package/node_modules/@cap-js/db-service/index.js +19 -0
  25. package/node_modules/@cap-js/db-service/lib/InsertResults.js +122 -0
  26. package/node_modules/@cap-js/db-service/lib/SQLService.js +581 -0
  27. package/node_modules/@cap-js/db-service/lib/common/DatabaseService.js +179 -0
  28. package/node_modules/@cap-js/db-service/lib/common/factory.d.ts +5 -0
  29. package/node_modules/@cap-js/db-service/lib/common/generic-pool.js +310 -0
  30. package/node_modules/@cap-js/db-service/lib/common/session-context.js +46 -0
  31. package/node_modules/@cap-js/db-service/lib/converters.d.ts +24 -0
  32. package/node_modules/@cap-js/db-service/lib/cql-functions.js +464 -0
  33. package/node_modules/@cap-js/db-service/lib/cqn2pql.js +116 -0
  34. package/node_modules/@cap-js/db-service/lib/cqn2sql.js +1612 -0
  35. package/node_modules/@cap-js/db-service/lib/cqn4sql.js +2895 -0
  36. package/node_modules/@cap-js/db-service/lib/deep-queries.js +290 -0
  37. package/node_modules/@cap-js/db-service/lib/fill-in-keys.js +75 -0
  38. package/node_modules/@cap-js/db-service/lib/infer/cqn.d.ts +42 -0
  39. package/node_modules/@cap-js/db-service/lib/infer/index.js +1290 -0
  40. package/node_modules/@cap-js/db-service/lib/infer/join-tree.js +278 -0
  41. package/node_modules/@cap-js/db-service/lib/infer/pseudos.js +24 -0
  42. package/node_modules/@cap-js/db-service/lib/search.js +220 -0
  43. package/node_modules/@cap-js/db-service/lib/utils.js +177 -0
  44. package/node_modules/@cap-js/db-service/package.json +36 -0
  45. package/node_modules/@cap-js/sqlite/CHANGELOG.md +404 -0
  46. package/node_modules/@cap-js/sqlite/LICENSE +201 -0
  47. package/node_modules/@cap-js/sqlite/README.md +34 -0
  48. package/node_modules/@cap-js/sqlite/cds-plugin.js +0 -0
  49. package/node_modules/@cap-js/sqlite/index.js +2 -0
  50. package/node_modules/@cap-js/sqlite/lib/SQLiteService.js +324 -0
  51. package/node_modules/@cap-js/sqlite/lib/cql-functions.js +160 -0
  52. package/node_modules/@cap-js/sqlite/lib/node-sqlite.js +36 -0
  53. package/node_modules/@cap-js/sqlite/lib/session.json +3 -0
  54. package/node_modules/@cap-js/sqlite/lib/sql.js.js +99 -0
  55. package/node_modules/@cap-js/sqlite/package.json +68 -0
  56. package/node_modules/@sap/cds-compiler/CHANGELOG.md +14 -0
  57. package/node_modules/@sap/cds-compiler/bin/cdsse.js +1 -1
  58. package/node_modules/@sap/cds-compiler/bin/cdsv2m.js +1 -1
  59. package/node_modules/@sap/cds-compiler/doc/IncompatibleChanges_v7.md +20 -13
  60. package/node_modules/@sap/cds-compiler/lib/base/builtins.js +4 -1
  61. package/node_modules/@sap/cds-compiler/lib/base/message-registry.js +15 -2
  62. package/node_modules/@sap/cds-compiler/lib/checks/defaultValues.js +0 -1
  63. package/node_modules/@sap/cds-compiler/lib/compiler/resolve.js +6 -5
  64. package/node_modules/@sap/cds-compiler/lib/compiler/tweak-assocs.js +27 -6
  65. package/node_modules/@sap/cds-compiler/lib/edm/annotations/edmJson.js +5 -0
  66. package/node_modules/@sap/cds-compiler/lib/json/from-csn.js +3 -3
  67. package/node_modules/@sap/cds-compiler/lib/json/to-csn.js +5 -4
  68. package/node_modules/@sap/cds-compiler/lib/parsers/AstBuildingParser.js +14 -3
  69. package/node_modules/@sap/cds-compiler/lib/tool-lib/enrichCsn.js +1 -1
  70. package/node_modules/@sap/cds-compiler/lib/transform/db/cdsPersistence.js +3 -2
  71. package/node_modules/@sap/cds-compiler/lib/transform/db/views.js +2 -2
  72. package/node_modules/@sap/cds-compiler/lib/transform/transformUtils.js +1 -2
  73. package/node_modules/@sap/cds-compiler/package.json +1 -1
  74. package/node_modules/iconv-lite/encodings/sbcs-data.js +2 -0
  75. package/node_modules/iconv-lite/encodings/utf32.js +10 -3
  76. package/node_modules/iconv-lite/package.json +2 -2
  77. package/node_modules/iconv-lite/types/encodings.d.ts +2 -0
  78. package/node_modules/ws/lib/receiver.js +15 -32
  79. package/node_modules/ws/lib/websocket-server.js +4 -4
  80. package/node_modules/ws/lib/websocket.js +4 -4
  81. package/node_modules/ws/package.json +5 -1
  82. package/package.json +5 -3
@@ -0,0 +1,1290 @@
1
+ 'use strict'
2
+
3
+ const cds = require('@sap/cds')
4
+
5
+ const JoinTree = require('./join-tree')
6
+ const { pseudos } = require('./pseudos')
7
+ const { isCalculatedOnRead, getImplicitAlias, getModelUtils, defineProperty, hasOwnSkip, isRuntimeView } = require('../utils')
8
+ const cdsTypes = cds.builtin.types
9
+ /**
10
+ * @param {import('@sap/cds/apis/cqn').Query|string} originalQuery
11
+ * @param {import('@sap/cds/apis/csn').CSN} [model]
12
+ * @returns {import('./cqn').Query} = q with .target and .elements
13
+ */
14
+ function infer(originalQuery, model, useTechnicalAlias = true) {
15
+ if (!model) throw new Error('Please specify a model')
16
+ const inferred = originalQuery
17
+
18
+ const { getDefinition } = getModelUtils(model, originalQuery)
19
+
20
+ // REVISIT: The more edge use cases we support, thes less optimized are we for the 90+% use cases
21
+ // e.g. there's a lot of overhead for infer( SELECT.from(Books) )
22
+ if (originalQuery.SET) throw new Error('”UNION” based queries are not supported')
23
+ const _ =
24
+ inferred.SELECT ||
25
+ inferred.INSERT ||
26
+ inferred.UPSERT ||
27
+ inferred.UPDATE ||
28
+ inferred.DELETE ||
29
+ inferred.CREATE ||
30
+ inferred.DROP
31
+
32
+ // cache for already processed calculated elements
33
+ const alreadySeenCalcElements = new Set()
34
+
35
+ let $combinedElements
36
+
37
+ const sources = inferTarget(_.into || _.from || _.entity, {}, useTechnicalAlias) // IMPORTANT: _.into has to go before _.from for INSERT.into().from(SELECT)
38
+ const aliases = Object.keys(sources)
39
+ const target = aliases.length === 1 ? getDefinitionFromSources(sources, aliases[0]) : originalQuery
40
+ Object.defineProperties(inferred, {
41
+ // REVISIT: public, or for local reuse, or in cqn4sql only?
42
+ sources: { value: sources, writable: true },
43
+ _target: { value: target, writable: true, configurable: true }, // REVISIT: legacy?
44
+ })
45
+ // also enrich original query -> writable because it may be inferred again
46
+ Object.defineProperties(originalQuery, {
47
+ sources: { value: sources, writable: true },
48
+ _target: { value: target, writable: true, configurable: true },
49
+ })
50
+ if (originalQuery.SELECT || originalQuery.DELETE || originalQuery.UPDATE) {
51
+ $combinedElements = inferCombinedElements()
52
+ /**
53
+ * TODO: this function is currently only called on DELETE's
54
+ * because it correctly set's up the $refLink's in the
55
+ * where clause: This functionality should be pulled out
56
+ * of ´inferQueryElement()` as this is a subtle side effect
57
+ */
58
+ const elements = inferQueryElements()
59
+ Object.defineProperties(inferred, {
60
+ $combinedElements: { value: $combinedElements, writable: true, configurable: true },
61
+ elements: { value: elements, writable: true, configurable: true },
62
+ })
63
+ // also enrich original query -> writable because it may be inferred again
64
+ defineProperty(originalQuery, 'elements', elements)
65
+ }
66
+ return inferred
67
+
68
+ /**
69
+ * Infers all query sources from a given SQL-like query's `from` clause.
70
+ * It drills down into join arguments of the `from` clause.
71
+ *
72
+ * This function helps identify each source, target, and association within the `from` clause.
73
+ * It processes the `from` clause in the query and maps each source to a respective target and alias.
74
+ * In case of any errors like missing definitions or associations, this function will throw an error.
75
+ *
76
+ * @function inferTarget
77
+ * @param {object|string} from - The `from` clause of the query to infer the target from.
78
+ * It could be an object or a string.
79
+ * @param {object} querySources - An object to map the query sources.
80
+ * Each key is a query source alias, and its value is the corresponding CSN Definition.
81
+ * @returns {object} The updated `querySources` object with inferred sources from the `from` clause.
82
+ */
83
+ function inferTarget(from, querySources, useTechnicalAlias) {
84
+ const { ref } = from
85
+ // Given a from clause `Root:parent[$main.name = name].parent as Foo`
86
+ // we need to first resolve until to the last step of the from.ref
87
+ // before we can replace $main with `Foo`
88
+ const $mainLazyResolve = [] // TODO: remove and replace with real alias breakout
89
+ if (ref) {
90
+ const { id, args } = ref[0]
91
+ const first = id || ref[0]
92
+ let target = getDefinition(first) || cds.error`"${first}" not found in the definitions of your model`
93
+ if (!target) throw new Error(`"${first}" not found in the definitions of your model`)
94
+ if (ref.length > 1) {
95
+ target = from.ref.slice(1).reduce((d, r) => {
96
+ const next = getDefinition(d.elements[r.id || r]?.target) || d.elements[r.id || r]
97
+ if (!next) throw new Error(`No association “${r.id || r}” in ${d.kind} “${d.name}”`)
98
+ return next
99
+ }, target)
100
+ }
101
+ if (target.kind !== 'entity' && !target.isAssociation)
102
+ throw new Error('Query source must be a an entity or an association')
103
+
104
+ inferArg(from, null, null, { inFrom: true, $mainLazyResolve })
105
+ const alias =
106
+ from.uniqueSubqueryAlias ||
107
+ from.as ||
108
+ (ref.length === 1
109
+ ? getImplicitAlias(first, useTechnicalAlias)
110
+ : getImplicitAlias(ref.at(-1).id || ref.at(-1), useTechnicalAlias))
111
+ if (alias in querySources) throw new Error(`Duplicate alias "${alias}"`)
112
+ querySources[alias] = { definition: getDefinition(target.name), args }
113
+ const last = from.$refLinks.at(-1)
114
+ last.alias = alias
115
+ } else if (from.args) {
116
+ from.args.forEach(a => inferTarget(a, querySources, false))
117
+ } else if (from.SELECT) {
118
+ const subqueryInFrom = infer(from, model) // we need the .elements in the sources
119
+ // if no explicit alias is provided, we make up one
120
+ const subqueryAlias =
121
+ from.as || subqueryInFrom.joinTree.addNextAvailableTableAlias('__select__', subqueryInFrom.outerQueries)
122
+ querySources[subqueryAlias] = { definition: from }
123
+ } else if (typeof from === 'string') {
124
+ // TODO: Create unique alias, what about duplicates?
125
+ const definition = getDefinition(from) || cds.error`"${from}" not found in the definitions of your model`
126
+ querySources[getImplicitAlias(from, useTechnicalAlias)] = { definition }
127
+ } else if (from.SET) {
128
+ infer(from, model)
129
+ }
130
+
131
+ const joinTree = new JoinTree(querySources)
132
+ Object.defineProperty( inferred, 'joinTree', { value: joinTree, writable: true, configurable: true } )
133
+ for(const lazyRef of $mainLazyResolve) inferArg(lazyRef)
134
+ return querySources
135
+ }
136
+
137
+ /**
138
+ * Calculates the `$combinedElements` based on the provided queries `sources`.
139
+ * The `$combinedElements` of a query consist of all accessible elements across all
140
+ * the table aliases found in the from clause.
141
+ *
142
+ * The `$combinedElements` are attached to the query as a non-enumerable property.
143
+ * Each entry in the `$combinedElements` dictionary maps from the element name
144
+ * to an array of objects containing the index and table alias where the element can be found.
145
+ *
146
+ * @returns {object} The `$combinedElements` dictionary, which maps element names to an array of objects
147
+ * containing the index and table alias where the element can be found.
148
+ */
149
+ function inferCombinedElements() {
150
+ const combinedElements = {}
151
+ for (const index in sources) {
152
+ const tableAlias = getDefinitionFromSources(sources, index)
153
+ for (const key in tableAlias.elements) {
154
+ if (key in combinedElements) combinedElements[key].push({ index, tableAlias })
155
+ else combinedElements[key] = [{ index, tableAlias }]
156
+ }
157
+ }
158
+ return combinedElements
159
+ }
160
+
161
+ /**
162
+ * Assigns the given `element` as non-enumerable property 'element' onto `col`.
163
+ *
164
+ * @param {object} col
165
+ * @param {csn.Element} element
166
+ */
167
+ function setElementOnColumns(col, element) {
168
+ defineProperty(col, 'element', element)
169
+ }
170
+
171
+ /**
172
+ * Walks over all columns of a query's `SELECT` and infers each `ref`, `xpr`, or `val` as a query element
173
+ * based on the query's `$combinedElements` and `sources`.
174
+ *
175
+ * The inferred `elements` are attached to the query as a non-enumerable property.
176
+ *
177
+ * Also walks over other `ref`s in the query, validates them, and attaches `$refLinks`.
178
+ * This includes handling `where`, infix filters within column `refs`, or other `csn` paths.
179
+ *
180
+ * @param {object} $combinedElements The `$combinedElements` dictionary of the query, which maps element names
181
+ * to an array of objects containing the index and table alias where the element can be found.
182
+ * @returns {object} The inferred `elements` dictionary of the query, which maps element names to their corresponding definitions.
183
+ */
184
+ function inferQueryElements() {
185
+ let queryElements = {}
186
+ const { columns, where, groupBy, having, orderBy } = _
187
+ if (!columns) {
188
+ inferElementsFromWildCard(queryElements)
189
+ } else {
190
+ let wildcardSelect = false
191
+ const dollarSelfRefs = []
192
+ columns.forEach(col => {
193
+ if (col === '*') {
194
+ if (wildcardSelect) throw new Error('Duplicate wildcard "*" in column list')
195
+ wildcardSelect = true
196
+ } else if (col.val !== undefined || col.xpr || col.SELECT || col.func || col.param) {
197
+ const as = col.as || col.func || col.val
198
+ if (as === undefined) cds.error`Expecting expression to have an alias name`
199
+ if (queryElements[as]) rejectDuplicatedElement(as)
200
+ if (col.xpr || col.SELECT) {
201
+ queryElements[as] = getElementForXprOrSubquery(col, queryElements, dollarSelfRefs)
202
+ }
203
+ if (col.func) {
204
+ if (col.args) {
205
+ // {func}.args are optional
206
+ applyToFunctionArgs(col.args, inferArg, [false, null, { dollarSelfRefs }])
207
+ }
208
+ queryElements[as] = getElementForCast(col)
209
+ }
210
+ if (!queryElements[as]) {
211
+ // either binding parameter (col.param) or value
212
+ queryElements[as] = col.cast ? getElementForCast(col) : getCdsTypeForVal(col.val)
213
+ }
214
+ setElementOnColumns(col, queryElements[as])
215
+ } else if (col.ref) {
216
+ const firstStepIsTableAlias =
217
+ (col.ref.length > 1 && col.ref[0] in sources) ||
218
+ // nested projection on table alias
219
+ (col.ref.length === 1 && col.ref[0] in sources && col.inline)
220
+ const firstStepIsSelf =
221
+ !firstStepIsTableAlias && col.ref.length > 1 && ['$self', '$projection'].includes(col.ref[0])
222
+ // we must handle $self references after the query elements have been calculated
223
+ if (firstStepIsSelf) dollarSelfRefs.push(col)
224
+ else handleRef(col)
225
+ } else if (col.expand) {
226
+ inferArg(col, queryElements, null)
227
+ } else {
228
+ cds.error`Not supported: ${JSON.stringify(col)}`
229
+ }
230
+ })
231
+
232
+ if (dollarSelfRefs.length) inferDollarSelfRefs(dollarSelfRefs)
233
+
234
+ if (wildcardSelect) inferElementsFromWildCard(queryElements)
235
+ }
236
+ if (orderBy) {
237
+ // link $refLinks -> special name resolution rules for orderBy
238
+ orderBy.forEach(token => {
239
+ let $baseLink
240
+ let needsElementsOfQueryAsBase
241
+ // first check if token ref is resolvable in query elements
242
+ if (columns) {
243
+ const firstStep = token.ref?.[0].id || token.ref?.[0]
244
+ const tokenPointsToQueryElements = columns.some(c => {
245
+ const columnName = c.as || c.flatName || c.ref?.at(-1).id || c.ref?.at(-1) || c.func
246
+ return columnName === firstStep
247
+ })
248
+ needsElementsOfQueryAsBase =
249
+ tokenPointsToQueryElements &&
250
+ queryElements[firstStep] &&
251
+ /* expand on structure can be addressed */ !queryElements[firstStep].$assocExpand
252
+
253
+ if (needsElementsOfQueryAsBase) $baseLink = { definition: { elements: queryElements }, target: inferred }
254
+ } else {
255
+ // fallback to elements of query source
256
+ $baseLink = null
257
+ }
258
+
259
+ inferArg(token, queryElements, $baseLink, { inQueryModifier: true })
260
+ // if the ref points into the query itself and follows an exposed association
261
+ // to a non-fk column, we must reject the ref, as we can't join with the queries own results
262
+ if (token.isJoinRelevant && needsElementsOfQueryAsBase) {
263
+ // reverse the array, find the last association and calculate the index of the association in non-reversed order
264
+ const assocIndex =
265
+ token.$refLinks.length - 1 - token.$refLinks.reverse().findIndex(link => link.definition.isAssociation)
266
+
267
+ throw new Error(
268
+ `Can follow managed association “${token.ref[assocIndex].id || token.ref[assocIndex]}” only to the keys of its target, not to “${token.ref[assocIndex + 1].id || token.ref[assocIndex + 1]}”`,
269
+ )
270
+ }
271
+ })
272
+ }
273
+
274
+ // walk over all paths in other query properties
275
+ if (where) walkTokenStream(where, true)
276
+ if (groupBy) walkTokenStream(groupBy)
277
+ if (having) walkTokenStream(having)
278
+ if (_.with)
279
+ // consider UPDATE.with
280
+ Object.values(_.with).forEach(val => inferArg(val, queryElements, null, { inXpr: true }))
281
+
282
+ return queryElements
283
+
284
+ /**
285
+ * Recursively drill down into a tokenStream (`where` or `having`) and pass
286
+ * on the information whether the next token is resolved within an `exists` predicates.
287
+ * If such a token has an infix filter, it is not join relevant, because the filter
288
+ * condition is applied to the generated `exists <subquery>` condition.
289
+ *
290
+ * @param {array} tokenStream
291
+ */
292
+ function walkTokenStream(tokenStream, inXpr = false) {
293
+ let skipJoins
294
+ const processToken = t => {
295
+ if (t === 'exists') {
296
+ // no joins for infix filters along `exists <path>`
297
+ skipJoins = true
298
+ } else if (t.xpr) {
299
+ // don't miss an exists within an expression
300
+ t.xpr.forEach(processToken)
301
+ } else {
302
+ inferArg(t, queryElements, null, { inExists: skipJoins, inXpr, inQueryModifier: true })
303
+ skipJoins = false
304
+ }
305
+ }
306
+ tokenStream.forEach(processToken)
307
+ }
308
+ /**
309
+ * Processes references starting with `$self`, which are intended to target other query elements.
310
+ * These `$self` paths must be handled after processing the "regular" columns since they are dependent on other query elements.
311
+ *
312
+ * This function checks for `$self` references that may target other `$self` columns, and delays their processing.
313
+ * `$self` references not targeting other `$self` references are handled by the generic `handleRef` function immediately.
314
+ *
315
+ * @param {array} dollarSelfColumns - An array of column objects containing `$self` references.
316
+ */
317
+ function inferDollarSelfRefs(dollarSelfColumns) {
318
+ do {
319
+ const unprocessedColumns = []
320
+
321
+ for (const currentDollarSelfColumn of dollarSelfColumns) {
322
+ const { ref, inXpr } = currentDollarSelfColumn
323
+ const stepToFind = ref[1]
324
+
325
+ const referencesOtherDollarSelfColumn = dollarSelfColumns.find(
326
+ otherDollarSelfCol =>
327
+ !(stepToFind in queryElements) &&
328
+ otherDollarSelfCol !== currentDollarSelfColumn &&
329
+ (otherDollarSelfCol.as
330
+ ? stepToFind === otherDollarSelfCol.as
331
+ : stepToFind === otherDollarSelfCol.ref?.[otherDollarSelfCol.ref.length - 1]),
332
+ )
333
+
334
+ if (referencesOtherDollarSelfColumn) {
335
+ unprocessedColumns.push(currentDollarSelfColumn)
336
+ } else {
337
+ handleRef(currentDollarSelfColumn, inXpr)
338
+ }
339
+ }
340
+
341
+ dollarSelfColumns = unprocessedColumns
342
+ } while (dollarSelfColumns.length > 0)
343
+ }
344
+
345
+ function handleRef(col, inXpr) {
346
+ inferArg(col, queryElements, null, { inXpr })
347
+ const { definition } = col.$refLinks[col.$refLinks.length - 1]
348
+ if (col.cast)
349
+ // final type overwritten -> element not visible anymore
350
+ setElementOnColumns(col, getElementForCast(col))
351
+ else if ((col.ref.length === 1) & (col.ref[0] === '$user'))
352
+ // shortcut to $user.id
353
+ setElementOnColumns(col, queryElements[col.as || '$user'])
354
+ else setElementOnColumns(col, definition)
355
+ }
356
+ }
357
+
358
+ /**
359
+ * This function is responsible for inferring a query element based on a provided column.
360
+ * It initializes and attaches a non-enumerable `$refLinks` property to the column,
361
+ * which stores an array of objects that represent the corresponding artifact of the ref step.
362
+ * Each object in the `$refLinks` array corresponds to the same index position in the `column.ref` array.
363
+ * Based on the leaf artifact (last object in the `$refLinks` array), the query element is inferred.
364
+ *
365
+ * @param {object} arg - The column object that contains the properties to infer a query element.
366
+ * @param {boolean} [queryElements=true] - Determines whether the inferred element should be inserted into the queries elements.
367
+ * For instance, it's set to false when walking over the where clause.
368
+ * @param {object} [$baseLink=null] - A base reference link, usually it's an object with a definition and a target.
369
+ * Used for infix filters, exists <assoc> and nested projections.
370
+ * @param {object} [context={}] - Contextual information for element inference.
371
+ * @param {boolean} [context.inExists=false] - Flag to control the creation of joins for non-association path traversals.
372
+ * for `exists <assoc>` paths we do not need to create joins for path expressions as they are part of the semi-joined subquery.
373
+ * @param {boolean} [context.inXpr=false] - Flag to signal whether the element is part of an expression.
374
+ * Used to ignore non-persisted elements.
375
+ * @param {boolean} [context.inNestedProjection=false] - Flag to signal whether the element is part of a nested projection.
376
+ *
377
+ * Note:
378
+ * - `inExists` is used to specify cases where no joins should be created for non-association path traversals.
379
+ * It is primarily used for infix filters in `exists assoc[parent.foo='bar']`, where it becomes part of a semi-join.
380
+ * - Columns with a `param` property are parameter references resolved into values only at execution time.
381
+ * - Columns with an `args` property are function calls in expressions.
382
+ * - Columns with a `list` property represent a list of values (e.g., for the IN operator).
383
+ * - Columns with a `SELECT` property represent subqueries.
384
+ *
385
+ * @throws {Error} If an unmanaged association is found in an infix filter path, an error is thrown.
386
+ * @throws {Error} If a non-foreign key traversal is found in an infix filter, an error is thrown.
387
+ * @throws {Error} If a first step is not found in the combined elements, an error is thrown.
388
+ * @throws {Error} If a filter is provided while navigating along non-associations, an error is thrown.
389
+ * @throws {Error} If the same element name is inferred more than once, an error is thrown.
390
+ *
391
+ * @returns {void}
392
+ */
393
+
394
+ function inferArg(arg, queryElements = null, $baseLink = null, context = {}) {
395
+ const { inExists, inXpr, inCalcElement, baseColumn, inInfixFilter, inQueryModifier, inFrom, dollarSelfRefs } =
396
+ context
397
+ if (arg.param || arg.SELECT) return // parameter references are only resolved into values on execution e.g. :val, :1 or ?
398
+ if (arg.args) applyToFunctionArgs(arg.args, inferArg, [null, $baseLink, context])
399
+ if (arg.list) arg.list.forEach(arg => inferArg(arg, null, $baseLink, context))
400
+ if (arg.xpr)
401
+ arg.xpr.forEach((token, i) =>
402
+ inferArg(token, queryElements, $baseLink, { ...context, inXpr: true, inExists: inExists || arg.xpr[i - 1] === 'exists' }),
403
+ ) // e.g. function in expression
404
+
405
+ if (!arg.ref) {
406
+ if (arg.expand && queryElements) queryElements[arg.as] = resolveExpand(arg)
407
+ return
408
+ }
409
+
410
+ // Before the arg is linked, it's meta information should be cleaned up.
411
+ // This may be important if one manipulates the arg object
412
+ // __after__ a query has been fired and re-uses the manipulated query
413
+ defineProperty(arg, '$refLinks', [])
414
+ defineProperty(arg, 'isJoinRelevant', false)
415
+
416
+ // if any path step points to an artifact with `@cds.persistence.skip`
417
+ // we must ignore the element from the queries elements
418
+ let isPersisted = true
419
+ let firstStepIsTableAlias, firstStepIsSelf, expandOnTableAlias
420
+ const firstStepIsDollarMain = arg.ref.length > 1 && arg.ref[0] === '$main'
421
+ if (!inFrom && !firstStepIsDollarMain) {
422
+ firstStepIsTableAlias = arg.ref.length > 1 && arg.ref[0] in sources
423
+ firstStepIsSelf = !firstStepIsTableAlias && arg.ref.length > 1 && ['$self', '$projection'].includes(arg.ref[0])
424
+ expandOnTableAlias = arg.ref.length === 1 && arg.ref[0] in sources && (arg.expand || arg.inline)
425
+ }
426
+
427
+ if (dollarSelfRefs && firstStepIsSelf) {
428
+ defineProperty(arg, 'inXpr', true)
429
+ dollarSelfRefs.push(arg)
430
+ return
431
+ }
432
+ const nameSegments = []
433
+ // if a (segment) of a (structured) foreign key is renamed, we must not include
434
+ // the aliased ref segments into the name of the final foreign key which is e.g. used in
435
+ // on conditions of joins
436
+ const skipAliasedFkSegmentsOfNameStack = []
437
+ let pseudoPath = false
438
+ for(let i = 0; i < arg.ref.length; i++) {
439
+ const step = arg.ref[i]
440
+ const id = step.id || step
441
+ if (i === 0) {
442
+ if(firstStepIsDollarMain) {
443
+ if(inFrom) { // we need to resolve the full from clause first
444
+ context.$mainLazyResolve.push(arg)
445
+ return; // this will be done once the from clause is fully resolved
446
+ } else {
447
+ // replace $main with the alias of the outermost query
448
+ const mainAlias = getMainAlias(inferred)
449
+ arg.$refLinks.push(Object.assign(mainAlias, {$main: true}))
450
+ }
451
+ } else if (id in pseudos.elements) {
452
+ // pseudo path
453
+ arg.$refLinks.push({ definition: pseudos.elements[id], target: pseudos })
454
+ pseudoPath = true // only first path step must be well defined
455
+ nameSegments.push(id)
456
+ } else if ($baseLink && !firstStepIsSelf) {
457
+ const { definition, target } = $baseLink
458
+ const elements = getDefinition(definition.target)?.elements || definition.elements
459
+ if (elements && id in elements) {
460
+ const element = elements[id]
461
+ if (inInfixFilter) {
462
+ const nextStep = arg.ref[1]?.id || arg.ref[1]
463
+ if (isNonForeignKeyNavigation(element, nextStep) || arg.ref[0]?.where) {
464
+ if (inExists) {
465
+ defineProperty($baseLink, 'pathExpressionInsideFilter', true)
466
+ } else if (!inFrom) {
467
+ rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
468
+ }
469
+ }
470
+ }
471
+ const resolvableIn = getDefinition(definition.target) || target
472
+ const $refLink = { definition: elements[id], target: resolvableIn }
473
+ arg.$refLinks.push($refLink)
474
+ } else {
475
+ stepNotFoundInPredecessor(id, definition.name)
476
+ }
477
+ nameSegments.push(id)
478
+ } else if (inFrom) {
479
+ const definition = getDefinition(id) || cds.error`"${id}" not found in the definitions of your model`
480
+ arg.$refLinks.push({ definition, target: definition })
481
+ } else if (firstStepIsTableAlias) {
482
+ arg.$refLinks.push({
483
+ definition: getDefinitionFromSources(sources, id),
484
+ target: getDefinitionFromSources(sources, id),
485
+ })
486
+ } else if (firstStepIsSelf) {
487
+ const nextStep = arg.ref[1]?.id || arg.ref[1]
488
+ let elements = queryElements
489
+ if (nextStep && (!queryElements || !(nextStep in queryElements)) && inferred.outerQueries) {
490
+ const outerQuery = inferred.outerQueries[0]
491
+ if (outerQuery?.elements && nextStep in outerQuery.elements) {
492
+ elements = outerQuery.elements
493
+ }
494
+ }
495
+ arg.$refLinks.push({ definition: { elements }, target: { elements } })
496
+ } else if (arg.ref.length > 1 && inferred.outerQueries?.find(outer => id in outer.sources)) {
497
+ // outer query accessed via alias
498
+ const outerAlias = inferred.outerQueries.find(outer => id in outer.sources)
499
+ arg.$refLinks.push({
500
+ definition: getDefinitionFromSources(outerAlias.sources, id),
501
+ target: getDefinitionFromSources(outerAlias.sources, id),
502
+ })
503
+ } else if (id in $combinedElements) {
504
+ if ($combinedElements[id].length > 1) stepIsAmbiguous(id) // exit
505
+ const definition = $combinedElements[id][0].tableAlias.elements[id]
506
+ const $refLink = { definition, target: $combinedElements[id][0].tableAlias }
507
+ arg.$refLinks.push($refLink)
508
+ nameSegments.push(id)
509
+ } else if (expandOnTableAlias) {
510
+ // expand on table alias
511
+ arg.$refLinks.push({
512
+ definition: getDefinitionFromSources(sources, id),
513
+ target: getDefinitionFromSources(sources, id),
514
+ })
515
+ } else {
516
+ stepNotFoundInCombinedElements(id) // REVISIT: fails with {__proto__:elements)
517
+ }
518
+ } else {
519
+ const { definition } = arg.$refLinks[i - 1]
520
+ const elements = getDefinition(definition.target)?.elements || definition.elements //> go for assoc._target first, instead of assoc as struct
521
+ const element = elements?.[id]
522
+
523
+ if (firstStepIsSelf && element?.isAssociation) {
524
+ throw new Error(
525
+ `Paths starting with “$self” must not contain steps of type “cds.Association”: ref: [ ${arg.ref
526
+ .map(idOnly)
527
+ .join(', ')} ]`,
528
+ )
529
+ }
530
+
531
+ const target = getDefinition(definition.target) || arg.$refLinks[i - 1].target
532
+ if (element) {
533
+ if ($baseLink && inInfixFilter) {
534
+ const nextStep = arg.ref[i + 1]?.id || arg.ref[i + 1]
535
+ if (isNonForeignKeyNavigation(element, nextStep) || arg.ref[i-1]?.where) {
536
+ if (inExists) {
537
+ defineProperty($baseLink, 'pathExpressionInsideFilter', true)
538
+ } else if (!inFrom) {
539
+ rejectNonFkNavigation(element, element.on ? $baseLink.definition.name : nextStep)
540
+ }
541
+ }
542
+ }
543
+ const $refLink = { definition: elements[id], target }
544
+ arg.$refLinks.push($refLink)
545
+ } else if (firstStepIsSelf) {
546
+ stepNotFoundInColumnList(id)
547
+ } else if (arg.ref[0] === '$user' && pseudoPath) {
548
+ // `$user.some.unknown.element` -> no error
549
+ arg.$refLinks.push({ definition: {}, target })
550
+ } else if (id === '$dummy') {
551
+ // `some.known.element.$dummy` -> no error; used by cds.ql to simulate joins
552
+ arg.$refLinks.push({ definition: { name: '$dummy', parent: arg.$refLinks[i - 1].target } })
553
+ defineProperty(arg, 'isJoinRelevant', true)
554
+ } else {
555
+ const notFoundIn = pseudoPath ? arg.ref[i - 1] : getFullPathForLinkedArg(arg)
556
+ stepNotFoundInPredecessor(id, notFoundIn)
557
+ }
558
+ const foreignKeyAlias = Array.isArray(definition.keys)
559
+ ? definition.keys.find(k => {
560
+ if (k.ref.every((step, j) => arg.ref[i + j] === step)) {
561
+ skipAliasedFkSegmentsOfNameStack.push(...k.ref.slice(1))
562
+ return true
563
+ }
564
+ return false
565
+ })?.as
566
+ : null
567
+ if (foreignKeyAlias) nameSegments.push(foreignKeyAlias)
568
+ else if (skipAliasedFkSegmentsOfNameStack[0] === id) skipAliasedFkSegmentsOfNameStack.shift()
569
+ else {
570
+ nameSegments.push(firstStepIsSelf && i === 1 ? element.__proto__.name : id)
571
+ }
572
+ }
573
+
574
+ if (step.where) {
575
+ const danglingFilter = !(arg.ref[i + 1] || arg.expand || arg.inline || inExists)
576
+ const definition = arg.$refLinks[i].definition
577
+ if ((!definition.target && definition.kind !== 'entity') || (!inFrom && !inCalcElement && danglingFilter))
578
+ throw new Error('A filter can only be provided when navigating along associations')
579
+ if (!inFrom && !arg.expand)defineProperty(arg, 'isJoinRelevant', true)
580
+ let skipJoinsForFilter = false
581
+ step.where.forEach(token => {
582
+ if (token === 'exists') {
583
+ // books[exists genre[code='A']].title --> column is join relevant but inner exists filter is not
584
+ skipJoinsForFilter = true
585
+ } else if (token.ref || token.xpr || token.list) {
586
+ // For scoped queries (non-dangling filters in FROM), treat filter contents as EXISTS context
587
+ // because they will become part of an EXISTS subquery
588
+ inferArg(token, false, arg.$refLinks[i], {
589
+ ...context,
590
+ inExists: skipJoinsForFilter || inExists || (inFrom && !danglingFilter),
591
+ inXpr: !!token.xpr,
592
+ inInfixFilter: true,
593
+ inFrom,
594
+ })
595
+ } else if (token.func) {
596
+ if (token.args) {
597
+ applyToFunctionArgs(token.args, inferArg, [
598
+ false,
599
+ arg.$refLinks[i],
600
+ { inExists: skipJoinsForFilter || inExists || (inFrom && !danglingFilter), inXpr: true, inInfixFilter: true, inFrom },
601
+ ])
602
+ }
603
+ }
604
+ })
605
+ }
606
+
607
+ if(!arg.$refLinks[i].$main)
608
+ arg.$refLinks[i].alias = !arg.ref[i + 1] && arg.as ? arg.as : id.split('.').pop()
609
+ const def = getDefinition(arg.$refLinks[i].definition.target)
610
+ if (hasOwnSkip(def) && !isRuntimeView(def)) isPersisted = false
611
+ if (!arg.ref[i + 1]) {
612
+ const flatName = nameSegments.join('_')
613
+ defineProperty(arg, 'flatName', flatName)
614
+ // if column is casted, we overwrite it's origin with the new type
615
+ if (arg.cast) {
616
+ const base = getElementForCast(arg)
617
+ if (insertIntoQueryElements()) queryElements[arg.as || flatName] = getCopyWithAnnos(arg, base)
618
+ } else if (arg.expand) {
619
+ const elements = resolveExpand(arg)
620
+ let elementName
621
+ // expand on table alias
622
+ if (arg.$refLinks.length === 1 && arg.$refLinks[0].definition.kind === 'entity')
623
+ elementName = arg.$refLinks[0].alias
624
+ else elementName = arg.as || flatName
625
+
626
+ if (queryElements) {
627
+ if (queryElements[elementName] !== undefined)
628
+ rejectDuplicatedElement(elementName)
629
+ queryElements[elementName] = elements
630
+ }
631
+ } else if (arg.inline && queryElements) {
632
+ const elements = resolveInline(arg)
633
+ for (const elName in elements) {
634
+ if (queryElements[elName] !== undefined) rejectDuplicatedElement(elName)
635
+ }
636
+ Object.assign(queryElements, elements)
637
+ } else {
638
+ // shortcut for `ref: ['$user']` -> `ref: ['$user', 'id']`
639
+ const leafArt =
640
+ i === 0 && id === '$user' ? arg.$refLinks[i].definition.elements.id : arg.$refLinks[i].definition
641
+ // infer element based on leaf artifact of path
642
+ if (insertIntoQueryElements()) {
643
+ let elementName
644
+ if (arg.as) {
645
+ elementName = arg.as
646
+ } else {
647
+ // if the navigation the user has written differs from the final flat ref - e.g. for renamed foreign keys -
648
+ // the inferred name of the element equals the flat version of the user-written ref.
649
+ const refNavigation = arg.ref
650
+ .slice(firstStepIsSelf || firstStepIsTableAlias ? 1 : 0)
651
+ .map(idOnly)
652
+ .join('_')
653
+ if (refNavigation !== flatName) elementName = refNavigation
654
+ else elementName = flatName
655
+ }
656
+ if (queryElements[elementName] !== undefined)
657
+ rejectDuplicatedElement(elementName)
658
+ const element = getCopyWithAnnos(arg, leafArt)
659
+ queryElements[elementName] = element
660
+ }
661
+ }
662
+ }
663
+ }
664
+ // we need inner joins for the path expressions inside filter expressions after exists predicate
665
+ if ($baseLink?.pathExpressionInsideFilter) defineProperty(arg, 'join', 'inner')
666
+
667
+ // ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
668
+ if (arg.expand) {
669
+ const { $refLinks } = arg
670
+
671
+ const skip = $refLinks.some(link => {
672
+ const def = getDefinition(link.definition.target)
673
+ return hasOwnSkip(def) && !isRuntimeView(def)
674
+ })
675
+ if (skip) {
676
+ $refLinks[$refLinks.length - 1].skipExpand = true
677
+ return
678
+ }
679
+ }
680
+ const leafArt = arg.$refLinks[arg.$refLinks.length - 1].definition
681
+ const virtual = (leafArt.virtual || !isPersisted) && !inXpr
682
+ // check if we need to merge the column `ref` into the join tree of the query
683
+ if (!inFrom && !inExists && !virtual && !inCalcElement) {
684
+ // for a ref inside an `inline` we need to consider the column `ref` which has the `inline` prop
685
+ const colWithBase = baseColumn
686
+ ? { ref: [...baseColumn.ref, ...arg.ref], $refLinks: [...baseColumn.$refLinks, ...arg.$refLinks] }
687
+ : arg
688
+ if (isColumnJoinRelevant(colWithBase)) {
689
+ defineProperty(arg, 'isJoinRelevant', true)
690
+ // join resolved in outer query
691
+ if(!(arg.$refLinks[0].$main && originalQuery.outerQueries))
692
+ inferred.joinTree.mergeColumn(colWithBase, originalQuery.outerQueries)
693
+ }
694
+ }
695
+ if (isCalculatedOnRead(leafArt)) {
696
+ linkCalculatedElement(arg, $baseLink, baseColumn, context)
697
+ }
698
+
699
+ function insertIntoQueryElements() {
700
+ return queryElements && !inXpr && !inInfixFilter && !inQueryModifier
701
+ }
702
+
703
+ /**
704
+ * Resolves and processes the inline attribute of a column in a database query.
705
+ *
706
+ * @param {object} col - The column object with properties: `inline` and `$refLinks`.
707
+ * @param {string} [namePrefix=col.as || col.flatName] - Prefix for naming new columns. Defaults to `col.as` or `col.flatName`.
708
+ * @returns {object} - An object with resolved and processed inline column definitions.
709
+ *
710
+ * Procedure:
711
+ * 1. Iterate through `inline` array. For each `inlineCol`:
712
+ * a. If `inlineCol` equals '*', wildcard elements are processed and added to the `elements` object.
713
+ * b. If `inlineCol` has inline or expand attributes, corresponding functions are called recursively and the resulting elements are added to the `elements` object.
714
+ * c. If `inlineCol` has val or func attributes, new elements are created and added to the `elements` object.
715
+ * d. Otherwise, the corresponding `$refLinks` definition is added to the `elements` object.
716
+ * 2. Returns the `elements` object.
717
+ */
718
+ function resolveInline(col, namePrefix = col.as || col.flatName, outerBase = null) {
719
+ const { inline, $refLinks } = col
720
+ const $leafLink = $refLinks[$refLinks.length - 1]
721
+ if (!$leafLink.definition.target && !$leafLink.definition.elements) {
722
+ throw new Error(
723
+ `Unexpected “inline” on “${col.ref.map(idOnly)}”; can only be used after a reference to a structure, association or table alias`,
724
+ )
725
+ }
726
+ const effectiveBase = outerBase
727
+ ? { ref: [...outerBase.ref, ...col.ref], $refLinks: [...outerBase.$refLinks, ...col.$refLinks] }
728
+ : col
729
+ let elements = {}
730
+ let seenWildcard = false
731
+ inline.forEach(inlineCol => {
732
+ inferArg(inlineCol, null, $leafLink, { inXpr: true, baseColumn: effectiveBase })
733
+ if (inlineCol === '*') {
734
+ if (seenWildcard) throw new Error(`Duplicate wildcard "*" in inline of "${col.as || col.ref.map(idOnly).join('_')}"`)
735
+ seenWildcard = true
736
+ const wildCardElements = {}
737
+ // either the `.elements´ of the struct or the `.elements` of the assoc target
738
+ const targetDef = getDefinition($leafLink.definition.target)
739
+ const leafLinkElements = targetDef?.elements || $leafLink.definition.elements
740
+ const isAssociation = !!$leafLink.definition.target
741
+
742
+ const deferredCalcElements = []
743
+ Object.entries(leafLinkElements).forEach(([k, v]) => {
744
+ const name = namePrefix ? `${namePrefix}_${k}` : k
745
+ // if overwritten/excluded omit from wildcard elements
746
+ // in elements the names are already flat so consider the prefix
747
+ // in excluding, the elements are addressed without the prefix
748
+ if (!(name in elements || col.excluding?.includes(k))) {
749
+ wildCardElements[name] = v
750
+
751
+ if(v.value) {
752
+ // defer linkCalculatedElement calls until after all association joins are registered
753
+ // so that the join tree order is correct
754
+ deferredCalcElements.push({ k, v })
755
+ }
756
+ else if (isAssociation && !v.virtual && v.type !== 'cds.LargeBinary' && !(v.on && !v.keys)) {
757
+ // Check if this element is a foreign key (FK elements don't need join)
758
+ const isFK = $leafLink.definition.keys?.some(key => key.ref[0] === k)
759
+ if (!isFK) {
760
+ // Create a fake column with ref [<inlined assoc>, <element name>] and proper $refLinks
761
+ const fakeCol = {
762
+ ref: [...col.ref, k],
763
+ }
764
+ // Copy $refLinks and add new link for the target element with proper alias
765
+ const fakeRefLinks = [
766
+ ...$refLinks,
767
+ { definition: v, target: targetDef, alias: k }
768
+ ]
769
+ defineProperty(fakeCol, '$refLinks', fakeRefLinks)
770
+ defineProperty(fakeCol, 'isJoinRelevant', true)
771
+ // Merge into join tree
772
+ inferred.joinTree.mergeColumn(fakeCol, originalQuery.outerQueries)
773
+ }
774
+ }
775
+ }
776
+ })
777
+ // link calculated elements after association joins are registered in the join tree
778
+ for (const { k, v } of deferredCalcElements) {
779
+ linkCalculatedElement(
780
+ { ref: [k], $refLinks: [{ definition: v, target: targetDef }] },
781
+ $leafLink,
782
+ )
783
+ }
784
+ elements = { ...elements, ...wildCardElements }
785
+ } else {
786
+ const nameParts = namePrefix ? [namePrefix] : []
787
+ if (inlineCol.as) nameParts.push(inlineCol.as)
788
+ else if (inlineCol.ref) nameParts.push(...inlineCol.ref.map(idOnly))
789
+ const name = nameParts.join('_')
790
+ if (inlineCol.inline) {
791
+ const inlineElements = resolveInline(inlineCol, name, effectiveBase)
792
+ elements = { ...elements, ...inlineElements }
793
+ } else if (inlineCol.expand) {
794
+ const expandElements = resolveExpand(inlineCol)
795
+ elements = { ...elements, [name]: expandElements }
796
+ } else if (inlineCol.val) {
797
+ elements[name] = getCdsTypeForVal(inlineCol.val)
798
+ } else if (inlineCol.func) {
799
+ elements[name] = {}
800
+ } else if (inlineCol.xpr) {
801
+ elements[name] = {}
802
+ } else {
803
+ elements[name] = inlineCol.$refLinks[inlineCol.$refLinks.length - 1].definition
804
+ }
805
+ }
806
+ })
807
+ return elements
808
+ }
809
+
810
+ /**
811
+ * Resolves a query column which has an `expand` property.
812
+ *
813
+ * @param {object} col - The column object with properties: `expand` and `$refLinks`.
814
+ * @returns {object} - A `cds.struct` object with expanded column definitions.
815
+ *
816
+ * Procedure:
817
+ * - if `$leafLink` is an association, constructs an `expandSubquery` and infers a new query structure.
818
+ * Returns a new `cds.struct` if the association has a target cardinality === 1 or a `cds.array` for to many relations.
819
+ * - else constructs an `elements` object based on the refs `expand` found in the expand and returns a new `cds.struct` with these `elements`.
820
+ */
821
+ function resolveExpand(col) {
822
+ const { expand, $refLinks } = col
823
+ const $leafLink = $refLinks?.[$refLinks.length - 1] || inferred.SELECT.from.$refLinks.at(-1) // fallback to anonymous expand
824
+ if (!$leafLink.definition.target && !$leafLink.definition.elements) {
825
+ throw new Error(
826
+ `Unexpected “expand” on “${col.ref.map(idOnly)}”; can only be used after a reference to a structure, association or table alias`,
827
+ )
828
+ }
829
+ // Check for duplicate wildcards before creating the subquery
830
+ let seenWildcard = false
831
+ for (const e of expand) {
832
+ if (e === '*') {
833
+ if (seenWildcard) {
834
+ throw new Error(`Duplicate wildcard "*" in expand of "${col.as || col.ref.map(idOnly).join('_')}"`)
835
+ }
836
+ seenWildcard = true
837
+ }
838
+ }
839
+ const target = getDefinition($leafLink.definition.target)
840
+ if (target) {
841
+ const expandSubquery = {
842
+ SELECT: {
843
+ from: target.name,
844
+ columns: expand.filter(c => !c.inline),
845
+ },
846
+ }
847
+ if (col.excluding) expandSubquery.SELECT.excluding = col.excluding
848
+ if (col.as) expandSubquery.SELECT.as = col.as
849
+ const inferredExpandSubquery = infer(expandSubquery, model)
850
+ const res = $leafLink.definition.is2one
851
+ ? new cds.struct({ elements: inferredExpandSubquery.elements })
852
+ : new cds.array({ items: new cds.struct({ elements: inferredExpandSubquery.elements }) })
853
+ return defineProperty(res, '$assocExpand', true)
854
+ } else if ($leafLink.definition.elements) {
855
+ let elements = {}
856
+ expand.forEach(e => {
857
+ if (e === '*') {
858
+ elements = { ...elements, ...$leafLink.definition.elements }
859
+ } else {
860
+ inferArg(e, false, $leafLink, { inXpr: true })
861
+ if (e.expand) elements[e.as || e.flatName] = resolveExpand(e)
862
+ if (e.inline) elements = { ...elements, ...resolveInline(e) }
863
+ else elements[e.as || e.flatName] = e.$refLinks ? e.$refLinks[e.$refLinks.length - 1].definition : e
864
+ }
865
+ })
866
+ return new cds.struct({ elements })
867
+ }
868
+ }
869
+
870
+ function stepNotFoundInPredecessor(step, def) {
871
+ throw new Error(`"${step}" not found in "${def}"`)
872
+ }
873
+
874
+ function stepIsAmbiguous(step) {
875
+ throw new Error(
876
+ `ambiguous reference to "${step}", write ${Object.values($combinedElements[step])
877
+ .map(ta => `"${ta.index}.${step}"`)
878
+ .join(', ')} instead`,
879
+ )
880
+ }
881
+
882
+ function stepNotFoundInCombinedElements(step) {
883
+ throw new Error(
884
+ `"${step}" not found in the elements of ${Object.values(sources)
885
+ .map(s => s.definition)
886
+ .map(def => `"${def.name || /* subquery */ def.as}"`)
887
+ .join(', ')}`,
888
+ )
889
+ }
890
+
891
+ function stepNotFoundInColumnList(step) {
892
+ const err = [`"${step}" not found in the columns list of query`]
893
+ // if the `elt` from a `$self.elt` path is found in the `$combinedElements` -> hint to remove `$self`
894
+ if (step in $combinedElements)
895
+ err.push(` did you mean ${$combinedElements[step].map(ta => `"${ta.index || ta.as}.${step}"`).join(',')}?`)
896
+ throw new Error(err.join(','))
897
+ }
898
+ }
899
+ function rejectDuplicatedElement(elementName) {
900
+ throw new Error(`Duplicate definition of element “${elementName}”`)
901
+ }
902
+
903
+ function linkCalculatedElement(column, baseLink, baseColumn, context = {}) {
904
+ const calcElement = column.$refLinks?.[column.$refLinks.length - 1].definition || column
905
+ if (alreadySeenCalcElements.has(calcElement)) return
906
+ else alreadySeenCalcElements.add(calcElement)
907
+ const { ref, xpr } = calcElement.value
908
+ if (ref || xpr) {
909
+ const parentElementDefinition = getDefinition(calcElement.parent.name)
910
+ baseLink = { definition: parentElementDefinition, target: parentElementDefinition }
911
+ inferArg(calcElement.value, null, baseLink, { inCalcElement: true, ...context })
912
+ const basePath =
913
+ column.$refLinks?.length > 1
914
+ ? { $refLinks: column.$refLinks.slice(0, -1), ref: column.ref.slice(0, -1) }
915
+ : { $refLinks: [], ref: [] }
916
+ if (baseColumn) {
917
+ basePath.$refLinks.push(...baseColumn.$refLinks)
918
+ basePath.ref.push(...baseColumn.ref)
919
+ }
920
+ mergePathsIntoJoinTree(calcElement.value, basePath)
921
+ }
922
+
923
+ if (calcElement.value.args) {
924
+ const processArgument = (arg, calcElement, column) => {
925
+ const parentElementDefinition = getDefinition(calcElement.parent.name)
926
+ inferArg(
927
+ arg,
928
+ null,
929
+ { definition: parentElementDefinition, target: parentElementDefinition },
930
+ { inCalcElement: true, ...context },
931
+ )
932
+ const basePath =
933
+ column.$refLinks?.length > 1
934
+ ? { $refLinks: column.$refLinks.slice(0, -1), ref: column.ref.slice(0, -1) }
935
+ : { $refLinks: [], ref: [] }
936
+ mergePathsIntoJoinTree(arg, basePath)
937
+ }
938
+
939
+ if (calcElement.value.args) {
940
+ applyToFunctionArgs(calcElement.value.args, processArgument, [calcElement, column])
941
+ }
942
+ }
943
+
944
+ /**
945
+ * Calculates all paths from a given ref and merges them into the join tree.
946
+ * Recursively walks into refs of calculated elements.
947
+ *
948
+ * @param {object} arg with a ref and sibling $refLinks
949
+ * @param {object} basePath with a ref and sibling $refLinks, used for recursion
950
+ */
951
+ function mergePathsIntoJoinTree(arg, basePath = null) {
952
+ basePath = basePath || { $refLinks: [], ref: [] }
953
+ if (arg.ref) {
954
+ arg.$refLinks.forEach((link, i) => {
955
+ const { definition } = link
956
+ if (!definition.value) {
957
+ basePath.$refLinks.push(link)
958
+ basePath.ref.push(arg.ref[i])
959
+ }
960
+ })
961
+ const leafOfCalculatedElementRef = arg.$refLinks[arg.$refLinks.length - 1].definition
962
+ if (leafOfCalculatedElementRef.value) mergePathsIntoJoinTree(leafOfCalculatedElementRef.value, basePath)
963
+
964
+ mergePathIfNecessary(basePath, arg)
965
+ } else if (arg.xpr || arg.args) {
966
+ const prop = arg.xpr ? 'xpr' : 'args'
967
+ let inExists = false
968
+ arg[prop].forEach(step => {
969
+ if (step === 'exists') {
970
+ inExists = true
971
+ return
972
+ }
973
+ let subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
974
+ if (step.ref) {
975
+ if (inExists) {
976
+ // refs following `exists` become subqueries in cqn4sql — only the basePath prefix
977
+ // needs a JOIN (for correlation), the exists-target association itself does not.
978
+ if (subPath.$refLinks.length > 0) {
979
+ inferred.joinTree.mergeColumn(subPath, originalQuery.outerQueries)
980
+ // The exists subquery correlates against the last assoc in basePath,
981
+ // so it's not just a FK access — force a real JOIN.
982
+ const lastLink = subPath.$refLinks[subPath.$refLinks.length - 1]
983
+ if (lastLink.onlyForeignKeyAccess) lastLink.onlyForeignKeyAccess = false
984
+ if (!calcElement.value.isJoinRelevant)
985
+ defineProperty(step, 'isJoinRelevant', true)
986
+ }
987
+ } else {
988
+ step.$refLinks.forEach((link, i) => {
989
+ const { definition } = link
990
+ if (definition.value) {
991
+ mergePathsIntoJoinTree(definition.value, subPath)
992
+ } else {
993
+ subPath.$refLinks.push(link)
994
+ subPath.ref.push(step.ref[i])
995
+ }
996
+ })
997
+ mergePathIfNecessary(subPath, step)
998
+ }
999
+ inExists = false
1000
+ } else if (step.args || step.xpr) {
1001
+ const nestedProp = step.xpr ? 'xpr' : 'args'
1002
+ step[nestedProp].forEach(a => {
1003
+ // reset sub path for each nested argument
1004
+ // e.g. case when <path> then <otherPath> else <anotherPath> end
1005
+ if (!a.ref) subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
1006
+ mergePathsIntoJoinTree(a, subPath)
1007
+ })
1008
+ inExists = false
1009
+ } else {
1010
+ if (step !== 'not') inExists = false
1011
+ }
1012
+ })
1013
+ }
1014
+
1015
+ function mergePathIfNecessary(p, step) {
1016
+ const calcElementIsJoinRelevant = isColumnJoinRelevant(p)
1017
+ if (calcElementIsJoinRelevant) {
1018
+ if (!calcElement.value.isJoinRelevant)
1019
+ defineProperty(step, 'isJoinRelevant',true)
1020
+ inferred.joinTree.mergeColumn(p, originalQuery.outerQueries)
1021
+ } else {
1022
+ // we need to explicitly set the value to false in this case,
1023
+ // e.g. `SELECT from booksCalc.Books { ID, author.{name }, author {name } }`
1024
+ // --> for the inline column, the name is join relevant, while for the expand, it is not
1025
+ defineProperty(step, 'isJoinRelevant', false)
1026
+ }
1027
+ }
1028
+ }
1029
+ }
1030
+
1031
+ /**
1032
+ * Checks whether or not the `ref` of the given column is join relevant.
1033
+ * A `ref` is considered join relevant if it includes an association traversal and:
1034
+ * - the association is unmanaged
1035
+ * - a non-foreign key access is performed
1036
+ * - an infix filter is applied at the association
1037
+ *
1038
+ * @param {object} column the column with the `ref` to check for join relevance
1039
+ * @returns {boolean} true if the column ref needs to be merged into a join tree
1040
+ */
1041
+ function isColumnJoinRelevant(column) {
1042
+ let fkAccess = false
1043
+ let assoc = null
1044
+ for (let i = 0; i < column.ref.length; i++) {
1045
+ const ref = column.ref[i]
1046
+ const link = column.$refLinks[i]
1047
+ if (link.definition.on && link.definition.isAssociation) {
1048
+ if (!column.ref[i + 1]) {
1049
+ if (column.expand && assoc) return true
1050
+ // if unmanaged assoc is exposed, ignore it
1051
+ return false
1052
+ }
1053
+ return true
1054
+ }
1055
+ if (assoc) {
1056
+ // if(!link.definition.isAssociation) continue
1057
+ let fkIndex = assoc.keys?.findIndex(key => key.ref.every((step, j) => column.ref[i + j] === step))
1058
+ // foreign key access without filters never join relevant
1059
+ if (fkIndex !== -1) {
1060
+ if(column.ref.slice(i).some(s => s.where)) continue // probably join relevant later on
1061
+ fkAccess = true
1062
+ assoc = null
1063
+ continue
1064
+ }
1065
+ // <assoc>.<anotherAssoc>.<…> is join relevant as <anotherAssoc> is not fk of <assoc>
1066
+ return true
1067
+ }
1068
+ if (link.definition.target && link.definition.keys) {
1069
+ if (column.ref[i + 1] || assoc) fkAccess = false
1070
+ else fkAccess = true
1071
+ assoc = link.definition
1072
+ if (ref.where) {
1073
+ // always join relevant except for expand assoc
1074
+ if (column.expand && !column.ref[i + 1]) return false
1075
+ return true
1076
+ }
1077
+ }
1078
+ }
1079
+
1080
+ if (!assoc) return false
1081
+ if (fkAccess) return false
1082
+ return true
1083
+ }
1084
+
1085
+ /**
1086
+ * Iterates over all `$combinedElements` of the `query` and puts them into the `query`s `elements`,
1087
+ * if there is not already an element with the same name present.
1088
+ */
1089
+ function inferElementsFromWildCard(queryElements) {
1090
+ const exclude = _.excluding ? x => _.excluding.includes(x) : () => false
1091
+
1092
+ if (Object.keys(queryElements).length === 0 && aliases.length === 1) {
1093
+ const { elements } = getDefinitionFromSources(sources, aliases[0])
1094
+ // only one query source and no overwritten columns
1095
+ for (const k in elements) {
1096
+ if (!exclude(k)) {
1097
+ const element = elements[k]
1098
+ if (element.type !== 'cds.LargeBinary') {
1099
+ queryElements[k] = element
1100
+ }
1101
+ // only relevant if we actually select the calculated element
1102
+ if (originalQuery.SELECT && isCalculatedOnRead(element)) {
1103
+ linkCalculatedElement(element)
1104
+ }
1105
+ }
1106
+ }
1107
+ return
1108
+ }
1109
+
1110
+ const ambiguousElements = {}
1111
+ Object.entries($combinedElements).forEach(([name, tableAliases]) => {
1112
+ if (Object.keys(tableAliases).length > 1) {
1113
+ ambiguousElements[name] = tableAliases
1114
+ return ambiguousElements[name]
1115
+ }
1116
+ if (exclude(name) || name in queryElements) return true
1117
+ const element = tableAliases[0].tableAlias.elements[name]
1118
+ if (element.type !== 'cds.LargeBinary') queryElements[name] = element
1119
+ if (isCalculatedOnRead(element)) {
1120
+ linkCalculatedElement(element)
1121
+ }
1122
+ })
1123
+
1124
+ if (Object.keys(ambiguousElements).length > 0) throwAmbiguousWildcardError()
1125
+
1126
+ function throwAmbiguousWildcardError() {
1127
+ const err = []
1128
+ err.push('Ambiguous wildcard elements:')
1129
+ Object.keys(ambiguousElements).forEach(name => {
1130
+ const tableAliasNames = Object.values(ambiguousElements[name]).map(v => v.index)
1131
+ err.push(
1132
+ ` select "${name}" explicitly with ${tableAliasNames.map(taName => `"${taName}.${name}"`).join(', ')}`,
1133
+ )
1134
+ })
1135
+ throw new Error(err.join('\n'))
1136
+ }
1137
+ }
1138
+
1139
+ /**
1140
+ * Returns a new object which is the inferred element for the given `col`.
1141
+ * A cast type (via cast function) on the column gets preserved.
1142
+ *
1143
+ * @param {object} col
1144
+ * @returns object
1145
+ */
1146
+ function getElementForXprOrSubquery(col, queryElements, dollarSelfRefs) {
1147
+ const { xpr } = col
1148
+ let skipJoins = false
1149
+ xpr?.forEach(token => {
1150
+ if (token === 'exists') {
1151
+ // no joins for infix filters along `exists <path>`
1152
+ skipJoins = true
1153
+ } else {
1154
+ inferArg(token, queryElements, null, { inExists: skipJoins, inXpr: true, dollarSelfRefs })
1155
+ skipJoins = false
1156
+ }
1157
+ })
1158
+ const base = getElementForCast(col.cast ? col : xpr?.[0] || col)
1159
+ if (col.key) base.key = col.key // > preserve key on column
1160
+ return getCopyWithAnnos(col, base)
1161
+ }
1162
+
1163
+ /**
1164
+ * Returns an object with the cast-type defined in the cast of the `thing`.
1165
+ * If no cast property is present, it just returns an empty object.
1166
+ * The type of the cast is mapped to the `cds` type if possible.
1167
+ *
1168
+ * @param {object} thing with the cast property
1169
+ * @returns {object}
1170
+ */
1171
+ function getElementForCast(thing) {
1172
+ const { cast, $refLinks } = thing
1173
+ if (!cast) return {}
1174
+ if ($refLinks?.[$refLinks.length - 1].definition.elements)
1175
+ // no cast on structure
1176
+ cds.error`Structured elements can't be cast to a different type`
1177
+ const cdsType = cdsTypes[cast.type]
1178
+ thing.cast = cdsType ? new cdsType.constructor(cast) : cast
1179
+ if (cdsType)
1180
+ thing.cast.type = cdsType._type
1181
+ return thing.cast
1182
+ }
1183
+
1184
+ /**
1185
+ * return a new object based on @param base
1186
+ * with all annotations found in @param from
1187
+ *
1188
+ * @param {object} from
1189
+ * @param {object} base
1190
+ * @returns {object} a copy of @param base with all annotations of @param from
1191
+ * @TODO prototype based
1192
+ */
1193
+ function getCopyWithAnnos(from, base) {
1194
+ const result = { ...base }
1195
+ // REVISIT: we don't need to and hence should not handle annotations at runtime
1196
+ for (const prop in from) {
1197
+ if (prop.startsWith('@')) result[prop] = from[prop]
1198
+ }
1199
+
1200
+ if (from.as && base.name !== from.as) defineProperty(result, 'name', from.as) // TODO double check if this is needed
1201
+ // in subqueries we need the linked element if an outer query accesses it
1202
+ return Object.setPrototypeOf(result, base)
1203
+ }
1204
+
1205
+ function getCdsTypeForVal(val) {
1206
+ // REVISIT: JS null should have a type for proper DB layer conversion logic
1207
+ // if(val === null) return {type:'cds.String'}
1208
+ switch (typeof val) {
1209
+ case 'string':
1210
+ return new cdsTypes.String.constructor()
1211
+ case 'boolean':
1212
+ return new cdsTypes.Boolean.constructor()
1213
+ case 'number':
1214
+ return Number.isSafeInteger(val) ? new cdsTypes.Integer.constructor() : new cdsTypes.Decimal.constructor()
1215
+ default:
1216
+ return {}
1217
+ }
1218
+ }
1219
+
1220
+ function getDefinitionFromSources(sources, id) {
1221
+ return sources[id].definition
1222
+ }
1223
+
1224
+ /**
1225
+ * Returns the csn path as string for a given column ref with sibling $refLinks
1226
+ *
1227
+ * @param {object} arg
1228
+ * @returns {string}
1229
+ */
1230
+ function getFullPathForLinkedArg(arg) {
1231
+ let firstStepIsEntity = false
1232
+ return arg.$refLinks.reduce((res, cur, i) => {
1233
+ if (cur.definition.kind === 'entity') {
1234
+ firstStepIsEntity = true
1235
+ if (arg.$refLinks.length === 1) return `${cur.definition.name}`
1236
+ return `${cur.definition.name}`
1237
+ } else if (cur.definition.SELECT) {
1238
+ return `${cur.definition.as}`
1239
+ }
1240
+ const dot = i === 1 && firstStepIsEntity ? ':' : '.' // divide with colon if first step is entity
1241
+ return res !== '' ? res + dot + cur.definition.name : cur.definition.name
1242
+ }, '')
1243
+ }
1244
+ }
1245
+
1246
+ /**
1247
+ * Determines if a given association is a non-foreign key navigation.
1248
+ *
1249
+ * @param {Object} assoc - The association.
1250
+ * @param {Object} nextStep - The next step in the navigation path.
1251
+ * @returns {boolean} - Returns true if the next step is a non-foreign key navigation, otherwise false.
1252
+ */
1253
+ function isNonForeignKeyNavigation(assoc, nextStep) {
1254
+ if (!nextStep || !assoc.target) return false
1255
+
1256
+ return assoc.on || !isForeignKeyOf(nextStep, assoc)
1257
+ }
1258
+
1259
+ function rejectNonFkNavigation(assoc, additionalInfo) {
1260
+ if (assoc.on) {
1261
+ throw new Error(`Unexpected unmanaged association “${assoc.name}” in filter expression of “${additionalInfo}”`)
1262
+ }
1263
+ throw new Error(`Only foreign keys of “${assoc.name}” can be accessed in infix filter, but found “${additionalInfo}”`)
1264
+ }
1265
+
1266
+ /**
1267
+ * Returns true if e is a foreign key of assoc.
1268
+ * this function is also compatible with unfolded csn (UCSN),
1269
+ * where association do not have foreign keys anymore.
1270
+ */
1271
+ function isForeignKeyOf(e, assoc) {
1272
+ if (!assoc.isAssociation) return false
1273
+ return e in (assoc.elements || assoc.foreignKeys || {})
1274
+ }
1275
+ const idOnly = ref => ref.id || ref
1276
+
1277
+ function applyToFunctionArgs(funcArgs, cb, cbArgs) {
1278
+ if (Array.isArray(funcArgs)) funcArgs.forEach(arg => cb(arg, ...cbArgs))
1279
+ else if (typeof funcArgs === 'object') Object.keys(funcArgs).forEach(prop => cb(funcArgs[prop], ...cbArgs))
1280
+ }
1281
+
1282
+ function getMainAlias (query) {
1283
+ let mainAlias
1284
+ if (query.outerQueries) mainAlias = query.outerQueries[0].SELECT?.from.$refLinks.at(-1)
1285
+ else mainAlias = query.SELECT?.from.$refLinks.at(-1)
1286
+ if(!mainAlias) throw new Error('Cannot determine main query source for $main, please report this')
1287
+ return mainAlias
1288
+ }
1289
+
1290
+ module.exports = infer