Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: assign artificial alias if selecting from anonymous subquery #608

Merged
merged 7 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion db-service/lib/cqn4sql.js
Original file line number Diff line number Diff line change
Expand Up @@ -1576,9 +1576,13 @@ function cqn4sql(originalQuery, model) {
return { transformedFrom }
} else if (from.SELECT) {
transformedFrom = transformSubquery(from)
if (from.as)
if (from.as) {
// preserve explicit TA
transformedFrom.as = from.as
} else {
// select from anonymous query, use artificial alias
transformedFrom.as = Object.keys(originalQuery.sources)[0]
}
return { transformedFrom }
} else {
return _transformFrom()
Expand Down
6 changes: 4 additions & 2 deletions db-service/lib/infer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,10 @@ function infer(originalQuery, model) {
} else if (from.args) {
from.args.forEach(a => inferTarget(a, querySources))
} else if (from.SELECT) {
infer(from, model) // we need the .elements in the sources
querySources[from.as || ''] = { definition: from }
const subqueryInFrom = infer(from, model) // we need the .elements in the sources
// if no explicit alias is provided, we make up one
const subqueryAlias = from.as || subqueryInFrom.joinTree.addNextAvailableTableAlias('__select__', subqueryInFrom.outerQueries)
querySources[subqueryAlias] = { definition: from }
} else if (typeof from === 'string') {
// TODO: Create unique alias, what about duplicates?
const definition = getDefinition(from) || cds.error`"${from}" not found in the definitions of your model`
Expand Down
2 changes: 2 additions & 0 deletions db-service/lib/infer/join-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ class JoinTree {
i += 1 // skip first step which is table alias
}

// if no root node was found, the column is selected from a subquery
if(!node) return
while (i < col.ref.length) {
const step = col.ref[i]
const { where, args } = step
Expand Down
7 changes: 7 additions & 0 deletions db-service/test/bookshop/db/booksWithExpr.cds
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,10 @@ entity LBooks {
width : Decimal;
area : Decimal = length * width;
}

entity Simple {
key ID: Integer;
name: String;
my: Association to Simple;
myName: String = my.name;
}
49 changes: 49 additions & 0 deletions db-service/test/cqn4sql/calculated-elements.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,55 @@ describe('Unfolding calculated elements in select list', () => {
expect(JSON.parse(JSON.stringify(query))).to.deep.equal(expected)
})

it('wildcard select from subquery', () => {
let query = cqn4sql(
CQL`SELECT from ( SELECT FROM booksCalc.Simple { * } )`,
model,
)
const expected = CQL`
SELECT from (
SELECT from booksCalc.Simple as Simple
left join booksCalc.Simple as my on my.ID = Simple.my_ID
{
Simple.ID,
Simple.name,
Simple.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(
CQL`SELECT from ( SELECT FROM booksCalc.Simple { * } ) {
my.name as otherName
}`,
model,
)
const expected = CQL`
SELECT from (
SELECT from booksCalc.Simple as Simple
left join booksCalc.Simple as my2 on my2.ID = Simple.my_ID
{
Simple.ID,
Simple.name,
Simple.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(
CQL`SELECT from booksCalc.Books { *, volume as ctitle } excluding { length, width, height, stock, price, youngAuthorName }`,
Expand Down
87 changes: 66 additions & 21 deletions db-service/test/cqn4sql/table-alias.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,59 @@ describe('table alias access', () => {
expect(query).to.deep.equal(CQL`SELECT from bookshop.Books as Books { Books.ID }`)
})

it('omits alias for anonymous query which selects from other query', () => {
it('creates unique alias for anonymous query which selects from other query', () => {
let query = cqn4sql(CQL`SELECT from (SELECT from bookshop.Books { ID } )`, model)
expect(query).to.deep.equal(CQL`SELECT from (SELECT from bookshop.Books as Books { Books.ID }) { ID }`)
expect(query).to.deep.equal(
CQL`SELECT from (SELECT from bookshop.Books as Books { Books.ID }) as __select__ { __select__.ID }`,
)
})

it('the unique alias for anonymous query does not collide with user provided aliases', () => {
let query = cqn4sql(CQL`SELECT from (SELECT from bookshop.Books as __select__ { ID } )`, model)
expect(query).to.deep.equal(
CQL`SELECT from (SELECT from bookshop.Books as __select__ { __select__.ID }) as __select__2 { __select__2.ID }`,
)
})
it('the unique alias for anonymous query does not collide with user provided aliases in case of joins', () => {
let query = cqn4sql(
CQL`SELECT from (SELECT from bookshop.Books as __select__ { ID, author } ) { author.name }`,
model,
)
expect(query).to.deep.equal(CQL`
SELECT from (
SELECT from bookshop.Books as __select__ { __select__.ID, __select__.author_ID }
) as __select__2 left join bookshop.Authors as author on author.ID = __select__2.author_ID
{
author.name as author_name
}`)
})

it('the unique alias for anonymous query does not collide with user provided aliases nested', () => {
// author association bubbles up to the top query where the join finally is done
// --> note that the most outer query uses user defined __select__ alias
let query = cqn4sql(
CQL`
SELECT from (
SELECT from (
SELECT from bookshop.Books { ID, author }
)
) as __select__
{
__select__.author.name
}`,
model,
)
expect(query).to.deep.equal(
CQL`
SELECT from (
SELECT from (
SELECT from bookshop.Books as Books { Books.ID, Books.author_ID }
) as __select__2 { __select__2.ID, __select__2.author_ID }
) as __select__ left join bookshop.Authors as author on author.ID = __select__.author_ID
{
author.name as author_name
}`,
)
})

it('preserves table alias at field access', () => {
Expand Down Expand Up @@ -444,16 +494,12 @@ describe('table alias access', () => {
}
ORDER BY Books.title, Books.title`)
})
it('dont try to prepend table alias if we select from anonymous subquery', async () => {
it('prepend artificial table alias if we select from anonymous subquery', async () => {
const subquery = SELECT.localized.from('bookshop.SimpleBook').orderBy('title')
const query = SELECT.localized
.columns('ID', 'title', 'author')
.from(subquery)
.orderBy('title')
.groupBy('title')

const query = SELECT.localized.columns('ID', 'title', 'author').from(subquery).orderBy('title').groupBy('title')

query.SELECT.count = true

const res = cqn4sql(query, model)

const expected = CQL`
Expand All @@ -464,14 +510,14 @@ describe('table alias access', () => {
SimpleBook.author_ID
from bookshop.SimpleBook as SimpleBook
order by SimpleBook.title
)
) __select__
{
ID,
title,
author_ID
__select__.ID,
__select__.title,
__select__.author_ID
}
group by title
order by title
group by __select__.title
order by __select__.title
`
expect(JSON.parse(JSON.stringify(res))).to.deep.equal(expected)
})
Expand Down Expand Up @@ -561,7 +607,6 @@ describe('table alias access', () => {
{ SimpleBook.ID, SimpleBook.title, SimpleBook.author_ID } order by author.name`
expect(query).to.deep.equal(expected)
})

})

describe('replace usage of implicit aliases in subqueries', () => {
Expand Down Expand Up @@ -836,7 +881,7 @@ describe('table alias access', () => {
}`,
)
})
it('no alias for function args or expressions on top of anonymous subquery', () => {
it('prepends unique alias for function args or expressions on top of anonymous subquery', () => {
let query = cqn4sql(
CQL`SELECT from ( SELECT from bookshop.Orders ) {
sum(ID) as foo,
Expand All @@ -849,9 +894,9 @@ describe('table alias access', () => {
SELECT from bookshop.Orders as Orders {
Orders.ID
}
) {
sum(ID) as foo,
ID + 42 as anotherFoo
) as __select__ {
sum(__select__.ID) as foo,
__select__.ID + 42 as anotherFoo
}`,
)
})
Expand Down
Loading