Skip to content

Commit baa3528

Browse files
fix: resolve $self in infix filters with path expressions and nested exists (#1604)
When an infix filter contains a path expression or nested exists, `$self` resolution failed because the subquery generated by `transformSubquery` lacks the outer query's elements in its projection. - Guard `$baseLink` branch so `$self` refs skip it and reach proper resolution - Fall back to outermost query elements when local `queryElements` don't contain the referenced element - Resolve `$self` directly to the outer query alias in `getTransformedTokenStream` for subquery contexts
1 parent e57365b commit baa3528

3 files changed

Lines changed: 111 additions & 2 deletions

File tree

db-service/lib/cqn4sql.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1823,6 +1823,17 @@ function cqn4sql(originalQuery, model, useTechnicalAlias = true) {
18231823
continue
18241824
}
18251825
if (token.ref.length > 1 && token.ref[0] === '$self' && !token.$refLinks[0].definition.kind) {
1826+
if (inferred.outerQueries) {
1827+
const outerQuery = inferred.outerQueries[0]
1828+
const stepToFind = token.ref[1]?.id || token.ref[1]
1829+
const outerAlias = outerQuery.$combinedElements?.[stepToFind]?.[0].index
1830+
if (outerAlias) {
1831+
let result = copy(token)
1832+
result.ref = [outerAlias, token.flatName]
1833+
transformedTokenStream.push(result)
1834+
continue
1835+
}
1836+
}
18261837
const dollarSelfReplacement = [calculateDollarSelfColumn(token, true)]
18271838
transformedTokenStream.push(...getTransformedTokenStream(dollarSelfReplacement))
18281839
continue

db-service/lib/infer/index.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ function infer(originalQuery, model, useTechnicalAlias = true) {
453453
arg.$refLinks.push({ definition: pseudos.elements[id], target: pseudos })
454454
pseudoPath = true // only first path step must be well defined
455455
nameSegments.push(id)
456-
} else if ($baseLink) {
456+
} else if ($baseLink && !firstStepIsSelf) {
457457
const { definition, target } = $baseLink
458458
const elements = getDefinition(definition.target)?.elements || definition.elements
459459
if (elements && id in elements) {
@@ -484,7 +484,15 @@ function infer(originalQuery, model, useTechnicalAlias = true) {
484484
target: getDefinitionFromSources(sources, id),
485485
})
486486
} else if (firstStepIsSelf) {
487-
arg.$refLinks.push({ definition: { elements: queryElements }, target: { elements: queryElements } })
487+
const nextStep = arg.ref[1]?.id || arg.ref[1]
488+
let elements = queryElements
489+
if (nextStep && (!queryElements || !(nextStep in queryElements)) && inferred.outerQueries) {
490+
const outerQuery = inferred.outerQueries[0]
491+
if (outerQuery?.elements && nextStep in outerQuery.elements) {
492+
elements = outerQuery.elements
493+
}
494+
}
495+
arg.$refLinks.push({ definition: { elements }, target: { elements } })
488496
} else if (arg.ref.length > 1 && inferred.outerQueries?.find(outer => id in outer.sources)) {
489497
// outer query accessed via alias
490498
const outerAlias = inferred.outerQueries.find(outer => id in outer.sources)

db-service/test/cqn4sql/table-alias.test.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,96 @@ describe('table alias access', () => {
593593
}`
594594
expect(JSON.parse(JSON.stringify(transformed))).to.deep.equal(expectation)
595595
})
596+
it('$self in infix filter alongside path expression', () => {
597+
const q = cds.ql`
598+
SELECT from bookshop.Books as Books {
599+
title,
600+
exists author.books[ author.name = title and title = $self.title ] as s
601+
}`
602+
const transformed = cqn4sql(q, model)
603+
const expected = cds.ql`
604+
SELECT from bookshop.Books as Books {
605+
Books.title,
606+
exists (
607+
SELECT 1 from bookshop.Authors as $a where $a.ID = Books.author_ID and exists (
608+
SELECT 1 from bookshop.Books as $b
609+
inner join bookshop.Authors as author on author.ID = $b.author_ID
610+
where $b.author_ID = $a.ID and author.name = $b.title and $b.title = Books.title
611+
)
612+
) as s
613+
}`
614+
expect(JSON.parse(JSON.stringify(transformed))).to.deep.equal(expected)
615+
})
616+
it('$self in nested exists infix filter', () => {
617+
const q = cds.ql`
618+
SELECT from bookshop.Books as Books {
619+
title,
620+
exists author.books[ exists author.books[ title = $self.title ] ] as s
621+
}`
622+
const transformed = cqn4sql(q, model)
623+
const expected = cds.ql`
624+
SELECT from bookshop.Books as Books {
625+
Books.title,
626+
exists (
627+
SELECT 1 from bookshop.Authors as $a where $a.ID = Books.author_ID and exists (
628+
SELECT 1 from bookshop.Books as $b where $b.author_ID = $a.ID and exists (
629+
SELECT 1 from bookshop.Authors as $a2 where $a2.ID = $b.author_ID and exists (
630+
SELECT 1 from bookshop.Books as $b2 where $b2.author_ID = $a2.ID and $b2.title = Books.title
631+
)
632+
)
633+
)
634+
) as s
635+
}`
636+
expect(JSON.parse(JSON.stringify(transformed))).to.deep.equal(expected)
637+
})
638+
it('$self in deeply nested infix filter with multiple path expressions', () => {
639+
const q = cds.ql`
640+
SELECT from bookshop.Books as Books {
641+
title,
642+
exists author.books[ author.name = title and exists author.books[ author.name = title and title = $self.title ] ] as s
643+
}`
644+
const transformed = cqn4sql(q, model)
645+
const expected = cds.ql`
646+
SELECT from bookshop.Books as Books {
647+
Books.title,
648+
exists (
649+
SELECT 1 from bookshop.Authors as $a where $a.ID = Books.author_ID and exists (
650+
SELECT 1 from bookshop.Books as $b
651+
inner join bookshop.Authors as author on author.ID = $b.author_ID
652+
where $b.author_ID = $a.ID and author.name = $b.title and exists (
653+
SELECT 1 from bookshop.Authors as $a2 where $a2.ID = $b.author_ID and exists (
654+
SELECT 1 from bookshop.Books as $b2
655+
inner join bookshop.Authors as author2 on author2.ID = $b2.author_ID
656+
where $b2.author_ID = $a2.ID and author2.name = $b2.title and $b2.title = Books.title
657+
)
658+
)
659+
)
660+
) as s
661+
}`
662+
expect(JSON.parse(JSON.stringify(transformed))).to.deep.equal(expected)
663+
})
664+
it('$self in subquery refers to own projection, not outer query', () => {
665+
const q = cds.ql`
666+
SELECT from bookshop.Authors as Authors {
667+
ID,
668+
1+1 as foo
669+
} where exists (
670+
SELECT from bookshop.Books { 2+2 as foo, $self.foo as bar }
671+
where author[$self.foo = 4].ID = 42
672+
)`
673+
const transformed = cqn4sql(q, model)
674+
const expected = cds.ql`
675+
SELECT from bookshop.Authors as Authors {
676+
Authors.ID,
677+
1+1 as foo
678+
} where exists (
679+
SELECT from bookshop.Books as $B
680+
left outer join bookshop.Authors as author on author.ID = $B.author_ID and (2+2) = 4
681+
{ 2+2 as foo, 2+2 as bar }
682+
where author.ID = 42
683+
)`
684+
expect(JSON.parse(JSON.stringify(transformed))).to.deep.equal(expected)
685+
})
596686
})
597687

598688
describe('in ORDER BY', () => {

0 commit comments

Comments
 (0)