Skip to content

Commit 643a3a9

Browse files
christophstroblmp911de
authored andcommitted
Use * instead of primary alias in count queries with CTE.
Closes #3726 Original pull request: #3730
1 parent 3e99eee commit 643a3a9

File tree

4 files changed

+207
-11
lines changed

4 files changed

+207
-11
lines changed

Diff for: spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class HqlCountQueryTransformer extends HqlQueryRenderer {
3636

3737
private final @Nullable String countProjection;
3838
private final @Nullable String primaryFromAlias;
39+
private boolean containsCTE = false;
3940

4041
HqlCountQueryTransformer(@Nullable String countProjection, @Nullable String primaryFromAlias) {
4142
this.countProjection = countProjection;
@@ -66,6 +67,12 @@ public QueryRendererBuilder visitOrderedQuery(HqlParser.OrderedQueryContext ctx)
6667
return builder;
6768
}
6869

70+
@Override
71+
public QueryTokenStream visitCte(HqlParser.CteContext ctx) {
72+
this.containsCTE = true;
73+
return super.visitCte(ctx);
74+
}
75+
6976
@Override
7077
public QueryRendererBuilder visitFromQuery(HqlParser.FromQueryContext ctx) {
7178

@@ -189,7 +196,9 @@ public QueryTokenStream visitSelectClause(HqlParser.SelectClauseContext ctx) {
189196
nested.append(QueryTokens.expression(ctx.DISTINCT()));
190197
nested.append(getDistinctCountSelection(visit(ctx.selectionList())));
191198
} else {
192-
nested.append(QueryTokens.token(primaryFromAlias));
199+
200+
// with CTE primary alias fails with hibernate (WITH entities AS (…) SELECT count(c) FROM entities c)
201+
nested.append(containsCTE ? QueryTokens.token("*") : QueryTokens.token(primaryFromAlias));
193202
}
194203
} else {
195204
builder.append(QueryTokens.token(countProjection));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.repository;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
import static org.assertj.core.api.Assumptions.*;
20+
21+
import jakarta.persistence.EntityManager;
22+
23+
import org.junit.jupiter.api.BeforeEach;
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.api.extension.ExtendWith;
26+
27+
import org.springframework.beans.factory.annotation.Autowired;
28+
import org.springframework.context.annotation.ComponentScan;
29+
import org.springframework.context.annotation.Configuration;
30+
import org.springframework.context.annotation.FilterType;
31+
import org.springframework.context.annotation.ImportResource;
32+
import org.springframework.data.domain.Page;
33+
import org.springframework.data.domain.PageRequest;
34+
import org.springframework.data.domain.Pageable;
35+
import org.springframework.data.jpa.domain.sample.Role;
36+
import org.springframework.data.jpa.domain.sample.User;
37+
import org.springframework.data.jpa.provider.PersistenceProvider;
38+
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
39+
import org.springframework.data.jpa.repository.sample.RoleRepository;
40+
import org.springframework.data.jpa.repository.sample.UserRepository;
41+
import org.springframework.data.repository.CrudRepository;
42+
import org.springframework.test.context.ContextConfiguration;
43+
import org.springframework.test.context.junit.jupiter.SpringExtension;
44+
import org.springframework.transaction.annotation.Transactional;
45+
46+
/**
47+
* Hibernate-specific repository tests.
48+
*
49+
* @author Mark Paluch
50+
*/
51+
@ExtendWith(SpringExtension.class)
52+
@ContextConfiguration()
53+
@Transactional
54+
class HibernateRepositoryTests {
55+
56+
@Autowired UserRepository userRepository;
57+
@Autowired RoleRepository roleRepository;
58+
@Autowired CteUserRepository cteUserRepository;
59+
@Autowired EntityManager em;
60+
61+
PersistenceProvider provider;
62+
User dave;
63+
User carter;
64+
User oliver;
65+
Role drummer;
66+
Role guitarist;
67+
Role singer;
68+
69+
@BeforeEach
70+
void setUp() {
71+
provider = PersistenceProvider.fromEntityManager(em);
72+
73+
assumeThat(provider).isEqualTo(PersistenceProvider.HIBERNATE);
74+
roleRepository.deleteAll();
75+
userRepository.deleteAll();
76+
77+
drummer = roleRepository.save(new Role("DRUMMER"));
78+
guitarist = roleRepository.save(new Role("GUITARIST"));
79+
singer = roleRepository.save(new Role("SINGER"));
80+
81+
dave = userRepository.save(new User("Dave", "Matthews", "[email protected]", singer));
82+
carter = userRepository.save(new User("Carter", "Beauford", "[email protected]", singer, drummer));
83+
oliver = userRepository.save(new User("Oliver August", "Matthews", "[email protected]"));
84+
}
85+
86+
@Test // GH-3726
87+
void testQueryWithCTE() {
88+
89+
Page<UserExcerptDto> result = cteUserRepository.findWithCTE(PageRequest.of(0, 1));
90+
assertThat(result.getTotalElements()).isEqualTo(3);
91+
}
92+
93+
@ImportResource({ "classpath:infrastructure.xml" })
94+
@Configuration
95+
@EnableJpaRepositories(basePackageClasses = HibernateRepositoryTests.class, considerNestedRepositories = true,
96+
includeFilters = @ComponentScan.Filter(
97+
classes = { CteUserRepository.class, UserRepository.class, RoleRepository.class },
98+
type = FilterType.ASSIGNABLE_TYPE))
99+
static class TestConfig {}
100+
101+
interface CteUserRepository extends CrudRepository<User, Integer> {
102+
103+
/*
104+
WITH entities AS (
105+
SELECT
106+
e.id as id,
107+
e.number as number
108+
FROM TestEntity e
109+
)
110+
SELECT new com.example.demo.Result('X', c.id, c.number)
111+
FROM entities c
112+
*/
113+
114+
@Query("""
115+
WITH cte_select AS (select u.firstname as firstname, u.lastname as lastname from User u)
116+
SELECT new org.springframework.data.jpa.repository.UserExcerptDto(c.firstname, c.lastname)
117+
FROM cte_select c
118+
""")
119+
Page<UserExcerptDto> findWithCTE(Pageable page);
120+
121+
}
122+
123+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.repository;
17+
18+
/**
19+
* Hibernate is still a bit picky on records so let's use a class, just in case.
20+
*
21+
* @author Christoph Strobl
22+
*/
23+
public class UserExcerptDto {
24+
25+
private String firstname;
26+
private String lastname;
27+
28+
public UserExcerptDto(String firstname, String lastname) {
29+
this.firstname = firstname;
30+
this.lastname = lastname;
31+
}
32+
33+
public String getFirstname() {
34+
return firstname;
35+
}
36+
37+
public void setFirstname(String firstname) {
38+
this.firstname = firstname;
39+
}
40+
41+
public String getLastname() {
42+
return lastname;
43+
}
44+
45+
public void setLastname(String lastname) {
46+
this.lastname = lastname;
47+
}
48+
}

Diff for: spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java

+26-10
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,11 @@ void nullFirstLastSorting() {
8686

8787
assertThat(createQueryFor(original, Sort.unsorted())).isEqualTo(original);
8888

89-
assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsLast())))
90-
.startsWith(original)
91-
.endsWithIgnoringCase("e.lastName DESC NULLS LAST");
89+
assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsLast()))).startsWith(original)
90+
.endsWithIgnoringCase("e.lastName DESC NULLS LAST");
9291

