Skip to content

Commit 410152a

Browse files
christophstroblmp911de
authored andcommitted
Support NULLS {FIRST | LAST} in JPQL queries.
This commit adds support for parsing and appending order by items that define a NULL precedence. Closes #3529
1 parent a784a41 commit 410152a

File tree

7 files changed

+85
-1
lines changed

7 files changed

+85
-1
lines changed

spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4

+8-1
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,11 @@ orderby_clause
236236

237237
// TODO Error in spec BNF, correctly shown elsewhere in spec.
238238
orderby_item
239-
: (state_field_path_expression | general_identification_variable | result_variable ) (ASC | DESC)?
239+
: (state_field_path_expression | general_identification_variable | result_variable ) (ASC | DESC)? nullsPrecedence?
240+
;
241+
242+
nullsPrecedence
243+
: NULLS (FIRST | LAST)
240244
;
241245

242246
subquery
@@ -879,6 +883,7 @@ EXP : E X P;
879883
EXTRACT : E X T R A C T;
880884
FALSE : F A L S E;
881885
FETCH : F E T C H;
886+
FIRST : F I R S T;
882887
FLOOR : F L O O R;
883888
FROM : F R O M;
884889
FUNCTION : F U N C T I O N;
@@ -890,6 +895,7 @@ INNER : I N N E R;
890895
IS : I S;
891896
JOIN : J O I N;
892897
KEY : K E Y;
898+
LAST : L A S T;
893899
LEADING : L E A D I N G;
894900
LEFT : L E F T;
895901
LENGTH : L E N G T H;
@@ -906,6 +912,7 @@ NEW : N E W;
906912
NOT : N O T;
907913
NULL : N U L L;
908914
NULLIF : N U L L I F;
915+
NULLS : N U L L S;
909916
OBJECT : O B J E C T;
910917
OF : O F;
911918
ON : O N;

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

+7
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import org.springframework.dao.InvalidDataAccessApiUsageException;
1212
import org.springframework.data.domain.Sort;
13+
import org.springframework.data.domain.Sort.NullHandling;
1314
import org.springframework.data.jpa.domain.JpaSort;
1415
import org.springframework.lang.Nullable;
1516
import org.springframework.util.ObjectUtils;
@@ -76,6 +77,12 @@ List<QueryToken> orderBy(@Nullable String primaryFromAlias, Sort sort) {
7677

7778
builder.append(order.isDescending() ? TOKEN_DESC : TOKEN_ASC);
7879

80+
if(order.getNullHandling() == NullHandling.NULLS_FIRST) {
81+
builder.append(" NULLS FIRST");
82+
} else if (order.getNullHandling() == NullHandling.NULLS_LAST) {
83+
builder.append(" NULLS LAST");
84+
}
85+
7986
if (!tokens.isEmpty()) {
8087
tokens.add(TOKEN_COMMA);
8188
}

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

+11
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import org.antlr.v4.runtime.tree.ParseTree;
2424

25+
import org.springframework.data.jpa.repository.query.JpqlParser.NullsPrecedenceContext;
2526
import org.springframework.data.jpa.repository.query.JpqlParser.Reserved_wordContext;
2627
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
2728

@@ -795,9 +796,19 @@ public QueryTokenStream visitOrderby_item(JpqlParser.Orderby_itemContext ctx) {
795796
builder.append(QueryTokens.expression(ctx.DESC()));
796797
}
797798

799+
if(ctx.nullsPrecedence() != null) {
800+
builder.append(visit(ctx.nullsPrecedence()));
801+
}
802+
798803
return builder;
799804
}
800805

