Skip to content

Commit

Permalink
Merge branch 'main' into sqlite/wasm
Browse files Browse the repository at this point in the history
  • Loading branch information
BobdenOs committed Apr 24, 2024
2 parents 3ebf2f4 + e1a7711 commit 1f5d2c7
Show file tree
Hide file tree
Showing 24 changed files with 306 additions and 123 deletions.
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
35 changes: 20 additions & 15 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 Expand Up @@ -180,13 +182,15 @@ function infer(originalQuery, model) {
// only fk access in infix filter
const nextStep = ref[1]?.id || ref[1]
// no unmanaged assoc in infix filter path
if (!expandOrExists && e.on)
throw new Error(
`"${e.name}" in path "${arg.ref.map(idOnly).join('.')}" must not be an unmanaged association`,
)
if (!expandOrExists && e.on) {
const err = `Unexpected unmanaged association “${e.name}” in filter expression of “${$baseLink.definition.name}”`
throw new Error(err)
}
// no non-fk traversal in infix filter
if (!expandOrExists && nextStep && !isForeignKeyOf(nextStep, e))
throw new Error(`Only foreign keys of "${e.name}" can be accessed in infix filter`)
throw new Error(
`Only foreign keys of “${e.name}” can be accessed in infix filter, but found “${nextStep}”`,
)
}
arg.$refLinks.push({ definition: e, target: definition })
// filter paths are flattened
Expand Down Expand Up @@ -615,11 +619,10 @@ function infer(originalQuery, model) {
if (!column.$refLinks[i].definition.target || danglingFilter)
throw new Error('A filter can only be provided when navigating along associations')
if (!column.expand) Object.defineProperty(column, 'isJoinRelevant', { value: true })
// books[exists genre[code='A']].title --> column is join relevant but inner exists filter is not
let skipJoinsForFilter = inExists
let skipJoinsForFilter = false
step.where.forEach(token => {
if (token === 'exists') {
// no joins for infix filters along `exists <path>`
// books[exists genre[code='A']].title --> column is join relevant but inner exists filter is not
skipJoinsForFilter = true
} else if (token.ref || token.xpr) {
inferQueryElement(token, false, column.$refLinks[i], {
Expand Down Expand Up @@ -698,13 +701,15 @@ function infer(originalQuery, model) {
// only fk access in infix filter
const nextStep = column.ref[i + 1]?.id || column.ref[i + 1]
// no unmanaged assoc in infix filter path
if (!inExists && assoc.on)
throw new Error(
`"${assoc.name}" in path "${column.ref.map(idOnly).join('.')}" must not be an unmanaged association`,
)
if (!inExists && assoc.on) {
const err = `Unexpected unmanaged association “${assoc.name}” in filter expression of “${$baseLink.definition.name}”`
throw new Error(err)
}
// no non-fk traversal in infix filter in non-exists path
if (nextStep && !assoc.on && !isForeignKeyOf(nextStep, assoc))
throw new Error(`Only foreign keys of "${assoc.name}" can be accessed in infix filter, not "${nextStep}"`)
throw new Error(
`Only foreign keys of “${assoc.name}” can be accessed in infix filter, but found “${nextStep}”`,
)
}
}
})
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;
}
13 changes: 11 additions & 2 deletions db-service/test/cds-infer/negative.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -392,9 +392,18 @@ describe('negative', () => {
describe('infix filters', () => {
it('rejects non fk traversal in infix filter in from', () => {
expect(() => _inferred(CQL`SELECT from bookshop.Books[author.name = 'Kurt']`, model)).to.throw(
/Only foreign keys of "author" can be accessed in infix filter/,
/Only foreign keys of author can be accessed in infix filter, but found “name”/,
)
})
it('rejects non fk traversal in infix filter in where exists', () => {
let query = CQL`SELECT from bookshop.Books where exists author.books[author.name = 'John Doe']`
expect(() => _inferred(query)).to.throw(/Only foreign keys of “author” can be accessed in infix filter, but found “name”/,) // revisit: better error location ""bookshop.Books:author"
})
it('rejects unmanaged traversal in infix filter in where exists', () => {
let query = CQL`SELECT from bookshop.Books where exists author.books[coAuthorUnmanaged.name = 'John Doe']`
expect(() => _inferred(query)).to.throw(/Unexpected unmanaged association “coAuthorUnmanaged” in filter expression of “books”/,) // revisit: better error location ""bookshop.Books:author"
})

it('rejects non fk traversal in infix filter in column', () => {
expect(() =>
_inferred(
Expand All @@ -403,7 +412,7 @@ describe('negative', () => {
}`,
model,
),
).to.throw(/Only foreign keys of "author" can be accessed in infix filter/)
).to.throw(/Only foreign keys of author can be accessed in infix filter/)
})
})

Expand Down
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
5 changes: 3 additions & 2 deletions db-service/test/cqn4sql/expand.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,13 +193,14 @@ describe('Unfold expands on associations to special subselects', () => {
// - they can return multiple rows
it('rejects unmanaged association in infix filter of expand path', () => {
expect(() => cqn4sql(CQL`SELECT from bookshop.Books { author[books.title = 'foo'] { name } }`, model)).to.throw(
/"books" in path "books.title" must not be an unmanaged association/,
/Unexpected unmanaged association “books” in filter expression of “author”/,
)

})
it('rejects non-fk access in infix filter of expand path', () => {
expect(() =>
cqn4sql(CQL`SELECT from bookshop.EStrucSibling { self[sibling.struc1 = 'foo'] { ID } }`, model),
).to.throw(/Only foreign keys of "sibling" can be accessed in infix filter/)
).to.throw(/Only foreign keys of sibling can be accessed in infix filter/)
})
it('unfold expand, one field', () => {
const q = CQL`SELECT from bookshop.Books {
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
6 changes: 3 additions & 3 deletions db-service/test/cqn4sql/where-exists.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,15 +218,15 @@ describe('EXISTS predicate in where', () => {
CQL`SELECT from bookshop.Authors { ID } WHERE EXISTS books[dedication.addressee.name = 'Hasso']`,
model,
),
).to.throw('Only foreign keys of "addressee" can be accessed in infix filter')
).to.throw('Only foreign keys of addressee can be accessed in infix filter')
})
it('MUST fail if following managed assoc in filter', () => {
expect(() =>
cqn4sql(
CQL`SELECT from bookshop.Authors { ID, books[dedication.addressee.name = 'Hasso'].dedication.addressee.name as Hasso }`,
model,
),
).to.throw('Only foreign keys of "addressee" can be accessed in infix filter')
).to.throw('Only foreign keys of addressee can be accessed in infix filter')
})

it('MUST handle simple where exists with multiple association and also with $self backlink', () => {
Expand Down Expand Up @@ -719,7 +719,7 @@ describe('EXISTS predicate in infix filter', () => {
`
expect(() => {
cqn4sql(q, cds.compile.for.nodejs(JSON.parse(JSON.stringify(model))))
}).to.throw(/Only foreign keys of "participant" can be accessed in infix filter/)
}).to.throw(/Only foreign keys of participant can be accessed in infix filter/)
})
})

Expand Down
7 changes: 4 additions & 3 deletions db-service/test/tsc/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export class TestCQN2SQL extends CQN2SQL {
super(context)
}

SELECT(cqn: any): string {
SELECT(cqn: JSON): string {
cqn
return ''
}
Expand Down Expand Up @@ -173,10 +173,10 @@ type sourceElementResult<TARGET extends source, COL extends sourceElements<TARGE
}

class SELECT<TARGET extends source, COLS extends sourceElements<TARGET>> {
from = <SRC extends source>(x: SRC): SELECT<SRC, COLS> => {
from = <SRC extends source>(_x: SRC): SELECT<SRC, COLS> => {
return this
}
columns = <COL extends sourceElements<TARGET>>(x: sourceElementRef<TARGET, COL>[]): SELECT<TARGET, COL> => {
columns = <COL extends sourceElements<TARGET>>(_x: sourceElementRef<TARGET, COL>[]): SELECT<TARGET, COL> => {
return this
}
then = <RET extends sourceElementResult<TARGET, COLS>>(
Expand All @@ -185,6 +185,7 @@ class SELECT<TARGET extends source, COLS extends sourceElements<TARGET>> {
): void => {
try {
// This is not a real solution, but it would work in javascript
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resolve({ ID: 1 } as any)
} catch (e) {
reject(new Error('oops'))
Expand Down
Loading

0 comments on commit 1f5d2c7

Please sign in to comment.