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() {