From cee25e33cf289592a87779cfa34dddc53e467676 Mon Sep 17 00:00:00 2001 From: Patrice Bender Date: Thu, 26 Oct 2023 16:49:15 +0200 Subject: [PATCH] fix(calculated elements): path expressions in `func.args` within `xpr` (#321) the `args` of the `func` have not been considered, now the correct `join` is produced --- db-service/lib/infer/index.js | 14 +++-- db-service/test/bookshop/db/booksWithExpr.cds | 5 ++ .../cds-infer/calculated-elements.test.js | 2 + .../test/cqn4sql/calculated-elements.test.js | 63 ++++++++++++++++--- 4 files changed, 72 insertions(+), 12 deletions(-) diff --git a/db-service/lib/infer/index.js b/db-service/lib/infer/index.js index b83e547ee..6810d0320 100644 --- a/db-service/lib/infer/index.js +++ b/db-service/lib/infer/index.js @@ -873,20 +873,26 @@ function infer(originalQuery, model = cds.context?.model || cds.model) { if (leafOfCalculatedElementRef.value) mergePathsIntoJoinTree(leafOfCalculatedElementRef.value, basePath) mergePathIfNecessary(basePath, arg) - } else if (arg.xpr) { - arg.xpr.forEach(step => { + } else if (arg.xpr || arg.args) { + const prop = arg.xpr ? 'xpr' : 'args' + arg[prop].forEach(step => { + const subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] } if (step.ref) { - const subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] } step.$refLinks.forEach((link, i) => { const { definition } = link if (definition.value) { - mergePathsIntoJoinTree(definition.value) + mergePathsIntoJoinTree(definition.value, subPath) } else { subPath.$refLinks.push(link) subPath.ref.push(step.ref[i]) } }) mergePathIfNecessary(subPath, step) + } else if (step.args || step.xpr) { + const nestedProp = step.xpr ? 'xpr' : 'args' + step[nestedProp].forEach(a => { + mergePathsIntoJoinTree(a, subPath) + }) } }) } diff --git a/db-service/test/bookshop/db/booksWithExpr.cds b/db-service/test/bookshop/db/booksWithExpr.cds index b037a764a..c21ab9caf 100644 --- a/db-service/test/bookshop/db/booksWithExpr.cds +++ b/db-service/test/bookshop/db/booksWithExpr.cds @@ -32,10 +32,15 @@ entity Books { authorAdrText = author.addressText; authorAge: Integer = years_between( author.sortCode, author.sortCode ); + authorAgeNativePG: Integer = DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth); + + // calculated element is `xpr` which has subsequent `xpr` + authorAgeInDogYears: Integer = ( DATE_PART('year', author.dateOfDeath) - DATE_PART('year', author.dateOfBirth) ) * 7; } entity Authors { key ID : Integer; + firstName : String; lastName : String; diff --git a/db-service/test/cds-infer/calculated-elements.test.js b/db-service/test/cds-infer/calculated-elements.test.js index 0faa6690c..abec3665a 100644 --- a/db-service/test/cds-infer/calculated-elements.test.js +++ b/db-service/test/cds-infer/calculated-elements.test.js @@ -54,6 +54,8 @@ describe('Infer types of calculated elements in select list', () => { authorAdrText: Books.elements.authorAdrText, authorAge: Books.elements.authorAge, youngAuthorName: Books.elements.youngAuthorName, + authorAgeNativePG: Books.elements.authorAgeNativePG, + authorAgeInDogYears: Books.elements.authorAgeInDogYears, }) }) }) diff --git a/db-service/test/cqn4sql/calculated-elements.test.js b/db-service/test/cqn4sql/calculated-elements.test.js index 808fbd7a7..52bd6a0e3 100644 --- a/db-service/test/cqn4sql/calculated-elements.test.js +++ b/db-service/test/cqn4sql/calculated-elements.test.js @@ -63,6 +63,38 @@ describe('Unfolding calculated elements in select list', () => { }` expect(query).to.deep.equal(expected) }) + it('calc elem is xpr with multiple functions as args', () => { + let query = cqn4sql(CQL`SELECT from booksCalc.Books { ID, authorAgeNativePG }`, model) + const expected = CQL`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(CQL`SELECT from booksCalc.Books { ID, authorAgeInDogYears }`, model) + const expected = CQL`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(CQL`SELECT from booksCalc.Books { ID, author.books.authorAgeNativePG }`, model) + const expected = CQL`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(CQL`SELECT from booksCalc.Books { ID, ctitle || title as f }`, model) @@ -434,7 +466,10 @@ describe('Unfolding calculated elements in select list', () => { 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 + 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) }) @@ -466,7 +501,10 @@ describe('Unfolding calculated elements in select list', () => { 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 + 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) }) @@ -498,7 +536,10 @@ describe('Unfolding calculated elements in select list', () => { 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 + 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) }) @@ -616,9 +657,9 @@ describe('Unfolding calculated elements in select list', () => { it('exists cannot leverage calculated elements w/ path expressions', () => { // at the leaf of a where exists path, there must be an association // calc elements can't end in an association, hence this does not work, yet. - expect(() => cqn4sql(CQL`SELECT from booksCalc.Books { ID } where exists author.books.youngAuthorName`, model)).to.throw( - 'Calculated elements cannot be used in “exists” predicates in: “exists author.books.youngAuthorName”', - ) + expect(() => + cqn4sql(CQL`SELECT from booksCalc.Books { ID } where exists author.books.youngAuthorName`, model), + ).to.throw('Calculated elements cannot be used in “exists” predicates in: “exists author.books.youngAuthorName”') }) it('exists cannot leverage calculated elements in CASE', () => { @@ -681,7 +722,10 @@ describe('Unfolding calculated elements in select list', () => { (author2.firstName || ' ' || author2.lastName) || ' ' || (address.street || ', ' || address.city) as authorFullNameWithAddress, address.street || ', ' || address.city as authorAdrText, - years_between( author2.sortCode, author2.sortCode ) as authorAge + 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 = books.author_ID ) as books }` @@ -722,7 +766,10 @@ describe('Unfolding calculated elements in select list', () => { (author.firstName || ' ' || author.lastName) || ' ' || (address.street || ', ' || address.city) as authorFullNameWithAddress, address.street || ', ' || address.city as authorAdrText, - years_between( author.sortCode, author.sortCode ) as authorAge + 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 = books.author_ID ) as books }`