diff --git a/pom.xml b/pom.xml index c94ae35cd5..6e62fb99c6 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 3.1.0-SNAPSHOT + 3.1.0-GH-2773-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index db915d7c3b..6e24605d3c 100755 --- a/spring-data-envers/pom.xml +++ b/spring-data-envers/pom.xml @@ -5,12 +5,12 @@ org.springframework.data spring-data-envers - 3.1.0-SNAPSHOT + 3.1.0-GH-2773-SNAPSHOT org.springframework.data spring-data-jpa-parent - 3.1.0-SNAPSHOT + 3.1.0-GH-2773-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index a5cb2f09b5..52cf3c12a0 100644 --- a/spring-data-jpa-distribution/pom.xml +++ b/spring-data-jpa-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-jpa-parent - 3.1.0-SNAPSHOT + 3.1.0-GH-2773-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 95a83aac44..bd8aa8d5cb 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-jpa - 3.1.0-SNAPSHOT + 3.1.0-GH-2773-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-jpa-parent - 3.1.0-SNAPSHOT + 3.1.0-GH-2773-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java index 53a07bf6f9..92387c973e 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancer.java @@ -46,7 +46,7 @@ public String detectAlias() { @Override public String createCountQueryFor(@Nullable String countProjection) { - return QueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection); + return QueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection, this.query.isNativeQuery()); } @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java index 7a44873ac4..cd76876ac7 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java @@ -15,9 +15,8 @@ */ package org.springframework.data.jpa.repository.query; -import static org.springframework.data.jpa.repository.query.JSqlParserUtils.getJSqlCount; -import static org.springframework.data.jpa.repository.query.JSqlParserUtils.getJSqlLower; -import static org.springframework.data.jpa.repository.query.QueryUtils.checkSortExpression; +import static org.springframework.data.jpa.repository.query.JSqlParserUtils.*; +import static org.springframework.data.jpa.repository.query.QueryUtils.*; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.Alias; @@ -29,11 +28,23 @@ import net.sf.jsqlparser.statement.delete.Delete; import net.sf.jsqlparser.statement.insert.Insert; import net.sf.jsqlparser.statement.merge.Merge; -import net.sf.jsqlparser.statement.select.*; +import net.sf.jsqlparser.statement.select.OrderByElement; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.select.SelectBody; +import net.sf.jsqlparser.statement.select.SelectExpressionItem; +import net.sf.jsqlparser.statement.select.SelectItem; +import net.sf.jsqlparser.statement.select.SetOperationList; +import net.sf.jsqlparser.statement.select.WithItem; import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.statement.values.ValuesStatement; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import org.springframework.data.domain.Sort; @@ -400,7 +411,7 @@ public String createCountQueryFor(@Nullable String countProjection) { return selectBody.toString(); } - String countProp = tableAlias == null ? "*" : tableAlias; + String countProp = query.isNativeQuery() ? (distinct ? "*" : "1") : tableAlias == null ? "*" : tableAlias; Function jSqlCount = getJSqlCount(Collections.singletonList(countProp), distinct); selectBody.setSelectItems(Collections.singletonList(new SelectExpressionItem(jSqlCount))); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java index 5707caec26..785dde580d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java @@ -18,10 +18,23 @@ import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType.*; import static java.util.regex.Pattern.*; -import jakarta.persistence.*; -import jakarta.persistence.criteria.*; -import jakarta.persistence.metamodel.*; +import jakarta.persistence.EntityManager; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Parameter; +import jakarta.persistence.Query; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Fetch; +import jakarta.persistence.criteria.From; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.metamodel.Attribute; import jakarta.persistence.metamodel.Attribute.PersistentAttributeType; +import jakarta.persistence.metamodel.Bindable; +import jakarta.persistence.metamodel.ManagedType; +import jakarta.persistence.metamodel.PluralAttribute; +import jakarta.persistence.metamodel.SingularAttribute; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; @@ -570,6 +583,19 @@ public static String createCountQueryFor(String originalQuery) { */ @Deprecated public static String createCountQueryFor(String originalQuery, @Nullable String countProjection) { + return createCountQueryFor(originalQuery, countProjection, false); + } + + /** + * Creates a count projected query from the given original query. + * + * @param originalQuery must not be {@literal null}. + * @param countProjection may be {@literal null}. + * @param nativeQuery whether the underlying query is a native query. + * @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}. + * @since 2.7.8 + */ + static String createCountQueryFor(String originalQuery, @Nullable String countProjection, boolean nativeQuery) { Assert.hasText(originalQuery, "OriginalQuery must not be null or empty"); @@ -591,9 +617,14 @@ public static String createCountQueryFor(String originalQuery, @Nullable String String replacement = useVariable ? SIMPLE_COUNT_VALUE : complexCountValue; - String alias = QueryUtils.detectAlias(originalQuery); - if ("*".equals(variable) && alias != null) { - replacement = alias; + if (nativeQuery && (variable.contains(",") || "*".equals(variable))) { + replacement = "1"; + } else { + + String alias = QueryUtils.detectAlias(originalQuery); + if (("*".equals(variable) && alias != null)) { + replacement = alias; + } } countQuery = matcher.replaceFirst(String.format(COUNT_REPLACEMENT_TEMPLATE, replacement)); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java new file mode 100644 index 0000000000..8b38061bf1 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultQueryEnhancerUnitTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2023 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.query; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +/** + * TCK Tests for {@link DefaultQueryEnhancer}. + * + * @author Mark Paluch + */ +public class DefaultQueryEnhancerUnitTests extends QueryEnhancerTckTests { + + @Override + QueryEnhancer createQueryEnhancer(DeclaredQuery declaredQuery) { + return new DefaultQueryEnhancer(declaredQuery); + } + + @Override + @Test // GH-2511, GH-2773 + @Disabled("Not properly supported by QueryUtils") + void shouldDeriveNativeCountQueryWithVariable(String query, String expected) {} +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java new file mode 100644 index 0000000000..5d97802439 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java @@ -0,0 +1,220 @@ +/* + * Copyright 2023 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.query; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assumptions.*; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.data.domain.Sort; + +/** + * TCK Tests for {@link JSqlParserQueryEnhancer}. + * + * @author Mark Paluch + * @author Diego Krupitza + * @author Geoffrey Deremetz + */ +public class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { + + @Override + QueryEnhancer createQueryEnhancer(DeclaredQuery declaredQuery) { + return new JSqlParserQueryEnhancer(declaredQuery); + } + + @Override + @ParameterizedTest // GH-2773 + @MethodSource("jpqlCountQueries") + void shouldDeriveJpqlCountQuery(String query, String expected) { + + assumeThat(query).as("JSQLParser does not support simple JPQL syntax").doesNotStartWithIgnoringCase("FROM"); + + assumeThat(query).as("JSQLParser does not support constructor JPQL syntax").doesNotContain(" new "); + + super.shouldDeriveJpqlCountQuery(query, expected); + } + + @Test + // GH-2578 + void setOperationListWorks() { + + String setQuery = "select SOME_COLUMN from SOME_TABLE where REPORTING_DATE = :REPORTING_DATE \n" // + + "except \n" // + + "select SOME_COLUMN from SOME_OTHER_TABLE where REPORTING_DATE = :REPORTING_DATE"; + + StringQuery stringQuery = new StringQuery(setQuery, true); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + + assertThat(stringQuery.getAlias()).isNullOrEmpty(); + assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); + assertThat(stringQuery.hasConstructorExpression()).isFalse(); + + assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase(setQuery); + assertThat(queryEnhancer.applySorting(Sort.by("SOME_COLUMN"))).endsWith("ORDER BY SOME_COLUMN ASC"); + assertThat(queryEnhancer.getJoinAliases()).isEmpty(); + assertThat(queryEnhancer.detectAlias()).isNullOrEmpty(); + assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); + assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); + } + + @Test // GH-2578 + void complexSetOperationListWorks() { + + String setQuery = "select SOME_COLUMN from SOME_TABLE where REPORTING_DATE = :REPORTING_DATE \n" // + + "except \n" // + + "select SOME_COLUMN from SOME_OTHER_TABLE where REPORTING_DATE = :REPORTING_DATE \n" // + + "union select SOME_COLUMN from SOME_OTHER_OTHER_TABLE"; + + StringQuery stringQuery = new StringQuery(setQuery, true); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + + assertThat(stringQuery.getAlias()).isNullOrEmpty(); + assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); + assertThat(stringQuery.hasConstructorExpression()).isFalse(); + + assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase(setQuery); + assertThat(queryEnhancer.applySorting(Sort.by("SOME_COLUMN").ascending())).endsWith("ORDER BY SOME_COLUMN ASC"); + assertThat(queryEnhancer.getJoinAliases()).isEmpty(); + assertThat(queryEnhancer.detectAlias()).isNullOrEmpty(); + assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); + assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); + } + + @Test // GH-2578 + void deeplyNestedcomplexSetOperationListWorks() { + + String setQuery = "SELECT CustomerID FROM (\n" // + + "\t\t\tselect * from Customers\n" // + + "\t\t\texcept\n"// + + "\t\t\tselect * from Customers where country = 'Austria'\n"// + + "\t)\n" // + + "\texcept\n"// + + "\tselect CustomerID from customers where country = 'Germany'\n"// + + "\t;"; + + StringQuery stringQuery = new StringQuery(setQuery, true); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + + assertThat(stringQuery.getAlias()).isNullOrEmpty(); + assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("CustomerID"); + assertThat(stringQuery.hasConstructorExpression()).isFalse(); + + assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase(setQuery); + assertThat(queryEnhancer.applySorting(Sort.by("CustomerID").descending())).endsWith("ORDER BY CustomerID DESC"); + assertThat(queryEnhancer.getJoinAliases()).isEmpty(); + assertThat(queryEnhancer.detectAlias()).isNullOrEmpty(); + assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase("CustomerID"); + assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); + } + + @Test // GH-2578 + void valuesStatementsWorks() { + + String setQuery = "VALUES (1, 2, 'test')"; + + StringQuery stringQuery = new StringQuery(setQuery, true); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + + assertThat(stringQuery.getAlias()).isNullOrEmpty(); + assertThat(stringQuery.getProjection()).isNullOrEmpty(); + assertThat(stringQuery.hasConstructorExpression()).isFalse(); + + assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase(setQuery); + assertThat(queryEnhancer.applySorting(Sort.by("CustomerID").descending())).isEqualTo(setQuery); + assertThat(queryEnhancer.getJoinAliases()).isEmpty(); + assertThat(queryEnhancer.detectAlias()).isNullOrEmpty(); + assertThat(queryEnhancer.getProjection()).isNullOrEmpty(); + assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); + } + + @Test // GH-2578 + void withStatementsWorks() { + + String setQuery = "with sample_data(day, value) as (values ((0, 13), (1, 12), (2, 15), (3, 4), (4, 8), (5, 16))) \n" + + "select day, value from sample_data as a"; + + StringQuery stringQuery = new StringQuery(setQuery, true); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + + assertThat(stringQuery.getAlias()).isEqualToIgnoringCase("a"); + assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("day, value"); + assertThat(stringQuery.hasConstructorExpression()).isFalse(); + + assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase( + "with sample_data (day, value) AS (VALUES ((0, 13), (1, 12), (2, 15), (3, 4), (4, 8), (5, 16)))\n" + + "SELECT count(1) FROM sample_data AS a"); + assertThat(queryEnhancer.applySorting(Sort.by("day").descending())).endsWith("ORDER BY a.day DESC"); + assertThat(queryEnhancer.getJoinAliases()).isEmpty(); + assertThat(queryEnhancer.detectAlias()).isEqualToIgnoringCase("a"); + assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase("day, value"); + assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); + } + + @Test // GH-2578 + void multipleWithStatementsWorks() { + + String setQuery = "with sample_data(day, value) as (values ((0, 13), (1, 12), (2, 15), (3, 4), (4, 8), (5, 16))), test2 as (values (1,2,3)) \n" + + "select day, value from sample_data as a"; + + StringQuery stringQuery = new StringQuery(setQuery, true); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + + assertThat(stringQuery.getAlias()).isEqualToIgnoringCase("a"); + assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("day, value"); + assertThat(stringQuery.hasConstructorExpression()).isFalse(); + + assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase( + "with sample_data (day, value) AS (VALUES ((0, 13), (1, 12), (2, 15), (3, 4), (4, 8), (5, 16))),test2 AS (VALUES (1, 2, 3))\n" + + "SELECT count(1) FROM sample_data AS a"); + assertThat(queryEnhancer.applySorting(Sort.by("day").descending())).endsWith("ORDER BY a.day DESC"); + assertThat(queryEnhancer.getJoinAliases()).isEmpty(); + assertThat(queryEnhancer.detectAlias()).isEqualToIgnoringCase("a"); + assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase("day, value"); + assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); + } + + @ParameterizedTest // GH-2641 + @MethodSource("mergeStatementWorksSource") + void mergeStatementWorksWithJSqlParser(String query, String alias) { + + StringQuery stringQuery = new StringQuery(query, true); + QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + + assertThat(queryEnhancer.detectAlias()).isEqualTo(alias); + assertThat(QueryUtils.detectAlias(query)).isNull(); + + assertThat(queryEnhancer.getJoinAliases()).isEmpty(); + assertThat(queryEnhancer.detectAlias()).isEqualTo(alias); + assertThat(queryEnhancer.getProjection()).isEmpty(); + assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); + } + + static Stream mergeStatementWorksSource() { + + return Stream.of( // + Arguments.of( + "merge into a using (select id, value from b) query on (a.id = query.id) when matched then update set a.value = value", + "query"), + Arguments.of( + "merge into a using (select id2, value from b) on (id = id2) when matched then update set a.value = value", + null)); + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java new file mode 100644 index 0000000000..4bbeb008fb --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java @@ -0,0 +1,206 @@ +/* + * Copyright 2023 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.query; + +import static org.assertj.core.api.Assertions.*; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * TCK Tests for {@link QueryEnhancer}. + * + * @author Mark Paluch + */ +abstract class QueryEnhancerTckTests { + + @ParameterizedTest + @MethodSource("nativeCountQueries") // GH-2773 + void shouldDeriveNativeCountQuery(String query, String expected) { + + DeclaredQuery declaredQuery = DeclaredQuery.of(query, true); + QueryEnhancer enhancer = createQueryEnhancer(declaredQuery); + String countQueryFor = enhancer.createCountQueryFor(); + + // lenient cleanup to allow for rendering variance + String sanitized = countQueryFor.replaceAll("\r", " ").replaceAll("\n", " ").replaceAll(" {2}", " ") + .replaceAll(" {2}", " ").trim(); + + assertThat(sanitized).isEqualToIgnoringCase(expected); + } + + static Stream nativeCountQueries() { + + return Stream.of(Arguments.of( // + "SELECT * FROM table_name some_alias", // + "select count(1) FROM table_name some_alias"), // + + Arguments.of( // + "SELECT name FROM table_name some_alias", // + "select count(name) FROM table_name some_alias"), // + + Arguments.of( // + "SELECT DISTINCT name FROM table_name some_alias", // + "select count(DISTINCT name) FROM table_name some_alias"), // + + Arguments.of( // + "select distinct u from User u where u.foo = ?", // + "select count(distinct u) from User u where u.foo = ?"), + + Arguments.of( // + "select u from User as u", // + "select count(u) from User as u"), + + Arguments.of( // + "SELECT u FROM User u where u.foo.bar = ?", // + "select count(u) FROM User u where u.foo.bar = ?"), + + Arguments.of( // + "select p.lastname,p.firstname from Person p", // + "select count(1) from Person p"), + + // whitespace quirks + Arguments.of( // + """ + select user.age, + user.name + from User user + where user.age = 18 + order + by + user.name + \s""", // + "select count(1) from User user where user.age = 18"), + + Arguments.of( // + "select * from User user\n" + // + " where user.age = 18\n" + // + " order by user.name\n ", // + "select count(1) from User user where user.age = 18"), + + Arguments.of( // + "SELECT DISTINCT entity1\nFROM Entity1 entity1\nLEFT JOIN Entity2 entity2 ON entity1.key = entity2.key", // + "select count(DISTINCT entity1) FROM Entity1 entity1 LEFT JOIN Entity2 entity2 ON entity1.key = entity2.key"), + + Arguments.of( // + "SELECT DISTINCT entity1\nFROM Entity1 entity1 LEFT JOIN Entity2 entity2 ON entity1.key = entity2.key", // + "select count(DISTINCT entity1) FROM Entity1 entity1 LEFT JOIN Entity2 entity2 ON entity1.key = entity2.key"), + + Arguments.of( // + "SELECT DISTINCT entity1\nFROM Entity1 entity1 LEFT JOIN Entity2 entity2 ON entity1.key = entity2.key\nwhere entity1.id = 1799", // + "select count(DISTINCT entity1) FROM Entity1 entity1 LEFT JOIN Entity2 entity2 ON entity1.key = entity2.key where entity1.id = 1799"), + + Arguments.of( // + "select distinct m.genre from Media m where m.user = ?1 OrDer By m.genre ASC", // + "select count(distinct m.genre) from Media m where m.user = ?1")); + } + + @ParameterizedTest // GH-2773 + @MethodSource("jpqlCountQueries") + void shouldDeriveJpqlCountQuery(String query, String expected) { + + DeclaredQuery declaredQuery = DeclaredQuery.of(query, false); + QueryEnhancer enhancer = createQueryEnhancer(declaredQuery); + String countQueryFor = enhancer.createCountQueryFor(null); + + assertThat(countQueryFor).isEqualToIgnoringCase(expected); + } + + static Stream jpqlCountQueries() { + + return Stream.of(Arguments.of( // + "SELECT some_alias FROM table_name some_alias", // + "select count(some_alias) FROM table_name some_alias"), // + + Arguments.of( // + "SELECT name FROM table_name some_alias", // + "select count(name) FROM table_name some_alias"), // + + Arguments.of( // + "SELECT DISTINCT name FROM table_name some_alias", // + "select count(DISTINCT name) FROM table_name some_alias"), + + Arguments.of( // + "select distinct new User(u.name) from User u where u.foo = ?", // + "select count(distinct u) from User u where u.foo = ?"), + + Arguments.of( // + "FROM User u WHERE u.foo.bar = ?", // + "select count(u) FROM User u WHERE u.foo.bar = ?"), + + Arguments.of( // + "from User u", // + "select count(u) FROM User u"), + + Arguments.of( // + "select u from User as u", // + "select count(u) from User as u"), + + Arguments.of( // + "select p.lastname,p.firstname from Person p", // + "select count(p) from Person p"), + + Arguments.of( // + "select a.b from A a", // + "select count(a.b) from A a"), + + Arguments.of( // + "select distinct m.genre from Media m where m.user = ?1 order by m.genre asc", // + "select count(distinct m.genre) from Media m where m.user = ?1")); + } + + @ParameterizedTest // GH-2511, GH-2773 + @MethodSource("nativeQueriesWithVariables") + void shouldDeriveNativeCountQueryWithVariable(String query, String expected) { + + DeclaredQuery declaredQuery = DeclaredQuery.of(query, true); + QueryEnhancer enhancer = createQueryEnhancer(declaredQuery); + String countQueryFor = enhancer.createCountQueryFor(); + + assertThat(countQueryFor).isEqualToIgnoringCase(expected); + } + + static Stream nativeQueriesWithVariables() { + + return Stream.of(Arguments.of( // + "SELECT * FROM User WHERE created_at > $1", // + "SELECT count(1) FROM User WHERE created_at > $1"), // + + Arguments.of( // + "SELECT * FROM (select * from test) ", // + "SELECT count(1) FROM (SELECT * FROM test)"), // + + Arguments.of( // + "SELECT * FROM (select * from test) as test", // + "SELECT count(1) FROM (SELECT * FROM test) AS test")); + } + + @Test + // DATAJPA-1696 + void findProjectionClauseWithIncludedFrom() { + + StringQuery query = new StringQuery("select x, frommage, y from t", true); + + assertThat(createQueryEnhancer(query).getProjection()).isEqualTo("x, frommage, y"); + } + + abstract QueryEnhancer createQueryEnhancer(DeclaredQuery declaredQuery); + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerUnitTests.java index 5c1137aea2..549528160f 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerUnitTests.java @@ -15,9 +15,7 @@ */ package org.springframework.data.jpa.repository.query; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.*; import java.util.Arrays; import java.util.Collections; @@ -47,37 +45,8 @@ class QueryEnhancerUnitTests { private static final String FQ_QUERY = "select u from org.acme.domain.User$Foo_Bar u"; private static final String SIMPLE_QUERY = "from User u"; private static final String COUNT_QUERY = "select count(u) from User u"; - private static final String QUERY_WITH_AS = "select u from User as u where u.username = ?"; - @Test - void createsCountQueryCorrectly() { - assertCountQuery(QUERY, COUNT_QUERY, true); - } - - @Test - void createsCountQueriesCorrectlyForCapitalLetterJPQL() { - - assertCountQuery("FROM User u WHERE u.foo.bar = ?", "select count(u) FROM User u WHERE u.foo.bar = ?", false); - - assertCountQuery("SELECT u FROM User u where u.foo.bar = ?", "select count(u) FROM User u where u.foo.bar = ?", - true); - } - - @Test - void createsCountQueryForDistinctQueries() { - - assertCountQuery("select distinct u from User u where u.foo = ?", - "select count(distinct u) from User u where u.foo = ?", true); - } - - @Test - void createsCountQueryForConstructorQueries() { - - assertCountQuery("select distinct new User(u.name) from User u where u.foo = ?", - "select count(distinct u) from User u where u.foo = ?", false); - } - @Test void createsCountQueryForJoinsNoneNative() { @@ -99,11 +68,6 @@ void createsCountQueryForQueriesWithSubSelects() { "select count(u) from User u left outer join u.roles r where r in (select r from Role)", true); } - @Test - void createsCountQueryForAliasesCorrectly() { - assertCountQuery("select u from User as u", "select count(u) from User as u", true); - } - @Test void allowsShortJpaSyntax() { assertCountQuery(SIMPLE_QUERY, COUNT_QUERY, false); @@ -179,13 +143,6 @@ void appendsIgnoreCaseOrderingCorrectly() { .endsWithIgnoringCase("order by p.lastname asc, lower(p.firstname) asc"); } - @Test // DATAJPA-342 - void usesReturnedVariableInCountProjectionIfSet() { - - assertCountQuery("select distinct m.genre from Media m where m.user = ?1 order by m.genre asc", - "select count(distinct m.genre) from Media m where m.user = ?1", true); - } - @Test // DATAJPA-343 void projectsCountQueriesForQueriesWithSubSelects() { @@ -205,13 +162,6 @@ void doesNotPrefixSortsIfFunction() { .isInstanceOf(InvalidDataAccessApiUsageException.class); } - @Test // DATAJPA-377 - void removesOrderByInGeneratedCountQueryFromOriginalQueryIfPresent() { - - assertCountQuery("select distinct m.genre from Media m where m.user = ?1 OrDer By m.genre ASC", - "select count(distinct m.genre) from Media m where m.user = ?1", true); - } - @Test // DATAJPA-375 void findsExistingOrderByIndependentOfCase() { @@ -222,16 +172,6 @@ void findsExistingOrderByIndependentOfCase() { assertThat(query).endsWithIgnoringCase("ORDER BY p.firstname, p.lastname asc"); } - @Test // DATAJPA-409 - void createsCountQueryForNestedReferenceCorrectly() { - assertCountQuery("select a.b from A a", "select count(a.b) from A a", true); - } - - @Test // DATAJPA-420 - void createsCountQueryForScalarSelects() { - assertCountQuery("select p.lastname,p.firstname from Person p", "select count(p) from Person p", true); - } - @Test // DATAJPA-456 void createCountQueryFromTheGivenCountProjection() { @@ -482,30 +422,6 @@ void detectsAliasWithGroupAndOrderBy() { assertThat(getEnhancer(queryWithOrderAlias).detectAlias()).isEqualTo("u"); } - @Test // DATAJPA-1500 - void createCountQuerySupportsWhitespaceCharacters() { - - StringQuery query = new StringQuery("select * from User user\n" + // - " where user.age = 18\n" + // - " order by user.name\n ", true); - - assertThat(getEnhancer(query).createCountQueryFor()) - .isEqualToIgnoringCase("select count(user) from User user where user.age = 18"); - } - - @Test - void createCountQuerySupportsLineBreaksInSelectClause() { - - StringQuery query = new StringQuery("select user.age,\n" + // - " user.name\n" + // - " from User user\n" + // - " where user.age = 18\n" + // - " order\nby\nuser.name\n ", true); - - assertThat(getEnhancer(query).createCountQueryFor()) - .isEqualToIgnoringCase("select count(user) from User user where user.age = 18"); - } - @Test // DATAJPA-1061 void appliesSortCorrectlyForFieldAliases() { @@ -627,52 +543,6 @@ void findProjectionClauseWithSubselectNative() { assertThat(getEnhancer(query).getProjection()).isEqualTo("*"); } - @Test // DATAJPA-1696 - void findProjectionClauseWithIncludedFrom() { - - StringQuery query = new StringQuery("select x, frommage, y from t", true); - - assertThat(getEnhancer(query).getProjection()).isEqualTo("x, frommage, y"); - } - - @Test - void countProjectionDistinctQueryIncludesNewLineAfterFromAndBeforeJoin() { - - StringQuery originalQuery = new StringQuery( - "SELECT DISTINCT entity1\nFROM Entity1 entity1\nLEFT JOIN Entity2 entity2 ON entity1.key = entity2.key", true); - - assertCountQuery(originalQuery, - "select count(DISTINCT entity1) FROM Entity1 entity1 LEFT JOIN Entity2 entity2 ON entity1.key = entity2.key"); - } - - @Test - void countProjectionDistinctQueryIncludesNewLineAfterEntity() { - - StringQuery originalQuery = new StringQuery( - "SELECT DISTINCT entity1\nFROM Entity1 entity1 LEFT JOIN Entity2 entity2 ON entity1.key = entity2.key", true); - - assertCountQuery(originalQuery, - "select count(DISTINCT entity1) FROM Entity1 entity1 LEFT JOIN Entity2 entity2 ON entity1.key = entity2.key"); - } - - @Test - void countProjectionDistinctQueryIncludesNewLineAfterEntityAndBeforeWhere() { - - StringQuery originalQuery = new StringQuery( - "SELECT DISTINCT entity1\nFROM Entity1 entity1 LEFT JOIN Entity2 entity2 ON entity1.key = entity2.key\nwhere entity1.id = 1799", - true); - - assertCountQuery(originalQuery, - "select count(DISTINCT entity1) FROM Entity1 entity1 LEFT JOIN Entity2 entity2 ON entity1.key = entity2.key where entity1.id = 1799"); - } - - @Test - void createsCountQueriesCorrectlyForCapitalLetter() { - - assertCountQuery("SELECT u FROM User u where u.foo.bar = ?", "select count(u) FROM User u where u.foo.bar = ?", - true); - } - @ParameterizedTest // DATAJPA-252 @MethodSource("detectsJoinAliasesCorrectlySource") void detectsJoinAliasesCorrectly(String queryString, List aliases) { @@ -686,7 +556,6 @@ void detectsJoinAliasesCorrectly(String queryString, List aliases) { assertThat(nonNativeJoinAliases).containsAll(nativeJoinAliases); assertThat(nativeJoinAliases).hasSameSizeAs(aliases) // .containsAll(aliases); - } @Test // GH-2441 @@ -717,26 +586,6 @@ void correctApplySortOnComplexNestedFunctionQuery() { assertThat(result).containsIgnoringCase("order by dd.institutesIds"); } - @Test // GH-2511 - void countQueryUsesCorrectVariable() { - - StringQuery nativeQuery = new StringQuery("SELECT * FROM User WHERE created_at > $1", true); - - QueryEnhancer queryEnhancer = getEnhancer(nativeQuery); - String countQueryFor = queryEnhancer.createCountQueryFor(); - assertThat(countQueryFor).isEqualTo("SELECT count(*) FROM User WHERE created_at > $1"); - - nativeQuery = new StringQuery("SELECT * FROM (select * from test) ", true); - queryEnhancer = getEnhancer(nativeQuery); - countQueryFor = queryEnhancer.createCountQueryFor(); - assertThat(countQueryFor).isEqualTo("SELECT count(*) FROM (SELECT * FROM test)"); - - nativeQuery = new StringQuery("SELECT * FROM (select * from test) as test", true); - queryEnhancer = getEnhancer(nativeQuery); - countQueryFor = queryEnhancer.createCountQueryFor(); - assertThat(countQueryFor).isEqualTo("SELECT count(test) FROM (SELECT * FROM test) AS test"); - } - @Test // GH-2555 void modifyingQueriesAreDetectedCorrectly() { @@ -757,143 +606,6 @@ void modifyingQueriesAreDetectedCorrectly() { assertThat(QueryEnhancerFactory.forQuery(modiQuery).createCountQueryFor()).isEqualToIgnoringCase(modifyingQuery); } - @Test // GH-2578 - void setOperationListWorksWithJSQLParser() { - - String setQuery = "select SOME_COLUMN from SOME_TABLE where REPORTING_DATE = :REPORTING_DATE \n" // - + "except \n" // - + "select SOME_COLUMN from SOME_OTHER_TABLE where REPORTING_DATE = :REPORTING_DATE"; - - StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); - - assertThat(stringQuery.getAlias()).isNullOrEmpty(); - assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); - assertThat(stringQuery.hasConstructorExpression()).isFalse(); - - assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase(setQuery); - assertThat(queryEnhancer.applySorting(Sort.by("SOME_COLUMN"))).endsWith("ORDER BY SOME_COLUMN ASC"); - assertThat(queryEnhancer.getJoinAliases()).isEmpty(); - assertThat(queryEnhancer.detectAlias()).isNullOrEmpty(); - assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); - assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); - } - - @Test // GH-2578 - void complexSetOperationListWorksWithJSQLParser() { - - String setQuery = "select SOME_COLUMN from SOME_TABLE where REPORTING_DATE = :REPORTING_DATE \n" // - + "except \n" // - + "select SOME_COLUMN from SOME_OTHER_TABLE where REPORTING_DATE = :REPORTING_DATE \n" // - + "union select SOME_COLUMN from SOME_OTHER_OTHER_TABLE"; - - StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); - - assertThat(stringQuery.getAlias()).isNullOrEmpty(); - assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); - assertThat(stringQuery.hasConstructorExpression()).isFalse(); - - assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase(setQuery); - assertThat(queryEnhancer.applySorting(Sort.by("SOME_COLUMN").ascending())).endsWith("ORDER BY SOME_COLUMN ASC"); - assertThat(queryEnhancer.getJoinAliases()).isEmpty(); - assertThat(queryEnhancer.detectAlias()).isNullOrEmpty(); - assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase("SOME_COLUMN"); - assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); - } - - @Test // GH-2578 - void deeplyNestedcomplexSetOperationListWorksWithJSQLParser() { - - String setQuery = "SELECT CustomerID FROM (\n" // - + "\t\t\tselect * from Customers\n" // - + "\t\t\texcept\n"// - + "\t\t\tselect * from Customers where country = 'Austria'\n"// - + "\t)\n" // - + "\texcept\n"// - + "\tselect CustomerID from customers where country = 'Germany'\n"// - + "\t;"; - - StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); - - assertThat(stringQuery.getAlias()).isNullOrEmpty(); - assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("CustomerID"); - assertThat(stringQuery.hasConstructorExpression()).isFalse(); - - assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase(setQuery); - assertThat(queryEnhancer.applySorting(Sort.by("CustomerID").descending())).endsWith("ORDER BY CustomerID DESC"); - assertThat(queryEnhancer.getJoinAliases()).isEmpty(); - assertThat(queryEnhancer.detectAlias()).isNullOrEmpty(); - assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase("CustomerID"); - assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); - } - - @Test // GH-2578 - void valuesStatementsWorksWithJSQLParser() { - - String setQuery = "VALUES (1, 2, 'test')"; - - StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); - - assertThat(stringQuery.getAlias()).isNullOrEmpty(); - assertThat(stringQuery.getProjection()).isNullOrEmpty(); - assertThat(stringQuery.hasConstructorExpression()).isFalse(); - - assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase(setQuery); - assertThat(queryEnhancer.applySorting(Sort.by("CustomerID").descending())).isEqualTo(setQuery); - assertThat(queryEnhancer.getJoinAliases()).isEmpty(); - assertThat(queryEnhancer.detectAlias()).isNullOrEmpty(); - assertThat(queryEnhancer.getProjection()).isNullOrEmpty(); - assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); - } - - @Test // GH-2578 - void withStatementsWorksWithJSQLParser() { - - String setQuery = "with sample_data(day, value) as (values ((0, 13), (1, 12), (2, 15), (3, 4), (4, 8), (5, 16))) \n" - + "select day, value from sample_data as a"; - - StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); - - assertThat(stringQuery.getAlias()).isEqualToIgnoringCase("a"); - assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("day, value"); - assertThat(stringQuery.hasConstructorExpression()).isFalse(); - - assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase( - "with sample_data (day, value) AS (VALUES ((0, 13), (1, 12), (2, 15), (3, 4), (4, 8), (5, 16)))\n" - + "SELECT count(a) FROM sample_data AS a"); - assertThat(queryEnhancer.applySorting(Sort.by("day").descending())).endsWith("ORDER BY a.day DESC"); - assertThat(queryEnhancer.getJoinAliases()).isEmpty(); - assertThat(queryEnhancer.detectAlias()).isEqualToIgnoringCase("a"); - assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase("day, value"); - assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); - } - - @Test // GH-2578 - void multipleWithStatementsWorksWithJSQLParser() { - - String setQuery = "with sample_data(day, value) as (values ((0, 13), (1, 12), (2, 15), (3, 4), (4, 8), (5, 16))), test2 as (values (1,2,3)) \n" - + "select day, value from sample_data as a"; - - StringQuery stringQuery = new StringQuery(setQuery, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); - - assertThat(stringQuery.getAlias()).isEqualToIgnoringCase("a"); - assertThat(stringQuery.getProjection()).isEqualToIgnoringCase("day, value"); - assertThat(stringQuery.hasConstructorExpression()).isFalse(); - - assertThat(queryEnhancer.createCountQueryFor()).isEqualToIgnoringCase( - "with sample_data (day, value) AS (VALUES ((0, 13), (1, 12), (2, 15), (3, 4), (4, 8), (5, 16))),test2 AS (VALUES (1, 2, 3))\n" - + "SELECT count(a) FROM sample_data AS a"); - assertThat(queryEnhancer.applySorting(Sort.by("day").descending())).endsWith("ORDER BY a.day DESC"); - assertThat(queryEnhancer.getJoinAliases()).isEmpty(); - assertThat(queryEnhancer.detectAlias()).isEqualToIgnoringCase("a"); - assertThat(queryEnhancer.getProjection()).isEqualToIgnoringCase("day, value"); - assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); - } @ParameterizedTest // GH-2593 @MethodSource("insertStatementIsProcessedSameAsDefaultSource") @@ -926,21 +638,7 @@ void insertStatementIsProcessedSameAsDefault(String insertQuery) { assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); } - @ParameterizedTest // GH-2641 - @MethodSource("mergeStatementWorksWithJSqlParserSource") - void mergeStatementWorksWithJSqlParser(String query, String alias) { - - StringQuery stringQuery = new StringQuery(query, true); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); - assertThat(queryEnhancer.detectAlias()).isEqualTo(alias); - assertThat(QueryUtils.detectAlias(query)).isNull(); - - assertThat(queryEnhancer.getJoinAliases()).isEmpty(); - assertThat(queryEnhancer.detectAlias()).isEqualTo(alias); - assertThat(queryEnhancer.getProjection()).isEmpty(); - assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); - } public static Stream insertStatementIsProcessedSameAsDefaultSource() { @@ -950,17 +648,6 @@ public static Stream insertStatementIsProcessedSameAsDefaultSource() ); } - public static Stream mergeStatementWorksWithJSqlParserSource() { - - return Stream.of( // - Arguments.of( - "merge into a using (select id, value from b) query on (a.id = query.id) when matched then update set a.value = value", - "query"), - Arguments.of( - "merge into a using (select id2, value from b) on (id = id2) when matched then update set a.value = value", - null)); - } - public static Stream detectsJoinAliasesCorrectlySource() { return Stream.of( // @@ -985,4 +672,5 @@ private static void assertCountQuery(StringQuery originalQuery, String countQuer private static QueryEnhancer getEnhancer(DeclaredQuery query) { return QueryEnhancerFactory.forQuery(query); } + }