Support NULLS {FIRST | LAST} in JPQL queries.
This commit adds support for parsing and appending order by items that define a NULL precedence. Closes #3529
This commit is contained in:
committed by
Mark Paluch
parent
a784a419f2
commit
410152af3a
@@ -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;
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.util.regex.Pattern;
|
||||
|
||||
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 @@ class JpaQueryTransformerSupport {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.util.List;
|
||||
|
||||
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 @@ class JpqlQueryRenderer extends JpqlBaseVisitor<QueryTokenStream> {
|
||||
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) {
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import java.util.Collection;
|
||||
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 @@ interface QueryTokenStream extends Streamable<QueryToken> {
|
||||
return concat(elements, visitor, QueryRenderer::inline, separator);
|
||||
}
|
||||
|
||||
static <T> QueryTokenStream justAs(Collection<T> elements, Function<T, QueryToken> 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.
|
||||
*
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.junit.jupiter.params.provider.Arguments;
|
||||
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 @@ class EqlQueryTransformerTests {
|
||||
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() {
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.junit.jupiter.params.provider.ValueSource;
|
||||
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;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -77,6 +78,23 @@ class HqlQueryTransformerTests {
|
||||
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() {
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.junit.jupiter.params.provider.Arguments;
|
||||
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 @@ class JpqlQueryTransformerTests {
|
||||
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() {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user