Skip to content

Commit

Permalink
Support Panache Sort Null Precedence
Browse files Browse the repository at this point in the history
Allow users sorting by either nulls first or nulls last.
Example of usage:

```java
Sort.by("foo", Sort.Direction.Ascending, Sort.NullPrecedence.NULLS_FIRST);
```

(See more examples in tests)

This PR also adds support of nulls handling for the Spring Data JPA Quarkus extension: it translates the Spring Sort object to the new Panache Sort object. 

Finally, as the Panache Sort API is also used by the Mongo Quarkus extension and Mongo does not easily support sorting by nulls, I decided to simply print a WARNING message if users try to use it.

Fix #26172
  • Loading branch information
Sgitario committed Jul 1, 2022
1 parent 13a652a commit b2f8443
Show file tree
Hide file tree
Showing 15 changed files with 234 additions and 6 deletions.
5 changes: 4 additions & 1 deletion docs/src/main/asciidoc/hibernate-orm-panache.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -606,9 +606,12 @@ List<Person> persons = Person.list(Sort.by("name").and("birth"));
// and with more restrictions
List<Person> persons = Person.list("status", Sort.by("name").and("birth"), Status.Alive);
// and list first the entries with null values in the field "birth"
List<Person> persons = Person.list(Sort.by("birth", Sort.NullPrecedence.NULLS_FIRST));
----

The `Sort` class has plenty of methods for adding columns and specifying sort direction.
The `Sort` class has plenty of methods for adding columns and specifying sort direction or the null precedence.

=== Simplified queries

Expand Down
5 changes: 4 additions & 1 deletion docs/src/main/asciidoc/hibernate-reactive-panache.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -491,9 +491,12 @@ Uni<List<Person>> persons = Person.list(Sort.by("name").and("birth"));
// and with more restrictions
Uni<List<Person>> persons = Person.list("status", Sort.by("name").and("birth"), Status.Alive);
// and list first the entries with null values in the field "birth"
Uni<List<Person>> persons = Person.list(Sort.by("birth", Sort.NullPrecedence.NULLS_FIRST));
----

The `Sort` class has plenty of methods for adding columns and specifying sort direction.
The `Sort` class has plenty of methods for adding columns and specifying sort direction or the null precedence.

=== Simplified queries

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,16 @@ public void testEmptySortEmptyYieldsEmptyString() {
assertEquals("", PanacheJpaUtil.toOrderBy(emptySort));
}

@Test
public void testSortByNullsFirst() {
Sort emptySort = Sort.by("foo", Sort.Direction.Ascending, Sort.NullPrecedence.NULLS_FIRST);
assertEquals(" ORDER BY foo NULLS FIRST", PanacheJpaUtil.toOrderBy(emptySort));
}