93-
assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsFirst())))
94-
.startsWith(original)
95-
.endsWithIgnoringCase("e.lastName DESC NULLS FIRST");
92+
assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsFirst()))).startsWith(original)
93+
.endsWithIgnoringCase("e.lastName DESC NULLS FIRST");
9694
}
9795

9896
@Test
@@ -151,6 +149,24 @@ void applyCountToAlreadySortedQuery() {
151149
assertThat(results).isEqualTo("SELECT count(e) FROM Employee e where e.name = :name");
152150
}
153151

152+
@Test // GH-3726
153+
void shouldCreateCountQueryForCTE() {
154+
155+
// given
156+
var original = """
157+
WITH cte_select AS (select u.firstname as firstname, u.lastname as lastname from User u)
158+
SELECT new org.springframework.data.jpa.repository.sample.UserExcerptDto(c.firstname, c.lastname)
159+
FROM cte_select c
160+
""";
161+
162+
// when
163+
var results = createCountQueryFor(original);
164+
165+
// then
166+
assertThat(results).isEqualToIgnoringWhitespace(
167+
"WITH cte_select AS (select u.firstname as firstname, u.lastname as lastname from User u) SELECT count(*) FROM cte_select c");
168+
}
169+
154170
@Test
155171
void multipleAliasesShouldBeGathered() {
156172

@@ -539,7 +555,7 @@ WITH maxId AS(select max(sr.snapshot.id) snapshotId from SnapshotReference sr
539555
""");
540556

541557
assertThat(countQuery).startsWith("WITH maxId AS (select max(sr.snapshot.id) snapshotId from SnapshotReference sr")
542-
.endsWith("select count(m) from maxId m join SnapshotReference sr on sr.snapshot.id = m.snapshotId");
558+
.endsWith("select count(*) from maxId m join SnapshotReference sr on sr.snapshot.id = m.snapshotId");
543559
}
544560

545561
@Test // GH-3504
@@ -1039,8 +1055,7 @@ select max(id), col
10391055
""", """
10401056
delete MyEntity AS mes
10411057
where mes.col = 'test'
1042-
"""
1043-
}) // GH-2977, GH-3649
1058+
""" }) // GH-2977, GH-3649
10441059
void isSubqueryThrowsException(String query) {
10451060
assertThat(createQueryFor(query, Sort.unsorted())).isEqualToIgnoringWhitespace(query);
10461061
}
@@ -1101,7 +1116,8 @@ void createsCountQueryUsingAliasCorrectly() {
11011116
"select count(distinct a, b, sum(amount), d) from Employee AS __ GROUP BY n");
11021117
assertCountQuery("select distinct a, count(b) as c from Employee GROUP BY n",
11031118
"select count(distinct a, count(b)) from Employee AS __ GROUP BY n");
1104-
assertCountQuery("select distinct substring(e.firstname, 1, position('a' in e.lastname)) as x from from Employee", "select count(distinct substring(e.firstname, 1, position('a' in e.lastname))) from from Employee");
1119+
assertCountQuery("select distinct substring(e.firstname, 1, position('a' in e.lastname)) as x from from Employee",
1120+
"select count(distinct substring(e.firstname, 1, position('a' in e.lastname))) from from Employee");
11051121
}
11061122

11071123
@Test // GH-3427

0 commit comments

Comments
 (0)