Skip to content

Commit 5103be3

Browse files
darinmanicagregturn
authored andcommitted
Correctly handle order by aliases.
In order to correctly identify aliases in the order by clause, we cannot process the from clauses left-to-right using regular expressions. These must be removed inner-to-outer. Commit c93aa25 resulted in a bug where the subquery would be incorrectly identified as the alias, as by the following query: ``` from Parent p join p.children c where not c.id not in (select c2.id from Child c2) ``` Passing in a Sort.by("name") would result in "order by c2.name" instead of "order by p.name". Thus, it was using the alias of the inner query instead of the outer query. [This comment](#2260 (comment)) suggests removing the content of the inner query, with the caveat of the entire query being surrounded by parenthesis. This commit does exactly that, by removing the subquery before the alias is identified. It also handles the case when the entire query is surrounded by parenthesis. Unit tests illustrate this along with several examples of removing the subquery to correctly identify the alias for the order by clause. See #2260 (c93aa25), #2500, #2518.
1 parent 1f2e4a1 commit 5103be3

File tree

2 files changed

+108
-2
lines changed

2 files changed

+108
-2
lines changed

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

+61-1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
* @author Greg Turnquist
7777
* @author Diego Krupitza
7878
* @author Jędrzej Biedrzycki
79+
* @author Darin Manica
7980
*/
8081
public abstract class QueryUtils {
8182

@@ -100,6 +101,8 @@ public abstract class QueryUtils {
100101

101102
private static final Pattern ALIAS_MATCH;
102103
private static final Pattern COUNT_MATCH;
104+
private static final Pattern STARTS_WITH_PAREN = Pattern.compile("^\\s*\\(");
105+
private static final Pattern PARENS_TO_REMOVE = Pattern.compile("(\\(.*\\bfrom\\b[^)]+\\))", CASE_INSENSITIVE);
103106
private static final Pattern PROJECTION_CLAUSE = Pattern.compile("select\\s+(?:distinct\\s+)?(.+)\\s+from",
104107
Pattern.CASE_INSENSITIVE);
105108

@@ -431,13 +434,70 @@ private static String toJpaDirection(Order order) {
431434
@Deprecated
432435
public static String detectAlias(String query) {
433436
String alias = null;
434-
Matcher matcher = ALIAS_MATCH.matcher(query);
437+
Matcher matcher = ALIAS_MATCH.matcher(removeSubqueries(query));
435438
while (matcher.find()) {
436439
alias = matcher.group(2);
437440
}
438441
return alias;
439442
}
440443

444+
/**
445+
* Remove subqueries from the query, in order to identify the correct alias
446+
* in order by clauses. If the entire query is surrounded by parenthesis, the
447+
* outermost parenthesis are not removed.
448+
*
449+
* @param query
450+
* @return query with all subqueries removed.
451+
*/
452+
static String removeSubqueries(String query) {
453+
if (!StringUtils.hasText(query)) {
454+
return query;
455+
}
456+
457+
final List<Integer> opens = new ArrayList<>();
458+
final List<Integer> closes = new ArrayList<>();
459+
final List<Boolean> closeMatches = new ArrayList<>();
460+
for (int i=0; i<query.length(); i++) {
461+
final char c = query.charAt(i);
462+
if (c == '(') {
463+
opens.add(i);
464+
} else if (c == ')') {
465+
closes.add(i);
466+
closeMatches.add(Boolean.FALSE);
467+
}
468+
}
469+
470+
final StringBuilder sb = new StringBuilder(query);
471+
final boolean startsWithParen = STARTS_WITH_PAREN.matcher(query).find();
472+
for (int i=opens.size()-1; i>=(startsWithParen?1:0); i--) {
473+
final Integer open = opens.get(i);
474+
final Integer close = findClose(open, closes, closeMatches) + 1;
475+
476+
477+
if (close > open) {
478+
final String subquery = sb.substring(open, close);
479+
final Matcher matcher = PARENS_TO_REMOVE.matcher(subquery);
480+
if (matcher.find()) {
481+
sb.replace(open, close, new String(new char[close-open]).replace('\0', ' '));
482+
}
483+
}
484+
}
485+
486+
return sb.toString();
487+
}
488+
489+
private static Integer findClose(final Integer open, final List<Integer> closes, final List<Boolean> closeMatches) {
490+
for (int i=0; i<closes.size(); i++) {
491+
final int close = closes.get(i);
492+
if (close > open && !closeMatches.get(i)) {
493+
closeMatches.set(i, Boolean.TRUE);
494+
return close;
495+
}
496+
}
497+
498+
return -1;
499+
}
500+
441501
/**
442502
* Creates a where-clause referencing the given entities and appends it to the given query string. Binds the given
443503
* entities to the query.

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

+47-1
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@
2020

2121
import java.util.Collections;
2222
import java.util.Set;
23+
import java.util.regex.Matcher;
24+
import java.util.regex.Pattern;
2325

2426
import org.assertj.core.api.SoftAssertions;
2527
import org.junit.jupiter.api.Test;
2628
import org.springframework.dao.InvalidDataAccessApiUsageException;
2729
import org.springframework.data.domain.Sort;
2830
import org.springframework.data.domain.Sort.Order;
2931
import org.springframework.data.jpa.domain.JpaSort;
32+
import org.springframework.util.StringUtils;
3033

3134
/**
3235
* Unit test for {@link QueryUtils}.
@@ -41,6 +44,7 @@
4144
* @author Mohammad Hewedy
4245
* @author Greg Turnquist
4346
* @author Jędrzej Biedrzycki
47+
* @author Darin Manica
4448
*/
4549
class QueryUtilsUnitTests {
4650

@@ -50,6 +54,7 @@ class QueryUtilsUnitTests {
5054
private static final String COUNT_QUERY = "select count(u) from User u";
5155

5256
private static final String QUERY_WITH_AS = "select u from User as u where u.username = ?";
57+
private static final Pattern MULTI_WHITESPACE = Pattern.compile("\\s+");
5358

5459
@Test
5560
void createsCountQueryCorrectly() {
@@ -102,7 +107,7 @@ void allowsShortJpaSyntax() {
102107
assertCountQuery(SIMPLE_QUERY, COUNT_QUERY);
103108
}
104109

105-
@Test
110+
@Test // GH-2260
106111
void detectsAliasCorrectly() {
107112

108113
assertThat(detectAlias(QUERY)).isEqualTo("u");
@@ -113,6 +118,47 @@ void detectsAliasCorrectly() {
113118
assertThat(detectAlias("select u from User u")).isEqualTo("u");
114119
assertThat(detectAlias("select u from com.acme.User u")).isEqualTo("u");
115120
assertThat(detectAlias("select u from T05User u")).isEqualTo("u");
121+
assertThat(detectAlias("select u from User u where not exists (from User u2)")).isEqualTo("u");
122+
assertThat(detectAlias("(select u from User u where not exists (from User u2))")).isEqualTo("u");
123+
assertThat(detectAlias("(select u from User u where not exists ((from User u2 where not exists (from User u3))))")).isEqualTo("u");
124+
assertThat(detectAlias("from Foo f left join f.bar b with type(b) = BarChild where (f.id = (select max(f.id) from Foo f2 where type(f2) = FooChild) or 1 <> 1) and 1=1")).isEqualTo("f");
125+
assertThat(detectAlias("(from Foo f max(f) ((((select * from Foo f2 (from Foo f3) max(*)) (from Foo f4)) max(f5)) (f6)) (from Foo f7))")).isEqualTo("f");
126+
}
127+
128+
@Test // GH-2260
129+
void testRemoveSubqueries() throws Exception {
130+
// boundary conditions
131+
assertThat(removeSubqueries(null)).isNull();
132+
assertThat(removeSubqueries("")).isEmpty();
133+
assertThat(removeSubqueries(" ")).isEqualTo(" ");
134+
assertThat(removeSubqueries("(")).isEqualTo("(");
135+
assertThat(removeSubqueries(")")).isEqualTo(")");
136+
assertThat(removeSubqueries("(()")).isEqualTo("(()");
137+
assertThat(removeSubqueries("())")).isEqualTo("())");
138+
139+
// realistic conditions
140+
assertThat(removeSubqueries(QUERY)).isEqualTo(QUERY);
141+
assertThat(removeSubqueries(SIMPLE_QUERY)).isEqualTo(SIMPLE_QUERY);
142+
assertThat(removeSubqueries(COUNT_QUERY)).isEqualTo(COUNT_QUERY);
143+
assertThat(removeSubqueries(QUERY_WITH_AS)).isEqualTo(QUERY_WITH_AS);
144+
assertThat(removeSubqueries("SELECT FROM USER U")).isEqualTo("SELECT FROM USER U");
145+
assertThat(removeSubqueries("select u from User u")).isEqualTo("select u from User u");
146+
assertThat(removeSubqueries("select u from com.acme.User u")).isEqualTo("select u from com.acme.User u");
147+
assertThat(removeSubqueries("select u from T05User u")).isEqualTo("select u from T05User u");
148+
assertThat(normalizeWhitespace(removeSubqueries("select u from User u where not exists (from User u2)"))).isEqualTo("select u from User u where not exists");
149+
assertThat(normalizeWhitespace(removeSubqueries("(select u from User u where not exists (from User u2))"))).isEqualTo("(select u from User u where not exists )");
150+
assertThat(normalizeWhitespace(removeSubqueries("select u from User u where not exists (from User u2 where not exists (from User u3))"))).isEqualTo("select u from User u where not exists");
151+
assertThat(normalizeWhitespace(removeSubqueries("select u from User u where not exists ((from User u2 where not exists (from User u3)))"))).isEqualTo("select u from User u where not exists ( )");
152+
assertThat(normalizeWhitespace(removeSubqueries("(select u from User u where not exists ((from User u2 where not exists (from User u3))))"))).isEqualTo("(select u from User u where not exists ( ))");
153+
}
154+
155+
private String normalizeWhitespace(String s) {
156+
Matcher matcher = MULTI_WHITESPACE.matcher(s);
157+
if (matcher.find()) {
158+
return matcher.replaceAll(" ").trim();
159+
}
160+
161+
return StringUtils.trimWhitespace(s);
116162
}
117163

118164
@Test

0 commit comments

Comments
 (0)