@cap-js/db-service 1.0.1 → 1.1.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 +11 -2
- package/index.js +16 -2
- package/lib/InsertResults.js +20 -3
- package/lib/SQLService.js +112 -28
- package/lib/common/DatabaseService.js +55 -4
- package/lib/common/factory.d.ts +5 -0
- package/lib/converters.d.ts +24 -0
- package/lib/cql-functions.js +191 -4
- package/lib/cqn2sql.js +270 -5
- package/lib/cqn4sql.js +60 -17
- package/lib/deep-queries.js +27 -0
- package/lib/fill-in-keys.js +10 -0
- package/lib/infer/cqn.d.ts +45 -0
- package/lib/infer/index.js +102 -27
- package/lib/infer/join-tree.js +62 -17
- package/package.json +18 -6
package/lib/cqn4sql.js
CHANGED
|
@@ -70,13 +70,14 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
70
70
|
// Transform the from clause: association path steps turn into `WHERE EXISTS` subqueries.
|
|
71
71
|
// The already transformed `where` clause is then glued together with the resulting subqueries.
|
|
72
72
|
const { transformedWhere, transformedFrom } = getTransformedFrom(from || entity, transformedProp.where)
|
|
73
|
+
const queryNeedsJoins = inferred.joinTree && !inferred.joinTree.isInitial
|
|
73
74
|
|
|
74
75
|
if (inferred.SELECT) {
|
|
75
76
|
transformedQuery = transformSelectQuery(queryProp, transformedFrom, transformedWhere, transformedQuery)
|
|
76
77
|
} else {
|
|
77
78
|
if (from) {
|
|
78
79
|
transformedProp.from = transformedFrom
|
|
79
|
-
} else {
|
|
80
|
+
} else if (!queryNeedsJoins) {
|
|
80
81
|
transformedProp.entity = transformedFrom
|
|
81
82
|
}
|
|
82
83
|
|
|
@@ -94,7 +95,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
if (
|
|
98
|
+
if (queryNeedsJoins) {
|
|
98
99
|
transformedQuery[kind].from = translateAssocsToJoins(transformedQuery[kind].from)
|
|
99
100
|
}
|
|
100
101
|
}
|
|
@@ -119,7 +120,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
119
120
|
if (columns) {
|
|
120
121
|
transformedQuery.SELECT.columns = getTransformedColumns(columns)
|
|
121
122
|
} else {
|
|
122
|
-
transformedQuery.SELECT.columns = getColumnsForWildcard()
|
|
123
|
+
transformedQuery.SELECT.columns = getColumnsForWildcard(originalQuery.SELECT?.excluding)
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
// Like the WHERE clause, aliases from the SELECT list are not accessible for `group by`/`having` (in most DB's)
|
|
@@ -158,7 +159,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
158
159
|
*
|
|
159
160
|
* @param {string} kind - The type of operation: "INSERT" or "UPSERT".
|
|
160
161
|
*
|
|
161
|
-
* @returns {
|
|
162
|
+
* @returns {object} - The transformed query with updated `into` clause.
|
|
162
163
|
*/
|
|
163
164
|
function transformQueryForInsertUpsert(kind) {
|
|
164
165
|
const { as } = transformedQuery[kind].into
|
|
@@ -170,10 +171,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
170
171
|
/**
|
|
171
172
|
* Transforms a stream query, replacing the `where` and `into` clauses after processing.
|
|
172
173
|
*
|
|
173
|
-
* @param {
|
|
174
|
-
* @param {
|
|
174
|
+
* @param {object} inferred - The inferred object containing the STREAM query.
|
|
175
|
+
* @param {object} transformedQuery - The query object to be transformed.
|
|
175
176
|
*
|
|
176
|
-
* @returns {
|
|
177
|
+
* @returns {object} - The transformed query with updated STREAM clauses.
|
|
177
178
|
*/
|
|
178
179
|
function transformStreamQuery() {
|
|
179
180
|
const { into, where } = inferred.STREAM
|
|
@@ -193,8 +194,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
193
194
|
/**
|
|
194
195
|
* Transforms a search expression to a WHERE clause for a SELECT operation.
|
|
195
196
|
*
|
|
196
|
-
* @param {
|
|
197
|
-
* @param {
|
|
197
|
+
* @param {object} search - The search expression which shall be applied to the searchable columns on the query source.
|
|
198
|
+
* @param {object} from - The FROM clause of the CQN statement.
|
|
198
199
|
*
|
|
199
200
|
* @returns {(Object|Array|undefined)} - If the target of the query contains searchable elements, the function returns an array that represents the WHERE clause.
|
|
200
201
|
* If the SELECT query already contains a WHERE clause, this array includes the existing clause and appends an AND condition with the new 'contains' clause.
|
|
@@ -293,6 +294,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
293
294
|
}
|
|
294
295
|
}
|
|
295
296
|
|
|
297
|
+
function isCalculatedOnRead(def) {
|
|
298
|
+
return def?.value && !def.value.stored
|
|
299
|
+
}
|
|
300
|
+
|
|
296
301
|
/**
|
|
297
302
|
* Walks over a list of columns (ref's, xpr, subqueries, val), applies flattening on structured types and expands wildcards.
|
|
298
303
|
*
|
|
@@ -304,7 +309,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
304
309
|
for (let i = 0; i < columns.length; i++) {
|
|
305
310
|
const col = columns[i]
|
|
306
311
|
|
|
307
|
-
if (col.
|
|
312
|
+
if (isCalculatedOnRead(col.$refLinks?.[col.$refLinks.length - 1].definition)) {
|
|
313
|
+
const calcElement = resolveCalculatedElement(col)
|
|
314
|
+
transformedColumns.push(calcElement)
|
|
315
|
+
} else if (col.expand) {
|
|
308
316
|
if (col.ref?.length > 1 && col.ref[0] === '$self' && !col.$refLinks[0].definition.kind) {
|
|
309
317
|
const dollarSelfReplacement = calculateDollarSelfColumn(col)
|
|
310
318
|
transformedColumns.push(...getTransformedColumns([dollarSelfReplacement]))
|
|
@@ -433,6 +441,30 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
433
441
|
}
|
|
434
442
|
}
|
|
435
443
|
|
|
444
|
+
function resolveCalculatedElement(column, omitAlias = false, baseLink = null) {
|
|
445
|
+
let value
|
|
446
|
+
|
|
447
|
+
if (column.$refLinks) {
|
|
448
|
+
const { $refLinks } = column
|
|
449
|
+
value = $refLinks[$refLinks.length - 1].definition.value
|
|
450
|
+
baseLink = [...column.$refLinks].reverse().find(link => link.definition.isAssociation) || baseLink
|
|
451
|
+
} else {
|
|
452
|
+
value = column.value
|
|
453
|
+
}
|
|
454
|
+
const { ref, val, xpr, func } = value
|
|
455
|
+
|
|
456
|
+
let res
|
|
457
|
+
if (ref) {
|
|
458
|
+
res = getTransformedTokenStream([value], baseLink)[0]
|
|
459
|
+
} else if (xpr) {
|
|
460
|
+
res = { xpr: getTransformedTokenStream(value.xpr, baseLink) }
|
|
461
|
+
} else if (val) {
|
|
462
|
+
res = { val }
|
|
463
|
+
} else if (func) res = { args: getTransformedTokenStream(value.args), func: value.func }
|
|
464
|
+
if (!omitAlias) res.as = column.as || column.name || column.flatName
|
|
465
|
+
return res
|
|
466
|
+
}
|
|
467
|
+
|
|
436
468
|
/**
|
|
437
469
|
* This function resolves a `ref` starting with a `$self`.
|
|
438
470
|
* Such a path targets another element of the query by it's implicit, or explicit alias.
|
|
@@ -632,7 +664,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
632
664
|
*
|
|
633
665
|
* @param {CSN.column} column - The column with the 'expand' property to be transformed into a subquery.
|
|
634
666
|
*
|
|
635
|
-
* @returns {
|
|
667
|
+
* @returns {object} Returns a subquery correlated with the enclosing query, with added properties `expand:true` and `one:true|false`.
|
|
636
668
|
*/
|
|
637
669
|
function expandColumn(column) {
|
|
638
670
|
let outerAlias
|
|
@@ -726,7 +758,10 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
726
758
|
const res = []
|
|
727
759
|
for (let i = 0; i < columns.length; i++) {
|
|
728
760
|
const col = columns[i]
|
|
729
|
-
if (col.
|
|
761
|
+
if (isCalculatedOnRead(col.$refLinks?.[col.$refLinks.length - 1].definition)) {
|
|
762
|
+
const calcElement = resolveCalculatedElement(col, true)
|
|
763
|
+
res.push(calcElement)
|
|
764
|
+
} else if (col.isJoinRelevant) {
|
|
730
765
|
const tableAlias$refLink = getQuerySourceName(col)
|
|
731
766
|
const transformedColumn = {
|
|
732
767
|
ref: [tableAlias$refLink, getFullName(col.$refLinks[col.$refLinks.length - 1].definition)],
|
|
@@ -826,6 +861,8 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
826
861
|
// for wildcard on subquery in from, just reference the elements
|
|
827
862
|
if (tableAlias.SELECT && !element.elements && !element.target) {
|
|
828
863
|
wildcardColumns.push(index ? { ref: [index, k] } : { ref: [k] })
|
|
864
|
+
} else if (isCalculatedOnRead(element)) {
|
|
865
|
+
wildcardColumns.push(resolveCalculatedElement(element))
|
|
829
866
|
} else {
|
|
830
867
|
const flatColumns = getFlatColumnsFor(element, { tableAlias: index }, [], { exclude, replace }, true)
|
|
831
868
|
wildcardColumns.push(...flatColumns)
|
|
@@ -1157,6 +1194,12 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1157
1194
|
|
|
1158
1195
|
let result = is_regexp(token?.val) ? token : copy(token) // REVISIT: too expensive! //
|
|
1159
1196
|
if (token.ref) {
|
|
1197
|
+
const { definition } = token.$refLinks[token.$refLinks.length - 1]
|
|
1198
|
+
if (isCalculatedOnRead(definition)) {
|
|
1199
|
+
const calculatedElement = resolveCalculatedElement(token, true, $baseLink)
|
|
1200
|
+
transformedTokenStream.push(calculatedElement)
|
|
1201
|
+
continue
|
|
1202
|
+
}
|
|
1160
1203
|
if (token.ref.length > 1 && token.ref[0] === '$self' && !token.$refLinks[0].definition.kind) {
|
|
1161
1204
|
const dollarSelfReplacement = [calculateDollarSelfColumn(token, true)]
|
|
1162
1205
|
transformedTokenStream.push(...getTransformedTokenStream(dollarSelfReplacement))
|
|
@@ -1472,11 +1515,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
1472
1515
|
each.ref[0] in { $self: true, $projection: true } ? getParentEntity(assoc) : target,
|
|
1473
1516
|
),
|
|
1474
1517
|
)
|
|
1475
|
-
|
|
1476
|
-
function getParentEntity(element) {
|
|
1477
|
-
if (element.kind === 'entity') return element
|
|
1478
|
-
else return getParentEntity(element.parent)
|
|
1479
|
-
}
|
|
1480
1518
|
}
|
|
1481
1519
|
|
|
1482
1520
|
/**
|
|
@@ -1920,6 +1958,11 @@ function getLastStringSegment(str) {
|
|
|
1920
1958
|
return index != -1 ? str.substring(index + 1) : str
|
|
1921
1959
|
}
|
|
1922
1960
|
|
|
1961
|
+
function getParentEntity(element) {
|
|
1962
|
+
if (element.kind === 'entity') return element
|
|
1963
|
+
else return getParentEntity(element.parent)
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1923
1966
|
/**
|
|
1924
1967
|
* Assigns the given `element` as non-enumerable property 'element' onto `col`.
|
|
1925
1968
|
*
|
package/lib/deep-queries.js
CHANGED
|
@@ -4,6 +4,17 @@ const { _target_name4 } = require('./SQLService')
|
|
|
4
4
|
|
|
5
5
|
const handledDeep = Symbol('handledDeep')
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* @callback nextCallback
|
|
9
|
+
* @param {Error|undefined} error
|
|
10
|
+
* @returns {Promise<unknown>}
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {import('@sap/cds/apis/services').Request} req
|
|
15
|
+
* @param {nextCallback} next
|
|
16
|
+
* @returns {Promise<number>}
|
|
17
|
+
*/
|
|
7
18
|
async function onDeep(req, next) {
|
|
8
19
|
const { query } = req
|
|
9
20
|
// REVISIT: req.target does not match the query.INSERT target for path insert
|
|
@@ -136,11 +147,16 @@ const _calculateExpandColumns = (target, data, expandColumns = [], elementMap =
|
|
|
136
147
|
}
|
|
137
148
|
}
|
|
138
149
|
|
|
150
|
+
/**
|
|
151
|
+
* @param {import('@sap/cds/apis/cqn').Query} query
|
|
152
|
+
* @param {import('@sap/cds/apis/csn').Definition} target
|
|
153
|
+
*/
|
|
139
154
|
const getExpandForDeep = (query, target) => {
|
|
140
155
|
const from = query.DELETE?.from || query.UPDATE?.entity
|
|
141
156
|
const data = query.UPDATE?.data || null
|
|
142
157
|
const where = query.DELETE?.where || query.UPDATE?.where
|
|
143
158
|
|
|
159
|
+
/** @type {import("@sap/cds/apis/ql").SELECT<unknown>} */
|
|
144
160
|
const cqn = SELECT.from(from)
|
|
145
161
|
if (where) cqn.SELECT.where = where
|
|
146
162
|
|
|
@@ -150,6 +166,12 @@ const getExpandForDeep = (query, target) => {
|
|
|
150
166
|
return cqn
|
|
151
167
|
}
|
|
152
168
|
|
|
169
|
+
/**
|
|
170
|
+
* @param {import('@sap/cds/apis/cqn').Query} query
|
|
171
|
+
* @param {unknown[]} dbData
|
|
172
|
+
* @param {import('@sap/cds/apis/csn').Definition} target
|
|
173
|
+
* @returns
|
|
174
|
+
*/
|
|
153
175
|
const getDeepQueries = (query, dbData, target) => {
|
|
154
176
|
let queryData
|
|
155
177
|
if (query.INSERT) {
|
|
@@ -174,6 +196,11 @@ const _hasManagedElements = target => {
|
|
|
174
196
|
return Object.keys(target.elements).filter(elementName => target.elements[elementName]['@cds.on.update']).length > 0
|
|
175
197
|
}
|
|
176
198
|
|
|
199
|
+
/**
|
|
200
|
+
* @param {unknown[]} diff
|
|
201
|
+
* @param {import('@sap/cds/apis/csn').Definition} target
|
|
202
|
+
* @returns {import('@sap/cds/apis/cqn').Query[]}
|
|
203
|
+
*/
|
|
177
204
|
const _getDeepQueries = (diff, target) => {
|
|
178
205
|
const queries = []
|
|
179
206
|
|
package/lib/fill-in-keys.js
CHANGED
|
@@ -41,6 +41,16 @@ const generateUUIDandPropagateKeys = (target, data, event) => {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* @callback nextCallback
|
|
46
|
+
* @param {Error|undefined} error
|
|
47
|
+
* @returns {Promise<unknown>}
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {import('@sap/cds/apis/services').Request} req
|
|
52
|
+
* @param {nextCallback} next
|
|
53
|
+
*/
|
|
44
54
|
module.exports = async function fill_in_keys(req, next) {
|
|
45
55
|
// REVISIT dummy handler until we have input processing
|
|
46
56
|
if (!req.target || !this.model || req.target._unresolved) return next()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as cqn from '@sap/cds/apis/cqn'
|
|
2
|
+
import * as csn from '@sap/cds/apis/csn'
|
|
3
|
+
|
|
4
|
+
type linkedQuery = {
|
|
5
|
+
target: csn.Definition
|
|
6
|
+
elements: elements
|
|
7
|
+
}
|
|
8
|
+
export type SELECT = cqn.SELECT & linkedQuery
|
|
9
|
+
export type INSERT = cqn.INSERT & linkedQuery
|
|
10
|
+
export type UPSERT = cqn.UPSERT & linkedQuery
|
|
11
|
+
export type UPDATE = cqn.UPDATE & linkedQuery
|
|
12
|
+
export type STREAM = {
|
|
13
|
+
STREAM: { into: cqn.name | ref; data: ReadableStream<Buffer>; from: cqn.source; column?: ref; where: predicate }
|
|
14
|
+
} & linkedQuery
|
|
15
|
+
export type DELETE = cqn.DELETE & linkedQuery
|
|
16
|
+
export type CREATE = cqn.CREATE & linkedQuery
|
|
17
|
+
export type DROP = cqn.DROP & linkedQuery
|
|
18
|
+
|
|
19
|
+
export type Query = SELECT | INSERT | UPSERT | UPDATE | DELETE | CREATE | DROP
|
|
20
|
+
|
|
21
|
+
export type element = csn.Element & {
|
|
22
|
+
key?: boolean
|
|
23
|
+
virtual?: boolean
|
|
24
|
+
unique?: boolean
|
|
25
|
+
notNull?: boolean
|
|
26
|
+
}
|
|
27
|
+
export type elements = {
|
|
28
|
+
[name: string]: element
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type col = cqn.column_expr & { element: element }
|
|
32
|
+
|
|
33
|
+
export type list = {
|
|
34
|
+
list: cqn.expr[]
|
|
35
|
+
}
|
|
36
|
+
// Passthrough
|
|
37
|
+
export type source = cqn.source
|
|
38
|
+
export type ref = cqn.ref
|
|
39
|
+
export type val = cqn.val
|
|
40
|
+
export type xpr = cqn.xpr
|
|
41
|
+
export type expr = cqn.expr
|
|
42
|
+
export type func = cqn.function_call
|
|
43
|
+
export type predicate = cqn.predicate
|
|
44
|
+
export type ordering_term = cqn.ordering_term
|
|
45
|
+
export type limit = { rows: val; offset: val }
|
package/lib/infer/index.js
CHANGED
|
@@ -4,7 +4,6 @@ const cds = require('@sap/cds/lib')
|
|
|
4
4
|
|
|
5
5
|
const JoinTree = require('./join-tree')
|
|
6
6
|
const { pseudos } = require('./pseudos')
|
|
7
|
-
// REVISIT: we should always return cds.linked elements
|
|
8
7
|
const cdsTypes = cds.linked({
|
|
9
8
|
definitions: {
|
|
10
9
|
Timestamp: { type: 'cds.Timestamp' },
|
|
@@ -18,11 +17,10 @@ const cdsTypes = cds.linked({
|
|
|
18
17
|
},
|
|
19
18
|
}).definitions
|
|
20
19
|
for (const each in cdsTypes) cdsTypes[`cds.${each}`] = cdsTypes[each]
|
|
21
|
-
|
|
22
20
|
/**
|
|
23
|
-
* @param {
|
|
24
|
-
* @param {CSN} [model]
|
|
25
|
-
* @returns {
|
|
21
|
+
* @param {import('@sap/cds/apis/cqn').Query|string} originalQuery
|
|
22
|
+
* @param {import('@sap/cds/apis/csn').CSN} [model]
|
|
23
|
+
* @returns {import('./cqn').Query} = q with .target and .elements
|
|
26
24
|
*/
|
|
27
25
|
function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
28
26
|
if (!model) cds.error('Please specify a model')
|
|
@@ -40,6 +38,10 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
40
38
|
inferred.CREATE ||
|
|
41
39
|
inferred.DROP ||
|
|
42
40
|
inferred.STREAM
|
|
41
|
+
|
|
42
|
+
// cache for already processed calculated elements
|
|
43
|
+
const alreadySeenCalcElements = new Set()
|
|
44
|
+
|
|
43
45
|
const sources = inferTarget(_.from || _.into || _.entity, {})
|
|
44
46
|
const joinTree = new JoinTree(sources)
|
|
45
47
|
const aliases = Object.keys(sources)
|
|
@@ -134,11 +136,11 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
134
136
|
* next 'ref' step should be looked up.
|
|
135
137
|
*
|
|
136
138
|
*
|
|
137
|
-
* @param {
|
|
139
|
+
* @param {object} arg - The argument object that will be augmented with additional properties.
|
|
138
140
|
* It must contain a 'ref' property, which is an array representing the steps to be processed.
|
|
139
141
|
* Optionally, it can also contain an 'xpr' property, which is also processed recursively.
|
|
140
142
|
*
|
|
141
|
-
* @param {
|
|
143
|
+
* @param {object} $baseLink - Optional parameter. It represents the environment in which the first 'ref' step should be
|
|
142
144
|
* resolved. It's needed for infix filter / expand columns. It must contain a 'definition'
|
|
143
145
|
* property, which is an object representing the base environment.
|
|
144
146
|
*
|
|
@@ -177,7 +179,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
177
179
|
if (!expandOrExists && nextStep && !(nextStep in e.foreignKeys))
|
|
178
180
|
throw new Error(`Only foreign keys of "${e.name}" can be accessed in infix filter`)
|
|
179
181
|
}
|
|
180
|
-
arg.$refLinks.push({ definition: e, target:
|
|
182
|
+
arg.$refLinks.push({ definition: e, target: definition })
|
|
181
183
|
// filter paths are flattened
|
|
182
184
|
// REVISIT: too much augmentation -> better remove flatName..
|
|
183
185
|
Object.defineProperty(arg, 'flatName', { value: ref.join('_'), writable: true })
|
|
@@ -188,7 +190,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
188
190
|
}
|
|
189
191
|
} else {
|
|
190
192
|
const recent = arg.$refLinks[i - 1]
|
|
191
|
-
const { elements } = recent.
|
|
193
|
+
const { elements } = recent.definition._target || recent.definition
|
|
192
194
|
const e = elements[id]
|
|
193
195
|
if (!e) throw new Error(`"${id}" not found in the elements of "${arg.$refLinks[i - 1].definition.name}"`)
|
|
194
196
|
arg.$refLinks.push({ definition: e, target: e._target || e })
|
|
@@ -216,6 +218,11 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
216
218
|
} else throw new Error('A filter can only be provided when navigating along associations')
|
|
217
219
|
}
|
|
218
220
|
})
|
|
221
|
+
const { definition, target } = arg.$refLinks[arg.$refLinks.length - 1]
|
|
222
|
+
if (definition.value) {
|
|
223
|
+
// nested calculated element
|
|
224
|
+
attachRefLinksToArg(definition.value, { definition: definition.parent, target }, true)
|
|
225
|
+
}
|
|
219
226
|
}
|
|
220
227
|
|
|
221
228
|
/**
|
|
@@ -227,7 +234,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
227
234
|
* Each entry in the `$combinedElements` dictionary maps from the element name
|
|
228
235
|
* to an array of objects containing the index and table alias where the element can be found.
|
|
229
236
|
*
|
|
230
|
-
* @returns {
|
|
237
|
+
* @returns {object} The `$combinedElements` dictionary, which maps element names to an array of objects
|
|
231
238
|
* containing the index and table alias where the element can be found.
|
|
232
239
|
*/
|
|
233
240
|
function inferCombinedElements() {
|
|
@@ -264,9 +271,9 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
264
271
|
* Also walks over other `ref`s in the query, validates them, and attaches `$refLinks`.
|
|
265
272
|
* This includes handling `where`, infix filters within column `refs`, or other `csn` paths.
|
|
266
273
|
*
|
|
267
|
-
* @param {
|
|
274
|
+
* @param {object} $combinedElements The `$combinedElements` dictionary of the query, which maps element names
|
|
268
275
|
* to an array of objects containing the index and table alias where the element can be found.
|
|
269
|
-
* @returns {
|
|
276
|
+
* @returns {object} The inferred `elements` dictionary of the query, which maps element names to their corresponding definitions.
|
|
270
277
|
*/
|
|
271
278
|
function inferQueryElements($combinedElements) {
|
|
272
279
|
let queryElements = {}
|
|
@@ -444,7 +451,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
444
451
|
*/
|
|
445
452
|
|
|
446
453
|
function inferQueryElement(column, insertIntoQueryElements = true, $baseLink = null, context) {
|
|
447
|
-
const { inExists, inExpr, inNestedProjection } = context || {}
|
|
454
|
+
const { inExists, inExpr, inNestedProjection, inCalcElement } = context || {}
|
|
448
455
|
if (column.param) return // parameter references are only resolved into values on execution e.g. :val, :1 or ?
|
|
449
456
|
if (column.args) column.args.forEach(arg => inferQueryElement(arg, false, $baseLink, context)) // e.g. function in expression
|
|
450
457
|
if (column.list) column.list.forEach(arg => inferQueryElement(arg, false, $baseLink, context))
|
|
@@ -486,7 +493,7 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
486
493
|
const elements = definition.elements || definition._target?.elements
|
|
487
494
|
if (elements && id in elements) {
|
|
488
495
|
const element = elements[id]
|
|
489
|
-
if (!inExists && !inNestedProjection && element.target) {
|
|
496
|
+
if (!inExists && !inNestedProjection && !inCalcElement && element.target) {
|
|
490
497
|
// only fk access in infix filter
|
|
491
498
|
const nextStep = column.ref[1]?.id || column.ref[1]
|
|
492
499
|
// no unmanaged assoc in infix filter path
|
|
@@ -500,7 +507,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
500
507
|
if (nextStep && !(nextStep in element.foreignKeys))
|
|
501
508
|
throw new Error(`Only foreign keys of "${element.name}" can be accessed in infix filter`)
|
|
502
509
|
}
|
|
503
|
-
|
|
510
|
+
const resolvableIn = definition.target ? definition._target : target
|
|
511
|
+
column.$refLinks.push({ definition: elements[id], target: resolvableIn })
|
|
504
512
|
} else {
|
|
505
513
|
stepNotFoundInPredecessor(id, definition.name)
|
|
506
514
|
}
|
|
@@ -535,14 +543,15 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
535
543
|
)
|
|
536
544
|
}
|
|
537
545
|
|
|
546
|
+
const target = definition._target || column.$refLinks[i - 1].target
|
|
538
547
|
if (element) {
|
|
539
|
-
const $refLink = { definition: elements[id], target
|
|
548
|
+
const $refLink = { definition: elements[id], target }
|
|
540
549
|
column.$refLinks.push($refLink)
|
|
541
550
|
} else if (firstStepIsSelf) {
|
|
542
551
|
stepNotFoundInColumnList(id)
|
|
543
552
|
} else if (column.ref[0] === '$user' && pseudoPath) {
|
|
544
553
|
// `$user.some.unknown.element` -> no error
|
|
545
|
-
column.$refLinks.push({ definition: {}, target
|
|
554
|
+
column.$refLinks.push({ definition: {}, target })
|
|
546
555
|
} else if (id === '$dummy') {
|
|
547
556
|
// `some.known.element.$dummy` -> no error; used by cds.ql to simulate joins
|
|
548
557
|
column.$refLinks.push({ definition: { name: '$dummy', parent: column.$refLinks[i - 1].target } })
|
|
@@ -627,7 +636,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
627
636
|
}
|
|
628
637
|
if (queryElements[elementName] !== undefined)
|
|
629
638
|
throw new Error(`Duplicate definition of element “${elementName}”`)
|
|
630
|
-
|
|
639
|
+
const element = getCopyWithAnnos(column, leafArt)
|
|
640
|
+
queryElements[elementName] = element
|
|
631
641
|
}
|
|
632
642
|
}
|
|
633
643
|
}
|
|
@@ -644,19 +654,27 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
644
654
|
return
|
|
645
655
|
}
|
|
646
656
|
}
|
|
647
|
-
const
|
|
657
|
+
const leafArt = column.$refLinks[column.$refLinks.length - 1].definition
|
|
658
|
+
const virtual = (leafArt.virtual || !isPersisted) && !inExpr
|
|
648
659
|
// check if we need to merge the column `ref` into the join tree of the query
|
|
649
|
-
if (!inExists && !virtual && isColumnJoinRelevant(column, firstStepIsSelf)) {
|
|
660
|
+
if (!inExists && !virtual && !inCalcElement && isColumnJoinRelevant(column, firstStepIsSelf)) {
|
|
661
|
+
if (originalQuery.UPDATE)
|
|
662
|
+
throw cds.error(
|
|
663
|
+
'Path expressions for UPDATE statements are not supported. Use “where exists” with infix filters instead.',
|
|
664
|
+
)
|
|
650
665
|
Object.defineProperty(column, 'isJoinRelevant', { value: true })
|
|
651
|
-
joinTree.mergeColumn(column)
|
|
666
|
+
joinTree.mergeColumn(column, $baseLink)
|
|
667
|
+
}
|
|
668
|
+
if (leafArt.value && !leafArt.value.stored) {
|
|
669
|
+
resolveCalculatedElement(leafArt, column)
|
|
652
670
|
}
|
|
653
671
|
|
|
654
672
|
/**
|
|
655
673
|
* Resolves and processes the inline attribute of a column in a database query.
|
|
656
674
|
*
|
|
657
|
-
* @param {
|
|
675
|
+
* @param {object} col - The column object with properties: `inline` and `$refLinks`.
|
|
658
676
|
* @param {string} [namePrefix=col.as || col.flatName] - Prefix for naming new columns. Defaults to `col.as` or `col.flatName`.
|
|
659
|
-
* @returns {
|
|
677
|
+
* @returns {object} - An object with resolved and processed inline column definitions.
|
|
660
678
|
*
|
|
661
679
|
* Procedure:
|
|
662
680
|
* 1. Iterate through `inline` array. For each `inlineCol`:
|
|
@@ -710,8 +728,8 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
710
728
|
/**
|
|
711
729
|
* Resolves a query column which has an `expand` property.
|
|
712
730
|
*
|
|
713
|
-
* @param {
|
|
714
|
-
* @returns {
|
|
731
|
+
* @param {object} col - The column object with properties: `expand` and `$refLinks`.
|
|
732
|
+
* @returns {object} - A `cds.struct` object with expanded column definitions.
|
|
715
733
|
*
|
|
716
734
|
* Procedure:
|
|
717
735
|
* - if `$leafLink` is an association, constructs an `expandSubquery` and infers a new query structure.
|
|
@@ -777,6 +795,58 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
777
795
|
throw new Error(err)
|
|
778
796
|
}
|
|
779
797
|
}
|
|
798
|
+
function resolveCalculatedElement(calcElement) {
|
|
799
|
+
if (alreadySeenCalcElements.has(calcElement)) return
|
|
800
|
+
else alreadySeenCalcElements.add(calcElement)
|
|
801
|
+
const { ref, xpr, func } = calcElement.value
|
|
802
|
+
if (ref || xpr) {
|
|
803
|
+
attachRefLinksToArg(calcElement.value, { definition: calcElement.parent, target: calcElement.parent }, true)
|
|
804
|
+
// column is now fully linked, now we need to find out if we need to merge it into the join tree
|
|
805
|
+
// for that, we calculate all paths from a calc element and merge them into the join tree
|
|
806
|
+
mergePathsIntoJoinTree(calcElement.value)
|
|
807
|
+
}
|
|
808
|
+
if (func) calcElement.value.args?.forEach(arg => inferQueryElement(arg, false)) // {func}.args are optional
|
|
809
|
+
function mergePathsIntoJoinTree(e, basePath = null) {
|
|
810
|
+
basePath = basePath || { $refLinks: [], ref: [] }
|
|
811
|
+
if (e.ref) {
|
|
812
|
+
e.$refLinks.forEach((link, i) => {
|
|
813
|
+
const { definition } = link
|
|
814
|
+
if (!definition.value) {
|
|
815
|
+
basePath.$refLinks.push(link)
|
|
816
|
+
basePath.ref.push(e.ref[i])
|
|
817
|
+
}
|
|
818
|
+
})
|
|
819
|
+
const leafOfCalculatedElementRef = e.$refLinks[e.$refLinks.length - 1].definition
|
|
820
|
+
if (leafOfCalculatedElementRef.value) mergePathsIntoJoinTree(leafOfCalculatedElementRef.value, basePath)
|
|
821
|
+
|
|
822
|
+
mergePathIfNecessary(basePath, e)
|
|
823
|
+
} else if (e.xpr) {
|
|
824
|
+
e.xpr.forEach(step => {
|
|
825
|
+
if (step.ref) {
|
|
826
|
+
const subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] }
|
|
827
|
+
step.$refLinks.forEach((link, i) => {
|
|
828
|
+
const { definition } = link
|
|
829
|
+
if (definition.value) {
|
|
830
|
+
mergePathsIntoJoinTree(definition.value)
|
|
831
|
+
} else {
|
|
832
|
+
subPath.$refLinks.push(link)
|
|
833
|
+
subPath.ref.push(step.ref[i])
|
|
834
|
+
}
|
|
835
|
+
})
|
|
836
|
+
mergePathIfNecessary(subPath, step)
|
|
837
|
+
}
|
|
838
|
+
})
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function mergePathIfNecessary(p, step) {
|
|
842
|
+
const calcElementIsJoinRelevant = isColumnJoinRelevant(p)
|
|
843
|
+
if (calcElementIsJoinRelevant) {
|
|
844
|
+
if (!calcElement.value.isColumnJoinRelevant) Object.defineProperty(step, 'isJoinRelevant', { value: true })
|
|
845
|
+
joinTree.mergeColumn(p)
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
780
850
|
|
|
781
851
|
/**
|
|
782
852
|
* Checks whether or not the `ref` of the given column is join relevant.
|
|
@@ -828,15 +898,20 @@ function infer(originalQuery, model = cds.context?.model || cds.model) {
|
|
|
828
898
|
* if there is not already an element with the same name present.
|
|
829
899
|
*/
|
|
830
900
|
function inferElementsFromWildCard() {
|
|
901
|
+
const exclude = _.excluding ? x => _.excluding.includes(x) : () => false
|
|
902
|
+
|
|
831
903
|
if (Object.keys(queryElements).length === 0 && aliases.length === 1) {
|
|
832
904
|
// only one query source and no overwritten columns
|
|
833
905
|
Object.entries(sources[aliases[0]].elements).forEach(([name, element]) => {
|
|
834
|
-
if (element.type !== 'cds.LargeBinary') queryElements[name] = element
|
|
906
|
+
if (!exclude(name) && element.type !== 'cds.LargeBinary') queryElements[name] = element
|
|
907
|
+
if (element.value) {
|
|
908
|
+
// we might have join relevant calculated elements
|
|
909
|
+
resolveCalculatedElement(element)
|
|
910
|
+
}
|
|
835
911
|
})
|
|
836
912
|
return
|
|
837
913
|
}
|
|
838
914
|
|
|
839
|
-
const exclude = _.excluding ? x => _.excluding.includes(x) : () => false
|
|
840
915
|
const ambiguousElements = {}
|
|
841
916
|
Object.entries($combinedElements).forEach(([name, tableAliases]) => {
|
|
842
917
|
if (Object.keys(tableAliases).length > 1) {
|