Skip to content

Commit

Permalink
Support Panache Sort Null Handling
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.NullHandling.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 Jun 28, 2022
1 parent 8349f71 commit a84b529
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 3 deletions.
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.NullHandling.NULLS_FIRST);
assertEquals(" ORDER BY foo NULLS FIRST", PanacheJpaUtil.toOrderBy(emptySort));
}

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

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkus.hibernate.orm.panache.deployment.test;

import java.util.List;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
Expand All @@ -9,6 +11,8 @@

import org.jboss.resteasy.annotations.jaxrs.PathParam;

import io.quarkus.panache.common.Sort;

@Path("entity")
public class MyTestResource {

Expand All @@ -21,4 +25,18 @@ public MyEntity get(@PathParam long id) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
return ret;
}

@GET
@Path("/list/by/nulls/first")
@Produces(MediaType.APPLICATION_JSON)
public List<MyEntity> listOrderByNullsFirst() {
return MyEntity.listAll(Sort.by("name", Sort.Direction.Ascending, Sort.NullHandling.NULLS_FIRST));
}

@GET
@Path("/list/by/nulls/last")
@Produces(MediaType.APPLICATION_JSON)
public List<MyEntity> listOrderByNullsLast() {
return MyEntity.listAll(Sort.by("name", Sort.Direction.Ascending, Sort.NullHandling.NULLS_LAST));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.quarkus.hibernate.orm.panache.deployment.test;

import static org.hamcrest.Matchers.is;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;

public class PanacheSortTestCase {
@RegisterExtension
final static QuarkusUnitTest TEST = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(MyEntity.class, MyTestResource.class)
.addAsResource("application-test.properties", "application.properties")
.addAsResource("import.sql"));

@Test
public void testByNullFirstOptions() {
RestAssured.when().get("/entity/list/by/nulls/first").then().statusCode(200)
.body(is("[{\"id\":2},{\"id\":1,\"name\":\"my name\"}]"));
}

@Test
public void testByNullLastOptions() {
RestAssured.when().get("/entity/list/by/nulls/last").then().statusCode(200)
.body(is("[{\"id\":1,\"name\":\"my name\"},{\"id\":2}]"));
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
INSERT INTO MyEntity(id, name) VALUES(1, 'my name');
-- To verify null columns sorting
INSERT INTO MyEntity(id) VALUES(2);
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.getNullHandling() != null) {
LOGGER.warn("Sort by null first or null last is not supported. It will be ignored.");
}
}
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.getNullHandling() != null) {
LOGGER.warn("Sort by null first or null last is not supported. It will be ignored.");
}
}
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 NullHandling {
/**
* 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 NullHandling nullHandling;

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, NullHandling nullHandling) {
this.name = name;
this.direction = direction;
this.nullHandling = nullHandling;
}

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

public NullHandling getNullHandling() {
return nullHandling;
}

public void setNullHandling(NullHandling nullHandling) {
this.nullHandling = nullHandling;
}
}

private List<Column> columns = new ArrayList<>();
Expand Down Expand Up @@ -100,6 +129,20 @@ public static Sort by(String column, Direction direction) {
return new Sort().and(column, direction);
}

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

/**
* Sort by the given columns, in ascending order. Equivalent to {@link #ascending(String...)}.
*
Expand Down Expand Up @@ -202,6 +245,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 +254,20 @@ public Sort and(String name, Direction direction) {
return this;
}

/**
* 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
* @param nullHandling the null handling to use.
* @return this instance, modified.
* @see #and(String)
*/
public Sort and(String name, Direction direction, NullHandling nullHandling) {
columns.add(new Column(name, direction, nullHandling));
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.getNullHandling() != null) {
if (column.getNullHandling() == Sort.NullHandling.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), getNullHandling(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), getNullHandling(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.NullHandling getNullHandling(org.springframework.data.domain.Sort.Order order) {
if (order.getNullHandling() != null) {
switch (order.getNullHandling()) {
case NULLS_FIRST:
return Sort.NullHandling.NULLS_FIRST;
case NULLS_LAST:
return Sort.NullHandling.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

0 comments on commit a84b529

Please sign in to comment.