Skip to content

Commit f48bbab

Browse files
ctailor2schauder
authored andcommitted
Null precedence is now supported if the underlying database supports it.
Original pull request #1156 Closes #821
1 parent 44b7b8f commit f48bbab

File tree

13 files changed

+258
-17
lines changed

13 files changed

+258
-17
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2021 the original author or authors.
2+
* Copyright 2017-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -51,6 +51,7 @@
5151
* @author Milan Milanov
5252
* @author Myeonghyeon Lee
5353
* @author Mikhail Polivakha
54+
* @author Chirag Tailor
5455
*/
5556
class SqlGenerator {
5657

@@ -714,7 +715,7 @@ private OrderByField orderToOrderByField(Sort.Order order) {
714715

715716
SqlIdentifier columnName = this.entity.getRequiredPersistentProperty(order.getProperty()).getColumnName();
716717
Column column = Column.create(columnName, this.getTable());
717-
return OrderByField.from(column, order.getDirection());
718+
return OrderByField.from(column, order.getDirection()).withNullHandling(order.getNullHandling());
718719
}
719720

720721
/**

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java

+15
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,21 @@ void saveAndLoadManyEntitiesWithReferencedEntitySortedAndPaged() {
257257
.containsExactly("Star");
258258
}
259259

260+
@Test // GH-821
261+
@EnabledOnFeature({SUPPORTS_QUOTED_IDS, SUPPORTS_NULL_HANDLING})
262+
void saveAndLoadManyEntitiesWithReferencedEntitySortedWithNullHandling() {
263+
264+
template.save(createLegoSet(null));
265+
template.save(createLegoSet("Star"));
266+
template.save(createLegoSet("Frozen"));
267+
268+
Iterable<LegoSet> reloadedLegoSets = template.findAll(LegoSet.class, Sort.by(new Sort.Order(Sort.Direction.ASC, "name", Sort.NullHandling.NULLS_LAST)));
269+
270+
assertThat(reloadedLegoSets) //
271+
.extracting("name") //
272+
.containsExactly("Frozen", "Star", null);
273+
}
274+
260275
@Test // DATAJDBC-112
261276
@EnabledOnFeature(SUPPORTS_QUOTED_IDS)
262277
void saveAndLoadManyEntitiesByIdWithReferencedEntity() {

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2021 the original author or authors.
2+
* Copyright 2017-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -25,7 +25,6 @@
2525

2626
import org.junit.jupiter.api.BeforeEach;
2727
import org.junit.jupiter.api.Test;
28-
2928
import org.springframework.data.annotation.Id;
3029
import org.springframework.data.annotation.ReadOnlyProperty;
3130
import org.springframework.data.annotation.Version;
@@ -64,6 +63,7 @@
6463
* @author Milan Milanov
6564
* @author Myeonghyeon Lee
6665
* @author Mikhail Polivakha
66+
* @author Chirag Tailor
6767
*/
6868
class SqlGeneratorUnitTests {
6969

@@ -245,6 +245,26 @@ void findAllSortedByMultipleFields() {
245245
"x_other ASC");
246246
}
247247

248+
@Test // GH-821
249+
void findAllSortedWithNullHandling_resolvesNullHandlingWhenDialectSupportsIt() {
250+
251+
SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class, PostgresDialect.INSTANCE);
252+
253+
String sql = sqlGenerator.getFindAll(Sort.by(new Sort.Order(Sort.Direction.ASC, "name", Sort.NullHandling.NULLS_LAST)));
254+
255+
assertThat(sql).contains("ORDER BY \"dummy_entity\".\"x_name\" ASC NULLS LAST");
256+
}
257+
258+
@Test // GH-821
259+
void findAllSortedWithNullHandling_ignoresNullHandlingWhenDialectDoesNotSupportIt() {
260+
261+
SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class, SqlServerDialect.INSTANCE);
262+
263+
String sql = sqlGenerator.getFindAll(Sort.by(new Sort.Order(Sort.Direction.ASC, "name", Sort.NullHandling.NULLS_LAST)));
264+
265+
assertThat(sql).endsWith("ORDER BY dummy_entity.x_name ASC");
266+
}
267+
248268
@Test // DATAJDBC-101
249269
void findAllPagedByUnpaged() {
250270

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestDatabaseFeatures.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2021 the original author or authors.
2+
* Copyright 2020-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@
2929
* presence or absence of features in tests.
3030
*
3131
* @author Jens Schauder
32+
* @author Chirag Tailor
3233
*/
3334
public class TestDatabaseFeatures {
3435

@@ -83,6 +84,10 @@ private void supportsMultiDimensionalArrays() {
8384
assumeThat(database).isNotIn(Database.H2, Database.Hsql);
8485
}
8586

87+
private void supportsNullHandling() {
88+
assumeThat(database).isNotIn(Database.MySql, Database.MariaDb, Database.SqlServer);
89+
}
90+
8691
public void databaseIs(Database database) {
8792
assumeThat(this.database).isEqualTo(database);
8893
}
@@ -115,6 +120,7 @@ public enum Feature {
115120
SUPPORTS_ARRAYS(TestDatabaseFeatures::supportsArrays), //
116121
SUPPORTS_GENERATED_IDS_IN_REFERENCED_ENTITIES(TestDatabaseFeatures::supportsGeneratedIdsInReferencedEntities), //
117122
SUPPORTS_NANOSECOND_PRECISION(TestDatabaseFeatures::supportsNanosecondPrecision), //
123+
SUPPORTS_NULL_HANDLING(TestDatabaseFeatures::supportsNullHandling),
118124
IS_POSTGRES(f -> f.databaseIs(Database.PostgreSql)), //
119125
IS_HSQL(f -> f.databaseIs(Database.Hsql));
120126

spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/AbstractDialect.java

+12-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019-2021 the original author or authors.
2+
* Copyright 2019-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
1818
import java.util.OptionalLong;
1919
import java.util.function.Function;
2020

21+
import org.springframework.data.domain.Sort;
2122
import org.springframework.data.relational.core.sql.LockMode;
2223
import org.springframework.data.relational.core.sql.LockOptions;
2324
import org.springframework.data.relational.core.sql.Select;
@@ -28,6 +29,7 @@
2829
*
2930
* @author Mark Paluch
3031
* @author Myeonghyeon Lee
32+
* @author Chirag Tailor
3133
* @since 1.1
3234
*/
3335
public abstract class AbstractDialect implements Dialect {
@@ -42,7 +44,7 @@ public SelectRenderContext getSelectContext() {
4244
Function<Select, ? extends CharSequence> afterFromTable = getAfterFromTable();
4345
Function<Select, ? extends CharSequence> afterOrderBy = getAfterOrderBy();
4446

45-
return new DialectSelectRenderContext(afterFromTable, afterOrderBy);
47+
return new DialectSelectRenderContext(afterFromTable, afterOrderBy, orderByNullHandling());
4648
}
4749

4850
/**
@@ -105,12 +107,14 @@ static class DialectSelectRenderContext implements SelectRenderContext {
105107

106108
private final Function<Select, ? extends CharSequence> afterFromTable;
107109
private final Function<Select, ? extends CharSequence> afterOrderBy;
110+
private final OrderByNullHandling orderByNullHandling;
108111

109112
DialectSelectRenderContext(Function<Select, ? extends CharSequence> afterFromTable,
110-
Function<Select, ? extends CharSequence> afterOrderBy) {
113+
Function<Select, ? extends CharSequence> afterOrderBy, OrderByNullHandling orderByNullHandling) {
111114

112115
this.afterFromTable = afterFromTable;
113116
this.afterOrderBy = afterOrderBy;
117+
this.orderByNullHandling = orderByNullHandling;
114118
}
115119

116120
/*
@@ -130,6 +134,11 @@ static class DialectSelectRenderContext implements SelectRenderContext {
130134
public Function<Select, ? extends CharSequence> afterOrderBy(boolean hasOrderBy) {
131135
return afterOrderBy;
132136
}
137+
138+
@Override
139+
public String evaluateOrderByNullHandling(Sort.NullHandling nullHandling) {
140+
return orderByNullHandling.evaluate(nullHandling);
141+
}
133142
}
134143

135144
/**

spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019-2021 the original author or authors.
2+
* Copyright 2019-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -33,6 +33,7 @@
3333
* @author Myeonghyeon Lee
3434
* @author Christoph Strobl
3535
* @author Mikhail Polivakha
36+
* @author Chirag Tailor
3637
* @since 1.1
3738
*/
3839
public interface Dialect {
@@ -120,4 +121,13 @@ default Set<Class<?>> simpleTypes() {
120121
default InsertRenderContext getInsertRenderContext() {
121122
return InsertRenderContexts.DEFAULT;
122123
}
124+
125+
/**
126+
* Return the {@link OrderByNullHandling} used by this dialect.
127+
*
128+
* @return the {@link OrderByNullHandling} used by this dialect.
129+
*/
130+
default OrderByNullHandling orderByNullHandling() {
131+
return OrderByNullHandling.SQL_STANDARD;
132+
}
123133
}

spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java

+5
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,9 @@ public IdentifierProcessing getIdentifierProcessing() {
169169
public Collection<Object> getConverters() {
170170
return Collections.singletonList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE);
171171
}
172+
173+
@Override
174+
public OrderByNullHandling orderByNullHandling() {
175+
return OrderByNullHandling.NONE;
176+
}
172177
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.relational.core.dialect;
17+
18+
import org.springframework.data.domain.Sort;
19+
20+
/**
21+
* Represents how the {@link Sort.NullHandling} option of an {@code ORDER BY} sort expression is to be evaluated.
22+
*
23+
* @author Chirag Tailor
24+
*/
25+
public interface OrderByNullHandling {
26+
/**
27+
* An {@link OrderByNullHandling} that can be used for databases conforming to the SQL standard which uses
28+
* {@code NULLS FIRST} and {@code NULLS LAST} in {@code ORDER BY} sort expressions to make null values appear before
29+
* or after non-null values in the result set.
30+
*/
31+
OrderByNullHandling SQL_STANDARD = new SqlStandardOrderByNullHandling();
32+
33+
/**
34+
* An {@link OrderByNullHandling} that can be used for databases that do not support the SQL standard usage of
35+
* {@code NULLS FIRST} and {@code NULLS LAST} in {@code ORDER BY} sort expressions to control where null values appear
36+
* respective to non-null values in the result set.
37+
*/
38+
OrderByNullHandling NONE = nullHandling -> "";
39+
40+
/**
41+
* Converts a {@link Sort.NullHandling} option to the appropriate SQL text to be included an {@code ORDER BY} sort
42+
* expression.
43+
*/
44+
String evaluate(Sort.NullHandling nullHandling);
45+
46+
/**
47+
* An {@link OrderByNullHandling} implementation for databases conforming to the SQL standard which uses
48+
* {@code NULLS FIRST} and {@code NULLS LAST} in {@code ORDER BY} sort expressions to make null values appear before
49+
* or after non-null values in the result set.
50+
*
51+
* @author Chirag Tailor
52+
*/
53+
class SqlStandardOrderByNullHandling implements OrderByNullHandling {
54+
55+
private static final String NULLS_FIRST = "NULLS FIRST";
56+
private static final String NULLS_LAST = "NULLS LAST";
57+
private static final String UNSPECIFIED = "";
58+
59+
@Override
60+
public String evaluate(Sort.NullHandling nullHandling) {
61+
62+
switch (nullHandling) {
63+
case NULLS_FIRST: return NULLS_FIRST;
64+
case NULLS_LAST: return NULLS_LAST;
65+
case NATIVE: return UNSPECIFIED;
66+
default:
67+
throw new UnsupportedOperationException("Sort.NullHandling " + nullHandling + " not supported");
68+
}
69+
}
70+
}
71+
}

spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java

+5
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,9 @@ public IdentifierProcessing getIdentifierProcessing() {
156156
public InsertRenderContext getInsertRenderContext() {
157157
return InsertRenderContexts.MS_SQL_SERVER;
158158
}
159+
160+
@Override
161+
public OrderByNullHandling orderByNullHandling() {
162+
return OrderByNullHandling.NONE;
163+
}
159164
}

spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/OrderByClauseVisitor.java

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019-2021 the original author or authors.
2+
* Copyright 2019-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@
2424
*
2525
* @author Mark Paluch
2626
* @author Jens Schauder
27+
* @author Chirag Tailor
2728
* @since 1.1
2829
*/
2930
class OrderByClauseVisitor extends TypedSubtreeVisitor<OrderByField> implements PartRenderer {
@@ -59,11 +60,15 @@ Delegation enterMatched(OrderByField segment) {
5960
@Override
6061
Delegation leaveMatched(OrderByField segment) {
6162

62-
OrderByField field = segment;
63+
if (segment.getDirection() != null) {
64+
builder.append(" ") //
65+
.append(segment.getDirection());
66+
}
6367

64-
if (field.getDirection() != null) {
68+
String nullHandling = context.getSelectRenderContext().evaluateOrderByNullHandling(segment.getNullHandling());
69+
if (!nullHandling.isEmpty()) {
6570
builder.append(" ") //
66-
.append(field.getDirection());
71+
.append(nullHandling);
6772
}
6873

6974
return Delegation.leave();

spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectRenderContext.java

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019-2021 the original author or authors.
2+
* Copyright 2019-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,17 +18,20 @@
1818
import java.util.OptionalLong;
1919
import java.util.function.Function;
2020

21+
import org.springframework.data.domain.Sort;
22+
import org.springframework.data.relational.core.dialect.OrderByNullHandling;
2123
import org.springframework.data.relational.core.sql.LockMode;
2224
import org.springframework.data.relational.core.sql.Select;
2325

2426
/**
2527
* Render context specifically for {@code SELECT} statements. This interface declares rendering hooks that are called
26-
* before/after a specific {@code SELECT} clause part. The rendering content is appended directly after/before an
28+
* before/after/during a specific {@code SELECT} clause part. The rendering content is appended directly after/before an
2729
* element without further whitespace processing. Hooks are responsible for adding required surrounding whitespaces.
2830
*
2931
* @author Mark Paluch
3032
* @author Myeonghyeon Lee
3133
* @author Jens Schauder
34+
* @author Chirag Tailor
3235
* @since 1.1
3336
*/
3437
public interface SelectRenderContext {
@@ -86,4 +89,14 @@ public interface SelectRenderContext {
8689
return lockPrefix;
8790
};
8891
}
92+
93+
/**
94+
* Customization hook: Rendition of the null handling option for an {@code ORDER BY} sort expression.
95+
*
96+
* @param nullHandling the {@link Sort.NullHandling} for the {@code ORDER BY} sort expression. Must not be {@literal null}.
97+
* @return render {@link String} SQL text to be included in an {@code ORDER BY} sort expression.
98+
*/
99+
default String evaluateOrderByNullHandling(Sort.NullHandling nullHandling) {
100+
return OrderByNullHandling.NONE.evaluate(nullHandling);
101+
}
89102
}

0 commit comments

Comments
 (0)