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 de53a6360..f743d5066 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 @@ -796,7 +796,7 @@ class JpqlQueryRenderer extends JpqlBaseVisitor { builder.append(QueryTokens.expression(ctx.DESC())); } - if(ctx.nullsPrecedence() != null) { + if (ctx.nullsPrecedence() != null) { builder.append(visit(ctx.nullsPrecedence())); } @@ -805,8 +805,18 @@ class JpqlQueryRenderer extends JpqlBaseVisitor { @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())); + + QueryRendererBuilder builder = QueryRenderer.builder(); + + builder.append(TOKEN_NULLS); + + if (ctx.FIRST() != null) { + builder.append(TOKEN_FIRST); + } else if (ctx.LAST() != null) { + builder.append(TOKEN_LAST); + } + + return builder; } @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java index 0265b5549..9d60eb68c 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java @@ -753,6 +753,10 @@ public abstract class QueryUtils { PropertyPath property = PropertyPath.from(order.getProperty(), from.getJavaType()); Expression expression = toExpressionRecursively(from, property); + if (order.getNullHandling() != Sort.NullHandling.NATIVE) { + throw new UnsupportedOperationException("Applying Null Precedence using Criteria Queries is not yet supported."); + } + if (order.isIgnoreCase() && String.class.equals(expression.getJavaType())) { Expression upper = cb.lower((Expression) expression); return order.isAscending() ? cb.asc(upper) : cb.desc(upper); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java index ffbee9e80..59eb5f866 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java @@ -15,11 +15,9 @@ */ package org.springframework.data.jpa.repository.query; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; +import static java.util.Collections.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import jakarta.persistence.Entity; import jakarta.persistence.EntityManager; @@ -48,6 +46,7 @@ import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; + import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.jpa.domain.sample.Category; @@ -315,6 +314,20 @@ class QueryUtilsIntegrationTests { assertThat(orders).hasSize(1); } + @Test // GH-3529 + void nullPrecedenceThroughCriteriaApiNotYetSupported() { + + CriteriaBuilder builder = em.getCriteriaBuilder(); + CriteriaQuery query = builder.createQuery(User.class); + Root root = query.from(User.class); + Join join = root.join("manager", JoinType.LEFT); + + Sort sort = Sort.by(Sort.Order.desc("manager").nullsFirst()); + + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> QueryUtils.toOrders(sort, join, builder)); + } + /** * This test documents an ambiguity in the JPA spec (or it's implementation in Hibernate vs EclipseLink) that we have * to work around in the test {@link #doesNotCreateJoinForOptionalAssociationWithoutFurtherNavigation()}. See also: