Skip to content

Commit f474af7

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. Closes #3630 Original pull request: #3429 See #3427
1 parent 9b3e776 commit f474af7

File tree

5 files changed

+115
-21
lines changed

5 files changed

+115
-21
lines changed

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

+26-13
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.data.domain.Sort;
2525
import org.springframework.lang.Nullable;
2626
import org.springframework.util.Assert;
27+
import org.springframework.util.ObjectUtils;
2728

2829
/**
2930
* An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed EQL query.
@@ -105,27 +106,39 @@ public List<JpaQueryParsingToken> visitSelect_statement(EqlParser.Select_stateme
105106

106107
if (!countQuery) {
107108

108-
if (ctx.orderby_clause() != null) {
109-
tokens.addAll(visit(ctx.orderby_clause()));
109+
doVisitOrderBy(tokens, ctx, ObjectUtils.isEmpty(ctx.setOperator()) ? this.sort : Sort.unsorted());
110+
111+
for (int i = 0; i < ctx.setOperator().size(); i++) {
112+
113+
tokens.addAll(visit(ctx.setOperator(i)));
114+
tokens.addAll(visit(ctx.select_statement(i)));
110115
}
111116

112-
if (sort.isSorted()) {
117+
}
113118

114-
if (ctx.orderby_clause() != null) {
119+
return tokens;
120+
}
115121

116-
NOSPACE(tokens);
117-
tokens.add(TOKEN_COMMA);
118-
} else {
122+
private void doVisitOrderBy(List<JpaQueryParsingToken> tokens, EqlParser.Select_statementContext ctx, Sort sort) {
119123

120-
SPACE(tokens);
121-
tokens.add(TOKEN_ORDER_BY);
122-
}
124+
if (ctx.orderby_clause() != null) {
125+
tokens.addAll(visit(ctx.orderby_clause()));
126+
}
123127

124-
tokens.addAll(transformerSupport.generateOrderByArguments(primaryFromAlias, sort));
128+
if (sort.isSorted()) {
129+
130+
if (ctx.orderby_clause() != null) {
131+
132+
NOSPACE(tokens);
133+
tokens.add(TOKEN_COMMA);
134+
} else {
135+
136+
SPACE(tokens);
137+
tokens.add(TOKEN_ORDER_BY);
125138
}
126-
}
127139

128-
return tokens;
140+
tokens.addAll(transformerSupport.generateOrderByArguments(primaryFromAlias, sort));
141+
}
129142
}
130143

131144
@Override

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

+38-4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.springframework.data.jpa.repository.query.HqlParser.SelectionContext;
2727
import org.springframework.lang.Nullable;
2828
import org.springframework.util.Assert;
29+
import org.springframework.util.ObjectUtils;
2930

3031
/**
3132
* An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that transforms a parsed HQL query.
@@ -105,8 +106,7 @@ private static boolean isSubquery(ParserRuleContext ctx) {
105106
}
106107
}
107108

108-
@Override
109-
public List<JpaQueryParsingToken> visitOrderedQuery(HqlParser.OrderedQueryContext ctx) {
109+
private List<JpaQueryParsingToken> visitOrderedQuery(HqlParser.OrderedQueryContext ctx, Sort sort) {
110110

111111
List<JpaQueryParsingToken> tokens = newArrayList();
112112

@@ -190,6 +190,40 @@ public List<JpaQueryParsingToken> visitFromQuery(HqlParser.FromQueryContext ctx)
190190
return tokens;
191191
}
192192

193+
@Override
194+
public List<JpaQueryParsingToken> visitQueryExpression(HqlParser.QueryExpressionContext ctx) {
195+
196+
if (ObjectUtils.isEmpty(ctx.setOperator())) {
197+
return super.visitQueryExpression(ctx);
198+
}
199+
200+
List<JpaQueryParsingToken> builder = new ArrayList<>();
201+
if (ctx.withClause() != null) {
202+
builder.addAll(visit(ctx.withClause()));
203+
}
204+
205+
List<HqlParser.OrderedQueryContext> orderedQueries = ctx.orderedQuery();
206+
for (int i = 0; i < orderedQueries.size(); i++) {
207+
208+
if (i != 0) {
209+
builder.addAll(visit(ctx.setOperator(i - 1)));
210+
}
211+
212+
if (i == orderedQueries.size() - 1) {
213+
builder.addAll(visitOrderedQuery(ctx.orderedQuery(i), this.sort));
214+
} else {
215+
builder.addAll(visitOrderedQuery(ctx.orderedQuery(i), Sort.unsorted()));
216+
}
217+
}
218+
219+
return builder;
220+
}
221+
222+
@Override
223+
public List<JpaQueryParsingToken> visitOrderedQuery(HqlParser.OrderedQueryContext ctx) {
224+
return visitOrderedQuery(ctx, this.sort);
225+
}
226+
193227
@Override
194228
public List<JpaQueryParsingToken> visitQueryOrder(HqlParser.QueryOrderContext ctx) {
195229

@@ -325,7 +359,7 @@ public List<JpaQueryParsingToken> visitVariable(HqlParser.VariableContext ctx) {
325359

326360
List<JpaQueryParsingToken> tokens = super.visitVariable(ctx);
327361

328-
if (ctx.identifier() != null) {
362+
if (ctx.identifier() != null && !tokens.isEmpty() && !isSubquery(ctx)) {
329363
transformerSupport.registerAlias(tokens.get(tokens.size() - 1).getToken());
330364
}
331365

@@ -335,7 +369,7 @@ public List<JpaQueryParsingToken> visitVariable(HqlParser.VariableContext ctx) {
335369
@Override
336370
public List<JpaQueryParsingToken> visitSelection(SelectionContext ctx) {
337371

338-
if(!countQuery || isSubquery(ctx)) {
372+
if (!countQuery || isSubquery(ctx)) {
339373
return super.visitSelection(ctx);
340374
}
341375

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
/**
1818
* Transformational operations needed to support either {@link HqlQueryTransformer} or {@link JpqlQueryTransformer}.
19-
*
19+
*
2020
* @author Greg Turnquist
2121
* @author Donghun Shin
2222
* @since 3.1
@@ -47,12 +47,12 @@ void registerAlias(String token) {
4747
/**
4848
* Using the primary {@literal FROM} clause's alias and a {@link Sort}, construct all the {@literal ORDER BY}
4949
* arguments.
50-
*
50+
*
5151
* @param primaryFromAlias
5252
* @param sort
5353
* @return
5454
*/
55-
List<JpaQueryParsingToken> generateOrderByArguments(String primaryFromAlias, Sort sort) {
55+
List<JpaQueryParsingToken> generateOrderByArguments(@Nullable String primaryFromAlias, Sort sort) {
5656

5757
List<JpaQueryParsingToken> tokens = new ArrayList<>();
5858

@@ -98,7 +98,7 @@ private void checkSortExpression(Sort.Order order) {
9898
/**
9999
* Using the {@code primaryFromAlias} and the {@link org.springframework.data.domain.Sort.Order}, construct a suitable
100100
* argument to be added to an {@literal ORDER BY} expression.
101-
*
101+
*
102102
* @param primaryFromAlias
103103
* @param order
104104
* @return

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

+9
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,15 @@ void sortingRecognizesJoinAliases() {
753753
""");
754754
}
755755

756+
@Test // GH-3427
757+
void sortShouldBeAppendedToFullSelectOnlyInCaseOfSetOperator() {
758+
759+
String source = "SELECT tb FROM Test tb WHERE (tb.type='A') UNION SELECT tb FROM Test tb WHERE (tb.type='B')";
760+
String target = createQueryFor(source, Sort.by("Type").ascending());
761+
762+
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");
763+
}
764+
756765
static Stream<Arguments> queriesWithReservedWordsAsIdentifiers() {
757766

758767
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 {@link HqlQueryParser}.
@@ -1060,6 +1064,40 @@ void createsCountQueryUsingAliasCorrectly() {
10601064
assertCountQuery("select distinct a, count(b) as c from Employee GROUP BY n","select count(distinct a, count(b)) from Employee AS __ GROUP BY n");
10611065
}
10621066

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

0 commit comments

Comments
 (0)