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:
Christoph Strobl
2024-07-01 12:17:05 +02:00
committed by Mark Paluch
parent a784a419f2
commit 410152af3a
7 changed files with 85 additions and 1 deletions

View File

@@ -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;

View File

@@ -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);
}

View File

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

View File

@@ -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.
*

View File

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

View File

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

View File

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