diff --git a/src/main/asciidoc/repositories-paging-sorting.adoc b/src/main/asciidoc/repositories-paging-sorting.adoc index e58176a0e..b4b1f9632 100644 --- a/src/main/asciidoc/repositories-paging-sorting.adoc +++ b/src/main/asciidoc/repositories-paging-sorting.adoc @@ -14,7 +14,7 @@ Page findByLastname(String lastname, Pageable pageable); Slice findByLastname(String lastname, Pageable pageable); -Scroll findTop10ByLastname(String lastname, ScrollPosition position, Sort sort); +Window findTop10ByLastname(String lastname, ScrollPosition position, Sort sort); List findByLastname(String lastname, Sort sort); diff --git a/src/main/java/org/springframework/data/domain/KeysetScrollPosition.java b/src/main/java/org/springframework/data/domain/KeysetScrollPosition.java index 81b3058e2..0729bc9e8 100644 --- a/src/main/java/org/springframework/data/domain/KeysetScrollPosition.java +++ b/src/main/java/org/springframework/data/domain/KeysetScrollPosition.java @@ -18,27 +18,33 @@ package org.springframework.data.domain; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** - * A {@link ScrollPosition} based on the last seen keyset. Keyset scrolling must be associated with a {@link Sort - * well-defined sort} to be able to extract the keyset when resuming scrolling within the sorted result set. + * A {@link ScrollPosition} based on the last seen key set. Keyset scrolling must be associated with a {@link Sort + * well-defined sort} to be able to extract the key set when resuming scrolling within the sorted result set. * * @author Mark Paluch + * @author Oliver Drotbohm * @since 3.1 */ public final class KeysetScrollPosition implements ScrollPosition { - private static final KeysetScrollPosition initial = new KeysetScrollPosition(Collections.emptyMap(), - Direction.Forward); + private static final KeysetScrollPosition INITIAL = new KeysetScrollPosition(Collections.emptyMap(), + Direction.FORWARD); private final Map keys; - private final Direction direction; private KeysetScrollPosition(Map keys, Direction direction) { + + Assert.notNull(keys, "Keys must not be null"); + Assert.notNull(direction, "Direction must not be null"); + this.keys = keys; this.direction = direction; } @@ -46,44 +52,27 @@ public final class KeysetScrollPosition implements ScrollPosition { /** * Creates a new initial {@link KeysetScrollPosition} to start scrolling using keyset-queries. * - * @return a new initial {@link KeysetScrollPosition} to start scrolling using keyset-queries. - */ - public static KeysetScrollPosition initial() { - return initial; - } - - /** - * Creates a new {@link KeysetScrollPosition} from a keyset. - * - * @param keys must not be {@literal null}. - * @return a new {@link KeysetScrollPosition} for the given keyset. + * @return will never be {@literal null}. */ - public static KeysetScrollPosition of(Map keys) { - return of(keys, Direction.Forward); + static KeysetScrollPosition initial() { + return INITIAL; } /** - * Creates a new {@link KeysetScrollPosition} from a keyset and {@link Direction}. + * Creates a new {@link KeysetScrollPosition} from a key set and {@link Direction}. * * @param keys must not be {@literal null}. * @param direction must not be {@literal null}. - * @return a new {@link KeysetScrollPosition} for the given keyset and {@link Direction}. + * @return will never be {@literal null}. */ - public static KeysetScrollPosition of(Map keys, Direction direction) { + static KeysetScrollPosition of(Map keys, Direction direction) { Assert.notNull(keys, "Keys must not be null"); Assert.notNull(direction, "Direction must not be null"); - if (keys.isEmpty()) { - return initial(); - } - - return new KeysetScrollPosition(Collections.unmodifiableMap(new LinkedHashMap<>(keys)), direction); - } - - @Override - public boolean isInitial() { - return keys.isEmpty(); + return keys.isEmpty() + ? initial() + : new KeysetScrollPosition(Collections.unmodifiableMap(new LinkedHashMap<>(keys)), direction); } /** @@ -100,45 +89,78 @@ public final class KeysetScrollPosition implements ScrollPosition { return direction; } - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - KeysetScrollPosition that = (KeysetScrollPosition) o; - return ObjectUtils.nullSafeEquals(keys, that.keys) && direction == that.direction; + /** + * Returns whether the current {@link KeysetScrollPosition} scrolls forward. + * + * @return whether the current {@link KeysetScrollPosition} scrolls forward. + */ + public boolean scrollsForward() { + return direction == Direction.FORWARD; } - @Override - public int hashCode() { + /** + * Returns whether the current {@link KeysetScrollPosition} scrolls backward. + * + * @return whether the current {@link KeysetScrollPosition} scrolls backward. + */ + public boolean scrollsBackward() { + return direction == Direction.BACKWARD; + } - int result = 17; + /** + * Returns a {@link KeysetScrollPosition} based on the same keyset and scrolling forward. + * + * @return will never be {@literal null}. + */ + public KeysetScrollPosition forward() { + return direction == Direction.FORWARD ? this : new KeysetScrollPosition(keys, Direction.FORWARD); + } - result += 31 * ObjectUtils.nullSafeHashCode(keys); - result += 31 * ObjectUtils.nullSafeHashCode(direction); + /** + * Returns a {@link KeysetScrollPosition} based on the same keyset and scrolling backward. + * + * @return will never be {@literal null}. + */ + public KeysetScrollPosition backward() { + return direction == Direction.BACKWARD ? this : new KeysetScrollPosition(keys, Direction.BACKWARD); + } - return result; + /** + * Returns a new {@link KeysetScrollPosition} with the direction reversed. + * + * @return will never be {@literal null}. + */ + public KeysetScrollPosition reverse() { + return new KeysetScrollPosition(keys, direction.reverse()); } @Override - public String toString() { - return String.format("KeysetScrollPosition [%s, %s]", direction, keys); + public boolean isInitial() { + return keys.isEmpty(); } - /** - * Keyset scrolling direction. - */ - public enum Direction { + @Override + public boolean equals(@Nullable Object o) { + + if (this == o) { + return true; + } - /** - * Forward (default) direction to scroll from the beginning of the results to their end. - */ - Forward, + if (!(o instanceof KeysetScrollPosition that)) { + return false; + } + + return ObjectUtils.nullSafeEquals(keys, that.keys) // + && direction == that.direction; + } - /** - * Backward direction to scroll from the end of the results to their beginning. - */ - Backward; + @Override + public int hashCode() { + return Objects.hash(keys, direction); + } + + @Override + public String toString() { + return String.format("KeysetScrollPosition [%s, %s]", direction, keys); } } diff --git a/src/main/java/org/springframework/data/domain/OffsetScrollPosition.java b/src/main/java/org/springframework/data/domain/OffsetScrollPosition.java index 07000c7b8..99551ea74 100644 --- a/src/main/java/org/springframework/data/domain/OffsetScrollPosition.java +++ b/src/main/java/org/springframework/data/domain/OffsetScrollPosition.java @@ -15,49 +15,54 @@ */ package org.springframework.data.domain; +import java.util.Objects; import java.util.function.IntFunction; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; /** * A {@link ScrollPosition} based on the offsets within query results. * * @author Mark Paluch + * @author Oliver Drotbohm * @since 3.1 */ public final class OffsetScrollPosition implements ScrollPosition { - private static final OffsetScrollPosition initial = new OffsetScrollPosition(0); + private static final OffsetScrollPosition INITIAL = new OffsetScrollPosition(0); private final long offset; + /** + * Creates a new {@link OffsetScrollPosition} for the given non-negative offset. + * + * @param offset must be greater or equal to zero. + */ private OffsetScrollPosition(long offset) { + + Assert.isTrue(offset >= 0, "Offset must not be negative"); + this.offset = offset; } /** * Creates a new initial {@link OffsetScrollPosition} to start scrolling using offset/limit. * - * @return a new initial {@link OffsetScrollPosition} to start scrolling using offset/limit. + * @return will never be {@literal null}. */ - public static OffsetScrollPosition initial() { - return initial; + static OffsetScrollPosition initial() { + return INITIAL; } /** * Creates a new {@link OffsetScrollPosition} from an {@code offset}. * - * @param offset - * @return a new {@link OffsetScrollPosition} with the given {@code offset}. + * @param offset the non-negative offset to start at. + * @return will never be {@literal null}. */ - public static OffsetScrollPosition of(long offset) { - - if (offset == 0) { - return initial(); - } - - return new OffsetScrollPosition(offset); + static OffsetScrollPosition of(long offset) { + return offset == 0 ? initial() : new OffsetScrollPosition(offset); } /** @@ -73,36 +78,51 @@ public final class OffsetScrollPosition implements ScrollPosition { return startOffset == 0 ? OffsetPositionFunction.ZERO : new OffsetPositionFunction(startOffset); } - @Override - public boolean isInitial() { - return offset == 0; - } - /** + * The zero or positive offset. + * * @return the offset. */ public long getOffset() { return offset; } + /** + * Returns a new {@link OffsetScrollPosition} that has been advanced by the given value. Negative deltas will be + * constrained so that the new offset is at least zero. + * + * @param delta the value to advance the current offset by. + * @return will never be {@literal null}. + */ + public OffsetScrollPosition advanceBy(long delta) { + + var value = offset + delta; + + return new OffsetScrollPosition(value < 0 ? 0 : value); + } + + @Override + public boolean isInitial() { + return offset == 0; + } + @Override - public boolean equals(Object o) { - if (this == o) + public boolean equals(@Nullable Object o) { + + if (this == o) { return true; - if (o == null || getClass() != o.getClass()) + } + + if (!(o instanceof OffsetScrollPosition that)) { return false; - OffsetScrollPosition that = (OffsetScrollPosition) o; + } + return offset == that.offset; } @Override public int hashCode() { - - int result = 17; - - result += 31 * ObjectUtils.nullSafeHashCode(offset); - - return result; + return Objects.hash(offset); } @Override diff --git a/src/main/java/org/springframework/data/domain/Pageable.java b/src/main/java/org/springframework/data/domain/Pageable.java index 4486394de..ff830f92f 100644 --- a/src/main/java/org/springframework/data/domain/Pageable.java +++ b/src/main/java/org/springframework/data/domain/Pageable.java @@ -174,7 +174,6 @@ public interface Pageable { throw new IllegalStateException("Cannot create OffsetScrollPosition from an unpaged instance"); } - return OffsetScrollPosition.of(getOffset()); + return ScrollPosition.offset(getOffset()); } - } diff --git a/src/main/java/org/springframework/data/domain/ScrollPosition.java b/src/main/java/org/springframework/data/domain/ScrollPosition.java index 0eeba7530..76eb5c101 100644 --- a/src/main/java/org/springframework/data/domain/ScrollPosition.java +++ b/src/main/java/org/springframework/data/domain/ScrollPosition.java @@ -15,6 +15,8 @@ */ package org.springframework.data.domain; +import java.util.Map; + /** * Interface to specify a position within a total query result. Scroll positions are used to start scrolling from the * beginning of a query result or to resume scrolling from a given position within the query result. @@ -30,4 +32,83 @@ public interface ScrollPosition { * @return */ boolean isInitial(); + + /** + * Creates a new initial {@link ScrollPosition} to start scrolling using keyset-queries. + * + * @return will never be {@literal null}. + */ + public static KeysetScrollPosition keyset() { + return KeysetScrollPosition.initial(); + } + + /** + * Creates a new initial {@link ScrollPosition} to start scrolling using offset / limit. + * + * @return will never be {@literal null}. + */ + public static OffsetScrollPosition offset() { + return OffsetScrollPosition.initial(); + } + + /** + * Creates a new {@link ScrollPosition} from an {@code offset}. + * + * @param offset + * @return a new {@link OffsetScrollPosition} with the given {@code offset}. + */ + public static OffsetScrollPosition offset(long offset) { + return OffsetScrollPosition.of(offset); + } + + /** + * Creates a new {@link ScrollPosition} from a key set scrolling forward. + * + * @param keys must not be {@literal null}. + * @return will never be {@literal null}. + */ + public static KeysetScrollPosition forward(Map keys) { + return of(keys, Direction.FORWARD); + } + + /** + * Creates a new {@link ScrollPosition} from a key set scrolling backward. + * + * @param keys must not be {@literal null}. + * @return will never be {@literal null}. + */ + public static KeysetScrollPosition backward(Map keys) { + return of(keys, Direction.BACKWARD); + } + + /** + * Creates a new {@link ScrollPosition} from a key set and {@link Direction}. + * + * @param keys must not be {@literal null}. + * @param direction must not be {@literal null}. + * @return will never be {@literal null}. + */ + public static KeysetScrollPosition of(Map keys, Direction direction) { + return KeysetScrollPosition.of(keys, direction); + } + + /** + * Keyset scrolling direction. + */ + public enum Direction { + + /** + * Forward (default) direction to scroll from the beginning of the results to their end. + */ + FORWARD, + + /** + * Backward direction to scroll from the end of the results to their beginning. + */ + BACKWARD; + + Direction reverse() { + return this == FORWARD ? BACKWARD : FORWARD; + } + } } diff --git a/src/main/java/org/springframework/data/domain/WindowIterator.java b/src/main/java/org/springframework/data/support/WindowIterator.java similarity index 95% rename from src/main/java/org/springframework/data/domain/WindowIterator.java rename to src/main/java/org/springframework/data/support/WindowIterator.java index 923dd44cf..210bc4bcb 100644 --- a/src/main/java/org/springframework/data/domain/WindowIterator.java +++ b/src/main/java/org/springframework/data/support/WindowIterator.java @@ -13,12 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.domain; +package org.springframework.data.support; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.function.Function; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Window; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -28,7 +30,7 @@ import org.springframework.util.Assert; * *
  * WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10By…("spring", position))
- *   .startingAt(OffsetScrollPosition.initial());
+ *   .startingAt(ScrollPosition.offset());
  *
  * while (users.hasNext()) {
  *   User u = users.next();
diff --git a/src/test/java/org/springframework/data/domain/KeysetScrollPositionUnitTests.java b/src/test/java/org/springframework/data/domain/KeysetScrollPositionUnitTests.java
deleted file mode 100644
index 6fe28d459..000000000
--- a/src/test/java/org/springframework/data/domain/KeysetScrollPositionUnitTests.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright 2023 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.springframework.data.domain;
-
-import static org.assertj.core.api.Assertions.*;
-
-import java.util.Collections;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.data.domain.KeysetScrollPosition.Direction;
-
-/**
- * Unit tests for {@link KeysetScrollPosition}.
- *
- * @author Mark Paluch
- */
-class KeysetScrollPositionUnitTests {
-
-	@Test // GH-2151
-	void equalsAndHashCode() {
-
-		KeysetScrollPosition foo1 = KeysetScrollPosition.of(Collections.singletonMap("k", "v"));
-		KeysetScrollPosition foo2 = KeysetScrollPosition.of(Collections.singletonMap("k", "v"));
-		KeysetScrollPosition bar = KeysetScrollPosition.of(Collections.singletonMap("k", "v"), Direction.Backward);
-
-		assertThat(foo1).isEqualTo(foo2).hasSameClassAs(foo2);
-		assertThat(foo1).isNotEqualTo(bar).doesNotHaveSameHashCodeAs(bar);
-	}
-
-}
diff --git a/src/test/java/org/springframework/data/domain/OffsetScrollPositionUnitTests.java b/src/test/java/org/springframework/data/domain/OffsetScrollPositionUnitTests.java
deleted file mode 100644
index a04f57752..000000000
--- a/src/test/java/org/springframework/data/domain/OffsetScrollPositionUnitTests.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 2023 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.springframework.data.domain;
-
-import static org.assertj.core.api.Assertions.*;
-import static org.springframework.data.domain.OffsetScrollPosition.*;
-
-import org.junit.jupiter.api.Test;
-
-/**
- * Unit tests for {@link OffsetScrollPosition}.
- *
- * @author Mark Paluch
- */
-class OffsetScrollPositionUnitTests {
-
-	@Test // GH-2151
-	void equalsAndHashCode() {
-
-		OffsetScrollPosition foo1 = OffsetScrollPosition.of(1);
-		OffsetScrollPosition foo2 = OffsetScrollPosition.of(1);
-		OffsetScrollPosition bar = OffsetScrollPosition.of(2);
-
-		assertThat(foo1).isEqualTo(foo2).hasSameClassAs(foo2);
-		assertThat(foo1).isNotEqualTo(bar).doesNotHaveSameHashCodeAs(bar);
-	}
-
-	@Test // GH-2151
-	void shouldCreateCorrectIndexPosition() {
-
-		assertThat(positionFunction(0).apply(0)).isEqualTo(OffsetScrollPosition.of(1));
-		assertThat(positionFunction(0).apply(1)).isEqualTo(OffsetScrollPosition.of(2));
-
-		assertThat(positionFunction(100).apply(0)).isEqualTo(OffsetScrollPosition.of(101));
-		assertThat(positionFunction(100).apply(1)).isEqualTo(OffsetScrollPosition.of(102));
-	}
-}
diff --git a/src/test/java/org/springframework/data/domain/PageRequestUnitTests.java b/src/test/java/org/springframework/data/domain/PageRequestUnitTests.java
index fe0bfe31f..dbbaae5e6 100755
--- a/src/test/java/org/springframework/data/domain/PageRequestUnitTests.java
+++ b/src/test/java/org/springframework/data/domain/PageRequestUnitTests.java
@@ -72,6 +72,6 @@ class PageRequestUnitTests extends AbstractPageRequestUnitTests {
 
 		PageRequest request = PageRequest.of(1, 10);
 
-		assertThat(request.toScrollPosition()).isEqualTo(OffsetScrollPosition.of(10));
+		assertThat(request.toScrollPosition()).isEqualTo(ScrollPosition.offset(10));
 	}
 }
diff --git a/src/test/java/org/springframework/data/domain/ScrollPositionUnitTests.java b/src/test/java/org/springframework/data/domain/ScrollPositionUnitTests.java
new file mode 100644
index 000000000..f4ad67b32
--- /dev/null
+++ b/src/test/java/org/springframework/data/domain/ScrollPositionUnitTests.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.domain;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.springframework.data.domain.OffsetScrollPosition.*;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.data.domain.ScrollPosition.Direction;
+
+/**
+ * Unit tests for {@link KeysetScrollPosition}.
+ *
+ * @author Mark Paluch
+ * @author Oliver Drotbohm
+ */
+class ScrollPositionUnitTests {
+
+	private static final Map KEYS = Collections.singletonMap("k", "v");
+
+	@Test // GH-2151
+	void equalsAndHashCodeForKeysets() {
+
+		ScrollPosition foo1 = ScrollPosition.forward(KEYS);
+		ScrollPosition foo2 = ScrollPosition.forward(KEYS);
+		ScrollPosition bar = ScrollPosition.backward(KEYS);
+
+		assertThat(foo1).isEqualTo(foo2).hasSameClassAs(foo2);
+		assertThat(foo1).isNotEqualTo(bar).doesNotHaveSameHashCodeAs(bar);
+	}
+
+	@Test // GH-2151
+	void equalsAndHashCodeForOffsets() {
+
+		ScrollPosition foo1 = ScrollPosition.offset(1);
+		ScrollPosition foo2 = ScrollPosition.offset(1);
+		ScrollPosition bar = ScrollPosition.offset(2);
+
+		assertThat(foo1).isEqualTo(foo2).hasSameClassAs(foo2);
+		assertThat(foo1).isNotEqualTo(bar).doesNotHaveSameHashCodeAs(bar);
+	}
+
+	@Test // GH-2151
+	void shouldCreateCorrectIndexPosition() {
+
+		assertThat(positionFunction(0).apply(0)).isEqualTo(ScrollPosition.offset(1));
+		assertThat(positionFunction(0).apply(1)).isEqualTo(ScrollPosition.offset(2));
+
+		assertThat(positionFunction(100).apply(0)).isEqualTo(ScrollPosition.offset(101));
+		assertThat(positionFunction(100).apply(1)).isEqualTo(ScrollPosition.offset(102));
+	}
+
+	@Test // GH-2151
+	void rejectsNegativeOffset() {
+		assertThatIllegalArgumentException().isThrownBy(() -> ScrollPosition.offset(-1));
+	}
+
+	@Test // GH-2151
+	void advanceOffsetBelowZeroCapsAtZero() {
+
+		OffsetScrollPosition offset = ScrollPosition.offset(5);
+
+		assertThat(offset.getOffset()).isEqualTo(5);
+		assertThat(offset.advanceBy(-10)).isEqualTo(ScrollPosition.offset(0));
+	}
+
+	@Test // GH-2824
+	void setsUpForwardScrolling() {
+
+		KeysetScrollPosition position = ScrollPosition.forward(KEYS);
+
+		assertThat(position.getKeys()).isEqualTo(KEYS);
+		assertThat(position.getDirection()).isEqualTo(Direction.FORWARD);
+		assertThat(position.scrollsForward()).isTrue();
+		assertThat(position.scrollsBackward()).isFalse();
+
+		KeysetScrollPosition backward = position.backward();
+
+		assertThat(backward.getKeys()).isEqualTo(KEYS);
+		assertThat(backward.getDirection()).isEqualTo(Direction.BACKWARD);
+		assertThat(backward.scrollsForward()).isFalse();
+		assertThat(backward.scrollsBackward()).isTrue();
+
+		assertThat(position.reverse()).isEqualTo(backward);
+	}
+
+	@Test // GH-2824
+	void setsUpBackwardScrolling() {
+
+		KeysetScrollPosition position = ScrollPosition.backward(KEYS);
+
+		assertThat(position.getKeys()).isEqualTo(KEYS);
+		assertThat(position.getDirection()).isEqualTo(Direction.BACKWARD);
+		assertThat(position.scrollsForward()).isFalse();
+		assertThat(position.scrollsBackward()).isTrue();
+
+		KeysetScrollPosition forward = position.forward();
+
+		assertThat(forward.getKeys()).isEqualTo(KEYS);
+		assertThat(forward.getDirection()).isEqualTo(Direction.FORWARD);
+		assertThat(forward.scrollsForward()).isTrue();
+		assertThat(forward.scrollsBackward()).isFalse();
+
+		assertThat(position.reverse()).isEqualTo(forward);
+	}
+
+	@Test // GH-2824
+	void initialOffsetPosition() {
+
+		OffsetScrollPosition position = ScrollPosition.offset();
+
+		assertThat(position.isInitial()).isTrue();
+		assertThat(position.getOffset()).isEqualTo(0);
+	}
+
+	@Test // GH-2824
+	void initialKeysetPosition() {
+
+		KeysetScrollPosition keyset = ScrollPosition.keyset();
+
+		assertThat(keyset.isInitial()).isTrue();
+		assertThat(keyset.scrollsForward()).isTrue();
+	}
+}
diff --git a/src/test/java/org/springframework/data/domain/WindowIteratorUnitTests.java b/src/test/java/org/springframework/data/domain/WindowIteratorUnitTests.java
index 9b27d15fe..1d0dff970 100644
--- a/src/test/java/org/springframework/data/domain/WindowIteratorUnitTests.java
+++ b/src/test/java/org/springframework/data/domain/WindowIteratorUnitTests.java
@@ -30,6 +30,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.mockito.junit.jupiter.MockitoSettings;
 import org.mockito.quality.Strictness;
+import org.springframework.data.support.WindowIterator;
 
 /**
  * Unit tests for {@link WindowIterator}.
@@ -45,20 +46,20 @@ class WindowIteratorUnitTests {
 	void loadsDataOnNext() {
 
 		Function> fkt = mock(Function.class);
-		WindowIterator iterator = WindowIterator.of(fkt).startingAt(OffsetScrollPosition.initial());
+		WindowIterator iterator = WindowIterator.of(fkt).startingAt(ScrollPosition.offset());
 		verifyNoInteractions(fkt);
 
-		when(fkt.apply(any())).thenReturn(Window.from(Collections.emptyList(), value -> OffsetScrollPosition.initial()));
+		when(fkt.apply(any())).thenReturn(Window.from(Collections.emptyList(), value -> ScrollPosition.offset()));
 
 		iterator.hasNext();
-		verify(fkt).apply(OffsetScrollPosition.initial());
+		verify(fkt).apply(ScrollPosition.offset());
 	}
 
 	@Test // GH-2151
 	void hasNextReturnsFalseIfNoDataAvailable() {
 
-		Window window = Window.from(Collections.emptyList(), value -> OffsetScrollPosition.initial());
-		WindowIterator iterator = WindowIterator.of(it -> window).startingAt(OffsetScrollPosition.initial());
+		Window window = Window.from(Collections.emptyList(), value -> ScrollPosition.offset());
+		WindowIterator iterator = WindowIterator.of(it -> window).startingAt(ScrollPosition.offset());
 
 		assertThat(iterator.hasNext()).isFalse();
 	}
@@ -66,8 +67,8 @@ class WindowIteratorUnitTests {
 	@Test // GH-2151
 	void nextThrowsExceptionIfNoElementAvailable() {
 
-		Window window = Window.from(Collections.emptyList(), value -> OffsetScrollPosition.initial());
-		WindowIterator iterator = WindowIterator.of(it -> window).startingAt(OffsetScrollPosition.initial());
+		Window window = Window.from(Collections.emptyList(), value -> ScrollPosition.offset());
+		WindowIterator iterator = WindowIterator.of(it -> window).startingAt(ScrollPosition.offset());
 
 		assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(iterator::next);
 	}
@@ -75,8 +76,8 @@ class WindowIteratorUnitTests {
 	@Test // GH-2151
 	void hasNextReturnsTrueIfDataAvailableButOnlyOnePage() {
 
-		Window window = Window.from(List.of("a", "b"), value -> OffsetScrollPosition.initial());
-		WindowIterator iterator = WindowIterator.of(it -> window).startingAt(OffsetScrollPosition.initial());
+		Window window = Window.from(List.of("a", "b"), value -> ScrollPosition.offset());
+		WindowIterator iterator = WindowIterator.of(it -> window).startingAt(ScrollPosition.offset());
 
 		assertThat(iterator.hasNext()).isTrue();
 		assertThat(iterator.next()).isEqualTo("a");
@@ -89,14 +90,14 @@ class WindowIteratorUnitTests {
 	@Test // GH-2151
 	void hasNextReturnsCorrectlyIfNextPageIsEmpty() {
 
-		Window window = Window.from(List.of("a", "b"), value -> OffsetScrollPosition.initial());
+		Window window = Window.from(List.of("a", "b"), value -> ScrollPosition.offset());
 		WindowIterator iterator = WindowIterator.of(it -> {
 			if (it.isInitial()) {
 				return window;
 			}
 
 			return Window.from(Collections.emptyList(), OffsetScrollPosition::of, false);
-		}).startingAt(OffsetScrollPosition.initial());
+		}).startingAt(ScrollPosition.offset());
 
 		assertThat(iterator.hasNext()).isTrue();
 		assertThat(iterator.next()).isEqualTo("a");
@@ -109,15 +110,15 @@ class WindowIteratorUnitTests {
 	@Test // GH-2151
 	void allowsToIterateAllWindows() {
 
-		Window window1 = Window.from(List.of("a", "b"), OffsetScrollPosition::of, true);
-		Window window2 = Window.from(List.of("c", "d"), value -> OffsetScrollPosition.of(2 + value));
+		Window window1 = Window.from(List.of("a", "b"), ScrollPosition::offset, true);
+		Window window2 = Window.from(List.of("c", "d"), value -> ScrollPosition.offset(2 + value));
 		WindowIterator iterator = WindowIterator.of(it -> {
 			if (it.isInitial()) {
 				return window1;
 			}
 
 			return window2;
-		}).startingAt(OffsetScrollPosition.initial());
+		}).startingAt(ScrollPosition.offset());
 
 		List capturedResult = new ArrayList<>(4);
 		while (iterator.hasNext()) {
diff --git a/src/test/java/org/springframework/data/domain/WindowUnitTests.java b/src/test/java/org/springframework/data/domain/WindowUnitTests.java
index 210fc9327..a66b43eb2 100644
--- a/src/test/java/org/springframework/data/domain/WindowUnitTests.java
+++ b/src/test/java/org/springframework/data/domain/WindowUnitTests.java
@@ -58,13 +58,13 @@ class WindowUnitTests {
 
 		Window window = Window.from(List.of(1, 2, 3), OffsetScrollPosition.positionFunction(0));
 
-		assertThat(window.positionAt(0)).isEqualTo(OffsetScrollPosition.of(1));
-		assertThat(window.positionAt(window.size() - 1)).isEqualTo(OffsetScrollPosition.of(3));
+		assertThat(window.positionAt(0)).isEqualTo(ScrollPosition.offset(1));
+		assertThat(window.positionAt(window.size() - 1)).isEqualTo(ScrollPosition.offset(3));
 
 		// by index
-		assertThat(window.positionAt(1)).isEqualTo(OffsetScrollPosition.of(2));
+		assertThat(window.positionAt(1)).isEqualTo(ScrollPosition.offset(2));
 
 		// by object
-		assertThat(window.positionAt(Integer.valueOf(1))).isEqualTo(OffsetScrollPosition.of(1));
+		assertThat(window.positionAt(Integer.valueOf(1))).isEqualTo(ScrollPosition.offset(1));
 	}
 }
diff --git a/src/test/java/org/springframework/data/repository/query/ParametersParameterAccessorUnitTests.java b/src/test/java/org/springframework/data/repository/query/ParametersParameterAccessorUnitTests.java
index 68c7b5168..ec82d1397 100755
--- a/src/test/java/org/springframework/data/repository/query/ParametersParameterAccessorUnitTests.java
+++ b/src/test/java/org/springframework/data/repository/query/ParametersParameterAccessorUnitTests.java
@@ -19,7 +19,6 @@ import static org.assertj.core.api.Assertions.*;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.springframework.data.domain.OffsetScrollPosition;
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.ScrollPosition;
@@ -84,7 +83,7 @@ class ParametersParameterAccessorUnitTests {
 		var method = Sample.class.getMethod("method", ScrollPosition.class, String.class);
 		var parameters = new DefaultParameters(method);
 
-		var accessor = new ParametersParameterAccessor(parameters, new Object[] { OffsetScrollPosition.of(1), "Foo" });
+		var accessor = new ParametersParameterAccessor(parameters, new Object[] { ScrollPosition.offset(1), "Foo" });
 
 		assertThat(accessor).hasSize(1);
 		assertThat(accessor.getBindableValue(0)).isEqualTo("Foo");
diff --git a/src/test/java/org/springframework/data/repository/query/SimpleParameterAccessorUnitTests.java b/src/test/java/org/springframework/data/repository/query/SimpleParameterAccessorUnitTests.java
index f17f7264a..af63565f7 100755
--- a/src/test/java/org/springframework/data/repository/query/SimpleParameterAccessorUnitTests.java
+++ b/src/test/java/org/springframework/data/repository/query/SimpleParameterAccessorUnitTests.java
@@ -19,7 +19,6 @@ import static org.assertj.core.api.Assertions.*;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.springframework.data.domain.OffsetScrollPosition;
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.ScrollPosition;
@@ -61,7 +60,8 @@ class SimpleParameterAccessorUnitTests {
 
 	@Test
 	void rejectsTooLittleNumberOfArguments() {
-		assertThatIllegalArgumentException().isThrownBy(() -> new ParametersParameterAccessor(parameters, new Object[0]));
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> new ParametersParameterAccessor(parameters, new Object[0]));
 	}
 
 	@Test
@@ -82,7 +82,7 @@ class SimpleParameterAccessorUnitTests {
 	@Test // GH-2151
 	void returnsScrollPositionIfAvailable() {
 
-		var cursorRequest = OffsetScrollPosition.of(1);
+		var cursorRequest = ScrollPosition.offset(1);
 		ParameterAccessor accessor = new ParametersParameterAccessor(cursorRequestParameters,
 				new Object[] { cursorRequest });
 
@@ -103,7 +103,8 @@ class SimpleParameterAccessorUnitTests {
 	void returnsPageableIfAvailable() {
 
 		Pageable pageable = PageRequest.of(0, 10);
-		ParameterAccessor accessor = new ParametersParameterAccessor(pageableParameters, new Object[] { "test", pageable });
+		ParameterAccessor accessor = new ParametersParameterAccessor(pageableParameters,
+				new Object[] { "test", pageable });
 
 		assertThat(accessor.getPageable()).isEqualTo(pageable);
 		assertThat(accessor.getSort().isSorted()).isFalse();
@@ -114,7 +115,8 @@ class SimpleParameterAccessorUnitTests {
 
 		var sort = Sort.by("foo");
 		Pageable pageable = PageRequest.of(0, 10, sort);
-		ParameterAccessor accessor = new ParametersParameterAccessor(pageableParameters, new Object[] { "test", pageable });
+		ParameterAccessor accessor = new ParametersParameterAccessor(pageableParameters,
+				new Object[] { "test", pageable });
 
 		assertThat(accessor.getPageable()).isEqualTo(pageable);
 		assertThat(accessor.getSort()).isEqualTo(sort);