Use * instead of primary alias in count queries with CTE.

Closes #3726
Original pull request: #3730
This commit is contained in:
Christoph Strobl
2025-01-13 15:06:24 +01:00
committed by Mark Paluch
parent 3cde3ca12f
commit b79f64da25
4 changed files with 207 additions and 11 deletions

View File

@@ -36,6 +36,7 @@ class HqlCountQueryTransformer extends HqlQueryRenderer {
private final @Nullable String countProjection;
private final @Nullable String primaryFromAlias;
private boolean containsCTE = false;
HqlCountQueryTransformer(@Nullable String countProjection, @Nullable String primaryFromAlias) {
this.countProjection = countProjection;
@@ -66,6 +67,12 @@ class HqlCountQueryTransformer extends HqlQueryRenderer {
return builder;
}
@Override
public QueryTokenStream visitCte(HqlParser.CteContext ctx) {
this.containsCTE = true;
return super.visitCte(ctx);
}
@Override
public QueryRendererBuilder visitFromQuery(HqlParser.FromQueryContext ctx) {
@@ -189,7 +196,9 @@ class HqlCountQueryTransformer extends HqlQueryRenderer {
nested.append(QueryTokens.expression(ctx.DISTINCT()));
nested.append(getDistinctCountSelection(visit(ctx.selectionList())));
} else {
nested.append(QueryTokens.token(primaryFromAlias));
// with CTE primary alias fails with hibernate (WITH entities AS (…) SELECT count(c) FROM entities c)
nested.append(containsCTE ? QueryTokens.token("*") : QueryTokens.token(primaryFromAlias));
}
} else {
builder.append(QueryTokens.token(countProjection));

View File

@@ -0,0 +1,123 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assumptions.*;
import jakarta.persistence.EntityManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ImportResource;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.sample.Role;
import org.springframework.data.jpa.domain.sample.User;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.jpa.repository.sample.RoleRepository;
import org.springframework.data.jpa.repository.sample.UserRepository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.annotation.Transactional;
/**
* Hibernate-specific repository tests.
*
* @author Mark Paluch
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration()
@Transactional
class HibernateRepositoryTests {
@Autowired UserRepository userRepository;
@Autowired RoleRepository roleRepository;
@Autowired CteUserRepository cteUserRepository;
@Autowired EntityManager em;
PersistenceProvider provider;
User dave;
User carter;
User oliver;
Role drummer;
Role guitarist;
Role singer;
@BeforeEach
void setUp() {
provider = PersistenceProvider.fromEntityManager(em);
assumeThat(provider).isEqualTo(PersistenceProvider.HIBERNATE);
roleRepository.deleteAll();
userRepository.deleteAll();
drummer = roleRepository.save(new Role("DRUMMER"));
guitarist = roleRepository.save(new Role("GUITARIST"));
singer = roleRepository.save(new Role("SINGER"));
dave = userRepository.save(new User("Dave", "Matthews", "dave@dmband.com", singer));
carter = userRepository.save(new User("Carter", "Beauford", "carter@dmband.com", singer, drummer));
oliver = userRepository.save(new User("Oliver August", "Matthews", "oliver@dmband.com"));
}
@Test // GH-3726
void testQueryWithCTE() {
Page<UserExcerptDto> result = cteUserRepository.findWithCTE(PageRequest.of(0, 1));
assertThat(result.getTotalElements()).isEqualTo(3);
}
@ImportResource({ "classpath:infrastructure.xml" })
@Configuration
@EnableJpaRepositories(basePackageClasses = HibernateRepositoryTests.class, considerNestedRepositories = true,
includeFilters = @ComponentScan.Filter(
classes = { CteUserRepository.class, UserRepository.class, RoleRepository.class },
type = FilterType.ASSIGNABLE_TYPE))
static class TestConfig {}
interface CteUserRepository extends CrudRepository<User, Integer> {
/*
WITH entities AS (
SELECT
e.id as id,
e.number as number
FROM TestEntity e
)
SELECT new com.example.demo.Result('X', c.id, c.number)
FROM entities c
*/
@Query("""
WITH cte_select AS (select u.firstname as firstname, u.lastname as lastname from User u)
SELECT new org.springframework.data.jpa.repository.UserExcerptDto(c.firstname, c.lastname)
FROM cte_select c
""")
Page<UserExcerptDto> findWithCTE(Pageable page);
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository;
/**
* Hibernate is still a bit picky on records so let's use a class, just in case.
*
* @author Christoph Strobl
*/
public class UserExcerptDto {
private String firstname;
private String lastname;
public UserExcerptDto(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
}

View File

@@ -86,13 +86,11 @@ class HqlQueryTransformerTests {
assertThat(createQueryFor(original, Sort.unsorted())).isEqualTo(original);
assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsLast())))
.startsWith(original)
.endsWithIgnoringCase("e.lastName DESC NULLS LAST");
assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsLast()))).startsWith(original)
.endsWithIgnoringCase("e.lastName DESC NULLS LAST");
assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsFirst())))
.startsWith(original)
.endsWithIgnoringCase("e.lastName DESC NULLS FIRST");
assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsFirst()))).startsWith(original)
.endsWithIgnoringCase("e.lastName DESC NULLS FIRST");
}
@Test
@@ -151,6 +149,24 @@ class HqlQueryTransformerTests {
assertThat(results).isEqualTo("SELECT count(e) FROM Employee e where e.name = :name");
}
@Test // GH-3726
void shouldCreateCountQueryForCTE() {
// given
var original = """
WITH cte_select AS (select u.firstname as firstname, u.lastname as lastname from User u)
SELECT new org.springframework.data.jpa.repository.sample.UserExcerptDto(c.firstname, c.lastname)
FROM cte_select c
""";
// when
var results = createCountQueryFor(original);
// then
assertThat(results).isEqualToIgnoringWhitespace(
"WITH cte_select AS (select u.firstname as firstname, u.lastname as lastname from User u) SELECT count(*) FROM cte_select c");
}
@Test
void multipleAliasesShouldBeGathered() {
@@ -539,7 +555,7 @@ class HqlQueryTransformerTests {
""");
assertThat(countQuery).startsWith("WITH maxId AS (select max(sr.snapshot.id) snapshotId from SnapshotReference sr")
.endsWith("select count(m) from maxId m join SnapshotReference sr on sr.snapshot.id = m.snapshotId");
.endsWith("select count(*) from maxId m join SnapshotReference sr on sr.snapshot.id = m.snapshotId");
}
@Test // GH-3504
@@ -1039,8 +1055,7 @@ class HqlQueryTransformerTests {
""", """
delete MyEntity AS mes
where mes.col = 'test'
"""
}) // GH-2977, GH-3649
""" }) // GH-2977, GH-3649
void isSubqueryThrowsException(String query) {
assertThat(createQueryFor(query, Sort.unsorted())).isEqualToIgnoringWhitespace(query);
}
@@ -1101,7 +1116,8 @@ class HqlQueryTransformerTests {
"select count(distinct a, b, sum(amount), d) from Employee AS __ GROUP BY n");
assertCountQuery("select distinct a, count(b) as c from Employee GROUP BY n",
"select count(distinct a, count(b)) from Employee AS __ GROUP BY n");
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");
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");
}
@Test // GH-3427