Skip to content

Commit c8573cb

Browse files
committed
Align offset scrolling with Spring Data 2024.0
Closes gh-946
1 parent 86ba988 commit c8573cb

File tree

7 files changed

+39
-85
lines changed

7 files changed

+39
-85
lines changed

platform/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ dependencies {
1212
api(platform("io.projectreactor:reactor-bom:2023.0.3"))
1313
api(platform("io.micrometer:micrometer-bom:1.13.0-M1"))
1414
api(platform("io.micrometer:micrometer-tracing-bom:1.3.0-M1"))
15-
api(platform("org.springframework.data:spring-data-bom:2023.1.3"))
15+
api(platform("org.springframework.data:spring-data-bom:2024.0.0-SNAPSHOT"))
1616
api(platform("org.springframework.security:spring-security-bom:6.3.0-M2"))
1717
api(platform("com.querydsl:querydsl-bom:5.0.0"))
1818
api(platform("io.rsocket:rsocket-bom:1.1.4"))

spring-graphql/build.gradle

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ dependencies {
2121
compileOnly 'org.springframework.security:spring-security-core'
2222

2323
compileOnly 'com.querydsl:querydsl-core'
24-
compileOnly 'org.springframework.data:spring-data-commons:3.1.0-SNAPSHOT'
24+
compileOnly 'org.springframework.data:spring-data-commons'
2525

2626
compileOnly 'io.rsocket:rsocket-core'
2727
compileOnly 'io.rsocket:rsocket-transport-netty'
@@ -51,14 +51,14 @@ dependencies {
5151
testImplementation 'org.springframework:spring-websocket'
5252
testImplementation 'org.springframework.data:spring-data-commons'
5353
testImplementation 'org.springframework.data:spring-data-keyvalue'
54-
testImplementation 'org.springframework.data:spring-data-jpa:3.1.0-SNAPSHOT'
54+
testImplementation 'org.springframework.data:spring-data-jpa'
5555
testImplementation 'io.micrometer:micrometer-observation-test'
5656
testImplementation 'io.micrometer:micrometer-tracing-test'
5757
testImplementation 'com.h2database:h2'
5858
testImplementation 'org.hibernate:hibernate-core'
5959
testImplementation 'org.hibernate.validator:hibernate-validator'
6060
testImplementation 'org.springframework.data:spring-data-mongodb'
61-
testImplementation 'org.springframework.data:spring-data-neo4j:7.1.1'
61+
testImplementation 'org.springframework.data:spring-data-neo4j'
6262
testImplementation 'org.mongodb:mongodb-driver-sync'
6363
testImplementation 'org.mongodb:mongodb-driver-reactivestreams'
6464
testImplementation 'org.testcontainers:mongodb'

spring-graphql/src/main/java/org/springframework/graphql/data/query/ScrollSubrange.java

Lines changed: 17 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,7 @@
2424
import org.springframework.lang.Nullable;
2525

2626
/**
27-
* Container for parameters that limit result elements to a subrange including a
28-
* relative {@link ScrollPosition}, number of elements, and direction.
29-
*
30-
* <p> For backward pagination, the offset of an {@link OffsetScrollPosition}
31-
* is adjusted to point to the first item in the range by subtracting the count
32-
* from it. Hence, for {@code OffsetScrollPosition} {@link #forward()} is
33-
* always {@code true}.
27+
* {@link Subrange} implementation for a {@link ScrollPosition} cursor.
3428
*
3529
* @author Rossen Stoyanchev
3630
* @author Oliver Drotbohm
@@ -40,45 +34,16 @@ public final class ScrollSubrange extends Subrange<ScrollPosition> {
4034

4135

4236
@SuppressWarnings("unused")
43-
private ScrollSubrange(
44-
@Nullable ScrollPosition pos, @Nullable Integer count, boolean forward,
45-
@Nullable Object unused /* temporarily to differentiate from deprecated constructor */) {
46-
37+
private ScrollSubrange(@Nullable ScrollPosition pos, @Nullable Integer count, boolean forward) {
4738
super(pos, count, forward);
4839
}
4940

50-
/**
51-
* Public constructor.
52-
* @param pos the reference position, or {@code null} if not specified
53-
* @param count how many to return, or {@code null} if not specified
54-
* @param forward whether scroll forward (true) or backward (false)
55-
* @deprecated in favor of {@link #create}, to be removed in 1.3.
56-
*/
57-
@Deprecated(since = "1.2.4", forRemoval = true)
58-
public ScrollSubrange(@Nullable ScrollPosition pos, @Nullable Integer count, boolean forward) {
59-
super(initPosition(pos, count, forward), count, (pos instanceof OffsetScrollPosition || forward));
60-
}
61-
62-
@Nullable
63-
private static ScrollPosition initPosition(@Nullable ScrollPosition pos, @Nullable Integer count, boolean forward) {
64-
if (!forward) {
65-
if (pos instanceof OffsetScrollPosition offsetPosition && count != null) {
66-
return offsetPosition.advanceBy(-count);
67-
}
68-
else if (pos instanceof KeysetScrollPosition keysetPosition) {
69-
pos = keysetPosition.backward();
70-
}
71-
}
72-
return pos;
73-
}
74-
7541

7642
/**
7743
* Create a {@link ScrollSubrange} from the given inputs.
78-
* <p>Pagination with offset-based scrolling is always forward and inclusive
79-
* of the referenced item. Therefore, an {@link OffsetScrollPosition} is
80-
* adjusted as follows. For forward pagination, advanced by 1. For backward
81-
* pagination, advanced back by the count, and switched to forward.
44+
* <p>Offset scrolling is always forward and exclusive of the referenced item.
45+
* Therefore, for backward pagination, the offset is advanced back by the
46+
* count + 1, and the direction is switched to forward.
8247
* @param position the reference position, or {@code null} if not specified
8348
* @param count how many to return, or {@code null} if not specified
8449
* @param forward whether scroll forward (true) or backward (false)
@@ -98,23 +63,24 @@ else if (position instanceof KeysetScrollPosition keysetScrollPosition) {
9863
return initFromKeysetPosition(keysetScrollPosition, count, forward);
9964
}
10065
else {
101-
return new ScrollSubrange(position, count, forward, null);
66+
return new ScrollSubrange(position, count, forward);
10267
}
10368
}
10469

10570
private static ScrollSubrange initFromOffsetPosition(
10671
OffsetScrollPosition position, @Nullable Integer count, boolean forward) {
10772

108-
// Offset is inclusive, adapt to exclusive:
109-
// - for forward, add 1 to return items after position
110-
// - for backward, subtract count to get items before position
73+
// Offset is exclusive:
74+
// - for forward, nothing to do
75+
// - for backward, subtract (count + 1) to get items before position
11176

112-
if (forward) {
113-
position = position.advanceBy(1);
114-
}
115-
else {
77+
if (!forward) {
11678
// Advance back by 1 at least to item before position
117-
int advanceCount = (count != null) ? count : 1;
79+
int advanceCount = ((count != null) ? count : 1);
80+
81+
// Add 1 more to exclude item at reference position
82+
advanceCount++;
83+
11884
if (position.getOffset() >= advanceCount) {
11985
position = position.advanceBy(-advanceCount);
12086
}
@@ -124,7 +90,7 @@ private static ScrollSubrange initFromOffsetPosition(
12490
}
12591
}
12692

127-
return new ScrollSubrange(position, count, true, null);
93+
return new ScrollSubrange(position, count, true);
12894
}
12995

13096
private static ScrollSubrange initFromKeysetPosition(
@@ -133,7 +99,7 @@ private static ScrollSubrange initFromKeysetPosition(
13399
if (!forward) {
134100
position = position.backward();
135101
}
136-
return new ScrollSubrange(position, count, forward, null);
102+
return new ScrollSubrange(position, count, forward);
137103
}
138104

139105
}

spring-graphql/src/main/java/org/springframework/graphql/data/query/WindowConnectionAdapter.java

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,6 @@
3535
public final class WindowConnectionAdapter
3636
extends ConnectionAdapterSupport<ScrollPosition> implements ConnectionAdapter {
3737

38-
private static final long ZERO_OFFSET_ADJUSTMENT =
39-
OffsetScrollPosition.positionFunction(0).apply(0).getOffset();
40-
4138

4239
public WindowConnectionAdapter(CursorStrategy<ScrollPosition> strategy) {
4340
super(strategy);
@@ -59,10 +56,13 @@ public <T> Collection<T> getContent(Object container) {
5956
public boolean hasPrevious(Object container) {
6057
Window<?> window = window(container);
6158
if (!window.isEmpty()) {
62-
ScrollPosition position = positionAt(window, 0);
59+
ScrollPosition position = window.positionAt(0);
6360
if (position instanceof KeysetScrollPosition keysetPosition) {
6461
return (keysetPosition.scrollsBackward() && window.hasNext());
6562
}
63+
else if (position instanceof OffsetScrollPosition offsetPosition) {
64+
return (offsetPosition.getOffset() != 0);
65+
}
6666
else {
6767
return !position.isInitial();
6868
}
@@ -74,7 +74,7 @@ public boolean hasPrevious(Object container) {
7474
public boolean hasNext(Object container) {
7575
Window<?> window = window(container);
7676
if (!window.isEmpty()) {
77-
ScrollPosition pos = positionAt(window, 0);
77+
ScrollPosition pos = window.positionAt(0);
7878
if (pos instanceof KeysetScrollPosition keysetPos) {
7979
return (keysetPos.scrollsForward() && window.hasNext());
8080
}
@@ -87,22 +87,10 @@ public boolean hasNext(Object container) {
8787

8888
@Override
8989
public String cursorAt(Object container, int index) {
90-
ScrollPosition position = positionAt(window(container), index);
90+
ScrollPosition position = window(container).positionAt(index);
9191
return getCursorStrategy().toCursor(position);
9292
}
9393

94-
private ScrollPosition positionAt(Window<?> window, int index) {
95-
ScrollPosition position = window.positionAt(index);
96-
97-
// Workaround for OffsetScrollPosition#positionFunction adding 1 to the actual offset:
98-
// See https://github.com/spring-projects/spring-data-commons/issues/3070
99-
if (ZERO_OFFSET_ADJUSTMENT > 0 && position instanceof OffsetScrollPosition offsetPos) {
100-
position = offsetPos.advanceBy(-ZERO_OFFSET_ADJUSTMENT);
101-
}
102-
103-
return position;
104-
}
105-
10694
@SuppressWarnings("unchecked")
10795
private <T> Window<T> window(Object container) {
10896
return (Window<T>) container;

spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingPaginationTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ private static class BookController {
112112
@QueryMapping
113113
public Window<Book> books(ScrollSubrange subrange) {
114114
int offset = (int) ((OffsetScrollPosition) subrange.position().orElse(ScrollPosition.offset())).getOffset();
115-
int count = subrange.count().orElse(5);
116-
List<Book> books = BookSource.books().subList(offset, offset + count);
115+
offset++; // data stores treat offset as exclusive
116+
List<Book> books = BookSource.books().subList(offset, offset + subrange.count().orElse(5));
117117
return Window.from(books, OffsetScrollPosition.positionFunction(offset));
118118
}
119119

spring-graphql/src/test/java/org/springframework/graphql/data/query/RepositoryUtilsTests.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@
2222
import graphql.schema.DataFetchingEnvironment;
2323
import graphql.schema.DataFetchingEnvironmentImpl;
2424
import org.junit.jupiter.api.Test;
25-
import org.testcontainers.shaded.org.checkerframework.checker.nullness.qual.Nullable;
2625

2726
import org.springframework.data.domain.OffsetScrollPosition;
2827
import org.springframework.data.domain.ScrollPosition;
2928
import org.springframework.graphql.data.pagination.CursorStrategy;
29+
import org.springframework.lang.Nullable;
3030

3131
import static org.assertj.core.api.Assertions.assertThat;
3232

@@ -46,7 +46,7 @@ void forward() {
4646
DataFetchingEnvironment env = environment(Map.of("first", count, "after", cursorStrategy.toCursor(pos)));
4747
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy);
4848

49-
assertSubrange(true, count, pos.advanceBy(1), range);
49+
assertSubrange(true, count, pos, range);
5050
}
5151

5252
@Test
@@ -56,7 +56,7 @@ void backward() {
5656
DataFetchingEnvironment env = environment(Map.of("last", count, "before", cursorStrategy.toCursor(pos)));
5757
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy);
5858

59-
assertSubrange(true, count, ScrollPosition.offset(5), range);
59+
assertSubrange(true, count, ScrollPosition.offset(4), range);
6060
}
6161

6262
@Test
@@ -74,7 +74,7 @@ void forwardWithPositionOnly() {
7474
DataFetchingEnvironment env = environment(Map.of("after", cursorStrategy.toCursor(pos)));
7575
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy);
7676

77-
assertSubrange(true, null, pos.advanceBy(1), range);
77+
assertSubrange(true, null, pos, range);
7878
}
7979

8080
@Test
@@ -92,7 +92,7 @@ void backwardWithPositionOnly() {
9292
DataFetchingEnvironment env = environment(Map.of("before", cursorStrategy.toCursor(pos)));
9393
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy);
9494

95-
assertSubrange(true, null, pos.advanceBy(-1), range);
95+
assertSubrange(true, null, pos.advanceBy(-2), range);
9696
}
9797

9898
@Test

spring-graphql/src/test/java/org/springframework/graphql/data/query/ScrollSubrangeTests.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ void offsetForward() {
4141
int count = 10;
4242
ScrollSubrange subrange = ScrollSubrange.create(ScrollPosition.offset(30), count, true);
4343

44-
assertThat(getOffset(subrange)).isEqualTo(31);
44+
assertThat(getOffset(subrange)).isEqualTo(30);
4545
assertThat(subrange.count().orElse(0)).isEqualTo(count);
4646
assertThat(subrange.forward()).isTrue();
4747
}
@@ -51,7 +51,7 @@ void offsetBackward() {
5151
int count = 10;
5252
ScrollSubrange subrange = ScrollSubrange.create(ScrollPosition.offset(30), count, false);
5353

54-
assertThat(getOffset(subrange)).isEqualTo(20);
54+
assertThat(getOffset(subrange)).isEqualTo(19);
5555
assertThat(subrange.count().orElse(0)).isEqualTo(count);
5656
assertThat(subrange.forward()).isTrue();
5757
}
@@ -103,7 +103,7 @@ void nullInput() {
103103
void offsetBackwardWithInsufficientCount() {
104104
ScrollSubrange subrange = ScrollSubrange.create(ScrollPosition.offset(5), 10, false);
105105

106-
assertThat(getOffset(subrange)).isEqualTo(0);
106+
assertThat(subrange.position().get().isInitial()).isTrue();
107107
assertThat(subrange.count().getAsInt()).isEqualTo(5);
108108
assertThat(subrange.forward()).isTrue();
109109
}
@@ -112,7 +112,7 @@ void offsetBackwardWithInsufficientCount() {
112112
void offsetBackwardFromInitialOffset() {
113113
ScrollSubrange subrange = ScrollSubrange.create(ScrollPosition.offset(0), 10, false);
114114

115-
assertThat(getOffset(subrange)).isEqualTo(0);
115+
assertThat(subrange.position().get().isInitial()).isTrue();
116116
assertThat(subrange.count().getAsInt()).isEqualTo(0);
117117
assertThat(subrange.forward()).isTrue();
118118
}
@@ -121,7 +121,7 @@ void offsetBackwardFromInitialOffset() {
121121
void offsetBackwardWithNullCount() {
122122
ScrollSubrange subrange = ScrollSubrange.create(ScrollPosition.offset(30), null, false);
123123

124-
assertThat(getOffset(subrange)).isEqualTo(29);
124+
assertThat(getOffset(subrange)).isEqualTo(28);
125125
assertThat(subrange.count()).isNotPresent();
126126
assertThat(subrange.forward()).isTrue();
127127
}

0 commit comments

Comments
 (0)