@cap-js/db-service 2.6.0 → 2.8.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 +25 -0
- package/lib/cql-functions.js +42 -0
- package/lib/cqn2sql.js +41 -22
- package/lib/cqn4sql.js +31 -20
- package/lib/infer/index.js +55 -14
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@
|
|
|
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.8.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.7.0...db-service-v2.8.0) (2025-12-15)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* Added support to use `UPSERT` from `SELECT` ([#1435](https://github.com/cap-js/cds-dbs/issues/1435)) ([68f3db8](https://github.com/cap-js/cds-dbs/commit/68f3db8d79aa120768fe81324cd164782b9eec1b))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
* propagte target to subquery ([#1438](https://github.com/cap-js/cds-dbs/issues/1438)) ([5460e43](https://github.com/cap-js/cds-dbs/commit/5460e4398079750ec3afec9a1747007618d23ecd))
|
|
18
|
+
|
|
19
|
+
## [2.7.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.6.0...db-service-v2.7.0) (2025-11-26)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
* `error` standard function ([#1421](https://github.com/cap-js/cds-dbs/issues/1421)) ([b1b0fca](https://github.com/cap-js/cds-dbs/commit/b1b0fca00387c45ed91280b2df4282be90ea0a6e))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
* LimitedRank with compositions ([#1391](https://github.com/cap-js/cds-dbs/issues/1391)) ([31766cd](https://github.com/cap-js/cds-dbs/commit/31766cd8f9b626d090129b174ac9a04b4d578c21))
|
|
30
|
+
* reject nested projection if duplicated ([#1411](https://github.com/cap-js/cds-dbs/issues/1411)) ([6e924c9](https://github.com/cap-js/cds-dbs/commit/6e924c9942de6e6a4abf7b2c168d4378efcaefa9))
|
|
31
|
+
|
|
7
32
|
## [2.6.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.5.1...db-service-v2.6.0) (2025-10-23)
|
|
8
33
|
|
|
9
34
|
|
package/lib/cql-functions.js
CHANGED
|
@@ -4,6 +4,48 @@ const cds = require('@sap/cds')
|
|
|
4
4
|
|
|
5
5
|
// OData: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_CanonicalFunctions
|
|
6
6
|
const StandardFunctions = {
|
|
7
|
+
/**
|
|
8
|
+
* Generates SQL statement that produces a runtime compatible error object
|
|
9
|
+
* @param {string|object} message - The i18n key or message of the error object
|
|
10
|
+
* @param {Array<xpr>} args - The arguments to apply to the i18n string
|
|
11
|
+
* @param {Array<xpr>} targets - The name of the element that the error is related to
|
|
12
|
+
* @return {string} - SQL statement
|
|
13
|
+
*/
|
|
14
|
+
error: function (message, args, targets) {
|
|
15
|
+
targets = targets && (targets.list || (targets.val || targets.ref) && [targets])
|
|
16
|
+
if (Array.isArray(targets)) targets = targets.map(e => e.ref && { val: e.ref.at(-1) } || e)
|
|
17
|
+
args = args && (args.list || (args.val || args.ref) && [args])
|
|
18
|
+
|
|
19
|
+
return `(${this.SELECT({
|
|
20
|
+
SELECT: {
|
|
21
|
+
expand: 'root',
|
|
22
|
+
columns: [
|
|
23
|
+
{
|
|
24
|
+
__proto__: (message || { val: null }),
|
|
25
|
+
as: 'message',
|
|
26
|
+
},
|
|
27
|
+
args ? {
|
|
28
|
+
func: 'json_array',
|
|
29
|
+
args: args,
|
|
30
|
+
as: 'args',
|
|
31
|
+
element: cds.builtin.types.Map,
|
|
32
|
+
} : { val: null, as: 'args' },
|
|
33
|
+
targets ? {
|
|
34
|
+
func: 'json_array',
|
|
35
|
+
args: targets,
|
|
36
|
+
as: 'targets',
|
|
37
|
+
element: cds.builtin.types.Map,
|
|
38
|
+
} : { val: null, as: 'targets' },
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
elements: {
|
|
42
|
+
message: cds.builtin.types.String,
|
|
43
|
+
args: cds.builtin.types.Map,
|
|
44
|
+
targets: cds.builtin.types.Map,
|
|
45
|
+
}
|
|
46
|
+
})})`
|
|
47
|
+
},
|
|
48
|
+
|
|
7
49
|
/**
|
|
8
50
|
* Generates SQL statement that produces a boolean value indicating whether the search term is contained in the given columns
|
|
9
51
|
* @param {string} ref - The reference object containing column information
|
package/lib/cqn2sql.js
CHANGED
|
@@ -325,7 +325,7 @@ class CQN2SQLRenderer {
|
|
|
325
325
|
DistanceFromRoot: { xpr: [{ ref: ['HIERARCHY_LEVEL'] }, '-', { val: 1, param: false }], as: 'DistanceFromRoot' },
|
|
326
326
|
DrillState: false,
|
|
327
327
|
LimitedDescendantCount: { xpr: [{ ref: ['HIERARCHY_TREE_SIZE'] }, '-', { val: 1, param: false }], as: 'LimitedDescendantCount' },
|
|
328
|
-
LimitedRank: { xpr: [{ func: 'row_number', args: [] }, 'OVER', { xpr: [] }, '-', { val: 1, param: false }], as: 'LimitedRank' }
|
|
328
|
+
LimitedRank: { xpr: [{ func: 'row_number', args: [] }, 'OVER', { xpr: ['ORDER', 'BY', { ref: ['HIERARCHY_RANK'] }, 'ASC'] }, '-', { val: 1, param: false }], as: 'LimitedRank' }
|
|
329
329
|
}
|
|
330
330
|
|
|
331
331
|
const columnsFiltered = columns
|
|
@@ -393,11 +393,11 @@ class CQN2SQLRenderer {
|
|
|
393
393
|
const alias = stableFrom.as
|
|
394
394
|
const source = () => {
|
|
395
395
|
return ({
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
396
|
+
func: 'HIERARCHY',
|
|
397
|
+
args: [{ xpr: ['SOURCE', { SELECT: { columns: columnsIn, from: stableFrom } }, ...(orderBy ? ['SIBLING', 'ORDER', 'BY', `${this.orderBy(orderBy)}`] : [])] }],
|
|
398
|
+
as: alias
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
401
|
|
|
402
402
|
const expandedByNr = { list: [] } // DistanceTo(...,null)
|
|
403
403
|
const expandedByOne = { list: [] } // DistanceTo(...,1)
|
|
@@ -1019,12 +1019,17 @@ class CQN2SQLRenderer {
|
|
|
1019
1019
|
const entity = this.name(q._target.name, q)
|
|
1020
1020
|
const alias = INSERT.into.as
|
|
1021
1021
|
const elements = q.elements || q._target?.elements || {}
|
|
1022
|
-
|
|
1022
|
+
let columns = (this.columns = (INSERT.columns || ObjectKeys(elements)).filter(
|
|
1023
1023
|
c => c in elements && !elements[c].virtual && !elements[c].isAssociation,
|
|
1024
1024
|
))
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
)}`
|
|
1025
|
+
|
|
1026
|
+
const src = this.cqn4sql(INSERT.from)
|
|
1027
|
+
const extractions = this._managed = this.managed(columns.map(c => ({ name: c, sql: `NEW.${this.quote(c)}` })), elements)
|
|
1028
|
+
const sql = extractions.length > columns.length
|
|
1029
|
+
? `SELECT ${extractions.map(c => `${c.insert} AS ${this.quote(c.name)}`)} FROM (${this.SELECT(src)}) AS NEW`
|
|
1030
|
+
: this.SELECT(src)
|
|
1031
|
+
if (extractions.length > columns.length) columns = this.columns = extractions.map(c => c.name)
|
|
1032
|
+
this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns.map(c => this.quote(c))}) ${sql}`
|
|
1028
1033
|
this.entries = [this.values]
|
|
1029
1034
|
return this.sql
|
|
1030
1035
|
}
|
|
@@ -1077,18 +1082,32 @@ class CQN2SQLRenderer {
|
|
|
1077
1082
|
.map(k => `NEW.${this.quote(k)}=OLD.${this.quote(k)}`)
|
|
1078
1083
|
.join(' AND ')
|
|
1079
1084
|
|
|
1080
|
-
|
|
1081
|
-
const
|
|
1085
|
+
let columns = this.columns // this.columns is computed as part of this.INSERT
|
|
1086
|
+
const entity = this.name(q._target?.name || UPSERT.into.ref[0], q)
|
|
1087
|
+
if (UPSERT.entries || UPSERT.rows || UPSERT.values) {
|
|
1088
|
+
const managed = this._managed.slice(0, columns.length)
|
|
1082
1089
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1090
|
+
const extractkeys = managed
|
|
1091
|
+
.filter(c => keys.includes(c.name))
|
|
1092
|
+
.map(c => `${c.onInsert || c.sql} as ${this.quote(c.name)}`)
|
|
1086
1093
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1094
|
+
sql = `SELECT ${managed.map(c => c.upsert
|
|
1095
|
+
.replace(/value->/g, '"$$$$value$$$$"->')
|
|
1096
|
+
.replace(/json_type\(value,/g, 'json_type("$$$$value$$$$",'))
|
|
1097
|
+
} FROM (SELECT value as "$$value$$", ${extractkeys} from json_each(?)) as NEW LEFT JOIN ${this.quote(entity)} AS OLD ON ${keyCompare}`
|
|
1098
|
+
} else {
|
|
1099
|
+
const extractions = this._managed
|
|
1100
|
+
if (this.values) this.values = [] // Clear previously computed values
|
|
1101
|
+
const src = this.cqn4sql(UPSERT.from || UPSERT.as)
|
|
1102
|
+
const aliasedQuery = cds.ql.SELECT
|
|
1103
|
+
.columns(src.SELECT.columns
|
|
1104
|
+
.map((c, i) => ({ ref: [this.column_name(c)], as: this.columns[i] }))
|
|
1105
|
+
)
|
|
1106
|
+
.from(src)
|
|
1107
|
+
sql = `SELECT ${extractions.map(c => `${c.upsert}`)} FROM (${this.SELECT(aliasedQuery)}) AS NEW LEFT JOIN ${this.quote(entity)} AS OLD ON ${keyCompare}`
|
|
1108
|
+
if (extractions.length > columns.length) columns = this.columns = extractions.map(c => c.name)
|
|
1109
|
+
this.entries = [this.values]
|
|
1110
|
+
}
|
|
1092
1111
|
|
|
1093
1112
|
const updateColumns = columns.filter(c => {
|
|
1094
1113
|
if (keys.includes(c)) return false //> keys go into ON CONFLICT clause
|
|
@@ -1326,7 +1345,7 @@ class CQN2SQLRenderer {
|
|
|
1326
1345
|
} else {
|
|
1327
1346
|
cds.error`Invalid arguments provided for function '${func}' (${args})`
|
|
1328
1347
|
}
|
|
1329
|
-
const fn = this.class.Functions[func]?.apply(this, args) || `${func}(${args})`
|
|
1348
|
+
const fn = this.class.Functions[func]?.apply(this, Array.isArray(args) ? args : [args]) || `${func}(${args})`
|
|
1330
1349
|
if (xpr) return `${fn} ${this.xpr({ xpr })}`
|
|
1331
1350
|
return fn
|
|
1332
1351
|
}
|
|
@@ -1430,7 +1449,7 @@ class CQN2SQLRenderer {
|
|
|
1430
1449
|
|
|
1431
1450
|
let onInsert = this.managed_session_context(element[cdsOnInsert]?.['='])
|
|
1432
1451
|
|| this.managed_session_context(element.default?.ref?.[0])
|
|
1433
|
-
|| (element.default && { __proto__:
|
|
1452
|
+
|| (element.default && { __proto__: element.default, param: false })
|
|
1434
1453
|
let onUpdate = this.managed_session_context(element[cdsOnUpdate]?.['='])
|
|
1435
1454
|
|
|
1436
1455
|
if (onInsert) onInsert = this.expr(onInsert)
|
package/lib/cqn4sql.js
CHANGED
|
@@ -74,6 +74,12 @@ function cqn4sql(originalQuery, model) {
|
|
|
74
74
|
if (inferred.SELECT?.from.ref?.at(-1).id) {
|
|
75
75
|
assignQueryModifiers(inferred.SELECT, inferred.SELECT.from.ref.at(-1))
|
|
76
76
|
}
|
|
77
|
+
if (inferred.DELETE?.from.ref?.at(-1).id) {
|
|
78
|
+
assignQueryModifiers(inferred.DELETE, inferred.DELETE.from.ref.at(-1))
|
|
79
|
+
}
|
|
80
|
+
if (inferred.UPDATE?.entity.ref?.at(-1).id) {
|
|
81
|
+
assignQueryModifiers(inferred.UPDATE, inferred.UPDATE.entity.ref.at(-1))
|
|
82
|
+
}
|
|
77
83
|
inferred = infer(inferred, model)
|
|
78
84
|
const { getLocalizedName, isLocalized, getDefinition } = getModelUtils(model, originalQuery) // TODO: pass model to getModelUtils
|
|
79
85
|
// if the query has custom joins we don't want to transform it
|
|
@@ -275,7 +281,11 @@ function cqn4sql(originalQuery, model) {
|
|
|
275
281
|
const alreadySeen = new Map()
|
|
276
282
|
inferred.joinTree._roots.forEach(r => {
|
|
277
283
|
const args = []
|
|
278
|
-
if (r.queryArtifact.SELECT)
|
|
284
|
+
if (r.queryArtifact.SELECT) {
|
|
285
|
+
const transformedSubquery = transformSubquery(r.queryArtifact)
|
|
286
|
+
transformedSubquery.as = r.alias
|
|
287
|
+
args.push(transformedSubquery)
|
|
288
|
+
}
|
|
279
289
|
else {
|
|
280
290
|
const id = getLocalizedName(r.queryArtifact)
|
|
281
291
|
args.push({ ref: [r.args ? { id, args: r.args } : id], as: r.alias })
|
|
@@ -1460,24 +1470,12 @@ function cqn4sql(originalQuery, model) {
|
|
|
1460
1470
|
else transformedTokenStream.push({ list: getTransformedTokenStream(list, { $baseLink, prop: 'list' }) })
|
|
1461
1471
|
}
|
|
1462
1472
|
} else if (tokenStream.length === 1 && token.val && $baseLink) {
|
|
1463
|
-
// infix filter - OData variant w/o mentioning key
|
|
1473
|
+
// infix filter - OData variant w/o mentioning key
|
|
1464
1474
|
const def = getDefinition($baseLink.definition.target) || $baseLink.definition
|
|
1465
|
-
const
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
if (v !== backlinkFor($baseLink.definition)?.[0]) {
|
|
1470
|
-
// up__ID already part of inner where exists, no need to add it explicitly here
|
|
1471
|
-
flatKeys.push(...getFlatColumnsFor(v, { tableAlias: $baseLink.alias }))
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
// TODO: improve error message, the current message is generally not true (only for OData shortcut notation)
|
|
1475
|
-
if (flatKeys.length > 1)
|
|
1476
|
-
throw new Error('Filters can only be applied to managed associations which result in a single foreign key')
|
|
1477
|
-
flatKeys.forEach(c => keyValComparisons.push([...[c, '=', token]]))
|
|
1478
|
-
keyValComparisons.forEach((kv, j) =>
|
|
1479
|
-
transformedTokenStream.push(...kv) && keyValComparisons[j + 1] ? transformedTokenStream.push('and') : null,
|
|
1480
|
-
)
|
|
1475
|
+
const flatKeys = getPrimaryKey(def, $baseLink.alias)
|
|
1476
|
+
if (flatKeys.length > 1) // TODO: what about keyless?
|
|
1477
|
+
throw new Error(`Shortcut notation “[${token.val}]” not available for composite primary key of “${def.name}”, write “<key> = ${token.val}” explicitly`)
|
|
1478
|
+
transformedTokenStream.push(...[flatKeys[0], '=', token]);
|
|
1481
1479
|
} else if (token.ref && token.param) {
|
|
1482
1480
|
transformedTokenStream.push({ ...token })
|
|
1483
1481
|
} else if (pseudos.elements[token.ref?.[0]]) {
|
|
@@ -1785,9 +1783,10 @@ function cqn4sql(originalQuery, model) {
|
|
|
1785
1783
|
}
|
|
1786
1784
|
}
|
|
1787
1785
|
|
|
1788
|
-
//
|
|
1789
|
-
if (refReverse[0].where)
|
|
1786
|
+
// OData variant w/o mentioning key
|
|
1787
|
+
if (refReverse[0].where?.length === 1 && refReverse[0].where[0].val) {
|
|
1790
1788
|
filterConditions.push(getTransformedTokenStream(refReverse[0].where,{ $baseLink: $refLinksReverse[0] }))
|
|
1789
|
+
}
|
|
1791
1790
|
|
|
1792
1791
|
if (existingWhere.length > 0) filterConditions.push(existingWhere)
|
|
1793
1792
|
if (whereExistsSubSelects.length > 0) {
|
|
@@ -2283,6 +2282,12 @@ function cqn4sql(originalQuery, model) {
|
|
|
2283
2282
|
if (!node || !node.$refLinks || !node.ref) {
|
|
2284
2283
|
throw new Error('Invalid node')
|
|
2285
2284
|
}
|
|
2285
|
+
if(node.$refLinks[0].$main) {
|
|
2286
|
+
if (node.isJoinRelevant) {
|
|
2287
|
+
return getJoinRelevantAlias(node)
|
|
2288
|
+
}
|
|
2289
|
+
return node.$refLinks[0].alias
|
|
2290
|
+
}
|
|
2286
2291
|
if ($baseLink) {
|
|
2287
2292
|
return getBaseLinkAlias($baseLink)
|
|
2288
2293
|
}
|
|
@@ -2418,6 +2423,12 @@ function assignQueryModifiers(SELECT, modifiers) {
|
|
|
2418
2423
|
} else if (key === 'having') {
|
|
2419
2424
|
if (!SELECT.having) SELECT.having = val
|
|
2420
2425
|
else SELECT.having.push('and', ...val)
|
|
2426
|
+
} else if (key === 'where') {
|
|
2427
|
+
// ignore OData shortcut variant: `… bookshop.Orders:items[2]`
|
|
2428
|
+
if(!val || val.length === 1 && val[0].val) continue
|
|
2429
|
+
if (!SELECT.where) SELECT.where = val
|
|
2430
|
+
// infix filter comes first in resulting where
|
|
2431
|
+
else SELECT.where = [...(hasLogicalOr(val) ? [asXpr(val)] : val), 'and', ...(hasLogicalOr(SELECT.where) ? [asXpr(SELECT.where)] : SELECT.where)]
|
|
2421
2432
|
}
|
|
2422
2433
|
}
|
|
2423
2434
|
}
|
package/lib/infer/index.js
CHANGED
|
@@ -47,7 +47,6 @@ function infer(originalQuery, model) {
|
|
|
47
47
|
let $combinedElements
|
|
48
48
|
|
|
49
49
|
const sources = inferTarget(_.into || _.from || _.entity, {}) // IMPORTANT: _.into has to go before _.from for INSERT.into().from(SELECT)
|
|
50
|
-
const joinTree = new JoinTree(sources)
|
|
51
50
|
const aliases = Object.keys(sources)
|
|
52
51
|
const target = aliases.length === 1 ? getDefinitionFromSources(sources, aliases[0]) : originalQuery
|
|
53
52
|
Object.defineProperties(inferred, {
|
|
@@ -72,7 +71,6 @@ function infer(originalQuery, model) {
|
|
|
72
71
|
Object.defineProperties(inferred, {
|
|
73
72
|
$combinedElements: { value: $combinedElements, writable: true, configurable: true },
|
|
74
73
|
elements: { value: elements, writable: true, configurable: true },
|
|
75
|
-
joinTree: { value: joinTree, writable: true, configurable: true }, // REVISIT: eliminate
|
|
76
74
|
})
|
|
77
75
|
// also enrich original query -> writable because it may be inferred again
|
|
78
76
|
defineProperty(originalQuery, 'elements', elements)
|
|
@@ -96,6 +94,10 @@ function infer(originalQuery, model) {
|
|
|
96
94
|
*/
|
|
97
95
|
function inferTarget(from, querySources, useTechnicalAlias = true) {
|
|
98
96
|
const { ref } = from
|
|
97
|
+
// Given a from clause `Root:parent[$main.name = name].parent as Foo`
|
|
98
|
+
// we need to first resolve until to the last step of the from.ref
|
|
99
|
+
// before we can replace $main with `Foo`
|
|
100
|
+
const $mainLazyResolve = [] // TODO: remove and replace with real alias breakout
|
|
99
101
|
if (ref) {
|
|
100
102
|
const { id, args } = ref[0]
|
|
101
103
|
const first = id || ref[0]
|
|
@@ -111,7 +113,7 @@ function infer(originalQuery, model) {
|
|
|
111
113
|
if (target.kind !== 'entity' && !target.isAssociation)
|
|
112
114
|
throw new Error('Query source must be a an entity or an association')
|
|
113
115
|
|
|
114
|
-
inferArg(from, null, null, { inFrom: true })
|
|
116
|
+
inferArg(from, null, null, { inFrom: true, $mainLazyResolve })
|
|
115
117
|
const alias =
|
|
116
118
|
from.uniqueSubqueryAlias ||
|
|
117
119
|
from.as ||
|
|
@@ -137,6 +139,10 @@ function infer(originalQuery, model) {
|
|
|
137
139
|
} else if (from.SET) {
|
|
138
140
|
infer(from, model)
|
|
139
141
|
}
|
|
142
|
+
|
|
143
|
+
const joinTree = new JoinTree(querySources)
|
|
144
|
+
Object.defineProperty( inferred, 'joinTree', { value: joinTree, writable: true, configurable: true } )
|
|
145
|
+
for(const lazyRef of $mainLazyResolve) inferArg(lazyRef)
|
|
140
146
|
return querySources
|
|
141
147
|
}
|
|
142
148
|
|
|
@@ -201,7 +207,7 @@ function infer(originalQuery, model) {
|
|
|
201
207
|
} else if (col.val !== undefined || col.xpr || col.SELECT || col.func || col.param) {
|
|
202
208
|
const as = col.as || col.func || col.val
|
|
203
209
|
if (as === undefined) cds.error`Expecting expression to have an alias name`
|
|
204
|
-
if (queryElements[as])
|
|
210
|
+
if (queryElements[as]) rejectDuplicatedElement(as)
|
|
205
211
|
if (col.xpr || col.SELECT) {
|
|
206
212
|
queryElements[as] = getElementForXprOrSubquery(col, queryElements, dollarSelfRefs)
|
|
207
213
|
}
|
|
@@ -422,11 +428,13 @@ function infer(originalQuery, model) {
|
|
|
422
428
|
// we must ignore the element from the queries elements
|
|
423
429
|
let isPersisted = true
|
|
424
430
|
let firstStepIsTableAlias, firstStepIsSelf, expandOnTableAlias
|
|
425
|
-
|
|
431
|
+
const firstStepIsDollarMain = arg.ref.length > 1 && arg.ref[0] === '$main'
|
|
432
|
+
if (!inFrom && !firstStepIsDollarMain) {
|
|
426
433
|
firstStepIsTableAlias = arg.ref.length > 1 && arg.ref[0] in sources
|
|
427
434
|
firstStepIsSelf = !firstStepIsTableAlias && arg.ref.length > 1 && ['$self', '$projection'].includes(arg.ref[0])
|
|
428
435
|
expandOnTableAlias = arg.ref.length === 1 && arg.ref[0] in sources && (arg.expand || arg.inline)
|
|
429
436
|
}
|
|
437
|
+
|
|
430
438
|
if (dollarSelfRefs && firstStepIsSelf) {
|
|
431
439
|
defineProperty(arg, 'inXpr', true)
|
|
432
440
|
dollarSelfRefs.push(arg)
|
|
@@ -438,10 +446,20 @@ function infer(originalQuery, model) {
|
|
|
438
446
|
// on conditions of joins
|
|
439
447
|
const skipAliasedFkSegmentsOfNameStack = []
|
|
440
448
|
let pseudoPath = false
|
|
441
|
-
arg.ref.
|
|
449
|
+
for(let i = 0; i < arg.ref.length; i++) {
|
|
450
|
+
const step = arg.ref[i]
|
|
442
451
|
const id = step.id || step
|
|
443
452
|
if (i === 0) {
|
|
444
|
-
if
|
|
453
|
+
if(firstStepIsDollarMain) {
|
|
454
|
+
if(inFrom) { // we need to resolve the full from clause first
|
|
455
|
+
context.$mainLazyResolve.push(arg)
|
|
456
|
+
return; // this will be done once the from clause is fully resolved
|
|
457
|
+
} else {
|
|
458
|
+
// replace $main with the alias of the outermost query
|
|
459
|
+
const mainAlias = getMainAlias(inferred)
|
|
460
|
+
arg.$refLinks.push(Object.assign(mainAlias, {$main: true}))
|
|
461
|
+
}
|
|
462
|
+
} else if (id in pseudos.elements) {
|
|
445
463
|
// pseudo path
|
|
446
464
|
arg.$refLinks.push({ definition: pseudos.elements[id], target: pseudos })
|
|
447
465
|
pseudoPath = true // only first path step must be well defined
|
|
@@ -569,6 +587,7 @@ function infer(originalQuery, model) {
|
|
|
569
587
|
skipJoinsForFilter = true
|
|
570
588
|
} else if (token.ref || token.xpr || token.list) {
|
|
571
589
|
inferArg(token, false, arg.$refLinks[i], {
|
|
590
|
+
...context,
|
|
572
591
|
inExists: skipJoinsForFilter || inExists,
|
|
573
592
|
inXpr: !!token.xpr,
|
|
574
593
|
inInfixFilter: true,
|
|
@@ -586,7 +605,8 @@ function infer(originalQuery, model) {
|
|
|
586
605
|
})
|
|
587
606
|
}
|
|
588
607
|
|
|
589
|
-
arg.$refLinks[i]
|
|
608
|
+
if(!arg.$refLinks[i].$main)
|
|
609
|
+
arg.$refLinks[i].alias = !arg.ref[i + 1] && arg.as ? arg.as : id.split('.').pop()
|
|
590
610
|
if (hasOwnSkip(getDefinition(arg.$refLinks[i].definition.target))) isPersisted = false
|
|
591
611
|
if (!arg.ref[i + 1]) {
|
|
592
612
|
const flatName = nameSegments.join('_')
|
|
@@ -602,9 +622,17 @@ function infer(originalQuery, model) {
|
|
|
602
622
|
if (arg.$refLinks.length === 1 && arg.$refLinks[0].definition.kind === 'entity')
|
|
603
623
|
elementName = arg.$refLinks[0].alias
|
|
604
624
|
else elementName = arg.as || flatName
|
|
605
|
-
|
|
625
|
+
|
|
626
|
+
if (queryElements) {
|
|
627
|
+
if (queryElements[elementName] !== undefined)
|
|
628
|
+
rejectDuplicatedElement(elementName)
|
|
629
|
+
queryElements[elementName] = elements
|
|
630
|
+
}
|
|
606
631
|
} else if (arg.inline && queryElements) {
|
|
607
632
|
const elements = resolveInline(arg)
|
|
633
|
+
for (const elName in elements) {
|
|
634
|
+
if (queryElements[elName] !== undefined) rejectDuplicatedElement(elName)
|
|
635
|
+
}
|
|
608
636
|
Object.assign(queryElements, elements)
|
|
609
637
|
} else {
|
|
610
638
|
// shortcut for `ref: ['$user']` -> `ref: ['$user', 'id']`
|
|
@@ -626,14 +654,13 @@ function infer(originalQuery, model) {
|
|
|
626
654
|
else elementName = flatName
|
|
627
655
|
}
|
|
628
656
|
if (queryElements[elementName] !== undefined)
|
|
629
|
-
|
|
657
|
+
rejectDuplicatedElement(elementName)
|
|
630
658
|
const element = getCopyWithAnnos(arg, leafArt)
|
|
631
659
|
queryElements[elementName] = element
|
|
632
660
|
}
|
|
633
661
|
}
|
|
634
662
|
}
|
|
635
|
-
}
|
|
636
|
-
|
|
663
|
+
}
|
|
637
664
|
// we need inner joins for the path expressions inside filter expressions after exists predicate
|
|
638
665
|
if ($baseLink?.pathExpressionInsideFilter) defineProperty(arg, 'join', 'inner')
|
|
639
666
|
|
|
@@ -656,7 +683,9 @@ function infer(originalQuery, model) {
|
|
|
656
683
|
: arg
|
|
657
684
|
if (isColumnJoinRelevant(colWithBase)) {
|
|
658
685
|
defineProperty(arg, 'isJoinRelevant', true)
|
|
659
|
-
|
|
686
|
+
// join resolved in outer query
|
|
687
|
+
if(!(arg.$refLinks[0].$main && originalQuery.outerQueries))
|
|
688
|
+
inferred.joinTree.mergeColumn(colWithBase, originalQuery.outerQueries)
|
|
660
689
|
}
|
|
661
690
|
}
|
|
662
691
|
if (isCalculatedOnRead(leafArt)) {
|
|
@@ -807,6 +836,10 @@ function infer(originalQuery, model) {
|
|
|
807
836
|
throw new Error(err.join(','))
|
|
808
837
|
}
|
|
809
838
|
}
|
|
839
|
+
function rejectDuplicatedElement(elementName) {
|
|
840
|
+
throw new Error(`Duplicate definition of element “${elementName}”`)
|
|
841
|
+
}
|
|
842
|
+
|
|
810
843
|
function linkCalculatedElement(column, baseLink, baseColumn, context = {}) {
|
|
811
844
|
const calcElement = column.$refLinks?.[column.$refLinks.length - 1].definition || column
|
|
812
845
|
if (alreadySeenCalcElements.has(calcElement)) return
|
|
@@ -901,7 +934,7 @@ function infer(originalQuery, model) {
|
|
|
901
934
|
if (calcElementIsJoinRelevant) {
|
|
902
935
|
if (!calcElement.value.isJoinRelevant)
|
|
903
936
|
defineProperty(step, 'isJoinRelevant',true)
|
|
904
|
-
joinTree.mergeColumn(p, originalQuery.outerQueries)
|
|
937
|
+
inferred.joinTree.mergeColumn(p, originalQuery.outerQueries)
|
|
905
938
|
} else {
|
|
906
939
|
// we need to explicitly set the value to false in this case,
|
|
907
940
|
// e.g. `SELECT from booksCalc.Books { ID, author.{name }, author {name } }`
|
|
@@ -1160,4 +1193,12 @@ function applyToFunctionArgs(funcArgs, cb, cbArgs) {
|
|
|
1160
1193
|
else if (typeof funcArgs === 'object') Object.keys(funcArgs).forEach(prop => cb(funcArgs[prop], ...cbArgs))
|
|
1161
1194
|
}
|
|
1162
1195
|
|
|
1196
|
+
function getMainAlias (query) {
|
|
1197
|
+
let mainAlias
|
|
1198
|
+
if (query.outerQueries) mainAlias = query.outerQueries[0].SELECT?.from.$refLinks.at(-1)
|
|
1199
|
+
else mainAlias = query.SELECT?.from.$refLinks.at(-1)
|
|
1200
|
+
if(!mainAlias) throw new Error('Cannot determine main query source for $main, please report this')
|
|
1201
|
+
return mainAlias
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1163
1204
|
module.exports = infer
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/db-service",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "CDS base database service",
|
|
5
5
|
"homepage": "https://github.com/cap-js/cds-dbs/tree/main/db-service#cds-base-database-service",
|
|
6
6
|
"repository": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"generic-pool": "^3.9.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@sap/cds": ">=9"
|
|
30
|
+
"@sap/cds": ">=9.4.5"
|
|
31
31
|
},
|
|
32
32
|
"license": "Apache-2.0"
|
|
33
33
|
}
|