diff --git a/db-service/lib/cqn4sql.js b/db-service/lib/cqn4sql.js index 96c32fb59..cb5f71282 100644 --- a/db-service/lib/cqn4sql.js +++ b/db-service/lib/cqn4sql.js @@ -1827,7 +1827,6 @@ function cqn4sql(originalQuery, model) { const { on, keys } = assocRefLink.definition const target = getDefinition(assocRefLink.definition.target) let res - // technically we could have multiple backlinks if (keys) { const fkPkPairs = getParentKeyForeignKeyPairs(assocRefLink.definition, targetSideRefLink, true) const transformedOn = [] @@ -1960,6 +1959,12 @@ function cqn4sql(originalQuery, model) { } }) } else if (backlink.keys) { + // sanity check: error out if we can't produce a join + if (backlink.keys.length === 0) { + throw new Error( + `Path step “${assocRefLink.alias}” is a self comparison with “${getFullName(backlink)}” that has no foreign keys`, + ) + } // managed backlink -> calculate fk-pk pairs const fkPkPairs = getParentKeyForeignKeyPairs(backlink, targetSideRefLink) fkPkPairs.forEach((pair, j) => { diff --git a/db-service/test/cqn4sql/keyless.test.js b/db-service/test/cqn4sql/keyless.test.js index 258c4d632..b55c2345f 100644 --- a/db-service/test/cqn4sql/keyless.test.js +++ b/db-service/test/cqn4sql/keyless.test.js @@ -12,61 +12,96 @@ describe('keyless entities', () => { beforeAll(async () => { model = await cds.load(__dirname + '/model/keyless').then(cds.linked) }) - - it('no foreign keys for join', () => { - const { Books } = model.entities - const q = SELECT.from(Books).where(`author[ID = 42].book[ID = 42].author.name LIKE 'King'`) - expect(() => cqn4sql(q, model)).to.throw( - 'Path step “author” of “author[…].book[…].author.name” has no foreign keys', - ) - // ok if explicit foreign key is used - const qOk = SELECT.columns('ID').from(Books).where(`authorWithExplicitForeignKey[ID = 42].name LIKE 'King'`) - expect(cqn4sql(qOk, model)).to.eql( - CQL`SELECT Books.ID FROM Books as Books + describe('managed assocs', () => { + it('no foreign keys for join', () => { + const { Books } = model.entities + const q = SELECT.from(Books).where(`author[ID = 42].book[ID = 42].author.name LIKE 'King'`) + expect(() => cqn4sql(q, model)).to.throw( + 'Path step “author” of “author[…].book[…].author.name” has no foreign keys', + ) + // ok if explicit foreign key is used + const qOk = SELECT.columns('ID').from(Books).where(`authorWithExplicitForeignKey[ID = 42].name LIKE 'King'`) + expect(cqn4sql(qOk, model)).to.eql( + CQL`SELECT Books.ID FROM Books as Books left join Authors as authorWithExplicitForeignKey on authorWithExplicitForeignKey.ID = Books.authorWithExplicitForeignKey_ID and authorWithExplicitForeignKey.ID = 42 where authorWithExplicitForeignKey.name LIKE 'King'`, - ) - }) - it('scoped query leading to where exists subquery cant be constructed', () => { - const q = SELECT.from('Books:author') - expect(() => cqn4sql(q, model)).to.throw(`Path step “author” of “Books:author” has no foreign keys`) + ) + }) + it('no foreign keys for join (2)', () => { + const { Authors } = model.entities + const q = SELECT.from(Authors).where(`book.authorWithExplicitForeignKey.book.my.author LIKE 'King'`) + expect(() => cqn4sql(q, model)).to.throw( + 'Path step “author” of “book.authorWithExplicitForeignKey.book.my.author” has no foreign keys', + ) + }) + it('scoped query leading to where exists subquery cant be constructed', () => { + const q = SELECT.from('Books:author') + expect(() => cqn4sql(q, model)).to.throw(`Path step “author” of “Books:author” has no foreign keys`) - // ok if explicit foreign key is used - const qOk = SELECT.from('Books:authorWithExplicitForeignKey').columns('ID') - expect(cqn4sql(qOk, model)).to.eql( - CQL`SELECT authorWithExplicitForeignKey.ID FROM Authors as authorWithExplicitForeignKey + // ok if explicit foreign key is used + const qOk = SELECT.from('Books:authorWithExplicitForeignKey').columns('ID') + expect(cqn4sql(qOk, model)).to.eql( + CQL`SELECT authorWithExplicitForeignKey.ID FROM Authors as authorWithExplicitForeignKey where exists ( SELECT 1 from Books as Books where Books.authorWithExplicitForeignKey_ID = authorWithExplicitForeignKey.ID )`, - ) - }) - it('where exists predicate cant be transformed to subquery', () => { - const q = SELECT.from('Books').where('exists author') - expect(() => cqn4sql(q, model)).to.throw(`Path step “author” of “author” has no foreign keys`) - // ok if explicit foreign key is used - const qOk = SELECT.from('Books').columns('ID').where('exists authorWithExplicitForeignKey') - expect(cqn4sql(qOk, model)).to.eql( - CQL`SELECT Books.ID FROM Books as Books + ) + }) + it('where exists predicate cant be transformed to subquery', () => { + const q = SELECT.from('Books').where('exists author') + expect(() => cqn4sql(q, model)).to.throw(`Path step “author” of “author” has no foreign keys`) + // ok if explicit foreign key is used + const qOk = SELECT.from('Books').columns('ID').where('exists authorWithExplicitForeignKey') + expect(cqn4sql(qOk, model)).to.eql( + CQL`SELECT Books.ID FROM Books as Books where exists ( SELECT 1 from Authors as authorWithExplicitForeignKey where authorWithExplicitForeignKey.ID = Books.authorWithExplicitForeignKey_ID )`, - ) - }) - it('correlated subquery for expand cant be constructed', () => { - const q = CQL`SELECT author { name } from Books` - expect(() => cqn4sql(q, model)).to.throw(`Can't expand “author” as it has no foreign keys`) - // ok if explicit foreign key is used - const qOk = CQL`SELECT authorWithExplicitForeignKey { name } from Books` - expect(JSON.parse(JSON.stringify(cqn4sql(qOk, model)))).to.eql( - CQL` + ) + }) + it('correlated subquery for expand cant be constructed', () => { + const q = CQL`SELECT author { name } from Books` + expect(() => cqn4sql(q, model)).to.throw(`Can't expand “author” as it has no foreign keys`) + // ok if explicit foreign key is used + const qOk = CQL`SELECT authorWithExplicitForeignKey { name } from Books` + expect(JSON.parse(JSON.stringify(cqn4sql(qOk, model)))).to.eql( + CQL` SELECT ( SELECT authorWithExplicitForeignKey.name from Authors as authorWithExplicitForeignKey where Books.authorWithExplicitForeignKey_ID = authorWithExplicitForeignKey.ID ) as authorWithExplicitForeignKey from Books as Books`, - ) + ) + }) + }) + describe('managed assocs as backlinks', () => { + it('backlink has no foreign keys for join', () => { + const { Authors } = model.entities + const q = SELECT.from(Authors).where(`bookWithBackLink.title LIKE 'Potter'`) + expect(() => cqn4sql(q, model)).to.throw( + `Path step “bookWithBackLink” is a self comparison with “author” that has no foreign keys`, + ) + }) + it('backlink has no foreign keys for scoped query', () => { + const q = SELECT.from('Authors:bookWithBackLink') + expect(() => cqn4sql(q, model)).to.throw( + `Path step “bookWithBackLink” is a self comparison with “author” that has no foreign keys`, + ) + }) + it('backlink has no foreign keys for where exists subquery', () => { + const q = SELECT.from('Authors').where('exists bookWithBackLink') + expect(() => cqn4sql(q, model)).to.throw( + `Path step “bookWithBackLink” is a self comparison with “author” that has no foreign keys`, + ) + }) + it('backlink has no foreign keys for expand subquery', () => { + const q = CQL`SELECT bookWithBackLink { title } from Authors` + expect(() => cqn4sql(q, model)).to.throw( + `Path step “bookWithBackLink” is a self comparison with “author” that has no foreign keys`, + ) + }) }) }) diff --git a/db-service/test/cqn4sql/model/keyless.cds b/db-service/test/cqn4sql/model/keyless.cds index 815413259..efb756ca0 100644 --- a/db-service/test/cqn4sql/model/keyless.cds +++ b/db-service/test/cqn4sql/model/keyless.cds @@ -12,4 +12,6 @@ entity Authors { ID : Integer; name : String; book: Association to Books; + // backlink has no foreign keys... + bookWithBackLink: Association to Books on bookWithBackLink.author = $self; }