diff --git a/spring-data-jpa-performance/pom.xml b/spring-data-jpa-performance/pom.xml index cdf321e07..97b7cdf93 100644 --- a/spring-data-jpa-performance/pom.xml +++ b/spring-data-jpa-performance/pom.xml @@ -85,6 +85,13 @@ test + + com.github.jsqlparser + jsqlparser + ${jsqlparser} + test + + diff --git a/spring-data-jpa-performance/src/main/java/org/springframework/data/jpa/repository/PersonRepository.java b/spring-data-jpa-performance/src/main/java/org/springframework/data/jpa/repository/PersonRepository.java index ea5771bce..ace25ba62 100644 --- a/spring-data-jpa-performance/src/main/java/org/springframework/data/jpa/repository/PersonRepository.java +++ b/spring-data-jpa-performance/src/main/java/org/springframework/data/jpa/repository/PersonRepository.java @@ -27,16 +27,16 @@ import org.springframework.data.repository.ListCrudRepository; */ public interface PersonRepository extends ListCrudRepository { - List findAllByFirstname(String firstname); + List findAllByFirstname(String firstname); - List findAllAndProjectToInterfaceByFirstname(String firstname); + List findAllAndProjectToInterfaceByFirstname(String firstname); - @Query("SELECT p FROM org.springframework.data.jpa.model.Person p WHERE p.firstname = ?1") - List findAllWithAnnotatedQueryByFirstname(String firstname); + @Query("SELECT p FROM org.springframework.data.jpa.model.Person p WHERE p.firstname = ?1") + List findAllWithAnnotatedQueryByFirstname(String firstname); - @Query("SELECT p FROM org.springframework.data.jpa.model.Person p WHERE p.firstname = ?1") - List findAllWithAnnotatedQueryByFirstname(String firstname, Sort sort); + @Query("SELECT p FROM org.springframework.data.jpa.model.Person p WHERE p.firstname = ?1") + List findAllWithAnnotatedQueryByFirstname(String firstname, Sort sort); - @Query(value = "SELECT * FROM person WHERE firstname = ?1", nativeQuery = true) - List findAllWithNativeQueryByFirstname(String firstname); + @Query(value = "SELECT * FROM person WHERE firstname = ?1", nativeQuery = true) + List findAllWithNativeQueryByFirstname(String firstname); } diff --git a/spring-data-jpa-performance/src/test/java/org/springframework/data/jpa/repository/Profiler.java b/spring-data-jpa-performance/src/test/java/org/springframework/data/jpa/repository/Profiler.java index eef1b9bae..f394ae6f8 100644 --- a/spring-data-jpa-performance/src/test/java/org/springframework/data/jpa/repository/Profiler.java +++ b/spring-data-jpa-performance/src/test/java/org/springframework/data/jpa/repository/Profiler.java @@ -15,26 +15,37 @@ */ package org.springframework.data.jpa.repository; +import java.lang.reflect.Method; + import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.repository.query.JpaQueryMethod; +import org.springframework.data.jpa.repository.query.StringQuery; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; /** * @author Mark Paluch */ public class Profiler { - public static void main(String[] args) throws InterruptedException { + public static void main(String[] args) throws Exception { - RepositoryFinderTests tests = new RepositoryFinderTests(); - RepositoryFinderTests.BenchmarkParameters params = new RepositoryFinderTests.BenchmarkParameters(); - params.doSetup(); + DefaultRepositoryMetadata art = new DefaultRepositoryMetadata(PersonRepository.class); + Method method = PersonRepository.class.getMethod("findAllWithAnnotatedQueryByFirstname", String.class, Sort.class); + ProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); System.out.println("Ready. Waiting 10sec"); Thread.sleep(10000); System.out.println("Go!"); + while (true) { - params.repositoryProxy.findAllWithAnnotatedQueryByFirstname("first", Sort.by("firstname")); + + JpaQueryMethod queryMethod = new JpaQueryMethod(method, art, projectionFactory, PersistenceProvider.HIBERNATE); + StringQuery stringQuery = new StringQuery(queryMethod.getRequiredAnnotatedQuery(), false); } } diff --git a/spring-data-jpa-performance/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerTests.java b/spring-data-jpa-performance/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerTests.java new file mode 100644 index 000000000..33a462cfe --- /dev/null +++ b/spring-data-jpa-performance/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2024 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 jmh.mbr.junit5.Microbenchmark; + +import java.io.IOException; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Timeout; +import org.openjdk.jmh.annotations.Warmup; + +import org.springframework.data.domain.Sort; + +/** + * @author Mark Paluch + */ +@Microbenchmark +@Fork(1) +@Warmup(time = 2, iterations = 3) +@Measurement(time = 2) +@Timeout(time = 2) +public class JSqlParserQueryEnhancerTests { + + @State(Scope.Benchmark) + public static class BenchmarkParameters { + + JSqlParserQueryEnhancer enhancer; + Sort sort = Sort.by("foo"); + private byte[] serialized; + + @Setup(Level.Iteration) + public void doSetup() throws IOException { + + String s = """ + select SOME_COLUMN from SOME_TABLE where REPORTING_DATE = :REPORTING_DATE + except + select SOME_COLUMN from SOME_OTHER_TABLE where REPORTING_DATE = :REPORTING_DATE + union select SOME_COLUMN from SOME_OTHER_OTHER_TABLE"""; + + enhancer = new JSqlParserQueryEnhancer(DeclaredQuery.of(s, true)); + + } + } + + @Benchmark + public Object applySortWithParsing(BenchmarkParameters p) { + return p.enhancer.applySorting(p.sort); + } + +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java index 750de16c6..4e5442440 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/DeclaredQuery.java @@ -40,15 +40,6 @@ interface DeclaredQuery { return ObjectUtils.isEmpty(query) ? EmptyDeclaredQuery.EMPTY_QUERY : new StringQuery(query, nativeQuery); } - static boolean hasNamedParameter(String query) { - - if (ObjectUtils.isEmpty(query)) { - return false; - } - - return StringQuery.hasNamedParameter(query); - } - /** * @return whether the underlying query has at least one named parameter. */ 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 3053bb95f..bc66d1f3b 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 @@ -29,9 +29,22 @@ import org.springframework.lang.Nullable; public class DefaultQueryEnhancer implements QueryEnhancer { private final DeclaredQuery query; + private final boolean hasConstructorExpression; + private final String alias; + private final String projection; + private final Set joinAliases; public DefaultQueryEnhancer(DeclaredQuery query) { this.query = query; + this.hasConstructorExpression = QueryUtils.hasConstructorExpression(query.getQueryString()); + this.alias = QueryUtils.detectAlias(query.getQueryString()); + this.projection = QueryUtils.getProjection(this.query.getQueryString()); + this.joinAliases = QueryUtils.getOuterJoinAliases(this.query.getQueryString()); + } + + @Override + public String applySorting(Sort sort) { + return QueryUtils.applySorting(this.query.getQueryString(), sort, this.alias); } @Override @@ -39,24 +52,29 @@ public class DefaultQueryEnhancer implements QueryEnhancer { return QueryUtils.applySorting(this.query.getQueryString(), sort, alias); } - @Override - public String detectAlias() { - return QueryUtils.detectAlias(this.query.getQueryString()); - } - @Override public String createCountQueryFor(@Nullable String countProjection) { return QueryUtils.createCountQueryFor(this.query.getQueryString(), countProjection, this.query.isNativeQuery()); } + @Override + public boolean hasConstructorExpression() { + return this.hasConstructorExpression; + } + + @Override + public String detectAlias() { + return this.alias; + } + @Override public String getProjection() { - return QueryUtils.getProjection(this.query.getQueryString()); + return this.projection; } @Override public Set getJoinAliases() { - return QueryUtils.getOuterJoinAliases(this.query.getQueryString()); + return this.joinAliases; } @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryParser.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryParser.java deleted file mode 100644 index 7f029c46c..000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryParser.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2023-2024 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; - -/** - * Implements the {@code EQL} parsing operations of a {@link JpaQueryParser} using the ANTLR-generated - * {@link EqlParser}. - * - * @author Greg Turnquist - * @author Mark Paluch - * @since 3.2 - */ -class EqlQueryParser extends JpaQueryParser { - - private EqlQueryParser(String query) { - super(parse(query, EqlLexer::new, EqlParser::new, EqlParser::start), new EqlQueryIntrospector(), - EqlSortedQueryTransformer::new, EqlCountQueryTransformer::new); - } - - /** - * Parse a EQL query. - * - * @param query - * @return the query parser. - * @throws BadJpqlGrammarException - */ - public static EqlQueryParser parseQuery(String query) throws BadJpqlGrammarException { - return new EqlQueryParser(query); - } - -} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryParser.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryParser.java deleted file mode 100644 index 96b8b4aaf..000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryParser.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2022-2024 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; - -/** - * Implements the {@code HQL} parsing operations of a {@link JpaQueryParser} using the ANTLR-generated {@link HqlParser} - * and {@link HqlSortedQueryTransformer}. - * - * @author Greg Turnquist - * @author Mark Paluch - * @since 3.1 - */ -class HqlQueryParser extends JpaQueryParser { - - private HqlQueryParser(String query) { - super(parse(query, HqlLexer::new, HqlParser::new, HqlParser::start), new HqlQueryIntrospector(), - HqlSortedQueryTransformer::new, HqlCountQueryTransformer::new); - } - - /** - * Parse a HQL query. - * - * @param query - * @return the query parser. - * @throws BadJpqlGrammarException - */ - public static HqlQueryParser parseQuery(String query) throws BadJpqlGrammarException { - return new HqlQueryParser(query); - } - -} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java index 2e86c4c9c..0ecbaf61a 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java @@ -294,6 +294,10 @@ class HqlQueryRenderer extends HqlBaseVisitor { @Override public QueryRendererBuilder visitQueryOrder(HqlParser.QueryOrderContext ctx) { + if (ctx.limitClause() == null && ctx.offsetClause() == null && ctx.fetchClause() == null) { + return visit(ctx.orderByClause()); + } + QueryRendererBuilder builder = QueryRenderer.builder(); builder.appendExpression(visit(ctx.orderByClause())); @@ -406,14 +410,16 @@ class HqlQueryRenderer extends HqlBaseVisitor { @Override public QueryRendererBuilder visitJoinPath(HqlParser.JoinPathContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); + HqlParser.VariableContext variable = ctx.variable(); - builder.appendExpression(visit(ctx.path())); - - if (ctx.variable() != null) { - builder.appendExpression(visit(ctx.variable())); + if (variable == null) { + return visit(ctx.path()); } + QueryRendererBuilder builder = QueryRenderer.builder(); + builder.appendExpression(visit(ctx.path())); + builder.appendExpression(visit(variable)); + return builder; } @@ -461,14 +467,16 @@ class HqlQueryRenderer extends HqlBaseVisitor { @Override public QueryRendererBuilder visitTargetEntity(HqlParser.TargetEntityContext ctx) { - QueryRendererBuilder builder = QueryRenderer.builder(); + HqlParser.VariableContext variable = ctx.variable(); - builder.appendExpression(visit(ctx.entityName())); - - if (ctx.variable() != null) { - builder.appendExpression(visit(ctx.variable())); + if (variable == null) { + return visit(ctx.entityName()); } + QueryRendererBuilder builder = QueryRenderer.builder(); + builder.appendExpression(visit(ctx.entityName())); + builder.appendExpression(visit(variable)); + return builder; } 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 4ea5c2f5b..18bb07dbc 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 @@ -28,6 +28,7 @@ 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.Join; import net.sf.jsqlparser.statement.select.OrderByElement; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.Select; @@ -36,18 +37,21 @@ import net.sf.jsqlparser.statement.select.SetOperationList; import net.sf.jsqlparser.statement.select.Values; import net.sf.jsqlparser.statement.update.Update; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; 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 java.util.StringJoiner; import org.springframework.data.domain.Sort; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import org.springframework.util.SerializationUtils; import org.springframework.util.StringUtils; /** @@ -65,6 +69,12 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { private final DeclaredQuery query; private final Statement statement; private final ParsedType parsedType; + private final boolean hasConstructorExpression; + private final @Nullable String primaryAlias; + private final String projection; + private final Set joinAliases; + private final Set selectAliases; + private final byte[] serialized; /** * @param query the query we want to enhance. Must not be {@literal null}. @@ -72,13 +82,150 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { public JSqlParserQueryEnhancer(DeclaredQuery query) { this.query = query; - try { - this.statement = CCJSqlParserUtil.parse(this.query.getQueryString()); - } catch (JSQLParserException e) { - throw new IllegalArgumentException("The query is not a valid SQL Query", e); - } + this.statement = parseStatement(query.getQueryString(), Statement.class); this.parsedType = detectParsedType(statement); + this.hasConstructorExpression = QueryUtils.hasConstructorExpression(query.getQueryString()); + this.primaryAlias = detectAlias(this.parsedType, this.statement); + this.projection = detectProjection(this.statement); + this.selectAliases = Collections.unmodifiableSet(getSelectionAliases(this.statement)); + this.joinAliases = Collections.unmodifiableSet(getJoinAliases(this.statement)); + this.serialized = SerializationUtils.serialize(this.statement); + } + + /** + * Parses a query string with JSqlParser. + * + * @param query the query to parse + * @return the parsed query + */ + private static T parseStatement(String query, Class classOfT) { + + try { + return classOfT.cast(CCJSqlParserUtil.parse(query)); + } catch (JSQLParserException e) { + throw new IllegalArgumentException("The query you provided is not a valid SQL Query", e); + } + } + + /** + * Resolves the alias for the entity to be retrieved from the given JPA query. Note that you only provide valid Query + * strings. Things such as from User u will throw an {@link IllegalArgumentException}. + * + * @return Might return {@literal null}. + */ + @Nullable + private static String detectAlias(ParsedType parsedType, Statement statement) { + + if (ParsedType.MERGE.equals(parsedType)) { + + Merge mergeStatement = (Merge) statement; + + Alias alias = mergeStatement.getUsingAlias(); + return alias == null ? null : alias.getName(); + + } + + if (ParsedType.SELECT.equals(parsedType)) { + + Select selectStatement = (Select) statement; + + /* + * For all the other types ({@link ValuesStatement} and {@link SetOperationList}) it does not make sense to provide + * alias since: + * ValuesStatement has no alias + * SetOperation can have multiple alias for each operation item + */ + if (!(selectStatement instanceof PlainSelect selectBody)) { + return null; + } + + if (selectBody.getFromItem() == null) { + return null; + } + + Alias alias = selectBody.getFromItem().getAlias(); + return alias == null ? null : alias.getName(); + } + + return null; + } + + /** + * Returns the aliases used inside the selection part in the query. + * + * @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}. + */ + private static Set getSelectionAliases(Statement statement) { + + if (!(statement instanceof PlainSelect select) || CollectionUtils.isEmpty(select.getSelectItems())) { + return Collections.emptySet(); + } + + Set set = new HashSet<>(select.getSelectItems().size()); + + for (SelectItem selectItem : select.getSelectItems()) { + Alias alias = selectItem.getAlias(); + if (alias != null) { + set.add(alias.getName()); + } + } + + return set; + } + + /** + * Returns the aliases used for {@code join}s. + * + * @return a {@literal Set} of aliases used in the query. Guaranteed to be not {@literal null}. + */ + private static Set getJoinAliases(Statement statement) { + + if (!(statement instanceof PlainSelect selectBody) || CollectionUtils.isEmpty(selectBody.getJoins())) { + return Collections.emptySet(); + } + + Set set = new HashSet<>(selectBody.getJoins().size()); + + for (Join join : selectBody.getJoins()) { + Alias alias = join.getRightItem().getAlias(); + if (alias != null) { + set.add(alias.getName()); + } + } + + return set; + + } + + private static String detectProjection(Statement statement) { + + if (!(statement instanceof Select select)) { + return ""; + } + + if (select instanceof Values) { + return ""; + } + + Select selectBody = select; + + if (select instanceof SetOperationList setOperationList) { + + // using the first one since for setoperations the projection has to be the same + selectBody = setOperationList.getSelects().get(0); + + if (!(selectBody instanceof PlainSelect)) { + return ""; + } + } + + StringJoiner joiner = new StringJoiner(", "); + for (SelectItem selectItem : ((PlainSelect) selectBody).getSelectItems()) { + joiner.add(selectItem.toString()); + } + return joiner.toString().trim(); + } /** @@ -103,259 +250,77 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { } } + @Override + public boolean hasConstructorExpression() { + return hasConstructorExpression; + } + + @Override + public String detectAlias() { + return this.primaryAlias; + } + + @Override + public String getProjection() { + return this.projection; + } + + @Override + public Set getJoinAliases() { + return joinAliases; + } + + public Set getSelectionAliases() { + return selectAliases; + } + + @Override + public DeclaredQuery getQuery() { + return this.query; + } + + @Override + public String applySorting(Sort sort) { + return applySorting(sort, detectAlias()); + } + @Override public String applySorting(Sort sort, @Nullable String alias) { String queryString = query.getQueryString(); Assert.hasText(queryString, "Query must not be null or empty"); - if (this.parsedType != ParsedType.SELECT) { + if (this.parsedType != ParsedType.SELECT || sort.isUnsorted()) { return queryString; } - if (sort.isUnsorted()) { - return queryString; - } + return applySorting((Select) deserialize(this.serialized), sort, alias); + } - Select selectStatement = parseSelectStatement(queryString); + private String applySorting(Select selectStatement, Sort sort, @Nullable String alias) { if (selectStatement instanceof SetOperationList setOperationList) { return applySortingToSetOperationList(setOperationList, sort); } if (!(selectStatement instanceof PlainSelect selectBody)) { - return queryString; + return selectStatement.toString(); } - Set joinAliases = getJoinAliases(selectBody); - Set selectionAliases = getSelectionAliases(selectBody); - - List orderByElements = sort.stream() // - .map(order -> getOrderClause(joinAliases, selectionAliases, alias, order)) // - .toList(); + List orderByElements = new ArrayList<>(16); + for (Sort.Order order : sort) { + orderByElements.add(getOrderClause(joinAliases, selectAliases, alias, order)); + } if (CollectionUtils.isEmpty(selectBody.getOrderByElements())) { - selectBody.setOrderByElements(new ArrayList<>()); + selectBody.setOrderByElements(orderByElements); + } else { + selectBody.getOrderByElements().addAll(orderByElements); } - selectBody.getOrderByElements().addAll(orderByElements); - return selectStatement.toString(); } - /** - * Returns the {@link SetOperationList} as a string query with {@link Sort}s applied in the right order. - * - * @param setOperationListStatement - * @param sort - * @return - */ - private String applySortingToSetOperationList(SetOperationList setOperationListStatement, Sort sort) { - - // special case: ValuesStatements are detected as nested OperationListStatements - if (setOperationListStatement.getSelects().stream().anyMatch(Values.class::isInstance)) { - return setOperationListStatement.toString(); - } - - // if (CollectionUtils.isEmpty(setOperationListStatement.getOrderByElements())) { - if (setOperationListStatement.getOrderByElements() == null) { - setOperationListStatement.setOrderByElements(new ArrayList<>()); - } - - List orderByElements = sort.stream() // - .map(order -> getOrderClause(Collections.emptySet(), Collections.emptySet(), null, order)) // - .toList(); - setOperationListStatement.getOrderByElements().addAll(orderByElements); - - return setOperationListStatement.toString(); - } - - /** - * Returns the aliases used inside the selection part in the query. - * - * @param selectBody a {@link PlainSelect} containing a query. Must not be {@literal null}. - * @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}. - */ - private Set getSelectionAliases(PlainSelect selectBody) { - - if (CollectionUtils.isEmpty(selectBody.getSelectItems())) { - return new HashSet<>(); - } - - return selectBody.getSelectItems().stream() // - .filter(SelectItem.class::isInstance) // - .map(item -> ((SelectItem) item).getAlias()) // - .filter(Objects::nonNull) // - .map(Alias::getName) // - .collect(Collectors.toSet()); - } - - /** - * Returns the aliases used inside the selection part in the query. - * - * @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}. - */ - Set getSelectionAliases() { - - if (this.parsedType != ParsedType.SELECT) { - return new HashSet<>(); - } - - return this.getSelectionAliases((PlainSelect) statement); - } - - /** - * Returns the aliases used for {@code join}s. - * - * @param query a query string to extract the aliases of joins from. Must not be {@literal null}. - * @return a {@literal Set} of aliases used in the query. Guaranteed to be not {@literal null}. - */ - private Set getJoinAliases(String query) { - - if (this.parsedType != ParsedType.SELECT) { - return new HashSet<>(); - } - - Select selectStatement = (Select) statement; - if (selectStatement instanceof PlainSelect selectBody) { - return getJoinAliases(selectBody); - } - - return new HashSet<>(); - } - - /** - * Returns the aliases used for {@code join}s. - * - * @param selectBody the selection body to extract the aliases of joins from. Must not be {@literal null}. - * @return a {@literal Set} of aliases used in the query. Guaranteed to be not {@literal null}. - */ - private Set getJoinAliases(PlainSelect selectBody) { - - if (CollectionUtils.isEmpty(selectBody.getJoins())) { - return new HashSet<>(); - } - - return selectBody.getJoins().stream() // - .map(join -> join.getRightItem().getAlias()) // - .filter(Objects::nonNull) // - .map(Alias::getName) // - .collect(Collectors.toSet()); - } - - /** - * Returns the order clause for the given {@link Sort.Order}. Will prefix the clause with the given alias if the - * referenced property refers to a join alias, i.e. starts with {@code $alias.}. - * - * @param joinAliases the join aliases of the original query. Must not be {@literal null}. - * @param alias the alias for the root entity. May be {@literal null}. - * @param order the order object to build the clause for. Must not be {@literal null}. - * @return a {@link OrderByElement} containing an order clause. Guaranteed to be not {@literal null}. - */ - private OrderByElement getOrderClause(final Set joinAliases, final Set selectionAliases, - @Nullable final String alias, final Sort.Order order) { - - final OrderByElement orderByElement = new OrderByElement(); - orderByElement.setAsc(order.getDirection().isAscending()); - orderByElement.setAscDescPresent(true); - - final String property = order.getProperty(); - - checkSortExpression(order); - - if (selectionAliases.contains(property)) { - Expression orderExpression = order.isIgnoreCase() ? getJSqlLower(property) : new Column(property); - - orderByElement.setExpression(orderExpression); - return orderByElement; - } - - boolean qualifyReference = joinAliases // - .parallelStream() // - .map(joinAlias -> joinAlias.concat(".")) // - .noneMatch(property::startsWith); - - boolean functionIndicator = property.contains("("); - - String reference = qualifyReference && !functionIndicator && StringUtils.hasText(alias) - ? String.format("%s.%s", alias, property) - : property; - Expression orderExpression = order.isIgnoreCase() ? getJSqlLower(reference) : new Column(reference); - orderByElement.setExpression(orderExpression); - return orderByElement; - } - - @Override - public String detectAlias() { - return detectAlias(this.query.getQueryString()); - } - - /** - * Resolves the alias for the entity to be retrieved from the given JPA query. Note that you only provide valid Query - * strings. Things such as from User u will throw an {@link IllegalArgumentException}. - * - * @param query must not be {@literal null}. - * @return Might return {@literal null}. - */ - @Nullable - private String detectAlias(String query) { - - if (ParsedType.MERGE.equals(this.parsedType)) { - - Merge mergeStatement = (Merge) statement; - return detectAlias(mergeStatement); - - } else if (ParsedType.SELECT.equals(this.parsedType)) { - - Select selectStatement = (Select) statement; - - /* - * For all the other types ({@link ValuesStatement} and {@link SetOperationList}) it does not make sense to provide - * alias since: - * ValuesStatement has no alias - * SetOperation can have multiple alias for each operation item - */ - if (!(selectStatement instanceof PlainSelect selectBody)) { - return null; - } - - return detectAlias(selectBody); - } - - return null; - } - - /** - * Resolves the alias for the entity to be retrieved from the given {@link PlainSelect}. Note that you only provide - * valid Query strings. Things such as from User u will throw an {@link IllegalArgumentException}. - * - * @param selectBody must not be {@literal null}. - * @return Might return {@literal null}. - */ - @Nullable - private String detectAlias(PlainSelect selectBody) { - - if (selectBody.getFromItem() == null) { - return null; - } - - Alias alias = selectBody.getFromItem().getAlias(); - return alias == null ? null : alias.getName(); - } - - /** - * Resolves the alias for the given {@link Merge} statement. - * - * @param mergeStatement must not be {@literal null}. - * @return Might return {@literal null}. - */ - @Nullable - private String detectAlias(Merge mergeStatement) { - - Alias alias = mergeStatement.getUsingAlias(); - return alias == null ? null : alias.getName(); - } - @Override public String createCountQueryFor(@Nullable String countProjection) { @@ -365,111 +330,112 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { Assert.hasText(this.query.getQueryString(), "OriginalQuery must not be null or empty"); - Select selectStatement = parseSelectStatement(this.query.getQueryString()); - + Statement statement = (Statement) deserialize(this.serialized); /* We only support count queries for {@link PlainSelect}. */ - if (!(selectStatement instanceof PlainSelect selectBody)) { + if (!(statement instanceof PlainSelect selectBody)) { return this.query.getQueryString(); } + return createCountQueryFor(this.query, selectBody, countProjection); + } + + private static String createCountQueryFor(DeclaredQuery query, PlainSelect selectBody, + @Nullable String countProjection) { + // remove order by selectBody.setOrderByElements(null); if (StringUtils.hasText(countProjection)) { - Function jSqlCount = getJSqlCount(Collections.singletonList(countProjection), false); + selectBody.setSelectItems( + Collections.singletonList(SelectItem.from(getJSqlCount(Collections.singletonList(countProjection), false)))); + } else { + + boolean distinct = selectBody.getDistinct() != null; + selectBody.setDistinct(null); // reset possible distinct + + Function jSqlCount = getJSqlCount( + Collections.singletonList(countPropertyNameForSelection(selectBody.getSelectItems(), distinct)), distinct); selectBody.setSelectItems(Collections.singletonList(SelectItem.from(jSqlCount))); - return selectBody.toString(); } - boolean distinct = selectBody.getDistinct() != null; - selectBody.setDistinct(null); // reset possible distinct - - String tableAlias = detectAlias(selectBody); - String countProperty = countPropertyNameForSelection(selectBody.getSelectItems(), distinct, tableAlias); - - Function jSqlCount = getJSqlCount(Collections.singletonList(countProperty), distinct); - selectBody.setSelectItems(Collections.singletonList(SelectItem.from(jSqlCount))); - return selectBody.toString(); } - @Override - public String getProjection() { + /** + * Returns the {@link SetOperationList} as a string query with {@link Sort}s applied in the right order. + * + * @param setOperationListStatement + * @param sort + * @return + */ + private static String applySortingToSetOperationList(SetOperationList setOperationListStatement, Sort sort) { - if (this.parsedType != ParsedType.SELECT) { - return ""; - } - - Assert.hasText(query.getQueryString(), "Query must not be null or empty"); - - Select selectStatement = (Select) statement; - - if (selectStatement instanceof Values) { - return ""; - } - - Select selectBody = selectStatement; - - if (selectStatement instanceof SetOperationList setOperationList) { - - // using the first one since for setoperations the projection has to be the same - selectBody = setOperationList.getSelects().get(0); - - if (!(selectBody instanceof PlainSelect)) { - return ""; + // special case: ValuesStatements are detected as nested OperationListStatements + for (Select select : setOperationListStatement.getSelects()) { + if (select instanceof Values) { + return setOperationListStatement.toString(); } } - return ((PlainSelect) selectBody).getSelectItems() // - .stream() // - .map(Object::toString) // - .collect(Collectors.joining(", ")).trim(); - } - - @Override - public Set getJoinAliases() { - return this.getJoinAliases(this.query.getQueryString()); - } - - /** - * Parses a query string with JSqlParser. - * - * @param query the query to parse - * @return the parsed query - */ - private T parseSelectStatement(String query, Class classOfT) { - - try { - return classOfT.cast(CCJSqlParserUtil.parse(query)); - } catch (JSQLParserException e) { - throw new IllegalArgumentException("The query you provided is not a valid SQL Query", e); + List orderByElements = new ArrayList<>(16); + for (Sort.Order order : sort) { + orderByElements.add(getOrderClause(Collections.emptySet(), Collections.emptySet(), null, order)); } + + if (setOperationListStatement.getOrderByElements() == null) { + setOperationListStatement.setOrderByElements(orderByElements); + } else { + setOperationListStatement.getOrderByElements().addAll(orderByElements); + } + + return setOperationListStatement.toString(); } /** - * Parses a query string with JSqlParser. + * Returns the order clause for the given {@link Sort.Order}. Will prefix the clause with the given alias if the + * referenced property refers to a join alias, i.e. starts with {@code $alias.}. * - * @param query the query to parse - * @return the parsed query + * @param joinAliases the join aliases of the original query. Must not be {@literal null}. + * @param alias the alias for the root entity. May be {@literal null}. + * @param order the order object to build the clause for. Must not be {@literal null}. + * @return a {@link OrderByElement} containing an order clause. Guaranteed to be not {@literal null}. */ - private Select parseSelectStatement(String query) { - return parseSelectStatement(query, Select.class); - } + private static OrderByElement getOrderClause(Set joinAliases, Set selectionAliases, + @Nullable String alias, Sort.Order order) { - /** - * Checks whether a given projection only contains a single column definition (aka without functions, etc.) - * - * @param projection the projection to analyse - * @return true when the projection only contains a single column definition otherwise false - */ - private boolean onlyASingleColumnProjection(List> projection) { + OrderByElement orderByElement = new OrderByElement(); + orderByElement.setAsc(order.getDirection().isAscending()); + orderByElement.setAscDescPresent(true); - // this is unfortunately the only way to check without any hacky & hard string regex magic - return projection.size() == 1 && projection.get(0) instanceof SelectItem - && ((projection.get(0)).getExpression()) instanceof Column; + String property = order.getProperty(); + + checkSortExpression(order); + + if (selectionAliases.contains(property)) { + + Expression orderExpression = order.isIgnoreCase() ? getJSqlLower(property) : new Column(property); + orderByElement.setExpression(orderExpression); + return orderByElement; + } + + boolean qualifyReference = true; + for (String joinAlias : joinAliases) { + if (property.startsWith(joinAlias.concat("."))) { + qualifyReference = false; + break; + } + } + + boolean functionIndicator = property.contains("("); + + String reference = qualifyReference && !functionIndicator && StringUtils.hasText(alias) ? alias + "." + property + : property; + Expression orderExpression = order.isIgnoreCase() ? getJSqlLower(reference) : new Column(reference); + orderByElement.setExpression(orderExpression); + return orderByElement; } /** @@ -481,8 +447,7 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { * @param tableAlias the table alias which can be {@literal null}. * @return */ - private String countPropertyNameForSelection(List> selectItems, boolean distinct, - @Nullable String tableAlias) { + private static String countPropertyNameForSelection(List> selectItems, boolean distinct) { if (onlyASingleColumnProjection(selectItems)) { @@ -491,12 +456,20 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { return column.getFullyQualifiedName(); } - return query.isNativeQuery() ? (distinct ? "*" : "1") : tableAlias == null ? "*" : tableAlias; + return (distinct ? "*" : "1"); } - @Override - public DeclaredQuery getQuery() { - return this.query; + /** + * Checks whether a given projection only contains a single column definition (aka without functions, etc.) + * + * @param projection the projection to analyse. + * @return {@code true} when the projection only contains a single column definition otherwise {@code false}. + */ + private static boolean onlyASingleColumnProjection(List> projection) { + + // this is unfortunately the only way to check without any hacky & hard string regex magic + return projection.size() == 1 && projection.get(0) instanceof SelectItem + && ((projection.get(0)).getExpression()) instanceof Column; } /** @@ -514,4 +487,20 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { DELETE, UPDATE, SELECT, INSERT, MERGE, OTHER; } + /** + * Deserialize the byte array into an object. + * + * @param bytes a serialized object + * @return the result of deserializing the bytes + */ + private static Object deserialize(byte[] bytes) { + try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) { + return ois.readObject(); + } catch (IOException ex) { + throw new IllegalArgumentException("Failed to deserialize object", ex); + } catch (ClassNotFoundException ex) { + throw new IllegalStateException("Failed to deserialize object type", ex); + } + } + } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserUtils.java index efe56e189..5b5cdeb9d 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserUtils.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserUtils.java @@ -20,9 +20,9 @@ import net.sf.jsqlparser.expression.Function; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.schema.Column; +import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; /** * A utility class for JSqlParser. @@ -30,7 +30,9 @@ import java.util.stream.Collectors; * @author Diego Krupitza * @author Greg Turnquist * @since 2.7.0 + * @deprecated internal utility class. */ +@Deprecated public final class JSqlParserUtils { private JSqlParserUtils() {} @@ -42,12 +44,13 @@ public final class JSqlParserUtils { * @param distinct if it should be a distinct count * @return the generated count function call */ - public static Function getJSqlCount(final List countFields, final boolean distinct) { + public static Function getJSqlCount(List countFields, boolean distinct) { - List countColumns = countFields // - .stream() // - .map(Column::new) // - .collect(Collectors.toList()); + List countColumns = new ArrayList<>(countFields.size()); + for (String countField : countFields) { + Column column = new Column(countField); + countColumns.add(column); + } ExpressionList countExpression = new ExpressionList<>(countColumns); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java index d5edc977c..321246775 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java @@ -15,14 +15,29 @@ */ package org.springframework.data.jpa.repository.query; +import static org.springframework.data.jpa.repository.query.JpaQueryParsingToken.*; + +import java.util.List; import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; + +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.Lexer; +import org.antlr.v4.runtime.Parser; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.TokenStream; +import org.antlr.v4.runtime.atn.PredictionMode; +import org.antlr.v4.runtime.tree.ParseTreeVisitor; import org.springframework.data.domain.Sort; import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** - * Implementation of {@link QueryEnhancer} to enhance JPA queries using a {@link JpaQueryParser}. + * Implementation of {@link QueryEnhancer} to enhance JPA queries using ANTLR parsers. * * @author Greg Turnquist * @author Mark Paluch @@ -33,23 +48,59 @@ import org.springframework.util.Assert; */ class JpaQueryEnhancer implements QueryEnhancer { - private final DeclaredQuery query; - private final JpaQueryParser queryParser; + private final ParserRuleContext context; + private final ParsedQueryIntrospector introspector; + private final String projection; + private final BiFunction> sortFunction; + private final BiFunction> countQueryFunction; - /** - * Initialize with an {@link JpaQueryParser}. - * - * @param query - * @param queryParser - */ - private JpaQueryEnhancer(DeclaredQuery query, JpaQueryParser queryParser) { + JpaQueryEnhancer(ParserRuleContext context, ParsedQueryIntrospector introspector, + @Nullable BiFunction> sortFunction, + @Nullable BiFunction> countQueryFunction) { - this.query = query; - this.queryParser = queryParser; + this.context = context; + this.introspector = introspector; + this.sortFunction = sortFunction; + this.countQueryFunction = countQueryFunction; + this.introspector.visit(context); + + List tokens = introspector.getProjection(); + this.projection = tokens.isEmpty() ? "" : render(tokens); + } + + static

ParserRuleContext parse(String query, Function lexerFactoryFunction, + Function parserFactoryFunction, Function parseFunction) { + + Lexer lexer = lexerFactoryFunction.apply(CharStreams.fromString(query)); + P parser = parserFactoryFunction.apply(new CommonTokenStream(lexer)); + + configureParser(query, lexer, parser); + + return parseFunction.apply(parser); } /** - * Factory method to create a {@link JpaQueryParser} for {@link DeclaredQuery} using JPQL grammar. + * Apply common configuration (SLL prediction for performance, our own error listeners). + * + * @param query + * @param lexer + * @param parser + */ + static void configureParser(String query, Lexer lexer, Parser parser) { + + BadJpqlGrammarErrorListener errorListener = new BadJpqlGrammarErrorListener(query); + + lexer.removeErrorListeners(); + lexer.addErrorListener(errorListener); + + parser.getInterpreter().setPredictionMode(PredictionMode.SLL); + + parser.removeErrorListeners(); + parser.addErrorListener(errorListener); + } + + /** + * Factory method to create a {@link JpaQueryEnhancer} for {@link DeclaredQuery} using JPQL grammar. * * @param query must not be {@literal null}. * @return a new {@link JpaQueryEnhancer} using JPQL. @@ -58,11 +109,11 @@ class JpaQueryEnhancer implements QueryEnhancer { Assert.notNull(query, "DeclaredQuery must not be null!"); - return new JpaQueryEnhancer(query, JpqlQueryParser.parseQuery(query.getQueryString())); + return JpqlQueryParser.parseQuery(query.getQueryString()); } /** - * Factory method to create a {@link JpaQueryParser} for {@link DeclaredQuery} using HQL grammar. + * Factory method to create a {@link JpaQueryEnhancer} for {@link DeclaredQuery} using HQL grammar. * * @param query must not be {@literal null}. * @return a new {@link JpaQueryEnhancer} using HQL. @@ -71,11 +122,11 @@ class JpaQueryEnhancer implements QueryEnhancer { Assert.notNull(query, "DeclaredQuery must not be null!"); - return new JpaQueryEnhancer(query, HqlQueryParser.parseQuery(query.getQueryString())); + return HqlQueryParser.parseQuery(query.getQueryString()); } /** - * Factory method to create a {@link JpaQueryParser} for {@link DeclaredQuery} using EQL grammar. + * Factory method to create a {@link JpaQueryEnhancer} for {@link DeclaredQuery} using EQL grammar. * * @param query must not be {@literal null}. * @return a new {@link JpaQueryEnhancer} using EQL. @@ -85,11 +136,54 @@ class JpaQueryEnhancer implements QueryEnhancer { Assert.notNull(query, "DeclaredQuery must not be null!"); - return new JpaQueryEnhancer(query, EqlQueryParser.parseQuery(query.getQueryString())); + return EqlQueryParser.parseQuery(query.getQueryString()); } - protected JpaQueryParser getQueryParsingStrategy() { - return queryParser; + /** + * Checks if the select clause has a new constructor instantiation in the JPA query. + * + * @return Guaranteed to return {@literal true} or {@literal false}. + */ + @Override + public boolean hasConstructorExpression() { + return this.introspector.hasConstructorExpression(); + } + + /** + * Resolves the alias for the entity in the FROM clause from the JPA query. Since the {@link JpaQueryParser} can + * already find the alias when generating sorted and count queries, this is mainly to serve test cases. + */ + @Override + public String detectAlias() { + return this.introspector.getAlias(); + } + + /** + * Looks up the projection of the JPA query. Since the {@link JpaQueryParser} can already find the projection when + * generating sorted and count queries, this is mainly to serve test cases. + */ + @Override + public String getProjection() { + return this.projection; + } + + /** + * Since the {@link JpaQueryParser} can already fully transform sorted and count queries by itself, this is a + * placeholder method. + * + * @return empty set + */ + @Override + public Set getJoinAliases() { + return Set.of(); + } + + /** + * Look up the {@link DeclaredQuery} from the {@link JpaQueryParser}. + */ + @Override + public DeclaredQuery getQuery() { + throw new UnsupportedOperationException(); } /** @@ -100,7 +194,7 @@ class JpaQueryEnhancer implements QueryEnhancer { */ @Override public String applySorting(Sort sort) { - return queryParser.renderSortedQuery(sort); + return render(sortFunction.apply(sort, detectAlias()).visit(context)); } /** @@ -115,15 +209,6 @@ class JpaQueryEnhancer implements QueryEnhancer { return applySorting(sort); } - /** - * Resolves the alias for the entity in the FROM clause from the JPA query. Since the {@link JpaQueryParser} can - * already find the alias when generating sorted and count queries, this is mainly to serve test cases. - */ - @Override - public String detectAlias() { - return queryParser.findAlias(); - } - /** * Creates a count query from the original query, with no count projection. * @@ -141,44 +226,89 @@ class JpaQueryEnhancer implements QueryEnhancer { */ @Override public String createCountQueryFor(@Nullable String countProjection) { - return queryParser.createCountQuery(countProjection); + return render(countQueryFunction.apply(countProjection, detectAlias()).visit(context)); } /** - * Checks if the select clause has a new constructor instantiation in the JPA query. + * Implements the {@code HQL} parsing operations of a {@link JpaQueryEnhancer} using the ANTLR-generated + * {@link HqlParser} and {@link HqlSortedQueryTransformer}. * - * @return Guaranteed to return {@literal true} or {@literal false}. + * @author Greg Turnquist + * @author Mark Paluch + * @since 3.1 */ - @Override - public boolean hasConstructorExpression() { - return queryParser.hasConstructorExpression(); + static class HqlQueryParser extends JpaQueryEnhancer { + + private HqlQueryParser(String query) { + super(parse(query, HqlLexer::new, HqlParser::new, HqlParser::start), new HqlQueryIntrospector(), + HqlSortedQueryTransformer::new, HqlCountQueryTransformer::new); + } + + /** + * Parse a HQL query. + * + * @param query + * @return the query parser. + * @throws BadJpqlGrammarException + */ + public static HqlQueryParser parseQuery(String query) throws BadJpqlGrammarException { + return new HqlQueryParser(query); + } + } /** - * Looks up the projection of the JPA query. Since the {@link JpaQueryParser} can already find the projection when - * generating sorted and count queries, this is mainly to serve test cases. - */ - @Override - public String getProjection() { - return queryParser.getProjection(); - } - - /** - * Since the {@link JpaQueryParser} can already fully transform sorted and count queries by itself, this is a - * placeholder method. + * Implements the {@code EQL} parsing operations of a {@link JpaQueryEnhancer} using the ANTLR-generated + * {@link EqlParser}. * - * @return empty set + * @author Greg Turnquist + * @author Mark Paluch + * @since 3.2 */ - @Override - public Set getJoinAliases() { - return Set.of(); + static class EqlQueryParser extends JpaQueryEnhancer { + + private EqlQueryParser(String query) { + super(parse(query, EqlLexer::new, EqlParser::new, EqlParser::start), new EqlQueryIntrospector(), + EqlSortedQueryTransformer::new, EqlCountQueryTransformer::new); + } + + /** + * Parse a EQL query. + * + * @param query + * @return the query parser. + * @throws BadJpqlGrammarException + */ + public static EqlQueryParser parseQuery(String query) throws BadJpqlGrammarException { + return new EqlQueryParser(query); + } + } /** - * Look up the {@link DeclaredQuery} from the {@link JpaQueryParser}. + * Implements the {@code JPQL} parsing operations of a {@link JpaQueryEnhancer} using the ANTLR-generated + * {@link JpqlParser} and {@link JpqlSortedQueryTransformer}. + * + * @author Greg Turnquist + * @author Mark Paluch + * @since 3.1 */ - @Override - public DeclaredQuery getQuery() { - return query; + static class JpqlQueryParser extends JpaQueryEnhancer { + + private JpqlQueryParser(String query) { + super(parse(query, JpqlLexer::new, JpqlParser::new, JpqlParser::start), new JpqlQueryIntrospector(), + JpqlSortedQueryTransformer::new, JpqlCountQueryTransformer::new); + } + + /** + * Parse a JPQL query. + * + * @param query + * @return the query parser. + * @throws BadJpqlGrammarException + */ + public static JpqlQueryParser parseQuery(String query) throws BadJpqlGrammarException { + return new JpqlQueryParser(query); + } } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java index d56d43d1f..b299d87c4 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java @@ -39,7 +39,6 @@ import org.springframework.data.jpa.repository.QueryHints; import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; -import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersSource; import org.springframework.data.repository.query.QueryMethod; @@ -108,7 +107,7 @@ public class JpaQueryMethod extends QueryMethod { * @param factory must not be {@literal null} * @param extractor must not be {@literal null} */ - protected JpaQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory, + public JpaQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory, QueryExtractor extractor) { super(method, metadata, factory); @@ -145,7 +144,6 @@ public class JpaQueryMethod extends QueryMethod { Assert.isTrue(!(isModifyingQuery() && getParameters().hasSpecialParameter()), () -> String.format("Modifying method must not contain %s", Parameters.TYPES)); - assertParameterNamesInAnnotatedQuery(); } private static Class potentiallyUnwrapReturnTypeFor(RepositoryMetadata metadata, Method method) { @@ -160,29 +158,7 @@ public class JpaQueryMethod extends QueryMethod { return returnType.getType(); } - private void assertParameterNamesInAnnotatedQuery() { - String annotatedQuery = getAnnotatedQuery(); - - if (!DeclaredQuery.hasNamedParameter(annotatedQuery)) { - return; - } - - for (Parameter parameter : getParameters()) { - - if (!parameter.isNamedParameter()) { - continue; - } - - if (!StringUtils.hasText(annotatedQuery) - || !annotatedQuery.contains(String.format(":%s", parameter.getName().get())) - && !annotatedQuery.contains(String.format("#%s", parameter.getName().get()))) { - throw new IllegalStateException( - String.format("Using named parameters for method %s but parameter '%s' not found in annotated query '%s'", - method, parameter.getName(), annotatedQuery)); - } - } - } @Override @SuppressWarnings({ "rawtypes", "unchecked" }) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryParser.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryParser.java deleted file mode 100644 index 835b71a7b..000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryParser.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2022-2024 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.springframework.data.jpa.repository.query.JpaQueryParsingToken.*; - -import java.util.List; -import java.util.function.BiFunction; -import java.util.function.Function; - -import org.antlr.v4.runtime.CharStream; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CommonTokenStream; -import org.antlr.v4.runtime.Lexer; -import org.antlr.v4.runtime.Parser; -import org.antlr.v4.runtime.ParserRuleContext; -import org.antlr.v4.runtime.TokenStream; -import org.antlr.v4.runtime.atn.PredictionMode; -import org.antlr.v4.runtime.tree.ParseTreeVisitor; - -import org.springframework.data.domain.Sort; -import org.springframework.lang.Nullable; - -/** - * Operations needed to parse a JPA query. - * - * @author Greg Turnquist - * @author Mark Paluch - * @since 3.1 - */ -abstract class JpaQueryParser { - - private final ParserRuleContext context; - private final ParsedQueryIntrospector introspector; - private final String projection; - private final BiFunction> sortFunction; - private final BiFunction> countQueryFunction; - - JpaQueryParser(ParserRuleContext context, ParsedQueryIntrospector introspector, - @Nullable BiFunction> sortFunction, - @Nullable BiFunction> countQueryFunction) { - - this.context = context; - this.introspector = introspector; - this.sortFunction = sortFunction; - this.countQueryFunction = countQueryFunction; - this.introspector.visit(context); - - List tokens = introspector.getProjection(); - this.projection = tokens.isEmpty() ? "" : render(tokens); - } - - static

ParserRuleContext parse(String query, Function lexerFactoryFunction, - Function parserFactoryFunction, Function parseFunction) { - - Lexer lexer = lexerFactoryFunction.apply(CharStreams.fromString(query)); - P parser = parserFactoryFunction.apply(new CommonTokenStream(lexer)); - - configureParser(query, lexer, parser); - - return parseFunction.apply(parser); - } - - /** - * Generate a query using the original query with an {@literal order by} clause added (or amended) based upon the - * provider {@link Sort} parameter. - * - * @param sort can be {@literal null} - */ - String renderSortedQuery(Sort sort) { - return render(sortFunction.apply(sort, findAlias()).visit(context)); - } - - /** - * Generate a count-based query derived from the original query. - * - * @param countProjection - */ - String createCountQuery(@Nullable String countProjection) { - return render(countQueryFunction.apply(countProjection, findAlias()).visit(context)); - } - - /** - * Find the projection of the query. - */ - String getProjection() { - return this.projection; - } - - /** - * Find the alias of the query's primary FROM clause - * - * @return can be {@literal null} - */ - @Nullable - String findAlias() { - return this.introspector.getAlias(); - } - - /** - * Discern if the query has a {@code new com.example.Dto()} DTO constructor in the select clause. - * - * @return Guaranteed to be {@literal true} or {@literal false}. - */ - boolean hasConstructorExpression() { - return this.introspector.hasConstructorExpression(); - } - - /** - * Apply common configuration (SLL prediction for performance, our own error listeners). - * - * @param query - * @param lexer - * @param parser - */ - static void configureParser(String query, Lexer lexer, Parser parser) { - - BadJpqlGrammarErrorListener errorListener = new BadJpqlGrammarErrorListener(query); - - lexer.removeErrorListeners(); - lexer.addErrorListener(errorListener); - - parser.getInterpreter().setPredictionMode(PredictionMode.SLL); - - parser.removeErrorListeners(); - parser.addErrorListener(errorListener); - } - -} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryParser.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryParser.java deleted file mode 100644 index 39276bcf5..000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryParser.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2022-2024 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; - -/** - * Implements the {@code JPQL} parsing operations of a {@link JpaQueryParser} using the ANTLR-generated - * {@link JpqlParser} and {@link JpqlSortedQueryTransformer}. - * - * @author Greg Turnquist - * @author Mark Paluch - * @since 3.1 - */ -class JpqlQueryParser extends JpaQueryParser { - - private JpqlQueryParser(String query) { - super(parse(query, JpqlLexer::new, JpqlParser::new, JpqlParser::start), new JpqlQueryIntrospector(), - JpqlSortedQueryTransformer::new, JpqlCountQueryTransformer::new); - } - - /** - * Parse a JPQL query. - * - * @param query - * @return the query parser. - * @throws BadJpqlGrammarException - */ - public static JpqlQueryParser parseQuery(String query) throws BadJpqlGrammarException { - return new JpqlQueryParser(query); - } -} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java index ebe0278b0..99bf03c43 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java @@ -28,6 +28,7 @@ import org.springframework.data.repository.query.QueryCreationException; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; +import org.springframework.data.util.Lazy; import org.springframework.lang.Nullable; /** @@ -50,7 +51,7 @@ final class NamedQuery extends AbstractJpaQuery { private final String countQueryName; private final @Nullable String countProjection; private final boolean namedCountQueryIsPresent; - private final DeclaredQuery declaredQuery; + private final Lazy declaredQuery; private final QueryParameterSetter.QueryMetadataCache metadataCache; /** @@ -75,11 +76,6 @@ final class NamedQuery extends AbstractJpaQuery { this.namedCountQueryIsPresent = hasNamedQuery(em, countQueryName); Query query = em.createNamedQuery(queryName); - String queryString = extractor.extractQueryString(query); - - // TODO: Detect whether a named query is a named one. - this.declaredQuery = DeclaredQuery.of(queryString, query != null && query.toString().contains("NativeQuery")); - boolean weNeedToCreateCountQuery = !namedCountQueryIsPresent && method.getParameters().hasLimitingParameters(); boolean cantExtractQuery = !extractor.canExtractQuery(); @@ -93,6 +89,10 @@ final class NamedQuery extends AbstractJpaQuery { method)); } + String queryString = extractor.extractQueryString(query); + + // TODO: Detect whether a named query is a native one. + this.declaredQuery = Lazy.of(() -> DeclaredQuery.of(queryString, query.toString().contains("NativeQuery"))); this.metadataCache = new QueryParameterSetter.QueryMetadataCache(); } @@ -188,7 +188,7 @@ final class NamedQuery extends AbstractJpaQuery { } else { - String countQueryString = declaredQuery.deriveCountQuery(countProjection).getQueryString(); + String countQueryString = declaredQuery.get().deriveCountQuery(countProjection).getQueryString(); cacheKey = countQueryString; countQuery = em.createQuery(countQueryString, Long.class); } @@ -220,7 +220,7 @@ final class NamedQuery extends AbstractJpaQuery { return type.isInterface() ? Tuple.class : null; } - return declaredQuery.hasConstructorExpression() // + return declaredQuery.get().hasConstructorExpression() // ? null // : super.getTypeToRead(returnedType); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java index 52dfd37e2..a2bb681e1 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java @@ -42,6 +42,8 @@ import org.springframework.lang.Nullable; */ final class NativeJpaQuery extends AbstractStringBasedJpaQuery { + private final boolean queryForEntity; + /** * Creates a new {@link NativeJpaQuery} encapsulating the query annotated on the given {@link JpaQueryMethod}. * @@ -57,6 +59,8 @@ final class NativeJpaQuery extends AbstractStringBasedJpaQuery { super(method, em, queryString, countQueryString, rewriter, evaluationContextProvider, parser); + this.queryForEntity = getQueryMethod().isQueryForEntity(); + Parameters parameters = method.getParameters(); if (parameters.hasSortParameter() && !queryString.contains("#sort")) { @@ -77,9 +81,9 @@ final class NativeJpaQuery extends AbstractStringBasedJpaQuery { @Nullable private Class getTypeToQueryFor(ReturnedType returnedType) { - Class result = getQueryMethod().isQueryForEntity() ? returnedType.getDomainType() : null; + Class result = queryForEntity ? returnedType.getDomainType() : null; - if (this.getQuery().hasConstructorExpression() || this.getQuery().isDefaultProjection()) { + if (getQuery().hasConstructorExpression() || getQuery().isDefaultProjection()) { return result; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java index ec28f9e5a..2a84cce83 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryEnhancer.java @@ -30,23 +30,11 @@ import org.springframework.lang.Nullable; public interface QueryEnhancer { /** - * Adds {@literal order by} clause to the JPQL query. Uses the first alias to bind the sorting property to. + * Returns whether the given JPQL query contains a constructor expression. * - * @param sort the sort specification to apply. - * @return the modified query string. + * @return whether the given JPQL query contains a constructor expression. */ - default String applySorting(Sort sort) { - return applySorting(sort, detectAlias()); - } - - /** - * Adds {@literal order by} clause to the JPQL query. - * - * @param sort the sort specification to apply. - * @param alias the alias to be used in the order by clause. May be {@literal null} or empty. - * @return the modified query string. - */ - String applySorting(Sort sort, @Nullable String alias); + boolean hasConstructorExpression(); /** * Resolves the alias for the entity to be retrieved from the given JPA query. @@ -56,6 +44,47 @@ public interface QueryEnhancer { @Nullable String detectAlias(); + /** + * Returns the projection part of the query, i.e. everything between {@code select} and {@code from}. + * + * @return the projection part of the query. + */ + String getProjection(); + + /** + * Returns the join aliases of the query. + * + * @return the join aliases of the query. + */ + @Deprecated(forRemoval = true) + Set getJoinAliases(); + + /** + * Gets the query we want to use for enhancements. + * + * @return non-null {@link DeclaredQuery} that wraps the query + */ + @Deprecated(forRemoval = true) + DeclaredQuery getQuery(); + + /** + * Adds {@literal order by} clause to the JPQL query. Uses the first alias to bind the sorting property to. + * + * @param sort the sort specification to apply. + * @return the modified query string. + */ + String applySorting(Sort sort); + + /** + * Adds {@literal order by} clause to the JPQL query. + * + * @param sort the sort specification to apply. + * @param alias the alias to be used in the order by clause. May be {@literal null} or empty. + * @return the modified query string. + */ + @Deprecated + String applySorting(Sort sort, @Nullable String alias); + /** * Creates a count projected query from the given original query. * @@ -72,29 +101,4 @@ public interface QueryEnhancer { * @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}. */ String createCountQueryFor(@Nullable String countProjection); - - /** - * Returns whether the given JPQL query contains a constructor expression. - * - * @return whether the given JPQL query contains a constructor expression. - */ - default boolean hasConstructorExpression() { - return QueryUtils.hasConstructorExpression(getQuery().getQueryString()); - } - - /** - * Returns the projection part of the query, i.e. everything between {@code select} and {@code from}. - * - * @return the projection part of the query. - */ - String getProjection(); - - Set getJoinAliases(); - - /** - * Gets the query we want to use for enhancements. - * - * @return non-null {@link DeclaredQuery} that wraps the query - */ - DeclaredQuery getQuery(); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java index ed57263e8..d8308d136 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java @@ -56,7 +56,7 @@ import org.springframework.util.StringUtils; * @author Greg Turnquist * @author Yuriy Tsarkov */ -class StringQuery implements DeclaredQuery { +public class StringQuery implements DeclaredQuery { private final String query; private final List bindings; @@ -64,13 +64,14 @@ class StringQuery implements DeclaredQuery { private final boolean usesJdbcStyleParameters; private final boolean isNative; private final QueryEnhancer queryEnhancer; + private final boolean hasNamedParameters; /** * Creates a new {@link StringQuery} from the given JPQL query. * * @param query must not be {@literal null} or empty. */ - StringQuery(String query, boolean isNative) { + public StringQuery(String query, boolean isNative) { Assert.hasText(query, "Query must not be null or empty"); @@ -83,24 +84,17 @@ class StringQuery implements DeclaredQuery { this.bindings, queryMeta); this.usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters; - this.queryEnhancer = QueryEnhancerFactory.forQuery(this); - } - // TODO: Conflict with eager JpaQueryMethod.assertParameterNamesInAnnotatedQuery validation that attempts parsing - // without pre-processing the query leaving #{#entityName} substitution to a later time. - public static boolean hasNamedParameter(String query) { - - if (ObjectUtils.isEmpty(query)) { - return false; + boolean hasNamedParameters = false; + for (ParameterBinding parameterBinding : getParameterBindings()) { + if (parameterBinding.getIdentifier().hasName() && parameterBinding.getOrigin().isMethodArgument()) { + hasNamedParameters = true; + break; + } } - List parameterBindings = new ArrayList<>(); - Metadata queryMeta = new Metadata(); - ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query, - parameterBindings, queryMeta); - - return parameterBindings.stream().anyMatch(b -> b.getIdentifier().hasName()); + this.hasNamedParameters = hasNamedParameters; } /** @@ -161,7 +155,7 @@ class StringQuery implements DeclaredQuery { @Override public boolean hasNamedParameter() { - return bindings.stream().anyMatch(b -> b.getIdentifier().hasName()); + return hasNamedParameters; } @Override @@ -233,8 +227,8 @@ class StringQuery implements DeclaredQuery { * Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings. Returns * the cleaned up query. */ - String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query, - List bindings, Metadata queryMeta) { + String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query, List bindings, + Metadata queryMeta) { int greatestParameterIndex = tryFindGreatestParameterIndexIn(query); boolean parametersShouldBeAccessedByIndex = greatestParameterIndex != -1; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java index 4f6752c17..241a5310b 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlParserQueryEnhancerUnitTests.java @@ -21,7 +21,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; /** - * TCK Tests for {@link EqlQueryParser} mixed into {@link JpaQueryEnhancer}. + * TCK Tests for {@link JpaQueryEnhancer.EqlQueryParser} mixed into {@link JpaQueryEnhancer}. * * @author Greg Turnquist */ diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java index 18c42c875..52bdcd82d 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java @@ -30,7 +30,8 @@ import org.springframework.data.jpa.domain.JpaSort; import org.springframework.lang.Nullable; /** - * Verify that EQL queries are properly transformed through the {@link JpaQueryEnhancer} and the {@link EqlQueryParser}. + * Verify that EQL queries are properly transformed through the {@link JpaQueryEnhancer} and the + * {@link JpaQueryEnhancer.EqlQueryParser}. * * @author Greg Turnquist */ @@ -718,7 +719,7 @@ class EqlQueryTransformerTests { @MethodSource("queriesWithReservedWordsAsIdentifiers") // GH-2864 void usingReservedWordAsRelationshipNameShouldWork(String relationshipName, String joinAlias) { - EqlQueryParser.parseQuery(String.format(""" + JpaQueryEnhancer.EqlQueryParser.parseQuery(String.format(""" select u from UserAccountEntity u join u.lossInspectorLimitConfiguration lil diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java index f19c4acc7..7d57ed37a 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlParserQueryEnhancerUnitTests.java @@ -21,7 +21,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; /** - * TCK Tests for {@link HqlQueryParser} mixed into {@link JpaQueryEnhancer}. + * TCK Tests for {@link JpaQueryEnhancer.HqlQueryParser} mixed into {@link JpaQueryEnhancer}. * * @author Greg Turnquist */ diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java index 94f3d2691..5cc6b6ddb 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java @@ -31,7 +31,8 @@ import org.springframework.data.jpa.domain.JpaSort; import org.springframework.lang.Nullable; /** - * Verify that HQL queries are properly transformed through the {@link JpaQueryEnhancer} and the {@link HqlQueryParser}. + * Verify that HQL queries are properly transformed through the {@link JpaQueryEnhancer} and the + * {@link JpaQueryEnhancer.HqlQueryParser}. * * @author Greg Turnquist * @author Christoph Strobl @@ -869,7 +870,7 @@ class HqlQueryTransformerTests { @MethodSource("queriesWithReservedWordsAsIdentifiers") // GH-2864 void usingReservedWordAsRelationshipNameShouldWork(String relationshipName, String joinAlias) { - HqlQueryParser.parseQuery(String.format(""" + JpaQueryEnhancer.HqlQueryParser.parseQuery(String.format(""" select u from UserAccountEntity u join fetch u.lossInspectorLimitConfiguration lil 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 index 58b5b6589..d5dc2b859 100644 --- 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 @@ -45,14 +45,7 @@ public class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { @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 "); - - assumeThat(query).as("JSQLParser does not support MOD JPQL syntax").doesNotContain("MOD("); - - super.shouldDeriveJpqlCountQuery(query, expected); + assumeThat(query).as("JSQLParser does not support JPQL").isNull(); } @Test diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java index eee309518..4d54b1950 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java @@ -34,6 +34,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; + import org.springframework.core.annotation.AliasFor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -282,20 +283,6 @@ class JpaQueryMethodUnitTests { assertThat(method.getNamedCountQueryName()).isEqualTo("HateoasAwareSpringDataWebConfiguration.bar.count"); } - @Test // DATAJPA-185 - void rejectsInvalidNamedParameter() { - - assertThatThrownBy(() -> getQueryMethod(InvalidRepository.class, "findByAnnotatedQuery", String.class)) - .isInstanceOf(IllegalStateException.class) - // Parameter from query - .hasMessageContaining("foo") - // Parameter name from annotation - .hasMessageContaining("param") - // Method name - .hasMessageContaining("findByAnnotatedQuery"); - - } - @Test // DATAJPA-207 @SuppressWarnings({ "rawtypes", "unchecked" }) void returnsTrueIfReturnTypeIsEntity() { @@ -529,9 +516,6 @@ class JpaQueryMethodUnitTests { @Modifying void updateMethod(String firstname, Sort sort); - // Typo in named parameter - @Query("select u from User u where u.firstname = :foo") - List findByAnnotatedQuery(@Param("param") String param); } interface ValidRepository extends Repository { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlParserQueryEnhancerUnitTests.java index a026e9036..b867aba84 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlParserQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlParserQueryEnhancerUnitTests.java @@ -21,7 +21,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; /** - * TCK Tests for {@link JpqlQueryParser} mixed into {@link JpaQueryEnhancer}. + * TCK Tests for {@link JpaQueryEnhancer.JpqlQueryParser} mixed into {@link JpaQueryEnhancer}. * * @author Greg Turnquist */ diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java index 6bce914c7..eb04b8de0 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java @@ -31,7 +31,7 @@ import org.springframework.lang.Nullable; /** * Verify that JPQL queries are properly transformed through the {@link JpaQueryEnhancer} and the - * {@link JpqlQueryParser}. + * {@link JpaQueryEnhancer.JpqlQueryParser}. * * @author Greg Turnquist * @author Mark Paluch @@ -736,7 +736,7 @@ class JpqlQueryTransformerTests { @MethodSource("queriesWithReservedWordsAsIdentifiers") // GH-2864 void usingReservedWordAsRelationshipNameShouldWork(String relationshipName, String joinAlias) { - JpqlQueryParser.parseQuery(String.format(""" + JpaQueryEnhancer.JpqlQueryParser.parseQuery(String.format(""" select u from UserAccountEntity u join u.lossInspectorLimitConfiguration lil diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java index 61fe6a2aa..78fda1311 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java @@ -39,7 +39,7 @@ class QueryEnhancerFactoryUnitTests { JpaQueryEnhancer queryParsingEnhancer = (JpaQueryEnhancer) queryEnhancer; - assertThat(queryParsingEnhancer.getQueryParsingStrategy()).isInstanceOf(HqlQueryParser.class); + assertThat(queryParsingEnhancer).isInstanceOf(JpaQueryEnhancer.HqlQueryParser.class); } @Test diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java index 558dc747e..234ea4ec8 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java @@ -50,6 +50,7 @@ import org.springframework.data.jpa.repository.sample.UserRepository; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.util.TypeInformation; @@ -328,6 +329,9 @@ class SimpleJpaQueryUnitTests { @Query(value = "select u from User u", countQuery = "select count(u.id) from #{#entityName} u where u.name = :#{#arg0}") List findAllWithBindingsOnlyInCountQuery(String arg0, Pageable pageable); + // Typo in named parameter + @Query("select u from User u where u.firstname = :foo") + List findByAnnotatedQuery(@Param("param") String param); } interface UserProjection {}