Skip to content

Commit 8999194

Browse files
TAndronicusgregturn
authored andcommitted
Properly handle nested ORDER BY clauses.
In several ways, it's possible to have various ORDER BY clauses, including SQL OVER (windowing) clauses. See #2260.
1 parent 2545e6d commit 8999194

File tree

2 files changed

+86
-7
lines changed

2 files changed

+86
-7
lines changed

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

+36-7
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
* @author Peter Großmann
7676
* @author Greg Turnquist
7777
* @author Diego Krupitza
78+
* @author Jędrzej Biedrzycki
7879
*/
7980
public abstract class QueryUtils {
8081

@@ -108,7 +109,8 @@ public abstract class QueryUtils {
108109
private static final Pattern JOIN_PATTERN = Pattern.compile(JOIN, Pattern.CASE_INSENSITIVE);
109110

110111
private static final String EQUALS_CONDITION_STRING = "%s.%s = :%s";
111-
private static final Pattern ORDER_BY = Pattern.compile(".*order\\s+by\\s+.*", CASE_INSENSITIVE);
112+
private static final Pattern ORDER_BY = Pattern.compile("(order\\s+by\\s+)", CASE_INSENSITIVE);
113+
private static final Pattern ORDER_BY_IN_WINDOW_OR_SUBSELECT = Pattern.compile("(\\(\\s*[a-z0-9 ,.*]*order\\s+by\\s+[a-z0-9 ,.]*\\s*\\))", CASE_INSENSITIVE);
112114

113115
private static final Pattern NAMED_PARAMETER = Pattern.compile(COLON_NO_DOUBLE_COLON + IDENTIFIER + "|#" + IDENTIFIER,
114116
CASE_INSENSITIVE);
@@ -257,10 +259,10 @@ public static String applySorting(String query, Sort sort, @Nullable String alia
257259

258260
StringBuilder builder = new StringBuilder(query);
259261

260-
if (!ORDER_BY.matcher(query).matches()) {
261-
builder.append(" order by ");
262-
} else {
262+
if (hasOrderByClause(query)) {
263263
builder.append(", ");
264+
} else {
265+
builder.append(" order by ");
264266
}
265267

266268
Set<String> joinAliases = getOuterJoinAliases(query);
@@ -276,6 +278,31 @@ public static String applySorting(String query, Sort sort, @Nullable String alia
276278
return builder.toString();
277279
}
278280

281+
/**
282+
* Returns {@code true} if the query has {@code order by} clause.
283+
* The query has {@code order by} clause if there is an {@code order by} which is not part of window clause.
284+
*
285+
* @param query the analysed query string
286+
* @return {@code true} if the query has {@code order by} clause, {@code false} otherwise
287+
*/
288+
private static boolean hasOrderByClause(String query) {
289+
return countOccurences(ORDER_BY, query) > countOccurences(ORDER_BY_IN_WINDOW_OR_SUBSELECT, query);
290+
}
291+
292+
/**
293+
* Counts the number of occurrences of the pattern in the string
294+
*
295+
* @param pattern regex with a group to match
296+
* @param string analysed string
297+
* @return the number of occurences of the pattern in the string
298+
*/
299+
private static int countOccurences(Pattern pattern, String string) {
300+
Matcher matcher = pattern.matcher(string);
301+
int occurences = 0;
302+
while (matcher.find()) occurences++;
303+
return occurences;
304+
}
305+
279306
/**
280307
* Returns the order clause for the given {@link Order}. Will prefix the clause with the given alias if the referenced
281308
* property refers to a join alias, i.e. starts with {@code $alias.}.
@@ -397,10 +424,12 @@ private static String toJpaDirection(Order order) {
397424
@Nullable
398425
@Deprecated
399426
public static String detectAlias(String query) {
400-
427+
String alias = null;
401428
Matcher matcher = ALIAS_MATCH.matcher(query);
402-
403-
return matcher.find() ? matcher.group(2) : null;
429+
while (matcher.find()) {
430+
alias = matcher.group(2);
431+
}
432+
return alias;
404433
}
405434

406435
/**

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

+50
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
* @author Grégoire Druant
4141
* @author Mohammad Hewedy
4242
* @author Greg Turnquist
43+
* @author Jędrzej Biedrzycki
4344
*/
4445
class QueryUtilsUnitTests {
4546

@@ -552,4 +553,53 @@ void countProjectionDistinctQueryIncludesNewLineAfterEntityAndBeforeWhere() {
552553
private static void assertCountQuery(String originalQuery, String countQuery) {
553554
assertThat(createCountQueryFor(originalQuery)).isEqualTo(countQuery);
554555
}
556+
557+
@Test
558+
void applySortingAccountsForNativeWindowFunction() {
559+
Sort sort = Sort.by(Order.desc("age"));
560+
561+
// order by absent
562+
assertThat(QueryUtils.applySorting("select * from user u", sort))
563+
.isEqualTo("select * from user u order by u.age desc");
564+
// order by present
565+
assertThat(QueryUtils.applySorting("select * from user u order by u.lastname", sort))
566+
.isEqualTo("select * from user u order by u.lastname, u.age desc");
567+
// partition by
568+
assertThat(QueryUtils.applySorting("select dense_rank() over (partition by age) from user u", sort))
569+
.isEqualTo("select dense_rank() over (partition by age) from user u order by u.age desc");
570+
// order by in over clause
571+
assertThat(QueryUtils.applySorting("select dense_rank() over (order by lastname) from user u", sort))
572+
.isEqualTo("select dense_rank() over (order by lastname) from user u order by u.age desc");
573+
// order by in over clause (additional spaces)
574+
assertThat(QueryUtils.applySorting("select dense_rank() over ( order by lastname ) from user u", sort))
575+
.isEqualTo("select dense_rank() over ( order by lastname ) from user u order by u.age desc");
576+
// order by in over clause + at the end
577+
assertThat(QueryUtils.applySorting("select dense_rank() over (order by lastname) from user u order by u.lastname", sort))
578+
.isEqualTo("select dense_rank() over (order by lastname) from user u order by u.lastname, u.age desc");
579+
// partition by + order by in over clause
580+
assertThat(QueryUtils.applySorting("select dense_rank() over (partition by active, age order by lastname) from user u", sort))
581+
.isEqualTo("select dense_rank() over (partition by active, age order by lastname) from user u order by u.age desc");
582+
// partition by + order by in over clause + order by at the end
583+
assertThat(QueryUtils.applySorting("select dense_rank() over (partition by active, age order by lastname) from user u order by active", sort))
584+
.isEqualTo("select dense_rank() over (partition by active, age order by lastname) from user u order by active, u.age desc");
585+
// partition by + order by in over clause + frame clause
586+
assertThat(QueryUtils.applySorting("select dense_rank() over ( partition by active, age order by username rows between current row and unbounded following ) from user u", sort))
587+
.isEqualTo("select dense_rank() over ( partition by active, age order by username rows between current row and unbounded following ) from user u order by u.age desc");
588+
// partition by + order by in over clause + frame clause + order by at the end
589+
assertThat(QueryUtils.applySorting("select dense_rank() over ( partition by active, age order by username rows between current row and unbounded following ) from user u order by active", sort))
590+
.isEqualTo("select dense_rank() over ( partition by active, age order by username rows between current row and unbounded following ) from user u order by active, u.age desc");
591+
// order by in subselect (select expression)
592+
assertThat(QueryUtils.applySorting("select lastname, (select i.id from item i order by i.id limit 1) from user u", sort))
593+
.isEqualTo("select lastname, (select i.id from item i order by i.id limit 1) from user u order by u.age desc");
594+
// order by in subselect (select expression) + at the end
595+
assertThat(QueryUtils.applySorting("select lastname, (select i.id from item i order by 1 limit 1) from user u order by active", sort))
596+
.isEqualTo("select lastname, (select i.id from item i order by 1 limit 1) from user u order by active, u.age desc");
597+
// order by in subselect (from expression)
598+
assertThat(QueryUtils.applySorting("select * from (select * from user order by age desc limit 10) u", sort))
599+
.isEqualTo("select * from (select * from user order by age desc limit 10) u order by age desc");
600+
// order by in subselect (from expression) + at the end
601+
assertThat(QueryUtils.applySorting("select * from (select * from user order by 1, 2, 3 desc limit 10) u order by u.active asc", sort))
602+
.isEqualTo("select * from (select * from user order by 1, 2, 3 desc limit 10) u order by u.active asc, age desc");
603+
}
604+
555605
}

0 commit comments

Comments
 (0)