Skip to content

Commit edf06a9

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. Need to improve handling of ORDER BY. See #2260.
1 parent 3e64d9a commit edf06a9

File tree

2 files changed

+113
-7
lines changed

2 files changed

+113
-7
lines changed

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

+37-7
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@
7474
* @author Andriy Redko
7575
* @author Peter Großmann
7676
* @author Greg Turnquist
77+
* @author Diego Krupitza
78+
* @author Jędrzej Biedrzycki
7779
*/
7880
public abstract class QueryUtils {
7981

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

109111
private static final String EQUALS_CONDITION_STRING = "%s.%s = :%s";
110-
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);
111114

112115
private static final Pattern NAMED_PARAMETER = Pattern.compile(COLON_NO_DOUBLE_COLON + IDENTIFIER + "|#" + IDENTIFIER,
113116
CASE_INSENSITIVE);
@@ -256,10 +259,10 @@ public static String applySorting(String query, Sort sort, @Nullable String alia
256259

257260
StringBuilder builder = new StringBuilder(query);
258261

259-
if (!ORDER_BY.matcher(query).matches()) {
260-
builder.append(" order by ");
261-
} else {
262+
if (hasOrderByClause(query)) {
262263
builder.append(", ");
264+
} else {
265+
builder.append(" order by ");
263266
}
264267

265268
Set<String> joinAliases = getOuterJoinAliases(query);
@@ -275,6 +278,31 @@ public static String applySorting(String query, Sort sort, @Nullable String alia
275278
return builder.toString();
276279
}
277280

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+
278306
/**
279307
* Returns the order clause for the given {@link Order}. Will prefix the clause with the given alias if the referenced
280308
* property refers to a join alias, i.e. starts with {@code $alias.}.
@@ -396,10 +424,12 @@ private static String toJpaDirection(Order order) {
396424
@Nullable
397425
@Deprecated
398426
public static String detectAlias(String query) {
399-
427+
String alias = null;
400428
Matcher matcher = ALIAS_MATCH.matcher(query);
401-
402-
return matcher.find() ? matcher.group(2) : null;
429+
while (matcher.find()) {
430+
alias = matcher.group(2);
431+
}
432+
return alias;
403433
}
404434

405435
/**

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

+76
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

@@ -554,4 +555,79 @@ void countProjectionDistinctQueryIncludesNewLineAfterEntityAndBeforeWhere() {
554555
private static void assertCountQuery(String originalQuery, String countQuery) {
555556
assertThat(createCountQueryFor(originalQuery)).isEqualTo(countQuery);
556557
}
558+
559+
@Test // GH-2260
560+
void applySortingAccountsForNativeWindowFunction() {
561+
562+
Sort sort = Sort.by(Order.desc("age"));
563+
564+
// order by absent
565+
assertThat(QueryUtils.applySorting("select * from user u", sort))
566+
.isEqualTo("select * from user u order by u.age desc");
567+
568+
// order by present
569+
assertThat(QueryUtils.applySorting("select * from user u order by u.lastname", sort))
570+
.isEqualTo("select * from user u order by u.lastname, u.age desc");
571+
572+
// partition by
573+
assertThat(QueryUtils.applySorting("select dense_rank() over (partition by age) from user u", sort))
574+
.isEqualTo("select dense_rank() over (partition by age) from user u order by u.age desc");
575+
576+
// order by in over clause
577+
assertThat(QueryUtils.applySorting("select dense_rank() over (order by lastname) from user u", sort))
578+
.isEqualTo("select dense_rank() over (order by lastname) from user u order by u.age desc");
579+
580+
// order by in over clause (additional spaces)
581+
assertThat(QueryUtils.applySorting("select dense_rank() over ( order by lastname ) from user u", sort))
582+
.isEqualTo("select dense_rank() over ( order by lastname ) from user u order by u.age desc");
583+
584+
// order by in over clause + at the end
585+
assertThat(
586+
QueryUtils.applySorting("select dense_rank() over (order by lastname) from user u order by u.lastname", sort))
587+
.isEqualTo("select dense_rank() over (order by lastname) from user u order by u.lastname, u.age desc");
588+
589+
// partition by + order by in over clause
590+
assertThat(QueryUtils.applySorting(
591+
"select dense_rank() over (partition by active, age order by lastname) from user u", sort)).isEqualTo(
592+
"select dense_rank() over (partition by active, age order by lastname) from user u order by u.age desc");
593+
594+
// partition by + order by in over clause + order by at the end
595+
assertThat(QueryUtils.applySorting(
596+
"select dense_rank() over (partition by active, age order by lastname) from user u order by active", sort))
597+
.isEqualTo(
598+
"select dense_rank() over (partition by active, age order by lastname) from user u order by active, u.age desc");
599+
600+
// partition by + order by in over clause + frame clause
601+
assertThat(QueryUtils.applySorting(
602+
"select dense_rank() over ( partition by active, age order by username rows between current row and unbounded following ) from user u",
603+
sort)).isEqualTo(
604+
"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");
605+
606+
// partition by + order by in over clause + frame clause + order by at the end
607+
assertThat(QueryUtils.applySorting(
608+
"select dense_rank() over ( partition by active, age order by username rows between current row and unbounded following ) from user u order by active",
609+
sort)).isEqualTo(
610+
"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");
611+
612+
// order by in subselect (select expression)
613+
assertThat(
614+
QueryUtils.applySorting("select lastname, (select i.id from item i order by i.id limit 1) from user u", sort))
615+
.isEqualTo(
616+
"select lastname, (select i.id from item i order by i.id limit 1) from user u order by u.age desc");
617+
618+
// order by in subselect (select expression) + at the end
619+
assertThat(QueryUtils.applySorting(
620+
"select lastname, (select i.id from item i order by 1 limit 1) from user u order by active", sort)).isEqualTo(
621+
"select lastname, (select i.id from item i order by 1 limit 1) from user u order by active, u.age desc");
622+
623+
// order by in subselect (from expression)
624+
assertThat(QueryUtils.applySorting("select * from (select * from user order by age desc limit 10) u", sort))
625+
.isEqualTo("select * from (select * from user order by age desc limit 10) u order by age desc");
626+
627+
// order by in subselect (from expression) + at the end
628+
assertThat(QueryUtils.applySorting(
629+
"select * from (select * from user order by 1, 2, 3 desc limit 10) u order by u.active asc", sort)).isEqualTo(
630+
"select * from (select * from user order by 1, 2, 3 desc limit 10) u order by u.active asc, age desc");
631+
}
632+
557633
}

0 commit comments

Comments
 (0)