806+
@Override
807+
public QueryTokenStream visitNullsPrecedence(NullsPrecedenceContext ctx) {
808+
// return QueryTokenStream.concat(ctx.children, it-> QueryRendererBuilder.from(QueryTokens.token(it.getText())), TOKEN_SPACE);
809+
return QueryTokenStream.justAs(ctx.children, it-> QueryTokens.token(it.getText()));
810+
}
811+
801812
@Override
802813
public QueryTokenStream visitSubquery(JpqlParser.SubqueryContext ctx) {
803814

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

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.Iterator;
2020
import java.util.function.Function;
2121

22+
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
2223
import org.springframework.data.util.Streamable;
2324
import org.springframework.lang.Nullable;
2425
import org.springframework.util.CollectionUtils;
@@ -54,6 +55,10 @@ static <T> QueryTokenStream concat(Collection<T> elements, Function<T, QueryToke
5455
return concat(elements, visitor, QueryRenderer::inline, separator);
5556
}
5657

58+
static <T> QueryTokenStream justAs(Collection<T> elements, Function<T, QueryToken> converter) {
59+
return concat(elements, it-> QueryRendererBuilder.from(converter.apply(it)), QueryRenderer::inline, QueryTokens.TOKEN_SPACE);
60+
}
61+
5762
/**
5863
* Compose a {@link QueryTokenStream} from a collection of expression elements.
5964
*

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

+18
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.junit.jupiter.params.provider.MethodSource;
2727
import org.springframework.dao.InvalidDataAccessApiUsageException;
2828
import org.springframework.data.domain.Sort;
29+
import org.springframework.data.domain.Sort.Order;
2930
import org.springframework.data.jpa.domain.JpaSort;
3031
import org.springframework.lang.Nullable;
3132

@@ -71,6 +72,23 @@ void applyingSortShouldCreateAdditionalOrderByCriteria() {
7172
assertThat(results).contains("ORDER BY e.role, e.hire_date, e.first_name asc, e.last_name asc");
7273
}
7374

75+
@Test // GH-1280
76+
void nullFirstLastSorting() {
77+
78+
// given
79+
var original = "SELECT e FROM Employee e where e.name = :name ORDER BY e.first_name asc NULLS FIRST";
80+
81+
assertThat(createQueryFor(original, Sort.unsorted())).isEqualTo(original);
82+
83+
assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsLast())))
84+
.startsWith(original)
85+
.endsWithIgnoringCase("e.lastName DESC NULLS LAST");
86+
87+
assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsFirst())))
88+
.startsWith(original)
89+
.endsWithIgnoringCase("e.lastName DESC NULLS FIRST");
90+
}
91+
7492
@Test
7593
void applyCountToSimpleQuery() {
7694

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

+18
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.springframework.dao.InvalidDataAccessApiUsageException;
3131
import org.springframework.data.domain.PageRequest;
3232
import org.springframework.data.domain.Sort;
33+
import org.springframework.data.domain.Sort.Order;
3334
import org.springframework.data.jpa.domain.JpaSort;
3435
import org.springframework.lang.Nullable;
3536
import org.springframework.util.StringUtils;
@@ -77,6 +78,23 @@ void applyingSortShouldCreateAdditionalOrderByCriteria() {
7778
assertThat(results).contains("ORDER BY e.role, e.hire_date, e.first_name asc, e.last_name asc");
7879
}
7980

81+
@Test // GH-1280
82+
void nullFirstLastSorting() {
83+
84+
// given
85+
var original = "SELECT e FROM Employee e where e.name = :name ORDER BY e.first_name asc NULLS FIRST";
86+
87+
assertThat(createQueryFor(original, Sort.unsorted())).isEqualTo(original);
88+
89+
assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsLast())))
90+
.startsWith(original)
91+
.endsWithIgnoringCase("e.lastName DESC NULLS LAST");
92+
93+
assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsFirst())))
94+
.startsWith(original)
95+
.endsWithIgnoringCase("e.lastName DESC NULLS FIRST");
96+
}
97+
8098
@Test
8199
void applyCountToSimpleQuery() {
82100

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

+18
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.junit.jupiter.params.provider.MethodSource;
2727
import org.springframework.dao.InvalidDataAccessApiUsageException;
2828
import org.springframework.data.domain.Sort;
29+
import org.springframework.data.domain.Sort.Order;
2930
import org.springframework.data.jpa.domain.JpaSort;
3031
import org.springframework.lang.Nullable;
3132

@@ -72,6 +73,23 @@ void applyingSortShouldCreateAdditionalOrderByCriteria() {
7273
assertThat(results).contains("ORDER BY e.role, e.hire_date, e.first_name asc, e.last_name asc");
7374
}
7475

76+
@Test // GH-1280
77+
void nullFirstLastSorting() {
78+
79+
// given
80+
var original = "SELECT e FROM Employee e where e.name = :name ORDER BY e.first_name asc NULLS FIRST";
81+
82+
assertThat(createQueryFor(original, Sort.unsorted())).isEqualTo(original);
83+
84+
assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsLast())))
85+
.startsWith(original)
86+
.endsWithIgnoringCase("e.lastName DESC NULLS LAST");
87+
88+
assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsFirst())))
89+
.startsWith(original)
90+
.endsWithIgnoringCase("e.lastName DESC NULLS FIRST");
91+
}
92+
7593
@Test
7694
void applyCountToSimpleQuery() {
7795

0 commit comments

Comments
 (0)