diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlCountQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlCountQueryTransformer.java index 498a499ad8..934683c824 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlCountQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlCountQueryTransformer.java @@ -36,10 +36,9 @@ class EqlCountQueryTransformer extends EqlQueryRenderer { private final @Nullable String countProjection; private final @Nullable String primaryFromAlias; - EqlCountQueryTransformer(@Nullable String countProjection, @Nullable String primaryFromAlias) { - + EqlCountQueryTransformer(@Nullable String countProjection, QueryInformation queryInformation) { this.countProjection = countProjection; - this.primaryFromAlias = primaryFromAlias; + this.primaryFromAlias = queryInformation.getAlias(); } @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryIntrospector.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryIntrospector.java index d9a558540c..0f006f2388 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryIntrospector.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryIntrospector.java @@ -15,7 +15,7 @@ */ package org.springframework.data.jpa.repository.query; -import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_COMMA; +import static org.springframework.data.jpa.repository.query.QueryTokens.*; import java.util.ArrayList; import java.util.Collections; @@ -31,7 +31,7 @@ * @author Christoph Strobl */ @SuppressWarnings("UnreachableCode") -class EqlQueryIntrospector extends EqlBaseVisitor implements ParsedQueryIntrospector { +class EqlQueryIntrospector extends EqlBaseVisitor implements ParsedQueryIntrospector { private final EqlQueryRenderer renderer = new EqlQueryRenderer(); @@ -41,18 +41,9 @@ class EqlQueryIntrospector extends EqlBaseVisitor implements ParsedQueryIn private boolean hasConstructorExpression = false; @Override - public String getAlias() { - return primaryFromAlias; - } - - @Override - public List getProjection() { - return projection == null ? Collections.emptyList() : projection; - } - - @Override - public boolean hasConstructorExpression() { - return hasConstructorExpression; + public QueryInformation getParsedQueryInformation() { + return new QueryInformation(primaryFromAlias, projection == null ? Collections.emptyList() : projection, + hasConstructorExpression); } @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java index 16ad22dafb..e544024750 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java @@ -43,12 +43,13 @@ class EqlSortedQueryTransformer extends EqlQueryRenderer { private final @Nullable String primaryFromAlias; private final @Nullable DtoProjectionTransformerDelegate dtoDelegate; - EqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias, @Nullable ReturnedType returnedType) { + EqlSortedQueryTransformer(Sort sort, QueryInformation queryInformation, @Nullable ReturnedType returnedType) { Assert.notNull(sort, "Sort must not be null"); + Assert.notNull(queryInformation, "ParsedHqlQueryInformation must not be null"); this.sort = sort; - this.primaryFromAlias = primaryFromAlias; + this.primaryFromAlias = queryInformation.getAlias(); this.dtoDelegate = returnedType == null ? null : new DtoProjectionTransformerDelegate(returnedType); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HibernateQueryInformation.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HibernateQueryInformation.java new file mode 100644 index 0000000000..755dade914 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HibernateQueryInformation.java @@ -0,0 +1,41 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import java.util.List; + +import org.springframework.lang.Nullable; + +/** + * Hibernate-specific query details capturing common table expression details. + * + * @author Mark Paluch + * @since 3.5 + */ +class HibernateQueryInformation extends QueryInformation { + + private final boolean hasCte; + + public HibernateQueryInformation(@Nullable String alias, List projection, + boolean hasConstructorExpression, boolean hasCte) { + super(alias, projection, hasConstructorExpression); + this.hasCte = hasCte; + } + + public boolean hasCte() { + return hasCte; + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java index 0ea8649ef3..e96496cfec 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlCountQueryTransformer.java @@ -36,11 +36,12 @@ class HqlCountQueryTransformer extends HqlQueryRenderer { private final @Nullable String countProjection; private final @Nullable String primaryFromAlias; - private boolean containsCTE = false; + private final boolean containsCTE; - HqlCountQueryTransformer(@Nullable String countProjection, @Nullable String primaryFromAlias) { + HqlCountQueryTransformer(@Nullable String countProjection, HibernateQueryInformation queryInformation) { this.countProjection = countProjection; - this.primaryFromAlias = primaryFromAlias; + this.primaryFromAlias = queryInformation.getAlias(); + this.containsCTE = queryInformation.hasCte(); } @Override @@ -67,12 +68,6 @@ public QueryRendererBuilder visitOrderedQuery(HqlParser.OrderedQueryContext ctx) return builder; } - @Override - public QueryTokenStream visitCte(HqlParser.CteContext ctx) { - this.containsCTE = true; - return super.visitCte(ctx); - } - @Override public QueryRendererBuilder visitFromQuery(HqlParser.FromQueryContext ctx) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryIntrospector.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryIntrospector.java index ebec68efb2..5ccc7b3556 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryIntrospector.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryIntrospector.java @@ -30,7 +30,7 @@ * @author Mark Paluch */ @SuppressWarnings({ "UnreachableCode", "ConstantValue" }) -class HqlQueryIntrospector extends HqlBaseVisitor implements ParsedQueryIntrospector { +class HqlQueryIntrospector extends HqlBaseVisitor implements ParsedQueryIntrospector { private final HqlQueryRenderer renderer = new HqlQueryRenderer(); @@ -38,20 +38,12 @@ class HqlQueryIntrospector extends HqlBaseVisitor implements ParsedQueryIn private @Nullable List projection; private boolean projectionProcessed; private boolean hasConstructorExpression = false; + private boolean hasCte = false; @Override - public String getAlias() { - return primaryFromAlias; - } - - @Override - public List getProjection() { - return projection == null ? Collections.emptyList() : projection; - } - - @Override - public boolean hasConstructorExpression() { - return hasConstructorExpression; + public HibernateQueryInformation getParsedQueryInformation() { + return new HibernateQueryInformation(primaryFromAlias, projection == null ? Collections.emptyList() : projection, + hasConstructorExpression, hasCte); } @Override @@ -65,6 +57,12 @@ public Void visitSelectClause(HqlParser.SelectClauseContext ctx) { return super.visitSelectClause(ctx); } + @Override + public Void visitCte(HqlParser.CteContext ctx) { + this.hasCte = true; + return super.visitCte(ctx); + } + @Override public Void visitFromRoot(HqlParser.FromRootContext ctx) { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlSortedQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlSortedQueryTransformer.java index 82dc694a84..b5784b31dc 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlSortedQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlSortedQueryTransformer.java @@ -41,21 +41,14 @@ class HqlSortedQueryTransformer extends HqlQueryRenderer { private final @Nullable String primaryFromAlias; private final @Nullable DtoProjectionTransformerDelegate dtoDelegate; - HqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias) { + HqlSortedQueryTransformer(Sort sort, HibernateQueryInformation queryInformation, + @Nullable ReturnedType returnedType) { Assert.notNull(sort, "Sort must not be null"); + Assert.notNull(queryInformation, "ParsedHqlQueryInformation must not be null"); this.sort = sort; - this.primaryFromAlias = primaryFromAlias; - this.dtoDelegate = null; - } - - HqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias, @Nullable ReturnedType returnedType) { - - Assert.notNull(sort, "Sort must not be null"); - - this.sort = sort; - this.primaryFromAlias = primaryFromAlias; + this.primaryFromAlias = queryInformation.getAlias(); this.dtoDelegate = returnedType == null ? null : new DtoProjectionTransformerDelegate(returnedType); } 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 7f02ce3ecf..005c707e55 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 @@ -44,25 +44,27 @@ * @see HqlQueryParser * @see EqlQueryParser */ -class JpaQueryEnhancer implements QueryEnhancer { +@SuppressWarnings("removal") +class JpaQueryEnhancer implements QueryEnhancer { private final ParserRuleContext context; - private final ParsedQueryIntrospector introspector; + private final Q queryInformation; private final String projection; - private final SortedQueryRewriteFunction sortFunction; - private final BiFunction> countQueryFunction; + private final SortedQueryRewriteFunction sortFunction; + private final BiFunction> countQueryFunction; - JpaQueryEnhancer(ParserRuleContext context, ParsedQueryIntrospector introspector, - SortedQueryRewriteFunction sortFunction, - BiFunction> countQueryFunction) { + JpaQueryEnhancer(ParserRuleContext context, ParsedQueryIntrospector introspector, + SortedQueryRewriteFunction sortFunction, + BiFunction> countQueryFunction) { this.context = context; - this.introspector = introspector; this.sortFunction = sortFunction; this.countQueryFunction = countQueryFunction; - this.introspector.visit(context); + introspector.visit(context); - List tokens = introspector.getProjection(); + this.queryInformation = introspector.getParsedQueryInformation(); + + List tokens = queryInformation.getProjection(); this.projection = tokens.isEmpty() ? "" : new QueryRenderer.TokenRenderer(tokens).render(); } @@ -86,9 +88,10 @@ static

ParserRuleContext parse(String query, Function forJpql(DeclaredQuery query) { Assert.notNull(query, "DeclaredQuery must not be null!"); @@ -122,7 +125,7 @@ public static JpaQueryEnhancer forJpql(DeclaredQuery query) { * @param query must not be {@literal null}. * @return a new {@link JpaQueryEnhancer} using HQL. */ - public static JpaQueryEnhancer forHql(DeclaredQuery query) { + public static JpaQueryEnhancer forHql(DeclaredQuery query) { Assert.notNull(query, "DeclaredQuery must not be null!"); @@ -136,17 +139,27 @@ public static JpaQueryEnhancer forHql(DeclaredQuery query) { * @return a new {@link JpaQueryEnhancer} using EQL. * @since 3.2 */ - public static JpaQueryEnhancer forEql(DeclaredQuery query) { + public static JpaQueryEnhancer forEql(DeclaredQuery query) { Assert.notNull(query, "DeclaredQuery must not be null!"); return EqlQueryParser.parseQuery(query.getQueryString()); } + /** + * @return the parser context (AST) representing the parsed query. + */ ParserRuleContext getContext() { return context; } + /** + * @return the parsed query information. + */ + Q getQueryInformation() { + return queryInformation; + } + /** * Checks if the select clause has a new constructor instantiation in the JPA query. * @@ -154,7 +167,7 @@ ParserRuleContext getContext() { */ @Override public boolean hasConstructorExpression() { - return this.introspector.hasConstructorExpression(); + return this.queryInformation.hasConstructorExpression(); } /** @@ -163,7 +176,7 @@ public boolean hasConstructorExpression() { */ @Override public String detectAlias() { - return this.introspector.getAlias(); + return this.queryInformation.getAlias(); } /** @@ -176,8 +189,7 @@ public String getProjection() { } /** - * Since the {@link JpaQueryParser} can already fully transform sorted and count queries by itself, this is a - * placeholder method. + * Since the parser can already fully transform sorted and count queries by itself, this is a placeholder method. * * @return empty set */ @@ -202,13 +214,14 @@ public DeclaredQuery getQuery() { */ @Override public String applySorting(Sort sort) { - return QueryRenderer.TokenRenderer.render(sortFunction.apply(sort, detectAlias(), null).visit(context)); + return QueryRenderer.TokenRenderer.render(sortFunction.apply(sort, this.queryInformation, null).visit(context)); } @Override public String rewrite(QueryRewriteInformation rewriteInformation) { - return QueryRenderer.TokenRenderer.render(sortFunction - .apply(rewriteInformation.getSort(), detectAlias(), rewriteInformation.getReturnedType()).visit(context)); + return QueryRenderer.TokenRenderer.render( + sortFunction.apply(rewriteInformation.getSort(), this.queryInformation, rewriteInformation.getReturnedType()) + .visit(context)); } /** @@ -240,7 +253,21 @@ public String createCountQueryFor() { */ @Override public String createCountQueryFor(@Nullable String countProjection) { - return QueryRenderer.TokenRenderer.render(countQueryFunction.apply(countProjection, detectAlias()).visit(context)); + return QueryRenderer.TokenRenderer + .render(countQueryFunction.apply(countProjection, this.queryInformation).visit(context)); + } + + /** + * Functional interface to rewrite a query considering {@link Sort} and {@link ReturnedType}. The function returns a + * visitor object that can visit the parsed query tree. + * + * @since 3.5 + */ + @FunctionalInterface + interface SortedQueryRewriteFunction { + + ParseTreeVisitor apply(Sort sort, Q queryInformation, @Nullable ReturnedType returnedType); + } /** @@ -251,7 +278,7 @@ public String createCountQueryFor(@Nullable String countProjection) { * @author Mark Paluch * @since 3.1 */ - static class HqlQueryParser extends JpaQueryEnhancer { + static class HqlQueryParser extends JpaQueryEnhancer { private HqlQueryParser(String query) { super(parse(query, HqlLexer::new, HqlParser::new, HqlParser::start), new HqlQueryIntrospector(), @@ -261,9 +288,9 @@ private HqlQueryParser(String query) { /** * Parse a HQL query. * - * @param query + * @param query the query to parse. * @return the query parser. - * @throws BadJpqlGrammarException + * @throws BadJpqlGrammarException in case of malformed query. */ public static HqlQueryParser parseQuery(String query) throws BadJpqlGrammarException { return new HqlQueryParser(query); @@ -279,7 +306,7 @@ public static HqlQueryParser parseQuery(String query) throws BadJpqlGrammarExcep * @author Mark Paluch * @since 3.2 */ - static class EqlQueryParser extends JpaQueryEnhancer { + static class EqlQueryParser extends JpaQueryEnhancer { private EqlQueryParser(String query) { super(parse(query, EqlLexer::new, EqlParser::new, EqlParser::start), new EqlQueryIntrospector(), @@ -289,9 +316,9 @@ private EqlQueryParser(String query) { /** * Parse a EQL query. * - * @param query + * @param query the query to parse. * @return the query parser. - * @throws BadJpqlGrammarException + * @throws BadJpqlGrammarException in case of malformed query. */ public static EqlQueryParser parseQuery(String query) throws BadJpqlGrammarException { return new EqlQueryParser(query); @@ -307,7 +334,7 @@ public static EqlQueryParser parseQuery(String query) throws BadJpqlGrammarExcep * @author Mark Paluch * @since 3.1 */ - static class JpqlQueryParser extends JpaQueryEnhancer { + static class JpqlQueryParser extends JpaQueryEnhancer { private JpqlQueryParser(String query) { super(parse(query, JpqlLexer::new, JpqlParser::new, JpqlParser::start), new JpqlQueryIntrospector(), @@ -317,25 +344,13 @@ private JpqlQueryParser(String query) { /** * Parse a JPQL query. * - * @param query + * @param query the query to parse. * @return the query parser. - * @throws BadJpqlGrammarException + * @throws BadJpqlGrammarException in case of malformed query. */ public static JpqlQueryParser parseQuery(String query) throws BadJpqlGrammarException { return new JpqlQueryParser(query); } } - /** - * Functional interface to rewrite a query considering {@link Sort} and {@link ReturnedType}. The function returns a - * visitor object that can visit the parsed query tree. - * - * @since 3.5 - */ - @FunctionalInterface - interface SortedQueryRewriteFunction { - - ParseTreeVisitor apply(Sort sort, String primaryAlias, @Nullable ReturnedType returnedType); - - } } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java index e83059b1c6..b8fe1eedce 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlCountQueryTransformer.java @@ -36,9 +36,9 @@ class JpqlCountQueryTransformer extends JpqlQueryRenderer { private final @Nullable String countProjection; private final @Nullable String primaryFromAlias; - JpqlCountQueryTransformer(@Nullable String countProjection, @Nullable String primaryFromAlias) { + JpqlCountQueryTransformer(@Nullable String countProjection, QueryInformation queryInformation) { this.countProjection = countProjection; - this.primaryFromAlias = primaryFromAlias; + this.primaryFromAlias = queryInformation.getAlias(); } @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryIntrospector.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryIntrospector.java index 52162e791c..48f6fef46b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryIntrospector.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryIntrospector.java @@ -15,7 +15,7 @@ */ package org.springframework.data.jpa.repository.query; -import static org.springframework.data.jpa.repository.query.QueryTokens.TOKEN_COMMA; +import static org.springframework.data.jpa.repository.query.QueryTokens.*; import java.util.ArrayList; import java.util.Collections; @@ -30,7 +30,7 @@ * @author Christoph Strobl */ @SuppressWarnings({ "UnreachableCode", "ConstantValue" }) -class JpqlQueryIntrospector extends JpqlBaseVisitor implements ParsedQueryIntrospector { +class JpqlQueryIntrospector extends JpqlBaseVisitor implements ParsedQueryIntrospector { private final JpqlQueryRenderer renderer = new JpqlQueryRenderer(); @@ -39,17 +39,10 @@ class JpqlQueryIntrospector extends JpqlBaseVisitor implements ParsedQuery private boolean projectionProcessed; private boolean hasConstructorExpression = false; - @Nullable - public String getAlias() { - return primaryFromAlias; - } - - public List getProjection() { - return projection == null ? Collections.emptyList() : projection; - } - - public boolean hasConstructorExpression() { - return hasConstructorExpression; + @Override + public QueryInformation getParsedQueryInformation() { + return new QueryInformation(primaryFromAlias, projection == null ? Collections.emptyList() : projection, + hasConstructorExpression); } @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java index 108cb209fd..41d0661d2c 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlSortedQueryTransformer.java @@ -41,21 +41,13 @@ class JpqlSortedQueryTransformer extends JpqlQueryRenderer { private final @Nullable String primaryFromAlias; private final @Nullable DtoProjectionTransformerDelegate dtoDelegate; - JpqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias) { + JpqlSortedQueryTransformer(Sort sort, QueryInformation queryInformation, @Nullable ReturnedType returnedType) { Assert.notNull(sort, "Sort must not be null"); + Assert.notNull(queryInformation, "ParsedHqlQueryInformation must not be null"); this.sort = sort; - this.primaryFromAlias = primaryFromAlias; - this.dtoDelegate = null; - } - - JpqlSortedQueryTransformer(Sort sort, @Nullable String primaryFromAlias, @Nullable ReturnedType returnedType) { - - Assert.notNull(sort, "Sort must not be null"); - - this.sort = sort; - this.primaryFromAlias = primaryFromAlias; + this.primaryFromAlias = queryInformation.getAlias(); this.dtoDelegate = returnedType == null ? null : new DtoProjectionTransformerDelegate(returnedType); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParsedQueryIntrospector.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParsedQueryIntrospector.java index cab53a6e9f..caf74035ba 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParsedQueryIntrospector.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParsedQueryIntrospector.java @@ -15,19 +15,16 @@ */ package org.springframework.data.jpa.repository.query; -import java.util.List; - import org.antlr.v4.runtime.tree.ParseTree; -import org.springframework.lang.Nullable; - /** * Interface defining an introspector for String-queries providing details about the primary table alias, the primary * selection projection and whether the query makes use of constructor expressions. * * @author Mark Paluch + * @since 3.5 */ -interface ParsedQueryIntrospector { +interface ParsedQueryIntrospector { /** * Visit the parsed tree to introspect the AST tree. @@ -38,20 +35,10 @@ interface ParsedQueryIntrospector { Void visit(ParseTree tree); /** - * Primary table alias. Contains the first table name/table alias in case multiple tables are specified in the query. + * Returns the parsed query information. The information is available after the {@link #visit(ParseTree) + * introspection} has been completed. * - * @return - */ - @Nullable - String getAlias(); - - /** - * @return the primary selection. - */ - List getProjection(); - - /** - * @return {@code true} if the query uses a constructor expression. + * @return parsed query information. */ - boolean hasConstructorExpression(); + I getParsedQueryInformation(); } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryInformation.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryInformation.java new file mode 100644 index 0000000000..07c1def305 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryInformation.java @@ -0,0 +1,65 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import java.util.List; + +import org.springframework.lang.Nullable; + +/** + * Value object capturing introspection details of a parsed query. + * + * @author Mark Paluch + * @since 3.5 + */ +class QueryInformation { + + private final @Nullable String alias; + + private final List projection; + + private final boolean hasConstructorExpression; + + QueryInformation(@Nullable String alias, List projection, boolean hasConstructorExpression) { + this.alias = alias; + this.projection = projection; + this.hasConstructorExpression = hasConstructorExpression; + } + + /** + * Primary table alias. Contains the first table name/table alias in case multiple tables are specified in the query. + * + * @return + */ + @Nullable + public String getAlias() { + return alias; + } + + /** + * @return the primary selection. + */ + public List getProjection() { + return projection; + } + + /** + * @return {@code true} if the query uses a constructor expression. + */ + public boolean hasConstructorExpression() { + return hasConstructorExpression; + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlDtoQueryTransformerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlDtoQueryTransformerUnitTests.java index 9d0364410f..a2f1a125fb 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlDtoQueryTransformerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlDtoQueryTransformerUnitTests.java @@ -35,15 +35,13 @@ class EqlDtoQueryTransformerUnitTests { JpaQueryMethod method = getMethod("dtoProjection"); - EqlSortedQueryTransformer transformer = new EqlSortedQueryTransformer(Sort.unsorted(), null, - method.getResultProcessor().getReturnedType()); @Test // GH-3076 void shouldTranslateSingleProjectionToDto() { JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser.parseQuery("SELECT p from Person p"); - QueryTokenStream visit = transformer.visit(parser.getContext()); + QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( "SELECT new org.springframework.data.jpa.repository.query.EqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p"); @@ -55,7 +53,7 @@ void shouldRewriteQueriesWithSubselect() { JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser .parseQuery("select u from User u left outer join u.roles r where r in (select r from Role r)"); - QueryTokenStream visit = transformer.visit(parser.getContext()); + QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( "select new org.springframework.data.jpa.repository.query.EqlDtoQueryTransformerUnitTests$MyRecord(u.foo, u.bar) from User u left outer join u.roles r where r in (select r from Role r)"); @@ -67,7 +65,7 @@ void shouldNotTranslateConstructorExpressionQuery() { JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser .parseQuery("SELECT NEW String(p) from Person p"); - QueryTokenStream visit = transformer.visit(parser.getContext()); + QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW String(p) from Person p"); } @@ -78,6 +76,7 @@ void shouldTranslatePropertySelectionToDto() { JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser .parseQuery("SELECT p.foo, p.bar, sum(p.age) from Person p"); + EqlSortedQueryTransformer transformer = getTransformer(parser); QueryTokenStream visit = transformer.visit(parser.getContext()); assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( @@ -97,6 +96,11 @@ private JpaQueryMethod getMethod(String name, Class... parameterTypes) { } } + private EqlSortedQueryTransformer getTransformer(JpaQueryEnhancer.EqlQueryParser parser) { + return new EqlSortedQueryTransformer(Sort.unsorted(), parser.getQueryInformation(), + method.getResultProcessor().getReturnedType()); + } + interface MyRepo extends Repository { MyRecord dtoProjection(); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlDtoQueryTransformerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlDtoQueryTransformerUnitTests.java index 59488eff1f..9afdcd9764 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlDtoQueryTransformerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlDtoQueryTransformerUnitTests.java @@ -35,15 +35,13 @@ class HqlDtoQueryTransformerUnitTests { JpaQueryMethod method = getMethod("dtoProjection"); - HqlSortedQueryTransformer transformer = new HqlSortedQueryTransformer(Sort.unsorted(), null, - method.getResultProcessor().getReturnedType()); @Test // GH-3076 void shouldTranslateSingleProjectionToDto() { JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser.parseQuery("SELECT p from Person p"); - QueryTokenStream visit = transformer.visit(parser.getContext()); + QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( "SELECT new org.springframework.data.jpa.repository.query.HqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p"); @@ -55,7 +53,7 @@ void shouldRewriteQueriesWithSubselect() { JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser .parseQuery("select u from User u left outer join u.roles r where r in (select r from Role r)"); - QueryTokenStream visit = transformer.visit(parser.getContext()); + QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( "select new org.springframework.data.jpa.repository.query.HqlDtoQueryTransformerUnitTests$MyRecord(u.foo, u.bar) from User u left outer join u.roles r where r in (select r from Role r)"); @@ -67,7 +65,7 @@ void shouldNotTranslateConstructorExpressionQuery() { JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser .parseQuery("SELECT NEW String(p) from Person p"); - QueryTokenStream visit = transformer.visit(parser.getContext()); + QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW String(p) from Person p"); } @@ -78,7 +76,7 @@ void shouldTranslatePropertySelectionToDto() { JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser .parseQuery("SELECT p.foo, p.bar, sum(p.age) from Person p"); - QueryTokenStream visit = transformer.visit(parser.getContext()); + QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( "SELECT new org.springframework.data.jpa.repository.query.HqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar, sum(p.age)) from Person p"); @@ -97,6 +95,11 @@ private JpaQueryMethod getMethod(String name, Class... parameterTypes) { } } + private HqlSortedQueryTransformer getTransformer(JpaQueryEnhancer.HqlQueryParser parser) { + return new HqlSortedQueryTransformer(Sort.unsorted(), parser.getQueryInformation(), + method.getResultProcessor().getReturnedType()); + } + interface MyRepo extends Repository { MyRecord dtoProjection(); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlDtoQueryTransformerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlDtoQueryTransformerUnitTests.java index 4fbb6c8142..38c416271f 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlDtoQueryTransformerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlDtoQueryTransformerUnitTests.java @@ -35,15 +35,13 @@ class JpqlDtoQueryTransformerUnitTests { JpaQueryMethod method = getMethod("dtoProjection"); - JpqlSortedQueryTransformer transformer = new JpqlSortedQueryTransformer(Sort.unsorted(), null, - method.getResultProcessor().getReturnedType()); @Test // GH-3076 void shouldTranslateSingleProjectionToDto() { JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser.parseQuery("SELECT p from Person p"); - QueryTokenStream visit = transformer.visit(parser.getContext()); + QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( "SELECT new org.springframework.data.jpa.repository.query.JpqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar) from Person p"); @@ -55,7 +53,7 @@ void shouldRewriteQueriesWithSubselect() { JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser .parseQuery("select u from User u left outer join u.roles r where r in (select r from Role r)"); - QueryTokenStream visit = transformer.visit(parser.getContext()); + QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( "select new org.springframework.data.jpa.repository.query.JpqlDtoQueryTransformerUnitTests$MyRecord(u.foo, u.bar) from User u left outer join u.roles r where r in (select r from Role r)"); @@ -67,7 +65,7 @@ void shouldNotTranslateConstructorExpressionQuery() { JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser .parseQuery("SELECT NEW String(p) from Person p"); - QueryTokenStream visit = transformer.visit(parser.getContext()); + QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo("SELECT NEW String(p) from Person p"); } @@ -78,7 +76,7 @@ void shouldTranslatePropertySelectionToDto() { JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser .parseQuery("SELECT p.foo, p.bar, sum(p.age) from Person p"); - QueryTokenStream visit = transformer.visit(parser.getContext()); + QueryTokenStream visit = getTransformer(parser).visit(parser.getContext()); assertThat(QueryRenderer.TokenRenderer.render(visit)).isEqualTo( "SELECT new org.springframework.data.jpa.repository.query.JpqlDtoQueryTransformerUnitTests$MyRecord(p.foo, p.bar, sum(p.age)) from Person p"); @@ -97,6 +95,11 @@ private JpaQueryMethod getMethod(String name, Class... parameterTypes) { } } + private JpqlSortedQueryTransformer getTransformer(JpaQueryEnhancer.JpqlQueryParser parser) { + return new JpqlSortedQueryTransformer(Sort.unsorted(), parser.getQueryInformation(), + method.getResultProcessor().getReturnedType()); + } + interface MyRepo extends Repository { MyRecord dtoProjection();