diff --git a/db-service/lib/SQLService.js b/db-service/lib/SQLService.js index 19e6bf6c0..bd488f17f 100644 --- a/db-service/lib/SQLService.js +++ b/db-service/lib/SQLService.js @@ -257,9 +257,7 @@ class SQLService extends DatabaseService { const table = getDBTable(req.target) const { compositions } = table - // Check if we are in the hierarchy case - let isHierarchy = req.target.elements?.LimitedDescendantCount - const recursiveBacklinks = [] + const recursiveComps = [] if (compositions) { // Transform CQL`DELETE from Foo[p1] WHERE p2` into CQL`DELETE from Foo[p1 and p2]` @@ -271,28 +269,14 @@ class SQLService extends DatabaseService { from = { ref: [...from.ref.slice(0, -1), { id: last, where }] } } // Process child compositions depth-first - let { depth = 0, visited = [] } = req + let { visited = [] } = req visited.push(req.target.name) await Promise.all( Object.values(compositions).map(c => { if (c._target['@cds.persistence.skip'] === true) return if (c._target === req.target) { - // deep delete for hierarchies - if (isHierarchy) { - function _getBacklinkName(on) { - const i = on.findIndex(e => e.ref && e.ref[0] === '$self') - if (i === -1) return - let ref - if (on[i + 1] && on[i + 1] === '=') ref = on[i + 2].ref - if (on[i - 1] && on[i - 1] === '=') ref = on[i - 2].ref - return ref && ref[ref.length - 1] - } - const backlinkName = _getBacklinkName(c.on) - recursiveBacklinks.push(backlinkName) - return - } - // the Genre.children case - if (++depth > (c['@depth'] || 3)) return + recursiveComps.push(c.name) + return } else if (visited.includes(c._target.name)) throw new Error( `Transitive circular composition detected: \n\n` + @@ -302,10 +286,10 @@ class SQLService extends DatabaseService { // Prepare and run deep query, à la CQL`DELETE from Foo[pred]:comp1.comp2...` const query = DELETE.from({ ref: [...from.ref, c.name] }) query._target = c._target - return this.onDELETE({ query, depth, visited: [...visited], target: c._target }) + return this.onDELETE({ query, visited: [...visited], target: c._target }) }), ) - if (recursiveBacklinks.length) { + if (recursiveComps.length) { let key // For hierarchies, only a single key is supported for (const _key in req.target.keys) { @@ -314,10 +298,11 @@ class SQLService extends DatabaseService { break } const _where = [] - for (const backlink of recursiveBacklinks) { + for (const comp of recursiveComps) { const _recursiveQuery = SELECT.from(table).columns(key) _recursiveQuery.SELECT.recurse = { - ref: [backlink], + ref: [comp], + backward: false, where: from.ref[0].where, } _where.push({ ref: [key] }, 'in', _recursiveQuery, 'or') diff --git a/db-service/lib/cql-functions.js b/db-service/lib/cql-functions.js index f7433937d..cd86f15cc 100644 --- a/db-service/lib/cql-functions.js +++ b/db-service/lib/cql-functions.js @@ -204,6 +204,8 @@ const HANAFunctions = { // Ensure that the orderBy column are exposed by the source for hierarchy sorting const orderBy = args.xpr.find((_, i, arr) => /ORDER/i.test(arr[i - 2]) && /BY/i.test(arr[i - 1])) + const startWhere = args.xpr.find((_, i, arr) => /START/i.test(arr[i - 2]) && /WHERE/i.test(arr[i - 1])) + const passThroughColumns = src.SELECT.columns.map(c => ({ ref: ['Source', this.column_name(c)] })) src.as = 'H' + (uniqueCounter++) src = this.expr(this.with(src)) @@ -213,7 +215,7 @@ SELECT 1 as HIERARCHY_LEVEL, NODE_ID as HIERARCHY_ROOT_ID FROM ${src} AS Source -WHERE parent_ID IS NULL +WHERE ${startWhere ? this.expr(startWhere) : 'parent_ID IS NULL'} UNION ALL SELECT Parent.HIERARCHY_LEVEL + 1, diff --git a/db-service/lib/cqn2sql.js b/db-service/lib/cqn2sql.js index 5c21c742d..e7633a252 100644 --- a/db-service/lib/cqn2sql.js +++ b/db-service/lib/cqn2sql.js @@ -386,11 +386,11 @@ class CQN2SQLRenderer { const alias = stableFrom.as const source = () => { return ({ - func: 'HIERARCHY', - args: [{ xpr: ['SOURCE', { SELECT: { columns: columnsIn, from: stableFrom } }, ...(orderBy ? ['SIBLING', 'ORDER', 'BY', `${this.orderBy(orderBy)}`] : [])] }], - as: alias - }) - } + func: 'HIERARCHY', + args: [{ xpr: ['SOURCE', { SELECT: { columns: columnsIn, from: stableFrom } }, ...(orderBy ? ['SIBLING', 'ORDER', 'BY', `${this.orderBy(orderBy)}`] : [])] }], + as: alias + }) + } const expandedByNr = { list: [] } // DistanceTo(...,null) const expandedByOne = { list: [] } // DistanceTo(...,1) diff --git a/test/compliance/DELETE.test.js b/test/compliance/DELETE.test.js index 85b07daa8..913a2a700 100644 --- a/test/compliance/DELETE.test.js +++ b/test/compliance/DELETE.test.js @@ -30,11 +30,47 @@ const recusiveData = [ ], }, ], + recursiveToOne: { + ID: 103, + fooRoot: 'Recursive to one Horror', + children: [ + { + ID: 1031, + fooChild: 'bar', + children: [ + { + ID: 10311, + fooGrandChild: 'bar', + }, + ], + recursiveToOneChild: { + ID: 1032, + fooChild: 'Recursive to one Horror 2', + }, + }, + ], + recursiveToOne: { + ID: 1033, + fooRoot: 'Recursive to one Horror 3', + recursiveToOne: { + ID: 10331, + fooRoot: 'Recursive to one Horror 3', + recursiveToOne: { + ID: 103311, + fooRoot: 'Recursive to one Horror 3', + recursiveToOne: { + ID: 1033111, + fooRoot: 'Recursive to one Horror 3', + }, + }, + }, + }, + }, }, { ID: 11, fooRoot: 'Low Horror', - parent_ID: 10, + recParent_ID: 10, children: [ { ID: 111, @@ -61,7 +97,7 @@ const recusiveData = [ { ID: 12, fooRoot: 'Medium Horror', - parent_ID: 11, + recParent_ID: 11, children: [ { ID: 121, @@ -88,7 +124,7 @@ const recusiveData = [ { ID: 13, fooRoot: 'Hard Horror', - parent_ID: 11, + recParent_ID: 11, children: [ { ID: 131, @@ -115,7 +151,7 @@ const recusiveData = [ { ID: 14, fooRoot: 'Very Hard Horror', - parent_ID: 12, + recParent_ID: 12, children: [ { ID: 141, @@ -142,7 +178,7 @@ const recusiveData = [ { ID: 15, fooRoot: 'Very Very Hard Horror', - parent_ID: 14, + recParent_ID: 14, children: [ { ID: 151, diff --git a/test/compliance/resources/db/complex/associations.cds b/test/compliance/resources/db/complex/associations.cds index 283bebf80..61a44c2ad 100644 --- a/test/compliance/resources/db/complex/associations.cds +++ b/test/compliance/resources/db/complex/associations.cds @@ -3,9 +3,10 @@ namespace complex.associations; entity Root { key ID : Integer; fooRoot : String; - parent : Association to Root; + recursiveToOne : Composition of one Root; + recParent : Association to Root; recursive : Composition of many Root - on recursive.parent = $self; + on recursive.recParent = $self; children : Composition of many Child on children.parent = $self; }