diff --git a/db-service/test/cqn4sql/calculated-elements.test.js b/db-service/test/cqn4sql/calculated-elements.test.js deleted file mode 100644 index 4540b23fd..000000000 --- a/db-service/test/cqn4sql/calculated-elements.test.js +++ /dev/null @@ -1,1254 +0,0 @@ -'use strict' - -const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds') -const { expect } = cds.test -const { loadModel } = require('./helpers/model') -const { expectCqn } = require('./helpers/expectCqn') - -describe('Unfolding calculated elements in select list', () => { - let model - beforeAll(async () => { - model = cds.model = await cds.load(__dirname + '/../bookshop/db/booksWithExpr').then(cds.linked) - }) - - it('simple reference', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, stock2 }`, model) - const expected = cds.ql`SELECT from booksCalc.Books as Books { - Books.ID, - Books.stock as stock2 - }` - expect(query).to.deep.equal(expected) - }) - - it('simple val', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Authors as Authors { ID, IBAN }`, model) - const expected = cds.ql`SELECT from booksCalc.Authors as Authors { - Authors.ID, - 'DE' || Authors.checksum || Authors.sortCode || Authors.accountNumber as IBAN - }` - expect(query).to.deep.equal(expected) - }) - - it('directly', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, area }`, model) - const expected = cds.ql`SELECT from booksCalc.Books as Books { - Books.ID, - Books.length * Books.width as area - }` - expect(query).to.deep.equal(expected) - }) - - it('in expression', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, stock * area as f }`, model) - const expected = cds.ql`SELECT from booksCalc.Books as Books { - Books.ID, - Books.stock * ( Books.length * Books.width ) as f - }` - expect(query).to.deep.equal(expected) - }) - - it('in ternary', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Ternary as Ternary { ID, nestedTernary }`, model) - const expected = cds.ql`SELECT from booksCalc.Ternary as Ternary - left join booksCalc.Books as book on book.ID = Ternary.book_ID - { - Ternary.ID, - (case when 1 > 0 then 1 else (case when book.stock > 10 then Ternary.value else 3 end) end) as nestedTernary - }` - expect(query).to.deep.equal(expected) - }) - - it('calcualted element in nested ternary', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Ternary as Ternary { ID, calculatedElementInNestedTernary }`, model) - const expected = cds.ql`SELECT from booksCalc.Ternary as Ternary - left join booksCalc.Books as book on book.ID = Ternary.book_ID - left join booksCalc.Authors as author on author.ID = book.author_ID - { - Ternary.ID, - (case when 1 > 0 then 1 else (case when book.stock > (case when 1 > 0 then 1 else (case when book.stock > years_between(author.dateOfBirth, author.dateOfDeath) then Ternary.value else 3 end) end) then Ternary.value else 3 end) end) as calculatedElementInNestedTernary - }` - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - expect(query).to.deep.equal(expected) - }) - it('list in ternary', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Ternary as Ternary { ID, nestedTernaryWithNestedXpr }`, model) - const expected = cds.ql`SELECT from booksCalc.Ternary as Ternary - left join booksCalc.Books as book on book.ID = Ternary.book_ID - { - Ternary.ID, - (case when 1 > 0 then 1 else (case when ( (10 + book.stock) in (1, 2, 3, 4) ) then Ternary.value else 3 end) end) as nestedTernaryWithNestedXpr - }` - expect(query).to.deep.equal(expected) - }) - it('in function', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, round(area, 2) as f }`, model) - const expected = cds.ql`SELECT from booksCalc.Books as Books { - Books.ID, - round(Books.length * Books.width, 2) as f - }` - expect(query).to.deep.equal(expected) - }) - it('in function with named param', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Authors as Authors { ID, ageNamedParams as f }`, model) - const expected = cds.ql`SELECT from booksCalc.Authors as Authors { - Authors.ID, - years_between(DOB => Authors.dateOfBirth, DOD => Authors.dateOfDeath) as f - }` - expect(query).to.deep.equal(expected) - }) - - it('calc elem is function', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, ctitle }`, model) - const expected = cds.ql`SELECT from booksCalc.Books as Books { - Books.ID, - substring(Books.title, 3, Books.stock) as ctitle - }` - expect(query).to.deep.equal(expected) - }) - it('calc elem is xpr with multiple functions as args', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, authorAgeNativePG }`, model) - const expected = cds.ql`SELECT from booksCalc.Books as Books - left join booksCalc.Authors as author on author.ID = Books.author_ID - { - Books.ID, - DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) as authorAgeNativePG - }` - expect(query).to.deep.equal(expected) - }) - it('calc elem is xpr with nested xpr which has multiple functions as args', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, authorAgeInDogYears }`, model) - const expected = cds.ql`SELECT from booksCalc.Books as Books - left join booksCalc.Authors as author on author.ID = Books.author_ID - { - Books.ID, - ( DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) ) * 7 as authorAgeInDogYears - }` - expect(query).to.deep.equal(expected) - }) - it('calc elem is xpr with multiple functions as args - back and forth', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, author.books.authorAgeNativePG }`, model) - const expected = cds.ql`SELECT from booksCalc.Books as Books - left join booksCalc.Authors as author on author.ID = Books.author_ID - left join booksCalc.Books as books2 on books2.author_ID = author.ID - left join booksCalc.Authors as author2 on author2.ID = books2.author_ID - { - Books.ID, - DATE_PART('year', author2.dateOfDeath) - DATE_PART('year', author2.dateOfBirth) as author_books_authorAgeNativePG - }` - expect(query).to.deep.equal(expected) - }) - - it('calc elem is function, nested in direct expression', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, ctitle || title as f }`, model) - const expected = cds.ql`SELECT from booksCalc.Books as Books { - Books.ID, - substring(Books.title, 3, Books.stock) || Books.title as f - }` - expect(query).to.deep.equal(expected) - }) - - it('nested calc elems', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, volume, storageVolume }`, model) - const expected = cds.ql`SELECT from booksCalc.Books as Books { - Books.ID, - (Books.length * Books.width) * Books.height as volume, - Books.stock * ((Books.length * Books.width) * Books.height) as storageVolume - }` - expect(query).to.deep.equal(expected) - }) - - it('nested calc elems, nested in direct expression', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, storageVolume / volume as f }`, model) - const expected = cds.ql`SELECT from booksCalc.Books as Books { - Books.ID, - (Books.stock * ((Books.length * Books.width) * Books.height)) - / ((Books.length * Books.width) * Books.height) as f - }` - expect(query).to.deep.equal(expected) - }) - - // - // with associations - // - - it('via an association path', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, author.name }`, model) - // revisit: alias follows our "regular" naming scheme -> ref.join('_') - const expected = cds.ql`SELECT from booksCalc.Books as Books - left outer join booksCalc.Authors as author on author.ID = Books.author_ID { - Books.ID, - author.firstName || ' ' || author.lastName as author_name - }` - expect(query).to.deep.equal(expected) - }) - - it('via an association in columns and where', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, author.name } where author.name like '%Bro%'`, model) - // revisit: alias follows our "regular" naming scheme -> ref.join('_') - const expected = cds.ql`SELECT from booksCalc.Books as Books - left outer join booksCalc.Authors as author on author.ID = Books.author_ID { - Books.ID, - author.firstName || ' ' || author.lastName as author_name - } where (author.firstName || ' ' || author.lastName) like '%Bro%'` - expect(query).to.deep.equal(expected) - }) - - it('via an association path, nested in direct expression', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, substring(author.name, 2, stock) as f }`, model) - const expected = cds.ql`SELECT from booksCalc.Books as Books - left outer join booksCalc.Authors as author on author.ID = Books.author_ID { - Books.ID, - substring(author.firstName || ' ' || author.lastName, 2, Books.stock) as f - }` - expect(query).to.deep.equal(expected) - }) - - it('via two association paths', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Authors as Authors { ID, books[stock<5].area, books[stock>5].area as a2}`, model) - const expected = cds.ql`SELECT from booksCalc.Authors as Authors - left outer join booksCalc.Books as books on books.author_ID = Authors.ID and books.stock < 5 - left outer join booksCalc.Books as books2 on books2.author_ID = Authors.ID and books2.stock > 5 - { - Authors.ID, - books.length * books.width as books_area, - books2.length * books2.width as a2 - }` - expect(query).to.deep.equal(expected) - }) - - it('in filter', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Authors as Authors { ID, books[area >17].title }`, model) - // intermediate: - // SELECT from booksCalc.Authors { ID, books[(length * width) > 1].title } - const expected = cds.ql`SELECT from booksCalc.Authors as Authors - left outer join booksCalc.Books as books on books.author_ID = Authors.ID - and (books.length * books.width) > 17 - { - Authors.ID, - books.title as books_title - }` - expect(query).to.deep.equal(expected) - }) - - it('calc elem contains association', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, authorName, authorLastName }`, model) - // intermediate: - // SELECT from booksCalc.Books { ID, author.name, author.lastName } - const expected = cds.ql`SELECT from booksCalc.Books as Books - left outer join booksCalc.Authors as author on author.ID = Books.author_ID - { - Books.ID, - author.firstName || ' ' || author.lastName as authorName, - author.lastName as authorLastName - }` - expect(query).to.deep.equal(expected) - }) - - it('calc elem contains associations in xpr', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, authorFullName }`, model) - // intermediate: - // SELECT from booksCalc.Books { ID, author.name, author.lastName } - const expected = cds.ql`SELECT from booksCalc.Books as Books - left outer join booksCalc.Authors as author on author.ID = Books.author_ID - { - Books.ID, - author.firstName || ' ' || author.lastName as authorFullName, - }` - expect(query).to.deep.equal(expected) - }) - - it('calc elem contains other calculated element in xpr with nested joins', () => { - let query = cqn4sql( - cds.ql`SELECT from booksCalc.Books as Books { ID, authorFullNameWithAddress } where authorFullNameWithAddress = 'foo'`, - model, - ) - // intermediate: - // SELECT from booksCalc.Books { ID, author.name, author.lastName } - const expected = cds.ql`SELECT from booksCalc.Books as Books - left outer join booksCalc.Authors as author on author.ID = Books.author_ID - left outer join booksCalc.Addresses as address on address.ID = author.address_ID - { - Books.ID, - (author.firstName || ' ' || author.lastName) || ' ' || (address.street || ', ' || address.city) - as authorFullNameWithAddress, - } where ( (author.firstName || ' ' || author.lastName) || ' ' || (address.street || ', ' || address.city) ) = 'foo'` - expect(query).to.deep.equal(expected) - }) - - it('calc elem contains association, nested', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, authorAdrText }`, model) - // intermediate: - // SELECT from booksCalc.Books { ID, author.address.{street || ', ' || city} } - const expected = cds.ql`SELECT from booksCalc.Books as Books - left outer join booksCalc.Authors as author on author.ID = Books.author_ID - left outer join booksCalc.Addresses as address on address.ID = author.address_ID - { - Books.ID, - address.street || ', ' || address.city as authorAdrText - }` - expect(query).to.deep.equal(expected) - }) - - it('calc elem contains association with filter', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Authors as Authors { ID, addressTextFilter }`, model) - // intermediate: - // SELECT from booksCalc.Authors { ID, address[number * 2 > 17].{street || ', ' || city} } - const expected = cds.ql`SELECT from booksCalc.Authors as Authors - left outer join booksCalc.Addresses as address on address.ID = Authors.address_ID - and (address.number * 2) > 17 - { - Authors.ID, - address.street || ', ' || address.city as addressTextFilter - }` - expect(query).to.deep.equal(expected) - }) - - // - // inline, expand - // - it('in inline', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, author.{name, IBAN } }`, model) - const expected = cds.ql`SELECT from booksCalc.Books as Books - left outer join booksCalc.Authors as author on author.ID = Books.author_ID - { - Books.ID, - author.firstName || ' ' || author.lastName as author_name, - 'DE' || author.checksum || author.sortCode || author.accountNumber as author_IBAN - }` - expect(query).to.deep.equal(expected) - }) - it('in inline back and forth', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, author.books.author.{name, IBAN } }`, model) - const expected = cds.ql`SELECT from booksCalc.Books as Books - left outer join booksCalc.Authors as author on author.ID = Books.author_ID - left outer join booksCalc.Books as books2 on books2.author_ID = author.ID - left outer join booksCalc.Authors as author2 on author2.ID = books2.author_ID - { - Books.ID, - author2.firstName || ' ' || author2.lastName as author_books_author_name, - 'DE' || author2.checksum || author2.sortCode || author2.accountNumber as author_books_author_IBAN - }` - expect(query).to.deep.equal(expected) - }) - it('in subquery, using the same calc element - not join relevant in subquery', () => { - let query = cqn4sql( - cds.ql`SELECT from booksCalc.Books as Books { - ID, - ( - SELECT from booksCalc.Authors as Authors { - name, - IBAN, - addressText - } - ) as sub, - author.books.author.{name, IBAN, addressText }, - }`, - model, - ) - const expected = cds.ql`SELECT from booksCalc.Books as Books - left outer join booksCalc.Authors as author on author.ID = Books.author_ID - left outer join booksCalc.Books as books2 on books2.author_ID = author.ID - left outer join booksCalc.Authors as author2 on author2.ID = books2.author_ID - left outer join booksCalc.Addresses as address on address.ID = author2.address_ID - { - Books.ID, - ( - SELECT from booksCalc.Authors as Authors - left outer join booksCalc.Addresses as address2 on address2.ID = Authors.address_ID { - Authors.firstName || ' ' || Authors.lastName as name, - 'DE' || Authors.checksum || Authors.sortCode || Authors.accountNumber as IBAN, - address2.street || ', ' || address2.city as addressText - } - ) as sub, - author2.firstName || ' ' || author2.lastName as author_books_author_name, - 'DE' || author2.checksum || author2.sortCode || author2.accountNumber as author_books_author_IBAN, - address.street || ', ' || address.city as author_books_author_addressText - }` - expect(query).to.deep.equal(expected) - }) - - it('in inline, 2 assocs', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, author.{name, addressText } }`, model) - // intermediate: - // SELECT from booksCalc.Authors { ID, author.{firstName || ' ' || lastName, address.{street || ', ' || city}}} } - const expected = cds.ql`SELECT from booksCalc.Books as Books - left outer join booksCalc.Authors as author on author.ID = Books.author_ID - left outer join booksCalc.Addresses as address on address.ID = author.address_ID - { - Books.ID, - author.firstName || ' ' || author.lastName as author_name, - address.street || ', ' || address.city as author_addressText - }` - expect(query).to.deep.equal(expected) - }) - - it('in expand (to-one)', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, author {name, IBAN } }`, model) - const expected = cds.ql`SELECT from booksCalc.Books as Books - { - Books.ID, - ( - SELECT from booksCalc.Authors as $a { - $a.firstName || ' ' || $a.lastName as name, - 'DE' || $a.checksum || $a.sortCode || $a.accountNumber as IBAN - } where Books.author_ID = $a.ID - ) as author - }` - expect(JSON.parse(JSON.stringify(query))).to.deep.equal(expected) - }) - - it('in expand (to-one), 2 assocs', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, author {name, addressText } }`, model) - const expected = cds.ql`SELECT from booksCalc.Books as Books - { - Books.ID, - ( - SELECT from booksCalc.Authors as $a - left join booksCalc.Addresses as address on address.ID = $a.address_ID - { - $a.firstName || ' ' || $a.lastName as name, - address.street || ', ' || address.city as addressText - } where Books.author_ID = $a.ID - ) as author - }` - expect(JSON.parse(JSON.stringify(query))).to.deep.equal(expected) - }) - it('expand and inline target same calc element', () => { - let query = cqn4sql( - cds.ql`SELECT from booksCalc.Books as Books { ID, author.{name, addressText }, author {name, addressText } }`, - model, - ) - const expected = cds.ql`SELECT from booksCalc.Books as Books - left outer join booksCalc.Authors as author on author.ID = Books.author_ID - left outer join booksCalc.Addresses as address on address.ID = author.address_ID - { - Books.ID, - author.firstName || ' ' || author.lastName as author_name, - address.street || ', ' || address.city as author_addressText, - ( - SELECT from booksCalc.Authors as $a - left join booksCalc.Addresses as address2 on address2.ID = $a.address_ID - { - $a.firstName || ' ' || $a.lastName as name, - address2.street || ', ' || address2.city as addressText - } where Books.author_ID = $a.ID - ) as author - }` - expect(JSON.parse(JSON.stringify(query))).to.deep.equal(expected) - }) - it('expand and inline target same calc element inverted', () => { - let query = cqn4sql( - cds.ql`SELECT from booksCalc.Books as Books { ID, author {name, addressText }, author.{name, addressText } }`, - model, - ) - const expected = cds.ql`SELECT from booksCalc.Books as Books - left outer join booksCalc.Authors as author on author.ID = Books.author_ID - left outer join booksCalc.Addresses as address on address.ID = author.address_ID - { - Books.ID, - ( - SELECT from booksCalc.Authors as $a - left join booksCalc.Addresses as address2 on address2.ID = $a.address_ID - { - $a.firstName || ' ' || $a.lastName as name, - address2.street || ', ' || address2.city as addressText - } where Books.author_ID = $a.ID - ) as author, - author.firstName || ' ' || author.lastName as author_name, - address.street || ', ' || address.city as author_addressText - }` - expect(JSON.parse(JSON.stringify(query))).to.deep.equal(expected) - }) - - it('in expand (to-many)', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Authors { ID, books { ID, area, volume } }`, model) - - const expected = cds.ql`SELECT from booksCalc.Authors as $A { - $A.ID, - ( - SELECT from booksCalc.Books as $b - { - $b.ID, - $b.length * $b.width as area, - ($b.length * $b.width) * $b.height as volume, - } where $A.ID = $b.author_ID - ) as books - }` - expect(JSON.parse(JSON.stringify(query))).to.deep.equal(expected) - }) - - // - // wildcard - // - - it('via wildcard without columns', () => { - let query = cqn4sql( - cds.ql`SELECT from booksCalc.Books as Books excluding { length, width, height, stock, price, youngAuthorName }`, - model, - ) - const expected = cds.ql`SELECT from booksCalc.Books as Books - left outer join booksCalc.Authors as author on author.ID = Books.author_ID - left outer join booksCalc.Addresses as address on address.ID = author.address_ID - { - Books.ID, - Books.title, - Books.author_ID, - - Books.stock as stock2, - substring(Books.title, 3, Books.stock) as ctitle, - - Books.areaS, - - Books.length * Books.width as area, - (Books.length * Books.width) * Books.height as volume, - Books.stock * ((Books.length * Books.width) * Books.height) as storageVolume, - - author.lastName as authorLastName, - author.firstName || ' ' || author.lastName as authorName, - author.firstName || ' ' || author.lastName as authorFullName, - (author.firstName || ' ' || author.lastName) || ' ' || (address.street || ', ' || address.city) as authorFullNameWithAddress, - address.street || ', ' || address.city as authorAdrText, - years_between( author.sortCode, author.sortCode ) as authorAge, - DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) as authorAgeNativePG, - - ( DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) ) * 7 as authorAgeInDogYears - }` - expect(JSON.parse(JSON.stringify(query))).to.deep.equal(expected) - }) - - it('via wildcard', () => { - let query = cqn4sql( - cds.ql`SELECT from booksCalc.Books as Books { * } excluding { length, width, height, stock, price, youngAuthorName}`, - model, - ) - const expected = cds.ql`SELECT from booksCalc.Books as Books - left outer join booksCalc.Authors as author on author.ID = Books.author_ID - left outer join booksCalc.Addresses as address on address.ID = author.address_ID - { - Books.ID, - Books.title, - Books.author_ID, - - Books.stock as stock2, - substring(Books.title, 3, Books.stock) as ctitle, - - Books.areaS, - - Books.length * Books.width as area, - (Books.length * Books.width) * Books.height as volume, - Books.stock * ((Books.length * Books.width) * Books.height) as storageVolume, - - author.lastName as authorLastName, - author.firstName || ' ' || author.lastName as authorName, - author.firstName || ' ' || author.lastName as authorFullName, - (author.firstName || ' ' || author.lastName) || ' ' || (address.street || ', ' || address.city) as authorFullNameWithAddress, - address.street || ', ' || address.city as authorAdrText, - years_between( author.sortCode, author.sortCode ) as authorAge, - DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) as authorAgeNativePG, - - ( DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) ) * 7 as authorAgeInDogYears - }` - expect(JSON.parse(JSON.stringify(query))).to.deep.equal(expected) - }) - - it('wildcard select from subquery', () => { - let query = cqn4sql(cds.ql`SELECT from ( SELECT FROM booksCalc.Simple { * } )`, model) - const expected = cds.ql` - SELECT from ( - SELECT from booksCalc.Simple as $S - left join booksCalc.Simple as my on my.ID = $S.my_ID - { - $S.ID, - $S.name, - $S.my_ID, - my.name as myName - } - ) as __select__ { - __select__.ID, - __select__.name, - __select__.my_ID, - __select__.myName - } - ` - expect(JSON.parse(JSON.stringify(query))).to.deep.equal(expected) - }) - - it('wildcard select from subquery + join relevant path expression', () => { - let query = cqn4sql( - cds.ql`SELECT from ( SELECT FROM booksCalc.Simple { * } ) { - my.name as otherName - }`, - model, - ) - const expected = cds.ql` - SELECT from ( - SELECT from booksCalc.Simple as $S - left join booksCalc.Simple as my2 on my2.ID = $S.my_ID - { - $S.ID, - $S.name, - $S.my_ID, - my2.name as myName - } - ) as __select__ left join booksCalc.Simple as my on my.ID = __select__.my_ID { - my.name as otherName - } - ` - expect(JSON.parse(JSON.stringify(query))).to.deep.equal(expected) - }) - - it('replacement for calculated element is considered for wildcard expansion', () => { - let query = cqn4sql( - cds.ql`SELECT from booksCalc.Books as Books { *, volume as ctitle } excluding { length, width, height, stock, price, youngAuthorName }`, - model, - ) - const expected = cds.ql`SELECT from booksCalc.Books as Books - left outer join booksCalc.Authors as author on author.ID = Books.author_ID - left outer join booksCalc.Addresses as address on address.ID = author.address_ID - { - Books.ID, - Books.title, - Books.author_ID, - - Books.stock as stock2, - (Books.length * Books.width) * Books.height as ctitle, - - Books.areaS, - - Books.length * Books.width as area, - (Books.length * Books.width) * Books.height as volume, - Books.stock * ((Books.length * Books.width) * Books.height) as storageVolume, - - author.lastName as authorLastName, - author.firstName || ' ' || author.lastName as authorName, - author.firstName || ' ' || author.lastName as authorFullName, - (author.firstName || ' ' || author.lastName) || ' ' || (address.street || ', ' || address.city) as authorFullNameWithAddress, - address.street || ', ' || address.city as authorAdrText, - years_between( author.sortCode, author.sortCode ) as authorAge, - DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) as authorAgeNativePG, - - ( DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) ) * 7 as authorAgeInDogYears - }` - expect(JSON.parse(JSON.stringify(query))).to.deep.equal(expected) - }) - - it('calculated elements are join relevant and share same join node', () => { - // make sure that if a join was already calculated, - // calc elements which can use the same join have - // their table aliases properly rewritten to the already - // existing join node - let query = cqn4sql( - cds.ql` - SELECT from booksCalc.Books as Books { - youngAuthorName, - authorLastName, - authorName, - authorFullName, - authorAge - } - `, - model, - ) - - const expected = cds.ql` - SELECT from booksCalc.Books as Books - left outer join booksCalc.Authors as author on author.ID = Books.author_ID - and years_between(author.dateOfBirth, author.dateOfDeath) < 50 - left outer join booksCalc.Authors as author2 on author2.ID = Books.author_ID - { - author.firstName || ' ' || author.lastName as youngAuthorName, - author2.lastName as authorLastName, - author2.firstName || ' ' || author2.lastName as authorName, - author2.firstName || ' ' || author2.lastName as authorFullName, - years_between( author2.sortCode, author2.sortCode ) as authorAge - }` - expect(query).to.deep.equal(expected) - }) - - it('calculated elements are join relevant and share same join node indirect', () => { - // make sure that if a join was already calculated, - // calc elements which can use the same join have - // their table aliases properly rewritten to the already - // existing join node - let query = cqn4sql( - cds.ql` - SELECT from booksCalc.Authors as Authors { - books.youngAuthorName, - books.authorLastName, - books.authorName, - books.authorFullName, - books.authorAge - } where books.youngAuthorName = 'King' - `, - model, - ) - - const expected = cds.ql` - SELECT from booksCalc.Authors as Authors - left outer join booksCalc.Books as books on books.author_ID = Authors.ID - left outer join booksCalc.Authors as author on author.ID = books.author_ID - and years_between(author.dateOfBirth, author.dateOfDeath) < 50 - left outer join booksCalc.Authors as author2 on author2.ID = books.author_ID - { - author.firstName || ' ' || author.lastName as books_youngAuthorName, - author2.lastName as books_authorLastName, - author2.firstName || ' ' || author2.lastName as books_authorName, - author2.firstName || ' ' || author2.lastName as books_authorFullName, - years_between( author2.sortCode, author2.sortCode ) as books_authorAge - } where (author.firstName || ' ' || author.lastName) = 'King'` - expect(query).to.deep.equal(expected) - }) - - it('calculated elements are join relevant and share same join node indirect back and forth', () => { - // make sure that if a join was already calculated, - // calc elements which can use the same join have - // their table aliases properly rewritten to the already - // existing join node - let query = cqn4sql( - cds.ql` - SELECT from booksCalc.Authors as Authors { - books.author.books.youngAuthorName, - books.author.books.authorLastName, - books.author.books.authorName, - books.author.books.authorFullName, - books.author.books.authorAge - } where books.author.books.youngAuthorName = 'King' - `, - model, - ) - - const expected = cds.ql` - SELECT from booksCalc.Authors as Authors - left outer join booksCalc.Books as books on books.author_ID = Authors.ID - left outer join booksCalc.Authors as author on author.ID = books.author_ID - left outer join booksCalc.Books as books2 on books2.author_ID = author.ID - left outer join booksCalc.Authors as author2 on author2.ID = books2.author_ID - and years_between(author2.dateOfBirth, author2.dateOfDeath) < 50 - left outer join booksCalc.Authors as author3 on author3.ID = books2.author_ID - { - author2.firstName || ' ' || author2.lastName as books_author_books_youngAuthorName, - author3.lastName as books_author_books_authorLastName, - author3.firstName || ' ' || author3.lastName as books_author_books_authorName, - author3.firstName || ' ' || author3.lastName as books_author_books_authorFullName, - years_between( author3.sortCode, author3.sortCode ) as books_author_books_authorAge - } where (author2.firstName || ' ' || author2.lastName) = 'King'` - expect(query).to.deep.equal(expected) - }) - - it('exists cannot leverage calculated elements which ends in string', () => { - // at the leaf of a where exists path, there must be an association - expect(() => cqn4sql(cds.ql`SELECT from booksCalc.Books { ID } where exists youngAuthorName`, model)).to.throw( - `Expecting path “youngAuthorName” following “EXISTS” predicate to end with association/composition, found “cds.String”`, - ) - }) - it('exists cannot leverage calculated elements which is an expression', () => { - // at the leaf of a where exists path, there must be an association - expect(() => cqn4sql(cds.ql`SELECT from booksCalc.Books { ID } where exists authorFullName`, model)).to.throw( - `Expecting path “authorFullName” following “EXISTS” predicate to end with association/composition, found “expression”`, - ) - }) - it('exists cannot leverage calculated elements w/ path expressions', () => { - // at the leaf of a where exists path, there must be an association - expect(() => - cqn4sql(cds.ql`SELECT from booksCalc.Books { ID } where exists author.books.youngAuthorName`, model), - ).to.throw('Expecting path “author.books.youngAuthorName” following “EXISTS” predicate to end with association/composition, found “cds.String”') - }) - - it('exists cannot leverage calculated elements in CASE', () => { - expect(() => - cqn4sql( - cds.ql`SELECT from booksCalc.Books { - ID, - case when exists youngAuthorName then 'yes' - else 'no' - end as x - }`, - model, - ), - ).to.throw('Expecting path “youngAuthorName” following “EXISTS” predicate to end with association/composition, found “cds.String”') - }) - - it('scoped query cannot leverage calculated elements', () => { - // at the leaf of a where exists path, there must be an association - expect(() => cqn4sql(cds.ql`SELECT from booksCalc.Books:youngAuthorName { ID }`, model)).to.throw( - 'Query source must be a an entity or an association', - ) - }) - - it('via wildcard in expand subquery include complex calc element', () => { - let query = cqn4sql( - cds.ql` - SELECT from booksCalc.Authors as Authors { - books { * } excluding { length, width, height, stock, price} - } - `, - model, - ) - - const expected = cds.ql`SELECT from booksCalc.Authors as Authors { - ( - SELECT from booksCalc.Books as $b - left outer join booksCalc.Authors as author on author.ID = $b.author_ID - and years_between(author.dateOfBirth, author.dateOfDeath) < 50 - left outer join booksCalc.Authors as author2 on author2.ID = $b.author_ID - left outer join booksCalc.Addresses as address on address.ID = author2.address_ID - { - $b.ID, - $b.title, - $b.author_ID, - - $b.stock as stock2, - substring($b.title, 3, $b.stock) as ctitle, - - $b.areaS, - - $b.length * $b.width as area, - ($b.length * $b.width) * $b.height as volume, - $b.stock * (($b.length * $b.width) * $b.height) as storageVolume, - - author.firstName || ' ' || author.lastName as youngAuthorName, - author2.lastName as authorLastName, - author2.firstName || ' ' || author2.lastName as authorName, - author2.firstName || ' ' || author2.lastName as authorFullName, - (author2.firstName || ' ' || author2.lastName) || ' ' || (address.street || ', ' || address.city) as authorFullNameWithAddress, - address.street || ', ' || address.city as authorAdrText, - - years_between( author2.sortCode, author2.sortCode ) as authorAge, - DATE_PART('year', author2.dateOfDeath) - DATE_PART('year', author2.dateOfBirth) as authorAgeNativePG, - - ( DATE_PART('year', author2.dateOfDeath) - DATE_PART('year', author2.dateOfBirth) ) * 7 as authorAgeInDogYears - } where Authors.ID = $b.author_ID - ) as books - }` - expect(JSON.parse(JSON.stringify(query))).to.deep.equal(expected) - }) - it('via wildcard in expand subquery', () => { - let query = cqn4sql( - cds.ql` - SELECT from booksCalc.Authors as Authors { - books { * } excluding { length, width, height, stock, price, youngAuthorName} - } - `, - model, - ) - - const expected = cds.ql`SELECT from booksCalc.Authors as Authors { - ( - SELECT from booksCalc.Books as $b - left outer join booksCalc.Authors as author on author.ID = $b.author_ID - left outer join booksCalc.Addresses as address on address.ID = author.address_ID - { - $b.ID, - $b.title, - $b.author_ID, - - $b.stock as stock2, - substring($b.title, 3, $b.stock) as ctitle, - - $b.areaS, - - $b.length * $b.width as area, - ($b.length * $b.width) * $b.height as volume, - $b.stock * (($b.length * $b.width) * $b.height) as storageVolume, - - author.lastName as authorLastName, - author.firstName || ' ' || author.lastName as authorName, - author.firstName || ' ' || author.lastName as authorFullName, - (author.firstName || ' ' || author.lastName) || ' ' || (address.street || ', ' || address.city) as authorFullNameWithAddress, - address.street || ', ' || address.city as authorAdrText, - - years_between( author.sortCode, author.sortCode ) as authorAge, - DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) as authorAgeNativePG, - - ( DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) ) * 7 as authorAgeInDogYears - } where Authors.ID = $b.author_ID - ) as books - }` - expect(JSON.parse(JSON.stringify(query))).to.deep.equal(expected) - }) -}) - -describe('Unfolding calculated elements in other places', () => { - let model - beforeAll(async () => { - model = cds.model = await cds.load(__dirname + '/../bookshop/db/booksWithExpr').then(cds.linked) - }) - - it('in where', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID } where area < 13`, model) - const expected = cds.ql`SELECT from booksCalc.Books as Books { Books.ID } - where (Books.length * Books.width) < 13 - ` - expect(query).to.deep.equal(expected) - }) - - it('in the from clause', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books[area < 13] as Books { ID }`, model) - const expected = cds.ql`SELECT from booksCalc.Books as Books { Books.ID } - where (Books.length * Books.width) < 13 - ` - expect(query).to.deep.equal(expected) - }) - - it('in the from clause with a function', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Authors[age > 30] as Oldies { ID }`, model) - const expected = cds.ql` - SELECT from booksCalc.Authors as Oldies { Oldies.ID } - where years_between( Oldies.dateOfBirth, Oldies.dateOfDeath ) > 30 - ` - expect(query).to.deep.equal(expected) - }) - - it('in the from clause with a function in a scoped query', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Authors[age > 30]:books as BooksOfOldies { ID }`, model) - const expected = cds.ql` - SELECT from booksCalc.Books as BooksOfOldies { BooksOfOldies.ID } - where exists ( - select 1 from booksCalc.Authors as $A - where $A.ID = BooksOfOldies.author_ID - and years_between( $A.dateOfBirth, $A.dateOfDeath ) > 30 - ) - ` - expect(query).to.deep.equal(expected) - }) - it('in the from clause with a function in a scoped query, within another expression', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Authors[anotherFunc(age * (7+5)) + 42 > 'foo']:books as BooksOfOldies { ID }`, model) - const expected = cds.ql` - SELECT from booksCalc.Books as BooksOfOldies { BooksOfOldies.ID } - where exists ( - select 1 from booksCalc.Authors as $A - where $A.ID = BooksOfOldies.author_ID - and anotherFunc( years_between( $A.dateOfBirth, $A.dateOfDeath ) * (7 + 5) ) + 42 > 'foo' - ) - ` - expect(query).to.deep.equal(expected) - }) - - it.skip('in the from clause with a join relevant path', () => { - // TODO: infix filter at from leaf is only a regular where, - // we must not reject the join relevant path with: - // Error: Only foreign keys of “author” can be accessed in infix filter, but found “firstName” - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books[authorFullName = 'Brandon Sanderson'] as Books { ID }`, model) - const expected = cds.ql` - SELECT from booksCalc.Books as Books - left join booksCalc.Authors as author on author.ID = Books.author_ID - { Books.ID } - where (author.firstName || ' ' || author.lastName) = 'Brandon Sanderson' - ` - expect(query).to.deep.equal(expected) - }) - - it('in filter in where exists', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Authors { ID } where exists books[area < 13]`, model) - const expected = cds.ql`SELECT from booksCalc.Authors as $A { $A.ID } - where exists ( - select 1 from booksCalc.Books as $b where $b.author_ID = $A.ID - and ($b.length * $b.width) < 13 - ) - ` - expect(query).to.deep.equal(expected) - }) - - it('in group by & having', () => { - let query = cqn4sql( - cds.ql`SELECT from booksCalc.Books as Books { ID, sum(price) as tprice } - group by ctitle having ctitle like 'A%'`, - model, - ) - const expected = cds.ql`SELECT from booksCalc.Books as Books { - Books.ID, sum(Books.price) as tprice - } group by substring(Books.title, 3, Books.stock) - having substring(Books.title, 3, Books.stock) like 'A%' - ` - expect(query).to.deep.equal(expected) - }) - - it('in order by', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, title } order by ctitle`, model) - const expected = cds.ql`SELECT from booksCalc.Books as Books { - Books.ID, Books.title - } order by substring(Books.title, 3, Books.stock) - ` - expect(query).to.deep.equal(expected) - }) - - it('in filter in path in FROM', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Authors[name like 'A%'].books[storageVolume < 4] { ID }`, model) - const expected = cds.ql`SELECT from booksCalc.Books as $b { - $b.ID - } where exists (select 1 from booksCalc.Authors as $A - where $A.ID = $b.author_ID - and ($A.firstName || ' ' || $A.lastName) like 'A%') - and ($b.stock * (($b.length * $b.width) * $b.height)) < 4 - ` - expect(query).to.deep.equal(expected) - }) - - it('in a subquery', () => { - let query = cqn4sql( - cds.ql`SELECT from booksCalc.Books as Books { - ID, - (select from booksCalc.Authors as A { name } - where A.ID = Books.author.ID and A.IBAN = Books.area + Books.ctitle) as f - }`, - model, - ) - const expected = cds.ql`SELECT from booksCalc.Books as Books { - Books.ID, - (select from booksCalc.Authors as A { A.firstName || ' ' || A.lastName as name } - where A.ID = Books.author_ID - and ('DE' || A.checksum || A.sortCode || A.accountNumber) - = (Books.length * Books.width) + substring(Books.title, 3, Books.stock) - ) as f - }` - expect(query).to.deep.equal(expected) - }) - it('in a function, args are join relevant', () => { - let query = cqn4sql( - cds.ql`SELECT from booksCalc.Books as Books { - ID, - authorAge - }`, - model, - ) - const expected = cds.ql` - SELECT from booksCalc.Books as Books - left join booksCalc.Authors as author on author.ID = Books.author_ID - { - Books.ID, - years_between( author.sortCode, author.sortCode ) as authorAge - } - ` - expect(query).to.deep.equal(expected) - }) - it('calculated element has other calc element in infix filter', () => { - let query = cqn4sql( - cds.ql`SELECT from booksCalc.Books as Books { - ID, - youngAuthorName - }`, - model, - ) - const expected = cds.ql` - SELECT from booksCalc.Books as Books - left join booksCalc.Authors as author on author.ID = Books.author_ID - and years_between(author.dateOfBirth, author.dateOfDeath) < 50 - { - Books.ID, - author.firstName || ' ' || author.lastName as youngAuthorName - } - ` - expect(query).to.deep.equal(expected) - }) - it('in a subquery calc element is join relevant', () => { - let query = cqn4sql( - cds.ql`SELECT from booksCalc.Books as Books { - ID, - (select from booksCalc.Authors as A { books.title } - where A.ID = Books.author.ID and A.IBAN = Books.area + Books.ctitle) as f - }`, - model, - ) - const expected = cds.ql`SELECT from booksCalc.Books as Books { - Books.ID, - (select from booksCalc.Authors as A - left join booksCalc.Books as books2 on books2.author_ID = A.ID - { books2.title as books_title } - where A.ID = Books.author_ID - and ('DE' || A.checksum || A.sortCode || A.accountNumber) - = (Books.length * Books.width) + substring(Books.title, 3, Books.stock) - ) as f - }` - expect(query).to.deep.equal(expected) - }) - it('variable replacements are left untouched in calc element navigation', () => { - const q = cds.ql`SELECT from booksCalc.VariableReplacements as VariableReplacements { ID, authorAlive.firstName }` - const expected = cds.ql`SELECT from booksCalc.VariableReplacements as VariableReplacements - left join booksCalc.Authors as authorAlive on ( authorAlive.ID = VariableReplacements.author_ID ) - and ( authorAlive.dateOfBirth <= $now and authorAlive.dateOfDeath >= $now and $user.unknown.foo.bar = 'Bob' ) - { - VariableReplacements.ID, - authorAlive.firstName as authorAlive_firstName - }` - expect(cqn4sql(q, model)).to.deep.equal(expected) - }) - it('variable replacements are left untouched in calc elements via wildcard', () => { - const q = cds.ql`SELECT from booksCalc.VariableReplacements as VariableReplacements { * }` - const expected = cds.ql`SELECT from booksCalc.VariableReplacements as VariableReplacements - { - VariableReplacements.ID, - VariableReplacements.author_ID - }` - expect(cqn4sql(q, model)).to.deep.equal(expected) - }) - - it('with expand', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.VariableReplacements as VariableReplacements { ID, authorAlive { ID } }`, model) - const expected = cds.ql`SELECT from booksCalc.VariableReplacements as VariableReplacements { - VariableReplacements.ID, - ( - SELECT from booksCalc.Authors as $a - { - $a.ID, - } - where ($a.ID = VariableReplacements.author_ID) - and ( $a.dateOfBirth <= $now and $a.dateOfDeath >= $now and $user.unknown.foo.bar = 'Bob' ) - ) as authorAlive - }` - expect(JSON.parse(JSON.stringify(query))).to.deep.equal(expected) - }) -}) - -describe('Unfolding calculated elements ... misc', () => { - let model - beforeAll(async () => { - model = cds.model = await cds.load(__dirname + '/../bookshop/db/booksWithExpr').then(cds.linked) - }) - it('calculated element on-write (stored) is not unfolded', () => { - let query = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, areaS }`, model) - const expected = cds.ql`SELECT from booksCalc.Books as Books { Books.ID, Books.areaS }` - expect(query).to.deep.equal(expected) - }) -}) - -describe('Unfolding calculated elements and localized', () => { - let model - beforeAll(async () => { - model = cds.model = await cds.load(__dirname + '/../bookshop/db/booksWithExpr').then(cds.linked) - model = cds.compile.for.nodejs(model) - }) - - it('presence of localized element should not affect unfolding', () => { - const q = cds.ql`SELECT from booksCalc.LBooks as LBooks { ID, title, area }` - q.SELECT.localized = true - let query = cqn4sql(q, model) - const expected = cds.ql`SELECT from localized.booksCalc.LBooks as LBooks { - LBooks.ID, - LBooks.title, - LBooks.length * LBooks.width as area - }` - expected.SELECT.localized = true - expect(query).to.deep.equal(expected) - }) - - it('calculated element refers to localized element', () => { - const q = cds.ql`SELECT from booksCalc.LBooks as LBooks { ID, title, ctitle }` - q.SELECT.localized = true - let query = cqn4sql(q, model) - const expected = cds.ql`SELECT from localized.booksCalc.LBooks as LBooks { - LBooks.ID, - LBooks.title, - substring(LBooks.title, 3, 3) as ctitle - }` - expected.SELECT.localized = true - expect(query).to.deep.equal(expected) - }) - - it('calc elements with sub selects', () => { - const q = cds.ql`SELECT ID, area from (SELECT ID, area from booksCalc.Books)` - let query = cqn4sql(q, model) - const expected = cds.ql` - SELECT __select__.ID, __select__.area from ( - SELECT FROM booksCalc.Books as $B { - $B.ID, - $B.length * $B.width as area - } - ) AS __select__` - expect(query).to.deep.equal(expected) - }) - - it('calc elements with intermediate sub select star', () => { - const q = cds.ql`SELECT ID, area from (SELECT * FROM (SELECT ID, area from booksCalc.Books))` - let query = cqn4sql(q, model) - const expected = cds.ql` - SELECT __select__2.ID, __select__2.area from ( - SELECT __select__.ID, __select__.area FROM ( - SELECT FROM booksCalc.Books as $B { - $B.ID, - $B.length * $B.width as area - } - ) as __select__ - ) AS __select__2` - expect(query).to.deep.equal(expected) - }) -}) - -describe('calculated elements with exists accessed through association', () => { - let model - beforeAll(async () => { - model = await loadModel() - }) - - it('accessing parent calc element with exists through association produces correct JOIN', () => { - const transformed = cqn4sql( - cds.ql`SELECT from existsInCalcElement.Tasks as Tasks { ID, isUserNotMember }`, - model, - ) - const expected = cds.ql`SELECT from existsInCalcElement.Tasks as Tasks - left join existsInCalcElement.Projects as project on project.ID = Tasks.project_ID - { - Tasks.ID, - (case when (case when not exists ( - SELECT 1 from existsInCalcElement.Members as $m - where $m.project_ID = project.ID - and $m.userID = $user.id - ) then true else false end) = true then true else false end) as isUserNotMember - }` - expectCqn(transformed).to.equal(expected) - }) - - it('parent calc element with exists directly on entity needs no JOIN', () => { - const transformed = cqn4sql( - cds.ql`SELECT from existsInCalcElement.Projects as Projects { ID, isUserNotMember }`, - model, - ) - const expected = cds.ql`SELECT from existsInCalcElement.Projects as Projects - { - Projects.ID, - (case when not exists ( - SELECT 1 from existsInCalcElement.Members as $m - where $m.project_ID = Projects.ID - and $m.userID = $user.id - ) then true else false end) as isUserNotMember - }` - expectCqn(transformed).to.equal(expected) - }) -}) - -describe('calculated elements in draft enabled entities', () => { - let model - beforeAll(async () => { - model = cds.model = cds.compile.for.nodejs(await cds.load(__dirname + '/../bookshop/srv/calc-elem-service')) - }) - - it('keeps param: false for query against active entries', () => { - const transformed = cqn4sql( - cds.ql`SELECT from CalcService.Orders as Orders { ID, expensive }`, - model, - ) - const expected = cds.ql`SELECT from CalcService.Orders as Orders { - Orders.ID, - case when Orders.amount > 10 then 1 else 0 end as expensive - }` - expectCqn(transformed).to.equal(expected) - expect(transformed.SELECT.columns[1].xpr[4].param).to.be.false - expect(transformed.SELECT.columns[1].xpr[6].param).to.be.false - expect(transformed.SELECT.columns[1].xpr[8].param).to.be.false - }) - - it('keeps param: false for query against draft entries', () => { - const transformed = cqn4sql( - cds.ql`SELECT from CalcService.Orders.drafts as Orders { ID, expensive }`, - model, - ) - const expected = cds.ql`SELECT from CalcService.Orders.drafts as Orders { - Orders.ID, - case when Orders.amount > 10 then 1 else 0 end as expensive - }` - expectCqn(transformed).to.equal(expected) - expect(transformed.SELECT.columns[1].xpr[4].param).to.be.false - expect(transformed.SELECT.columns[1].xpr[6].param).to.be.false - expect(transformed.SELECT.columns[1].xpr[8].param).to.be.false - }) -}) diff --git a/db-service/test/cqn4sql/calculated-elements.test/basics.spec.js b/db-service/test/cqn4sql/calculated-elements.test/basics.spec.js new file mode 100644 index 000000000..948350ed5 --- /dev/null +++ b/db-service/test/cqn4sql/calculated-elements.test/basics.spec.js @@ -0,0 +1,65 @@ +'use strict' + +const cds = require('@sap/cds') +const { loadModel } = require('../helpers/model') +const { expectCqn } = require('../helpers/expectCqn') + +let cqn4sql = require('../../../lib/cqn4sql') + +describe('Unfolding calculated elements - basics', () => { + before(async () => { + const model = await loadModel() + const orig = cqn4sql + cqn4sql = (q, m) => orig(q, m ?? model) + }) + + it('simple reference', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, stock2 }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books { + Books.ID, + Books.stock as stock2 + }` + expectCqn(transformed).to.equal(expected) + }) + + it('simple val', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Authors as Authors { ID, IBAN }`) + const expected = cds.ql` + SELECT from booksCalc.Authors as Authors { + Authors.ID, + 'DE' || Authors.checksum || Authors.sortCode || Authors.accountNumber as IBAN + }` + expectCqn(transformed).to.equal(expected) + }) + + it('directly', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, area }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books { + Books.ID, + Books.length * Books.width as area + }` + expectCqn(transformed).to.equal(expected) + }) + + it('in expression', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, stock * area as f }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books { + Books.ID, + Books.stock * ( Books.length * Books.width ) as f + }` + expectCqn(transformed).to.equal(expected) + }) + + it('in function', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, round(area, 2) as f }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books { + Books.ID, + round(Books.length * Books.width, 2) as f + }` + expectCqn(transformed).to.equal(expected) + }) +}) diff --git a/db-service/test/cqn4sql/calculated-elements.test/drafts.spec.js b/db-service/test/cqn4sql/calculated-elements.test/drafts.spec.js new file mode 100644 index 000000000..937e02ba4 --- /dev/null +++ b/db-service/test/cqn4sql/calculated-elements.test/drafts.spec.js @@ -0,0 +1,39 @@ +'use strict' + +const cds = require('@sap/cds') +const { loadModel } = require('../helpers/model') +const { expectCqn, expect } = require('../helpers/expectCqn') + +let cqn4sql = require('../../../lib/cqn4sql') + +describe('calculated elements in draft enabled entities', () => { + before(async () => { + const model = await loadModel({ flat: true }, [__dirname + '/../../bookshop/srv/calc-elem-service']) + const orig = cqn4sql + cqn4sql = (q, m) => orig(q, m ?? model) + }) + + it('keeps param: false for query against active entries', () => { + const transformed = cqn4sql(cds.ql`SELECT from CalcService.Orders as Orders { ID, expensive }`) + const expected = cds.ql`SELECT from CalcService.Orders as Orders { + Orders.ID, + case when Orders.amount > 10 then 1 else 0 end as expensive + }` + expectCqn(transformed).to.equal(expected) + expect(transformed.SELECT.columns[1].xpr[4].param).to.be.false + expect(transformed.SELECT.columns[1].xpr[6].param).to.be.false + expect(transformed.SELECT.columns[1].xpr[8].param).to.be.false + }) + + it('keeps param: false for query against draft entries', () => { + const transformed = cqn4sql(cds.ql`SELECT from CalcService.Orders.drafts as Orders { ID, expensive }`) + const expected = cds.ql`SELECT from CalcService.Orders.drafts as Orders { + Orders.ID, + case when Orders.amount > 10 then 1 else 0 end as expensive + }` + expectCqn(transformed).to.equal(expected) + expect(transformed.SELECT.columns[1].xpr[4].param).to.be.false + expect(transformed.SELECT.columns[1].xpr[6].param).to.be.false + expect(transformed.SELECT.columns[1].xpr[8].param).to.be.false + }) +}) diff --git a/db-service/test/cqn4sql/calculated-elements.test/exists.spec.js b/db-service/test/cqn4sql/calculated-elements.test/exists.spec.js new file mode 100644 index 000000000..44655b5cb --- /dev/null +++ b/db-service/test/cqn4sql/calculated-elements.test/exists.spec.js @@ -0,0 +1,87 @@ +'use strict' + +const cds = require('@sap/cds') +const { loadModel } = require('../helpers/model') +const { expectCqn, expect } = require('../helpers/expectCqn') + +let cqn4sql = require('../../../lib/cqn4sql') + +describe('Unfolding calculated elements - exists', () => { + before(async () => { + const model = await loadModel() + const orig = cqn4sql + cqn4sql = (q, m) => orig(q, m ?? model) + }) + + it('exists cannot leverage calculated elements which ends in string', () => { + // at the leaf of a where exists path, there must be an association + expect(() => cqn4sql(cds.ql`SELECT from booksCalc.Books { ID } where exists youngAuthorName`)).to.throw( + `Expecting path “youngAuthorName” following “EXISTS” predicate to end with association/composition, found “cds.String”`, + ) + }) + + it('exists cannot leverage calculated elements which is an expression', () => { + // at the leaf of a where exists path, there must be an association + expect(() => cqn4sql(cds.ql`SELECT from booksCalc.Books { ID } where exists authorFullName`)).to.throw( + `Expecting path “authorFullName” following “EXISTS” predicate to end with association/composition, found “expression”`, + ) + }) + + it('exists cannot leverage calculated elements w/ path expressions', () => { + // at the leaf of a where exists path, there must be an association + expect(() => + cqn4sql(cds.ql`SELECT from booksCalc.Books { ID } where exists author.books.youngAuthorName`), + ).to.throw('Expecting path “author.books.youngAuthorName” following “EXISTS” predicate to end with association/composition, found “cds.String”') + }) + + it('exists cannot leverage calculated elements in CASE', () => { + expect(() => + cqn4sql(cds.ql`SELECT from booksCalc.Books { + ID, + case when exists youngAuthorName then 'yes' + else 'no' + end as x + }`), + ).to.throw('Expecting path “youngAuthorName” following “EXISTS” predicate to end with association/composition, found “cds.String”') + }) + + it('scoped query cannot leverage calculated elements', () => { + // at the leaf of a where exists path, there must be an association + expect(() => cqn4sql(cds.ql`SELECT from booksCalc.Books:youngAuthorName { ID }`)).to.throw( + 'Query source must be a an entity or an association', + ) + }) + + describe('calculated elements with exists accessed through association', () => { + it('accessing parent calc element with exists through association produces correct JOIN', () => { + const transformed = cqn4sql(cds.ql`SELECT from existsInCalcElement.Tasks as Tasks { ID, isUserNotMember }`) + const expected = cds.ql` + SELECT from existsInCalcElement.Tasks as Tasks + left join existsInCalcElement.Projects as project on project.ID = Tasks.project_ID + { + Tasks.ID, + (case when (case when not exists ( + SELECT 1 from existsInCalcElement.Members as $m + where $m.project_ID = project.ID + and $m.userID = $user.id + ) then true else false end) = true then true else false end) as isUserNotMember + }` + expectCqn(transformed).to.equal(expected) + }) + + it('parent calc element with exists directly on entity needs no JOIN', () => { + const transformed = cqn4sql(cds.ql`SELECT from existsInCalcElement.Projects as Projects { ID, isUserNotMember }`) + const expected = cds.ql` + SELECT from existsInCalcElement.Projects as Projects + { + Projects.ID, + (case when not exists ( + SELECT 1 from existsInCalcElement.Members as $m + where $m.project_ID = Projects.ID + and $m.userID = $user.id + ) then true else false end) as isUserNotMember + }` + expectCqn(transformed).to.equal(expected) + }) + }) +}) diff --git a/db-service/test/cqn4sql/calculated-elements.test/filters.spec.js b/db-service/test/cqn4sql/calculated-elements.test/filters.spec.js new file mode 100644 index 000000000..62ee9ab3b --- /dev/null +++ b/db-service/test/cqn4sql/calculated-elements.test/filters.spec.js @@ -0,0 +1,87 @@ +'use strict' + +const cds = require('@sap/cds') +const { loadModel } = require('../helpers/model') +const { expectCqn } = require('../helpers/expectCqn') + +let cqn4sql = require('../../../lib/cqn4sql') + +describe('Unfolding calculated elements - filters', () => { + before(async () => { + const model = await loadModel() + const orig = cqn4sql + cqn4sql = (q, m) => orig(q, m ?? model) + }) + + it('in filter', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Authors as Authors { ID, books[area >17].title }`) + // intermediate: + // SELECT from booksCalc.Authors { ID, books[(length * width) > 1].title } + const expected = cds.ql` + SELECT from booksCalc.Authors as Authors + left outer join booksCalc.Books as books on books.author_ID = Authors.ID + and (books.length * books.width) > 17 + { + Authors.ID, + books.title as books_title + }` + expectCqn(transformed).to.equal(expected) + }) + + it('calc elem contains association with filter', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Authors as Authors { ID, addressTextFilter }`) + // intermediate: + // SELECT from booksCalc.Authors { ID, address[number * 2 > 17].{street || ', ' || city} } + const expected = cds.ql` + SELECT from booksCalc.Authors as Authors + left outer join booksCalc.Addresses as address on address.ID = Authors.address_ID + and (address.number * 2) > 17 + { + Authors.ID, + address.street || ', ' || address.city as addressTextFilter + }` + expectCqn(transformed).to.equal(expected) + }) + + it('calculated element has other calc element in infix filter', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { + ID, + youngAuthorName + }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left join booksCalc.Authors as author on author.ID = Books.author_ID + and years_between(author.dateOfBirth, author.dateOfDeath) < 50 + { + Books.ID, + author.firstName || ' ' || author.lastName as youngAuthorName + } + ` + expectCqn(transformed).to.equal(expected) + }) + + it('in filter in path in FROM', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Authors[name like 'A%'].books[storageVolume < 4] { ID }`) + const expected = cds.ql` + SELECT from booksCalc.Books as $b { + $b.ID + } where exists (select 1 from booksCalc.Authors as $A + where $A.ID = $b.author_ID + and ($A.firstName || ' ' || $A.lastName) like 'A%') + and ($b.stock * (($b.length * $b.width) * $b.height)) < 4 + ` + expectCqn(transformed).to.equal(expected) + }) + + it('in filter in where exists', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Authors { ID } where exists books[area < 13]`) + const expected = cds.ql` + SELECT from booksCalc.Authors as $A { $A.ID } + where exists ( + select 1 from booksCalc.Books as $b where $b.author_ID = $A.ID + and ($b.length * $b.width) < 13 + ) + ` + expectCqn(transformed).to.equal(expected) + }) +}) diff --git a/db-service/test/cqn4sql/calculated-elements.test/functions.spec.js b/db-service/test/cqn4sql/calculated-elements.test/functions.spec.js new file mode 100644 index 000000000..af0768729 --- /dev/null +++ b/db-service/test/cqn4sql/calculated-elements.test/functions.spec.js @@ -0,0 +1,83 @@ +'use strict' + +const cds = require('@sap/cds') +const { loadModel } = require('../helpers/model') +const { expectCqn } = require('../helpers/expectCqn') + +let cqn4sql = require('../../../lib/cqn4sql') + +describe('Unfolding calculated elements - functions', () => { + before(async () => { + const model = await loadModel() + const orig = cqn4sql + cqn4sql = (q, m) => orig(q, m ?? model) + }) + + it('in function with named param', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Authors as Authors { ID, ageNamedParams as f }`) + const expected = cds.ql` + SELECT from booksCalc.Authors as Authors { + Authors.ID, + years_between(DOB => Authors.dateOfBirth, DOD => Authors.dateOfDeath) as f + }` + expectCqn(transformed).to.equal(expected) + }) + + it('calc elem is function', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, ctitle }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books { + Books.ID, + substring(Books.title, 3, Books.stock) as ctitle + }` + expectCqn(transformed).to.equal(expected) + }) + + it('calc elem is xpr with multiple functions as args', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, authorAgeNativePG }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left join booksCalc.Authors as author on author.ID = Books.author_ID + { + Books.ID, + DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) as authorAgeNativePG + }` + expectCqn(transformed).to.equal(expected) + }) + + it('calc elem is xpr with nested xpr which has multiple functions as args', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, authorAgeInDogYears }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left join booksCalc.Authors as author on author.ID = Books.author_ID + { + Books.ID, + ( DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) ) * 7 as authorAgeInDogYears + }` + expectCqn(transformed).to.equal(expected) + }) + + it('calc elem is xpr with multiple functions as args - back and forth', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, author.books.authorAgeNativePG }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left join booksCalc.Authors as author on author.ID = Books.author_ID + left join booksCalc.Books as books2 on books2.author_ID = author.ID + left join booksCalc.Authors as author2 on author2.ID = books2.author_ID + { + Books.ID, + DATE_PART('year', author2.dateOfDeath) - DATE_PART('year', author2.dateOfBirth) as author_books_authorAgeNativePG + }` + expectCqn(transformed).to.equal(expected) + }) + + it('calc elem is function, nested in direct expression', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, ctitle || title as f }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books { + Books.ID, + substring(Books.title, 3, Books.stock) || Books.title as f + }` + expectCqn(transformed).to.equal(expected) + }) +}) diff --git a/db-service/test/cqn4sql/calculated-elements.test/joins.spec.js b/db-service/test/cqn4sql/calculated-elements.test/joins.spec.js new file mode 100644 index 000000000..e7ce4f9b9 --- /dev/null +++ b/db-service/test/cqn4sql/calculated-elements.test/joins.spec.js @@ -0,0 +1,217 @@ +'use strict' + +const cds = require('@sap/cds') +const { loadModel } = require('../helpers/model') +const { expectCqn } = require('../helpers/expectCqn') + +let cqn4sql = require('../../../lib/cqn4sql') + +describe('Unfolding calculated elements - joins', () => { + before(async () => { + const model = await loadModel() + const orig = cqn4sql + cqn4sql = (q, m) => orig(q, m ?? model) + }) + + it('via an association path', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, author.name }`) + // revisit: alias follows our "regular" naming scheme -> ref.join('_') + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left outer join booksCalc.Authors as author on author.ID = Books.author_ID { + Books.ID, + author.firstName || ' ' || author.lastName as author_name + }` + expectCqn(transformed).to.equal(expected) + }) + + it('via an association in columns and where', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, author.name } where author.name like '%Bro%'`) + // revisit: alias follows our "regular" naming scheme -> ref.join('_') + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left outer join booksCalc.Authors as author on author.ID = Books.author_ID { + Books.ID, + author.firstName || ' ' || author.lastName as author_name + } where (author.firstName || ' ' || author.lastName) like '%Bro%'` + expectCqn(transformed).to.equal(expected) + }) + + it('via an association path, nested in direct expression', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, substring(author.name, 2, stock) as f }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left outer join booksCalc.Authors as author on author.ID = Books.author_ID { + Books.ID, + substring(author.firstName || ' ' || author.lastName, 2, Books.stock) as f + }` + expectCqn(transformed).to.equal(expected) + }) + + it('via two association paths', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Authors as Authors { ID, books[stock<5].area, books[stock>5].area as a2}`) + const expected = cds.ql` + SELECT from booksCalc.Authors as Authors + left outer join booksCalc.Books as books on books.author_ID = Authors.ID and books.stock < 5 + left outer join booksCalc.Books as books2 on books2.author_ID = Authors.ID and books2.stock > 5 + { + Authors.ID, + books.length * books.width as books_area, + books2.length * books2.width as a2 + }` + expectCqn(transformed).to.equal(expected) + }) + + it('calc elem contains association', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, authorName, authorLastName }`) + // intermediate: + // SELECT from booksCalc.Books { ID, author.name, author.lastName } + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left outer join booksCalc.Authors as author on author.ID = Books.author_ID + { + Books.ID, + author.firstName || ' ' || author.lastName as authorName, + author.lastName as authorLastName + }` + expectCqn(transformed).to.equal(expected) + }) + + it('calc elem contains associations in xpr', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, authorFullName }`) + // intermediate: + // SELECT from booksCalc.Books { ID, author.name, author.lastName } + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left outer join booksCalc.Authors as author on author.ID = Books.author_ID + { + Books.ID, + author.firstName || ' ' || author.lastName as authorFullName, + }` + expectCqn(transformed).to.equal(expected) + }) + + it('calc elem contains association, nested', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, authorAdrText }`) + // intermediate: + // SELECT from booksCalc.Books { ID, author.address.{street || ', ' || city} } + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left outer join booksCalc.Authors as author on author.ID = Books.author_ID + left outer join booksCalc.Addresses as address on address.ID = author.address_ID + { + Books.ID, + address.street || ', ' || address.city as authorAdrText + }` + expectCqn(transformed).to.equal(expected) + }) + + it('in a function, args are join relevant', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { + ID, + authorAge + }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left join booksCalc.Authors as author on author.ID = Books.author_ID + { + Books.ID, + years_between( author.sortCode, author.sortCode ) as authorAge + } + ` + expectCqn(transformed).to.equal(expected) + }) + + describe('join node reuse', () => { + it('calculated elements are join relevant and share same join node', () => { + // make sure that if a join was already calculated, + // calc elements which can use the same join have + // their table aliases properly rewritten to the already + // existing join node + const transformed = cqn4sql(cds.ql` + SELECT from booksCalc.Books as Books { + youngAuthorName, + authorLastName, + authorName, + authorFullName, + authorAge + } + `) + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left outer join booksCalc.Authors as author on author.ID = Books.author_ID + and years_between(author.dateOfBirth, author.dateOfDeath) < 50 + left outer join booksCalc.Authors as author2 on author2.ID = Books.author_ID + { + author.firstName || ' ' || author.lastName as youngAuthorName, + author2.lastName as authorLastName, + author2.firstName || ' ' || author2.lastName as authorName, + author2.firstName || ' ' || author2.lastName as authorFullName, + years_between( author2.sortCode, author2.sortCode ) as authorAge + }` + expectCqn(transformed).to.equal(expected) + }) + + it('calculated elements are join relevant and share same join node indirect', () => { + // make sure that if a join was already calculated, + // calc elements which can use the same join have + // their table aliases properly rewritten to the already + // existing join node + const transformed = cqn4sql(cds.ql` + SELECT from booksCalc.Authors as Authors { + books.youngAuthorName, + books.authorLastName, + books.authorName, + books.authorFullName, + books.authorAge + } where books.youngAuthorName = 'King' + `) + const expected = cds.ql` + SELECT from booksCalc.Authors as Authors + left outer join booksCalc.Books as books on books.author_ID = Authors.ID + left outer join booksCalc.Authors as author on author.ID = books.author_ID + and years_between(author.dateOfBirth, author.dateOfDeath) < 50 + left outer join booksCalc.Authors as author2 on author2.ID = books.author_ID + { + author.firstName || ' ' || author.lastName as books_youngAuthorName, + author2.lastName as books_authorLastName, + author2.firstName || ' ' || author2.lastName as books_authorName, + author2.firstName || ' ' || author2.lastName as books_authorFullName, + years_between( author2.sortCode, author2.sortCode ) as books_authorAge + } where (author.firstName || ' ' || author.lastName) = 'King'` + expectCqn(transformed).to.equal(expected) + }) + + it('calculated elements are join relevant and share same join node indirect back and forth', () => { + // make sure that if a join was already calculated, + // calc elements which can use the same join have + // their table aliases properly rewritten to the already + // existing join node + const transformed = cqn4sql(cds.ql` + SELECT from booksCalc.Authors as Authors { + books.author.books.youngAuthorName, + books.author.books.authorLastName, + books.author.books.authorName, + books.author.books.authorFullName, + books.author.books.authorAge + } where books.author.books.youngAuthorName = 'King' + `) + const expected = cds.ql` + SELECT from booksCalc.Authors as Authors + left outer join booksCalc.Books as books on books.author_ID = Authors.ID + left outer join booksCalc.Authors as author on author.ID = books.author_ID + left outer join booksCalc.Books as books2 on books2.author_ID = author.ID + left outer join booksCalc.Authors as author2 on author2.ID = books2.author_ID + and years_between(author2.dateOfBirth, author2.dateOfDeath) < 50 + left outer join booksCalc.Authors as author3 on author3.ID = books2.author_ID + { + author2.firstName || ' ' || author2.lastName as books_author_books_youngAuthorName, + author3.lastName as books_author_books_authorLastName, + author3.firstName || ' ' || author3.lastName as books_author_books_authorName, + author3.firstName || ' ' || author3.lastName as books_author_books_authorFullName, + years_between( author3.sortCode, author3.sortCode ) as books_author_books_authorAge + } where (author2.firstName || ' ' || author2.lastName) = 'King'` + expectCqn(transformed).to.equal(expected) + }) + }) +}) diff --git a/db-service/test/cqn4sql/calculated-elements.test/localized.spec.js b/db-service/test/cqn4sql/calculated-elements.test/localized.spec.js new file mode 100644 index 000000000..5976d5d54 --- /dev/null +++ b/db-service/test/cqn4sql/calculated-elements.test/localized.spec.js @@ -0,0 +1,70 @@ +'use strict' + +const cds = require('@sap/cds') +const { loadModel } = require('../helpers/model') +const { expectCqn, expect } = require('../helpers/expectCqn') + +let cqn4sql = require('../../../lib/cqn4sql') + +describe('Unfolding calculated elements and localized', () => { + before(async () => { + cds.model = await loadModel({}, [__dirname + '/../../bookshop/db/booksWithExpr']) + const model = cds.compile.for.nodejs(cds.model) + const orig = cqn4sql + cqn4sql = (q, m) => orig(q, m ?? model) + }) + + it('presence of localized element should not affect unfolding', () => { + const q = cds.ql`SELECT from booksCalc.LBooks as LBooks { ID, title, area }` + q.SELECT.localized = true + const transformed = cqn4sql(q) + const expected = cds.ql`SELECT from localized.booksCalc.LBooks as LBooks { + LBooks.ID, + LBooks.title, + LBooks.length * LBooks.width as area + }` + expectCqn(transformed).to.equal(expected) + expect(transformed.SELECT.localized).to.be.true + }) + + it('calculated element refers to localized element', () => { + const q = cds.ql`SELECT from booksCalc.LBooks as LBooks { ID, title, ctitle }` + q.SELECT.localized = true + const transformed = cqn4sql(q) + const expected = cds.ql`SELECT from localized.booksCalc.LBooks as LBooks { + LBooks.ID, + LBooks.title, + substring(LBooks.title, 3, 3) as ctitle + }` + expectCqn(transformed).to.equal(expected) + expect(transformed.SELECT.localized).to.be.true + }) + + it('calc elements with sub selects', () => { + const q = cds.ql`SELECT ID, area from (SELECT ID, area from booksCalc.Books)` + const transformed = cqn4sql(q) + const expected = cds.ql` + SELECT __select__.ID, __select__.area from ( + SELECT FROM booksCalc.Books as $B { + $B.ID, + $B.length * $B.width as area + } + ) AS __select__` + expectCqn(transformed).to.equal(expected) + }) + + it('calc elements with intermediate sub select star', () => { + const q = cds.ql`SELECT ID, area from (SELECT * FROM (SELECT ID, area from booksCalc.Books))` + const transformed = cqn4sql(q) + const expected = cds.ql` + SELECT __select__2.ID, __select__2.area from ( + SELECT __select__.ID, __select__.area FROM ( + SELECT FROM booksCalc.Books as $B { + $B.ID, + $B.length * $B.width as area + } + ) as __select__ + ) AS __select__2` + expectCqn(transformed).to.equal(expected) + }) +}) diff --git a/db-service/test/cqn4sql/calculated-elements.test/nested-projections.spec.js b/db-service/test/cqn4sql/calculated-elements.test/nested-projections.spec.js new file mode 100644 index 000000000..32a6983cd --- /dev/null +++ b/db-service/test/cqn4sql/calculated-elements.test/nested-projections.spec.js @@ -0,0 +1,193 @@ +'use strict' + +const cds = require('@sap/cds') +const { loadModel } = require('../helpers/model') +const { expectCqn } = require('../helpers/expectCqn') + +let cqn4sql = require('../../../lib/cqn4sql') + +describe('Unfolding calculated elements - inline / expand / subquery', () => { + before(async () => { + const model = await loadModel() + const orig = cqn4sql + cqn4sql = (q, m) => orig(q, m ?? model) + }) + + it('in inline', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, author.{name, IBAN } }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left outer join booksCalc.Authors as author on author.ID = Books.author_ID + { + Books.ID, + author.firstName || ' ' || author.lastName as author_name, + 'DE' || author.checksum || author.sortCode || author.accountNumber as author_IBAN + }` + expectCqn(transformed).to.equal(expected) + }) + + it('in inline back and forth', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, author.books.author.{name, IBAN } }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left outer join booksCalc.Authors as author on author.ID = Books.author_ID + left outer join booksCalc.Books as books2 on books2.author_ID = author.ID + left outer join booksCalc.Authors as author2 on author2.ID = books2.author_ID + { + Books.ID, + author2.firstName || ' ' || author2.lastName as author_books_author_name, + 'DE' || author2.checksum || author2.sortCode || author2.accountNumber as author_books_author_IBAN + }` + expectCqn(transformed).to.equal(expected) + }) + + it('in subquery, using the same calc element - not join relevant in subquery', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { + ID, + ( + SELECT from booksCalc.Authors as Authors { + name, + IBAN, + addressText + } + ) as sub, + author.books.author.{name, IBAN, addressText }, + }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left outer join booksCalc.Authors as author on author.ID = Books.author_ID + left outer join booksCalc.Books as books2 on books2.author_ID = author.ID + left outer join booksCalc.Authors as author2 on author2.ID = books2.author_ID + left outer join booksCalc.Addresses as address on address.ID = author2.address_ID + { + Books.ID, + ( + SELECT from booksCalc.Authors as Authors + left outer join booksCalc.Addresses as address2 on address2.ID = Authors.address_ID { + Authors.firstName || ' ' || Authors.lastName as name, + 'DE' || Authors.checksum || Authors.sortCode || Authors.accountNumber as IBAN, + address2.street || ', ' || address2.city as addressText + } + ) as sub, + author2.firstName || ' ' || author2.lastName as author_books_author_name, + 'DE' || author2.checksum || author2.sortCode || author2.accountNumber as author_books_author_IBAN, + address.street || ', ' || address.city as author_books_author_addressText + }` + expectCqn(transformed).to.equal(expected) + }) + + it('in inline, 2 assocs', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, author.{name, addressText } }`) + // intermediate: + // SELECT from booksCalc.Authors { ID, author.{firstName || ' ' || lastName, address.{street || ', ' || city}}} } + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left outer join booksCalc.Authors as author on author.ID = Books.author_ID + left outer join booksCalc.Addresses as address on address.ID = author.address_ID + { + Books.ID, + author.firstName || ' ' || author.lastName as author_name, + address.street || ', ' || address.city as author_addressText + }` + expectCqn(transformed).to.equal(expected) + }) + + it('in expand (to-one)', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, author {name, IBAN } }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books + { + Books.ID, + ( + SELECT from booksCalc.Authors as $a { + $a.firstName || ' ' || $a.lastName as name, + 'DE' || $a.checksum || $a.sortCode || $a.accountNumber as IBAN + } where Books.author_ID = $a.ID + ) as author + }` + expectCqn(transformed).to.equal(expected) + }) + + it('in expand (to-one), 2 assocs', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, author {name, addressText } }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books + { + Books.ID, + ( + SELECT from booksCalc.Authors as $a + left join booksCalc.Addresses as address on address.ID = $a.address_ID + { + $a.firstName || ' ' || $a.lastName as name, + address.street || ', ' || address.city as addressText + } where Books.author_ID = $a.ID + ) as author + }` + expectCqn(transformed).to.equal(expected) + }) + + it('expand and inline target same calc element', () => { + const transformed = cqn4sql( + cds.ql`SELECT from booksCalc.Books as Books { ID, author.{name, addressText }, author {name, addressText } }`, + ) + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left outer join booksCalc.Authors as author on author.ID = Books.author_ID + left outer join booksCalc.Addresses as address on address.ID = author.address_ID + { + Books.ID, + author.firstName || ' ' || author.lastName as author_name, + address.street || ', ' || address.city as author_addressText, + ( + SELECT from booksCalc.Authors as $a + left join booksCalc.Addresses as address2 on address2.ID = $a.address_ID + { + $a.firstName || ' ' || $a.lastName as name, + address2.street || ', ' || address2.city as addressText + } where Books.author_ID = $a.ID + ) as author + }` + expectCqn(transformed).to.equal(expected) + }) + + it('expand and inline target same calc element inverted', () => { + const transformed = cqn4sql( + cds.ql`SELECT from booksCalc.Books as Books { ID, author {name, addressText }, author.{name, addressText } }`, + ) + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left outer join booksCalc.Authors as author on author.ID = Books.author_ID + left outer join booksCalc.Addresses as address on address.ID = author.address_ID + { + Books.ID, + ( + SELECT from booksCalc.Authors as $a + left join booksCalc.Addresses as address2 on address2.ID = $a.address_ID + { + $a.firstName || ' ' || $a.lastName as name, + address2.street || ', ' || address2.city as addressText + } where Books.author_ID = $a.ID + ) as author, + author.firstName || ' ' || author.lastName as author_name, + address.street || ', ' || address.city as author_addressText + }` + expectCqn(transformed).to.equal(expected) + }) + + it('in expand (to-many)', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Authors { ID, books { ID, area, volume } }`) + const expected = cds.ql` + SELECT from booksCalc.Authors as $A { + $A.ID, + ( + SELECT from booksCalc.Books as $b + { + $b.ID, + $b.length * $b.width as area, + ($b.length * $b.width) * $b.height as volume, + } where $A.ID = $b.author_ID + ) as books + }` + expectCqn(transformed).to.equal(expected) + }) +}) diff --git a/db-service/test/cqn4sql/calculated-elements.test/nesting.spec.js b/db-service/test/cqn4sql/calculated-elements.test/nesting.spec.js new file mode 100644 index 000000000..822c26e4f --- /dev/null +++ b/db-service/test/cqn4sql/calculated-elements.test/nesting.spec.js @@ -0,0 +1,55 @@ +'use strict' + +const cds = require('@sap/cds') +const { loadModel } = require('../helpers/model') +const { expectCqn } = require('../helpers/expectCqn') + +let cqn4sql = require('../../../lib/cqn4sql') + +describe('Unfolding calculated elements - nesting (calc element references calc element)', () => { + before(async () => { + const model = await loadModel() + const orig = cqn4sql + cqn4sql = (q, m) => orig(q, m ?? model) + }) + + it('nested calc elems', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, volume, storageVolume }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books { + Books.ID, + (Books.length * Books.width) * Books.height as volume, + Books.stock * ((Books.length * Books.width) * Books.height) as storageVolume + }` + expectCqn(transformed).to.equal(expected) + }) + + it('nested calc elems, nested in direct expression', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, storageVolume / volume as f }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books { + Books.ID, + (Books.stock * ((Books.length * Books.width) * Books.height)) + / ((Books.length * Books.width) * Books.height) as f + }` + expectCqn(transformed).to.equal(expected) + }) + + it('calc elem contains other calculated element in xpr with nested joins', () => { + const transformed = cqn4sql( + cds.ql`SELECT from booksCalc.Books as Books { ID, authorFullNameWithAddress } where authorFullNameWithAddress = 'foo'`, + ) + // intermediate: + // SELECT from booksCalc.Books { ID, author.name, author.lastName } + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left outer join booksCalc.Authors as author on author.ID = Books.author_ID + left outer join booksCalc.Addresses as address on address.ID = author.address_ID + { + Books.ID, + (author.firstName || ' ' || author.lastName) || ' ' || (address.street || ', ' || address.city) + as authorFullNameWithAddress, + } where ( (author.firstName || ' ' || author.lastName) || ' ' || (address.street || ', ' || address.city) ) = 'foo'` + expectCqn(transformed).to.equal(expected) + }) +}) diff --git a/db-service/test/cqn4sql/calculated-elements.test/query-clauses.spec.js b/db-service/test/cqn4sql/calculated-elements.test/query-clauses.spec.js new file mode 100644 index 000000000..4161a4bbf --- /dev/null +++ b/db-service/test/cqn4sql/calculated-elements.test/query-clauses.spec.js @@ -0,0 +1,142 @@ +'use strict' + +const cds = require('@sap/cds') +const { loadModel } = require('../helpers/model') +const { expectCqn } = require('../helpers/expectCqn') + +let cqn4sql = require('../../../lib/cqn4sql') + +describe('Unfolding calculated elements - query clauses (where, from, group by, having, order by, subquery)', () => { + before(async () => { + const model = await loadModel() + const orig = cqn4sql + cqn4sql = (q, m) => orig(q, m ?? model) + }) + + it('in where', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID } where area < 13`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books { Books.ID } + where (Books.length * Books.width) < 13 + ` + expectCqn(transformed).to.equal(expected) + }) + + it('in the from clause', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books[area < 13] as Books { ID }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books { Books.ID } + where (Books.length * Books.width) < 13 + ` + expectCqn(transformed).to.equal(expected) + }) + + it('in the from clause with a function', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Authors[age > 30] as Oldies { ID }`) + const expected = cds.ql` + SELECT from booksCalc.Authors as Oldies { Oldies.ID } + where years_between( Oldies.dateOfBirth, Oldies.dateOfDeath ) > 30 + ` + expectCqn(transformed).to.equal(expected) + }) + + it('in the from clause with a function in a scoped query', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Authors[age > 30]:books as BooksOfOldies { ID }`) + const expected = cds.ql` + SELECT from booksCalc.Books as BooksOfOldies { BooksOfOldies.ID } + where exists ( + select 1 from booksCalc.Authors as $A + where $A.ID = BooksOfOldies.author_ID + and years_between( $A.dateOfBirth, $A.dateOfDeath ) > 30 + ) + ` + expectCqn(transformed).to.equal(expected) + }) + + it('in the from clause with a function in a scoped query, within another expression', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Authors[anotherFunc(age * (7+5)) + 42 > 'foo']:books as BooksOfOldies { ID }`) + const expected = cds.ql` + SELECT from booksCalc.Books as BooksOfOldies { BooksOfOldies.ID } + where exists ( + select 1 from booksCalc.Authors as $A + where $A.ID = BooksOfOldies.author_ID + and anotherFunc( years_between( $A.dateOfBirth, $A.dateOfDeath ) * (7 + 5) ) + 42 > 'foo' + ) + ` + expectCqn(transformed).to.equal(expected) + }) + + it.skip('in the from clause with a join relevant path', () => { + // TODO: infix filter at from leaf is only a regular where, + // we must not reject the join relevant path with: + // Error: Only foreign keys of “author” can be accessed in infix filter, but found “firstName” + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books[authorFullName = 'Brandon Sanderson'] as Books { ID }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left join booksCalc.Authors as author on author.ID = Books.author_ID + { Books.ID } + where (author.firstName || ' ' || author.lastName) = 'Brandon Sanderson' + ` + expectCqn(transformed).to.equal(expected) + }) + + it('in group by & having', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, sum(price) as tprice } + group by ctitle having ctitle like 'A%'`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books { + Books.ID, sum(Books.price) as tprice + } group by substring(Books.title, 3, Books.stock) + having substring(Books.title, 3, Books.stock) like 'A%' + ` + expectCqn(transformed).to.equal(expected) + }) + + it('in order by', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, title } order by ctitle`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books { + Books.ID, Books.title + } order by substring(Books.title, 3, Books.stock) + ` + expectCqn(transformed).to.equal(expected) + }) + + it('in a subquery', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { + ID, + (select from booksCalc.Authors as A { name } + where A.ID = Books.author.ID and A.IBAN = Books.area + Books.ctitle) as f + }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books { + Books.ID, + (select from booksCalc.Authors as A { A.firstName || ' ' || A.lastName as name } + where A.ID = Books.author_ID + and ('DE' || A.checksum || A.sortCode || A.accountNumber) + = (Books.length * Books.width) + substring(Books.title, 3, Books.stock) + ) as f + }` + expectCqn(transformed).to.equal(expected) + }) + + it('in a subquery calc element is join relevant', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { + ID, + (select from booksCalc.Authors as A { books.title } + where A.ID = Books.author.ID and A.IBAN = Books.area + Books.ctitle) as f + }`) + const expected = cds.ql` + SELECT from booksCalc.Books as Books { + Books.ID, + (select from booksCalc.Authors as A + left join booksCalc.Books as books2 on books2.author_ID = A.ID + { books2.title as books_title } + where A.ID = Books.author_ID + and ('DE' || A.checksum || A.sortCode || A.accountNumber) + = (Books.length * Books.width) + substring(Books.title, 3, Books.stock) + ) as f + }` + expectCqn(transformed).to.equal(expected) + }) +}) diff --git a/db-service/test/cqn4sql/calculated-elements.test/stored.spec.js b/db-service/test/cqn4sql/calculated-elements.test/stored.spec.js new file mode 100644 index 000000000..f7c69e5d6 --- /dev/null +++ b/db-service/test/cqn4sql/calculated-elements.test/stored.spec.js @@ -0,0 +1,21 @@ +'use strict' + +const cds = require('@sap/cds') +const { loadModel } = require('../helpers/model') +const { expectCqn } = require('../helpers/expectCqn') + +let cqn4sql = require('../../../lib/cqn4sql') + +describe('Stored (on-write) calculated elements', () => { + before(async () => { + const model = await loadModel() + const orig = cqn4sql + cqn4sql = (q, m) => orig(q, m ?? model) + }) + + it('calculated element on-write (stored) is not unfolded', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Books as Books { ID, areaS }`) + const expected = cds.ql`SELECT from booksCalc.Books as Books { Books.ID, Books.areaS }` + expectCqn(transformed).to.equal(expected) + }) +}) diff --git a/db-service/test/cqn4sql/calculated-elements.test/ternary.spec.js b/db-service/test/cqn4sql/calculated-elements.test/ternary.spec.js new file mode 100644 index 000000000..d52c7bfcf --- /dev/null +++ b/db-service/test/cqn4sql/calculated-elements.test/ternary.spec.js @@ -0,0 +1,53 @@ +'use strict' + +const cds = require('@sap/cds') +const { loadModel } = require('../helpers/model') +const { expectCqn } = require('../helpers/expectCqn') + +let cqn4sql = require('../../../lib/cqn4sql') + +describe('Unfolding calculated elements - ternary / CASE', () => { + before(async () => { + const model = await loadModel() + const orig = cqn4sql + cqn4sql = (q, m) => orig(q, m ?? model) + }) + + it('in ternary', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Ternary as Ternary { ID, nestedTernary }`) + const expected = cds.ql` + SELECT from booksCalc.Ternary as Ternary + left join booksCalc.Books as book on book.ID = Ternary.book_ID + { + Ternary.ID, + (case when 1 > 0 then 1 else (case when book.stock > 10 then Ternary.value else 3 end) end) as nestedTernary + }` + expectCqn(transformed).to.equal(expected) + }) + + it('calcualted element in nested ternary', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Ternary as Ternary { ID, calculatedElementInNestedTernary }`) + const expected = cds.ql` + SELECT from booksCalc.Ternary as Ternary + left join booksCalc.Books as book on book.ID = Ternary.book_ID + left join booksCalc.Authors as author on author.ID = book.author_ID + { + Ternary.ID, + (case when 1 > 0 then 1 else (case when book.stock > (case when 1 > 0 then 1 else (case when book.stock > years_between(author.dateOfBirth, author.dateOfDeath) then Ternary.value else 3 end) end) then Ternary.value else 3 end) end) as calculatedElementInNestedTernary + }` + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + expectCqn(transformed).to.equal(expected) + }) + + it('list in ternary', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.Ternary as Ternary { ID, nestedTernaryWithNestedXpr }`) + const expected = cds.ql` + SELECT from booksCalc.Ternary as Ternary + left join booksCalc.Books as book on book.ID = Ternary.book_ID + { + Ternary.ID, + (case when 1 > 0 then 1 else (case when ( (10 + book.stock) in (1, 2, 3, 4) ) then Ternary.value else 3 end) end) as nestedTernaryWithNestedXpr + }` + expectCqn(transformed).to.equal(expected) + }) +}) diff --git a/db-service/test/cqn4sql/calculated-elements.test/variable-replacements.spec.js b/db-service/test/cqn4sql/calculated-elements.test/variable-replacements.spec.js new file mode 100644 index 000000000..4b73265c3 --- /dev/null +++ b/db-service/test/cqn4sql/calculated-elements.test/variable-replacements.spec.js @@ -0,0 +1,56 @@ +'use strict' + +const cds = require('@sap/cds') +const { loadModel } = require('../helpers/model') +const { expectCqn } = require('../helpers/expectCqn') + +let cqn4sql = require('../../../lib/cqn4sql') + +describe('Unfolding calculated elements - variable replacements ($now, $user)', () => { + before(async () => { + const model = await loadModel() + const orig = cqn4sql + cqn4sql = (q, m) => orig(q, m ?? model) + }) + + it('variable replacements are left untouched in calc element navigation', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.VariableReplacements as VariableReplacements { ID, authorAlive.firstName }`) + const expected = cds.ql` + SELECT from booksCalc.VariableReplacements as VariableReplacements + left join booksCalc.Authors as authorAlive on ( authorAlive.ID = VariableReplacements.author_ID ) + and ( authorAlive.dateOfBirth <= $now and authorAlive.dateOfDeath >= $now and $user.unknown.foo.bar = 'Bob' ) + { + VariableReplacements.ID, + authorAlive.firstName as authorAlive_firstName + }` + expectCqn(transformed).to.equal(expected) + }) + + it('variable replacements are left untouched in calc elements via wildcard', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.VariableReplacements as VariableReplacements { * }`) + const expected = cds.ql` + SELECT from booksCalc.VariableReplacements as VariableReplacements + { + VariableReplacements.ID, + VariableReplacements.author_ID + }` + expectCqn(transformed).to.equal(expected) + }) + + it('with expand', () => { + const transformed = cqn4sql(cds.ql`SELECT from booksCalc.VariableReplacements as VariableReplacements { ID, authorAlive { ID } }`) + const expected = cds.ql` + SELECT from booksCalc.VariableReplacements as VariableReplacements { + VariableReplacements.ID, + ( + SELECT from booksCalc.Authors as $a + { + $a.ID, + } + where ($a.ID = VariableReplacements.author_ID) + and ( $a.dateOfBirth <= $now and $a.dateOfDeath >= $now and $user.unknown.foo.bar = 'Bob' ) + ) as authorAlive + }` + expectCqn(transformed).to.equal(expected) + }) +}) diff --git a/db-service/test/cqn4sql/calculated-elements.test/wildcard.spec.js b/db-service/test/cqn4sql/calculated-elements.test/wildcard.spec.js new file mode 100644 index 000000000..af9fa9e2b --- /dev/null +++ b/db-service/test/cqn4sql/calculated-elements.test/wildcard.spec.js @@ -0,0 +1,250 @@ +'use strict' + +const cds = require('@sap/cds') +const { loadModel } = require('../helpers/model') +const { expectCqn } = require('../helpers/expectCqn') + +let cqn4sql = require('../../../lib/cqn4sql') + +describe('Unfolding calculated elements - wildcard expansion', () => { + before(async () => { + const model = await loadModel() + const orig = cqn4sql + cqn4sql = (q, m) => orig(q, m ?? model) + }) + + it('via wildcard without columns', () => { + const transformed = cqn4sql( + cds.ql`SELECT from booksCalc.Books as Books excluding { length, width, height, stock, price, youngAuthorName }`, + ) + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left outer join booksCalc.Authors as author on author.ID = Books.author_ID + left outer join booksCalc.Addresses as address on address.ID = author.address_ID + { + Books.ID, + Books.title, + Books.author_ID, + + Books.stock as stock2, + substring(Books.title, 3, Books.stock) as ctitle, + + Books.areaS, + + Books.length * Books.width as area, + (Books.length * Books.width) * Books.height as volume, + Books.stock * ((Books.length * Books.width) * Books.height) as storageVolume, + + author.lastName as authorLastName, + author.firstName || ' ' || author.lastName as authorName, + author.firstName || ' ' || author.lastName as authorFullName, + (author.firstName || ' ' || author.lastName) || ' ' || (address.street || ', ' || address.city) as authorFullNameWithAddress, + address.street || ', ' || address.city as authorAdrText, + years_between( author.sortCode, author.sortCode ) as authorAge, + DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) as authorAgeNativePG, + + ( DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) ) * 7 as authorAgeInDogYears + }` + expectCqn(transformed).to.equal(expected) + }) + + it('via wildcard', () => { + const transformed = cqn4sql( + cds.ql`SELECT from booksCalc.Books as Books { * } excluding { length, width, height, stock, price, youngAuthorName}`, + ) + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left outer join booksCalc.Authors as author on author.ID = Books.author_ID + left outer join booksCalc.Addresses as address on address.ID = author.address_ID + { + Books.ID, + Books.title, + Books.author_ID, + + Books.stock as stock2, + substring(Books.title, 3, Books.stock) as ctitle, + + Books.areaS, + + Books.length * Books.width as area, + (Books.length * Books.width) * Books.height as volume, + Books.stock * ((Books.length * Books.width) * Books.height) as storageVolume, + + author.lastName as authorLastName, + author.firstName || ' ' || author.lastName as authorName, + author.firstName || ' ' || author.lastName as authorFullName, + (author.firstName || ' ' || author.lastName) || ' ' || (address.street || ', ' || address.city) as authorFullNameWithAddress, + address.street || ', ' || address.city as authorAdrText, + years_between( author.sortCode, author.sortCode ) as authorAge, + DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) as authorAgeNativePG, + + ( DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) ) * 7 as authorAgeInDogYears + }` + expectCqn(transformed).to.equal(expected) + }) + + it('wildcard select from subquery', () => { + const transformed = cqn4sql(cds.ql`SELECT from ( SELECT FROM booksCalc.Simple { * } )`) + const expected = cds.ql` + SELECT from ( + SELECT from booksCalc.Simple as $S + left join booksCalc.Simple as my on my.ID = $S.my_ID + { + $S.ID, + $S.name, + $S.my_ID, + my.name as myName + } + ) as __select__ { + __select__.ID, + __select__.name, + __select__.my_ID, + __select__.myName + }` + expectCqn(transformed).to.equal(expected) + }) + + it('wildcard select from subquery + join relevant path expression', () => { + const transformed = cqn4sql( + cds.ql`SELECT from ( SELECT FROM booksCalc.Simple { * } ) { + my.name as otherName + }`, + ) + const expected = cds.ql` + SELECT from ( + SELECT from booksCalc.Simple as $S + left join booksCalc.Simple as my2 on my2.ID = $S.my_ID + { + $S.ID, + $S.name, + $S.my_ID, + my2.name as myName + } + ) as __select__ left join booksCalc.Simple as my on my.ID = __select__.my_ID { + my.name as otherName + }` + expectCqn(transformed).to.equal(expected) + }) + + it('replacement for calculated element is considered for wildcard expansion', () => { + const transformed = cqn4sql( + cds.ql`SELECT from booksCalc.Books as Books { *, volume as ctitle } excluding { length, width, height, stock, price, youngAuthorName }`, + ) + const expected = cds.ql` + SELECT from booksCalc.Books as Books + left outer join booksCalc.Authors as author on author.ID = Books.author_ID + left outer join booksCalc.Addresses as address on address.ID = author.address_ID + { + Books.ID, + Books.title, + Books.author_ID, + + Books.stock as stock2, + (Books.length * Books.width) * Books.height as ctitle, + + Books.areaS, + + Books.length * Books.width as area, + (Books.length * Books.width) * Books.height as volume, + Books.stock * ((Books.length * Books.width) * Books.height) as storageVolume, + + author.lastName as authorLastName, + author.firstName || ' ' || author.lastName as authorName, + author.firstName || ' ' || author.lastName as authorFullName, + (author.firstName || ' ' || author.lastName) || ' ' || (address.street || ', ' || address.city) as authorFullNameWithAddress, + address.street || ', ' || address.city as authorAdrText, + years_between( author.sortCode, author.sortCode ) as authorAge, + DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) as authorAgeNativePG, + + ( DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) ) * 7 as authorAgeInDogYears + }` + expectCqn(transformed).to.equal(expected) + }) + + it('via wildcard in expand subquery include complex calc element', () => { + const transformed = cqn4sql(cds.ql` + SELECT from booksCalc.Authors as Authors { + books { * } excluding { length, width, height, stock, price} + } + `) + const expected = cds.ql` + SELECT from booksCalc.Authors as Authors { + ( + SELECT from booksCalc.Books as $b + left outer join booksCalc.Authors as author on author.ID = $b.author_ID + and years_between(author.dateOfBirth, author.dateOfDeath) < 50 + left outer join booksCalc.Authors as author2 on author2.ID = $b.author_ID + left outer join booksCalc.Addresses as address on address.ID = author2.address_ID + { + $b.ID, + $b.title, + $b.author_ID, + + $b.stock as stock2, + substring($b.title, 3, $b.stock) as ctitle, + + $b.areaS, + + $b.length * $b.width as area, + ($b.length * $b.width) * $b.height as volume, + $b.stock * (($b.length * $b.width) * $b.height) as storageVolume, + + author.firstName || ' ' || author.lastName as youngAuthorName, + author2.lastName as authorLastName, + author2.firstName || ' ' || author2.lastName as authorName, + author2.firstName || ' ' || author2.lastName as authorFullName, + (author2.firstName || ' ' || author2.lastName) || ' ' || (address.street || ', ' || address.city) as authorFullNameWithAddress, + address.street || ', ' || address.city as authorAdrText, + + years_between( author2.sortCode, author2.sortCode ) as authorAge, + DATE_PART('year', author2.dateOfDeath) - DATE_PART('year', author2.dateOfBirth) as authorAgeNativePG, + + ( DATE_PART('year', author2.dateOfDeath) - DATE_PART('year', author2.dateOfBirth) ) * 7 as authorAgeInDogYears + } where Authors.ID = $b.author_ID + ) as books + }` + expectCqn(transformed).to.equal(expected) + }) + + it('via wildcard in expand subquery', () => { + const transformed = cqn4sql(cds.ql` + SELECT from booksCalc.Authors as Authors { + books { * } excluding { length, width, height, stock, price, youngAuthorName} + } + `) + const expected = cds.ql` + SELECT from booksCalc.Authors as Authors { + ( + SELECT from booksCalc.Books as $b + left outer join booksCalc.Authors as author on author.ID = $b.author_ID + left outer join booksCalc.Addresses as address on address.ID = author.address_ID + { + $b.ID, + $b.title, + $b.author_ID, + + $b.stock as stock2, + substring($b.title, 3, $b.stock) as ctitle, + + $b.areaS, + + $b.length * $b.width as area, + ($b.length * $b.width) * $b.height as volume, + $b.stock * (($b.length * $b.width) * $b.height) as storageVolume, + + author.lastName as authorLastName, + author.firstName || ' ' || author.lastName as authorName, + author.firstName || ' ' || author.lastName as authorFullName, + (author.firstName || ' ' || author.lastName) || ' ' || (address.street || ', ' || address.city) as authorFullNameWithAddress, + address.street || ', ' || address.city as authorAdrText, + + years_between( author.sortCode, author.sortCode ) as authorAge, + DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) as authorAgeNativePG, + + ( DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) ) * 7 as authorAgeInDogYears + } where Authors.ID = $b.author_ID + ) as books + }` + expectCqn(transformed).to.equal(expected) + }) +})