@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.
- package/CHANGELOG.md +28 -0
- package/bin/debug.js +6 -2
- package/bin/up.js +1 -1
- package/bin/watch.js +1 -0
- package/lib/build/plugins/helm/files/templates/_deployment_helpers.tpl +2 -0
- package/lib/build/plugins/nodejs/index.js +6 -3
- package/lib/build/util.js +1 -1
- package/lib/init/template/ams/files/containerize.yaml.hbs +1 -0
- package/lib/init/template/approuter/files/values.yaml.hbs +9 -1
- package/lib/init/template/data/as-csv.js +4 -2
- package/lib/init/template/data/as-json.js +4 -1
- package/lib/init/template/github-actions/files/workflows/cf.yaml.hbs +6 -6
- package/lib/init/template/ias/files/values.yaml.hbs +4 -4
- package/lib/init/template/portal/files/values.yaml.hbs +12 -0
- package/lib/init/template/sample/files/app/browse/fiori-service.cds +2 -0
- package/lib/init/template/typer/files/xsconfig.json +1 -1
- package/lib/init/template/typescript/files/package.json +1 -1
- package/lib/lint/eslintHelp.json +1 -1
- package/lib/watch/esm-resolve-registration.js +27 -0
- package/lib/watch/vite-watch.js +1 -1
- package/node_modules/@cap-js/db-service/CHANGELOG.md +768 -0
- package/node_modules/@cap-js/db-service/LICENSE +201 -0
- package/node_modules/@cap-js/db-service/README.md +26 -0
- package/node_modules/@cap-js/db-service/index.js +19 -0
- package/node_modules/@cap-js/db-service/lib/InsertResults.js +122 -0
- package/node_modules/@cap-js/db-service/lib/SQLService.js +581 -0
- package/node_modules/@cap-js/db-service/lib/common/DatabaseService.js +179 -0
- package/node_modules/@cap-js/db-service/lib/common/factory.d.ts +5 -0
- package/node_modules/@cap-js/db-service/lib/common/generic-pool.js +310 -0
- package/node_modules/@cap-js/db-service/lib/common/session-context.js +46 -0
- package/node_modules/@cap-js/db-service/lib/converters.d.ts +24 -0
- package/node_modules/@cap-js/db-service/lib/cql-functions.js +464 -0
- package/node_modules/@cap-js/db-service/lib/cqn2pql.js +116 -0
- package/node_modules/@cap-js/db-service/lib/cqn2sql.js +1612 -0
- package/node_modules/@cap-js/db-service/lib/cqn4sql.js +2895 -0
- package/node_modules/@cap-js/db-service/lib/deep-queries.js +290 -0
- package/node_modules/@cap-js/db-service/lib/fill-in-keys.js +75 -0
- package/node_modules/@cap-js/db-service/lib/infer/cqn.d.ts +42 -0
- package/node_modules/@cap-js/db-service/lib/infer/index.js +1290 -0
- package/node_modules/@cap-js/db-service/lib/infer/join-tree.js +278 -0
- package/node_modules/@cap-js/db-service/lib/infer/pseudos.js +24 -0
- package/node_modules/@cap-js/db-service/lib/search.js +220 -0
- package/node_modules/@cap-js/db-service/lib/utils.js +177 -0
- package/node_modules/@cap-js/db-service/package.json +36 -0
- package/node_modules/@cap-js/sqlite/CHANGELOG.md +404 -0
- package/node_modules/@cap-js/sqlite/LICENSE +201 -0
- package/node_modules/@cap-js/sqlite/README.md +34 -0
- package/node_modules/@cap-js/sqlite/cds-plugin.js +0 -0
- package/node_modules/@cap-js/sqlite/index.js +2 -0
- package/node_modules/@cap-js/sqlite/lib/SQLiteService.js +324 -0
- package/node_modules/@cap-js/sqlite/lib/cql-functions.js +160 -0
- package/node_modules/@cap-js/sqlite/lib/node-sqlite.js +36 -0
- package/node_modules/@cap-js/sqlite/lib/session.json +3 -0
- package/node_modules/@cap-js/sqlite/lib/sql.js.js +99 -0
- package/node_modules/@cap-js/sqlite/package.json +68 -0
- package/node_modules/@sap/cds-compiler/CHANGELOG.md +14 -0
- package/node_modules/@sap/cds-compiler/bin/cdsse.js +1 -1
- package/node_modules/@sap/cds-compiler/bin/cdsv2m.js +1 -1
- package/node_modules/@sap/cds-compiler/doc/IncompatibleChanges_v7.md +20 -13
- package/node_modules/@sap/cds-compiler/lib/base/builtins.js +4 -1
- package/node_modules/@sap/cds-compiler/lib/base/message-registry.js +15 -2
- package/node_modules/@sap/cds-compiler/lib/checks/defaultValues.js +0 -1
- package/node_modules/@sap/cds-compiler/lib/compiler/resolve.js +6 -5
- package/node_modules/@sap/cds-compiler/lib/compiler/tweak-assocs.js +27 -6
- package/node_modules/@sap/cds-compiler/lib/edm/annotations/edmJson.js +5 -0
- package/node_modules/@sap/cds-compiler/lib/json/from-csn.js +3 -3
- package/node_modules/@sap/cds-compiler/lib/json/to-csn.js +5 -4
- package/node_modules/@sap/cds-compiler/lib/parsers/AstBuildingParser.js +14 -3
- package/node_modules/@sap/cds-compiler/lib/tool-lib/enrichCsn.js +1 -1
- package/node_modules/@sap/cds-compiler/lib/transform/db/cdsPersistence.js +3 -2
- package/node_modules/@sap/cds-compiler/lib/transform/db/views.js +2 -2
- package/node_modules/@sap/cds-compiler/lib/transform/transformUtils.js +1 -2
- package/node_modules/@sap/cds-compiler/package.json +1 -1
- package/node_modules/iconv-lite/encodings/sbcs-data.js +2 -0
- package/node_modules/iconv-lite/encodings/utf32.js +10 -3
- package/node_modules/iconv-lite/package.json +2 -2
- package/node_modules/iconv-lite/types/encodings.d.ts +2 -0
- package/node_modules/ws/lib/receiver.js +15 -32
- package/node_modules/ws/lib/websocket-server.js +4 -4
- package/node_modules/ws/lib/websocket.js +4 -4
- package/node_modules/ws/package.json +5 -1
- 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
|