@Test
public void testSortByNullsLast() {
Sort emptySort = Sort.by("foo", Sort.Direction.Descending, Sort.NullPrecedence.NULLS_LAST);
assertEquals(" ORDER BY foo DESC NULLS LAST", PanacheJpaUtil.toOrderBy(emptySort));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,9 @@ private Document sortToDocument(Sort sort) {
Document sortDoc = new Document();
for (Sort.Column col : sort.getColumns()) {
sortDoc.append(col.getName(), col.getDirection() == Sort.Direction.Ascending ? 1 : -1);
if (col.getNullPrecedence() != null) {
throw new UnsupportedOperationException("Cannot sort by nulls first or nulls last");
}
}
return sortDoc;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,9 @@ private Document sortToDocument(Sort sort) {
Document sortDoc = new Document();
for (Sort.Column col : sort.getColumns()) {
sortDoc.append(col.getName(), col.getDirection() == Sort.Direction.Ascending ? 1 : -1);
if (col.getNullPrecedence() != null) {
throw new UnsupportedOperationException("Cannot sort by nulls first or nulls last");
}
}
return sortDoc;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,24 @@ public enum Direction {
Descending;
}

/**
* Represents the order of null columns.
*/
public enum NullPrecedence {
/**
* Sort by the null columns listed first in the resulting query.
*/
NULLS_FIRST,
/**
* Sort by the null columns listed last in the resulting query.
*/
NULLS_LAST;
}

public static class Column {
private String name;
private Direction direction;
private NullPrecedence nullPrecedence;

public Column(String name) {
this(name, Direction.Ascending);
Expand All @@ -53,6 +68,12 @@ public Column(String name, Direction direction) {
this.direction = direction;
}

public Column(String name, Direction direction, NullPrecedence nullPrecedence) {
this.name = name;
this.direction = direction;
this.nullPrecedence = nullPrecedence;
}

public String getName() {
return name;
}
Expand All @@ -68,6 +89,14 @@ public Direction getDirection() {
public void setDirection(Direction direction) {
this.direction = direction;
}

public NullPrecedence getNullPrecedence() {
return nullPrecedence;
}

public void setNullPrecedence(NullPrecedence nullPrecedence) {
this.nullPrecedence = nullPrecedence;
}
}

private List<Column> columns = new ArrayList<>();
Expand All @@ -91,7 +120,7 @@ public static Sort by(String column) {
* Sort by the given column, in the given order.
*
* @param column the column to sort on, in the given order.
* @param direction the direction to sort on
* @param direction the direction to sort on.
* @return a new Sort instance which sorts on the given column in the given order.
* @see #by(String)
* @see #by(String...)
Expand All @@ -100,6 +129,33 @@ public static Sort by(String column, Direction direction) {
return new Sort().and(column, direction);
}

/**
* Sort by the given column, in the given order and in the given null precedence.
*
* @param column the column to sort on, in the given order.
* @param nullPrecedence the null precedence to use.
* @return a new Sort instance which sorts on the given column in the given order and null precedence.
* @see #by(String)
* @see #by(String...)
*/
public static Sort by(String column, NullPrecedence nullPrecedence) {
return by(column, Direction.Ascending, nullPrecedence);
}

/**
* Sort by the given column, in the given order and in the given null precedence.
*
* @param column the column to sort on, in the given order.
* @param direction the direction to sort on.
* @param nullPrecedence the null precedence to use.
* @return a new Sort instance which sorts on the given column in the given order and null precedence.
* @see #by(String)
* @see #by(String...)
*/
public static Sort by(String column, Direction direction, NullPrecedence nullPrecedence) {
return new Sort().and(column, direction, nullPrecedence);
}

/**
* Sort by the given columns, in ascending order. Equivalent to {@link #ascending(String...)}.
*
Expand Down Expand Up @@ -202,6 +258,7 @@ public Sort and(String name) {
* Adds a sort column, in the given order.
*
* @param name the new column to sort on, in the given order.
* @param direction the direction to sort on.
* @return this instance, modified.
* @see #and(String)
*/
Expand All @@ -210,6 +267,32 @@ public Sort and(String name, Direction direction) {
return this;
}

/**
* Adds a sort column, in the given null precedence.
*
* @param name the new column to sort on, in the given null precedence.
* @param nullPrecedence the null precedence to use.
* @return this instance, modified.
* @see #and(String)
*/
public Sort and(String name, NullPrecedence nullPrecedence) {
return and(name, Direction.Ascending, nullPrecedence);
}

/**
* Adds a sort column, in the given order and null precedence.
*
* @param name the new column to sort on, in the given order and null precedence.
* @param direction the direction to sort on.
* @param nullPrecedence the null precedence to use.
* @return this instance, modified.
* @see #and(String)
*/
public Sort and(String name, Direction direction, NullPrecedence nullPrecedence) {
columns.add(new Column(name, direction, nullPrecedence));
return this;
}

/**
* Get the sort columns
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,18 @@ public static String toOrderBy(Sort sort) {
if (i > 0)
sb.append(" , ");
sb.append(column.getName());
if (column.getDirection() != Sort.Direction.Ascending)
if (column.getDirection() != Sort.Direction.Ascending) {
sb.append(" DESC");
}

if (column.getNullPrecedence() != null) {
if (column.getNullPrecedence() == Sort.NullPrecedence.NULLS_FIRST) {
sb.append(" NULLS FIRST");
} else {
sb.append(" NULLS LAST");
}
}

}
return sb.toString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;

import io.quarkus.test.QuarkusUnitTest;

Expand Down Expand Up @@ -124,6 +125,28 @@ public void testMapInterfacesUsingSlice() {
assertEquals(0, nextPage.getNumberOfElements());
}

@Test
@Order(8)
@Transactional
public void testListOrderByNullHandling() throws MalformedURLException {
// Insert row with null in url
UUID uuidForTheNullUrlRecord = UUID.randomUUID();
BasicTypeData item = populateData(new BasicTypeData());
item.setUuid(uuidForTheNullUrlRecord);
item.setUrl(null);
repo.save(item);
// At this moment, there should be at least two records, one inserted in the first test,
// and the second with the "url" column to null.

// Check Nulls first
List<BasicTypeData> list = repo.findAll(Sort.by(Sort.Order.by("url").nullsFirst()));
assertEquals(uuidForTheNullUrlRecord, list.get(0).getUuid());

// Check Nulls last
list = repo.findAll(Sort.by(Sort.Order.by("url").nullsLast()));
assertEquals(uuidForTheNullUrlRecord, list.get(list.size() - 1).getUuid());
}

private BasicTypeData populateData(BasicTypeData basicTypeData) throws MalformedURLException {
basicTypeData.setDoubleValue(Math.PI);
basicTypeData.setBigDecimalValue(BigDecimal.valueOf(Math.PI * 2.0));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ public static io.quarkus.panache.common.Sort toPanacheSort(org.springframework.d
}

org.springframework.data.domain.Sort.Order firstOrder = orders.get(0);
Sort result = Sort.by(firstOrder.getProperty(), getDirection(firstOrder));
Sort result = Sort.by(firstOrder.getProperty(), getDirection(firstOrder), getNullPrecedence(firstOrder));
if (orders.size() == 1) {
return result;
}

for (int i = 1; i < orders.size(); i++) {
org.springframework.data.domain.Sort.Order order = orders.get(i);
result = result.and(order.getProperty(), getDirection(order));
result = result.and(order.getProperty(), getDirection(order), getNullPrecedence(order));
}
return result;
}
Expand All @@ -35,6 +35,19 @@ private static Sort.Direction getDirection(org.springframework.data.domain.Sort.
: Sort.Direction.Descending;
}

private static Sort.NullPrecedence getNullPrecedence(org.springframework.data.domain.Sort.Order order) {
if (order.getNullHandling() != null) {
switch (order.getNullHandling()) {
case NULLS_FIRST:
return Sort.NullPrecedence.NULLS_FIRST;
case NULLS_LAST:
return Sort.NullPrecedence.NULLS_LAST;
}
}

return null;
}

public static io.quarkus.panache.common.Page toPanachePage(org.springframework.data.domain.Pageable pageable) {
// only generate queries with paging if param is actually paged (ex. Unpaged.INSTANCE is a Pageable not paged)
if (pageable.isPaged()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1519,4 +1519,28 @@ public String testFilterWithCollections() {

return "OK";
}

@GET
@Path("testSortByNullPrecedence")
@Transactional
public String testSortByNullPrecedence() {
Person.deleteAll();

Person stefPerson = new Person();
stefPerson.name = "Stef";
stefPerson.persist();

Person josePerson = new Person();
josePerson.name = null;
josePerson.persist();

List<Person> persons = Person.findAll(Sort.by("name", Sort.NullPrecedence.NULLS_FIRST)).list();
assertEquals(josePerson.id, persons.get(0).id);
persons = Person.findAll(Sort.by("name", Sort.NullPrecedence.NULLS_LAST)).list();
assertEquals(josePerson.id, persons.get(persons.size() - 1).id);

Person.deleteAll();

return "OK";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ public void testFilterWithCollections() {
RestAssured.when().get("/test/testFilterWithCollections").then().body(is("OK"));
}

@Test
public void testSortByNullPrecedence() {
RestAssured.when().get("/test/testSortByNullPrecedence").then().body(is("OK"));
}

@Test
public void testJaxbAnnotationTransfer() {
RestAssured.when()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1878,4 +1878,31 @@ public Uni<String> testBug9036() {
}).map(v -> "OK");
});
}

@GET
@Path("testSortByNullPrecedence")
@ReactiveTransactional
public Uni<String> testSortByNullPrecedence() {
return Person.deleteAll()
.flatMap(v -> {
Person stefPerson = new Person();
stefPerson.name = "Stef";
stefPerson.uniqueName = "stef";

Person josePerson = new Person();
josePerson.name = null;
josePerson.uniqueName = "jose";
return Person.persist(stefPerson, josePerson);
}).flatMap(p -> {
return Person.findAll(Sort.by("name", Sort.NullPrecedence.NULLS_FIRST)).list();
}).flatMap(list -> {
assertEquals("jose", ((Person) list.get(0)).uniqueName);

return Person.findAll(Sort.by("name", Sort.NullPrecedence.NULLS_LAST)).list();
}).flatMap(list -> {
assertEquals("jose", ((Person) list.get(list.size() - 1)).uniqueName);

return Person.deleteAll();
}).map(v -> "OK");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@ public void testBug9036() {
RestAssured.when().get("/test/9036").then().body(is("OK"));
}

@Test
public void testSortByNullPrecedence() {
RestAssured.when().get("/test/testSortByNullPrecedence").then().body(is("OK"));
}

@DisabledOnNativeImage
@ReactiveTransactional
@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,11 @@ public Response rename(@QueryParam("previousName") String previousName, @QueryPa
personRepository.update("lastname", newName).where("lastname", previousName);
return Response.ok().build();
}

@GET
@Path("/search/by/nulls/precedence")
public Response searchPersonsByNullsPrecedence() {
personRepository.listAll(Sort.by("lastname", Sort.NullPrecedence.NULLS_FIRST));
return Response.ok().build();
}
}
Loading

0 comments on commit b2f8443

Please sign in to comment.