@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,2895 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const cds = require('@sap/cds')
|
|
4
|
+
cds.infer.target ??= q => q._target || q.target // instanceof cds.entity ? q._target : q.target
|
|
5
|
+
|
|
6
|
+
const infer = require('./infer')
|
|
7
|
+
const { computeColumnsToBeSearched } = require('./search')
|
|
8
|
+
const {
|
|
9
|
+
prettyPrintRef,
|
|
10
|
+
isCalculatedOnRead,
|
|
11
|
+
isCalculatedElement,
|
|
12
|
+
getImplicitAlias: _getImplicitAlias,
|
|
13
|
+
defineProperty,
|
|
14
|
+
getModelUtils,
|
|
15
|
+
hasOwnSkip,
|
|
16
|
+
isRuntimeView,
|
|
17
|
+
} = require('./utils')
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* For operators of <eqOps>, this is replaced by comparing all leaf elements with null, combined with and.
|
|
21
|
+
* If there are at least two leaf elements and if there are tokens before or after the recognized pattern, we enclose the resulting condition in parens (...)
|
|
22
|
+
*/
|
|
23
|
+
const eqOps = [['is'], ['='] /* ['=='] */]
|
|
24
|
+
/**
|
|
25
|
+
* For operators of <notEqOps>, do the same but use `or` instead of `and`.
|
|
26
|
+
* This ensures that `not struct == <value>` is the same as `struct != <value>`.
|
|
27
|
+
*/
|
|
28
|
+
const notEqOps = [['is', 'not'], ['<>'], ['!=']]
|
|
29
|
+
/**
|
|
30
|
+
* not supported in comparison w/ struct because of unclear semantics
|
|
31
|
+
*/
|
|
32
|
+
const notSupportedOps = [['>'], ['<'], ['>='], ['<='], ['*'], ['+'], ['-'], ['/']]
|
|
33
|
+
|
|
34
|
+
const allOps = eqOps.concat(eqOps).concat(notEqOps).concat(notSupportedOps)
|
|
35
|
+
|
|
36
|
+
const { pseudos } = require('./infer/pseudos')
|
|
37
|
+
/**
|
|
38
|
+
* Transforms a CDL style query into SQL-Like CQN:
|
|
39
|
+
* - transform association paths in `from` to `WHERE exists` subqueries
|
|
40
|
+
* - transforms columns into their flat representation.
|
|
41
|
+
* 1. Flatten managed associations to their foreign
|
|
42
|
+
* 2. Flatten structures to their leafs
|
|
43
|
+
* 3. Replace join-relevant ref paths (i.e. non-fk accesses in association paths) with the correct join alias
|
|
44
|
+
* - transforms `expand` columns into special, normalized subqueries
|
|
45
|
+
* - transform `where` clause.
|
|
46
|
+
* That is the flattening of all `ref`s and the expansion of `where exists` predicates
|
|
47
|
+
* - rewrites `from` clause:
|
|
48
|
+
* Each join relevant association path traversal is translated to a join condition.
|
|
49
|
+
*
|
|
50
|
+
* `cqn4sql` is applied recursively to all queries found in `from`, `columns` and `where`
|
|
51
|
+
* of a query.
|
|
52
|
+
*
|
|
53
|
+
* @param {object} originalQuery
|
|
54
|
+
* @param {object} model
|
|
55
|
+
* @returns {object} transformedQuery the transformed query
|
|
56
|
+
*/
|
|
57
|
+
function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
|
|
58
|
+
const getImplicitAlias = str => _getImplicitAlias(str, useTechnicalAlias)
|
|
59
|
+
let inferred = typeof originalQuery === 'string' ? cds.parse.cql(originalQuery) : cds.ql.clone(originalQuery)
|
|
60
|
+
const hasCustomJoins =
|
|
61
|
+
originalQuery.SELECT?.from.args && (!originalQuery.joinTree || originalQuery.joinTree.isInitial)
|
|
62
|
+
|
|
63
|
+
if (!hasCustomJoins && inferred.SELECT?.search) {
|
|
64
|
+
// we need an instance of query because the elements of the query are needed for the calculation of the search columns
|
|
65
|
+
if (!inferred.SELECT.elements) Object.setPrototypeOf(inferred, SELECT.class.prototype)
|
|
66
|
+
const searchTerm = getSearch(inferred.SELECT.search, inferred)
|
|
67
|
+
if (searchTerm) {
|
|
68
|
+
// Search target can be a navigation, in that case use _target to get the correct entity
|
|
69
|
+
const { where, having } = transformSearch(searchTerm)
|
|
70
|
+
if (where) inferred.SELECT.where = where
|
|
71
|
+
else if (having) inferred.SELECT.having = having
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// query modifiers can also be defined in from ref leaf infix filter
|
|
75
|
+
// > SELECT from bookshop.Books[order by price] {ID}
|
|
76
|
+
if (inferred.SELECT?.from.ref?.at(-1).id) {
|
|
77
|
+
assignQueryModifiers(inferred.SELECT, inferred.SELECT.from.ref.at(-1))
|
|
78
|
+
}
|
|
79
|
+
if (inferred.DELETE?.from.ref?.at(-1).id) {
|
|
80
|
+
assignQueryModifiers(inferred.DELETE, inferred.DELETE.from.ref.at(-1))
|
|
81
|
+
}
|
|
82
|
+
if (inferred.UPDATE?.entity.ref?.at(-1).id) {
|
|
83
|
+
assignQueryModifiers(inferred.UPDATE, inferred.UPDATE.entity.ref.at(-1))
|
|
84
|
+
}
|
|
85
|
+
inferred = infer(inferred, model, useTechnicalAlias)
|
|
86
|
+
const { getLocalizedName, isLocalized, getDefinition } = getModelUtils(model, originalQuery) // TODO: pass model to getModelUtils
|
|
87
|
+
// if the query has custom joins we don't want to transform it
|
|
88
|
+
// TODO: move all the way to the top of this function once cds.infer supports joins as well
|
|
89
|
+
// we need to infer the query even if no transformation will happen because cds.infer can't calculate the target
|
|
90
|
+
if (hasCustomJoins) return originalQuery
|
|
91
|
+
|
|
92
|
+
let transformedQuery = cds.ql.clone(inferred)
|
|
93
|
+
const kind = inferred.kind || Object.keys(inferred)[0]
|
|
94
|
+
|
|
95
|
+
if (inferred.INSERT || inferred.UPSERT) {
|
|
96
|
+
transformedQuery = transformQueryForInsertUpsert(kind)
|
|
97
|
+
} else {
|
|
98
|
+
const queryProp = inferred[kind]
|
|
99
|
+
const { entity, where } = queryProp
|
|
100
|
+
const from = queryProp.from
|
|
101
|
+
|
|
102
|
+
const transformedProp = { __proto__: queryProp } // IMPORTANT: don't lose anything you might not know of
|
|
103
|
+
const queryNeedsJoins = inferred.joinTree && !inferred.joinTree.isInitial
|
|
104
|
+
|
|
105
|
+
// Transform the existing where, prepend table aliases, and so on...
|
|
106
|
+
if (where) {
|
|
107
|
+
transformedProp.where = getTransformedTokenStream(where, { prop: 'where' })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Transform the from clause: association path steps turn into `WHERE EXISTS` subqueries.
|
|
111
|
+
// The already transformed `where` clause is then glued together with the resulting subqueries.
|
|
112
|
+
const { transformedWhere, transformedFrom } = getTransformedFrom(from || entity, transformedProp.where)
|
|
113
|
+
|
|
114
|
+
// build a subquery for DELETE / UPDATE queries with path expressions and match the primary keys
|
|
115
|
+
if (queryNeedsJoins && (inferred.UPDATE || inferred.DELETE)) {
|
|
116
|
+
const prop = inferred.UPDATE ? 'UPDATE' : 'DELETE'
|
|
117
|
+
const subquery = {
|
|
118
|
+
SELECT: {
|
|
119
|
+
from: { ...(from || entity) },
|
|
120
|
+
columns: [], // primary keys of the query target will be added later
|
|
121
|
+
where: [...where],
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
// The alias of the original query is now the alias for the subquery
|
|
125
|
+
// so that potential references in the where clause to the alias match.
|
|
126
|
+
// Hence, replace the alias of the original query with the next
|
|
127
|
+
// available alias, so that each alias is unique.
|
|
128
|
+
const uniqueSubqueryAlias = getNextAvailableTableAlias(transformedFrom.as)
|
|
129
|
+
transformedFrom.as = uniqueSubqueryAlias
|
|
130
|
+
|
|
131
|
+
// calculate the primary keys of the target entity, there is always exactly
|
|
132
|
+
// one query source for UPDATE / DELETE
|
|
133
|
+
const queryTarget = Object.values(inferred.sources)[0].definition
|
|
134
|
+
const primaryKey = { list: getPrimaryKey(queryTarget, uniqueSubqueryAlias) }
|
|
135
|
+
// match primary keys of the target entity with the subquery
|
|
136
|
+
primaryKey.list.forEach(k => subquery.SELECT.columns.push({ ref: k.ref.slice(1) }))
|
|
137
|
+
|
|
138
|
+
const transformedSubquery = cqn4sql(subquery, model, useTechnicalAlias)
|
|
139
|
+
|
|
140
|
+
// replace where condition of original query with the transformed subquery
|
|
141
|
+
// correlate UPDATE / DELETE query with subquery by primary key matches
|
|
142
|
+
transformedQuery[prop].where = [primaryKey, 'in', transformedSubquery]
|
|
143
|
+
|
|
144
|
+
if (prop === 'UPDATE') transformedQuery.UPDATE.entity = transformedFrom
|
|
145
|
+
else transformedQuery.DELETE.from = transformedFrom
|
|
146
|
+
|
|
147
|
+
return transformedQuery
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (inferred.SELECT) {
|
|
151
|
+
transformedQuery = transformSelectQuery(queryProp, transformedFrom, transformedWhere, transformedQuery)
|
|
152
|
+
} else {
|
|
153
|
+
if (from) {
|
|
154
|
+
transformedProp.from = transformedFrom
|
|
155
|
+
} else if (!queryNeedsJoins) {
|
|
156
|
+
transformedProp.entity = transformedFrom
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (transformedWhere?.length > 0) {
|
|
160
|
+
transformedProp.where = transformedWhere
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
transformedQuery[kind] = transformedProp
|
|
164
|
+
|
|
165
|
+
if (inferred.UPDATE?.with) {
|
|
166
|
+
Object.entries(inferred.UPDATE.with).forEach(([key, val]) => {
|
|
167
|
+
const transformed = getTransformedTokenStream([val])
|
|
168
|
+
inferred.UPDATE.with[key] = transformed[0]
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (queryNeedsJoins) {
|
|
174
|
+
transformedQuery[kind].from = translateAssocsToJoins(transformedQuery[kind].from)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (cds.env.features.runtime_views) processRuntimeViews(transformedQuery, model)
|
|
179
|
+
|
|
180
|
+
return transformedQuery
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* If the target entity is annotated with persistence skip and has an underlying db entity,
|
|
184
|
+
* we treat it as a runtime view and transform it into a CTE.
|
|
185
|
+
*
|
|
186
|
+
* @param {object} transformedQuery - The query object to be transformed.
|
|
187
|
+
* @param {string} model - The data model used for inference and transformation.
|
|
188
|
+
*/
|
|
189
|
+
function processRuntimeViews(transformedQuery, model) {
|
|
190
|
+
const currentDef = transformedQuery._target
|
|
191
|
+
|
|
192
|
+
if (hasOwnSkip(currentDef)) {
|
|
193
|
+
if (!isRuntimeView(currentDef)) throw new Error(`${currentDef.name} is not a runtime view`)
|
|
194
|
+
|
|
195
|
+
addWith(currentDef, transformedQuery, model)
|
|
196
|
+
updateRefsWithRTVAlias(transformedQuery._with, transformedQuery)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Recursively call cqn4sql for all nested runtime views to calculate cte and
|
|
202
|
+
* add it as a with clause to the transformed query.
|
|
203
|
+
* Alias the runtime view with a unique alias and update all references to the runtime view to point to the alias.
|
|
204
|
+
*
|
|
205
|
+
* @param {object} rootDefinition - The root definition of the query. This is used to recursively process nested runtime views.
|
|
206
|
+
* @param {object} transformedQuery - The query object to be transformed.
|
|
207
|
+
* @param {string} model - The data model used for infer and cqn4sql.
|
|
208
|
+
*/
|
|
209
|
+
function addWith(rootDefinition, transformedQuery, model) {
|
|
210
|
+
if (!rootDefinition?.query) return
|
|
211
|
+
|
|
212
|
+
// early exit if already processed
|
|
213
|
+
if (transformedQuery._with?.some(w => w._source === rootDefinition)) return
|
|
214
|
+
|
|
215
|
+
const q = cds.ql.clone(rootDefinition.query)
|
|
216
|
+
if (q.SELECT) {
|
|
217
|
+
if (!q.SELECT.columns) q.SELECT.columns = ['*']
|
|
218
|
+
if (q.SELECT.columns.includes('*')) {
|
|
219
|
+
// cache element names for faster lookup
|
|
220
|
+
const existingColumns = new Set(
|
|
221
|
+
q.SELECT.columns
|
|
222
|
+
.map(col => col.as || col.ref?.at(-1))
|
|
223
|
+
.filter(Boolean)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
for (let el of rootDefinition.elements) {
|
|
227
|
+
if (el.type === 'cds.LargeBinary' && !existingColumns.has(el.name)) {
|
|
228
|
+
q.SELECT.columns.push({ ref: [el.name] })
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const inferredDQ = infer(q, model, useTechnicalAlias)
|
|
234
|
+
inferredDQ._with = transformedQuery._with
|
|
235
|
+
const transformedDQ = cqn4sql(inferredDQ, model, useTechnicalAlias)
|
|
236
|
+
|
|
237
|
+
if (q.SELECT?.from?.args) {
|
|
238
|
+
for (const arg of q.SELECT.from.args) {
|
|
239
|
+
addWith(arg.$refLinks.at(-1).definition, inferredDQ, model)
|
|
240
|
+
arg.as ??= arg.ref.at(-1).split('.').at(-1) // apply @sap/cds-compiler default alias
|
|
241
|
+
updateRefsWithRTVAlias(inferredDQ._with, transformedDQ, arg.ref)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const newWiths = transformedDQ._with || []
|
|
246
|
+
const rootDefinitionName = rootDefinition.name
|
|
247
|
+
|
|
248
|
+
defineProperty(transformedDQ, '_source', rootDefinition)
|
|
249
|
+
const alias = `RTV_${getImplicitAlias(rootDefinitionName)}`
|
|
250
|
+
transformedDQ.as = transformedDQ.joinTree.addNextAvailableTableAlias(alias, newWiths, rootDefinitionName)
|
|
251
|
+
|
|
252
|
+
// update SELECT.from with runtime view alias
|
|
253
|
+
if (hasOwnSkip(transformedDQ._target)) updateRefsWithRTVAlias(transformedDQ._with, transformedDQ)
|
|
254
|
+
|
|
255
|
+
if (transformedDQ._with) delete transformedDQ._with
|
|
256
|
+
newWiths.push(transformedDQ)
|
|
257
|
+
|
|
258
|
+
// propagate with clauses
|
|
259
|
+
if (!transformedQuery._with) transformedQuery._with = newWiths
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function updateRefsWithRTVAlias(_with, query, ref) {
|
|
263
|
+
if (!_with?.length) return
|
|
264
|
+
|
|
265
|
+
const _updateRef = (ref) => {
|
|
266
|
+
const refAlias = ref[0]
|
|
267
|
+
if (/RTV_$/.test(refAlias)) return
|
|
268
|
+
for (const w of _with) {
|
|
269
|
+
const aliasValue = w.joinTree._queryAliases.get(refAlias)
|
|
270
|
+
if (aliasValue) {
|
|
271
|
+
ref[0] = aliasValue
|
|
272
|
+
if (query.joinTree?._queryAliases) {
|
|
273
|
+
query.joinTree._queryAliases.set(refAlias, aliasValue)
|
|
274
|
+
}
|
|
275
|
+
break
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (ref) return _updateRef(ref)
|
|
281
|
+
|
|
282
|
+
if (query.SELECT.from.args) for (const arg of query.SELECT.from.args) _updateRef(arg.ref)
|
|
283
|
+
else if (query.SELECT.from.ref) _updateRef(query.SELECT.from.ref)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function transformSelectQuery(queryProp, transformedFrom, transformedWhere, transformedQuery) {
|
|
287
|
+
const { columns, having, groupBy, orderBy, limit } = queryProp
|
|
288
|
+
|
|
289
|
+
// Trivial replacement -> no transformations needed
|
|
290
|
+
if (limit) {
|
|
291
|
+
transformedQuery.SELECT.limit = limit
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
transformedQuery.SELECT.from = transformedFrom
|
|
295
|
+
|
|
296
|
+
if (transformedWhere?.length > 0) {
|
|
297
|
+
transformedQuery.SELECT.where = transformedWhere
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (columns) {
|
|
301
|
+
transformedQuery.SELECT.columns = getTransformedColumns(columns)
|
|
302
|
+
} else {
|
|
303
|
+
transformedQuery.SELECT.columns = getColumnsForWildcard(inferred.SELECT?.excluding)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Like the WHERE clause, aliases from the SELECT list are not accessible for `group by`/`having` (in most DB's)
|
|
307
|
+
if (having) {
|
|
308
|
+
transformedQuery.SELECT.having = getTransformedTokenStream(having, { prop: 'having' })
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (groupBy) {
|
|
312
|
+
const transformedGroupBy = getTransformedOrderByGroupBy(groupBy)
|
|
313
|
+
if (transformedGroupBy.length) {
|
|
314
|
+
transformedQuery.SELECT.groupBy = transformedGroupBy
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Since all the expressions in the SELECT part of the query have been computed,
|
|
319
|
+
// one can reference aliases of the queries columns in the orderBy clause.
|
|
320
|
+
if (orderBy) {
|
|
321
|
+
const transformedOrderBy = getTransformedOrderByGroupBy(orderBy, true)
|
|
322
|
+
if (transformedOrderBy.length) {
|
|
323
|
+
transformedQuery.SELECT.orderBy = transformedOrderBy
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return transformedQuery
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Transforms a query object for INSERT or UPSERT operations by modifying the `into` clause.
|
|
331
|
+
*
|
|
332
|
+
* @param {string} kind - The type of operation: "INSERT" or "UPSERT".
|
|
333
|
+
*
|
|
334
|
+
* @returns {object} - The transformed query with updated `into` clause.
|
|
335
|
+
*/
|
|
336
|
+
function transformQueryForInsertUpsert(kind) {
|
|
337
|
+
const { as } = transformedQuery[kind].into
|
|
338
|
+
const target = cds.infer.target(inferred) // REVISIT: we should reliably use inferred._target instead
|
|
339
|
+
transformedQuery[kind].into = { ref: [target.name] }
|
|
340
|
+
if (as) transformedQuery[kind].into.as = as
|
|
341
|
+
return transformedQuery
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Transforms a search expression into a WHERE or HAVING clause for a SELECT operation, depending on the context of the query.
|
|
346
|
+
* The function decides whether to use a WHERE or HAVING clause based on the presence of aggregated columns in the search criteria.
|
|
347
|
+
*
|
|
348
|
+
* @param {object} searchTerm - The search expression to be applied to the searchable columns within the query source.
|
|
349
|
+
* @param {object} from - The FROM clause of the CQN statement.
|
|
350
|
+
*
|
|
351
|
+
* @returns {Object} - The function returns an object representing the WHERE or HAVING clause of the query.
|
|
352
|
+
*
|
|
353
|
+
* Note: The WHERE clause is used for filtering individual rows before any aggregation occurs.
|
|
354
|
+
* The HAVING clause is utilized for conditions on aggregated data, applied after grouping operations.
|
|
355
|
+
*/
|
|
356
|
+
function transformSearch(searchTerm) {
|
|
357
|
+
let prop = 'where'
|
|
358
|
+
|
|
359
|
+
// if the query is grouped and the queries columns contain an aggregate function,
|
|
360
|
+
// we must put the search term into the `having` clause, as the search expression
|
|
361
|
+
// is defined on the aggregated result, not on the individual rows
|
|
362
|
+
const usesAggregation =
|
|
363
|
+
inferred.SELECT.groupBy &&
|
|
364
|
+
(searchTerm.args[0].func || searchTerm.args[0].xpr || searchTerm.args[0].list?.some(c => c.func || c.xpr))
|
|
365
|
+
|
|
366
|
+
if (usesAggregation) prop = 'having'
|
|
367
|
+
if (inferred.SELECT[prop]) {
|
|
368
|
+
return { [prop]: [asXpr(inferred.SELECT.where), 'and', searchTerm] }
|
|
369
|
+
} else {
|
|
370
|
+
return { [prop]: searchTerm.xpr ? [...searchTerm.xpr] : [searchTerm] }
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Rewrites the from clause based on the `query.joinTree`.
|
|
376
|
+
*
|
|
377
|
+
* For each join relevant node in the join tree, the respective join is generated.
|
|
378
|
+
* Each join relevant node in the join tree has an unique table alias which is the query source for the respective
|
|
379
|
+
* path traversals. Hence, all join relevant `ref`s must be rewritten to point to the generated join aliases. However,
|
|
380
|
+
* this is done in the @function getFlatColumnsFor().
|
|
381
|
+
*
|
|
382
|
+
* @returns {CQN.from}
|
|
383
|
+
*/
|
|
384
|
+
function translateAssocsToJoins() {
|
|
385
|
+
let from
|
|
386
|
+
/**
|
|
387
|
+
* remember already seen aliases, do not create a join for them again
|
|
388
|
+
*/
|
|
389
|
+
const alreadySeen = new Map()
|
|
390
|
+
inferred.joinTree._roots.forEach(r => {
|
|
391
|
+
const args = []
|
|
392
|
+
if (r.queryArtifact.SELECT) {
|
|
393
|
+
const transformedSubquery = transformSubquery(r.queryArtifact)
|
|
394
|
+
transformedSubquery.as = r.alias
|
|
395
|
+
args.push(transformedSubquery)
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
const id = getLocalizedName(r.queryArtifact)
|
|
399
|
+
args.push({ ref: [r.args ? { id, args: r.args } : id], as: r.alias })
|
|
400
|
+
}
|
|
401
|
+
from = { join: r.join || 'left', args, on: [] }
|
|
402
|
+
r.children.forEach(c => {
|
|
403
|
+
from = joinForBranch(from, c)
|
|
404
|
+
from = { join: c.join || 'left', args: [from], on: [] }
|
|
405
|
+
})
|
|
406
|
+
})
|
|
407
|
+
return from.args.length > 1 ? from : from.args[0]
|
|
408
|
+
|
|
409
|
+
function joinForBranch(lhs, node) {
|
|
410
|
+
const nextAssoc = inferred.joinTree.findNextAssoc(node)
|
|
411
|
+
if (!nextAssoc || alreadySeen.has(nextAssoc.$refLink.alias)) return lhs.args.length > 1 ? lhs : lhs.args[0]
|
|
412
|
+
|
|
413
|
+
lhs.on.push(
|
|
414
|
+
...onCondFor(
|
|
415
|
+
nextAssoc.$refLink,
|
|
416
|
+
node.parent.$refLink || /** tree roots do not have $refLink */ {
|
|
417
|
+
alias: node.parent.alias,
|
|
418
|
+
definition: node.parent.queryArtifact,
|
|
419
|
+
target: node.parent.queryArtifact,
|
|
420
|
+
},
|
|
421
|
+
/** flip source and target in on condition */ true,
|
|
422
|
+
),
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
const def = getDefinition(nextAssoc.$refLink.definition.target)
|
|
426
|
+
const id = def.name
|
|
427
|
+
if (hasOwnSkip(def) && isRuntimeView(def)) addWith(model.definitions[id], transformedQuery, model)
|
|
428
|
+
const { args } = nextAssoc
|
|
429
|
+
const arg = {
|
|
430
|
+
ref: [args ? { id, args } : id],
|
|
431
|
+
as: nextAssoc.$refLink.alias,
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
lhs.args.push(arg)
|
|
435
|
+
alreadySeen.set(nextAssoc.$refLink.alias, true)
|
|
436
|
+
if (nextAssoc.where) {
|
|
437
|
+
const filter = getTransformedTokenStream(nextAssoc.where, { $baseLink: nextAssoc.$refLink })
|
|
438
|
+
lhs.on = [
|
|
439
|
+
...(hasLogicalOr(lhs.on) ? [asXpr(lhs.on)] : lhs.on),
|
|
440
|
+
'and',
|
|
441
|
+
...(hasLogicalOr(filter) ? [asXpr(filter)] : filter),
|
|
442
|
+
]
|
|
443
|
+
}
|
|
444
|
+
if (node.children) {
|
|
445
|
+
node.children.forEach(c => {
|
|
446
|
+
lhs = { join: c.join || 'left', args: [lhs], on: [] }
|
|
447
|
+
lhs = joinForBranch(lhs, c)
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
return lhs.args.length > 1 ? lhs : lhs.args[0]
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Walks over a list of columns (ref's, xpr, subqueries, val), applies flattening on structured types and expands wildcards.
|
|
456
|
+
*
|
|
457
|
+
* @param {object[]} columns
|
|
458
|
+
* @returns {object[]} the transformed representation of the input. Expanded and flattened.
|
|
459
|
+
*/
|
|
460
|
+
function getTransformedColumns(columns) {
|
|
461
|
+
const transformedColumns = []
|
|
462
|
+
for (let i = 0; i < columns.length; i++) {
|
|
463
|
+
const col = columns[i]
|
|
464
|
+
|
|
465
|
+
if (isCalculatedOnRead(col.$refLinks?.at(-1).definition) && !col.$refLinks?.at(-1).target?.SELECT) {
|
|
466
|
+
const name = getName(col)
|
|
467
|
+
if (!transformedColumns.some(inserted => getName(inserted) === name)) {
|
|
468
|
+
const calcElement = resolveCalculatedElement(col)
|
|
469
|
+
transformedColumns.push(calcElement)
|
|
470
|
+
}
|
|
471
|
+
} else if (col.expand) {
|
|
472
|
+
if (col.ref?.length > 1 && col.ref[0] === '$self' && !col.$refLinks[0].definition.kind) {
|
|
473
|
+
const dollarSelfReplacement = calculateDollarSelfColumn(col)
|
|
474
|
+
transformedColumns.push(...getTransformedColumns([dollarSelfReplacement]))
|
|
475
|
+
continue
|
|
476
|
+
}
|
|
477
|
+
transformedColumns.push(() => {
|
|
478
|
+
const expandResult = handleExpand(col)
|
|
479
|
+
if (expandResult.length > 1) {
|
|
480
|
+
return expandResult
|
|
481
|
+
} else {
|
|
482
|
+
return expandResult[0]
|
|
483
|
+
}
|
|
484
|
+
})
|
|
485
|
+
} else if (col.inline) {
|
|
486
|
+
handleInline(col)
|
|
487
|
+
} else if (col.ref) {
|
|
488
|
+
if (col.ref.length > 1 && col.ref[0] === '$self' && !col.$refLinks[0].definition.kind) {
|
|
489
|
+
const dollarSelfReplacement = calculateDollarSelfColumn(col)
|
|
490
|
+
transformedColumns.push(...getTransformedColumns([dollarSelfReplacement]))
|
|
491
|
+
continue
|
|
492
|
+
}
|
|
493
|
+
handleRef(col)
|
|
494
|
+
} else if (col === '*') {
|
|
495
|
+
handleWildcard(columns)
|
|
496
|
+
} else if (col.SELECT) {
|
|
497
|
+
handleSubquery(col)
|
|
498
|
+
} else {
|
|
499
|
+
handleDefault(col)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// subqueries are processed in the end
|
|
503
|
+
for (let i = 0; i < transformedColumns.length; i++) {
|
|
504
|
+
const c = transformedColumns[i]
|
|
505
|
+
if (typeof c === 'function') {
|
|
506
|
+
const res = c() || [] // target of expand / subquery could also be skipped -> no result
|
|
507
|
+
if (res.length !== undefined) {
|
|
508
|
+
transformedColumns.splice(i, 1, ...res)
|
|
509
|
+
i += res.length - 1
|
|
510
|
+
} else {
|
|
511
|
+
const replaceWith = res.as
|
|
512
|
+
? transformedColumns.findIndex(t => (t.as || t.ref?.[t.ref.length - 1]) === res.as)
|
|
513
|
+
: -1
|
|
514
|
+
if (replaceWith === -1) transformedColumns.splice(i, 1, res)
|
|
515
|
+
else {
|
|
516
|
+
transformedColumns.splice(replaceWith, 1, res)
|
|
517
|
+
transformedColumns.splice(i, 1)
|
|
518
|
+
// When removing an element, the next element moves to the current index
|
|
519
|
+
i--
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (transformedColumns.length === 0 && columns.length) {
|
|
526
|
+
handleEmptyColumns(columns)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return transformedColumns
|
|
530
|
+
|
|
531
|
+
function handleSubquery(col) {
|
|
532
|
+
transformedColumns.push(() => {
|
|
533
|
+
const res = transformSubquery(col)
|
|
534
|
+
if (col.as) res.as = col.as
|
|
535
|
+
return res
|
|
536
|
+
})
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function handleExpand(col) {
|
|
540
|
+
const { $refLinks } = col
|
|
541
|
+
const res = []
|
|
542
|
+
const last = $refLinks?.[$refLinks.length - 1]
|
|
543
|
+
if (last && !last.skipExpand && last.definition.isAssociation) {
|
|
544
|
+
const expandedSubqueryColumn = expandColumn(col)
|
|
545
|
+
if (!expandedSubqueryColumn) return []
|
|
546
|
+
setElementOnColumns(expandedSubqueryColumn, col.element)
|
|
547
|
+
res.push(expandedSubqueryColumn)
|
|
548
|
+
} else if (!last?.skipExpand) {
|
|
549
|
+
const expandCols = nestedProjectionOnStructure(col, 'expand')
|
|
550
|
+
res.push(...expandCols)
|
|
551
|
+
}
|
|
552
|
+
return res
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function handleInline(col) {
|
|
556
|
+
const inlineCols = nestedProjectionOnStructure(col)
|
|
557
|
+
transformedColumns.push(...inlineCols)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function handleRef(col) {
|
|
561
|
+
if (pseudos.elements[col.ref[0]] || col.param) {
|
|
562
|
+
transformedColumns.push({ ...col })
|
|
563
|
+
return
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const tableAlias = getTableAlias(col)
|
|
567
|
+
// re-adjust usage of implicit alias in subquery
|
|
568
|
+
if (col.$refLinks[0].definition.kind === 'entity' && col.ref[0] !== tableAlias) {
|
|
569
|
+
col.ref[0] = tableAlias
|
|
570
|
+
}
|
|
571
|
+
const leaf = col.$refLinks[col.$refLinks.length - 1].definition
|
|
572
|
+
if (leaf.virtual === true) return
|
|
573
|
+
|
|
574
|
+
let baseName
|
|
575
|
+
if (col.ref.length >= 2) {
|
|
576
|
+
baseName = col.ref
|
|
577
|
+
.map(idOnly)
|
|
578
|
+
.slice(col.ref[0] === tableAlias ? 1 : 0, col.ref.length - 1)
|
|
579
|
+
.join('_')
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
let columnAlias = col.as || (col.isJoinRelevant ? col.flatName : null)
|
|
583
|
+
const refNavigation = col.ref.slice(col.$refLinks[0].definition.kind !== 'element' ? 1 : 0).join('_')
|
|
584
|
+
if (!columnAlias && col.flatName && col.flatName !== refNavigation) columnAlias = refNavigation
|
|
585
|
+
|
|
586
|
+
if (
|
|
587
|
+
col.$refLinks.some(link => {
|
|
588
|
+
const def = getDefinition(link.definition.target)
|
|
589
|
+
return hasOwnSkip(def) && !isRuntimeView(def)
|
|
590
|
+
})
|
|
591
|
+
)
|
|
592
|
+
return
|
|
593
|
+
|
|
594
|
+
const flatColumns = getFlatColumnsFor(col, { baseName, columnAlias, tableAlias })
|
|
595
|
+
flatColumns.forEach(flatColumn => {
|
|
596
|
+
const name = getName(flatColumn)
|
|
597
|
+
if (!transformedColumns.some(inserted => getName(inserted) === name)) transformedColumns.push(flatColumn)
|
|
598
|
+
})
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function handleWildcard(columns) {
|
|
602
|
+
const wildcardIndex = columns.indexOf('*')
|
|
603
|
+
const ignoreInWildcardExpansion = columns.slice(0, wildcardIndex)
|
|
604
|
+
const { excluding } = inferred.SELECT
|
|
605
|
+
if (excluding) ignoreInWildcardExpansion.push(...excluding)
|
|
606
|
+
|
|
607
|
+
const wildcardColumns = getColumnsForWildcard(ignoreInWildcardExpansion, columns.slice(wildcardIndex + 1))
|
|
608
|
+
transformedColumns.push(...wildcardColumns)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function handleDefault(col) {
|
|
612
|
+
let transformedColumn = getTransformedColumn(col)
|
|
613
|
+
if (col.as) transformedColumn.as = col.as
|
|
614
|
+
|
|
615
|
+
const replaceWith = transformedColumns.findIndex(
|
|
616
|
+
t => (t.as || t.ref?.[t.ref.length - 1]) === transformedColumn.as,
|
|
617
|
+
)
|
|
618
|
+
if (replaceWith === -1) transformedColumns.push(transformedColumn)
|
|
619
|
+
else transformedColumns.splice(replaceWith, 1, transformedColumn)
|
|
620
|
+
|
|
621
|
+
setElementOnColumns(transformedColumn, inferred.elements[col.as])
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function getTransformedColumn(col) {
|
|
625
|
+
let ret
|
|
626
|
+
if (col !== null && typeof col === 'object' && '#' in col) {
|
|
627
|
+
ret = resolveEnumToken(col, [], -1)
|
|
628
|
+
// cast is already resolved inside resolveEnumToken; do not overwrite it here
|
|
629
|
+
return ret
|
|
630
|
+
}
|
|
631
|
+
if (col.func) {
|
|
632
|
+
ret = {
|
|
633
|
+
func: col.func,
|
|
634
|
+
args: getTransformedFunctionArgs(col.args),
|
|
635
|
+
as: col.func, // may be overwritten by the explicit alias
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (col.xpr) {
|
|
639
|
+
ret ??= {}
|
|
640
|
+
ret.xpr = getTransformedTokenStream(col.xpr)
|
|
641
|
+
}
|
|
642
|
+
if (ret) {
|
|
643
|
+
if (col.cast) ret.cast = resolveEnumCastType(col.cast)
|
|
644
|
+
return ret
|
|
645
|
+
}
|
|
646
|
+
return copy(col)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function handleEmptyColumns(columns) {
|
|
650
|
+
if (columns.some(c => c.$refLinks?.[c.$refLinks.length - 1].definition.type === 'cds.Composition')) return
|
|
651
|
+
throw new Error('Queries must have at least one non-virtual column')
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function resolveCalculatedElement(column, omitAlias = false, $baseLink = null) {
|
|
656
|
+
let value
|
|
657
|
+
|
|
658
|
+
if (column.$refLinks) {
|
|
659
|
+
const { $refLinks } = column
|
|
660
|
+
value = $refLinks[$refLinks.length - 1].definition.value
|
|
661
|
+
if (column.$refLinks.length > 1) {
|
|
662
|
+
$baseLink =
|
|
663
|
+
[...$refLinks].reverse().find($refLink => $refLink.definition.isAssociation) ||
|
|
664
|
+
// if there is no association in the path, the table alias is the base link
|
|
665
|
+
// TA might refer to subquery -> we need to propagate the alias to all paths of the calc element
|
|
666
|
+
column.$refLinks[0]
|
|
667
|
+
}
|
|
668
|
+
} else {
|
|
669
|
+
value = column.value
|
|
670
|
+
}
|
|
671
|
+
const { ref, val, xpr, func } = value
|
|
672
|
+
|
|
673
|
+
let res
|
|
674
|
+
if (ref) {
|
|
675
|
+
res = getTransformedTokenStream([value], { $baseLink })[0]
|
|
676
|
+
} else if (xpr) {
|
|
677
|
+
res = { xpr: getTransformedTokenStream(value.xpr, { $baseLink }) }
|
|
678
|
+
} else if (val !== undefined) {
|
|
679
|
+
res = { val }
|
|
680
|
+
} else if (func) {
|
|
681
|
+
res = { args: getTransformedFunctionArgs(value.args, $baseLink), func: value.func }
|
|
682
|
+
}
|
|
683
|
+
if (!omitAlias) res.as = column.as || column.name || column.flatName
|
|
684
|
+
setElementOnColumns(res, column.element || column)
|
|
685
|
+
return res
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* This function resolves a `ref` starting with a `$self`.
|
|
690
|
+
* Such a path targets another element of the query by it's implicit, or explicit alias.
|
|
691
|
+
*
|
|
692
|
+
* A `$self` reference may also target another `$self` path. In this case, this function
|
|
693
|
+
* recursively resolves the tail of the `$self` references (`$selfPath.ref.slice(2)`) onto it's
|
|
694
|
+
* new base.
|
|
695
|
+
*
|
|
696
|
+
* @param {object} col with a ref like `[ '$self', <target column>, <optional further path navigation> ]`
|
|
697
|
+
* @param {boolean} omitAlias if we replace a $self reference in an aggregation or a token stream, we must not add an "as" to the result
|
|
698
|
+
*/
|
|
699
|
+
function calculateDollarSelfColumn(col, omitAlias = false) {
|
|
700
|
+
const dummyColumn = buildDummyColumnForDollarSelf({ ...col }, col.$refLinks)
|
|
701
|
+
|
|
702
|
+
return dummyColumn
|
|
703
|
+
|
|
704
|
+
function buildDummyColumnForDollarSelf(dollarSelfColumn, $refLinks) {
|
|
705
|
+
const { ref, as } = dollarSelfColumn
|
|
706
|
+
const stepToFind = ref[1]
|
|
707
|
+
let referencedColumn = inferred.SELECT.columns.find(
|
|
708
|
+
otherColumn =>
|
|
709
|
+
otherColumn !== dollarSelfColumn &&
|
|
710
|
+
(otherColumn.as
|
|
711
|
+
? stepToFind === otherColumn.as
|
|
712
|
+
: stepToFind === otherColumn.ref?.[otherColumn.ref.length - 1]),
|
|
713
|
+
)
|
|
714
|
+
if (referencedColumn.ref?.[0] === '$self') {
|
|
715
|
+
referencedColumn = buildDummyColumnForDollarSelf({ ...referencedColumn }, referencedColumn.$refLinks)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (referencedColumn.ref) {
|
|
719
|
+
dollarSelfColumn.ref = [...referencedColumn.ref, ...dollarSelfColumn.ref.slice(2)]
|
|
720
|
+
Object.defineProperties(dollarSelfColumn, {
|
|
721
|
+
flatName: {
|
|
722
|
+
value:
|
|
723
|
+
referencedColumn.$refLinks[0].definition.kind === 'entity'
|
|
724
|
+
? dollarSelfColumn.ref.slice(1).join('_')
|
|
725
|
+
: dollarSelfColumn.ref.join('_'),
|
|
726
|
+
},
|
|
727
|
+
isJoinRelevant: {
|
|
728
|
+
value: referencedColumn.isJoinRelevant,
|
|
729
|
+
},
|
|
730
|
+
$refLinks: {
|
|
731
|
+
value: [...referencedColumn.$refLinks, ...$refLinks.slice(2)],
|
|
732
|
+
},
|
|
733
|
+
})
|
|
734
|
+
} else {
|
|
735
|
+
// target column is `val`, `xpr`, or `func` — destructure and throw away the ref with the $self
|
|
736
|
+
// eslint-disable-next-line no-unused-vars
|
|
737
|
+
const { xpr, val, func, args, ref, as: _as, ...rest } = referencedColumn
|
|
738
|
+
if (xpr) rest.xpr = xpr
|
|
739
|
+
else if (func) { rest.func = func; rest.args = args }
|
|
740
|
+
else rest.val = val
|
|
741
|
+
dollarSelfColumn = { ...rest } // reassign dummyColumn without 'ref'
|
|
742
|
+
if (!omitAlias) dollarSelfColumn.as = as
|
|
743
|
+
}
|
|
744
|
+
return dollarSelfColumn.ref?.[0] === '$self'
|
|
745
|
+
? buildDummyColumnForDollarSelf({ ...dollarSelfColumn }, $refLinks)
|
|
746
|
+
: dollarSelfColumn
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Calculates the columns for a nested projection on a structure.
|
|
752
|
+
*
|
|
753
|
+
* @param {object} col
|
|
754
|
+
* @param {'inline'|'expand'} prop the property on which to operate. Default is `inline`.
|
|
755
|
+
* @returns a list of flat columns.
|
|
756
|
+
*/
|
|
757
|
+
function nestedProjectionOnStructure(col, prop = 'inline') {
|
|
758
|
+
const res = []
|
|
759
|
+
|
|
760
|
+
col[prop].forEach((nestedProjection, i) => {
|
|
761
|
+
let rewrittenColumns = []
|
|
762
|
+
if (nestedProjection === '*') {
|
|
763
|
+
res.push(...expandNestedProjectionWildcard(col, i, prop))
|
|
764
|
+
} else {
|
|
765
|
+
const nameParts = col.as ? [col.as] : [col.ref.map(idOnly).join('_')]
|
|
766
|
+
nameParts.push(nestedProjection.as ? nestedProjection.as : nestedProjection.ref.map(idOnly).join('_'))
|
|
767
|
+
const name = nameParts.join('_')
|
|
768
|
+
if (nestedProjection.ref) {
|
|
769
|
+
const augmentedInlineCol = augmentInlineRefWithParent(nestedProjection, col)
|
|
770
|
+
if (
|
|
771
|
+
col.as ||
|
|
772
|
+
nestedProjection.as ||
|
|
773
|
+
nestedProjection.$refLinks[nestedProjection.$refLinks.length - 1].definition.value ||
|
|
774
|
+
nestedProjection.isJoinRelevant
|
|
775
|
+
) {
|
|
776
|
+
augmentedInlineCol.as = nameParts.join('_')
|
|
777
|
+
}
|
|
778
|
+
const flatColumns = getTransformedColumns([augmentedInlineCol])
|
|
779
|
+
flatColumns.forEach(flatColumn => {
|
|
780
|
+
const flatColumnName = flatColumn.as || flatColumn.ref[flatColumn.ref.length - 1]
|
|
781
|
+
if (!res.some(c => (c.as || c.ref.slice(1).map(idOnly).join('_')) === flatColumnName)) {
|
|
782
|
+
const rewrittenColumn = { ...flatColumn }
|
|
783
|
+
if (nestedProjection.as) rewrittenColumn.as = flatColumnName
|
|
784
|
+
rewrittenColumns.push(rewrittenColumn)
|
|
785
|
+
}
|
|
786
|
+
})
|
|
787
|
+
} else {
|
|
788
|
+
// func, xpr, val..
|
|
789
|
+
// we need to check if the column was already added
|
|
790
|
+
// in the wildcard expansion
|
|
791
|
+
if (!res.some(c => (c.as || c.ref.slice(1).map(idOnly).join('_')) === name)) {
|
|
792
|
+
const rewrittenColumn = { ...nestedProjection }
|
|
793
|
+
rewrittenColumn.as = name
|
|
794
|
+
// For xpr, we need to transform refs inside to include the struct prefix
|
|
795
|
+
if (nestedProjection.xpr && col.ref) {
|
|
796
|
+
rewrittenColumn.xpr = augmentInlineXprRefs(nestedProjection.xpr, col)
|
|
797
|
+
}
|
|
798
|
+
rewrittenColumns.push(rewrittenColumn)
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
res.push(...rewrittenColumns)
|
|
803
|
+
})
|
|
804
|
+
|
|
805
|
+
return res
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Augment a ref column with the parent column's path and $refLinks.
|
|
809
|
+
*/
|
|
810
|
+
function augmentInlineRefWithParent(refCol, parentCol) {
|
|
811
|
+
const augmented = { ...refCol }
|
|
812
|
+
augmented.ref = parentCol.ref ? [...parentCol.ref, ...refCol.ref] : refCol.ref
|
|
813
|
+
Object.defineProperties(augmented, {
|
|
814
|
+
$refLinks: { value: [...refCol.$refLinks], writable: true },
|
|
815
|
+
isJoinRelevant: { value: refCol.isJoinRelevant, writable: true },
|
|
816
|
+
})
|
|
817
|
+
if (parentCol.ref) {
|
|
818
|
+
augmented.$refLinks.unshift(...parentCol.$refLinks)
|
|
819
|
+
augmented.isJoinRelevant = augmented.isJoinRelevant || parentCol.isJoinRelevant
|
|
820
|
+
}
|
|
821
|
+
return augmented
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Augment refs inside an xpr with the parent column's path and $refLinks,
|
|
826
|
+
* then transform them to flat refs.
|
|
827
|
+
*/
|
|
828
|
+
function augmentInlineXprRefs(xpr, parentCol) {
|
|
829
|
+
return xpr.map(token => {
|
|
830
|
+
if (typeof token === 'string' || token.val !== undefined) {
|
|
831
|
+
return token
|
|
832
|
+
}
|
|
833
|
+
if (token.ref && token.$refLinks) {
|
|
834
|
+
const augmented = augmentInlineRefWithParent(token, parentCol)
|
|
835
|
+
// Transform this single ref column to get the flat version
|
|
836
|
+
const transformed = getTransformedColumns([augmented])
|
|
837
|
+
if (transformed.length === 1) {
|
|
838
|
+
return transformed[0]
|
|
839
|
+
}
|
|
840
|
+
return augmented
|
|
841
|
+
}
|
|
842
|
+
if (token.xpr) {
|
|
843
|
+
return { ...token, xpr: augmentInlineXprRefs(token.xpr, parentCol) }
|
|
844
|
+
}
|
|
845
|
+
if (token.func && token.args) {
|
|
846
|
+
return {
|
|
847
|
+
...token, args: token.args.map(arg =>
|
|
848
|
+
arg.ref ? augmentInlineXprRefs([arg], parentCol)[0] : arg
|
|
849
|
+
)
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return token
|
|
853
|
+
})
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Expand the wildcard of the given column into all leaf elements.
|
|
859
|
+
* Respect smart wildcard rules and excluding clause.
|
|
860
|
+
*
|
|
861
|
+
* Every column before the wildcardIndex is excluded from the wildcard expansion.
|
|
862
|
+
* Columns after the wildcardIndex overwrite columns within the wildcard expansion in place.
|
|
863
|
+
*
|
|
864
|
+
* @TODO use this also for `expand` wildcards on structures.
|
|
865
|
+
*
|
|
866
|
+
* @param {csn.Column} col
|
|
867
|
+
* @param {integer} wildcardIndex
|
|
868
|
+
* @returns an array of columns which represents the expanded wildcard
|
|
869
|
+
*/
|
|
870
|
+
function expandNestedProjectionWildcard(col, wildcardIndex, prop = 'inline') {
|
|
871
|
+
const res = []
|
|
872
|
+
// everything before the wildcard is inserted before the wildcard
|
|
873
|
+
// and ignored from the wildcard expansion
|
|
874
|
+
const exclude = col[prop].slice(0, wildcardIndex)
|
|
875
|
+
// everything after the wildcard, is a potential replacement
|
|
876
|
+
// in the wildcard expansion
|
|
877
|
+
const replace = []
|
|
878
|
+
|
|
879
|
+
const baseRef = col.ref || []
|
|
880
|
+
const baseRefLinks = col.$refLinks || []
|
|
881
|
+
|
|
882
|
+
// column has no ref, then it is an anonymous expand:
|
|
883
|
+
// select from books { { * } as bar }
|
|
884
|
+
// only possible if there is exactly one query source
|
|
885
|
+
if (!baseRef.length) {
|
|
886
|
+
const [tableAlias, { definition }] = Object.entries(inferred.sources)[0]
|
|
887
|
+
baseRef.push(tableAlias)
|
|
888
|
+
baseRefLinks.push({ definition, source: definition })
|
|
889
|
+
}
|
|
890
|
+
// we need to make the refs absolute
|
|
891
|
+
col[prop].slice(wildcardIndex + 1).forEach(c => {
|
|
892
|
+
const fakeColumn = { ...c }
|
|
893
|
+
if (fakeColumn.ref) {
|
|
894
|
+
fakeColumn.ref = [...baseRef, ...fakeColumn.ref]
|
|
895
|
+
fakeColumn.$refLinks = [...baseRefLinks, ...c.$refLinks]
|
|
896
|
+
}
|
|
897
|
+
replace.push(fakeColumn)
|
|
898
|
+
})
|
|
899
|
+
// respect excluding clause
|
|
900
|
+
if (col.excluding) {
|
|
901
|
+
// fake the ref since excluding only has strings
|
|
902
|
+
col.excluding.forEach(c => {
|
|
903
|
+
const fakeColumn = {
|
|
904
|
+
ref: [...baseRef, c],
|
|
905
|
+
}
|
|
906
|
+
exclude.push(fakeColumn)
|
|
907
|
+
})
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (baseRefLinks.at(-1).definition.kind === 'entity') {
|
|
911
|
+
res.push(...getColumnsForWildcard(exclude, replace, col.as))
|
|
912
|
+
} else if (baseRefLinks.at(-1).definition.target) {
|
|
913
|
+
// Wildcard on association - need to include FK columns and join-relevant target columns
|
|
914
|
+
res.push(...expandAssociationWildcard(col, baseRef, baseRefLinks, exclude, replace))
|
|
915
|
+
} else
|
|
916
|
+
res.push(
|
|
917
|
+
...getFlatColumnsFor(col, { columnAlias: col.as, tableAlias: getTableAlias(col) }, [], {
|
|
918
|
+
exclude,
|
|
919
|
+
replace,
|
|
920
|
+
}),
|
|
921
|
+
)
|
|
922
|
+
return res
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Expands a wildcard on an association into:
|
|
927
|
+
* 1. FK columns from the source table
|
|
928
|
+
* 2. Non-FK columns from the target via join
|
|
929
|
+
*/
|
|
930
|
+
function expandAssociationWildcard(col, baseRef, baseRefLinks, exclude, replace) {
|
|
931
|
+
const res = []
|
|
932
|
+
const assocDef = baseRefLinks.at(-1).definition
|
|
933
|
+
const targetDef = getDefinition(assocDef.target)
|
|
934
|
+
const columnAlias = col.as || baseRef.map(idOnly).join('_')
|
|
935
|
+
const sourceTableAlias = getTableAlias(col)
|
|
936
|
+
|
|
937
|
+
// Get the join alias for this association (set during join tree merge)
|
|
938
|
+
const joinAlias = baseRefLinks.at(-1).alias
|
|
939
|
+
|
|
940
|
+
// Collect FK element names
|
|
941
|
+
const fkNames = new Set()
|
|
942
|
+
if (assocDef.keys) {
|
|
943
|
+
for (const k of assocDef.keys) {
|
|
944
|
+
fkNames.add(k.ref[0])
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// First, add FK columns from source table
|
|
949
|
+
// These are accessed via the source table alias, not the join
|
|
950
|
+
const fkColumns = getFlatColumnsFor(col, { tableAlias: sourceTableAlias }, [], {
|
|
951
|
+
exclude,
|
|
952
|
+
replace,
|
|
953
|
+
})
|
|
954
|
+
res.push(...fkColumns.filter(fk => !col.excluding?.some(e => targetDef.elements[e] === fk.element)))
|
|
955
|
+
|
|
956
|
+
// Then, add non-FK columns from target via join
|
|
957
|
+
if (targetDef?.elements) {
|
|
958
|
+
for (const [elemName, elemDef] of Object.entries(targetDef.elements)) {
|
|
959
|
+
// Skip FK elements (already included above), virtual, blobs, and unmanaged assocs
|
|
960
|
+
if (fkNames.has(elemName)) continue
|
|
961
|
+
if (elemDef.virtual) continue
|
|
962
|
+
if (elemDef.type === 'cds.LargeBinary') continue
|
|
963
|
+
if (elemDef.on && !elemDef.keys) continue // unmanaged association
|
|
964
|
+
|
|
965
|
+
// Check exclusions
|
|
966
|
+
const fullName = `${columnAlias}_${elemName}`
|
|
967
|
+
if (exclude.some(e => (e.ref?.at(-1) || e.as || e) === elemName)) continue
|
|
968
|
+
if (exclude.some(e => (e.ref?.at(-1) || e.as || e) === fullName)) continue
|
|
969
|
+
|
|
970
|
+
// Check for replacement
|
|
971
|
+
const replacement = replace.find(r => (r.ref?.at(-1) || r.as) === elemName)
|
|
972
|
+
if (replacement) {
|
|
973
|
+
// Handle replacement - create augmented column
|
|
974
|
+
const augmented = { ...replacement }
|
|
975
|
+
augmented.as = fullName
|
|
976
|
+
res.push(...getTransformedColumns([augmented]))
|
|
977
|
+
continue
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Create column referencing the join alias
|
|
981
|
+
if (elemDef.elements) {
|
|
982
|
+
// Structured element - need to flatten it
|
|
983
|
+
const structCols = getFlatColumnsFor(
|
|
984
|
+
elemDef,
|
|
985
|
+
{ baseName: elemName, columnAlias: fullName, tableAlias: joinAlias },
|
|
986
|
+
[],
|
|
987
|
+
{ exclude, replace },
|
|
988
|
+
true,
|
|
989
|
+
)
|
|
990
|
+
res.push(...structCols)
|
|
991
|
+
} else if (elemDef.keys) {
|
|
992
|
+
// Association element - flatten its foreign keys
|
|
993
|
+
// The FK column name is: assocName_keyName (e.g., 'head_id')
|
|
994
|
+
for (const k of elemDef.keys) {
|
|
995
|
+
const keyName = k.as || k.ref.join('_')
|
|
996
|
+
const fkName = `${elemName}_${keyName}` // e.g., 'head_id'
|
|
997
|
+
const fkFullName = `${columnAlias}_${fkName}` // e.g., 'department_head_id'
|
|
998
|
+
|
|
999
|
+
// Check if this FK is excluded
|
|
1000
|
+
if (exclude.some(e => (e.ref?.at(-1) || e.as || e) === fkName)) continue
|
|
1001
|
+
if (exclude.some(e => (e.ref?.at(-1) || e.as || e) === fkFullName)) continue
|
|
1002
|
+
|
|
1003
|
+
const flatColumn = {
|
|
1004
|
+
ref: [joinAlias, fkName],
|
|
1005
|
+
as: fkFullName,
|
|
1006
|
+
}
|
|
1007
|
+
const fkElement = getElementForRef(k.ref, getDefinition(elemDef.target))
|
|
1008
|
+
setElementOnColumns(flatColumn, fkElement)
|
|
1009
|
+
res.push(flatColumn)
|
|
1010
|
+
}
|
|
1011
|
+
} else if (elemDef.value) {
|
|
1012
|
+
// Calculated element - resolve it
|
|
1013
|
+
const calcElement = resolveCalculatedElement({ $refLinks: [{ definition: elemDef }] }, true)
|
|
1014
|
+
if (calcElement.as) {
|
|
1015
|
+
calcElement.as = fullName
|
|
1016
|
+
} else {
|
|
1017
|
+
calcElement.as = fullName
|
|
1018
|
+
}
|
|
1019
|
+
res.push(calcElement)
|
|
1020
|
+
}
|
|
1021
|
+
else {
|
|
1022
|
+
// Scalar element
|
|
1023
|
+
const flatColumn = {
|
|
1024
|
+
ref: [joinAlias, elemName],
|
|
1025
|
+
as: fullName,
|
|
1026
|
+
}
|
|
1027
|
+
setElementOnColumns(flatColumn, elemDef)
|
|
1028
|
+
res.push(flatColumn)
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
return res
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* This function converts a column with an `expand` property into a subquery.
|
|
1038
|
+
*
|
|
1039
|
+
* It operates by using the following steps:
|
|
1040
|
+
*
|
|
1041
|
+
* 1. It creates an intermediate SQL query, selecting `from <effective query source>:...<column>.ref { ...<column>.expand }`.
|
|
1042
|
+
* For example, from the query `SELECT from Authors { books { title } }`, it generates:
|
|
1043
|
+
* - `SELECT from Authors:books as books {title}`
|
|
1044
|
+
*
|
|
1045
|
+
* 2. It then adds the properties `expand: true` and `one: <expand assoc>.is2one` to the intermediate SQL query.
|
|
1046
|
+
*
|
|
1047
|
+
* 3. It applies `cqn4sql` to the intermediate query (ensuring the aliases of the outer query are maintained).
|
|
1048
|
+
* For example, `cqn4sql(…)` is used to create the following query:
|
|
1049
|
+
* - `SELECT from Books as books {books.title} where exists ( SELECT 1 from Authors as Authors where Authors.ID = books.author_ID )`
|
|
1050
|
+
*
|
|
1051
|
+
* 4. It then replaces the `exists <subquery>` with the where condition of the `<subquery>` and correlates it with the effective query source.
|
|
1052
|
+
* For example, this query is created:
|
|
1053
|
+
* - `SELECT from Books as books { books.title } where Authors.ID = books.author_ID`
|
|
1054
|
+
*
|
|
1055
|
+
* 5. Lastly, it replaces the `expand` column of the original query with the transformed subquery.
|
|
1056
|
+
* For example, the query becomes:
|
|
1057
|
+
* - `SELECT from Authors { (SELECT from Books as books { books.title } where Authors.ID = books.author_ID) as books }`
|
|
1058
|
+
*
|
|
1059
|
+
* @param {CSN.column} column - The column with the 'expand' property to be transformed into a subquery.
|
|
1060
|
+
*
|
|
1061
|
+
* @returns {object} Returns a subquery correlated with the enclosing query, with added properties `expand:true` and `one:true|false`.
|
|
1062
|
+
*/
|
|
1063
|
+
function expandColumn(column) {
|
|
1064
|
+
let outerAlias
|
|
1065
|
+
let subqueryFromRef
|
|
1066
|
+
const ref = column.$refLinks[0].definition.kind === 'entity' ? column.ref.slice(1) : column.ref
|
|
1067
|
+
const assoc = column.$refLinks.at(-1)
|
|
1068
|
+
ensureValidForeignKeys(assoc.definition, column.ref, 'expand')
|
|
1069
|
+
if (column.isJoinRelevant) {
|
|
1070
|
+
// all n-1 steps of the expand column are already transformed into joins
|
|
1071
|
+
// find the last join relevant association. That is the n-1 assoc in the ref path.
|
|
1072
|
+
// slice the ref array beginning from the n-1 assoc in the ref and take that as the postfix for the subqueries from ref.
|
|
1073
|
+
;[...column.$refLinks]
|
|
1074
|
+
.reverse()
|
|
1075
|
+
.slice(1)
|
|
1076
|
+
.find((link, i) => {
|
|
1077
|
+
if (link.definition.isAssociation) {
|
|
1078
|
+
subqueryFromRef = [link.definition.target, ...column.ref.slice(-(i + 1), column.ref.length)]
|
|
1079
|
+
// alias of last join relevant association is also the correlation alias for the subquery
|
|
1080
|
+
outerAlias = link.alias
|
|
1081
|
+
return true
|
|
1082
|
+
}
|
|
1083
|
+
})
|
|
1084
|
+
} else {
|
|
1085
|
+
outerAlias = transformedQuery.SELECT.from.as
|
|
1086
|
+
subqueryFromRef = [transformedQuery._target.name, ...ref]
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// this is the alias of the column which holds the correlated subquery
|
|
1090
|
+
const columnAlias = column.as || ref.map(idOnly).join('_')
|
|
1091
|
+
|
|
1092
|
+
// if there is a group by on the main query, all
|
|
1093
|
+
// columns of the expand must be in the groupBy
|
|
1094
|
+
if (transformedQuery.SELECT.groupBy) {
|
|
1095
|
+
const baseRef = column.$refLinks[0].definition.SELECT || ref
|
|
1096
|
+
|
|
1097
|
+
return _subqueryForGroupBy(column, baseRef, columnAlias)
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// Alias in expand subquery is derived from but not equal to
|
|
1101
|
+
// the alias of the column because to account for potential ambiguities
|
|
1102
|
+
// the alias cannot be addressed anyways
|
|
1103
|
+
const uniqueSubqueryAlias = getNextAvailableTableAlias(getImplicitAlias(columnAlias), inferred.outerQueries)
|
|
1104
|
+
|
|
1105
|
+
// `SELECT from Authors { books.genre as genreOfBooks { name } } becomes `SELECT from Books:genre as genreOfBooks`
|
|
1106
|
+
const from = { ref: subqueryFromRef, as: uniqueSubqueryAlias }
|
|
1107
|
+
const subqueryBase = {}
|
|
1108
|
+
const queryModifiers = { ...column }
|
|
1109
|
+
for (const [key, value] of Object.entries(queryModifiers)) {
|
|
1110
|
+
if (key in { limit: 1, orderBy: 1, groupBy: 1, excluding: 1, where: 1, having: 1, count: 1 })
|
|
1111
|
+
subqueryBase[key] = value
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const subquery = {
|
|
1115
|
+
SELECT: {
|
|
1116
|
+
...subqueryBase,
|
|
1117
|
+
from,
|
|
1118
|
+
columns: JSON.parse(JSON.stringify(column.expand)),
|
|
1119
|
+
expand: true,
|
|
1120
|
+
one: column.$refLinks.at(-1).definition.is2one,
|
|
1121
|
+
},
|
|
1122
|
+
}
|
|
1123
|
+
const expanded = transformSubquery(subquery)
|
|
1124
|
+
const correlated = _correlate({ ...expanded, as: columnAlias }, outerAlias)
|
|
1125
|
+
defineProperty(correlated, 'elements', expanded.elements)
|
|
1126
|
+
return correlated
|
|
1127
|
+
|
|
1128
|
+
function _correlate(subq, outer) {
|
|
1129
|
+
const subqueryFollowingExists = (a, indexOfExists) => a[indexOfExists + 1]
|
|
1130
|
+
let {
|
|
1131
|
+
SELECT: { where },
|
|
1132
|
+
} = subq
|
|
1133
|
+
let recent = where
|
|
1134
|
+
let i = where.indexOf('exists')
|
|
1135
|
+
while (i !== -1) {
|
|
1136
|
+
where = subqueryFollowingExists((recent = where), i).SELECT.where
|
|
1137
|
+
i = where.indexOf('exists')
|
|
1138
|
+
}
|
|
1139
|
+
const existsIndex = recent.indexOf('exists')
|
|
1140
|
+
recent.splice(
|
|
1141
|
+
existsIndex,
|
|
1142
|
+
2,
|
|
1143
|
+
...where.map(x => {
|
|
1144
|
+
return replaceAliasWithSubqueryAlias(x)
|
|
1145
|
+
}),
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
function replaceAliasWithSubqueryAlias(x) {
|
|
1149
|
+
const existsSubqueryAlias = recent[existsIndex + 1].SELECT.from.as
|
|
1150
|
+
if (existsSubqueryAlias === x.ref?.[0]) return { ref: [outer, ...x.ref.slice(1)] }
|
|
1151
|
+
if (x.xpr) x.xpr = x.xpr.map(replaceAliasWithSubqueryAlias)
|
|
1152
|
+
if (x.args) x.args = x.args.map(replaceAliasWithSubqueryAlias)
|
|
1153
|
+
return x
|
|
1154
|
+
}
|
|
1155
|
+
return subq
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* Generates a special subquery for the `expand` of the `column`.
|
|
1160
|
+
* All columns in the `expand` must be part of the GROUP BY clause of the main query.
|
|
1161
|
+
* If this is the case, the subqueries columns match the corresponding references of the group by.
|
|
1162
|
+
* Nested expands are also supported.
|
|
1163
|
+
*
|
|
1164
|
+
* @param {Object} column - To expand.
|
|
1165
|
+
* @param {Array} baseRef - The base reference for the expanded column.
|
|
1166
|
+
* @param {string} subqueryAlias - The alias of the `expand` subquery column.
|
|
1167
|
+
* @returns {Object} - The subquery object or null if the expand has a wildcard.
|
|
1168
|
+
* @throws {Error} - If one of the `ref`s in the `column.expand` is not part of the GROUP BY clause.
|
|
1169
|
+
*/
|
|
1170
|
+
function _subqueryForGroupBy(column, baseRef, subqueryAlias) {
|
|
1171
|
+
const groupByLookup = new Map(
|
|
1172
|
+
transformedQuery.SELECT.groupBy.map(c => [c.ref && c.ref.map(refWithConditions).join('.'), c]),
|
|
1173
|
+
)
|
|
1174
|
+
|
|
1175
|
+
// to be attached to dummy query
|
|
1176
|
+
const elements = {}
|
|
1177
|
+
const containsWildcard = column.expand.includes('*')
|
|
1178
|
+
if (containsWildcard) {
|
|
1179
|
+
// expand with wildcard vanishes as expand is part of the group by (OData $apply + $expand)
|
|
1180
|
+
return null
|
|
1181
|
+
}
|
|
1182
|
+
const expandedColumns = column.expand
|
|
1183
|
+
.flatMap(expand => {
|
|
1184
|
+
if (!expand.ref) return expand
|
|
1185
|
+
const fullRef = [...baseRef, ...expand.ref]
|
|
1186
|
+
|
|
1187
|
+
if (expand.expand) {
|
|
1188
|
+
const nested = _subqueryForGroupBy(expand, fullRef, expand.as || expand.ref.map(idOnly).join('_'))
|
|
1189
|
+
if (nested) {
|
|
1190
|
+
setElementOnColumns(nested, expand.element)
|
|
1191
|
+
elements[expand.as || expand.ref.map(idOnly).join('_')] = nested
|
|
1192
|
+
}
|
|
1193
|
+
return nested
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
const groupByRef = groupByLookup.get(fullRef.map(refWithConditions).join('.'))
|
|
1197
|
+
if (!groupByRef) {
|
|
1198
|
+
throw new Error(
|
|
1199
|
+
`The expanded column "${fullRef.map(refWithConditions).join('.')}" must be part of the group by clause`,
|
|
1200
|
+
)
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const copy = Object.create(groupByRef)
|
|
1204
|
+
// always alias for this special case, so that they nested element names match the expected result structure
|
|
1205
|
+
// otherwise we'd get `author { <outer>.author_ID }`, but we need `author { <outer>.author_ID as ID }`
|
|
1206
|
+
copy.as = expand.as || expand.ref.at(-1)
|
|
1207
|
+
const tableAlias = getTableAlias(copy)
|
|
1208
|
+
const res = getFlatColumnsFor(copy, { tableAlias })
|
|
1209
|
+
res.forEach(c => {
|
|
1210
|
+
elements[c.as || c.ref.at(-1)] = c.element
|
|
1211
|
+
})
|
|
1212
|
+
return res
|
|
1213
|
+
})
|
|
1214
|
+
.filter(c => c)
|
|
1215
|
+
|
|
1216
|
+
if (expandedColumns.length === 0) {
|
|
1217
|
+
return null
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const SELECT = {
|
|
1221
|
+
from: null,
|
|
1222
|
+
columns: expandedColumns,
|
|
1223
|
+
}
|
|
1224
|
+
return Object.defineProperties(
|
|
1225
|
+
{},
|
|
1226
|
+
{
|
|
1227
|
+
SELECT: {
|
|
1228
|
+
value: Object.defineProperties(SELECT, {
|
|
1229
|
+
expand: { value: true, writable: true }, // non-enumerable
|
|
1230
|
+
one: { value: column.$refLinks.at(-1).definition.is2one, writable: true }, // non-enumerable
|
|
1231
|
+
}),
|
|
1232
|
+
enumerable: true,
|
|
1233
|
+
},
|
|
1234
|
+
as: { value: subqueryAlias, enumerable: true, writable: true },
|
|
1235
|
+
elements: { value: elements }, // non-enumerable
|
|
1236
|
+
},
|
|
1237
|
+
)
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function getTransformedOrderByGroupBy(columns, inOrderBy = false) {
|
|
1242
|
+
const res = []
|
|
1243
|
+
for (let i = 0; i < columns.length; i++) {
|
|
1244
|
+
let col = columns[i]
|
|
1245
|
+
if (isCalculatedOnRead(col.$refLinks?.[col.$refLinks.length - 1].definition)) {
|
|
1246
|
+
const calcElement = resolveCalculatedElement(col, true)
|
|
1247
|
+
res.push(calcElement)
|
|
1248
|
+
} else if (pseudos.elements[col.ref?.[0]]) {
|
|
1249
|
+
res.push({ ...col })
|
|
1250
|
+
} else if (col.ref) {
|
|
1251
|
+
if (
|
|
1252
|
+
col.$refLinks.some(link => {
|
|
1253
|
+
const def = getDefinition(link.definition.target)
|
|
1254
|
+
return hasOwnSkip(def) && !isRuntimeView(def)
|
|
1255
|
+
})
|
|
1256
|
+
)
|
|
1257
|
+
continue
|
|
1258
|
+
if (col.ref.length > 1 && col.ref[0] === '$self' && !col.$refLinks[0].definition.kind) {
|
|
1259
|
+
const dollarSelfReplacement = calculateDollarSelfColumn(col)
|
|
1260
|
+
res.push(...getTransformedOrderByGroupBy([dollarSelfReplacement], inOrderBy))
|
|
1261
|
+
continue
|
|
1262
|
+
}
|
|
1263
|
+
const { target, definition } = col.$refLinks[0]
|
|
1264
|
+
let tableAlias = null
|
|
1265
|
+
if (target.SELECT?.columns && inOrderBy) {
|
|
1266
|
+
// usually TA is omitted if order by ref is a column
|
|
1267
|
+
// if a localized sorting is requested, we add `COLLATE`s
|
|
1268
|
+
// later on, which transforms the simple name to an expression
|
|
1269
|
+
// --> in an expression, only source elements can be addressed, hence we must add TA
|
|
1270
|
+
if (target.SELECT.localized && definition.type === 'cds.String') {
|
|
1271
|
+
const referredCol = target.SELECT.columns.find(c => {
|
|
1272
|
+
return c.as === col.ref[0] || c.ref?.at(-1) === col.ref[0]
|
|
1273
|
+
})
|
|
1274
|
+
if (referredCol) {
|
|
1275
|
+
// keep sort and nulls properties
|
|
1276
|
+
referredCol.sort = col.sort
|
|
1277
|
+
referredCol.nulls = col.nulls
|
|
1278
|
+
col = referredCol
|
|
1279
|
+
if (definition.kind === 'element') {
|
|
1280
|
+
tableAlias = getTableAlias(col)
|
|
1281
|
+
} else {
|
|
1282
|
+
// we must replace the reference with the underlying expression
|
|
1283
|
+
const { val, func, args, xpr } = col
|
|
1284
|
+
if (val) res.push({ val })
|
|
1285
|
+
if (func) res.push({ func, args })
|
|
1286
|
+
if (xpr) res.push({ xpr })
|
|
1287
|
+
continue
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
} else {
|
|
1292
|
+
tableAlias = getTableAlias(col) // do not prepend TA if orderBy column addresses element of query
|
|
1293
|
+
}
|
|
1294
|
+
const leaf = col.$refLinks[col.$refLinks.length - 1].definition
|
|
1295
|
+
if (leaf.virtual === true) continue // already in getFlatColumnForElement
|
|
1296
|
+
let baseName
|
|
1297
|
+
if (col.ref.length >= 2) {
|
|
1298
|
+
// leaf might be intermediate structure
|
|
1299
|
+
baseName = col.ref
|
|
1300
|
+
.map(idOnly)
|
|
1301
|
+
.slice(col.ref[0] === tableAlias ? 1 : 0, col.ref.length - 1)
|
|
1302
|
+
.join('_')
|
|
1303
|
+
}
|
|
1304
|
+
const flatColumns = getFlatColumnsFor(col, { baseName, tableAlias })
|
|
1305
|
+
/**
|
|
1306
|
+
* We can't guarantee that the element order will NOT change in the future.
|
|
1307
|
+
* We claim that the element order doesn't matter, hence we can't allow elements
|
|
1308
|
+
* in the order by clause which expand to more than one column, as the order impacts
|
|
1309
|
+
* the result.
|
|
1310
|
+
*/
|
|
1311
|
+
if (inOrderBy && flatColumns.length > 1)
|
|
1312
|
+
throw new Error(`Structured element “${getFullName(leaf)}” expands to multiple fields and can't be used in order by`)
|
|
1313
|
+
flatColumns.forEach(fc => {
|
|
1314
|
+
if (col.nulls) fc.nulls = col.nulls
|
|
1315
|
+
if (col.sort) fc.sort = col.sort
|
|
1316
|
+
if (fc.as) delete fc.as
|
|
1317
|
+
})
|
|
1318
|
+
res.push(...flatColumns)
|
|
1319
|
+
} else {
|
|
1320
|
+
let transformedColumn
|
|
1321
|
+
if (col.SELECT) transformedColumn = transformSubquery(col)
|
|
1322
|
+
else if (col.xpr) transformedColumn = { xpr: getTransformedTokenStream(col.xpr) }
|
|
1323
|
+
else if (col.func) transformedColumn = { args: getTransformedFunctionArgs(col.args), func: col.func }
|
|
1324
|
+
// val
|
|
1325
|
+
else transformedColumn = copy(col)
|
|
1326
|
+
if (col.sort) transformedColumn.sort = col.sort
|
|
1327
|
+
if (col.nulls) transformedColumn.nulls = col.nulls
|
|
1328
|
+
res.push(transformedColumn)
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return res
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
/**
|
|
1335
|
+
* Transforms a subquery.
|
|
1336
|
+
*
|
|
1337
|
+
* If the current query contains outer queries (is itself a subquery),
|
|
1338
|
+
* it appends the current inferred query.
|
|
1339
|
+
* Otherwise, it initializes the `outerQueries` array and adds the inferred query.
|
|
1340
|
+
* The `outerQueries` property makes sure
|
|
1341
|
+
* that the table aliases of the outer queries are accessible within the scope of the subquery.
|
|
1342
|
+
* Lastly, it recursively calls cqn4sql on the subquery.
|
|
1343
|
+
*
|
|
1344
|
+
* @param {object} q - The query to be transformed. This should be a subquery object.
|
|
1345
|
+
* @returns {object} - The cqn4sql transformed subquery.
|
|
1346
|
+
*/
|
|
1347
|
+
function transformSubquery(q) {
|
|
1348
|
+
if (q.outerQueries) q.outerQueries.push(inferred)
|
|
1349
|
+
else {
|
|
1350
|
+
const outerQueries = inferred.outerQueries || []
|
|
1351
|
+
outerQueries.push(inferred)
|
|
1352
|
+
defineProperty(q, 'outerQueries', outerQueries)
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
const target = cds.infer.target(inferred) // REVISIT: we should reliably use inferred._target instead
|
|
1356
|
+
if (isLocalized(target)) q.SELECT.localized = true
|
|
1357
|
+
if (q.SELECT.from.ref && !q.SELECT.from.as) assignUniqueSubqueryAlias()
|
|
1358
|
+
if (cds.env.features.runtime_views) q._with = transformedQuery._with
|
|
1359
|
+
const _q = cqn4sql(q, model, useTechnicalAlias)
|
|
1360
|
+
if (cds.env.features.runtime_views && _q._with) {
|
|
1361
|
+
if (!transformedQuery._with) transformedQuery._with = _q._with
|
|
1362
|
+
delete _q._with
|
|
1363
|
+
}
|
|
1364
|
+
return _q
|
|
1365
|
+
|
|
1366
|
+
|
|
1367
|
+
function assignUniqueSubqueryAlias() {
|
|
1368
|
+
if (q.SELECT.from.uniqueSubqueryAlias) return
|
|
1369
|
+
const last = q.SELECT.from.ref.at(-1)
|
|
1370
|
+
const uniqueSubqueryAlias = inferred.joinTree.addNextAvailableTableAlias(
|
|
1371
|
+
getImplicitAlias(last.id || last),
|
|
1372
|
+
inferred.outerQueries,
|
|
1373
|
+
)
|
|
1374
|
+
defineProperty(q.SELECT.from, 'uniqueSubqueryAlias', uniqueSubqueryAlias)
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/**
|
|
1379
|
+
* This function converts a wildcard into explicit columns.
|
|
1380
|
+
*
|
|
1381
|
+
* Based on the query's `$combinedElements` attribute, the function computes the flat column representations
|
|
1382
|
+
* and returns them. Additionally, it prepends the respective table alias to each column. Columns specified
|
|
1383
|
+
* in the `excluding` clause are ignored during this transformation.
|
|
1384
|
+
*
|
|
1385
|
+
* Furthermore, foreign keys (FK) for OData CSN and blobs are excluded from the wildcard expansion.
|
|
1386
|
+
*
|
|
1387
|
+
* @param {array} exclude - An optional list of columns to be excluded during the wildcard expansion.
|
|
1388
|
+
* @param {array} replace - An optional list of columns to replace during the wildcard expansion.
|
|
1389
|
+
* @param {string} baseName - the explicit alias of the column.
|
|
1390
|
+
* Only possible for anonymous expands on implicit table alias: `select from books { { * } as FOO }`
|
|
1391
|
+
*
|
|
1392
|
+
* @returns {Array} Returns an array of explicit columns derived from the wildcard.
|
|
1393
|
+
*/
|
|
1394
|
+
function getColumnsForWildcard(exclude = [], replace = [], baseName = null) {
|
|
1395
|
+
const wildcardColumns = []
|
|
1396
|
+
for (const k of Object.keys(inferred.$combinedElements)) {
|
|
1397
|
+
if (!exclude.includes(k)) {
|
|
1398
|
+
const { index, tableAlias } = inferred.$combinedElements[k][0]
|
|
1399
|
+
const element = tableAlias.elements[k]
|
|
1400
|
+
// ignore FK for odata csn (but not for subquery sources where FK is not a separate element) / ignore blobs from wildcard expansion
|
|
1401
|
+
if ((!tableAlias.SELECT && isManagedAssocInFlatMode(element)) || element.type === 'cds.LargeBinary') continue
|
|
1402
|
+
// for wildcard on subquery in from, just reference the elements
|
|
1403
|
+
if (tableAlias.SELECT && !element.elements && !element.target) {
|
|
1404
|
+
wildcardColumns.push(index ? { ref: [index, k] } : { ref: [k] })
|
|
1405
|
+
} else if (isCalculatedOnRead(element)) {
|
|
1406
|
+
wildcardColumns.push(resolveCalculatedElement(replace.find(r => r.as === k) || element))
|
|
1407
|
+
} else {
|
|
1408
|
+
const flatColumns = getFlatColumnsFor(
|
|
1409
|
+
element,
|
|
1410
|
+
{ tableAlias: index, baseName },
|
|
1411
|
+
[],
|
|
1412
|
+
{ exclude, replace },
|
|
1413
|
+
true,
|
|
1414
|
+
)
|
|
1415
|
+
wildcardColumns.push(...flatColumns)
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
return wildcardColumns
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* foreign keys are already part of the elements in a flat model
|
|
1423
|
+
* not excluding the associations from the wildcard columns would cause duplicate columns upon foreign key expansion
|
|
1424
|
+
* @param {CSN.element} e
|
|
1425
|
+
* @returns {boolean} true if the element is a managed association and the model is flat
|
|
1426
|
+
*/
|
|
1427
|
+
function isManagedAssocInFlatMode(e) {
|
|
1428
|
+
return (
|
|
1429
|
+
e.isAssociation && e.keys && (model.meta.transformation === 'odata' || model.meta.unfolded?.includes('structs'))
|
|
1430
|
+
)
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* Resolve `ref` within `def` and return the element
|
|
1436
|
+
*
|
|
1437
|
+
* @param {string[]} ref
|
|
1438
|
+
* @param {CSN.Artifact} def
|
|
1439
|
+
* @returns {CSN.Element}
|
|
1440
|
+
*/
|
|
1441
|
+
function getElementForRef(ref, def) {
|
|
1442
|
+
return ref.reduce((prev, res) => {
|
|
1443
|
+
return (prev?.elements || prev?.foreignKeys)?.[res] || getDefinition(prev?.target)?.elements[res] // PLEASE REVIEW: should we add the .foreignKey check here for the non-ucsn case?
|
|
1444
|
+
}, def)
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* Recursively expands a structured element into flat columns, representing all leaf paths.
|
|
1449
|
+
*
|
|
1450
|
+
* Structures → flat sub-element columns. Associations → flat foreign key columns.
|
|
1451
|
+
* Scalars → single column reference.
|
|
1452
|
+
*
|
|
1453
|
+
* @param {object} column - The element to expand (may be a ref with $refLinks, or a raw element definition).
|
|
1454
|
+
* @param {{
|
|
1455
|
+
* baseName?: string,
|
|
1456
|
+
* columnAlias?: string,
|
|
1457
|
+
* tableAlias?: string
|
|
1458
|
+
* }} [names] - Naming context:
|
|
1459
|
+
* - `baseName` — accumulated underscore-joined prefix for the flat column ref (e.g. `'address'` → `'address_street'`).
|
|
1460
|
+
* - `columnAlias` — explicit alias for the output column. Defaults to `column.as` when omitted.
|
|
1461
|
+
* - `tableAlias` — table alias prepended to the column ref.
|
|
1462
|
+
* @param {string[]} [csnPath=[]] - Accumulated CSN element path (used for `_csnPath` metadata on leaf columns).
|
|
1463
|
+
* @param {{ exclude?: Array, replace?: Array }} [excludeAndReplace] - Columns to exclude or replace during wildcard expansion.
|
|
1464
|
+
* @param {boolean} [isWildcard=false] - Whether this expansion originates from a wildcard; filters out LargeBinary.
|
|
1465
|
+
* @returns {object[]} Flat column(s) for the given element.
|
|
1466
|
+
*/
|
|
1467
|
+
function getFlatColumnsFor(column, names, csnPath = [], excludeAndReplace, isWildcard = false) {
|
|
1468
|
+
if (!column) return column
|
|
1469
|
+
if (column.val || column.func || column.SELECT) return [column]
|
|
1470
|
+
|
|
1471
|
+
const structsAreUnfoldedAlready = model.meta.unfolded?.includes('structs')
|
|
1472
|
+
let { baseName, columnAlias = column.as, tableAlias } = names || {}
|
|
1473
|
+
const { exclude, replace } = excludeAndReplace || {}
|
|
1474
|
+
const { $refLinks, flatName, isJoinRelevant } = column
|
|
1475
|
+
let firstNonJoinRelevantAssoc, stepAfterAssoc
|
|
1476
|
+
let element = $refLinks ? $refLinks[$refLinks.length - 1].definition : column
|
|
1477
|
+
if (isWildcard && element.type === 'cds.LargeBinary') return []
|
|
1478
|
+
if (element.on && !element.keys) return [] // unmanaged doesn't make it into columns
|
|
1479
|
+
if (element.virtual === true) return []
|
|
1480
|
+
|
|
1481
|
+
if (!isJoinRelevant && flatName) baseName = flatName
|
|
1482
|
+
else if (isJoinRelevant) {
|
|
1483
|
+
const earlyResult = resolveJoinRelevantNames()
|
|
1484
|
+
if (earlyResult) return earlyResult
|
|
1485
|
+
} else if (!baseName && structsAreUnfoldedAlready) {
|
|
1486
|
+
baseName = element.name // name is already fully constructed
|
|
1487
|
+
} else {
|
|
1488
|
+
baseName = baseName ? `${baseName}_${element.name}` : getFullName(element)
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// now we have the name of the to be expanded column
|
|
1492
|
+
// it could be a structure, an association or a scalar
|
|
1493
|
+
// check if the column shall be skipped
|
|
1494
|
+
// e.g. for wildcard elements which have been overwritten before
|
|
1495
|
+
if (exclude && getReplacement(exclude)) return []
|
|
1496
|
+
const replacedBy = getReplacement(replace)
|
|
1497
|
+
if (replacedBy) {
|
|
1498
|
+
// the replacement alias is the baseName of the flat structure
|
|
1499
|
+
// e.g. `office.{ *, address.city as floor }`
|
|
1500
|
+
// for the `ref: [ office, floor ]` we find the replacement
|
|
1501
|
+
// `ref: [ office, address, city]` so the `baseName` of the replacement
|
|
1502
|
+
if (replacedBy.as) replacedBy.as = baseName
|
|
1503
|
+
// we might have a new base ref
|
|
1504
|
+
if (replacedBy.ref && replacedBy.ref.length > 1)
|
|
1505
|
+
baseName = getFullName(replacedBy.$refLinks?.[replacedBy.$refLinks.length - 2].definition)
|
|
1506
|
+
if (replacedBy.isJoinRelevant)
|
|
1507
|
+
// we need to provide the correct table alias
|
|
1508
|
+
tableAlias = getTableAlias(replacedBy)
|
|
1509
|
+
|
|
1510
|
+
if (replacedBy.expand) return [{ as: baseName }]
|
|
1511
|
+
|
|
1512
|
+
return getFlatColumnsFor(replacedBy, { baseName, columnAlias: replacedBy.as, tableAlias }, csnPath)
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
csnPath = [...csnPath, element.name]
|
|
1516
|
+
|
|
1517
|
+
if (element.keys) return flattenForeignKeys()
|
|
1518
|
+
if (element.elements && element.type !== 'cds.Map') return flattenStructElements()
|
|
1519
|
+
return buildScalarColumn()
|
|
1520
|
+
|
|
1521
|
+
function flattenStructElements() {
|
|
1522
|
+
const flatRefs = []
|
|
1523
|
+
for (const e of Object.values(element.elements)) {
|
|
1524
|
+
const alias = columnAlias ? `${columnAlias}_${e.name}` : null
|
|
1525
|
+
flatRefs.push(
|
|
1526
|
+
...getFlatColumnsFor(
|
|
1527
|
+
e,
|
|
1528
|
+
{ baseName, columnAlias: alias, tableAlias },
|
|
1529
|
+
csnPath,
|
|
1530
|
+
excludeAndReplace,
|
|
1531
|
+
isWildcard,
|
|
1532
|
+
),
|
|
1533
|
+
)
|
|
1534
|
+
}
|
|
1535
|
+
return flatRefs
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
function buildScalarColumn() {
|
|
1539
|
+
const flatRef = tableAlias ? { ref: [tableAlias, baseName] } : { ref: [baseName] }
|
|
1540
|
+
if (column.cast) {
|
|
1541
|
+
flatRef.cast = column.cast
|
|
1542
|
+
if (!columnAlias) columnAlias = baseName
|
|
1543
|
+
}
|
|
1544
|
+
if (column.sort) flatRef.sort = column.sort
|
|
1545
|
+
if (columnAlias) flatRef.as = columnAlias
|
|
1546
|
+
setElementOnColumns(flatRef, element)
|
|
1547
|
+
defineProperty(flatRef, '_csnPath', csnPath)
|
|
1548
|
+
return [flatRef]
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
function getReplacement(from) {
|
|
1552
|
+
return from?.find(replacement => {
|
|
1553
|
+
const nameOfExcludedColumn = replacement.as || replacement.ref?.at(-1) || replacement
|
|
1554
|
+
return nameOfExcludedColumn === element.name
|
|
1555
|
+
})
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
function resolveJoinRelevantNames() {
|
|
1559
|
+
const leafAssocIndex = column.$refLinks.findIndex(
|
|
1560
|
+
link => link.definition.isAssociation && link.onlyForeignKeyAccess,
|
|
1561
|
+
)
|
|
1562
|
+
firstNonJoinRelevantAssoc =
|
|
1563
|
+
column.$refLinks[leafAssocIndex] || [...column.$refLinks].reverse().find(link => link.definition.isAssociation)
|
|
1564
|
+
stepAfterAssoc = column.$refLinks.at(leafAssocIndex + 1) || column.$refLinks.at(-1)
|
|
1565
|
+
const targetElements = firstNonJoinRelevantAssoc.definition.elements || firstNonJoinRelevantAssoc.definition.foreignKeys
|
|
1566
|
+
if (targetElements && stepAfterAssoc.definition.name in targetElements) {
|
|
1567
|
+
element = firstNonJoinRelevantAssoc.definition
|
|
1568
|
+
baseName = getFullName(firstNonJoinRelevantAssoc.definition)
|
|
1569
|
+
columnAlias = column.as || column.ref.slice(0, -1).map(idOnly).join('_')
|
|
1570
|
+
} else {
|
|
1571
|
+
baseName = getFullName(column.$refLinks.at(-1).definition)
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
if (column.element && !isAssocOrStruct(column.element)) {
|
|
1575
|
+
columnAlias =
|
|
1576
|
+
column.as || (leafAssocIndex === -1 ? columnAlias : column.ref.slice(leafAssocIndex - 1).map(idOnly).join('_'))
|
|
1577
|
+
const res = { ref: [tableAlias, calculateElementName(column)], as: columnAlias }
|
|
1578
|
+
setElementOnColumns(res, column.element)
|
|
1579
|
+
return [res]
|
|
1580
|
+
}
|
|
1581
|
+
return null
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
function flattenForeignKeys() {
|
|
1585
|
+
const flatColumns = []
|
|
1586
|
+
for (const k of element.keys) {
|
|
1587
|
+
const fkElement = getElementForRef(k.ref, getDefinition(element.target))
|
|
1588
|
+
// if only one part of a foreign key is requested, only flatten the partial key
|
|
1589
|
+
const shouldFlatten =
|
|
1590
|
+
!$refLinks || // the association is passed as element, not as ref --> flatten full foreign key
|
|
1591
|
+
element === $refLinks.at(-1).definition || // the association is the leaf of the ref --> flatten full foreign key
|
|
1592
|
+
fkElement === stepAfterAssoc.definition // the foreign key is the leaf of the ref --> only flatten this specific foreign key
|
|
1593
|
+
if (!shouldFlatten) continue
|
|
1594
|
+
|
|
1595
|
+
// e.g. if foreign key is accessed via infix filter - use join alias to access key in target
|
|
1596
|
+
const fkBaseName = !firstNonJoinRelevantAssoc || firstNonJoinRelevantAssoc.onlyForeignKeyAccess
|
|
1597
|
+
? `${baseName}_${k.as || k.ref.at(-1)}`
|
|
1598
|
+
: k.ref.at(-1)
|
|
1599
|
+
const fkPath = [...csnPath, k.ref.at(-1)]
|
|
1600
|
+
|
|
1601
|
+
if (fkElement.elements) {
|
|
1602
|
+
// structured key
|
|
1603
|
+
for (const e of Object.values(fkElement.elements)) {
|
|
1604
|
+
let alias
|
|
1605
|
+
if (columnAlias) {
|
|
1606
|
+
const fkName = k.as
|
|
1607
|
+
? `${k.as}_${e.name}` // foreign key might also be re-named: `assoc { id as foo }`
|
|
1608
|
+
: `${k.ref.join('_')}_${e.name}`
|
|
1609
|
+
alias = `${columnAlias}_${fkName}`
|
|
1610
|
+
}
|
|
1611
|
+
flatColumns.push(
|
|
1612
|
+
...getFlatColumnsFor(
|
|
1613
|
+
e,
|
|
1614
|
+
{ baseName: fkBaseName, columnAlias: alias, tableAlias },
|
|
1615
|
+
fkPath,
|
|
1616
|
+
excludeAndReplace,
|
|
1617
|
+
isWildcard,
|
|
1618
|
+
),
|
|
1619
|
+
)
|
|
1620
|
+
}
|
|
1621
|
+
} else if (fkElement.isAssociation) {
|
|
1622
|
+
// assoc as key
|
|
1623
|
+
flatColumns.push(
|
|
1624
|
+
...getFlatColumnsFor(fkElement, { baseName, columnAlias, tableAlias }, csnPath, excludeAndReplace, isWildcard),
|
|
1625
|
+
)
|
|
1626
|
+
} else {
|
|
1627
|
+
// leaf reached
|
|
1628
|
+
let flatColumn
|
|
1629
|
+
if (columnAlias) {
|
|
1630
|
+
// if the column has an explicit alias AND the original ref
|
|
1631
|
+
// directly resolves to the foreign key, we must not append the fk name to the column alias
|
|
1632
|
+
// e.g. `assoc.fk as FOO` => columns.alias = FOO
|
|
1633
|
+
// `assoc as FOO` => columns.alias = FOO_fk
|
|
1634
|
+
let fkAlias = columnAlias
|
|
1635
|
+
if (!(column.as && fkElement === column.$refLinks?.at(-1).definition))
|
|
1636
|
+
fkAlias = `${columnAlias}_${k.as || k.ref.join('_')}`
|
|
1637
|
+
flatColumn = { ref: [fkBaseName], as: fkAlias }
|
|
1638
|
+
} else {
|
|
1639
|
+
flatColumn = { ref: [fkBaseName] }
|
|
1640
|
+
}
|
|
1641
|
+
if (tableAlias) flatColumn.ref.unshift(tableAlias)
|
|
1642
|
+
|
|
1643
|
+
// in a flat model, we must assign the foreign key rather than the key in the target
|
|
1644
|
+
const flatForeignKey = getDefinition(element.parent.name)?.elements[fkBaseName]
|
|
1645
|
+
setElementOnColumns(flatColumn, flatForeignKey || fkElement)
|
|
1646
|
+
defineProperty(flatColumn, '_csnPath', csnPath)
|
|
1647
|
+
flatColumns.push(flatColumn)
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
return flatColumns
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
/**
|
|
1655
|
+
* Transforms a CQN token stream (e.g. `where`, `xpr` or `having`) into a SQL like expression.
|
|
1656
|
+
*
|
|
1657
|
+
* Expand `exists <assoc>` into `WHERE EXISTS` subqueries, apply flattening to `ref`s.
|
|
1658
|
+
* Recursively apply `cqn4sql` to query expressions found in the token stream.
|
|
1659
|
+
*
|
|
1660
|
+
* @param {object[]} tokenStream - The token stream to transform. Each token in the stream is an
|
|
1661
|
+
* object representing a CQN construct such as a column, an operator,
|
|
1662
|
+
* or a subquery.
|
|
1663
|
+
* @param {object} [context] - Optional context object.
|
|
1664
|
+
* @param {object} [context.$baseLink] - The context in which the `ref`s in the token stream are resolvable.
|
|
1665
|
+
* It serves as the reference point for resolving associations in
|
|
1666
|
+
* statements like `{…} WHERE exists assoc[exists anotherAssoc]`.
|
|
1667
|
+
* Here, the $baseLink for `anotherAssoc` would be `assoc`.
|
|
1668
|
+
* @param {string} [context.prop] - The query property which holds the token stream which shall be
|
|
1669
|
+
* transformed by this function, e.g. "where".
|
|
1670
|
+
* @returns {object[]} - The transformed token stream.
|
|
1671
|
+
*/
|
|
1672
|
+
function getTransformedTokenStream(tokenStream, context = {}) {
|
|
1673
|
+
const transformedTokenStream = []
|
|
1674
|
+
const { $baseLink, /* prop */ } = context
|
|
1675
|
+
for (let i = 0; i < tokenStream.length; i++) {
|
|
1676
|
+
const token = tokenStream[i]
|
|
1677
|
+
if (token === 'exists') {
|
|
1678
|
+
transformedTokenStream.push(token)
|
|
1679
|
+
const whereExistsSubSelects = []
|
|
1680
|
+
const { ref, $refLinks } = tokenStream[i + 1]
|
|
1681
|
+
if (!ref) continue
|
|
1682
|
+
if (ref[0] in { $self: true, $projection: true })
|
|
1683
|
+
throw new Error(`Unexpected "${ref[0]}" following "exists", remove it or add a table alias instead`)
|
|
1684
|
+
const firstStepIsTableAlias = ref.length > 1 && ref[0] in inferred.sources
|
|
1685
|
+
for (let j = 0; j < ref.length; j += 1) {
|
|
1686
|
+
let current, next
|
|
1687
|
+
const step = ref[j]
|
|
1688
|
+
const id = step.id || step
|
|
1689
|
+
if (j === 0) {
|
|
1690
|
+
if (firstStepIsTableAlias) continue
|
|
1691
|
+
current = $baseLink || {
|
|
1692
|
+
definition: $refLinks[0].target,
|
|
1693
|
+
target: $refLinks[0].target,
|
|
1694
|
+
// if the first step of a where is not a table alias,
|
|
1695
|
+
// the table alias is the query source where the current ref step
|
|
1696
|
+
// originates from. As no table alias is specified, there must be
|
|
1697
|
+
// only one table alias for the given ref step
|
|
1698
|
+
alias: inferred.$combinedElements[id][0].index,
|
|
1699
|
+
}
|
|
1700
|
+
next = $refLinks[0]
|
|
1701
|
+
} else {
|
|
1702
|
+
current = $refLinks[j - 1]
|
|
1703
|
+
next = $refLinks[j]
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
if (isStructured(next.definition)) {
|
|
1707
|
+
// find next association / entity in the ref because this is actually our real nextStep
|
|
1708
|
+
const nextAssocIndex =
|
|
1709
|
+
2 + $refLinks.slice(j + 2).findIndex(rl => rl.definition.isAssociation || rl.definition.kind === 'entity')
|
|
1710
|
+
next = $refLinks[nextAssocIndex]
|
|
1711
|
+
j = nextAssocIndex
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
const as = getNextAvailableTableAlias(getImplicitAlias(next.alias))
|
|
1715
|
+
next.alias = as
|
|
1716
|
+
if (!next.definition.target) {
|
|
1717
|
+
let type = next.definition.type
|
|
1718
|
+
if (isCalculatedElement(next.definition)) {
|
|
1719
|
+
// try to infer the type at the leaf for better error message
|
|
1720
|
+
const { $refLinks } = next.definition.value
|
|
1721
|
+
type = $refLinks?.at(-1).definition.type || 'expression'
|
|
1722
|
+
}
|
|
1723
|
+
throw new Error(
|
|
1724
|
+
`Expecting path “${tokenStream[i + 1].ref
|
|
1725
|
+
.map(idOnly)
|
|
1726
|
+
.join('.')}” following “EXISTS” predicate to end with association/composition, found “${type}”`,
|
|
1727
|
+
)
|
|
1728
|
+
}
|
|
1729
|
+
const { definition: fkSource } = next
|
|
1730
|
+
ensureValidForeignKeys(fkSource, ref)
|
|
1731
|
+
const { where, ...args } = step
|
|
1732
|
+
whereExistsSubSelects.push(getWhereExistsSubquery(current, next, where, true, args))
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
const whereExists = { SELECT: whereExistsSubqueries(whereExistsSubSelects) }
|
|
1736
|
+
transformedTokenStream[i + 1] = whereExists
|
|
1737
|
+
// skip newly created subquery from being iterated
|
|
1738
|
+
i += 1
|
|
1739
|
+
} else if (token !== null && typeof token === 'object' && '#' in token) {
|
|
1740
|
+
// Enum token: resolve to its value
|
|
1741
|
+
transformedTokenStream.push(resolveEnumToken(token, tokenStream, i))
|
|
1742
|
+
} else if (token.list) {
|
|
1743
|
+
if (token.list.length === 0) {
|
|
1744
|
+
// replace `[not] in <empty list>` to harmonize behavior across dbs
|
|
1745
|
+
const precedingTwoTokens = tokenStream.slice(i - 2, i)
|
|
1746
|
+
const firstPrecedingToken =
|
|
1747
|
+
typeof precedingTwoTokens[0] === 'string' ? precedingTwoTokens[0].toLowerCase() : ''
|
|
1748
|
+
const secondPrecedingToken =
|
|
1749
|
+
typeof precedingTwoTokens[1] === 'string' ? precedingTwoTokens[1].toLowerCase() : ''
|
|
1750
|
+
|
|
1751
|
+
if (firstPrecedingToken === 'not') {
|
|
1752
|
+
transformedTokenStream.splice(i - 2, 2, 'is', 'not', 'null')
|
|
1753
|
+
} else if (secondPrecedingToken === 'in') {
|
|
1754
|
+
transformedTokenStream.splice(i - 1, 1, '=', { val: null })
|
|
1755
|
+
} else {
|
|
1756
|
+
transformedTokenStream.push({ list: [] })
|
|
1757
|
+
}
|
|
1758
|
+
} else {
|
|
1759
|
+
let { list } = token
|
|
1760
|
+
// Resolve enum tokens in list items using context from the parent token stream
|
|
1761
|
+
if (list.some(e => e !== null && typeof e === 'object' && '#' in e)) {
|
|
1762
|
+
const enumDef = findEnumDefinition(tokenStream, i)
|
|
1763
|
+
list = list.map(item => (item !== null && typeof item === 'object' && '#' in item) ? resolveEnumToken(item, tokenStream, i, enumDef) : item)
|
|
1764
|
+
}
|
|
1765
|
+
if (list.every(e => 'val' in e))
|
|
1766
|
+
// no need for transformation
|
|
1767
|
+
transformedTokenStream.push({ list })
|
|
1768
|
+
else transformedTokenStream.push({ list: getTransformedTokenStream(list, { $baseLink, prop: 'list' }) })
|
|
1769
|
+
}
|
|
1770
|
+
} else if (tokenStream.length === 1 && token.val && $baseLink) {
|
|
1771
|
+
// infix filter - OData variant w/o mentioning key
|
|
1772
|
+
const def = getDefinition($baseLink.definition.target) || $baseLink.definition
|
|
1773
|
+
const flatKeys = getPrimaryKey(def, $baseLink.alias)
|
|
1774
|
+
if (flatKeys.length > 1) // TODO: what about keyless?
|
|
1775
|
+
throw new Error(`Shortcut notation “[${token.val}]” not available for composite primary key of “${def.name}”, write “<key> = ${token.val}” explicitly`)
|
|
1776
|
+
transformedTokenStream.push(...[flatKeys[0], '=', token]);
|
|
1777
|
+
} else if (token.ref && token.param) {
|
|
1778
|
+
transformedTokenStream.push({ ...token })
|
|
1779
|
+
} else if (pseudos.elements[token.ref?.[0]]) {
|
|
1780
|
+
transformedTokenStream.push({ ...token })
|
|
1781
|
+
} else {
|
|
1782
|
+
// expand `struct = null | struct2`
|
|
1783
|
+
const next = tokenStream[i + 1]
|
|
1784
|
+
let indexRhs = i + 2
|
|
1785
|
+
let rhs = tokenStream[indexRhs] // either another operator (i.e. `not like` et. al.) or the operand, i.e. the val | null
|
|
1786
|
+
const lhsDef = token.$refLinks?.at(-1).definition
|
|
1787
|
+
let rhsDef = rhs?.$refLinks?.at(-1)?.definition
|
|
1788
|
+
if (
|
|
1789
|
+
allOps.some(([firstOp]) => firstOp === next) &&
|
|
1790
|
+
(lhsDef?.elements || lhsDef?.keys || rhsDef?.elements || rhsDef?.keys)
|
|
1791
|
+
) {
|
|
1792
|
+
const ops = [next]
|
|
1793
|
+
if (allOps.some(([, secondOp]) => secondOp === rhs)) {
|
|
1794
|
+
ops.push(rhs)
|
|
1795
|
+
rhs = tokenStream[i + 3]
|
|
1796
|
+
indexRhs += 1
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
if (notSupportedOps.some(([firstOp]) => firstOp === next))
|
|
1800
|
+
throw new Error(`The operator "${next}" can only be used with scalar operands`)
|
|
1801
|
+
|
|
1802
|
+
const newTokens = expandComparison(token, ops, rhs, $baseLink)
|
|
1803
|
+
if (newTokens.length === 0)
|
|
1804
|
+
throw new Error(`Can't compare two empty structures`)
|
|
1805
|
+
|
|
1806
|
+
const needXpr = Boolean(tokenStream[i - 1] || tokenStream[indexRhs + 1])
|
|
1807
|
+
transformedTokenStream.push(...(needXpr ? [asXpr(newTokens)] : newTokens))
|
|
1808
|
+
i = indexRhs // jump to next relevant index
|
|
1809
|
+
} else {
|
|
1810
|
+
// reject associations in expression, except if we are in an infix filter -> $baseLink is set
|
|
1811
|
+
assertNoStructInXpr(token, context)
|
|
1812
|
+
// reject virtual elements in expressions as they will lead to a sql error down the line
|
|
1813
|
+
if (lhsDef?.virtual) throw new Error(`Virtual elements are not allowed in expressions`)
|
|
1814
|
+
|
|
1815
|
+
let result = is_regexp(token?.val) ? token : copy(token) // REVISIT: too expensive!
|
|
1816
|
+
// REVISIT: required because we copy the token above and lose the not enumerable "param: false"
|
|
1817
|
+
if (typeof token === 'object' && 'val' in token && 'param' in token) Object.defineProperty(result, 'param', { value: token.param })
|
|
1818
|
+
if (token.ref) {
|
|
1819
|
+
const { definition } = token.$refLinks.at(-1)
|
|
1820
|
+
// Add definition to result
|
|
1821
|
+
setElementOnColumns(result, definition)
|
|
1822
|
+
if (isCalculatedOnRead(definition)) {
|
|
1823
|
+
const calculatedElement = resolveCalculatedElement(token, true, $baseLink)
|
|
1824
|
+
transformedTokenStream.push(calculatedElement)
|
|
1825
|
+
continue
|
|
1826
|
+
}
|
|
1827
|
+
if (token.ref.length > 1 && token.ref[0] === '$self' && !token.$refLinks[0].definition.kind) {
|
|
1828
|
+
if (inferred.outerQueries) {
|
|
1829
|
+
const outerQuery = inferred.outerQueries[0]
|
|
1830
|
+
const stepToFind = token.ref[1]?.id || token.ref[1]
|
|
1831
|
+
const outerAlias = outerQuery.$combinedElements?.[stepToFind]?.[0].index
|
|
1832
|
+
if (outerAlias) {
|
|
1833
|
+
let result = copy(token)
|
|
1834
|
+
result.ref = [outerAlias, token.flatName]
|
|
1835
|
+
transformedTokenStream.push(result)
|
|
1836
|
+
continue
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
const dollarSelfReplacement = [calculateDollarSelfColumn(token, true)]
|
|
1840
|
+
transformedTokenStream.push(...getTransformedTokenStream(dollarSelfReplacement))
|
|
1841
|
+
continue
|
|
1842
|
+
}
|
|
1843
|
+
// if we have e.g. a calculated element like `books.authorLastName,`
|
|
1844
|
+
// we have effectively a ref ['books', 'author', 'lastName']
|
|
1845
|
+
// in that case, we have a baseLink `books` which we need to resolve the following steps
|
|
1846
|
+
// however, the correct table alias has been assigned to the `author` step
|
|
1847
|
+
// hence we need to ignore the alias of the `$baseLink`
|
|
1848
|
+
const lastAssoc =
|
|
1849
|
+
token.isJoinRelevant && [...token.$refLinks].reverse().find(l => l.definition.isAssociation)
|
|
1850
|
+
const tableAlias = getTableAlias(token, (!lastAssoc?.onlyForeignKeyAccess && lastAssoc) || $baseLink)
|
|
1851
|
+
if (isAssocOrStruct(definition)) {
|
|
1852
|
+
const flat = getFlatColumnsFor(token, { tableAlias: $baseLink?.alias || tableAlias })
|
|
1853
|
+
if (flat.length === 0)
|
|
1854
|
+
throw new Error(`Structured element “${getFullName(definition)}” expands to nothing and can't be used in expressions`)
|
|
1855
|
+
else if (flat.length > 1 && context.prop !== 'list') // only acceptable in `list`
|
|
1856
|
+
throw new Error(`Structured element “${getFullName(definition)}” expands to multiple fields and can't be used in expressions`)
|
|
1857
|
+
transformedTokenStream.push(...flat)
|
|
1858
|
+
continue
|
|
1859
|
+
} else if ((!$baseLink || lastAssoc) && token.isJoinRelevant) {
|
|
1860
|
+
let name = calculateElementName(token)
|
|
1861
|
+
result.ref = [tableAlias, name]
|
|
1862
|
+
} else if (tableAlias) {
|
|
1863
|
+
result.ref = [tableAlias, token.flatName]
|
|
1864
|
+
} else {
|
|
1865
|
+
// if there is no table alias, we might select from an anonymous subquery
|
|
1866
|
+
result.ref = [token.flatName]
|
|
1867
|
+
}
|
|
1868
|
+
} else if (token.SELECT) {
|
|
1869
|
+
result = transformSubquery(token)
|
|
1870
|
+
} else {
|
|
1871
|
+
if (token.xpr) {
|
|
1872
|
+
result.xpr = getTransformedTokenStream(token.xpr, { $baseLink })
|
|
1873
|
+
}
|
|
1874
|
+
if (token.func && token.args) {
|
|
1875
|
+
result.args = getTransformedFunctionArgs(token.args, $baseLink)
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
if (result.cast) result.cast = resolveEnumCastType(result.cast)
|
|
1880
|
+
transformedTokenStream.push(result)
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
return transformedTokenStream
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
/**
|
|
1888
|
+
* Expand the given definition and compare all leafs to `val`.
|
|
1889
|
+
*
|
|
1890
|
+
* @param {object} lhs with $refLinks
|
|
1891
|
+
* @param {string} operator one of allOps
|
|
1892
|
+
* @param {object} rhs either `null` or a column (with `ref` and `$refLinks`)
|
|
1893
|
+
* @param {object} $baseLink optional base `$refLink`, e.g. for infix filters of scoped queries.
|
|
1894
|
+
* In the following example, we must pass `bookshop:Reproduce` as $baseLink for `author`:
|
|
1895
|
+
*
|
|
1896
|
+
* `DELETE.from('bookshop.Reproduce[author = null]:accessGroup')`
|
|
1897
|
+
* ^^^^^^
|
|
1898
|
+
* @returns {array}
|
|
1899
|
+
*/
|
|
1900
|
+
function expandComparison(lhs, operator, rhs, $baseLink = null) {
|
|
1901
|
+
const { definition: lhsDef, val: lhsVal } = lhs.val ? lhs : lhs.$refLinks.at(-1)
|
|
1902
|
+
const { definition: rhsDef, val: rhsVal } = rhs.val ? rhs : rhs.$refLinks?.at(-1) || {}
|
|
1903
|
+
const result = []
|
|
1904
|
+
if (lhsDef && rhsDef) {
|
|
1905
|
+
// both must be structured
|
|
1906
|
+
const lhsIsStructured = isAssocOrStruct(lhsDef)
|
|
1907
|
+
const rhsIsStructured = isAssocOrStruct(rhsDef)
|
|
1908
|
+
if (!lhsIsStructured)
|
|
1909
|
+
throw new Error(`Can't compare structure “${rhs.ref.map(idOnly).join('.')}” with non-structure “${lhs.ref.map(idOnly).join('.')}”`)
|
|
1910
|
+
if (!rhsIsStructured)
|
|
1911
|
+
throw new Error(`Can't compare structure “${lhs.ref.map(idOnly).join('.')}” with non-structure “${rhs.ref.map(idOnly).join('.')}”`)
|
|
1912
|
+
|
|
1913
|
+
const flatLhs = flattenWithBaseName(lhs)
|
|
1914
|
+
const flatRhs = flattenWithBaseName(rhs)
|
|
1915
|
+
const boolOp = notEqOps.some(([f, s]) => operator[0] === f && operator[1] === s) ? 'or' : 'and'
|
|
1916
|
+
while (flatLhs.length > 0) {
|
|
1917
|
+
// retrieve and remove one flat element from LHS and search for it in RHS (remove it there too)
|
|
1918
|
+
const { ref, _csnPath: lhs_csnPath } = flatLhs.shift()
|
|
1919
|
+
const indexOfElementOnRhs = flatRhs.findIndex(rhs => {
|
|
1920
|
+
const { _csnPath: rhs_csnPath } = rhs
|
|
1921
|
+
// all following steps must also be part of lhs
|
|
1922
|
+
return lhs_csnPath.slice(1).every((val, i) => val === rhs_csnPath[i + 1]) // first step is name of struct -> ignore
|
|
1923
|
+
})
|
|
1924
|
+
// not found in rhs --> exit
|
|
1925
|
+
if (indexOfElementOnRhs === -1) {
|
|
1926
|
+
const lhsPath = lhs.ref.map(idOnly).join('.')
|
|
1927
|
+
const rhsPath = rhs.ref.map(idOnly).join('.')
|
|
1928
|
+
throw new Error(`Can't compare "${lhsPath}" with "${rhsPath}": the operands must have the same structure`)
|
|
1929
|
+
}
|
|
1930
|
+
const cleansedRhs = flatRhs.splice(indexOfElementOnRhs, 1)[0] // remove the element also from RHS
|
|
1931
|
+
result.push({ ref }, ...operator, cleansedRhs)
|
|
1932
|
+
if (flatLhs.length > 0) result.push(boolOp)
|
|
1933
|
+
}
|
|
1934
|
+
} else if (lhsDef && (rhsVal || rhs === 'null' || rhs.val === null)) {
|
|
1935
|
+
// compare with value
|
|
1936
|
+
const flatLhs = flattenWithBaseName(lhs)
|
|
1937
|
+
if (flatLhs.length !== 1 && rhsVal && rhs !== 'null')
|
|
1938
|
+
canOnlyCompareToExactlyOneLeaf(lhsDef, lhs.ref, rhsVal)
|
|
1939
|
+
|
|
1940
|
+
const boolOp = notEqOps.some(([f, s]) => operator[0] === f && operator[1] === s) ? 'or' : 'and'
|
|
1941
|
+
flatLhs.forEach((column, i) => {
|
|
1942
|
+
result.push(column, ...operator, rhs)
|
|
1943
|
+
if (flatLhs[i + 1]) result.push(boolOp)
|
|
1944
|
+
})
|
|
1945
|
+
} else if (lhsVal && rhsDef) {
|
|
1946
|
+
const flatRhs = flattenWithBaseName(rhs)
|
|
1947
|
+
// comparing a struct to a value is ok if structure has exactly one leaf
|
|
1948
|
+
if (flatRhs.length !== 1 && lhsVal)
|
|
1949
|
+
canOnlyCompareToExactlyOneLeaf(rhsDef, rhs.ref, lhsVal)
|
|
1950
|
+
|
|
1951
|
+
const boolOp = notEqOps.some(([f, s]) => operator[0] === f && operator[1] === s) ? 'or' : 'and'
|
|
1952
|
+
flatRhs.forEach((column, i) => {
|
|
1953
|
+
result.push(lhs, ...operator, column)
|
|
1954
|
+
if (flatRhs[i + 1]) result.push(boolOp)
|
|
1955
|
+
})
|
|
1956
|
+
}
|
|
1957
|
+
return result
|
|
1958
|
+
|
|
1959
|
+
function canOnlyCompareToExactlyOneLeaf(struct, structRef, val) {
|
|
1960
|
+
const what = struct.isAssociation ? 'association' : 'structure'
|
|
1961
|
+
const postfix = struct.isAssociation ? 'associations with one foreign key' : 'structures with one sub-element'
|
|
1962
|
+
throw new Error(
|
|
1963
|
+
`Can't compare ${what} "${structRef.map(idOnly).join('.')}" to value "${val}"; only possible for ${postfix}`
|
|
1964
|
+
)
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
function flattenWithBaseName(def) {
|
|
1968
|
+
if (!def.$refLinks) return def
|
|
1969
|
+
const leaf = def.$refLinks[def.$refLinks.length - 1]
|
|
1970
|
+
const first = def.$refLinks[0]
|
|
1971
|
+
const tableAlias = getTableAlias(def, def.ref.length > 1 && first.definition.isAssociation ? first : $baseLink)
|
|
1972
|
+
if (leaf.definition.parent.kind !== 'entity')
|
|
1973
|
+
// we need the base name
|
|
1974
|
+
return getFlatColumnsFor(leaf.definition, {
|
|
1975
|
+
baseName: def.ref
|
|
1976
|
+
.map(idOnly)
|
|
1977
|
+
.slice(0, def.ref.length - 1)
|
|
1978
|
+
.join('_'),
|
|
1979
|
+
tableAlias,
|
|
1980
|
+
})
|
|
1981
|
+
return getFlatColumnsFor(leaf.definition, { tableAlias })
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
function assertNoStructInXpr(token, context) {
|
|
1986
|
+
const definition = token.$refLinks?.at(-1).definition
|
|
1987
|
+
if (!definition) return
|
|
1988
|
+
const rejectStructs = context && (context.prop in { where: 1, having: 1 })
|
|
1989
|
+
// unmanaged is always forbidden
|
|
1990
|
+
// expanding a ref in a `where`/`having` context
|
|
1991
|
+
if ((rejectStructs && definition?.target) || definition?.on)
|
|
1992
|
+
rejectAssocInExpression()
|
|
1993
|
+
if (rejectStructs && isStructured(definition))
|
|
1994
|
+
// REVISIT: let this through if not requested otherwise
|
|
1995
|
+
rejectStructInExpression()
|
|
1996
|
+
|
|
1997
|
+
function rejectAssocInExpression() {
|
|
1998
|
+
throw new Error(`An association can't be used as a value in an expression`)
|
|
1999
|
+
}
|
|
2000
|
+
function rejectStructInExpression() {
|
|
2001
|
+
throw new Error(`A structured element can't be used as a value in an expression`)
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
/**
|
|
2006
|
+
* Recursively walks over all `from` args. Association steps in the `ref`s
|
|
2007
|
+
* are transformed into `WHERE exists` subqueries. The given `from.ref`s
|
|
2008
|
+
* are always of length == 1 after processing.
|
|
2009
|
+
*
|
|
2010
|
+
* The steps in a `ref` are processed in reversed order. This is the main difference
|
|
2011
|
+
* to the `WHERE exists` expansion in the @function getTransformedTokenStream().
|
|
2012
|
+
*
|
|
2013
|
+
* @param {object} from
|
|
2014
|
+
* @param {object[]?} existingWhere custom where condition which is appended to the filter
|
|
2015
|
+
* conditions of the resulting `WHERE exists` subquery
|
|
2016
|
+
*/
|
|
2017
|
+
function getTransformedFrom(from, existingWhere = []) {
|
|
2018
|
+
const transformedWhere = []
|
|
2019
|
+
let transformedFrom = copy(from) // REVISIT: too expensive!
|
|
2020
|
+
if (from.$refLinks) defineProperty(transformedFrom, '$refLinks', [...from.$refLinks])
|
|
2021
|
+
if (from.args) {
|
|
2022
|
+
transformedFrom.args = []
|
|
2023
|
+
from.args.forEach(arg => {
|
|
2024
|
+
if (arg.SELECT) {
|
|
2025
|
+
const { whereExists: e, transformedFrom: f } = getTransformedFrom(arg.SELECT.from, arg.SELECT.where)
|
|
2026
|
+
const transformedArg = { SELECT: { from: f, where: e } }
|
|
2027
|
+
transformedFrom.args.push(transformedArg)
|
|
2028
|
+
} else {
|
|
2029
|
+
const { transformedFrom: f } = getTransformedFrom(arg)
|
|
2030
|
+
transformedFrom.args.push(f)
|
|
2031
|
+
}
|
|
2032
|
+
})
|
|
2033
|
+
return { transformedFrom, transformedWhere: existingWhere }
|
|
2034
|
+
} else if (from.SELECT) {
|
|
2035
|
+
transformedFrom = transformSubquery(from)
|
|
2036
|
+
if (from.as) {
|
|
2037
|
+
// preserve explicit TA
|
|
2038
|
+
transformedFrom.as = from.as
|
|
2039
|
+
} else {
|
|
2040
|
+
// select from anonymous query, use artificial alias
|
|
2041
|
+
transformedFrom.as = Object.keys(inferred.sources)[0]
|
|
2042
|
+
}
|
|
2043
|
+
return { transformedFrom, transformedWhere: existingWhere }
|
|
2044
|
+
} else {
|
|
2045
|
+
return _transformFrom()
|
|
2046
|
+
}
|
|
2047
|
+
function _transformFrom() {
|
|
2048
|
+
if (typeof from === 'string') {
|
|
2049
|
+
// normalize to `ref`, i.e. for `UPDATE.entity('bookshop.Books')`
|
|
2050
|
+
return { transformedFrom: { ref: [from], as: getImplicitAlias(from) } }
|
|
2051
|
+
}
|
|
2052
|
+
transformedFrom.as =
|
|
2053
|
+
from.uniqueSubqueryAlias ||
|
|
2054
|
+
from.as ||
|
|
2055
|
+
getImplicitAlias(transformedFrom.$refLinks[transformedFrom.$refLinks.length - 1].definition.name)
|
|
2056
|
+
const whereExistsSubSelects = []
|
|
2057
|
+
const filterConditions = []
|
|
2058
|
+
const refReverse = [...from.ref].reverse()
|
|
2059
|
+
const $refLinksReverse = [...transformedFrom.$refLinks].reverse()
|
|
2060
|
+
for (let i = 0; i < refReverse.length; i += 1) {
|
|
2061
|
+
const current = $refLinksReverse[i]
|
|
2062
|
+
|
|
2063
|
+
let next = $refLinksReverse[i + 1]
|
|
2064
|
+
const nextStep = refReverse[i + 1] // only because we want the filter condition
|
|
2065
|
+
|
|
2066
|
+
if (current.definition.target && next) {
|
|
2067
|
+
const { where, ...args } = nextStep
|
|
2068
|
+
if (isStructured(next.definition)) {
|
|
2069
|
+
// find next association / entity in the ref because this is actually our real nextStep
|
|
2070
|
+
const nextStepIndex =
|
|
2071
|
+
2 +
|
|
2072
|
+
$refLinksReverse
|
|
2073
|
+
.slice(i + 2)
|
|
2074
|
+
.findIndex(rl => rl.definition.isAssociation || rl.definition.kind === 'entity')
|
|
2075
|
+
next = $refLinksReverse[nextStepIndex]
|
|
2076
|
+
}
|
|
2077
|
+
let as = getImplicitAlias(next.alias)
|
|
2078
|
+
/**
|
|
2079
|
+
* for an `expand` subquery, we do not need to add
|
|
2080
|
+
* the table alias of the `expand` host to the join tree
|
|
2081
|
+
* --> This is an artificial query, which will later be correlated
|
|
2082
|
+
* with the main query alias. see @function expandColumn()
|
|
2083
|
+
* There is one exception:
|
|
2084
|
+
* - if current and next have the same alias, we need to assign a new alias to the next
|
|
2085
|
+
*
|
|
2086
|
+
*/
|
|
2087
|
+
if (!(inferred.SELECT?.expand === true && current.alias.toLowerCase() !== as.toLowerCase())) {
|
|
2088
|
+
as = getNextAvailableTableAlias(as)
|
|
2089
|
+
}
|
|
2090
|
+
next.alias = as
|
|
2091
|
+
const { definition: fkSource } = current
|
|
2092
|
+
ensureValidForeignKeys(fkSource, from.ref)
|
|
2093
|
+
whereExistsSubSelects.push(getWhereExistsSubquery(current, next, where, false, args))
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
// OData variant w/o mentioning key
|
|
2098
|
+
if (refReverse[0].where?.length === 1 && refReverse[0].where[0].val) {
|
|
2099
|
+
filterConditions.push(getTransformedTokenStream(refReverse[0].where, { $baseLink: $refLinksReverse[0] }))
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
if (existingWhere.length > 0) filterConditions.push(existingWhere)
|
|
2103
|
+
if (whereExistsSubSelects.length > 0) {
|
|
2104
|
+
const { definition: leafAssoc, alias } = transformedFrom.$refLinks[transformedFrom.$refLinks.length - 1]
|
|
2105
|
+
Object.assign(transformedFrom, {
|
|
2106
|
+
ref: [leafAssoc.target],
|
|
2107
|
+
as: alias,
|
|
2108
|
+
})
|
|
2109
|
+
transformedWhere.push(...['exists', { SELECT: whereExistsSubqueries(whereExistsSubSelects) }])
|
|
2110
|
+
filterConditions.forEach(f => {
|
|
2111
|
+
transformedWhere.push('and')
|
|
2112
|
+
if (filterConditions.length > 1) transformedWhere.push(asXpr(f))
|
|
2113
|
+
else if (f.length > 3 || f.includes('or') || f.includes('and') || f.includes('in')) transformedWhere.push(asXpr(f))
|
|
2114
|
+
else transformedWhere.push(...f)
|
|
2115
|
+
})
|
|
2116
|
+
} else {
|
|
2117
|
+
if (filterConditions.length > 0) {
|
|
2118
|
+
filterConditions.reverse().forEach((f, index) => {
|
|
2119
|
+
if (filterConditions.length > 1) transformedWhere.push(asXpr(f))
|
|
2120
|
+
else transformedWhere.push(...f)
|
|
2121
|
+
if (filterConditions[index + 1] !== undefined) transformedWhere.push('and')
|
|
2122
|
+
})
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
// adjust ref & $refLinks after associations have turned into where exists subqueries
|
|
2127
|
+
transformedFrom.$refLinks.splice(0, transformedFrom.$refLinks.length - 1)
|
|
2128
|
+
|
|
2129
|
+
const subquerySource =
|
|
2130
|
+
getDefinition(transformedFrom.$refLinks[0].definition.target) || transformedFrom.$refLinks[0].target
|
|
2131
|
+
const id = getLocalizedName(subquerySource)
|
|
2132
|
+
transformedFrom.ref = [subquerySource.params ? { id, args: from.ref.at(-1).args || {} } : id]
|
|
2133
|
+
|
|
2134
|
+
return { transformedWhere, transformedFrom }
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
function ensureValidForeignKeys(fkSource, ref, kind = null) {
|
|
2139
|
+
if (fkSource.keys && fkSource.keys.length === 0) {
|
|
2140
|
+
const path = prettyPrintRef(ref, model)
|
|
2141
|
+
if (kind === 'expand') throw new Error(`Can't expand “${fkSource.name}” as it has no foreign keys`)
|
|
2142
|
+
throw new Error(`Path step “${fkSource.name}” of “${path}” has no foreign keys`)
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
function whereExistsSubqueries(whereExistsSubSelects) {
|
|
2147
|
+
if (whereExistsSubSelects.length === 1) return whereExistsSubSelects[0]
|
|
2148
|
+
whereExistsSubSelects.reduce((prev, cur) => {
|
|
2149
|
+
if (prev.where) {
|
|
2150
|
+
prev.where.push('and', 'exists', { SELECT: cur })
|
|
2151
|
+
return cur
|
|
2152
|
+
} else {
|
|
2153
|
+
prev = cur
|
|
2154
|
+
}
|
|
2155
|
+
return prev
|
|
2156
|
+
}, {})
|
|
2157
|
+
return whereExistsSubSelects[0]
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
function getNextAvailableTableAlias(id) {
|
|
2161
|
+
return inferred.joinTree.addNextAvailableTableAlias(id, inferred.outerQueries)
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
/**
|
|
2165
|
+
* @param {CSN.Element} elt
|
|
2166
|
+
* @returns {boolean}
|
|
2167
|
+
*/
|
|
2168
|
+
function isStructured(elt) {
|
|
2169
|
+
return Boolean(elt?.kind !== 'entity' && elt?.elements && !elt.isAssociation)
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
/**
|
|
2173
|
+
* @param {CSN.Element} elt
|
|
2174
|
+
* @returns {boolean}
|
|
2175
|
+
*/
|
|
2176
|
+
function isAssocOrStruct(elt) {
|
|
2177
|
+
return elt?.keys || (elt?.elements && elt.kind === 'element')
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
/**
|
|
2181
|
+
* Calculates which elements are the backlinks of a $self comparison in a
|
|
2182
|
+
* given on-condition. That are the managed associations in the target of the
|
|
2183
|
+
* given association.
|
|
2184
|
+
*
|
|
2185
|
+
* @param {CSN.Association} assoc with on-condition
|
|
2186
|
+
* @returns {[CSN.Association] | null} all assocs which are compared to `$self`
|
|
2187
|
+
*/
|
|
2188
|
+
function backlinkFor(assoc) {
|
|
2189
|
+
if (!assoc.on) return null
|
|
2190
|
+
const target = getDefinition(assoc.target)
|
|
2191
|
+
// technically we could have multiple backlinks
|
|
2192
|
+
const backlinks = []
|
|
2193
|
+
for (let i = 0; i < assoc.on.length; i += 3) {
|
|
2194
|
+
const lhs = assoc.on[i]
|
|
2195
|
+
const rhs = assoc.on[i + 2]
|
|
2196
|
+
if (lhs?.ref?.length === 1 && lhs.ref[0] === '$self') backlinks.push(rhs)
|
|
2197
|
+
else if (rhs?.ref?.length === 1 && rhs.ref[0] === '$self') backlinks.push(lhs)
|
|
2198
|
+
}
|
|
2199
|
+
return backlinks.map(each =>
|
|
2200
|
+
getElementForRef(
|
|
2201
|
+
each.ref.slice(1),
|
|
2202
|
+
each.ref[0] in { $self: true, $projection: true } ? getParentEntity(assoc) : target,
|
|
2203
|
+
),
|
|
2204
|
+
)
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
/**
|
|
2208
|
+
* Calculates the on-condition for the given (un-)managed association.
|
|
2209
|
+
*
|
|
2210
|
+
* @param {$refLink} assocRefLink with on-condition
|
|
2211
|
+
* @param {$refLink} targetSideRefLink the reflink which has the target alias of the association
|
|
2212
|
+
* @returns {[CSN.Association] | null} all assocs which are compared to `$self`
|
|
2213
|
+
*/
|
|
2214
|
+
function onCondFor(assocRefLink, targetSideRefLink, inWhereOrJoin) {
|
|
2215
|
+
const { on, keys } = assocRefLink.definition
|
|
2216
|
+
const target = getDefinition(assocRefLink.definition.target)
|
|
2217
|
+
let res
|
|
2218
|
+
if (keys) {
|
|
2219
|
+
const fkPkPairs = getParentKeyForeignKeyPairs(assocRefLink.definition, targetSideRefLink, true)
|
|
2220
|
+
const transformedOn = []
|
|
2221
|
+
fkPkPairs.forEach((pair, i) => {
|
|
2222
|
+
const { sourceSide, targetSide } = pair
|
|
2223
|
+
sourceSide.ref.unshift(assocRefLink.alias)
|
|
2224
|
+
transformedOn.push(sourceSide, '=', targetSide)
|
|
2225
|
+
if (fkPkPairs[i + 1]) transformedOn.push('and')
|
|
2226
|
+
})
|
|
2227
|
+
res = transformedOn
|
|
2228
|
+
} else if (on) {
|
|
2229
|
+
res = calculateOnCondition(on)
|
|
2230
|
+
}
|
|
2231
|
+
return res
|
|
2232
|
+
|
|
2233
|
+
/**
|
|
2234
|
+
* For an unmanaged association, calculate the proper on-condition.
|
|
2235
|
+
* For a `$self = assoc.<backlink>` comparison, the three tokens are replaced
|
|
2236
|
+
* by the on-condition of the <backlink>.
|
|
2237
|
+
*
|
|
2238
|
+
*
|
|
2239
|
+
* @param {on} tokenStream the on condition of the unmanaged association
|
|
2240
|
+
* @returns the final on-condition for the unmanaged association
|
|
2241
|
+
*/
|
|
2242
|
+
function calculateOnCondition(tokenStream) {
|
|
2243
|
+
const result = copy(tokenStream) // REVISIT: too expensive!
|
|
2244
|
+
for (let i = 0; i < result.length; i += 1) {
|
|
2245
|
+
const lhs = result[i]
|
|
2246
|
+
if (lhs.xpr) {
|
|
2247
|
+
const xpr = calculateOnCondition(lhs.xpr)
|
|
2248
|
+
result[i] = asXpr(xpr)
|
|
2249
|
+
continue
|
|
2250
|
+
}
|
|
2251
|
+
if (lhs.args) {
|
|
2252
|
+
const args = calculateOnCondition(lhs.args)
|
|
2253
|
+
result[i] = { ...lhs, args }
|
|
2254
|
+
continue
|
|
2255
|
+
}
|
|
2256
|
+
const rhs = result[i + 2]
|
|
2257
|
+
if (rhs?.ref || lhs.ref) {
|
|
2258
|
+
// if we have refs on each side of the comparison, we might need to perform tuple expansion
|
|
2259
|
+
// or flatten the structures
|
|
2260
|
+
const refLinkFaker = thing => {
|
|
2261
|
+
const { ref } = thing
|
|
2262
|
+
const assocHost = getParentEntity(assocRefLink.definition)
|
|
2263
|
+
defineProperty(thing, '$refLinks', [])
|
|
2264
|
+
let pseudoPath = false
|
|
2265
|
+
ref.reduce((prev, res, i) => {
|
|
2266
|
+
if (res === '$self') {
|
|
2267
|
+
// next is resolvable in entity
|
|
2268
|
+
thing.$refLinks.push({ definition: prev, target: prev })
|
|
2269
|
+
return prev
|
|
2270
|
+
}
|
|
2271
|
+
if (res in pseudos.elements) {
|
|
2272
|
+
pseudoPath = true
|
|
2273
|
+
thing.$refLinks.push({ definition: pseudos.elements[res], target: pseudos })
|
|
2274
|
+
return pseudos.elements[res]
|
|
2275
|
+
} else if (pseudoPath) {
|
|
2276
|
+
thing.$refLinks.push({ definition: {}, target: pseudos })
|
|
2277
|
+
return prev?.elements[res]
|
|
2278
|
+
}
|
|
2279
|
+
const definition =
|
|
2280
|
+
prev?.elements?.[res] || getDefinition(prev?.target)?.elements[res] || pseudos.elements[res]
|
|
2281
|
+
const target = getParentEntity(definition)
|
|
2282
|
+
thing.$refLinks[i] = {
|
|
2283
|
+
definition,
|
|
2284
|
+
target,
|
|
2285
|
+
alias: definition === assocRefLink.definition ? assocRefLink.alias : definition.name,
|
|
2286
|
+
}
|
|
2287
|
+
return prev?.elements?.[res] || getDefinition(prev?.target)?.elements[res] || pseudos.elements[res]
|
|
2288
|
+
}, assocHost)
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
// comparison in on condition needs to be expanded...
|
|
2292
|
+
// re-use existing algorithm for that
|
|
2293
|
+
// we need to fake some $refLinks for that to work though...
|
|
2294
|
+
lhs?.ref && !lhs.$refLinks && refLinkFaker(lhs)
|
|
2295
|
+
rhs?.ref && !rhs.$refLinks && refLinkFaker(rhs)
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
let backlink
|
|
2299
|
+
if (rhs?.ref && lhs?.ref) {
|
|
2300
|
+
if (lhs?.ref?.length === 1 && lhs.ref[0] === '$self')
|
|
2301
|
+
backlink = getElementForRef(
|
|
2302
|
+
rhs.ref.slice(1),
|
|
2303
|
+
rhs.ref[0] in { $self: true, $projection: true } ? getParentEntity(assocRefLink.definition) : target,
|
|
2304
|
+
)
|
|
2305
|
+
else if (rhs?.ref?.length === 1 && rhs.ref[0] === '$self')
|
|
2306
|
+
backlink = getElementForRef(
|
|
2307
|
+
lhs.ref.slice(1),
|
|
2308
|
+
lhs.ref[0] in { $self: true, $projection: true } ? getParentEntity(assocRefLink.definition) : target,
|
|
2309
|
+
)
|
|
2310
|
+
else {
|
|
2311
|
+
const lhsLeafDef = lhs.ref && lhs.$refLinks.at(-1).definition
|
|
2312
|
+
const rhsLeafDef = rhs.ref && rhs.$refLinks.at(-1).definition
|
|
2313
|
+
const lhsFirstDef = lhs.$refLinks.find(r => r.definition.kind === 'element')?.definition
|
|
2314
|
+
// compare structures in on-condition
|
|
2315
|
+
if ((lhsLeafDef?.target && rhsLeafDef?.target) || (lhsLeafDef?.elements && rhsLeafDef?.elements)) {
|
|
2316
|
+
if (rhs.$refLinks[0].definition !== assocRefLink.definition) {
|
|
2317
|
+
rhs.ref.unshift(targetSideRefLink.alias)
|
|
2318
|
+
rhs.$refLinks.unshift(targetSideRefLink)
|
|
2319
|
+
}
|
|
2320
|
+
if (lhsFirstDef !== assocRefLink.definition) {
|
|
2321
|
+
lhs.ref.unshift(targetSideRefLink.alias)
|
|
2322
|
+
lhs.$refLinks.unshift(targetSideRefLink)
|
|
2323
|
+
}
|
|
2324
|
+
const expandedComparison = getTransformedTokenStream([lhs, result[i + 1], rhs])
|
|
2325
|
+
const res = tokenStream[i + 3] ? [asXpr(expandedComparison)] : expandedComparison
|
|
2326
|
+
result.splice(i, 3, ...res)
|
|
2327
|
+
i += res.length
|
|
2328
|
+
continue
|
|
2329
|
+
}
|
|
2330
|
+
// assumption: if first step is the association itself, all following ref steps must be resolvable
|
|
2331
|
+
// within target `assoc.assoc.fk` -> `assoc.assoc_fk`
|
|
2332
|
+
else if (lhsFirstDef === getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name])
|
|
2333
|
+
result[i].ref = [assocRefLink.alias, lhs.ref.slice(lhs.ref[0] === '$self' ? 2 : 1).join('_')]
|
|
2334
|
+
// naive assumption: if the path starts with an association which is not the association from
|
|
2335
|
+
// which the on-condition originates, it must be a foreign key and hence resolvable in the source
|
|
2336
|
+
else if (lhsFirstDef?.target) result[i].ref = [result[i].ref.join('_')]
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
if (backlink) {
|
|
2340
|
+
const wrapInXpr = result[i + 3] || result[i - 1] // if we have a complex on-condition, wrap each part in xpr
|
|
2341
|
+
let backlinkOnCondition = []
|
|
2342
|
+
if (backlink.on) {
|
|
2343
|
+
// unmanaged backlink -> prepend correct aliases
|
|
2344
|
+
backlinkOnCondition = backlink.on.map(t => {
|
|
2345
|
+
if (t.ref?.length > 1 && t.ref[0] === (backlink.name || targetSideRefLink.definition.name)) {
|
|
2346
|
+
return { ref: [targetSideRefLink.alias, ...t.ref.slice(1)] }
|
|
2347
|
+
} else if (t.ref) {
|
|
2348
|
+
if (t.ref.length > 1 && !(t.ref[0] in pseudos.elements))
|
|
2349
|
+
return { ref: [assocRefLink.alias, ...t.ref.slice(1)] }
|
|
2350
|
+
else return { ref: [assocRefLink.alias, ...t.ref] }
|
|
2351
|
+
} else {
|
|
2352
|
+
return t
|
|
2353
|
+
}
|
|
2354
|
+
})
|
|
2355
|
+
} else if (backlink.keys) {
|
|
2356
|
+
// sanity check: error out if we can't produce a join
|
|
2357
|
+
if (backlink.keys.length === 0) {
|
|
2358
|
+
throw new Error(
|
|
2359
|
+
`Path step “${assocRefLink.definition.name}” is a self comparison with “${getFullName(backlink)}” that has no foreign keys`,
|
|
2360
|
+
)
|
|
2361
|
+
}
|
|
2362
|
+
// managed backlink -> calculate fk-pk pairs
|
|
2363
|
+
const fkPkPairs = getParentKeyForeignKeyPairs(backlink, targetSideRefLink)
|
|
2364
|
+
fkPkPairs.forEach((pair, j) => {
|
|
2365
|
+
const { sourceSide, targetSide } = pair
|
|
2366
|
+
sourceSide.ref.unshift(assocRefLink.alias)
|
|
2367
|
+
backlinkOnCondition.push(sourceSide, '=', targetSide)
|
|
2368
|
+
if (!inWhereOrJoin) backlinkOnCondition.reverse()
|
|
2369
|
+
if (fkPkPairs[j + 1]) backlinkOnCondition.push('and')
|
|
2370
|
+
})
|
|
2371
|
+
}
|
|
2372
|
+
result.splice(i, 3, ...(wrapInXpr ? [asXpr(backlinkOnCondition)] : backlinkOnCondition))
|
|
2373
|
+
i += wrapInXpr ? 1 : backlinkOnCondition.length // skip inserted tokens
|
|
2374
|
+
} else if (lhs.ref && lhs.$refLinks[0]?.target !== pseudos) {
|
|
2375
|
+
if (lhs.ref[0] === '$self') {
|
|
2376
|
+
// $self in ref of length > 1
|
|
2377
|
+
// if $self is followed by association, the alias of the association must be used
|
|
2378
|
+
if (lhs.$refLinks[1].definition.isAssociation) result[i].ref.splice(0, 1)
|
|
2379
|
+
// otherwise $self is replaced by the alias of the entity
|
|
2380
|
+
else result[i].ref.splice(0, 1, targetSideRefLink.alias)
|
|
2381
|
+
} else if (lhs.ref.length > 1) {
|
|
2382
|
+
if (lhs.ref[0] !== assocRefLink.alias && lhs.ref[0] !== targetSideRefLink.alias) {
|
|
2383
|
+
// we need to find correct table alias for the structured access
|
|
2384
|
+
const { definition } = lhs.$refLinks[0]
|
|
2385
|
+
if (definition === getParentEntity(assocRefLink.definition).elements[assocRefLink.definition.name]) {
|
|
2386
|
+
// first step is the association itself -> use it's name as it becomes the table alias
|
|
2387
|
+
result[i].ref.splice(0, 1, assocRefLink.alias)
|
|
2388
|
+
} else if (
|
|
2389
|
+
definition.name in
|
|
2390
|
+
(targetSideRefLink.definition.elements || getDefinition(targetSideRefLink.definition.target).elements)
|
|
2391
|
+
) {
|
|
2392
|
+
// first step is association which refers to its foreign key by dot notation
|
|
2393
|
+
result[i].ref = [targetSideRefLink.alias, lhs.ref.join('_')]
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
} else if (lhs.ref.length === 1) result[i].ref.unshift(targetSideRefLink.alias)
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
return result
|
|
2400
|
+
}
|
|
2401
|
+
/**
|
|
2402
|
+
* Recursively calculates the containing entity for a given element.
|
|
2403
|
+
* If the query is localized, this function may return the localized entity.
|
|
2404
|
+
*
|
|
2405
|
+
* @param {CSN.element} element
|
|
2406
|
+
* @returns {CSN.definition} the entity containing the given element
|
|
2407
|
+
*/
|
|
2408
|
+
function getParentEntity(element) {
|
|
2409
|
+
if (!element.kind)
|
|
2410
|
+
// pseudo element
|
|
2411
|
+
return element
|
|
2412
|
+
if (element.kind === 'entity') return element
|
|
2413
|
+
else return getDefinition(getParentEntity(element.parent).name)
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
/**
|
|
2418
|
+
* For a given managed association, calculate the foreign key - parent key tuples.
|
|
2419
|
+
*
|
|
2420
|
+
* @param {CDS.Association} assoc the association for which the on condition shall be calculated
|
|
2421
|
+
* @param {object} targetSideRefLink the reflink which has the target alias of the (backlink) association
|
|
2422
|
+
* @param {boolean} flipSourceAndTarget target and source side are flipped in the where exists subquery
|
|
2423
|
+
* @returns {[{sourceSide: {ref: []}, targetSide: {ref:[]}}]} array of source side - target side reference tuples, i.e. the foreign keys and parent keys.
|
|
2424
|
+
*/
|
|
2425
|
+
function getParentKeyForeignKeyPairs(assoc, targetSideRefLink, flipSourceAndTarget = false) {
|
|
2426
|
+
const res = []
|
|
2427
|
+
const backlink = backlinkFor(assoc)?.[0]
|
|
2428
|
+
const { keys, target } = backlink || assoc
|
|
2429
|
+
if (keys) {
|
|
2430
|
+
keys.forEach(fk => {
|
|
2431
|
+
const { ref, as } = fk
|
|
2432
|
+
const elem = getElementForRef(ref, getDefinition(target)) // find the element (the target element of the foreign key) in the target of the (backlink) association
|
|
2433
|
+
const flatParentKeys = getFlatColumnsFor(elem, { baseName: ref.slice(0, ref.length - 1).join('_') }) // it might be a structured element, so expand it into the full parent key tuple
|
|
2434
|
+
const flatAssociationName = getFullName(backlink || assoc) // get the name of the (backlink) association
|
|
2435
|
+
const flatForeignKeys = getFlatColumnsFor(elem, { baseName: flatAssociationName, columnAlias: as }) // the name of the (backlink) association is the base of the foreign key tuple, also respect aliased fk.
|
|
2436
|
+
|
|
2437
|
+
for (let i = 0; i < flatForeignKeys.length; i++) {
|
|
2438
|
+
if (flipSourceAndTarget) {
|
|
2439
|
+
// `where exists <assoc>` expansion
|
|
2440
|
+
// a backlink will cause the foreign key to be on the source side
|
|
2441
|
+
const refInTarget = backlink ? flatParentKeys[i] : flatForeignKeys[i]
|
|
2442
|
+
const refInSource = backlink ? flatForeignKeys[i] : flatParentKeys[i]
|
|
2443
|
+
res.push({
|
|
2444
|
+
sourceSide: refInSource,
|
|
2445
|
+
targetSide: {
|
|
2446
|
+
ref: [
|
|
2447
|
+
targetSideRefLink.alias,
|
|
2448
|
+
refInTarget.as ? `${flatAssociationName}_${refInTarget.as}` : refInTarget.ref[0],
|
|
2449
|
+
],
|
|
2450
|
+
},
|
|
2451
|
+
})
|
|
2452
|
+
} else {
|
|
2453
|
+
// `select from <assoc>` to `where exists` expansion
|
|
2454
|
+
// a backlink will cause the foreign key to be on the target side
|
|
2455
|
+
const refInTarget = backlink ? flatForeignKeys[i] : flatParentKeys[i]
|
|
2456
|
+
const refInSource = backlink ? flatParentKeys[i] : flatForeignKeys[i]
|
|
2457
|
+
res.push({
|
|
2458
|
+
sourceSide: {
|
|
2459
|
+
ref: [refInSource.as ? `${flatAssociationName}_${refInSource.as}` : refInSource.ref[0]],
|
|
2460
|
+
},
|
|
2461
|
+
targetSide: { ref: [targetSideRefLink.alias, ...refInTarget.ref] },
|
|
2462
|
+
})
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
})
|
|
2466
|
+
}
|
|
2467
|
+
return res
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
/**
|
|
2471
|
+
* Constructs a where exists subquery for a given association - i.e. calculates foreign key / parent key
|
|
2472
|
+
* relations for the association.
|
|
2473
|
+
*
|
|
2474
|
+
* @param {$refLink} current step of the association path
|
|
2475
|
+
* @param {$refLink} next step of the association path
|
|
2476
|
+
* @param {object[]} customWhere infix filter which must be part of the where exists subquery on condition
|
|
2477
|
+
* @param {boolean} inWhere whether or not the path is part of the queries where clause
|
|
2478
|
+
* -> if it is, target and source side are flipped in the where exists subquery
|
|
2479
|
+
* @param {object} queryModifier optional query modifiers: group by, order by, limit, offset
|
|
2480
|
+
*
|
|
2481
|
+
* @returns {CQN.SELECT}
|
|
2482
|
+
*/
|
|
2483
|
+
function getWhereExistsSubquery(current, next, customWhere = null, inWhere = false, queryModifier = null) {
|
|
2484
|
+
const { definition } = current
|
|
2485
|
+
const { definition: nextDefinition } = next
|
|
2486
|
+
const on = []
|
|
2487
|
+
const fkSource = inWhere ? nextDefinition : definition
|
|
2488
|
+
// TODO: use onCondFor()
|
|
2489
|
+
if (fkSource.keys) {
|
|
2490
|
+
const pkFkPairs = getParentKeyForeignKeyPairs(fkSource, current, inWhere)
|
|
2491
|
+
pkFkPairs.forEach((pkFkPair, i) => {
|
|
2492
|
+
const { targetSide, sourceSide } = pkFkPair
|
|
2493
|
+
sourceSide.ref.unshift(next.alias)
|
|
2494
|
+
if (i > 0) on.push('and')
|
|
2495
|
+
on.push(sourceSide, '=', targetSide)
|
|
2496
|
+
})
|
|
2497
|
+
} else {
|
|
2498
|
+
const unmanagedOn = onCondFor(inWhere ? next : current, inWhere ? current : next, inWhere)
|
|
2499
|
+
on.push(...(customWhere && hasLogicalOr(unmanagedOn) ? [asXpr(unmanagedOn)] : unmanagedOn))
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
const subquerySource = getDefinition(nextDefinition.target) || nextDefinition
|
|
2503
|
+
const id = getLocalizedName(subquerySource)
|
|
2504
|
+
const SELECT = {
|
|
2505
|
+
from: {
|
|
2506
|
+
ref: [subquerySource.params ? { id, args: queryModifier.args || {} } : id],
|
|
2507
|
+
as: next.alias,
|
|
2508
|
+
},
|
|
2509
|
+
columns: [
|
|
2510
|
+
{
|
|
2511
|
+
val: 1,
|
|
2512
|
+
// as: 'dummy'
|
|
2513
|
+
},
|
|
2514
|
+
],
|
|
2515
|
+
where: on,
|
|
2516
|
+
}
|
|
2517
|
+
// this requires sub-sequent transformation of the subquery
|
|
2518
|
+
if (
|
|
2519
|
+
next.pathExpressionInsideFilter ||
|
|
2520
|
+
(queryModifier && ['orderBy', 'groupBy', 'having', 'limit', 'offset'].some(key => key in queryModifier))
|
|
2521
|
+
) {
|
|
2522
|
+
SELECT.where = customWhere || []
|
|
2523
|
+
if (queryModifier) assignQueryModifiers(SELECT, queryModifier)
|
|
2524
|
+
|
|
2525
|
+
const transformedExists = transformSubquery({ SELECT })
|
|
2526
|
+
if (transformedExists.SELECT.where?.length) {
|
|
2527
|
+
const wrappedWhere = hasLogicalOr(transformedExists.SELECT.where)
|
|
2528
|
+
? [asXpr(transformedExists.SELECT.where)]
|
|
2529
|
+
: transformedExists.SELECT.where
|
|
2530
|
+
|
|
2531
|
+
on.push('and', ...wrappedWhere)
|
|
2532
|
+
}
|
|
2533
|
+
transformedExists.SELECT.where = on
|
|
2534
|
+
return transformedExists.SELECT
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
if (customWhere) {
|
|
2538
|
+
const filter = getTransformedTokenStream(customWhere, { $baseLink: next })
|
|
2539
|
+
const wrappedFilter = hasLogicalOr(filter) ? [asXpr(filter)] : filter
|
|
2540
|
+
on.push('and', ...wrappedFilter)
|
|
2541
|
+
}
|
|
2542
|
+
return SELECT
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
/**
|
|
2546
|
+
* For a given search term calculate a search expression which can be used in a where clause.
|
|
2547
|
+
* The search function is pushed to a subquery and the primary key(s) of the entity is/are used to match
|
|
2548
|
+
* the search results of the subquery.
|
|
2549
|
+
*
|
|
2550
|
+
* @param {object} searchTerm - The search expression which shall be applied to the searchable columns on the query source.
|
|
2551
|
+
* @param {object} query - The FROM clause of the CQN statement.
|
|
2552
|
+
*
|
|
2553
|
+
* @returns {(Object|null)} returns either:
|
|
2554
|
+
* - an expression of the form `<primaryKey> in (select <primaryKey> from <entity> where search(<searchableColumns>, <searchTerm>))`
|
|
2555
|
+
* - a function with two arguments: The first one being the list of searchable columns, the second argument holds the search expression.
|
|
2556
|
+
* - or null, if no searchable columns are found in neither in `@cds.search` nor in the target entity itself.
|
|
2557
|
+
*/
|
|
2558
|
+
function getSearch(searchTerm, query) {
|
|
2559
|
+
const entity = query.SELECT.from.SELECT ? query.SELECT.from : cds.infer.target(query) // REVISIT: we should reliably use inferred._target instead
|
|
2560
|
+
const searchIn = computeColumnsToBeSearched(inferred, entity)
|
|
2561
|
+
if (searchIn.length === 0) return null
|
|
2562
|
+
|
|
2563
|
+
const searchFunc = {
|
|
2564
|
+
func: 'search',
|
|
2565
|
+
args: [
|
|
2566
|
+
searchIn.length === 1 ? searchIn[0] : { list: searchIn },
|
|
2567
|
+
searchTerm.length === 1 && 'val' in searchTerm[0] ? searchTerm[0] : { xpr: searchTerm },
|
|
2568
|
+
],
|
|
2569
|
+
}
|
|
2570
|
+
// for aggregated queries / search on subqueries we do not do a subquery search
|
|
2571
|
+
if (inferred.SELECT.groupBy || entity.SELECT)
|
|
2572
|
+
return searchFunc
|
|
2573
|
+
|
|
2574
|
+
const matchColumns = getPrimaryKey(entity)
|
|
2575
|
+
if (matchColumns.length === 0 || searchIn.every(r => r.ref.length === 1)) // keyless or not deep, fallback to old behavior
|
|
2576
|
+
return searchFunc
|
|
2577
|
+
|
|
2578
|
+
const subquery = SELECT.from(entity).columns(...matchColumns).where(searchFunc)
|
|
2579
|
+
return { xpr: [matchColumns.length === 1 ? matchColumns[0] : { list: matchColumns }, 'in', subquery] }
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
/**
|
|
2583
|
+
* Calculates the name of the source which can be used to address the given node.
|
|
2584
|
+
*
|
|
2585
|
+
* @param {object} node a csn object with a `ref` and `$refLinks`
|
|
2586
|
+
* @param {object} $baseLink optional base `$refLink`, e.g. for infix filters.
|
|
2587
|
+
* For an infix filter, we must explicitly pass the TA name
|
|
2588
|
+
* because the first step of the ref might not be part of
|
|
2589
|
+
* the combined elements of the query
|
|
2590
|
+
* @returns the source name which can be used to address the node
|
|
2591
|
+
*/
|
|
2592
|
+
function getTableAlias(node, $baseLink = null) {
|
|
2593
|
+
if (!node || !node.$refLinks || !node.ref) {
|
|
2594
|
+
throw new Error('Invalid node')
|
|
2595
|
+
}
|
|
2596
|
+
if (node.$refLinks[0].$main) {
|
|
2597
|
+
if (node.isJoinRelevant) {
|
|
2598
|
+
return getJoinRelevantAlias(node)
|
|
2599
|
+
}
|
|
2600
|
+
return node.$refLinks[0].alias
|
|
2601
|
+
}
|
|
2602
|
+
if ($baseLink) {
|
|
2603
|
+
return getBaseLinkAlias($baseLink)
|
|
2604
|
+
}
|
|
2605
|
+
if (node.isJoinRelevant) {
|
|
2606
|
+
return getJoinRelevantAlias(node)
|
|
2607
|
+
}
|
|
2608
|
+
return getSelectOrEntityAlias(node) || getCombinedElementAlias(node)
|
|
2609
|
+
function getBaseLinkAlias($baseLink) {
|
|
2610
|
+
return $baseLink.alias
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
function getJoinRelevantAlias(node) {
|
|
2614
|
+
return [...node.$refLinks]
|
|
2615
|
+
.reverse()
|
|
2616
|
+
.find($refLink => $refLink.definition.isAssociation && !$refLink.onlyForeignKeyAccess).alias
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
function getSelectOrEntityAlias(node) {
|
|
2620
|
+
let firstRefLink = node.$refLinks[0].definition
|
|
2621
|
+
if (firstRefLink.SELECT || firstRefLink.kind === 'entity') {
|
|
2622
|
+
const firstStep = node.ref[0]
|
|
2623
|
+
/**
|
|
2624
|
+
* If the node.ref refers to an implicit alias which is later on changed by cqn4sql,
|
|
2625
|
+
* we need to replace the usage of the implicit alias, with the correct, auto-generated table alias.
|
|
2626
|
+
*
|
|
2627
|
+
* This is the case if the following holds true:
|
|
2628
|
+
* - the original query has NO explicit alias
|
|
2629
|
+
* - ref[0] equals the implicit alias of the query (i.e. from.ref[ from.length - 1 ].split('.').pop())
|
|
2630
|
+
* - but differs from the explicit alias, assigned by cqn4sql (i.e. <subquery>.from.uniqueSubqueryAlias)
|
|
2631
|
+
*/
|
|
2632
|
+
if (
|
|
2633
|
+
inferred.SELECT?.from.uniqueSubqueryAlias &&
|
|
2634
|
+
!inferred.SELECT?.from.as &&
|
|
2635
|
+
getImplicitAlias(firstStep) === getImplicitAlias(transformedQuery.SELECT.from.ref[0])
|
|
2636
|
+
) {
|
|
2637
|
+
return inferred.SELECT?.from.uniqueSubqueryAlias
|
|
2638
|
+
}
|
|
2639
|
+
return node.ref[0]
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
function getCombinedElementAlias(node) {
|
|
2644
|
+
return inferred.$combinedElements[node.ref[0].id || node.ref[0]]?.[0].index
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
function getTransformedFunctionArgs(args, $baseLink = null) {
|
|
2648
|
+
let result = null
|
|
2649
|
+
if (Array.isArray(args)) {
|
|
2650
|
+
result = args.flatMap(t => {
|
|
2651
|
+
if (!t.val)
|
|
2652
|
+
// this must not be touched
|
|
2653
|
+
return getTransformedTokenStream([t], { $baseLink })
|
|
2654
|
+
return t
|
|
2655
|
+
})
|
|
2656
|
+
} else if (typeof args === 'object') {
|
|
2657
|
+
result = {}
|
|
2658
|
+
for (const prop in args) {
|
|
2659
|
+
const t = args[prop]
|
|
2660
|
+
if (!t.val)
|
|
2661
|
+
// this must not be touched
|
|
2662
|
+
result[prop] = getTransformedTokenStream([t], { $baseLink })[0]
|
|
2663
|
+
else result[prop] = t
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
return result
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
/**
|
|
2670
|
+
* Resolves an enum token to a value literal.
|
|
2671
|
+
*
|
|
2672
|
+
* If the token already has a `val`, it is used directly.
|
|
2673
|
+
* Otherwise, the enum value is resolved by looking up the symbol
|
|
2674
|
+
* in the enum definition found from the surrounding context.
|
|
2675
|
+
*
|
|
2676
|
+
* @param {object} token - The enum token with a `#` property.
|
|
2677
|
+
* @param {object[]} tokenStream - The surrounding token stream for context discovery.
|
|
2678
|
+
* @param {number} index - The index of the enum token in the token stream.
|
|
2679
|
+
* @param {object} [enumDef] - An already-discovered enum definition (optimization for lists).
|
|
2680
|
+
* @returns {object} A value token `{ val: resolvedValue }`.
|
|
2681
|
+
*/
|
|
2682
|
+
function resolveEnumToken(token, tokenStream, index, enumDef) {
|
|
2683
|
+
if ('val' in token) {
|
|
2684
|
+
const result = { val: token.val }
|
|
2685
|
+
if (token.cast) result.cast = resolveEnumCastType(token.cast)
|
|
2686
|
+
return result
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
// Check if the token itself has a cast with an enum type
|
|
2690
|
+
if (!enumDef && token.cast?.type) {
|
|
2691
|
+
const typeDef = model.definitions[token.cast.type]
|
|
2692
|
+
if (typeDef?.enum) enumDef = typeDef.enum
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
if (!enumDef) enumDef = findEnumDefinition(tokenStream, index)
|
|
2696
|
+
if (!enumDef) {
|
|
2697
|
+
throw new Error(`Can't resolve enum value "#${token['#']}"`)
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
const entry = enumDef[token['#']]
|
|
2701
|
+
if (!entry) {
|
|
2702
|
+
throw new Error(`Unknown enum symbol "#${token['#']}"`)
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
const result = { val: 'val' in entry ? entry.val : token['#'] }
|
|
2706
|
+
if (token.cast) result.cast = resolveEnumCastType(token.cast)
|
|
2707
|
+
return result
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
/**
|
|
2711
|
+
* If `cast.type` refers to a user-defined enum type, resolves it to the
|
|
2712
|
+
* underlying scalar CDS built-in type so that the SQL builder (`cqn2sql`)
|
|
2713
|
+
* can render a valid SQL type name.
|
|
2714
|
+
*
|
|
2715
|
+
* Example: `{ type: 'enums.Priority' }` → `{ type: 'cds.Integer' }`
|
|
2716
|
+
*
|
|
2717
|
+
* Non-enum types (including CDS built-ins) are returned unchanged.
|
|
2718
|
+
*
|
|
2719
|
+
* @param {object} cast - The cast descriptor with a `type` property.
|
|
2720
|
+
* @returns {object} The cast descriptor with the resolved type.
|
|
2721
|
+
*/
|
|
2722
|
+
function resolveEnumCastType(cast) {
|
|
2723
|
+
if (!cast?.type) return cast
|
|
2724
|
+
let def = model.definitions[cast.type]
|
|
2725
|
+
while (def?.enum) {
|
|
2726
|
+
const baseType = def.type
|
|
2727
|
+
if (!baseType) return cast // no base type declared – leave as-is
|
|
2728
|
+
if (cds.builtin.types[baseType]) return { ...cast, type: baseType }
|
|
2729
|
+
def = model.definitions[baseType]
|
|
2730
|
+
}
|
|
2731
|
+
return cast
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
/**
|
|
2735
|
+
* Scans the token stream around the given index to find an element
|
|
2736
|
+
* definition that has an `enum` property, which can be used to resolve
|
|
2737
|
+
* enum symbols to their values.
|
|
2738
|
+
*
|
|
2739
|
+
* @param {object[]} tokenStream - The token stream to scan.
|
|
2740
|
+
* @param {number} index - The index of the enum token.
|
|
2741
|
+
* @returns {object|null} The enum definition object, or null if not found.
|
|
2742
|
+
*/
|
|
2743
|
+
function findEnumDefinition(tokenStream, index) {
|
|
2744
|
+
// Scan backward
|
|
2745
|
+
for (let j = index - 1; j >= 0; j--) {
|
|
2746
|
+
const t = tokenStream[j]
|
|
2747
|
+
if (typeof t === 'string') continue // operators, keywords
|
|
2748
|
+
if (t !== null && typeof t === 'object' && '#' in t) continue // other enum tokens
|
|
2749
|
+
if ('val' in t && !t.ref) continue // plain value literals
|
|
2750
|
+
|
|
2751
|
+
const def = t.$refLinks?.at(-1)?.definition
|
|
2752
|
+
if (def?.enum) return def.enum
|
|
2753
|
+
if (t.cast?.type) {
|
|
2754
|
+
const typeDef = model.definitions[t.cast.type]
|
|
2755
|
+
if (typeDef?.enum) return typeDef.enum
|
|
2756
|
+
}
|
|
2757
|
+
if (def) break // found a ref without enum type, stop
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
// Scan forward
|
|
2761
|
+
for (let j = index + 1; j < tokenStream.length; j++) {
|
|
2762
|
+
const t = tokenStream[j]
|
|
2763
|
+
if (typeof t === 'string') continue
|
|
2764
|
+
if (t !== null && typeof t === 'object' && '#' in t) continue
|
|
2765
|
+
if ('val' in t && !t.ref) continue
|
|
2766
|
+
|
|
2767
|
+
const def = t.$refLinks?.at(-1)?.definition
|
|
2768
|
+
if (def?.enum) return def.enum
|
|
2769
|
+
if (t.cast?.type) {
|
|
2770
|
+
const typeDef = model.definitions[t.cast.type]
|
|
2771
|
+
if (typeDef?.enum) return typeDef.enum
|
|
2772
|
+
}
|
|
2773
|
+
if (def) break
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
return null
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
function calculateElementName(token) {
|
|
2781
|
+
const nonJoinRelevantAssoc = [...token.$refLinks].findIndex(l => l.definition.isAssociation && l.onlyForeignKeyAccess)
|
|
2782
|
+
let name
|
|
2783
|
+
if (nonJoinRelevantAssoc !== -1)
|
|
2784
|
+
// calculate fk name
|
|
2785
|
+
name = token.ref.slice(nonJoinRelevantAssoc).join('_')
|
|
2786
|
+
else name = getFullName(token.$refLinks[token.$refLinks.length - 1].definition)
|
|
2787
|
+
return name
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
/**
|
|
2791
|
+
* Calculate the flat name for a deeply nested element:
|
|
2792
|
+
* @example `entity E { struct: { foo: String} }` => `getFullName(foo)` => `struct_foo`
|
|
2793
|
+
*
|
|
2794
|
+
* @param {CSN.element} node an element
|
|
2795
|
+
* @param {object} name the last part of the name, e.g. the name of the deeply nested element
|
|
2796
|
+
* @returns the flat name of the element
|
|
2797
|
+
*/
|
|
2798
|
+
function getFullName(node, name = node.name) {
|
|
2799
|
+
// REVISIT: this is an unfortunate implementation
|
|
2800
|
+
if (!node.parent || node.parent.kind === 'entity') return name
|
|
2801
|
+
|
|
2802
|
+
return getFullName(node.parent, `${node.parent.name}_${name}`)
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
function copy(obj) {
|
|
2806
|
+
const walk = function (par, prop) {
|
|
2807
|
+
const val = prop ? par[prop] : par
|
|
2808
|
+
|
|
2809
|
+
// If value is native return
|
|
2810
|
+
if (typeof val !== 'object' || val == null || val instanceof RegExp || val instanceof Date || val instanceof Buffer)
|
|
2811
|
+
return val
|
|
2812
|
+
|
|
2813
|
+
const ret = Array.isArray(val) ? [] : {}
|
|
2814
|
+
Object.keys(val).forEach(k => {
|
|
2815
|
+
ret[k] = walk(val, k)
|
|
2816
|
+
})
|
|
2817
|
+
return ret
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
return walk(obj)
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
function hasLogicalOr(tokenStream) {
|
|
2824
|
+
return tokenStream.some(t => t in { OR: true, or: true })
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
function getParentEntity(element) {
|
|
2828
|
+
if (element.kind === 'entity') return element
|
|
2829
|
+
else return getParentEntity(element.parent)
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
function asXpr(thing) {
|
|
2833
|
+
return { xpr: thing }
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
function assignQueryModifiers(SELECT, modifiers) {
|
|
2837
|
+
for (const [key, val] of Object.entries(modifiers)) {
|
|
2838
|
+
if (key in { orderBy: 1, groupBy: 1 }) {
|
|
2839
|
+
if (SELECT[key]) SELECT[key].push(...val)
|
|
2840
|
+
else SELECT[key] = val
|
|
2841
|
+
} else if (key === 'limit') {
|
|
2842
|
+
// limit defined on the query has precedence
|
|
2843
|
+
if (!SELECT.limit) SELECT.limit = val
|
|
2844
|
+
} else if (key === 'having') {
|
|
2845
|
+
if (!SELECT.having) SELECT.having = val
|
|
2846
|
+
else SELECT.having.push('and', ...val)
|
|
2847
|
+
} else if (key === 'where') {
|
|
2848
|
+
// ignore OData shortcut variant: `… bookshop.Orders:items[2]`
|
|
2849
|
+
if (!val || val.length === 1 && val[0].val) continue
|
|
2850
|
+
if (!SELECT.where) SELECT.where = val
|
|
2851
|
+
// infix filter comes first in resulting where
|
|
2852
|
+
else SELECT.where = [...(hasLogicalOr(val) ? [asXpr(val)] : val), 'and', ...(hasLogicalOr(SELECT.where) ? [asXpr(SELECT.where)] : SELECT.where)]
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
/**
|
|
2858
|
+
* Assigns the given `element` as non-enumerable property 'element' onto `col`.
|
|
2859
|
+
*
|
|
2860
|
+
* @param {object} col
|
|
2861
|
+
* @param {csn.Element} element
|
|
2862
|
+
*/
|
|
2863
|
+
function setElementOnColumns(col, element) {
|
|
2864
|
+
defineProperty(col, 'element', element)
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
function getPrimaryKey(entity, tableAlias = null) {
|
|
2868
|
+
const primaryKey = []
|
|
2869
|
+
for (const k of Object.keys(entity.elements)) {
|
|
2870
|
+
const e = entity.elements[k]
|
|
2871
|
+
if (e.key === true && !e.virtual && e.isAssociation !== true) {
|
|
2872
|
+
primaryKey.push({ ref: tableAlias ? [tableAlias, e.name] : [e.name] })
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
return primaryKey
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
const getName = col => col.as || col.ref?.at(-1)
|
|
2879
|
+
const idOnly = ref => ref.id || ref
|
|
2880
|
+
const refWithConditions = step => {
|
|
2881
|
+
let appendix
|
|
2882
|
+
const { args, where } = step
|
|
2883
|
+
if (where && args) appendix = JSON.stringify(where) + JSON.stringify(args)
|
|
2884
|
+
else if (where) appendix = JSON.stringify(where)
|
|
2885
|
+
else if (args) appendix = JSON.stringify(args)
|
|
2886
|
+
return appendix ? step.id + appendix : step
|
|
2887
|
+
}
|
|
2888
|
+
const is_regexp = x => x?.constructor?.name === 'RegExp' // NOTE: x instanceof RegExp doesn't work in repl
|
|
2889
|
+
|
|
2890
|
+
module.exports = Object.assign(cqn4sql, {
|
|
2891
|
+
// for own tests only:
|
|
2892
|
+
eqOps,
|
|
2893
|
+
notEqOps,
|
|
2894
|
+
notSupportedOps,
|
|
2895
|
+
})
|