Skip to content

Commit 29dbf5b

Browse files
Merge branch 'main' into renovate/hdb-2.x
2 parents 756751e + ac5933b commit 29dbf5b

File tree

6 files changed

+134
-48
lines changed

6 files changed

+134
-48
lines changed

db-service/lib/cqn4sql.js

Lines changed: 52 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@ cds.infer.target ??= q => q._target || q.target // instanceof cds.entity ? q._ta
55

66
const infer = require('./infer')
77
const { computeColumnsToBeSearched } = require('./search')
8-
const { prettyPrintRef, isCalculatedOnRead, isCalculatedElement, getImplicitAlias, defineProperty, getModelUtils } = require('./utils')
8+
const {
9+
prettyPrintRef,
10+
isCalculatedOnRead,
11+
isCalculatedElement,
12+
getImplicitAlias,
13+
defineProperty,
14+
getModelUtils,
15+
} = require('./utils')
916

1017
/**
1118
* For operators of <eqOps>, this is replaced by comparing all leaf elements with null, combined with and.
@@ -95,6 +102,7 @@ function cqn4sql(originalQuery, model) {
95102
const from = queryProp.from
96103

97104
const transformedProp = { __proto__: queryProp } // IMPORTANT: don't lose anything you might not know of
105+
const queryNeedsJoins = inferred.joinTree && !inferred.joinTree.isInitial
98106

99107
// Transform the existing where, prepend table aliases, and so on...
100108
if (where) {
@@ -104,7 +112,47 @@ function cqn4sql(originalQuery, model) {
104112
// Transform the from clause: association path steps turn into `WHERE EXISTS` subqueries.
105113
// The already transformed `where` clause is then glued together with the resulting subqueries.
106114
const { transformedWhere, transformedFrom } = getTransformedFrom(from || entity, transformedProp.where)
107-
const queryNeedsJoins = inferred.joinTree && !inferred.joinTree.isInitial
115+
116+
// build a subquery for DELETE / UPDATE queries with path expressions and match the primary keys
117+
if (queryNeedsJoins && (inferred.UPDATE || inferred.DELETE)) {
118+
const prop = inferred.UPDATE ? 'UPDATE' : 'DELETE'
119+
const subquery = {
120+
SELECT: {
121+
from: { ...(from || entity) },
122+
columns: [], // primary keys of the query target will be added later
123+
where: [...where],
124+
},
125+
}
126+
// The alias of the original query is now the alias for the subquery
127+
// so that potential references in the where clause to the alias match.
128+
// Hence, replace the alias of the original query with the next
129+
// available alias, so that each alias is unique.
130+
const uniqueSubqueryAlias = getNextAvailableTableAlias(transformedFrom.as)
131+
transformedFrom.as = uniqueSubqueryAlias
132+
133+
// calculate the primary keys of the target entity, there is always exactly
134+
// one query source for UPDATE / DELETE
135+
const queryTarget = Object.values(inferred.sources)[0].definition
136+
const primaryKey = { list: [] }
137+
for (const k of Object.keys(queryTarget.elements)) {
138+
const e = queryTarget.elements[k]
139+
if (e.key === true && !e.virtual && e.isAssociation !== true) {
140+
subquery.SELECT.columns.push({ ref: [e.name] })
141+
primaryKey.list.push({ ref: [transformedFrom.as, e.name] })
142+
}
143+
}
144+
145+
const transformedSubquery = cqn4sql(subquery, model)
146+
147+
// replace where condition of original query with the transformed subquery
148+
// correlate UPDATE / DELETE query with subquery by primary key matches
149+
transformedQuery[prop].where = [primaryKey, 'in', transformedSubquery]
150+
151+
if (prop === 'UPDATE') transformedQuery.UPDATE.entity = transformedFrom
152+
else transformedQuery.DELETE.from = transformedFrom
153+
154+
return transformedQuery
155+
}
108156

109157
if (inferred.SELECT) {
110158
transformedQuery = transformSelectQuery(queryProp, transformedFrom, transformedWhere, transformedQuery)
@@ -130,45 +178,7 @@ function cqn4sql(originalQuery, model) {
130178
}
131179

132180
if (queryNeedsJoins) {
133-
if (inferred.UPDATE || inferred.DELETE) {
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-
}
181+
transformedQuery[kind].from = translateAssocsToJoins(transformedQuery[kind].from)
172182
}
173183
}
174184

@@ -1668,8 +1678,7 @@ function cqn4sql(originalQuery, model) {
16681678
function getTransformedFrom(from, existingWhere = []) {
16691679
const transformedWhere = []
16701680
let transformedFrom = copy(from) // REVISIT: too expensive!
1671-
if (from.$refLinks)
1672-
defineProperty(transformedFrom, '$refLinks', [...from.$refLinks])
1681+
if (from.$refLinks) defineProperty(transformedFrom, '$refLinks', [...from.$refLinks])
16731682
if (from.args) {
16741683
transformedFrom.args = []
16751684
from.args.forEach(arg => {

db-service/test/bookshop/db/schema.cds

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,3 +467,23 @@ entity Third as projection on Second {
467467
*,
468468
first: redirected to FirstRedirected on $self.ID = first.BUBU
469469
};
470+
471+
entity Car {
472+
key ID: Integer;
473+
make: String;
474+
model: String;
475+
doors: Association to many Door on doors.car = $self;
476+
}
477+
478+
entity Door {
479+
key ID: Integer;
480+
description: String;
481+
car: Association to Car;
482+
windows: Association to many Window on windows.door = $self
483+
}
484+
485+
entity Window {
486+
key ID: Integer;
487+
description: String;
488+
door: Association to Door;
489+
}

db-service/test/cqn4sql/DELETE.test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,44 @@ describe('DELETE', () => {
107107
]
108108
expect(query.DELETE).to.deep.equal(expected.DELETE)
109109
})
110+
it('DELETE with where exists expansion and path expression via multiple assocs', () => {
111+
const forNodeModel = cds.compile.for.nodejs(JSON.parse(JSON.stringify(cds.model)))
112+
const { DELETE } = cds.ql
113+
let d = DELETE.from('bookshop.Books:author as author').where(`books.genre.name = 'Fiction'`)
114+
const query = cqn4sql(d, forNodeModel)
115+
116+
// this is the final exists subquery
117+
const subquery = cds.ql`
118+
SELECT author.ID from bookshop.Authors as author
119+
left join bookshop.Books as books on books.author_ID = author.ID
120+
left join bookshop.Genres as genre on genre.ID = books.genre_ID
121+
where exists (
122+
SELECT 1 from bookshop.Books as $B where $B.author_ID = author.ID
123+
) and genre.name = 'Fiction'
124+
`
125+
const expected = JSON.parse(`{
126+
"DELETE": {
127+
"from": {
128+
"ref": [
129+
"bookshop.Authors"
130+
],
131+
"as": "author2"
132+
}
133+
}
134+
}`)
135+
expected.DELETE.where = [
136+
{
137+
list: [
138+
{
139+
ref: ['author2', 'ID'],
140+
},
141+
],
142+
},
143+
'in',
144+
subquery,
145+
]
146+
expect(query.DELETE).to.deep.equal(expected.DELETE)
147+
})
110148

111149
it('in a list with exactly one val, dont transform to key comparison', () => {
112150
const query = {

db-service/test/cqn4sql/UPDATE.test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,25 @@ describe('UPDATE', () => {
168168
}`)
169169
expect(query.UPDATE).to.deep.equal(expected.UPDATE)
170170
})
171+
172+
it('supports multiple path expressions in where clause', () => {
173+
const q = UPDATE('bookshop.Window as Window').set({ description: 'sliding window' }).where('door.car.make =', 'BMW')
174+
const res = cqn4sql(q, model)
175+
176+
const innerSelect = cds.ql`SELECT from bookshop.Window as Window
177+
left join bookshop.Door as door on door.ID = Window.door_ID
178+
left join bookshop.Car as car on car.ID = door.car_ID
179+
{ Window.ID }
180+
where car.make = 'BMW'`
181+
182+
const expected = UPDATE.entity({ ref: ['bookshop.Window'] }).alias('Window2')
183+
expected.UPDATE.where = [
184+
{ list: [{ ref: ['Window2', 'ID'] }] },
185+
'in',
186+
innerSelect,
187+
]
188+
expect (JSON.parse(JSON.stringify(res))).to.deep.equal(JSON.parse(JSON.stringify(expected)))
189+
})
171190
})
172191
describe('UPDATE with path expression', () => {
173192
let model

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/cds.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ cds.test = Object.setPrototypeOf(function () {
112112
}
113113

114114
// Clean cache
115-
delete cds.services._pending.db
115+
delete cds.services._pending?.db
116116
delete cds.services.db
117117
delete cds.db
118118
delete cds.model

0 commit comments

Comments
 (0)