Differentiate between JPQL and native queries in count query derivation.

We now consider whether a query is a native one when deriving a count query for pagination. Previously, the generated queries used JPQL syntax that doesn't comply with native SQL syntax rules.

Closes #2773
Original pull request #2777
This commit is contained in:
Mark Paluch
2023-01-25 12:08:19 +01:00
committed by Jens Schauder
parent 6dc008fb05
commit 64b5a22a24
7 changed files with 217 additions and 24 deletions

View File

@@ -46,7 +46,7 @@ public class DefaultQueryEnhancer implements QueryEnhancer {
@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

View File

@@ -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.Statement;
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 class JSqlParserQueryEnhancer implements QueryEnhancer {
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)));

View File

@@ -18,10 +18,23 @@ package org.springframework.data.jpa.repository.query;
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 abstract class QueryUtils {
*/
@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 abstract class QueryUtils {
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));

View File

@@ -0,0 +1,30 @@
/*
* 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;
/**
* TCK Tests for {@link DefaultQueryEnhancer}.
*
* @author Mark Paluch
*/
public class DefaultQueryEnhancerUnitTests extends QueryEnhancerTckTests {
@Override
QueryEnhancer createQueryEnhancer(DeclaredQuery declaredQuery) {
return new DefaultQueryEnhancer(declaredQuery);
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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;
/**
* TCK Tests for {@link JSqlParserQueryEnhancer}.
*
* @author Mark Paluch
*/
public class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests {
@Override
QueryEnhancer createQueryEnhancer(DeclaredQuery declaredQuery) {
return new JSqlParserQueryEnhancer(declaredQuery);
}
}

View File

@@ -0,0 +1,87 @@
/*
* 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.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(null);
assertThat(countQueryFor).isEqualToIgnoringCase(expected);
}
static Stream<Arguments> 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"));
}
@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<Arguments> 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"));
}
abstract QueryEnhancer createQueryEnhancer(DeclaredQuery declaredQuery);
}

View File

@@ -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;
@@ -229,7 +227,12 @@ class QueryEnhancerUnitTests {
@Test // DATAJPA-420
void createsCountQueryForScalarSelects() {
assertCountQuery("select p.lastname,p.firstname from Person p", "select count(p) from Person p", true);
assertCountQuery("select p.lastname,p.firstname from Person p", "select count(p) from Person p", false);
}
@Test // DATAJPA-420
void createsCountQueryForNativeScalarSelects() {
assertCountQuery("select p.lastname,p.firstname from Person p", "select count(1) from Person p", true);
}
@Test // DATAJPA-456
@@ -490,7 +493,7 @@ class QueryEnhancerUnitTests {
" order by user.name\n ", true);
assertThat(getEnhancer(query).createCountQueryFor())
.isEqualToIgnoringCase("select count(user) from User user where user.age = 18");
.isEqualToIgnoringCase("select count(1) from User user where user.age = 18");
}
@Test
@@ -503,7 +506,7 @@ class QueryEnhancerUnitTests {
" order\nby\nuser.name\n ", true);
assertThat(getEnhancer(query).createCountQueryFor())
.isEqualToIgnoringCase("select count(user) from User user where user.age = 18");
.isEqualToIgnoringCase("select count(1) from User user where user.age = 18");
}
@Test // DATAJPA-1061
@@ -724,17 +727,17 @@ class QueryEnhancerUnitTests {
QueryEnhancer queryEnhancer = getEnhancer(nativeQuery);
String countQueryFor = queryEnhancer.createCountQueryFor();
assertThat(countQueryFor).isEqualTo("SELECT count(*) FROM User WHERE created_at > $1");
assertThat(countQueryFor).isEqualTo("SELECT count(1) 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)");
assertThat(countQueryFor).isEqualTo("SELECT count(1) 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");
assertThat(countQueryFor).isEqualTo("SELECT count(1) FROM (SELECT * FROM test) AS test");
}
@Test // GH-2555
@@ -864,7 +867,7 @@ class QueryEnhancerUnitTests {
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");
+ "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");
@@ -887,7 +890,7 @@ class QueryEnhancerUnitTests {
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");
+ "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");
@@ -985,4 +988,5 @@ class QueryEnhancerUnitTests {
private static QueryEnhancer getEnhancer(DeclaredQuery query) {
return QueryEnhancerFactory.forQuery(query);
}
}