@cap-js/db-service 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/lib/SQLService.js +14 -3
- package/lib/cqn2sql.js +4 -3
- package/lib/cqn4sql.js +192 -145
- package/lib/fill-in-keys.js +1 -1
- package/lib/infer/index.js +12 -5
- package/lib/infer/join-tree.js +13 -0
- package/lib/utils.js +7 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,40 @@
|
|
|
4
4
|
- The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
5
5
|
- This project adheres to [Semantic Versioning](http://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [2.4.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.3.0...db-service-v2.4.0) (2025-08-27)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* **`tuple expansion`:** allow structs with exactly one element/fk in comparison ([#1291](https://github.com/cap-js/cds-dbs/issues/1291)) ([75ea826](https://github.com/cap-js/cds-dbs/commit/75ea82694faeafcaf78df9d4b0bbce37b4f65b63))
|
|
13
|
+
* cds.db.foreach uses real object mode streaming ([#1318](https://github.com/cap-js/cds-dbs/issues/1318)) ([cd28b53](https://github.com/cap-js/cds-dbs/commit/cd28b53966dbe28ad1d5ef3827767e78742e0fbd))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
* **`assoc2join`:** target side access detection ([#1282](https://github.com/cap-js/cds-dbs/issues/1282)) ([6f9befa](https://github.com/cap-js/cds-dbs/commit/6f9befa24a06bcc629fe853aa66290613734c3ef))
|
|
19
|
+
* **`cqn4sql`:** only consider `own` property `[@cds](https://github.com/cds).persistence.skip` ([#1324](https://github.com/cap-js/cds-dbs/issues/1324)) ([bd1f52f](https://github.com/cap-js/cds-dbs/commit/bd1f52f67fb4709dce3a27fea8856cb9b875da6b))
|
|
20
|
+
* **`exists`:** do not loose custom where ([#1322](https://github.com/cap-js/cds-dbs/issues/1322)) ([644918c](https://github.com/cap-js/cds-dbs/commit/644918c56d9d939f43f4a0346f42e16722bd6fe9))
|
|
21
|
+
* arithmetic operators can only be used with scalar operands ([#1307](https://github.com/cap-js/cds-dbs/issues/1307)) ([d58d335](https://github.com/cap-js/cds-dbs/commit/d58d33539e22f818d18240bb86ba596fc6fe21d1))
|
|
22
|
+
* detect path expression inside nested xpr after `exists` ([#1292](https://github.com/cap-js/cds-dbs/issues/1292)) ([852d915](https://github.com/cap-js/cds-dbs/commit/852d9155d5bb09a56a6c152259c8282662ceb29d)), closes [#1225](https://github.com/cap-js/cds-dbs/issues/1225)
|
|
23
|
+
* reject comparison of two empty structures ([#1306](https://github.com/cap-js/cds-dbs/issues/1306)) ([d97304d](https://github.com/cap-js/cds-dbs/commit/d97304d95c7f629afe75aba57192277b7124eb3e))
|
|
24
|
+
|
|
25
|
+
## [2.3.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.2.0...db-service-v2.3.0) (2025-07-28)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
* **`exists`:** support additional query modifiers ([#1261](https://github.com/cap-js/cds-dbs/issues/1261)) ([1394b46](https://github.com/cap-js/cds-dbs/commit/1394b46b10f628748dd1f945095ab4ce7f3963f6))
|
|
31
|
+
* **hierarchy:** LimitedRank ([#1268](https://github.com/cap-js/cds-dbs/issues/1268)) ([52e16db](https://github.com/cap-js/cds-dbs/commit/52e16db2c83d06e318ea05947a3c3c3153bd3ab2))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
* `count` subquery for queries with only `expand` columns ([#1264](https://github.com/cap-js/cds-dbs/issues/1264)) ([097a332](https://github.com/cap-js/cds-dbs/commit/097a332f78156526823b2088bdc35278aea09854))
|
|
37
|
+
* **cqn4sql:** multiple path expressions in where clause ([#1272](https://github.com/cap-js/cds-dbs/issues/1272)) ([9b35366](https://github.com/cap-js/cds-dbs/commit/9b353660f4c2568176f57baa642ab2b052cfcff9))
|
|
38
|
+
* **hierarchy:** only modify where if existent ([#1265](https://github.com/cap-js/cds-dbs/issues/1265)) ([eaca855](https://github.com/cap-js/cds-dbs/commit/eaca855ec06087e22bf780fac5e4010ef1b5ff4f))
|
|
39
|
+
* TypeError for empty `list` ([#1269](https://github.com/cap-js/cds-dbs/issues/1269)) ([f262718](https://github.com/cap-js/cds-dbs/commit/f26271813e161c9f7c05fbc82b10c4d5f14916a7))
|
|
40
|
+
|
|
7
41
|
## [2.2.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.1.2...db-service-v2.2.0) (2025-06-30)
|
|
8
42
|
|
|
9
43
|
|
package/lib/SQLService.js
CHANGED
|
@@ -337,9 +337,11 @@ class SQLService extends DatabaseService {
|
|
|
337
337
|
|
|
338
338
|
// Keep original query columns when potentially used insde conditions
|
|
339
339
|
const { having, groupBy } = query.SELECT
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
340
|
+
let columns = []
|
|
341
|
+
if((having?.length || groupBy?.length)) {
|
|
342
|
+
columns = query.SELECT.columns.filter(c => !c.expand)
|
|
343
|
+
}
|
|
344
|
+
if (columns.length === 0) columns.push({ val: 1 })
|
|
343
345
|
const cq = SELECT.one([{ func: 'count' }]).from(
|
|
344
346
|
cds.ql.clone(query, {
|
|
345
347
|
columns,
|
|
@@ -353,6 +355,15 @@ class SQLService extends DatabaseService {
|
|
|
353
355
|
return count
|
|
354
356
|
}
|
|
355
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Streaming API variant of .run().
|
|
360
|
+
* @param {import('@sap/cds/apis/cqn').SELECT} query - SELECT CQN
|
|
361
|
+
* @param {function} callback - Function to be invoked for each row
|
|
362
|
+
*/
|
|
363
|
+
foreach (query, callback) {
|
|
364
|
+
return query.foreach(callback)
|
|
365
|
+
}
|
|
366
|
+
|
|
356
367
|
/**
|
|
357
368
|
* Helper class for results of INSERTs.
|
|
358
369
|
* Subclasses may override this.
|
package/lib/cqn2sql.js
CHANGED
|
@@ -282,10 +282,10 @@ class CQN2SQLRenderer {
|
|
|
282
282
|
SELECT_recurse(q) {
|
|
283
283
|
let { from, columns, where, orderBy, recurse, _internal } = q.SELECT
|
|
284
284
|
|
|
285
|
-
const keys = []
|
|
286
285
|
const _target = q._target
|
|
287
286
|
|
|
288
|
-
if (_target) {
|
|
287
|
+
if (_target && where) {
|
|
288
|
+
const keys = []
|
|
289
289
|
for (const _key in _target.keys) {
|
|
290
290
|
const k = _target.keys[_key]
|
|
291
291
|
if (!k.virtual && !k.isAssociation && !k.value) {
|
|
@@ -324,6 +324,7 @@ class CQN2SQLRenderer {
|
|
|
324
324
|
DistanceFromRoot: { xpr: [{ ref: ['HIERARCHY_LEVEL'] }, '-', { val: 1, param: false }], as: 'DistanceFromRoot' },
|
|
325
325
|
DrillState: false,
|
|
326
326
|
LimitedDescendantCount: { xpr: [{ ref: ['HIERARCHY_TREE_SIZE'] }, '-', { val: 1, param: false }], as: 'LimitedDescendantCount' },
|
|
327
|
+
LimitedRank: { xpr: [{ func: 'row_number', args: [] }, 'OVER', { xpr: [] }, '-', { val: 1, param: false }], as: 'LimitedRank' }
|
|
327
328
|
}
|
|
328
329
|
|
|
329
330
|
const columnsFiltered = columns
|
|
@@ -1176,7 +1177,7 @@ class CQN2SQLRenderer {
|
|
|
1176
1177
|
.map((x, i) => {
|
|
1177
1178
|
if (x in { LIKE: 1, like: 1 } && is_regexp(xpr[i + 1]?.val)) return this.operator('regexp')
|
|
1178
1179
|
if (typeof x === 'string') return this.operator(x, i, xpr)
|
|
1179
|
-
if (x.xpr) return `(${this.xpr(x)})`
|
|
1180
|
+
if (x.xpr && !x.func) return `(${this.xpr(x)})`
|
|
1180
1181
|
else return this.expr(x)
|
|
1181
1182
|
})
|
|
1182
1183
|
.join(' ')
|
package/lib/cqn4sql.js
CHANGED
|
@@ -5,7 +5,15 @@ cds.infer.target ??= q => q._target || q.target // instanceof cds.entity ? q._ta
|
|
|
5
5
|
|
|
6
6
|
const infer = require('./infer')
|
|
7
7
|
const { computeColumnsToBeSearched } = require('./search')
|
|
8
|
-
const {
|
|
8
|
+
const {
|
|
9
|
+
prettyPrintRef,
|
|
10
|
+
isCalculatedOnRead,
|
|
11
|
+
isCalculatedElement,
|
|
12
|
+
getImplicitAlias,
|
|
13
|
+
defineProperty,
|
|
14
|
+
getModelUtils,
|
|
15
|
+
hasOwnSkip,
|
|
16
|
+
} = require('./utils')
|
|
9
17
|
|
|
10
18
|
/**
|
|
11
19
|
* For operators of <eqOps>, this is replaced by comparing all leaf elements with null, combined with and.
|
|
@@ -13,14 +21,14 @@ const { prettyPrintRef, isCalculatedOnRead, isCalculatedElement, getImplicitAlia
|
|
|
13
21
|
*/
|
|
14
22
|
const eqOps = [['is'], ['='] /* ['=='] */]
|
|
15
23
|
/**
|
|
16
|
-
* For operators of <notEqOps>, do the same but use or instead of and
|
|
17
|
-
* This ensures that not struct == <value
|
|
24
|
+
* For operators of <notEqOps>, do the same but use `or` instead of `and`.
|
|
25
|
+
* This ensures that `not struct == <value>` is the same as `struct != <value>`.
|
|
18
26
|
*/
|
|
19
27
|
const notEqOps = [['is', 'not'], ['<>'], ['!=']]
|
|
20
28
|
/**
|
|
21
29
|
* not supported in comparison w/ struct because of unclear semantics
|
|
22
30
|
*/
|
|
23
|
-
const notSupportedOps = [['>'], ['<'], ['>='], ['<=']]
|
|
31
|
+
const notSupportedOps = [['>'], ['<'], ['>='], ['<='], ['*'], ['+'], ['-'], ['/']]
|
|
24
32
|
|
|
25
33
|
const allOps = eqOps.concat(eqOps).concat(notEqOps).concat(notSupportedOps)
|
|
26
34
|
|
|
@@ -63,19 +71,8 @@ function cqn4sql(originalQuery, model) {
|
|
|
63
71
|
}
|
|
64
72
|
// query modifiers can also be defined in from ref leaf infix filter
|
|
65
73
|
// > SELECT from bookshop.Books[order by price] {ID}
|
|
66
|
-
if (inferred.SELECT?.from.ref) {
|
|
67
|
-
|
|
68
|
-
if (key in { orderBy: 1, groupBy: 1 }) {
|
|
69
|
-
if (inferred.SELECT[key]) inferred.SELECT[key].push(...val)
|
|
70
|
-
else inferred.SELECT[key] = val
|
|
71
|
-
} else if (key === 'limit') {
|
|
72
|
-
// limit defined on the query has precedence
|
|
73
|
-
if (!inferred.SELECT.limit) inferred.SELECT.limit = val
|
|
74
|
-
} else if (key === 'having') {
|
|
75
|
-
if (!inferred.SELECT.having) inferred.SELECT.having = val
|
|
76
|
-
else inferred.SELECT.having.push('and', ...val)
|
|
77
|
-
}
|
|
78
|
-
}
|
|
74
|
+
if (inferred.SELECT?.from.ref?.at(-1).id) {
|
|
75
|
+
assignQueryModifiers(inferred.SELECT, inferred.SELECT.from.ref.at(-1))
|
|
79
76
|
}
|
|
80
77
|
inferred = infer(inferred, model)
|
|
81
78
|
const { getLocalizedName, isLocalized, getDefinition } = getModelUtils(model, originalQuery) // TODO: pass model to getModelUtils
|
|
@@ -95,6 +92,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
95
92
|
const from = queryProp.from
|
|
96
93
|
|
|
97
94
|
const transformedProp = { __proto__: queryProp } // IMPORTANT: don't lose anything you might not know of
|
|
95
|
+
const queryNeedsJoins = inferred.joinTree && !inferred.joinTree.isInitial
|
|
98
96
|
|
|
99
97
|
// Transform the existing where, prepend table aliases, and so on...
|
|
100
98
|
if (where) {
|
|
@@ -104,7 +102,47 @@ function cqn4sql(originalQuery, model) {
|
|
|
104
102
|
// Transform the from clause: association path steps turn into `WHERE EXISTS` subqueries.
|
|
105
103
|
// The already transformed `where` clause is then glued together with the resulting subqueries.
|
|
106
104
|
const { transformedWhere, transformedFrom } = getTransformedFrom(from || entity, transformedProp.where)
|
|
107
|
-
|
|
105
|
+
|
|
106
|
+
// build a subquery for DELETE / UPDATE queries with path expressions and match the primary keys
|
|
107
|
+
if (queryNeedsJoins && (inferred.UPDATE || inferred.DELETE)) {
|
|
108
|
+
const prop = inferred.UPDATE ? 'UPDATE' : 'DELETE'
|
|
109
|
+
const subquery = {
|
|
110
|
+
SELECT: {
|
|
111
|
+
from: { ...(from || entity) },
|
|
112
|
+
columns: [], // primary keys of the query target will be added later
|
|
113
|
+
where: [...where],
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
// The alias of the original query is now the alias for the subquery
|
|
117
|
+
// so that potential references in the where clause to the alias match.
|
|
118
|
+
// Hence, replace the alias of the original query with the next
|
|
119
|
+
// available alias, so that each alias is unique.
|
|
120
|
+
const uniqueSubqueryAlias = getNextAvailableTableAlias(transformedFrom.as)
|
|
121
|
+
transformedFrom.as = uniqueSubqueryAlias
|
|
122
|
+
|
|
123
|
+
// calculate the primary keys of the target entity, there is always exactly
|
|
124
|
+
// one query source for UPDATE / DELETE
|
|
125
|
+
const queryTarget = Object.values(inferred.sources)[0].definition
|
|
126
|
+
const primaryKey = { list: [] }
|
|
127
|
+
for (const k of Object.keys(queryTarget.elements)) {
|
|
128
|
+
const e = queryTarget.elements[k]
|
|
129
|
+
if (e.key === true && !e.virtual && e.isAssociation !== true) {
|
|
130
|
+
subquery.SELECT.columns.push({ ref: [e.name] })
|
|
131
|
+
primaryKey.list.push({ ref: [transformedFrom.as, e.name] })
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const transformedSubquery = cqn4sql(subquery, model)
|
|
136
|
+
|
|
137
|
+
// replace where condition of original query with the transformed subquery
|
|
138
|
+
// correlate UPDATE / DELETE query with subquery by primary key matches
|
|
139
|
+
transformedQuery[prop].where = [primaryKey, 'in', transformedSubquery]
|
|
140
|
+
|
|
141
|
+
if (prop === 'UPDATE') transformedQuery.UPDATE.entity = transformedFrom
|
|
142
|
+
else transformedQuery.DELETE.from = transformedFrom
|
|
143
|
+
|
|
144
|
+
return transformedQuery
|
|
145
|
+
}
|
|
108
146
|
|
|
109
147
|
if (inferred.SELECT) {
|
|
110
148
|
transformedQuery = transformSelectQuery(queryProp, transformedFrom, transformedWhere, transformedQuery)
|
|
@@ -130,45 +168,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
130
168
|
}
|
|
131
169
|
|
|
132
170
|
if (queryNeedsJoins) {
|
|
133
|
-
|
|
134
|
-
const prop = inferred.UPDATE ? 'UPDATE' : 'DELETE'
|
|
135
|
-
const subquery = {
|
|
136
|
-
SELECT: {
|
|
137
|
-
from: { ...transformedFrom },
|
|
138
|
-
columns: [], // primary keys of the query target will be added later
|
|
139
|
-
where: [...transformedProp.where],
|
|
140
|
-
},
|
|
141
|
-
}
|
|
142
|
-
// The alias of the original query is now the alias for the subquery
|
|
143
|
-
// so that potential references in the where clause to the alias match.
|
|
144
|
-
// Hence, replace the alias of the original query with the next
|
|
145
|
-
// available alias, so that each alias is unique.
|
|
146
|
-
const uniqueSubqueryAlias = getNextAvailableTableAlias(transformedFrom.as)
|
|
147
|
-
transformedFrom.as = uniqueSubqueryAlias
|
|
148
|
-
|
|
149
|
-
// calculate the primary keys of the target entity, there is always exactly
|
|
150
|
-
// one query source for UPDATE / DELETE
|
|
151
|
-
const queryTarget = Object.values(inferred.sources)[0].definition
|
|
152
|
-
const primaryKey = { list: [] }
|
|
153
|
-
for (const k of Object.keys(queryTarget.elements)) {
|
|
154
|
-
const e = queryTarget.elements[k]
|
|
155
|
-
if (e.key === true && !e.virtual && e.isAssociation !== true) {
|
|
156
|
-
subquery.SELECT.columns.push({ ref: [e.name] })
|
|
157
|
-
primaryKey.list.push({ ref: [transformedFrom.as, e.name] })
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const transformedSubquery = cqn4sql(subquery, model)
|
|
162
|
-
|
|
163
|
-
// replace where condition of original query with the transformed subquery
|
|
164
|
-
// correlate UPDATE / DELETE query with subquery by primary key matches
|
|
165
|
-
transformedQuery[prop].where = [primaryKey, 'in', transformedSubquery]
|
|
166
|
-
|
|
167
|
-
if (prop === 'UPDATE') transformedQuery.UPDATE.entity = transformedFrom
|
|
168
|
-
else transformedQuery.DELETE.from = transformedFrom
|
|
169
|
-
} else {
|
|
170
|
-
transformedQuery[kind].from = translateAssocsToJoins(transformedQuery[kind].from)
|
|
171
|
-
}
|
|
171
|
+
transformedQuery[kind].from = translateAssocsToJoins(transformedQuery[kind].from)
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
@@ -468,7 +468,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
468
468
|
const refNavigation = col.ref.slice(col.$refLinks[0].definition.kind !== 'element' ? 1 : 0).join('_')
|
|
469
469
|
if (!columnAlias && col.flatName && col.flatName !== refNavigation) columnAlias = refNavigation
|
|
470
470
|
|
|
471
|
-
if (col.$refLinks.some(link => getDefinition(link.definition.target)
|
|
471
|
+
if (col.$refLinks.some(link => hasOwnSkip(getDefinition(link.definition.target)))) return
|
|
472
472
|
|
|
473
473
|
const flatColumns = getFlatColumnsFor(col, { baseName, columnAlias, tableAlias })
|
|
474
474
|
flatColumns.forEach(flatColumn => {
|
|
@@ -967,7 +967,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
967
967
|
} else if (pseudos.elements[col.ref?.[0]]) {
|
|
968
968
|
res.push({ ...col })
|
|
969
969
|
} else if (col.ref) {
|
|
970
|
-
if (col.$refLinks.some(link => getDefinition(link.definition.target)
|
|
970
|
+
if (col.$refLinks.some(link => hasOwnSkip(getDefinition(link.definition.target))))
|
|
971
971
|
continue
|
|
972
972
|
if (col.ref.length > 1 && col.ref[0] === '$self' && !col.$refLinks[0].definition.kind) {
|
|
973
973
|
const dollarSelfReplacement = calculateDollarSelfColumn(col)
|
|
@@ -1190,7 +1190,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1190
1190
|
let { baseName, columnAlias = column.as, tableAlias } = names
|
|
1191
1191
|
const { exclude, replace } = excludeAndReplace || {}
|
|
1192
1192
|
const { $refLinks, flatName, isJoinRelevant } = column
|
|
1193
|
-
let
|
|
1193
|
+
let firstNonJoinRelevantAssoc, stepAfterAssoc
|
|
1194
1194
|
let element = $refLinks ? $refLinks[$refLinks.length - 1].definition : column
|
|
1195
1195
|
if (isWildcard && element.type === 'cds.LargeBinary') return []
|
|
1196
1196
|
if (element.on && !element.keys)
|
|
@@ -1198,14 +1198,23 @@ function cqn4sql(originalQuery, model) {
|
|
|
1198
1198
|
else if (element.virtual === true) return []
|
|
1199
1199
|
else if (!isJoinRelevant && flatName) baseName = flatName
|
|
1200
1200
|
else if (isJoinRelevant) {
|
|
1201
|
-
const
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1201
|
+
const leafAssocIndex = column.$refLinks.findIndex(link => link.definition.isAssociation && link.onlyForeignKeyAccess)
|
|
1202
|
+
firstNonJoinRelevantAssoc = column.$refLinks[leafAssocIndex] || [...column.$refLinks].reverse().find(link => link.definition.isAssociation)
|
|
1203
|
+
stepAfterAssoc = column.$refLinks.at(leafAssocIndex + 1) || column.$refLinks.at(-1)
|
|
1204
|
+
let elements = firstNonJoinRelevantAssoc.definition.elements || firstNonJoinRelevantAssoc.definition.foreignKeys
|
|
1205
|
+
if (elements && stepAfterAssoc.definition.name in elements) {
|
|
1206
|
+
element = firstNonJoinRelevantAssoc.definition
|
|
1207
|
+
baseName = getFullName(firstNonJoinRelevantAssoc.definition)
|
|
1207
1208
|
columnAlias = column.as || column.ref.slice(0, -1).map(idOnly).join('_')
|
|
1208
1209
|
} else baseName = getFullName(column.$refLinks[column.$refLinks.length - 1].definition)
|
|
1210
|
+
|
|
1211
|
+
if(column.element && !isAssocOrStruct(column.element)) {
|
|
1212
|
+
columnAlias = column.as || leafAssocIndex === -1 ? columnAlias : column.ref.slice(leafAssocIndex - 1).map(idOnly).join('_')
|
|
1213
|
+
const res = { ref: [tableAlias, calculateElementName(column)], as: columnAlias }
|
|
1214
|
+
setElementOnColumns(res, element)
|
|
1215
|
+
return [res]
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1209
1218
|
} else if (!baseName && structsAreUnfoldedAlready) {
|
|
1210
1219
|
baseName = element.name // name is already fully constructed
|
|
1211
1220
|
} else {
|
|
@@ -1246,11 +1255,11 @@ function cqn4sql(originalQuery, model) {
|
|
|
1246
1255
|
const flattenThisForeignKey =
|
|
1247
1256
|
!$refLinks || // the association is passed as element, not as ref --> flatten full foreign key
|
|
1248
1257
|
element === $refLinks.at(-1).definition || // the association is the leaf of the ref --> flatten full foreign key
|
|
1249
|
-
keyElement ===
|
|
1258
|
+
keyElement === stepAfterAssoc.definition // the foreign key is the leaf of the ref --> only flatten this specific foreign key
|
|
1250
1259
|
if (flattenThisForeignKey) {
|
|
1251
1260
|
const fkElement = getElementForRef(k.ref, getDefinition(element.target))
|
|
1252
1261
|
let fkBaseName
|
|
1253
|
-
if (!
|
|
1262
|
+
if (!firstNonJoinRelevantAssoc || firstNonJoinRelevantAssoc.onlyForeignKeyAccess) fkBaseName = `${baseName}_${k.as || k.ref.at(-1)}`
|
|
1254
1263
|
// e.g. if foreign key is accessed via infix filter - use join alias to access key in target
|
|
1255
1264
|
else fkBaseName = k.ref.at(-1)
|
|
1256
1265
|
const fkPath = [...csnPath, k.ref.at(-1)]
|
|
@@ -1420,7 +1429,8 @@ function cqn4sql(originalQuery, model) {
|
|
|
1420
1429
|
}
|
|
1421
1430
|
const { definition: fkSource } = next
|
|
1422
1431
|
ensureValidForeignKeys(fkSource, ref)
|
|
1423
|
-
|
|
1432
|
+
const { where, ...args } = step
|
|
1433
|
+
whereExistsSubSelects.push(getWhereExistsSubquery(current, next, where, true, args))
|
|
1424
1434
|
}
|
|
1425
1435
|
|
|
1426
1436
|
const whereExists = { SELECT: whereExistsSubqueries(whereExistsSubSelects) }
|
|
@@ -1462,6 +1472,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1462
1472
|
flatKeys.push(...getFlatColumnsFor(v, { tableAlias: $baseLink.alias }))
|
|
1463
1473
|
}
|
|
1464
1474
|
}
|
|
1475
|
+
// TODO: improve error message, the current message is generally not true (only for OData shortcut notation)
|
|
1465
1476
|
if (flatKeys.length > 1)
|
|
1466
1477
|
throw new Error('Filters can only be applied to managed associations which result in a single foreign key')
|
|
1467
1478
|
flatKeys.forEach(c => keyValComparisons.push([...[c, '=', token]]))
|
|
@@ -1474,35 +1485,38 @@ function cqn4sql(originalQuery, model) {
|
|
|
1474
1485
|
transformedTokenStream.push({ ...token })
|
|
1475
1486
|
} else {
|
|
1476
1487
|
// expand `struct = null | struct2`
|
|
1477
|
-
const definition = token.$refLinks?.at(-1).definition
|
|
1478
1488
|
const next = tokenStream[i + 1]
|
|
1479
|
-
|
|
1489
|
+
let indexRhs = i + 2
|
|
1490
|
+
let rhs = tokenStream[indexRhs] // either another operator (i.e. `not like` et. al.) or the operand, i.e. the val | null
|
|
1491
|
+
const lhsDef = token.$refLinks?.at(-1).definition
|
|
1492
|
+
let rhsDef = rhs?.$refLinks?.at(-1)?.definition
|
|
1493
|
+
if (
|
|
1494
|
+
allOps.some(([firstOp]) => firstOp === next) &&
|
|
1495
|
+
(lhsDef?.elements || lhsDef?.keys || rhsDef?.elements || rhsDef?.keys)
|
|
1496
|
+
) {
|
|
1480
1497
|
const ops = [next]
|
|
1481
|
-
let indexRhs = i + 2
|
|
1482
|
-
let rhs = tokenStream[i + 2] // either another operator (i.e. `not like` et. al.) or the operand, i.e. the val | null
|
|
1483
1498
|
if (allOps.some(([, secondOp]) => secondOp === rhs)) {
|
|
1484
1499
|
ops.push(rhs)
|
|
1485
1500
|
rhs = tokenStream[i + 3]
|
|
1486
1501
|
indexRhs += 1
|
|
1502
|
+
rhsDef = rhs?.$refLinks?.at(-1)?.definition
|
|
1487
1503
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
)
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
i = indexRhs // jump to next relevant index
|
|
1500
|
-
}
|
|
1504
|
+
|
|
1505
|
+
if (notSupportedOps.some(([firstOp]) => firstOp === next))
|
|
1506
|
+
throw new Error(`The operator "${next}" can only be used with scalar operands`)
|
|
1507
|
+
|
|
1508
|
+
const newTokens = expandComparison(token, ops, rhs, $baseLink)
|
|
1509
|
+
if(newTokens.length === 0)
|
|
1510
|
+
throw new Error(`Can't compare two empty structures`)
|
|
1511
|
+
|
|
1512
|
+
const needXpr = Boolean(tokenStream[i - 1] || tokenStream[indexRhs + 1])
|
|
1513
|
+
transformedTokenStream.push(...(needXpr ? [asXpr(newTokens)] : newTokens))
|
|
1514
|
+
i = indexRhs // jump to next relevant index
|
|
1501
1515
|
} else {
|
|
1502
1516
|
// reject associations in expression, except if we are in an infix filter -> $baseLink is set
|
|
1503
1517
|
assertNoStructInXpr(token, $baseLink)
|
|
1504
1518
|
// reject virtual elements in expressions as they will lead to a sql error down the line
|
|
1505
|
-
if (
|
|
1519
|
+
if (lhsDef?.virtual) throw new Error(`Virtual elements are not allowed in expressions`)
|
|
1506
1520
|
|
|
1507
1521
|
let result = is_regexp(token?.val) ? token : copy(token) // REVISIT: too expensive! //
|
|
1508
1522
|
if (token.ref) {
|
|
@@ -1528,7 +1542,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1528
1542
|
token.isJoinRelevant && [...token.$refLinks].reverse().find(l => l.definition.isAssociation)
|
|
1529
1543
|
const tableAlias = getTableAlias(token, (!lastAssoc?.onlyForeignKeyAccess && lastAssoc) || $baseLink)
|
|
1530
1544
|
if ((!$baseLink || lastAssoc) && token.isJoinRelevant) {
|
|
1531
|
-
let name = calculateElementName(token
|
|
1545
|
+
let name = calculateElementName(token)
|
|
1532
1546
|
result.ref = [tableAlias, name]
|
|
1533
1547
|
} else if (tableAlias) {
|
|
1534
1548
|
result.ref = [tableAlias, token.flatName]
|
|
@@ -1557,9 +1571,9 @@ function cqn4sql(originalQuery, model) {
|
|
|
1557
1571
|
/**
|
|
1558
1572
|
* Expand the given definition and compare all leafs to `val`.
|
|
1559
1573
|
*
|
|
1560
|
-
* @param {object}
|
|
1574
|
+
* @param {object} lhs with $refLinks
|
|
1561
1575
|
* @param {string} operator one of allOps
|
|
1562
|
-
* @param {object}
|
|
1576
|
+
* @param {object} rhs either `null` or a column (with `ref` and `$refLinks`)
|
|
1563
1577
|
* @param {object} $baseLink optional base `$refLink`, e.g. for infix filters of scoped queries.
|
|
1564
1578
|
* In the following example, we must pass `bookshop:Reproduce` as $baseLink for `author`:
|
|
1565
1579
|
*
|
|
@@ -1567,26 +1581,21 @@ function cqn4sql(originalQuery, model) {
|
|
|
1567
1581
|
* ^^^^^^
|
|
1568
1582
|
* @returns {array}
|
|
1569
1583
|
*/
|
|
1570
|
-
function expandComparison(
|
|
1571
|
-
const { definition } =
|
|
1572
|
-
|
|
1584
|
+
function expandComparison(lhs, operator, rhs, $baseLink = null) {
|
|
1585
|
+
const { definition: lhsDef, val: lhsVal } = lhs.val ? lhs : lhs.$refLinks.at(-1)
|
|
1586
|
+
const { definition: rhsDef, val: rhsVal } = rhs.val ? rhs : rhs.$refLinks?.at(-1) || {}
|
|
1573
1587
|
const result = []
|
|
1574
|
-
if (
|
|
1575
|
-
//
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
value.$refLinks[value.$refLinks.length - 1].definition.name
|
|
1586
|
-
}": the operands must have the same structure`,
|
|
1587
|
-
)
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1588
|
+
if (lhsDef && rhsDef) {
|
|
1589
|
+
// both must be structured
|
|
1590
|
+
const lhsIsStructured = isAssocOrStruct(lhsDef)
|
|
1591
|
+
const rhsIsStructured = isAssocOrStruct(rhsDef)
|
|
1592
|
+
if (!lhsIsStructured)
|
|
1593
|
+
throw new Error(`Can't compare structure “${rhs.ref.map(idOnly).join('.')}” with non-structure “${lhs.ref.map(idOnly).join('.')}”`)
|
|
1594
|
+
if (!rhsIsStructured)
|
|
1595
|
+
throw new Error(`Can't compare structure “${lhs.ref.map(idOnly).join('.')}” with non-structure “${rhs.ref.map(idOnly).join('.')}”`)
|
|
1596
|
+
|
|
1597
|
+
const flatLhs = flattenWithBaseName(lhs)
|
|
1598
|
+
const flatRhs = flattenWithBaseName(rhs)
|
|
1590
1599
|
const boolOp = notEqOps.some(([f, s]) => operator[0] === f && operator[1] === s) ? 'or' : 'and'
|
|
1591
1600
|
while (flatLhs.length > 0) {
|
|
1592
1601
|
// retrieve and remove one flat element from LHS and search for it in RHS (remove it there too)
|
|
@@ -1598,27 +1607,47 @@ function cqn4sql(originalQuery, model) {
|
|
|
1598
1607
|
})
|
|
1599
1608
|
// not found in rhs --> exit
|
|
1600
1609
|
if (indexOfElementOnRhs === -1) {
|
|
1601
|
-
const lhsPath =
|
|
1602
|
-
const rhsPath =
|
|
1610
|
+
const lhsPath = lhs.ref.map(idOnly).join('.')
|
|
1611
|
+
const rhsPath = rhs.ref.map(idOnly).join('.')
|
|
1603
1612
|
throw new Error(`Can't compare "${lhsPath}" with "${rhsPath}": the operands must have the same structure`)
|
|
1604
1613
|
}
|
|
1605
|
-
const
|
|
1606
|
-
result.push({ ref }, ...operator,
|
|
1614
|
+
const cleansedRhs = flatRhs.splice(indexOfElementOnRhs, 1)[0] // remove the element also from RHS
|
|
1615
|
+
result.push({ ref }, ...operator, cleansedRhs)
|
|
1607
1616
|
if (flatLhs.length > 0) result.push(boolOp)
|
|
1608
1617
|
}
|
|
1609
|
-
} else {
|
|
1618
|
+
} else if (lhsDef && (rhsVal || rhs === 'null' || rhs.val === null)) {
|
|
1610
1619
|
// compare with value
|
|
1611
|
-
const flatLhs = flattenWithBaseName(
|
|
1612
|
-
if (flatLhs.length
|
|
1613
|
-
|
|
1620
|
+
const flatLhs = flattenWithBaseName(lhs)
|
|
1621
|
+
if (flatLhs.length !== 1 && rhsVal && rhs !== 'null')
|
|
1622
|
+
canOnlyCompareToExactlyOneLeaf(lhsDef, lhs.ref, rhsVal)
|
|
1623
|
+
|
|
1614
1624
|
const boolOp = notEqOps.some(([f, s]) => operator[0] === f && operator[1] === s) ? 'or' : 'and'
|
|
1615
1625
|
flatLhs.forEach((column, i) => {
|
|
1616
|
-
result.push(column, ...operator,
|
|
1626
|
+
result.push(column, ...operator, rhs)
|
|
1617
1627
|
if (flatLhs[i + 1]) result.push(boolOp)
|
|
1618
1628
|
})
|
|
1629
|
+
} else if (lhsVal && rhsDef) {
|
|
1630
|
+
const flatRhs = flattenWithBaseName(rhs)
|
|
1631
|
+
// comparing a struct to a value is ok if structure has exactly one leaf
|
|
1632
|
+
if (flatRhs.length !== 1 && lhsVal)
|
|
1633
|
+
canOnlyCompareToExactlyOneLeaf(rhsDef, rhs.ref, lhsVal)
|
|
1634
|
+
|
|
1635
|
+
const boolOp = notEqOps.some(([f, s]) => operator[0] === f && operator[1] === s) ? 'or' : 'and'
|
|
1636
|
+
flatRhs.forEach((column, i) => {
|
|
1637
|
+
result.push(lhs, ...operator, column)
|
|
1638
|
+
if (flatRhs[i + 1]) result.push(boolOp)
|
|
1639
|
+
})
|
|
1619
1640
|
}
|
|
1620
1641
|
return result
|
|
1621
1642
|
|
|
1643
|
+
function canOnlyCompareToExactlyOneLeaf(struct, structRef, val) {
|
|
1644
|
+
const what = struct.isAssociation ? 'association' : 'structure'
|
|
1645
|
+
const postfix = struct.isAssociation ? 'associations with one foreign key' : 'structures with one sub-element'
|
|
1646
|
+
throw new Error(
|
|
1647
|
+
`Can't compare ${what} "${structRef.map(idOnly).join('.')}" to value "${val}"; only possible for ${postfix}`
|
|
1648
|
+
)
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1622
1651
|
function flattenWithBaseName(def) {
|
|
1623
1652
|
if (!def.$refLinks) return def
|
|
1624
1653
|
const leaf = def.$refLinks[def.$refLinks.length - 1]
|
|
@@ -1668,8 +1697,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1668
1697
|
function getTransformedFrom(from, existingWhere = []) {
|
|
1669
1698
|
const transformedWhere = []
|
|
1670
1699
|
let transformedFrom = copy(from) // REVISIT: too expensive!
|
|
1671
|
-
if (from.$refLinks)
|
|
1672
|
-
defineProperty(transformedFrom, '$refLinks', [...from.$refLinks])
|
|
1700
|
+
if (from.$refLinks) defineProperty(transformedFrom, '$refLinks', [...from.$refLinks])
|
|
1673
1701
|
if (from.args) {
|
|
1674
1702
|
transformedFrom.args = []
|
|
1675
1703
|
from.args.forEach(arg => {
|
|
@@ -1716,7 +1744,7 @@ function cqn4sql(originalQuery, model) {
|
|
|
1716
1744
|
const nextStep = refReverse[i + 1] // only because we want the filter condition
|
|
1717
1745
|
|
|
1718
1746
|
if (current.definition.target && next) {
|
|
1719
|
-
const { where, args } = nextStep
|
|
1747
|
+
const { where, ...args } = nextStep
|
|
1720
1748
|
if (isStructured(next.definition)) {
|
|
1721
1749
|
// find next association / entity in the ref because this is actually our real nextStep
|
|
1722
1750
|
const nextStepIndex =
|
|
@@ -1777,12 +1805,10 @@ function cqn4sql(originalQuery, model) {
|
|
|
1777
1805
|
// adjust ref & $refLinks after associations have turned into where exists subqueries
|
|
1778
1806
|
transformedFrom.$refLinks.splice(0, transformedFrom.$refLinks.length - 1)
|
|
1779
1807
|
|
|
1780
|
-
let args = from.ref.at(-1).args
|
|
1781
1808
|
const subquerySource =
|
|
1782
1809
|
getDefinition(transformedFrom.$refLinks[0].definition.target) || transformedFrom.$refLinks[0].target
|
|
1783
|
-
if (subquerySource.params && !args) args = {}
|
|
1784
1810
|
const id = getLocalizedName(subquerySource)
|
|
1785
|
-
transformedFrom.ref = [
|
|
1811
|
+
transformedFrom.ref = [subquerySource.params ? { id, args: from.ref.at(-1).args || {} } : id]
|
|
1786
1812
|
|
|
1787
1813
|
return { transformedWhere, transformedFrom }
|
|
1788
1814
|
}
|
|
@@ -1814,10 +1840,6 @@ function cqn4sql(originalQuery, model) {
|
|
|
1814
1840
|
return inferred.joinTree.addNextAvailableTableAlias(id, inferred.outerQueries)
|
|
1815
1841
|
}
|
|
1816
1842
|
|
|
1817
|
-
function asXpr(thing) {
|
|
1818
|
-
return { xpr: thing }
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
1843
|
/**
|
|
1822
1844
|
* @param {CSN.Element} elt
|
|
1823
1845
|
* @returns {boolean}
|
|
@@ -2133,9 +2155,11 @@ function cqn4sql(originalQuery, model) {
|
|
|
2133
2155
|
* @param {object[]} customWhere infix filter which must be part of the where exists subquery on condition
|
|
2134
2156
|
* @param {boolean} inWhere whether or not the path is part of the queries where clause
|
|
2135
2157
|
* -> if it is, target and source side are flipped in the where exists subquery
|
|
2158
|
+
* @param {object} queryModifier optional query modifiers: group by, order by, limit, offset
|
|
2159
|
+
*
|
|
2136
2160
|
* @returns {CQN.SELECT}
|
|
2137
2161
|
*/
|
|
2138
|
-
function getWhereExistsSubquery(current, next, customWhere = null, inWhere = false,
|
|
2162
|
+
function getWhereExistsSubquery(current, next, customWhere = null, inWhere = false, queryModifier = null) {
|
|
2139
2163
|
const { definition } = current
|
|
2140
2164
|
const { definition: nextDefinition } = next
|
|
2141
2165
|
const on = []
|
|
@@ -2156,10 +2180,9 @@ function cqn4sql(originalQuery, model) {
|
|
|
2156
2180
|
|
|
2157
2181
|
const subquerySource = getDefinition(nextDefinition.target) || nextDefinition
|
|
2158
2182
|
const id = getLocalizedName(subquerySource)
|
|
2159
|
-
if (subquerySource.params && !customArgs) customArgs = {}
|
|
2160
2183
|
const SELECT = {
|
|
2161
2184
|
from: {
|
|
2162
|
-
ref: [
|
|
2185
|
+
ref: [subquerySource.params ? { id, args: queryModifier.args || {} } : id],
|
|
2163
2186
|
as: next.alias,
|
|
2164
2187
|
},
|
|
2165
2188
|
columns: [
|
|
@@ -2170,25 +2193,30 @@ function cqn4sql(originalQuery, model) {
|
|
|
2170
2193
|
],
|
|
2171
2194
|
where: on,
|
|
2172
2195
|
}
|
|
2173
|
-
|
|
2174
|
-
|
|
2196
|
+
// this requires sub-sequent transformation of the subquery
|
|
2197
|
+
if (
|
|
2198
|
+
next.pathExpressionInsideFilter ||
|
|
2199
|
+
(queryModifier && ['orderBy', 'groupBy', 'having', 'limit', 'offset'].some(key => key in queryModifier))
|
|
2200
|
+
) {
|
|
2201
|
+
SELECT.where = customWhere || []
|
|
2202
|
+
if (queryModifier) assignQueryModifiers(SELECT, queryModifier)
|
|
2203
|
+
|
|
2175
2204
|
const transformedExists = transformSubquery({ SELECT })
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
? [asXpr(transformedExists.SELECT.where)]
|
|
2183
|
-
: transformedExists.SELECT.where),
|
|
2184
|
-
],
|
|
2185
|
-
)
|
|
2205
|
+
if (transformedExists.SELECT.where?.length) {
|
|
2206
|
+
const wrappedWhere = hasLogicalOr(transformedExists.SELECT.where)
|
|
2207
|
+
? [asXpr(transformedExists.SELECT.where)]
|
|
2208
|
+
: transformedExists.SELECT.where
|
|
2209
|
+
|
|
2210
|
+
on.push('and', ...wrappedWhere)
|
|
2186
2211
|
}
|
|
2187
2212
|
transformedExists.SELECT.where = on
|
|
2188
2213
|
return transformedExists.SELECT
|
|
2189
|
-
}
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
if (customWhere) {
|
|
2190
2217
|
const filter = getTransformedTokenStream(customWhere, next)
|
|
2191
|
-
|
|
2218
|
+
const wrappedFilter = hasLogicalOr(filter) ? [asXpr(filter)] : filter
|
|
2219
|
+
on.push('and', ...wrappedFilter)
|
|
2192
2220
|
}
|
|
2193
2221
|
return SELECT
|
|
2194
2222
|
}
|
|
@@ -2353,6 +2381,25 @@ function getParentEntity(element) {
|
|
|
2353
2381
|
else return getParentEntity(element.parent)
|
|
2354
2382
|
}
|
|
2355
2383
|
|
|
2384
|
+
function asXpr(thing) {
|
|
2385
|
+
return { xpr: thing }
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
function assignQueryModifiers(SELECT, modifiers) {
|
|
2389
|
+
for (const [key, val] of Object.entries(modifiers)) {
|
|
2390
|
+
if (key in { orderBy: 1, groupBy: 1 }) {
|
|
2391
|
+
if (SELECT[key]) SELECT[key].push(...val)
|
|
2392
|
+
else SELECT[key] = val
|
|
2393
|
+
} else if (key === 'limit') {
|
|
2394
|
+
// limit defined on the query has precedence
|
|
2395
|
+
if (!SELECT.limit) SELECT.limit = val
|
|
2396
|
+
} else if (key === 'having') {
|
|
2397
|
+
if (!SELECT.having) SELECT.having = val
|
|
2398
|
+
else SELECT.having.push('and', ...val)
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2356
2403
|
/**
|
|
2357
2404
|
* Assigns the given `element` as non-enumerable property 'element' onto `col`.
|
|
2358
2405
|
*
|
package/lib/fill-in-keys.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const cds = require('@sap/cds')
|
|
2
|
-
const { hasDeep } = require('
|
|
2
|
+
const { hasDeep } = require('./deep-queries')
|
|
3
3
|
|
|
4
4
|
// REVISIT: very deep & fragile dependencies to internal modules -> copy these into here
|
|
5
5
|
const propagateForeignKeys = require('@sap/cds/libx/_runtime/common/utils/propagateForeignKeys')
|
package/lib/infer/index.js
CHANGED
|
@@ -4,7 +4,7 @@ const cds = require('@sap/cds')
|
|
|
4
4
|
|
|
5
5
|
const JoinTree = require('./join-tree')
|
|
6
6
|
const { pseudos } = require('./pseudos')
|
|
7
|
-
const { isCalculatedOnRead, getImplicitAlias, getModelUtils, defineProperty } = require('../utils')
|
|
7
|
+
const { isCalculatedOnRead, getImplicitAlias, getModelUtils, defineProperty, hasOwnSkip } = require('../utils')
|
|
8
8
|
const cdsTypes = cds.linked({
|
|
9
9
|
definitions: {
|
|
10
10
|
Timestamp: { type: 'cds.Timestamp' },
|
|
@@ -404,7 +404,7 @@ function infer(originalQuery, model) {
|
|
|
404
404
|
if (arg.list) arg.list.forEach(arg => inferArg(arg, null, $baseLink, context))
|
|
405
405
|
if (arg.xpr)
|
|
406
406
|
arg.xpr.forEach((token, i) =>
|
|
407
|
-
inferArg(token, queryElements, $baseLink, { ...context, inXpr: true, inExists: arg.xpr[i - 1] === 'exists' }),
|
|
407
|
+
inferArg(token, queryElements, $baseLink, { ...context, inXpr: true, inExists: inExists || arg.xpr[i - 1] === 'exists' }),
|
|
408
408
|
) // e.g. function in expression
|
|
409
409
|
|
|
410
410
|
if (!arg.ref) {
|
|
@@ -587,7 +587,7 @@ function infer(originalQuery, model) {
|
|
|
587
587
|
}
|
|
588
588
|
|
|
589
589
|
arg.$refLinks[i].alias = !arg.ref[i + 1] && arg.as ? arg.as : id.split('.').pop()
|
|
590
|
-
if (getDefinition(arg.$refLinks[i].definition.target)
|
|
590
|
+
if (hasOwnSkip(getDefinition(arg.$refLinks[i].definition.target))) isPersisted = false
|
|
591
591
|
if (!arg.ref[i + 1]) {
|
|
592
592
|
const flatName = nameSegments.join('_')
|
|
593
593
|
defineProperty(arg, 'flatName', flatName)
|
|
@@ -640,7 +640,7 @@ function infer(originalQuery, model) {
|
|
|
640
640
|
// ignore whole expand if target of assoc along path has ”@cds.persistence.skip”
|
|
641
641
|
if (arg.expand) {
|
|
642
642
|
const { $refLinks } = arg
|
|
643
|
-
const skip = $refLinks.some(link => getDefinition(link.definition.target)
|
|
643
|
+
const skip = $refLinks.some(link => hasOwnSkip(getDefinition(link.definition.target)))
|
|
644
644
|
if (skip) {
|
|
645
645
|
$refLinks[$refLinks.length - 1].skipExpand = true
|
|
646
646
|
return
|
|
@@ -937,8 +937,15 @@ function infer(originalQuery, model) {
|
|
|
937
937
|
return true
|
|
938
938
|
}
|
|
939
939
|
if (assoc) {
|
|
940
|
+
// if(!link.definition.isAssociation) continue
|
|
941
|
+
let fkIndex = assoc.keys?.findIndex(key => key.ref.every((step, j) => column.ref[i + j] === step))
|
|
940
942
|
// foreign key access without filters never join relevant
|
|
941
|
-
if (
|
|
943
|
+
if (fkIndex !== -1) {
|
|
944
|
+
if(column.ref.slice(i).some(s => s.where)) continue // probably join relevant later on
|
|
945
|
+
fkAccess = true
|
|
946
|
+
assoc = null
|
|
947
|
+
continue
|
|
948
|
+
}
|
|
942
949
|
// <assoc>.<anotherAssoc>.<…> is join relevant as <anotherAssoc> is not fk of <assoc>
|
|
943
950
|
return true
|
|
944
951
|
}
|
package/lib/infer/join-tree.js
CHANGED
|
@@ -212,6 +212,8 @@ class JoinTree {
|
|
|
212
212
|
// filter is always join relevant
|
|
213
213
|
// if the column ends up in an `inline` -> each assoc step is join relevant
|
|
214
214
|
child.$refLink.onlyForeignKeyAccess = false
|
|
215
|
+
// all parents are now also join relevant
|
|
216
|
+
markParentAsJoinRelevant(child.parent)
|
|
215
217
|
} else {
|
|
216
218
|
child.$refLink.onlyForeignKeyAccess = true
|
|
217
219
|
}
|
|
@@ -223,6 +225,8 @@ class JoinTree {
|
|
|
223
225
|
if (node.$refLink && (!elements || !(child.$refLink.definition.name in elements))) {
|
|
224
226
|
// no foreign key access
|
|
225
227
|
node.$refLink.onlyForeignKeyAccess = false
|
|
228
|
+
markParentAsJoinRelevant(node.parent)
|
|
229
|
+
|
|
226
230
|
col.$refLinks[i - 1] = node.$refLink
|
|
227
231
|
}
|
|
228
232
|
|
|
@@ -233,6 +237,15 @@ class JoinTree {
|
|
|
233
237
|
}
|
|
234
238
|
return true
|
|
235
239
|
|
|
240
|
+
function markParentAsJoinRelevant(parent) {
|
|
241
|
+
while (parent) {
|
|
242
|
+
if (parent.$refLink?.definition.isAssociation) {
|
|
243
|
+
parent.$refLink.onlyForeignKeyAccess = false
|
|
244
|
+
}
|
|
245
|
+
parent = parent.parent
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
236
249
|
function joinId(step, args, where) {
|
|
237
250
|
let appendix
|
|
238
251
|
if (where && args) appendix = JSON.stringify(where) + JSON.stringify(args)
|
package/lib/utils.js
CHANGED
|
@@ -21,6 +21,12 @@ function prettyPrintRef(ref, model = null) {
|
|
|
21
21
|
}, '')
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function hasOwnSkip(definition) {
|
|
25
|
+
return (
|
|
26
|
+
definition && Object.hasOwn(definition, '@cds.persistence.skip') && definition['@cds.persistence.skip'] === true
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
/**
|
|
25
31
|
* Determines if a definition is calculated on read.
|
|
26
32
|
* - Stored calculated elements are not unfolded
|
|
@@ -123,7 +129,6 @@ function getModelUtils(model, query) {
|
|
|
123
129
|
if (!def || !isLocalized(def)) return def
|
|
124
130
|
return model.definitions[`localized.${def.name}`] || def
|
|
125
131
|
}
|
|
126
|
-
|
|
127
132
|
return {
|
|
128
133
|
getLocalizedName,
|
|
129
134
|
isLocalized,
|
|
@@ -139,4 +144,5 @@ module.exports = {
|
|
|
139
144
|
getImplicitAlias,
|
|
140
145
|
defineProperty,
|
|
141
146
|
getModelUtils,
|
|
147
|
+
hasOwnSkip,
|
|
142
148
|
}
|
package/package.json
CHANGED