Skip to content

Commit b02081b

Browse files
christophstroblmp911de
authored andcommitted
Fix order by rendering for queries containing UNION.
Make sure to append space after order by clause and fix alias detection for wrapped sub select. Also make sure to ignore alias used in subselect so they do not conflict with root ones. Render order by only on full select if set operator is present in EQL. Original pull request: #3429 Closes #3427
1 parent 8a95dba commit b02081b

File tree

5 files changed

+117
-39
lines changed

5 files changed

+117
-39
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlSortedQueryTransformer.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@
2323
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
2424
import org.springframework.lang.Nullable;
2525
import org.springframework.util.Assert;
26+
import org.springframework.util.ObjectUtils;
2627

2728
/**
2829
* An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed EQL query by applying
2930
* {@link Sort}.
3031
*
3132
* @author Greg Turnquist
3233
* @author Mark Paluch
34+
* @author Christoph Strobl
3335
* @since 3.2
3436
*/
3537
@SuppressWarnings("ConstantValue")
@@ -67,7 +69,7 @@ public QueryRendererBuilder visitSelect_statement(EqlParser.Select_statementCont
6769
builder.appendExpression(visit(ctx.having_clause()));
6870
}
6971

70-
doVisitOrderBy(builder, ctx);
72+
doVisitOrderBy(builder, ctx, ObjectUtils.isEmpty(ctx.setOperator()) ? this.sort : Sort.unsorted());
7173

