diff --git a/pom.xml b/pom.xml
index 617cc011d3..8ec9c6db8e 100755
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-jpa-parent
- 3.4.0-SNAPSHOT
+ 3.4.x-GH-1280-SNAPSHOT
pom
Spring Data JPA Parent
diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml
index 8b836ae2f3..aa3483e6e1 100755
--- a/spring-data-envers/pom.xml
+++ b/spring-data-envers/pom.xml
@@ -5,12 +5,12 @@
org.springframework.data
spring-data-envers
- 3.4.0-SNAPSHOT
+ 3.4.x-GH-1280-SNAPSHOT
org.springframework.data
spring-data-jpa-parent
- 3.4.0-SNAPSHOT
+ 3.4.x-GH-1280-SNAPSHOT
../pom.xml
diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml
index a90c1f7282..ac5fbd4ddd 100644
--- a/spring-data-jpa-distribution/pom.xml
+++ b/spring-data-jpa-distribution/pom.xml
@@ -14,7 +14,7 @@
org.springframework.data
spring-data-jpa-parent
- 3.4.0-SNAPSHOT
+ 3.4.x-GH-1280-SNAPSHOT
../pom.xml
diff --git a/spring-data-jpa-performance/pom.xml b/spring-data-jpa-performance/pom.xml
index 38f130983e..a405ee7cc5 100644
--- a/spring-data-jpa-performance/pom.xml
+++ b/spring-data-jpa-performance/pom.xml
@@ -15,7 +15,7 @@
org.springframework.data
spring-data-jpa-parent
- 3.4.0-SNAPSHOT
+ 3.4.x-GH-1280-SNAPSHOT
../pom.xml
diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml
index 3006702796..eeebb6f825 100644
--- a/spring-data-jpa/pom.xml
+++ b/spring-data-jpa/pom.xml
@@ -6,7 +6,7 @@
org.springframework.data
spring-data-jpa
- 3.4.0-SNAPSHOT
+ 3.4.x-GH-1280-SNAPSHOT
Spring Data JPA
Spring Data module for JPA repositories.
@@ -15,7 +15,7 @@
org.springframework.data
spring-data-jpa-parent
- 3.4.0-SNAPSHOT
+ 3.4.x-GH-1280-SNAPSHOT
../pom.xml
diff --git a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4 b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4
index a7f319b793..e87d523b12 100644
--- a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4
+++ b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4
@@ -236,7 +236,11 @@ orderby_clause
// TODO Error in spec BNF, correctly shown elsewhere in spec.
orderby_item
- : (state_field_path_expression | general_identification_variable | result_variable ) (ASC | DESC)?
+ : (state_field_path_expression | general_identification_variable | result_variable ) (ASC | DESC)? nullsPrecedence?
+ ;
+
+nullsPrecedence
+ : NULLS (FIRST | LAST)
;
subquery
@@ -879,6 +883,7 @@ EXP : E X P;
EXTRACT : E X T R A C T;
FALSE : F A L S E;
FETCH : F E T C H;
+FIRST : F I R S T;
FLOOR : F L O O R;
FROM : F R O M;
FUNCTION : F U N C T I O N;
@@ -890,6 +895,7 @@ INNER : I N N E R;
IS : I S;
JOIN : J O I N;
KEY : K E Y;
+LAST : L A S T;
LEADING : L E A D I N G;
LEFT : L E F T;
LENGTH : L E N G T H;
@@ -906,6 +912,7 @@ NEW : N E W;
NOT : N O T;
NULL : N U L L;
NULLIF : N U L L I F;
+NULLS : N U L L S;
OBJECT : O B J E C T;
OF : O F;
ON : O N;
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryTransformerSupport.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryTransformerSupport.java
index c5e4cacba4..8e47f30df6 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryTransformerSupport.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryTransformerSupport.java
@@ -10,6 +10,7 @@
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Sort.NullHandling;
import org.springframework.data.jpa.domain.JpaSort;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
@@ -76,6 +77,12 @@ List orderBy(String primaryFromAlias, Sort sort) {
builder.append(order.isDescending() ? TOKEN_DESC : TOKEN_ASC);
+ if(order.getNullHandling() == NullHandling.NULLS_FIRST) {
+ builder.append(" NULLS FIRST");
+ } else if (order.getNullHandling() == NullHandling.NULLS_LAST) {
+ builder.append(" NULLS LAST");
+ }
+
if (!tokens.isEmpty()) {
tokens.add(TOKEN_COMMA);
}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java
index fe8fac1bfa..de53a63600 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java
@@ -22,6 +22,7 @@
import org.antlr.v4.runtime.tree.ParseTree;
+import org.springframework.data.jpa.repository.query.JpqlParser.NullsPrecedenceContext;
import org.springframework.data.jpa.repository.query.JpqlParser.Reserved_wordContext;
import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
@@ -795,9 +796,19 @@ public QueryTokenStream visitOrderby_item(JpqlParser.Orderby_itemContext ctx) {
builder.append(QueryTokens.expression(ctx.DESC()));
}
+ if(ctx.nullsPrecedence() != null) {
+ builder.append(visit(ctx.nullsPrecedence()));
+ }
+
return builder;
}
+ @Override
+ public QueryTokenStream visitNullsPrecedence(NullsPrecedenceContext ctx) {
+// return QueryTokenStream.concat(ctx.children, it-> QueryRendererBuilder.from(QueryTokens.token(it.getText())), TOKEN_SPACE);
+ return QueryTokenStream.justAs(ctx.children, it-> QueryTokens.token(it.getText()));
+ }
+
@Override
public QueryTokenStream visitSubquery(JpqlParser.SubqueryContext ctx) {
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokenStream.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokenStream.java
index 4c554ed571..233711d797 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokenStream.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryTokenStream.java
@@ -19,6 +19,7 @@
import java.util.Iterator;
import java.util.function.Function;
+import org.springframework.data.jpa.repository.query.QueryRenderer.QueryRendererBuilder;
import org.springframework.data.util.Streamable;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
@@ -54,6 +55,10 @@ static QueryTokenStream concat(Collection elements, Function QueryTokenStream justAs(Collection elements, Function converter) {
+ return concat(elements, it-> QueryRendererBuilder.from(converter.apply(it)), QueryRenderer::inline, QueryTokens.TOKEN_SPACE);
+ }
+
/**
* Compose a {@link QueryTokenStream} from a collection of expression elements.
*
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java
index 52bdcd82d1..82f4274998 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryTransformerTests.java
@@ -26,6 +26,7 @@
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Sort.Order;
import org.springframework.data.jpa.domain.JpaSort;
import org.springframework.lang.Nullable;
@@ -71,6 +72,23 @@ void applyingSortShouldCreateAdditionalOrderByCriteria() {
assertThat(results).contains("ORDER BY e.role, e.hire_date, e.first_name asc, e.last_name asc");
}
+ @Test // GH-1280
+ void nullFirstLastSorting() {
+
+ // given
+ var original = "SELECT e FROM Employee e where e.name = :name ORDER BY e.first_name asc NULLS FIRST";
+
+ assertThat(createQueryFor(original, Sort.unsorted())).isEqualTo(original);
+
+ assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsLast())))
+ .startsWith(original)
+ .endsWithIgnoringCase("e.lastName DESC NULLS LAST");
+
+ assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsFirst())))
+ .startsWith(original)
+ .endsWithIgnoringCase("e.lastName DESC NULLS FIRST");
+ }
+
@Test
void applyCountToSimpleQuery() {
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java
index b4ce21db0a..fe3f0e0463 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java
@@ -27,6 +27,7 @@
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Sort.Order;
import org.springframework.data.jpa.domain.JpaSort;
import org.springframework.lang.Nullable;
@@ -73,6 +74,23 @@ void applyingSortShouldCreateAdditionalOrderByCriteria() {
assertThat(results).contains("ORDER BY e.role, e.hire_date, e.first_name asc, e.last_name asc");
}
+ @Test // GH-1280
+ void nullFirstLastSorting() {
+
+ // given
+ var original = "SELECT e FROM Employee e where e.name = :name ORDER BY e.first_name asc NULLS FIRST";
+
+ assertThat(createQueryFor(original, Sort.unsorted())).isEqualTo(original);
+
+ assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsLast())))
+ .startsWith(original)
+ .endsWithIgnoringCase("e.lastName DESC NULLS LAST");
+
+ assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsFirst())))
+ .startsWith(original)
+ .endsWithIgnoringCase("e.lastName DESC NULLS FIRST");
+ }
+
@Test
void applyCountToSimpleQuery() {
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java
index eb04b8de07..4b9a9e6dc6 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryTransformerTests.java
@@ -26,6 +26,7 @@
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Sort.Order;
import org.springframework.data.jpa.domain.JpaSort;
import org.springframework.lang.Nullable;
@@ -72,6 +73,23 @@ void applyingSortShouldCreateAdditionalOrderByCriteria() {
assertThat(results).contains("ORDER BY e.role, e.hire_date, e.first_name asc, e.last_name asc");
}
+ @Test // GH-1280
+ void nullFirstLastSorting() {
+
+ // given
+ var original = "SELECT e FROM Employee e where e.name = :name ORDER BY e.first_name asc NULLS FIRST";
+
+ assertThat(createQueryFor(original, Sort.unsorted())).isEqualTo(original);
+
+ assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsLast())))
+ .startsWith(original)
+ .endsWithIgnoringCase("e.lastName DESC NULLS LAST");
+
+ assertThat(createQueryFor(original, Sort.by(Order.desc("lastName").nullsFirst())))
+ .startsWith(original)
+ .endsWithIgnoringCase("e.lastName DESC NULLS FIRST");
+ }
+
@Test
void applyCountToSimpleQuery() {