Move query rendering to dedicated TokenRenderer.

This commit introduces a QueryTokenStream to reduce the number of stream and collect operations to estimate the size before iterating entries.

See: #3309
This commit is contained in:
Christoph Strobl
2024-06-04 11:21:54 +02:00
parent 2dc94dde8c
commit f793c9eef0
23 changed files with 425 additions and 188 deletions

View File

@@ -39,4 +39,9 @@ public interface PersonRepository extends ListCrudRepository<Person, Integer> {
@Query(value = "SELECT * FROM person WHERE firstname = ?1", nativeQuery = true)
List<Person> findAllWithNativeQueryByFirstname(String firstname);
Long countByFirstname(String firstname);
@Query("SELECT COUNT(*) FROM org.springframework.data.jpa.model.Person p WHERE p.firstname = ?1")
Long countWithAnnotatedQueryByFirstname(String firstname);
}

View File

@@ -58,6 +58,9 @@ import org.springframework.util.ObjectUtils;
@Timeout(time = 2)
public class RepositoryFinderTests {
private static final String PERSON_FIRSTNAME = "first";
private static final String COLUMN_PERSON_FIRSTNAME = "firstname";
@State(Scope.Benchmark)
public static class BenchmarkParameters {
@@ -83,7 +86,7 @@ public class RepositoryFinderTests {
entityManager.persist(generalProfile);
entityManager.persist(sdUserProfile);
Person person = new Person("first", "last");
Person person = new Person(PERSON_FIRSTNAME, "last");
person.setProfiles(Set.of(generalProfile, sdUserProfile));
entityManager.persist(person);
entityManager.getTransaction().commit();
@@ -128,7 +131,7 @@ public class RepositoryFinderTests {
CriteriaQuery<Person> query = criteriaBuilder.createQuery(Person.class);
Root<Person> root = query.from(Person.class);
TypedQuery<Person> typedQuery = parameters.entityManager
.createQuery(query.where(criteriaBuilder.equal(root.get("firstname"), "first")));
.createQuery(query.where(criteriaBuilder.equal(root.get(COLUMN_PERSON_FIRSTNAME), PERSON_FIRSTNAME)));
return typedQuery.getResultList();
}
@@ -138,35 +141,52 @@ public class RepositoryFinderTests {
Query query = parameters.entityManager
.createQuery("SELECT p FROM org.springframework.data.jpa.model.Person p WHERE p.firstname = ?1");
query.setParameter(1, "first");
query.setParameter(1, PERSON_FIRSTNAME);
return query.getResultList();
}
@Benchmark
public Long baselineEntityManagerCount(BenchmarkParameters parameters) {
Query query = parameters.entityManager.createQuery("SELECT COUNT(*) FROM org.springframework.data.jpa.model.Person p WHERE p.firstname = ?1");
query.setParameter(1, PERSON_FIRSTNAME);
return (Long) query.getSingleResult();
}
@Benchmark
public List<Person> derivedFinderMethod(BenchmarkParameters parameters) {
return parameters.repositoryProxy.findAllByFirstname("first");
return parameters.repositoryProxy.findAllByFirstname(PERSON_FIRSTNAME);
}
@Benchmark
public List<IPersonProjection> derivedFinderMethodWithInterfaceProjection(BenchmarkParameters parameters) {
return parameters.repositoryProxy.findAllAndProjectToInterfaceByFirstname("first");
return parameters.repositoryProxy.findAllAndProjectToInterfaceByFirstname(PERSON_FIRSTNAME);
}
@Benchmark
public List<Person> stringBasedQuery(BenchmarkParameters parameters) {
return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname("first");
return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname(PERSON_FIRSTNAME);
}
@Benchmark
public List<Person> stringBasedQueryDynamicSort(BenchmarkParameters parameters) {
return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname("first", Sort.by("firstname"));
return parameters.repositoryProxy.findAllWithAnnotatedQueryByFirstname(PERSON_FIRSTNAME, Sort.by(COLUMN_PERSON_FIRSTNAME));
}
@Benchmark
public List<Person> stringBasedNativeQuery(BenchmarkParameters parameters) {
return parameters.repositoryProxy.findAllWithNativeQueryByFirstname("first");
return parameters.repositoryProxy.findAllWithNativeQueryByFirstname(PERSON_FIRSTNAME);
}
@Benchmark
public Long derivedCount(BenchmarkParameters parameters) {
return parameters.repositoryProxy.countByFirstname(PERSON_FIRSTNAME);
}
@Benchmark
public Long stringBasedCount(BenchmarkParameters parameters) {
return parameters.repositoryProxy.countWithAnnotatedQueryByFirstname(PERSON_FIRSTNAME);
}
}

View File

@@ -17,9 +17,8 @@ package org.springframework.data.jpa.repository.query;
import static org.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import java.util.List;
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
import org.springframework.data.jpa.repository.query.QueryTransformers.CountSelectionTokenStream;
import org.springframework.lang.Nullable;
/**
@@ -87,10 +86,10 @@ class EqlCountQueryTransformer extends EqlQueryRenderer {
QueryRendererBuilder selectionListbuilder = QueryRendererBuilder.concat(ctx.select_item(), this::visit,
TOKEN_COMMA);
List<JpaQueryParsingToken> countSelection = QueryTransformers
.filterCountSelection(selectionListbuilder.build().stream().toList());
CountSelectionTokenStream countSelection = QueryTransformers
.filterCountSelection(selectionListbuilder);
if (countSelection.stream().anyMatch(eqlToken -> eqlToken.getToken().contains("new"))) {
if (countSelection.requiresPrimaryAlias()) {
// constructor
nested.append(new JpaQueryParsingToken(primaryFromAlias));
} else {

View File

@@ -111,8 +111,7 @@ class EqlSortedQueryTransformer extends EqlQueryRenderer {
QueryRendererBuilder builder = super.visitSelect_item(ctx);
if (ctx.result_variable() != null) {
List<JpaQueryParsingToken> tokens = builder.build().stream().toList();
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
transformerSupport.registerAlias(builder.lastToken());
}
return builder;
@@ -122,9 +121,7 @@ class EqlSortedQueryTransformer extends EqlQueryRenderer {
public QueryRendererBuilder visitJoin(EqlParser.JoinContext ctx) {
QueryRendererBuilder builder = super.visitJoin(ctx);
List<JpaQueryParsingToken> tokens = builder.build().stream().toList();
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
transformerSupport.registerAlias(builder.lastToken());
return builder;
}

View File

@@ -17,9 +17,8 @@ package org.springframework.data.jpa.repository.query;
import static org.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import java.util.List;
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
import org.springframework.data.jpa.repository.query.QueryTransformers.CountSelectionTokenStream;
import org.springframework.lang.Nullable;
/**
@@ -196,10 +195,10 @@ class HqlCountQueryTransformer extends HqlQueryRenderer {
if (ctx.DISTINCT() != null) {
List<JpaQueryParsingToken> countSelection = QueryTransformers
.filterCountSelection(selectionListbuilder.build().stream().toList());
CountSelectionTokenStream countSelection = QueryTransformers
.filterCountSelection(selectionListbuilder);
if (countSelection.stream().anyMatch(hqlToken -> hqlToken.getToken().contains("new"))) {
if (countSelection.requiresPrimaryAlias()) {
// constructor
nested.append(new JpaQueryParsingToken(primaryFromAlias));
} else {

View File

@@ -2291,21 +2291,21 @@ class HqlQueryRenderer extends HqlBaseVisitor<QueryRendererBuilder> {
if (ctx.IS() != null) {
builder.append(JpaQueryExpression.expression(ctx.IS()));
builder.append(JpaExpressionToken.expression(ctx.IS()));
if (ctx.NOT() != null) {
builder.append(JpaQueryParsingToken.expression(ctx.NOT()));
}
builder.append(JpaQueryExpression.expression(ctx.EMPTY()));
builder.append(JpaExpressionToken.expression(ctx.EMPTY()));
} else if (ctx.MEMBER() != null) {
if (ctx.NOT() != null) {
builder.append(JpaQueryParsingToken.expression(ctx.NOT()));
}
builder.append(JpaQueryExpression.expression(ctx.MEMBER()));
builder.append(JpaQueryExpression.expression(ctx.OF()));
builder.append(JpaExpressionToken.expression(ctx.MEMBER()));
builder.append(JpaExpressionToken.expression(ctx.OF()));
builder.append(visit(ctx.path()));
}

View File

@@ -101,8 +101,7 @@ class HqlSortedQueryTransformer extends HqlQueryRenderer {
QueryRendererBuilder builder = super.visitJoinPath(ctx);
if (ctx.variable() != null) {
List<JpaQueryParsingToken> tokens = builder.build().stream().toList();
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
transformerSupport.registerAlias(builder.lastToken());
}
return builder;
@@ -114,8 +113,7 @@ class HqlSortedQueryTransformer extends HqlQueryRenderer {
QueryRendererBuilder builder = super.visitJoinSubquery(ctx);
if (ctx.variable() != null) {
List<JpaQueryParsingToken> tokens = builder.build().stream().toList();
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
transformerSupport.registerAlias(builder.lastToken());
}
return builder;
@@ -127,8 +125,7 @@ class HqlSortedQueryTransformer extends HqlQueryRenderer {
QueryRendererBuilder builder = super.visitVariable(ctx);
if (ctx.identifier() != null) {
List<JpaQueryParsingToken> tokens = builder.build().stream().toList();
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
transformerSupport.registerAlias(builder.lastToken());
}
return builder;

View File

@@ -15,8 +15,6 @@
*/
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;
@@ -31,7 +29,6 @@ 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;
@@ -65,7 +62,7 @@ class JpaQueryEnhancer implements QueryEnhancer {
this.introspector.visit(context);
List<JpaQueryParsingToken> tokens = introspector.getProjection();
this.projection = tokens.isEmpty() ? "" : render(tokens);
this.projection = tokens.isEmpty() ? "" : QueryRenderer.TokenRenderer.render(tokens);
}
static <P extends Parser> ParserRuleContext parse(String query, Function<CharStream, Lexer> lexerFactoryFunction,
@@ -194,7 +191,7 @@ class JpaQueryEnhancer implements QueryEnhancer {
*/
@Override
public String applySorting(Sort sort) {
return render(sortFunction.apply(sort, detectAlias()).visit(context));
return QueryRenderer.TokenRenderer.render(sortFunction.apply(sort, detectAlias()).visit(context));
}
/**
@@ -226,7 +223,7 @@ class JpaQueryEnhancer implements QueryEnhancer {
*/
@Override
public String createCountQueryFor(@Nullable String countProjection) {
return render(countQueryFunction.apply(countProjection, detectAlias()).visit(context));
return QueryRenderer.TokenRenderer.render(countQueryFunction.apply(countProjection, detectAlias()).visit(context));
}
/**

View File

@@ -15,7 +15,6 @@
*/
package org.springframework.data.jpa.repository.query;
import java.util.Collection;
import java.util.function.Supplier;
import org.antlr.v4.runtime.Token;
@@ -26,41 +25,42 @@ import org.antlr.v4.runtime.tree.TerminalNode;
* in the parsing process, so the text itself is wrapped in a {@link Supplier}.
*
* @author Greg Turnquist
* @author Christoph Strobl
* @since 3.1
*/
class JpaQueryParsingToken {
class JpaQueryParsingToken implements QueryToken {
/**
* Commonly use tokens.
*/
public static final JpaQueryParsingToken TOKEN_NONE = JpaQueryParsingToken.token("");
public static final JpaQueryParsingToken TOKEN_COMMA = JpaQueryParsingToken.token(", ");
public static final JpaQueryParsingToken TOKEN_SPACE = JpaQueryParsingToken.token(" ");
public static final JpaQueryParsingToken TOKEN_DOT = JpaQueryParsingToken.token(".");
public static final JpaQueryParsingToken TOKEN_EQUALS = JpaQueryParsingToken.token(" = ");
public static final JpaQueryParsingToken TOKEN_OPEN_PAREN = JpaQueryParsingToken.token("(");
public static final JpaQueryParsingToken TOKEN_CLOSE_PAREN = JpaQueryParsingToken.token(")");
public static final JpaQueryParsingToken TOKEN_ORDER_BY = JpaQueryParsingToken.expression("order by");
public static final JpaQueryParsingToken TOKEN_LOWER_FUNC = new JpaQueryParsingToken("lower(");
public static final JpaQueryParsingToken TOKEN_SELECT_COUNT = JpaQueryParsingToken.token("select count(");
public static final JpaQueryParsingToken TOKEN_COUNT_FUNC = JpaQueryParsingToken.token("count(");
public static final JpaQueryParsingToken TOKEN_DOUBLE_PIPE = JpaQueryParsingToken.token(" || ");
public static final JpaQueryParsingToken TOKEN_OPEN_SQUARE_BRACKET = JpaQueryParsingToken.token("[");
public static final JpaQueryParsingToken TOKEN_CLOSE_SQUARE_BRACKET = new JpaQueryParsingToken("]");
public static final JpaQueryParsingToken TOKEN_COLON = JpaQueryParsingToken.token(":");
public static final JpaQueryParsingToken TOKEN_QUESTION_MARK = JpaQueryParsingToken.token("?");
public static final JpaQueryParsingToken TOKEN_OPEN_BRACE = JpaQueryParsingToken.token("{");
public static final JpaQueryParsingToken TOKEN_CLOSE_BRACE = new JpaQueryParsingToken("}");
public static final JpaQueryParsingToken TOKEN_DOUBLE_UNDERSCORE = JpaQueryParsingToken.token("__");
public static final JpaQueryParsingToken TOKEN_AS = JpaQueryParsingToken.expression("AS");
public static final JpaQueryParsingToken TOKEN_DESC = JpaQueryParsingToken.expression("desc");
public static final JpaQueryParsingToken TOKEN_ASC = JpaQueryParsingToken.expression("asc");
public static final JpaQueryParsingToken TOKEN_WITH = JpaQueryParsingToken.expression("WITH");
public static final JpaQueryParsingToken TOKEN_NOT = JpaQueryParsingToken.expression("NOT");
public static final JpaQueryParsingToken TOKEN_MATERIALIZED = JpaQueryParsingToken.expression("materialized");
public static final JpaQueryParsingToken TOKEN_NULLS = JpaQueryParsingToken.expression("NULLS");
public static final JpaQueryParsingToken TOKEN_FIRST = JpaQueryParsingToken.expression("FIRST");
public static final JpaQueryParsingToken TOKEN_LAST = JpaQueryParsingToken.expression("LAST");
static final JpaQueryParsingToken TOKEN_NONE = JpaQueryParsingToken.token("");
static final JpaQueryParsingToken TOKEN_COMMA = JpaQueryParsingToken.token(", ");
static final JpaQueryParsingToken TOKEN_SPACE = JpaQueryParsingToken.token(" ");
static final JpaQueryParsingToken TOKEN_DOT = JpaQueryParsingToken.token(".");
static final JpaQueryParsingToken TOKEN_EQUALS = JpaQueryParsingToken.token(" = ");
static final JpaQueryParsingToken TOKEN_OPEN_PAREN = JpaQueryParsingToken.token("(");
static final JpaQueryParsingToken TOKEN_CLOSE_PAREN = JpaQueryParsingToken.token(")");
static final JpaQueryParsingToken TOKEN_ORDER_BY = JpaQueryParsingToken.expression("order by");
static final JpaQueryParsingToken TOKEN_LOWER_FUNC = JpaQueryParsingToken.token("lower(");
static final JpaQueryParsingToken TOKEN_SELECT_COUNT = JpaQueryParsingToken.token("select count(");
static final JpaQueryParsingToken TOKEN_COUNT_FUNC = JpaQueryParsingToken.token("count(");
static final JpaQueryParsingToken TOKEN_DOUBLE_PIPE = JpaQueryParsingToken.token(" || ");
static final JpaQueryParsingToken TOKEN_OPEN_SQUARE_BRACKET = JpaQueryParsingToken.token("[");
static final JpaQueryParsingToken TOKEN_CLOSE_SQUARE_BRACKET = JpaQueryParsingToken.token("]");
static final JpaQueryParsingToken TOKEN_COLON = JpaQueryParsingToken.token(":");
static final JpaQueryParsingToken TOKEN_QUESTION_MARK = JpaQueryParsingToken.token("?");
static final JpaQueryParsingToken TOKEN_OPEN_BRACE = JpaQueryParsingToken.token("{");
static final JpaQueryParsingToken TOKEN_CLOSE_BRACE = JpaQueryParsingToken.token("}");
static final JpaQueryParsingToken TOKEN_DOUBLE_UNDERSCORE = JpaQueryParsingToken.token("__");
static final JpaQueryParsingToken TOKEN_AS = JpaQueryParsingToken.expression("AS");
static final JpaQueryParsingToken TOKEN_DESC = JpaQueryParsingToken.expression("desc");
static final JpaQueryParsingToken TOKEN_ASC = JpaQueryParsingToken.expression("asc");
static final JpaQueryParsingToken TOKEN_WITH = JpaQueryParsingToken.expression("WITH");
static final JpaQueryParsingToken TOKEN_NOT = JpaQueryParsingToken.expression("NOT");
static final JpaQueryParsingToken TOKEN_MATERIALIZED = JpaQueryParsingToken.expression("materialized");
static final JpaQueryParsingToken TOKEN_NULLS = JpaQueryParsingToken.expression("NULLS");
static final JpaQueryParsingToken TOKEN_FIRST = JpaQueryParsingToken.expression("FIRST");
static final JpaQueryParsingToken TOKEN_LAST = JpaQueryParsingToken.expression("LAST");
/**
* The text value of the token.
@@ -71,11 +71,11 @@ class JpaQueryParsingToken {
this.token = token;
}
public static JpaQueryParsingToken token(TerminalNode node) {
static JpaQueryParsingToken token(TerminalNode node) {
return token(node.getText());
}
public static JpaQueryParsingToken token(Token token) {
static JpaQueryParsingToken token(Token token) {
return token(token.getText());
}
@@ -84,22 +84,26 @@ class JpaQueryParsingToken {
}
static JpaQueryParsingToken expression(String expression) {
return new JpaQueryExpression(expression);
return new JpaExpressionToken(expression);
}
public static JpaQueryParsingToken expression(Token token) {
static JpaQueryParsingToken expression(Token token) {
return expression(token.getText());
}
public static JpaQueryParsingToken expression(TerminalNode node) {
static JpaQueryParsingToken expression(TerminalNode node) {
return expression(node.getText());
}
public static JpaQueryParsingToken ventilated(Token op) {
static JpaQueryParsingToken ventilated(Token op) {
return new JpaQueryParsingToken(" " + op.getText() + " ");
}
String getToken() {
return value();
}
public String value() {
return token;
}
@@ -109,8 +113,8 @@ class JpaQueryParsingToken {
* @param token must not be {@literal null}.
* @return {@literal true} if both tokens are equals (using case-insensitive comparison).
*/
boolean isA(JpaQueryParsingToken token) {
return token.getToken().equalsIgnoreCase(this.getToken());
public boolean isA(QueryToken token) {
return token.value().equalsIgnoreCase(this.value());
}
@Override
@@ -118,52 +122,14 @@ class JpaQueryParsingToken {
return getToken();
}
/**
* Render a list of {@link JpaQueryParsingToken}s into a string.
*
* @param tokens
* @return rendered string containing either a query or some subset of that query
*/
static String render(Object tokens) {
static class JpaExpressionToken extends JpaQueryParsingToken {
if (tokens instanceof Collection tpr) {
return render(tpr);
}
return ((QueryRenderer.QueryRendererBuilder) tokens).build().render();
}
/**
* Render a list of {@link JpaQueryParsingToken}s into a string.
*
* @param tokens
* @return rendered string containing either a query or some subset of that query
*/
static String render(Collection<JpaQueryParsingToken> tokens) {
StringBuilder results = new StringBuilder();
boolean previousExpression = false;
for (JpaQueryParsingToken jpaQueryParsingToken : tokens) {
if (previousExpression) {
if (!results.isEmpty() && results.charAt(results.length() - 1) != ' ') {
results.append(' ');
}
}
previousExpression = jpaQueryParsingToken instanceof JpaQueryExpression;
results.append(jpaQueryParsingToken.getToken());
}
return results.toString();
}
static class JpaQueryExpression extends JpaQueryParsingToken {
JpaQueryExpression(String token) {
JpaExpressionToken(String token) {
super(token);
}
public boolean isExpression() {
return true;
}
}
}

View File

@@ -41,6 +41,10 @@ class JpaQueryTransformerSupport {
projectionAliases.add(token);
}
void registerAlias(QueryToken token) {
projectionAliases.add(token.value());
}
/**
* Using the primary {@literal FROM} clause's alias and a {@link Sort}, construct all the {@literal ORDER BY}
* arguments.

View File

@@ -17,9 +17,8 @@ package org.springframework.data.jpa.repository.query;
import static org.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import java.util.List;
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
import org.springframework.data.jpa.repository.query.QueryTransformers.CountSelectionTokenStream;
import org.springframework.lang.Nullable;
/**
@@ -87,10 +86,10 @@ class JpqlCountQueryTransformer extends JpqlQueryRenderer {
QueryRendererBuilder selectionListbuilder = QueryRendererBuilder.concat(ctx.select_item(), this::visit,
TOKEN_COMMA);
List<JpaQueryParsingToken> countSelection = QueryTransformers
.filterCountSelection(selectionListbuilder.build().stream().toList());
CountSelectionTokenStream countSelection = QueryTransformers
.filterCountSelection(selectionListbuilder);
if (countSelection.stream().anyMatch(jpqlToken -> jpqlToken.getToken().contains("new"))) {
if (countSelection.requiresPrimaryAlias()) {
// constructor
nested.append(new JpaQueryParsingToken(primaryFromAlias));
} else {

View File

@@ -105,8 +105,7 @@ class JpqlSortedQueryTransformer extends JpqlQueryRenderer {
QueryRendererBuilder builder = super.visitSelect_item(ctx);
if (ctx.result_variable() != null) {
List<JpaQueryParsingToken> tokens = builder.build().stream().toList();
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
transformerSupport.registerAlias(builder.lastToken());
}
return builder;
@@ -117,8 +116,7 @@ class JpqlSortedQueryTransformer extends JpqlQueryRenderer {
QueryRendererBuilder builder = super.visitJoin(ctx);
List<JpaQueryParsingToken> tokens = builder.build().stream().toList();
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
transformerSupport.registerAlias(builder.lastToken());
return builder;
}

View File

@@ -17,17 +17,22 @@ package org.springframework.data.jpa.repository.query;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;
import org.antlr.v4.runtime.TokenStream;
import org.springframework.util.CompositeIterator;
/**
* Abstraction to encapsulate query expressions and render a query.
* <p>
* Query rendering consists of multiple building blocks:
* <ul>
* <li>{@link JpaQueryParsingToken tokens} and
* {@link org.springframework.data.jpa.repository.query.JpaQueryParsingToken.JpaQueryExpression expression tokens}</li>
* {@link org.springframework.data.jpa.repository.query.JpaQueryParsingToken.JpaExpressionToken expression tokens}</li>
* <li>{@link QueryRenderer compositions} such as a composition of multiple tokens.</li>
* <li>{@link QueryRenderer expressions} that are individual parts such as {@code SELECT} or {@code ORDER BY …}</li>
* <li>{@link QueryRenderer inline expressions} such as composition of tokens and expressions such as function calls
@@ -36,7 +41,7 @@ import java.util.stream.Stream;
*
* @author Mark Paluch
*/
abstract class QueryRenderer {
abstract class QueryRenderer implements QueryTokenStream<QueryToken> {
/**
* Creates a QueryRenderer from a collection of {@link JpaQueryParsingToken}.
@@ -44,8 +49,8 @@ abstract class QueryRenderer {
* @param tokens
* @return
*/
static QueryRenderer from(Collection<JpaQueryParsingToken> tokens) {
List<JpaQueryParsingToken> tokensToUse = new ArrayList<>(32);
static QueryRenderer from(Collection<? extends QueryToken> tokens) {
List<QueryToken> tokensToUse = new ArrayList<>(Math.max(tokens.size(), 32));
tokensToUse.addAll(tokens);
return new TokenRenderer(tokensToUse);
}
@@ -80,10 +85,7 @@ abstract class QueryRenderer {
* @return
*/
QueryRenderer append(QueryRenderer renderer) {
List<QueryRenderer> objects = new ArrayList<>(32);
objects.add(this);
objects.add(renderer);
return new CompositeRenderer(objects);
return CompositeRenderer.combine(this, renderer);
}
/**
@@ -93,13 +95,6 @@ abstract class QueryRenderer {
return false;
}
/**
* @return stream of tokens.
*/
public Stream<JpaQueryParsingToken> stream() {
return Stream.empty();
}
@Override
public String toString() {
return render();
@@ -111,9 +106,21 @@ abstract class QueryRenderer {
static class CompositeRenderer extends QueryRenderer {
private final List<QueryRenderer> nested;
private int size;
CompositeRenderer(List<QueryRenderer> nested) {
this.nested = new ArrayList<>(nested);
static CompositeRenderer combine(QueryRenderer root, QueryRenderer nested) {
List<QueryRenderer> queryRenderers = new ArrayList<>(32);
queryRenderers.add(root);
queryRenderers.add(nested);
return new CompositeRenderer(queryRenderers, root.estimatedSize() + nested.estimatedSize());
}
private CompositeRenderer(List<QueryRenderer> nested, int size) {
this.nested = nested;
this.size = size;
}
@Override
@@ -140,6 +147,7 @@ abstract class QueryRenderer {
QueryRenderer append(QueryRenderer renderer) {
nested.add(renderer);
this.size += renderer.estimatedSize();
return this;
}
@@ -149,15 +157,18 @@ abstract class QueryRenderer {
}
@Override
public Stream<JpaQueryParsingToken> stream() {
Stream<JpaQueryParsingToken> stream = Stream.empty();
public Iterator<QueryToken> iterator() {
CompositeIterator<QueryToken> iterator = new CompositeIterator<>();
for (QueryRenderer renderer : nested) {
stream = Stream.concat(stream, renderer.stream());
iterator.add(renderer.iterator());
}
return iterator;
}
return stream;
@Override
public int estimatedSize() {
return size;
}
}
@@ -166,15 +177,15 @@ abstract class QueryRenderer {
*/
static class TokenRenderer extends QueryRenderer {
private final List<JpaQueryParsingToken> tokens;
private final List<QueryToken> tokens;
TokenRenderer(List<JpaQueryParsingToken> tokens) {
TokenRenderer(List<QueryToken> tokens) {
this.tokens = tokens;
}
@Override
String render() {
return JpaQueryParsingToken.render(tokens);
return render(tokens);
}
@Override
@@ -190,19 +201,80 @@ abstract class QueryRenderer {
@Override
public boolean isExpression() {
return !tokens.isEmpty() && tokens.get(tokens.size() - 1) instanceof JpaQueryParsingToken.JpaQueryExpression;
return !tokens.isEmpty() && tokens.get(tokens.size() - 1).isExpression();
}
@Override
public Stream<JpaQueryParsingToken> stream() {
public Stream<QueryToken> stream() {
return tokens.stream();
}
@Override
public Iterator<QueryToken> iterator() {
return tokens.iterator();
}
@Override
public List<QueryToken> toList() {
return tokens;
}
@Override
public int estimatedSize() {
return tokens.size();
}
/**
* Render a list of {@link JpaQueryParsingToken}s into a string.
*
* @param tokens
* @return rendered string containing either a query or some subset of that query
*/
static String render(Object tokens) {
if (tokens instanceof Collection tpr) {
return render(tpr);
}
if(tokens instanceof QueryRendererBuilder qrb) {
return qrb.build().render();
}
if(tokens instanceof QueryRenderer qr) {
return qr.render();
}
throw new IllegalArgumentException("Unknown token type %s".formatted(tokens));
}
static String render(Collection<QueryToken> tokens) {
StringBuilder results = new StringBuilder();
boolean previousExpression = false;
for (QueryToken jpaQueryParsingToken : tokens) {
if (previousExpression) {
if (!results.isEmpty() && results.charAt(results.length() - 1) != ' ') {
results.append(' ');
}
}
previousExpression = jpaQueryParsingToken.isExpression();
results.append(jpaQueryParsingToken.value());
}
return results.toString();
}
}
/**
* Builder for {@link QueryRenderer}.
*/
static class QueryRendererBuilder {
static class QueryRendererBuilder implements QueryTokenStream<QueryToken> {
protected QueryRenderer current = QueryRenderer.empty();
@@ -217,7 +289,7 @@ abstract class QueryRenderer {
* @param <T>
*/
public static <T> QueryRendererBuilder concat(Collection<T> elements, Function<T, QueryRendererBuilder> visitor,
JpaQueryParsingToken separator) {
QueryToken separator) {
return concat(elements, visitor, QueryRendererBuilder::toInline, separator);
}
@@ -232,7 +304,7 @@ abstract class QueryRenderer {
* @param <T>
*/
public static <T> QueryRendererBuilder concatExpressions(Collection<T> elements,
Function<T, QueryRendererBuilder> visitor, JpaQueryParsingToken separator) {
Function<T, QueryRendererBuilder> visitor, QueryToken separator) {
return concat(elements, visitor, QueryRendererBuilder::toExpression, separator);
}
@@ -248,7 +320,7 @@ abstract class QueryRenderer {
* @param <T>
*/
public static <T> QueryRendererBuilder concat(Collection<T> elements, Function<T, QueryRendererBuilder> visitor,
Function<QueryRendererBuilder, QueryRenderer> postProcess, JpaQueryParsingToken separator) {
Function<QueryRendererBuilder, QueryRenderer> postProcess, QueryToken separator) {
QueryRendererBuilder builder = new QueryRendererBuilder();
for (T element : elements) {
@@ -267,7 +339,7 @@ abstract class QueryRenderer {
* @param token
* @return
*/
public static QueryRendererBuilder from(JpaQueryParsingToken token) {
public static QueryRendererBuilder from(QueryToken token) {
return new QueryRendererBuilder().append(token);
}
@@ -277,7 +349,7 @@ abstract class QueryRenderer {
* @param token
* @return {@code this} builder.
*/
QueryRendererBuilder append(JpaQueryParsingToken token) {
QueryRendererBuilder append(QueryToken token) {
return append(List.of(token));
}
@@ -287,10 +359,14 @@ abstract class QueryRenderer {
* @param tokens
* @return {@code this} builder.
*/
QueryRendererBuilder append(Collection<JpaQueryParsingToken> tokens) {
QueryRendererBuilder append(Collection<? extends QueryToken> tokens) {
return append(QueryRenderer.from(tokens));
}
QueryRendererBuilder append(QueryTokenStream<? extends QueryToken> tokens) {
return append(QueryRenderer.from(tokens.toList()));
}
/**
* Append a QueryRendererBuilder.
*
@@ -378,7 +454,7 @@ abstract class QueryRenderer {
* @return
*/
public boolean isEmpty() {
return current instanceof EmptyQueryRenderer;
return current.isEmpty();
}
public QueryRenderer build() {
@@ -402,6 +478,26 @@ abstract class QueryRenderer {
public QueryRenderer toInline() {
return new InlineRenderer(current);
}
@Override
public List<QueryToken> toList() {
return current.toList();
}
@Override
public Stream<QueryToken> stream() {
return current.stream();
}
@Override
public int estimatedSize() {
return current.estimatedSize();
}
@Override
public Iterator<QueryToken> iterator() {
return current.iterator();
}
}
private static class InlineRenderer extends QueryRenderer {
@@ -418,9 +514,24 @@ abstract class QueryRenderer {
}
@Override
public Stream<JpaQueryParsingToken> stream() {
public Stream<QueryToken> stream() {
return delegate.stream();
}
@Override
public List<QueryToken> toList() {
return delegate.toList();
}
@Override
public Iterator<QueryToken> iterator() {
return delegate.iterator();
}
@Override
public int estimatedSize() {
return delegate.estimatedSize();
}
}
private static class ExpressionRenderer extends QueryRenderer {
@@ -442,9 +553,24 @@ abstract class QueryRenderer {
}
@Override
public Stream<JpaQueryParsingToken> stream() {
public Stream<QueryToken> stream() {
return delegate.stream();
}
@Override
public List<QueryToken> toList() {
return delegate.toList();
}
@Override
public Iterator<QueryToken> iterator() {
return delegate.iterator();
}
@Override
public int estimatedSize() {
return delegate.estimatedSize();
}
}
private static class EmptyQueryRenderer extends QueryRenderer {
@@ -460,5 +586,30 @@ abstract class QueryRenderer {
QueryRenderer append(QueryRenderer renderer) {
return renderer;
}
@Override
public boolean isEmpty() {
return true;
}
@Override
public List<QueryToken> toList() {
return Collections.emptyList();
}
@Override
public Stream<QueryToken> stream() {
return Stream.empty();
}
@Override
public Iterator<QueryToken> iterator() {
return Collections.emptyIterator();
}
@Override
public int estimatedSize() {
return 0;
}
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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;
/**
* @author Christoph Strobl
*/
public interface QueryToken {
String value();
default boolean isExpression() {
return false;
}
boolean isA(QueryToken queryToken);
}

View File

@@ -0,0 +1,38 @@
/*
* 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 org.springframework.data.util.Streamable;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
/**
* @author Christoph Strobl
*/
public interface QueryTokenStream<T extends QueryToken> extends Streamable<T> {
@Nullable
default T firstToken() {
return CollectionUtils.firstElement(toList());
}
@Nullable
default T lastToken() {
return CollectionUtils.lastElement(toList());
}
int estimatedSize();
}

View File

@@ -18,6 +18,7 @@ package org.springframework.data.jpa.repository.query;
import static org.springframework.data.jpa.repository.query.JpaQueryParsingToken.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
@@ -28,19 +29,14 @@ import java.util.List;
*/
class QueryTransformers {
/**
* Filter a token list from a {@code SELECT} clause to be used within a count query. That is, filter any {@code AS …}
* aliases.
*
* @param selection the input selection.
* @return filtered selection to be used with count queries.
*/
static List<JpaQueryParsingToken> filterCountSelection(List<JpaQueryParsingToken> selection) {
static CountSelectionTokenStream filterCountSelection(QueryTokenStream<QueryToken> selection) {
List<JpaQueryParsingToken> target = new ArrayList<>(selection.size());
List<QueryToken> target = new ArrayList<>(selection.estimatedSize());
boolean skipNext = false;
boolean containsNew = false;
for (QueryToken token : selection) {
for (JpaQueryParsingToken token : selection) {
if (skipNext) {
skipNext = false;
@@ -52,14 +48,48 @@ class QueryTransformers {
continue;
}
if (!token.isA(TOKEN_COMMA) && token instanceof JpaQueryExpression) {
token = JpaQueryParsingToken.token(token.getToken());
if (!token.isA(TOKEN_COMMA) && token.isExpression()) {
token = JpaQueryParsingToken.token(token.value());
}
if(!containsNew && token.value().contains("new")) {
containsNew = true;
}
target.add(token);
}
return target;
return new CountSelectionTokenStream(target, containsNew);
}
static class CountSelectionTokenStream implements QueryTokenStream<QueryToken> {
private final List<QueryToken> tokens;
private final boolean requiresPrimaryAlias;
public CountSelectionTokenStream(List<QueryToken> tokens, boolean containsNew) {
this.tokens = tokens;
this.requiresPrimaryAlias = containsNew;
}
@Override
public int estimatedSize() {
return tokens.size();
}
@Override
public Iterator<QueryToken> iterator() {
return tokens.iterator();
}
@Override
public List<QueryToken> toList() {
return tokens;
}
public boolean requiresPrimaryAlias() {
return requiresPrimaryAlias;
}
}
}

View File

@@ -21,6 +21,7 @@ import static org.springframework.data.jpa.repository.query.JpaQueryParsingToken
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.junit.jupiter.api.Test;
import org.springframework.data.jpa.repository.query.QueryRenderer.TokenRenderer;
/**
* Tests built around examples of EQL found in the EclipseLink's docs at
@@ -47,7 +48,7 @@ class EqlComplianceTests {
EqlParser.StartContext parsedQuery = parser.start();
return render(new EqlQueryRenderer().visit(parsedQuery));
return TokenRenderer.render(new EqlQueryRenderer().visit(parsedQuery));
}
private void assertQuery(String query) {

View File

@@ -28,6 +28,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.data.jpa.repository.query.QueryRenderer.TokenRenderer;
/**
* Tests built around examples of EQL found in the JPA spec
@@ -54,7 +55,7 @@ class EqlQueryRendererTests {
EqlParser.StartContext parsedQuery = parser.start();
return render(new EqlQueryRenderer().visit(parsedQuery));
return TokenRenderer.render(new EqlQueryRenderer().visit(parsedQuery));
}
static Stream<Arguments> reservedWords() {

View File

@@ -22,6 +22,7 @@ import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.data.jpa.repository.query.QueryRenderer.TokenRenderer;
/**
* Tests built around examples of EQL found in the JPA spec
@@ -44,7 +45,7 @@ class EqlSpecificationTests {
EqlParser.StartContext parsedQuery = parser.start();
return render(new EqlQueryRenderer().visit(parsedQuery));
return TokenRenderer.render(new EqlQueryRenderer().visit(parsedQuery));
}
private void assertQuery(String query) {

View File

@@ -22,6 +22,7 @@ import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.data.jpa.repository.query.QueryRenderer.TokenRenderer;
/**
* Tests built around examples of HQL found in
@@ -46,7 +47,7 @@ class HqlSpecificationTests {
HqlParser.StartContext parsedQuery = parser.start();
return render(new HqlQueryRenderer().visit(parsedQuery));
return TokenRenderer.render(new HqlQueryRenderer().visit(parsedQuery));
}
private void assertQuery(String query) {

View File

@@ -21,6 +21,7 @@ import static org.springframework.data.jpa.repository.query.JpaQueryParsingToken
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.junit.jupiter.api.Test;
import org.springframework.data.jpa.repository.query.QueryRenderer.TokenRenderer;
/**
* Test to verify compliance of {@link JpqlParser} with standard SQL. Other than {@link JpqlSpecificationTests} tests in
@@ -40,7 +41,7 @@ class JpqlComplianceTests {
JpqlParser.StartContext parsedQuery = parser.start();
return render(new JpqlQueryRenderer().visit(parsedQuery));
return TokenRenderer.render(new JpqlQueryRenderer().visit(parsedQuery));
}
private void assertQuery(String query) {

View File

@@ -28,6 +28,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.data.jpa.repository.query.QueryRenderer.TokenRenderer;
/**
* Tests built around examples of JPQL found in the JPA spec
@@ -55,7 +56,7 @@ class JpqlQueryRendererTests {
JpqlParser.StartContext parsedQuery = parser.start();
return render(new JpqlQueryRenderer().visit(parsedQuery));
return TokenRenderer.render(new JpqlQueryRenderer().visit(parsedQuery));
}
static Stream<Arguments> reservedWords() {

View File

@@ -22,6 +22,7 @@ import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.data.jpa.repository.query.QueryRenderer.TokenRenderer;
/**
* Tests built around examples of JPQL found in the JPA spec
@@ -48,7 +49,7 @@ class JpqlSpecificationTests {
JpqlParser.StartContext parsedQuery = parser.start();
return render(new JpqlQueryRenderer().visit(parsedQuery));
return TokenRenderer.render(new JpqlQueryRenderer().visit(parsedQuery));
}
private void assertQuery(String query) {