7274
for (int i = 0; i < ctx.setOperator().size(); i++) {
7375

@@ -78,7 +80,7 @@ public QueryRendererBuilder visitSelect_statement(EqlParser.Select_statementCont
7880
return builder;
7981
}
8082

81-
private void doVisitOrderBy(QueryRendererBuilder builder, EqlParser.Select_statementContext ctx) {
83+
private void doVisitOrderBy(QueryRendererBuilder builder, EqlParser.Select_statementContext ctx, Sort sort) {
8284

8385
if (ctx.orderby_clause() != null) {
8486
QueryTokenStream existingOrder = visit(ctx.orderby_clause());

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlSortedQueryTransformer.java

+65-36
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
2424
import org.springframework.lang.Nullable;
2525
import org.springframework.util.Assert;
26+
import org.springframework.util.ObjectUtils;
2627

2728
/**
2829
* An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed HQL query.
@@ -46,8 +47,72 @@ class HqlSortedQueryTransformer extends HqlQueryRenderer {
4647
this.primaryFromAlias = primaryFromAlias;
4748
}
4849

50+
51+
public QueryTokenStream visitQueryExpression(HqlParser.QueryExpressionContext ctx) {
52+
53+
if(ObjectUtils.isEmpty(ctx.setOperator())) {
54+
return super.visitQueryExpression(ctx);
55+
}
56+
57+
QueryRendererBuilder builder = QueryRenderer.builder();
58+
if (ctx.withClause() != null) {
59+
builder.appendExpression(visit(ctx.withClause()));
60+
}
61+
62+
builder.append(visitOrderedQuery(ctx.orderedQuery(0), Sort.unsorted()));
63+
64+
for (int i = 1; i < ctx.orderedQuery().size(); i++) {
65+
66+
builder.append(visit(ctx.setOperator(i - 1)));
67+
builder.append(visit(ctx.orderedQuery(i)));
68+
}
69+
70+
71+
return builder;
72+
}
73+
4974
@Override
5075
public QueryRendererBuilder visitOrderedQuery(HqlParser.OrderedQueryContext ctx) {
76+
return visitOrderedQuery(ctx, this.sort);
77+
}
78+
79+
@Override
80+
public QueryTokenStream visitJoinPath(HqlParser.JoinPathContext ctx) {
81+
82+
QueryTokenStream tokens = super.visitJoinPath(ctx);
83+
84+
if (ctx.variable() != null && !isSubquery(ctx)) {
85+
transformerSupport.registerAlias(tokens.getLast());
86+
}
87+
88+
return tokens;
89+
}
90+
91+
@Override
92+
public QueryTokenStream visitJoinSubquery(HqlParser.JoinSubqueryContext ctx) {
93+
94+
QueryTokenStream tokens = super.visitJoinSubquery(ctx);
95+
96+
if (ctx.variable() != null && !tokens.isEmpty() && !isSubquery(ctx)) {
97+
transformerSupport.registerAlias(tokens.getLast());
98+
}
99+
100+
return tokens;
101+
}
102+
103+
@Override
104+
public QueryTokenStream visitVariable(HqlParser.VariableContext ctx) {
105+
106+
QueryTokenStream tokens = super.visitVariable(ctx);
107+
108+
if (ctx.identifier() != null && !tokens.isEmpty() && !isSubquery(ctx)) {
109+
transformerSupport.registerAlias(tokens.getLast());
110+
}
111+
112+
return tokens;
113+
}
114+
115+
private QueryRendererBuilder visitOrderedQuery(HqlParser.OrderedQueryContext ctx, Sort sort) {
51116

52117
QueryRendererBuilder builder = QueryRenderer.builder();
53118

@@ -95,40 +160,4 @@ public QueryRendererBuilder visitOrderedQuery(HqlParser.OrderedQueryContext ctx)
95160
return builder;
96161
}
97162

98-
@Override
99-
public QueryTokenStream visitJoinPath(HqlParser.JoinPathContext ctx) {
100-
101-
QueryTokenStream tokens = super.visitJoinPath(ctx);
102-
103-
if (ctx.variable() != null) {
104-
transformerSupport.registerAlias(tokens.getLast());
105-
}
106-
107-
return tokens;
108-
}
109-
110-
@Override
111-
public QueryTokenStream visitJoinSubquery(HqlParser.JoinSubqueryContext ctx) {
112-
113-
QueryTokenStream tokens = super.visitJoinSubquery(ctx);
114-
115-
if (ctx.variable() != null && !tokens.isEmpty()) {
116-
transformerSupport.registerAlias(tokens.getLast());
117-
}
118-
119-
return tokens;
120-
}
121-
122-
@Override
123-
public QueryTokenStream visitVariable(HqlParser.VariableContext ctx) {
124-
125-
QueryTokenStream tokens = super.visitVariable(ctx);
126-
127-
if (ctx.identifier() != null && !tokens.isEmpty()) {
128-
transformerSupport.registerAlias(tokens.getLast());
129-
}
130-
131-
return tokens;
132-
}
133-
134163
}

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryTransformerSupport.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ void registerAlias(QueryToken token) {
5353
* @param sort
5454
* @return
5555
*/
56-
List<QueryToken> orderBy(String primaryFromAlias, Sort sort) {
56+
List<QueryToken> orderBy(@Nullable String primaryFromAlias, Sort sort) {
5757

5858
List<QueryToken> tokens = new ArrayList<>();
5959

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java

+9
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,15 @@ void sortingRecognizesJoinAliases() {
759759
""");
760760
}
761761

762+
@Test // GH-3427
763+
void sortShouldBeAppendedToFullSelectOnlyInCaseOfSetOperator() {
764+
765+
String source = "SELECT tb FROM Test tb WHERE (tb.type='A') UNION SELECT tb FROM Test tb WHERE (tb.type='B')";
766+
String target = createQueryFor(source, Sort.by("Type").ascending());
767+
768+
assertThat(target).isEqualTo("SELECT tb FROM Test tb WHERE (tb.type = 'A') UNION SELECT tb FROM Test tb WHERE (tb.type = 'B') order by tb.Type asc");
769+
}
770+
762771
static Stream<Arguments> queriesWithReservedWordsAsIdentifiers() {
763772

764773
return Stream.of( //

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java

+38
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,22 @@
1717

1818
import static org.assertj.core.api.Assertions.*;
1919

20+
import java.util.regex.Matcher;
21+
import java.util.regex.Pattern;
2022
import java.util.stream.Stream;
2123

2224
import org.assertj.core.api.SoftAssertions;
2325
import org.junit.jupiter.api.Test;
2426
import org.junit.jupiter.params.ParameterizedTest;
2527
import org.junit.jupiter.params.provider.Arguments;
2628
import org.junit.jupiter.params.provider.MethodSource;
29+
import org.junit.jupiter.params.provider.ValueSource;
2730
import org.springframework.dao.InvalidDataAccessApiUsageException;
2831
import org.springframework.data.domain.PageRequest;
2932
import org.springframework.data.domain.Sort;
3033
import org.springframework.data.jpa.domain.JpaSort;
3134
import org.springframework.lang.Nullable;
35+
import org.springframework.util.StringUtils;
3236

3337
/**
3438
* Verify that HQL queries are properly transformed through the {@link JpaQueryEnhancer} and the
@@ -1061,6 +1065,40 @@ void createsCountQueryUsingAliasCorrectly() {
10611065
"select count(distinct a, count(b)) from Employee AS __ GROUP BY n");
10621066
}
10631067

1068+
@Test // GH-3427
1069+
void sortShouldBeAppendedWithSpacingInCaseOfSetOperator() {
1070+
1071+
String source = "SELECT tb FROM Test tb WHERE (tb.type='A') UNION SELECT tb FROM Test tb WHERE (tb.type='B')";
1072+
String target = createQueryFor(source, Sort.by("Type").ascending());
1073+
1074+
assertThat(target).isEqualTo("SELECT tb FROM Test tb WHERE (tb.type = 'A') UNION SELECT tb FROM Test tb WHERE (tb.type = 'B') order by tb.Type asc");
1075+
}
1076+
1077+
@ParameterizedTest // GH-3427
1078+
@ValueSource(strings = {"", "res"})
1079+
void sortShouldBeAppendedToSubSelectWithSetOperatorInSubselect(String alias) {
1080+
1081+
String prefix = StringUtils.hasText(alias) ? (alias + ".") : "";
1082+
String source = "SELECT %sname FROM (SELECT c.name as name FROM Category c UNION SELECT t.name as name FROM Tag t)".formatted(prefix);
1083+
if(StringUtils.hasText(alias)) {
1084+
source = source + " %s".formatted(alias);
1085+
}
1086+
1087+
String target = createQueryFor(source, Sort.by("name").ascending());
1088+
1089+
assertThat(target).contains(" UNION SELECT ").doesNotContainPattern(Pattern.compile(".*\\SUNION"));
1090+
assertThat(target).endsWith("order by %sname asc".formatted(prefix)).satisfies(it -> {
1091+
Pattern pattern = Pattern.compile("order by %sname".formatted(prefix));
1092+
Matcher matcher = pattern.matcher(target);
1093+
int count = 0;
1094+
while(matcher.find()) {
1095+
count++;
1096+
}
1097+
assertThat(count).describedAs("Found order by clause more than once in: \n%s", it).isOne();
1098+
});
1099+
1100+
}
1101+
10641102
private void assertCountQuery(String originalQuery, String countQuery) {
10651103
assertThat(createCountQueryFor(originalQuery)).isEqualTo(countQuery);
10661104
}

0 commit comments

Comments
 (0)