diff --git a/pom.xml b/pom.xml
index 13bf0377b4..a8b89c32b4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.dataspring-data-commons
- 3.1.0-SNAPSHOT
+ 3.1.0-GH-2151-SNAPSHOTSpring Data CoreCore Spring concepts underpinning every Spring Data module.
diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc
index 6cc84d4cfb..14fd59748d 100644
--- a/src/main/asciidoc/index.adoc
+++ b/src/main/asciidoc/index.adoc
@@ -2,6 +2,7 @@
Oliver Gierke; Thomas Darimont; Christoph Strobl; Mark Pollack; Thomas Risberg; Mark Paluch; Jay Bryant
:revnumber: {version}
:revdate: {localdate}
+:feature-scroll: true
ifdef::backend-epub3[:front-cover-image: image:epub-cover.png[Front Cover,1050,1600]]
(C) 2008-2022 The original authors.
diff --git a/src/main/asciidoc/repositories-paging-sorting.adoc b/src/main/asciidoc/repositories-paging-sorting.adoc
new file mode 100644
index 0000000000..8bf940f474
--- /dev/null
+++ b/src/main/asciidoc/repositories-paging-sorting.adoc
@@ -0,0 +1,136 @@
+[[repositories.special-parameters]]
+=== Special parameter handling
+
+To handle parameters in your query, define method parameters as already seen in the preceding examples.
+Besides that, the infrastructure recognizes certain specific types like `Pageable` and `Sort`, to apply pagination and sorting to your queries dynamically.
+The following example demonstrates these features:
+
+ifdef::feature-scroll[]
+.Using `Pageable`, `Slice`, `ScrollPosition`, and `Sort` in query methods
+====
+[source,java]
+----
+Page findByLastname(String lastname, Pageable pageable);
+
+Slice findByLastname(String lastname, Pageable pageable);
+
+Scroll findTop10ByLastname(String lastname, ScrollPosition position, Sort sort);
+
+List findByLastname(String lastname, Sort sort);
+
+List findByLastname(String lastname, Pageable pageable);
+----
+====
+endif::[]
+
+ifndef::feature-scroll[]
+.Using `Pageable`, `Slice`, and `Sort` in query methods
+====
+[source,java]
+----
+Page findByLastname(String lastname, Pageable pageable);
+
+Slice findByLastname(String lastname, Pageable pageable);
+
+List findByLastname(String lastname, Sort sort);
+
+List findByLastname(String lastname, Pageable pageable);
+----
+====
+endif::[]
+
+IMPORTANT: APIs taking `Sort` and `Pageable` expect non-`null` values to be handed into methods.
+If you do not want to apply any sorting or pagination, use `Sort.unsorted()` and `Pageable.unpaged()`.
+
+The first method lets you pass an `org.springframework.data.domain.Pageable` instance to the query method to dynamically add paging to your statically defined query.
+A `Page` knows about the total number of elements and pages available.
+It does so by the infrastructure triggering a count query to calculate the overall number.
+As this might be expensive (depending on the store used), you can instead return a `Slice`.
+A `Slice` knows only about whether a next `Slice` is available, which might be sufficient when walking through a larger result set.
+
+Sorting options are handled through the `Pageable` instance, too.
+If you need only sorting, add an `org.springframework.data.domain.Sort` parameter to your method.
+As you can see, returning a `List` is also possible.
+In this case, the additional metadata required to build the actual `Page` instance is not created (which, in turn, means that the additional count query that would have been necessary is not issued).
+Rather, it restricts the query to look up only the given range of entities.
+
+NOTE: To find out how many pages you get for an entire query, you have to trigger an additional count query.
+By default, this query is derived from the query you actually trigger.
+
+[[repositories.paging-and-sorting]]
+==== Paging and Sorting
+
+You can define simple sorting expressions by using property names.
+You can concatenate expressions to collect multiple criteria into one expression.
+
+.Defining sort expressions
+====
+[source,java]
+----
+Sort sort = Sort.by("firstname").ascending()
+ .and(Sort.by("lastname").descending());
+----
+====
+
+For a more type-safe way to define sort expressions, start with the type for which to define the sort expression and use method references to define the properties on which to sort.
+
+.Defining sort expressions by using the type-safe API
+====
+[source,java]
+----
+TypedSort person = Sort.sort(Person.class);
+
+Sort sort = person.by(Person::getFirstname).ascending()
+ .and(person.by(Person::getLastname).descending());
+----
+====
+
+NOTE: `TypedSort.by(…)` makes use of runtime proxies by (typically) using CGlib, which may interfere with native image compilation when using tools such as Graal VM Native.
+
+If your store implementation supports Querydsl, you can also use the generated metamodel types to define sort expressions:
+
+.Defining sort expressions by using the Querydsl API
+====
+[source,java]
+----
+QSort sort = QSort.by(QPerson.firstname.asc())
+ .and(QSort.by(QPerson.lastname.desc()));
+----
+====
+
+ifdef::feature-scroll[]
+include::repositories-scrolling.adoc[]
+endif::[]
+
+[[repositories.limit-query-result]]
+=== Limiting Query Results
+
+You can limit the results of query methods by using the `first` or `top` keywords, which you can use interchangeably.
+You can append an optional numeric value to `top` or `first` to specify the maximum result size to be returned.
+If the number is left out, a result size of 1 is assumed.
+The following example shows how to limit the query size:
+
+.Limiting the result size of a query with `Top` and `First`
+====
+[source,java]
+----
+User findFirstByOrderByLastnameAsc();
+
+User findTopByOrderByAgeDesc();
+
+Page queryFirst10ByLastname(String lastname, Pageable pageable);
+
+Slice findTop3ByLastname(String lastname, Pageable pageable);
+
+List findFirst10ByLastname(String lastname, Sort sort);
+
+List findTop10ByLastname(String lastname, Pageable pageable);
+----
+====
+
+The limiting expressions also support the `Distinct` keyword for datastores that support distinct queries.
+Also, for the queries that limit the result set to one instance, wrapping the result into with the `Optional` keyword is supported.
+
+If pagination or slicing is applied to a limiting query pagination (and the calculation of the number of available pages), it is applied within the limited result.
+
+NOTE: Limiting the results in combination with dynamic sorting by using a `Sort` parameter lets you express query methods for the 'K' smallest as well as for the 'K' biggest elements.
diff --git a/src/main/asciidoc/repositories-scrolling.adoc b/src/main/asciidoc/repositories-scrolling.adoc
new file mode 100644
index 0000000000..5f90fadfc3
--- /dev/null
+++ b/src/main/asciidoc/repositories-scrolling.adoc
@@ -0,0 +1,102 @@
+[[repositories.scrolling]]
+==== Scrolling
+
+Scrolling is a more fine-grained approach to iterate through larger results set chunks.
+Scrolling consists of a stable sort, a scroll type (Offset- or Keyset-based scrolling) and result limiting.
+You can define simple sorting expressions by using property names and define static result limiting using the <> through query derivation.
+You can concatenate expressions to collect multiple criteria into one expression.
+
+Scroll queries return a `Window` that allows obtaining the scroll position to resume to obtain the next `Window` until your application has consumed the entire query result.
+Similar to consuming a Java `Iterator>` by obtaining the next batch of results, query result scrolling lets you access the a `ScrollPosition` through `Window.positionAt(...)`.
+
+[source,java]
+----
+Window users = repository.findFirst10ByLastnameOrderByFirstname("Doe", OffsetScrollPosition.initial());
+do {
+
+ for (User u : users) {
+ // consume the user
+ }
+
+ // obtain the next Scroll
+ users = repository.findFirst10ByLastnameOrderByFirstname("Doe", users.positionAt(users.size() - 1));
+} while (!users.isEmpty() && users.hasNext());
+----
+
+`WindowIterator` provides a utility to simplify scrolling across ``Window``s by removing the need to check for the presence of a next `Window` and applying the `ScrollPosition`.
+
+[source,java]
+----
+WindowIterator users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
+ .startingAt(OffsetScrollPosition.initial());
+
+while (users.hasNext()) {
+ User u = users.next();
+ // consume the user
+}
+----
+
+[[repositories.scrolling.offset]]
+===== Scrolling using Offset
+
+Offset scrolling uses similar to pagination, an Offset counter to skip a number of results and let the data source only return results beginning at the given Offset.
+This simple mechanism avoids large results being sent to the client application.
+However, most databases require materializing the full query result before your server can return the results.
+
+.Using `OffsetScrollPosition` with Repository Query Methods
+====
+[source,java]
+----
+interface UserRepository extends Repository {
+
+ Window findFirst10ByLastnameOrderByFirstname(String lastname, OffsetScrollPosition position);
+}
+
+WindowIterator users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
+ .startingAt(OffsetScrollPosition.initial()); <1>
+----
+
+<1> Start from the initial offset at position `0`.
+====
+
+[[repositories.scrolling.keyset]]
+===== Scrolling using Keyset-Filtering
+
+Offset-based requires most databases require materializing the entire result before your server can return the results.
+So while the client only sees the portion of the requested results, your server needs to build the full result, which causes additional load.
+
+Keyset-Filtering approaches result subset retrieval by leveraging built-in capabilities of your database aiming to reduce the computation and I/O requirements for individual queries.
+This approach maintains a set of keys to resume scrolling by passing keys into the query, effectively amending your filter criteria.
+
+The core idea of Keyset-Filtering is to start retrieving results using a stable sorting order.
+Once you want to scroll to the next chunk, you obtain a `ScrollPosition` that is used to reconstruct the position within the sorted result.
+The `ScrollPosition` captures the keyset of the last entity within the current `Window`.
+To run the query, reconstruction rewrites the criteria clause to include all sort fields and the primary key so that the database can leverage potential indexes to run the query.
+The database needs only constructing a much smaller result from the given keyset position without the need to fully materialize a large result and then skipping results until reaching a particular offset.
+
+[WARNING]
+====
+Keyset-Filtering requires the keyset properties (those used for sorting) to be non-nullable.
+This limitation applies due to the store specific `null` value handling of comparison operators as well as the need to run queries against an indexed source.
+Keyset-Filtering on nullable properties will lead to unexpected results.
+====
+
+.Using `KeysetScrollPosition` with Repository Query Methods
+====
+[source,java]
+----
+interface UserRepository extends Repository {
+
+ Window findFirst10ByLastnameOrderByFirstname(String lastname, KeysetScrollPosition position);
+}
+
+WindowIterator users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
+ .startingAt(KeysetScrollPosition.initial()); <1>
+----
+<1> Start at the very beginning and do not apply additional filtering.
+====
+
+Keyset-Filtering works best when your database contains an index that matches the sort fields, hence a static sort works well.
+Scroll queries applying Keyset-Filtering require to the properties used in the sort order to be returned by the query, and these must be mapped in the returned entity.
+
+You can use interface and DTO projections, however make sure to include all properties that you've sorted by to avoid keyset extraction failures.
diff --git a/src/main/asciidoc/repositories.adoc b/src/main/asciidoc/repositories.adoc
index c60fd8d691..2b2a520cd2 100644
--- a/src/main/asciidoc/repositories.adoc
+++ b/src/main/asciidoc/repositories.adoc
@@ -92,6 +92,10 @@ Page users = repository.findAll(PageRequest.of(1, 20));
----
====
+ifdef::feature-scroll[]
+In addition to pagination, scrolling provides a more fine-grained access to iterate through chunks of larger result sets.
+endif::[]
+
In addition to query methods, query derivation for both count and delete queries is available.
The following list shows the interface definition for a derived count query:
@@ -517,118 +521,7 @@ List findByAddress_ZipCode(ZipCode zipCode);
Because we treat the underscore character as a reserved character, we strongly advise following standard Java naming conventions (that is, not using underscores in property names but using camel case instead).
-[[repositories.special-parameters]]
-=== Special parameter handling
-
-To handle parameters in your query, define method parameters as already seen in the preceding examples.
-Besides that, the infrastructure recognizes certain specific types like `Pageable` and `Sort`, to apply pagination and sorting to your queries dynamically.
-The following example demonstrates these features:
-
-.Using `Pageable`, `Slice`, and `Sort` in query methods
-====
-[source,java]
-----
-Page findByLastname(String lastname, Pageable pageable);
-
-Slice findByLastname(String lastname, Pageable pageable);
-
-List findByLastname(String lastname, Sort sort);
-
-List findByLastname(String lastname, Pageable pageable);
-----
-====
-
-IMPORTANT: APIs taking `Sort` and `Pageable` expect non-`null` values to be handed into methods.
-If you do not want to apply any sorting or pagination, use `Sort.unsorted()` and `Pageable.unpaged()`.
-
-The first method lets you pass an `org.springframework.data.domain.Pageable` instance to the query method to dynamically add paging to your statically defined query.
-A `Page` knows about the total number of elements and pages available.
-It does so by the infrastructure triggering a count query to calculate the overall number.
-As this might be expensive (depending on the store used), you can instead return a `Slice`.
-A `Slice` knows only about whether a next `Slice` is available, which might be sufficient when walking through a larger result set.
-
-Sorting options are handled through the `Pageable` instance, too.
-If you need only sorting, add an `org.springframework.data.domain.Sort` parameter to your method.
-As you can see, returning a `List` is also possible.
-In this case, the additional metadata required to build the actual `Page` instance is not created (which, in turn, means that the additional count query that would have been necessary is not issued).
-Rather, it restricts the query to look up only the given range of entities.
-
-NOTE: To find out how many pages you get for an entire query, you have to trigger an additional count query.
-By default, this query is derived from the query you actually trigger.
-
-[[repositories.paging-and-sorting]]
-==== Paging and Sorting
-
-You can define simple sorting expressions by using property names.
-You can concatenate expressions to collect multiple criteria into one expression.
-
-.Defining sort expressions
-====
-[source,java]
-----
-Sort sort = Sort.by("firstname").ascending()
- .and(Sort.by("lastname").descending());
-----
-====
-
-For a more type-safe way to define sort expressions, start with the type for which to define the sort expression and use method references to define the properties on which to sort.
-
-.Defining sort expressions by using the type-safe API
-====
-[source,java]
-----
-TypedSort person = Sort.sort(Person.class);
-
-Sort sort = person.by(Person::getFirstname).ascending()
- .and(person.by(Person::getLastname).descending());
-----
-====
-
-NOTE: `TypedSort.by(…)` makes use of runtime proxies by (typically) using CGlib, which may interfere with native image compilation when using tools such as Graal VM Native.
-
-If your store implementation supports Querydsl, you can also use the generated metamodel types to define sort expressions:
-
-.Defining sort expressions by using the Querydsl API
-====
-[source,java]
-----
-QSort sort = QSort.by(QPerson.firstname.asc())
- .and(QSort.by(QPerson.lastname.desc()));
-----
-====
-
-[[repositories.limit-query-result]]
-=== Limiting Query Results
-
-You can limit the results of query methods by using the `first` or `top` keywords, which you can use interchangeably.
-You can append an optional numeric value to `top` or `first` to specify the maximum result size to be returned.
-If the number is left out, a result size of 1 is assumed.
-The following example shows how to limit the query size:
-
-.Limiting the result size of a query with `Top` and `First`
-====
-[source,java]
-----
-User findFirstByOrderByLastnameAsc();
-
-User findTopByOrderByAgeDesc();
-
-Page queryFirst10ByLastname(String lastname, Pageable pageable);
-
-Slice findTop3ByLastname(String lastname, Pageable pageable);
-
-List findFirst10ByLastname(String lastname, Sort sort);
-
-List findTop10ByLastname(String lastname, Pageable pageable);
-----
-====
-
-The limiting expressions also support the `Distinct` keyword for datastores that support distinct queries.
-Also, for the queries that limit the result set to one instance, wrapping the result into with the `Optional` keyword is supported.
-
-If pagination or slicing is applied to a limiting query pagination (and the calculation of the number of available pages), it is applied within the limited result.
-
-NOTE: Limiting the results in combination with dynamic sorting by using a `Sort` parameter lets you express query methods for the 'K' smallest as well as for the 'K' biggest elements.
+include::repositories-paging-sorting.adoc[]
[[repositories.collections-and-iterables]]
=== Repository Methods Returning Collections or Iterables
diff --git a/src/main/java/org/springframework/data/domain/KeysetScrollPosition.java b/src/main/java/org/springframework/data/domain/KeysetScrollPosition.java
new file mode 100644
index 0000000000..81b3058e21
--- /dev/null
+++ b/src/main/java/org/springframework/data/domain/KeysetScrollPosition.java
@@ -0,0 +1,144 @@
+/*
+ * 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 java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+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.
+ *
+ * @author Mark Paluch
+ * @since 3.1
+ */
+public final class KeysetScrollPosition implements ScrollPosition {
+
+ 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) {
+ this.keys = keys;
+ this.direction = direction;
+ }
+
+ /**
+ * 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.
+ */
+ public static KeysetScrollPosition of(Map keys) {
+ return of(keys, Direction.Forward);
+ }
+
+ /**
+ * Creates a new {@link KeysetScrollPosition} from a keyset 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}.
+ */
+ public 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 the keyset.
+ */
+ public Map getKeys() {
+ return keys;
+ }
+
+ /**
+ * @return the scroll direction.
+ */
+ public Direction getDirection() {
+ 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;
+ }
+
+ @Override
+ public int hashCode() {
+
+ int result = 17;
+
+ result += 31 * ObjectUtils.nullSafeHashCode(keys);
+ result += 31 * ObjectUtils.nullSafeHashCode(direction);
+
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("KeysetScrollPosition [%s, %s]", direction, keys);
+ }
+
+ /**
+ * 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;
+ }
+}
diff --git a/src/main/java/org/springframework/data/domain/OffsetScrollPosition.java b/src/main/java/org/springframework/data/domain/OffsetScrollPosition.java
new file mode 100644
index 0000000000..07000c7b8a
--- /dev/null
+++ b/src/main/java/org/springframework/data/domain/OffsetScrollPosition.java
@@ -0,0 +1,127 @@
+/*
+ * 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 java.util.function.IntFunction;
+
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+
+/**
+ * A {@link ScrollPosition} based on the offsets within query results.
+ *
+ * @author Mark Paluch
+ * @since 3.1
+ */
+public final class OffsetScrollPosition implements ScrollPosition {
+
+ private static final OffsetScrollPosition initial = new OffsetScrollPosition(0);
+
+ private final long offset;
+
+ private OffsetScrollPosition(long offset) {
+ 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.
+ */
+ public 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}.
+ */
+ public static OffsetScrollPosition of(long offset) {
+
+ if (offset == 0) {
+ return initial();
+ }
+
+ return new OffsetScrollPosition(offset);
+ }
+
+ /**
+ * Returns the {@link IntFunction position function} to calculate.
+ *
+ * @param startOffset the start offset to be used. Must not be negative.
+ * @return the offset-based position function.
+ */
+ public static IntFunction positionFunction(long startOffset) {
+
+ Assert.isTrue(startOffset >= 0, "Start offset must not be negative");
+
+ return startOffset == 0 ? OffsetPositionFunction.ZERO : new OffsetPositionFunction(startOffset);
+ }
+
+ @Override
+ public boolean isInitial() {
+ return offset == 0;
+ }
+
+ /**
+ * @return the offset.
+ */
+ public long getOffset() {
+ return offset;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ OffsetScrollPosition that = (OffsetScrollPosition) o;
+ return offset == that.offset;
+ }
+
+ @Override
+ public int hashCode() {
+
+ int result = 17;
+
+ result += 31 * ObjectUtils.nullSafeHashCode(offset);
+
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("OffsetScrollPosition [%s]", offset);
+ }
+
+ private record OffsetPositionFunction(long startOffset) implements IntFunction {
+
+ static final OffsetPositionFunction ZERO = new OffsetPositionFunction(0);
+
+ @Override
+ public OffsetScrollPosition apply(int offset) {
+
+ if (offset < 0) {
+ throw new IndexOutOfBoundsException(offset);
+ }
+
+ return of(startOffset + offset + 1);
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/data/domain/Pageable.java b/src/main/java/org/springframework/data/domain/Pageable.java
index 7fd873f721..4486394de6 100644
--- a/src/main/java/org/springframework/data/domain/Pageable.java
+++ b/src/main/java/org/springframework/data/domain/Pageable.java
@@ -161,4 +161,20 @@ default Optional toOptional() {
return isUnpaged() ? Optional.empty() : Optional.of(this);
}
+ /**
+ * Returns an {@link OffsetScrollPosition} from this pageable if the page request {@link #isPaged() is paged}.
+ *
+ * @return
+ * @throws IllegalStateException if the request is {@link #isUnpaged()}
+ * @since 3.1
+ */
+ default OffsetScrollPosition toScrollPosition() {
+
+ if (isUnpaged()) {
+ throw new IllegalStateException("Cannot create OffsetScrollPosition from an unpaged instance");
+ }
+
+ return OffsetScrollPosition.of(getOffset());
+ }
+
}
diff --git a/src/main/java/org/springframework/data/domain/ScrollPosition.java b/src/main/java/org/springframework/data/domain/ScrollPosition.java
new file mode 100644
index 0000000000..0eeba7530a
--- /dev/null
+++ b/src/main/java/org/springframework/data/domain/ScrollPosition.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+/**
+ * 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.
+ *
+ * @author Mark Paluch
+ * @since 3.1
+ */
+public interface ScrollPosition {
+
+ /**
+ * Returns whether the current scroll position is the initial one.
+ *
+ * @return
+ */
+ boolean isInitial();
+}
diff --git a/src/main/java/org/springframework/data/domain/Window.java b/src/main/java/org/springframework/data/domain/Window.java
new file mode 100644
index 0000000000..536e58f08d
--- /dev/null
+++ b/src/main/java/org/springframework/data/domain/Window.java
@@ -0,0 +1,154 @@
+/*
+ * 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 java.util.List;
+import java.util.NoSuchElementException;
+import java.util.function.Function;
+import java.util.function.IntFunction;
+
+import org.springframework.data.util.Streamable;
+
+/**
+ * A set of data consumed from an underlying query result. A {@link Window} is similar to {@link Slice} in the sense
+ * that it contains a subset of the actual query results for easier scrolling across large result sets. The window is
+ * less opinionated about the actual data retrieval, whether the query has used index/offset, keyset-based pagination or
+ * cursor resume tokens.
+ *
+ * @author Mark Paluch
+ * @author Christoph Strobl
+ * @since 3.1
+ * @see ScrollPosition
+ */
+public interface Window extends Streamable {
+
+ /**
+ * Construct a {@link Window}.
+ *
+ * @param items the list of data.
+ * @param positionFunction the list of data.
+ * @return the {@link Window}.
+ * @param
+ */
+ static Window from(List items, IntFunction extends ScrollPosition> positionFunction) {
+ return new WindowImpl<>(items, positionFunction, false);
+ }
+
+ /**
+ * Construct a {@link Window}.
+ *
+ * @param items the list of data.
+ * @param positionFunction the list of data.
+ * @param hasNext
+ * @return the {@link Window}.
+ * @param
+ */
+ static Window from(List items, IntFunction extends ScrollPosition> positionFunction, boolean hasNext) {
+ return new WindowImpl<>(items, positionFunction, hasNext);
+ }
+
+ /**
+ * Returns the number of elements in this window.
+ *
+ * @return the number of elements in this window.
+ */
+ int size();
+
+ /**
+ * Returns {@code true} if this window contains no elements.
+ *
+ * @return {@code true} if this window contains no elements
+ */
+ boolean isEmpty();
+
+ /**
+ * Returns the windows content as {@link List}.
+ *
+ * @return
+ */
+ List getContent();
+
+ /**
+ * Returns whether the current window is the last one.
+ *
+ * @return
+ */
+ default boolean isLast() {
+ return !hasNext();
+ }
+
+ /**
+ * Returns if there is a next window.
+ *
+ * @return if there is a next window.
+ */
+ boolean hasNext();
+
+ /**
+ * Returns whether the underlying scroll mechanism can provide a {@link ScrollPosition} at {@code index}.
+ *
+ * @param index
+ * @return {@code true} if a {@link ScrollPosition} can be created; {@code false} otherwise.
+ * @see #positionAt(int)
+ */
+ default boolean hasPosition(int index) {
+ try {
+ return positionAt(index) != null;
+ } catch (IllegalStateException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Returns the {@link ScrollPosition} at {@code index}.
+ *
+ * @param index
+ * @return
+ * @throws IndexOutOfBoundsException if the index is out of range ({@code index < 0 || index >= size()}).
+ * @throws IllegalStateException if the underlying scroll mechanism cannot provide a scroll position for the given
+ * object.
+ */
+ ScrollPosition positionAt(int index);
+
+ /**
+ * Returns the {@link ScrollPosition} for {@code object}.
+ *
+ * @param object
+ * @return
+ * @throws NoSuchElementException if the object is not part of the result.
+ * @throws IllegalStateException if the underlying scroll mechanism cannot provide a scroll position for the given
+ * object.
+ */
+ default ScrollPosition positionAt(T object) {
+
+ int index = getContent().indexOf(object);
+
+ if (index == -1) {
+ throw new NoSuchElementException();
+ }
+
+ return positionAt(index);
+ }
+
+ /**
+ * Returns a new {@link Window} with the content of the current one mapped by the given {@code converter}.
+ *
+ * @param converter must not be {@literal null}.
+ * @return a new {@link Window} with the content of the current one mapped by the given {@code converter}.
+ */
+ Window map(Function super T, ? extends U> converter);
+
+}
diff --git a/src/main/java/org/springframework/data/domain/WindowImpl.java b/src/main/java/org/springframework/data/domain/WindowImpl.java
new file mode 100644
index 0000000000..2272447f4e
--- /dev/null
+++ b/src/main/java/org/springframework/data/domain/WindowImpl.java
@@ -0,0 +1,119 @@
+/*
+ * 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 java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.IntFunction;
+import java.util.stream.Collectors;
+
+import org.jetbrains.annotations.NotNull;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+
+/**
+ * Default {@link Window} implementation.
+ *
+ * @author Mark Paluch
+ * @since 3.1
+ */
+class WindowImpl implements Window {
+
+ private final List items;
+ private final IntFunction extends ScrollPosition> positionFunction;
+
+ private final boolean hasNext;
+
+ WindowImpl(List items, IntFunction extends ScrollPosition> positionFunction, boolean hasNext) {
+
+ Assert.notNull(items, "List of items must not be null");
+ Assert.notNull(positionFunction, "Position function must not be null");
+
+ this.items = items;
+ this.positionFunction = positionFunction;
+ this.hasNext = hasNext;
+ }
+
+ @Override
+ public int size() {
+ return items.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return items.isEmpty();
+ }
+
+ @Override
+ public List getContent() {
+ return items;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return hasNext;
+ }
+
+ @Override
+ public ScrollPosition positionAt(int index) {
+
+ if (index < 0 || index >= size()) {
+ throw new IndexOutOfBoundsException(index);
+ }
+
+ return positionFunction.apply(index);
+ }
+
+ @Override
+ public Window map(Function super T, ? extends U> converter) {
+
+ Assert.notNull(converter, "Function must not be null");
+
+ return new WindowImpl<>(stream().map(converter).collect(Collectors.toList()), positionFunction, hasNext);
+ }
+
+ @NotNull
+ @Override
+ public Iterator iterator() {
+ return items.iterator();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ WindowImpl> that = (WindowImpl>) o;
+ return ObjectUtils.nullSafeEquals(items, that.items)
+ && ObjectUtils.nullSafeEquals(positionFunction, that.positionFunction)
+ && ObjectUtils.nullSafeEquals(hasNext, that.hasNext);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = ObjectUtils.nullSafeHashCode(items);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(positionFunction);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(hasNext);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Scroll " + items;
+ }
+}
diff --git a/src/main/java/org/springframework/data/domain/WindowIterator.java b/src/main/java/org/springframework/data/domain/WindowIterator.java
new file mode 100644
index 0000000000..923dd44cf7
--- /dev/null
+++ b/src/main/java/org/springframework/data/domain/WindowIterator.java
@@ -0,0 +1,145 @@
+/*
+ * 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 java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.function.Function;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Iterator} over multiple {@link Window Windows} obtained via a {@link Function window function}, that keeps
+ * track of the current {@link ScrollPosition} allowing scrolling across all result elements.
+ *
+ *
+ * WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10By…("spring", position))
+ * .startingAt(OffsetScrollPosition.initial());
+ *
+ * while (users.hasNext()) {
+ * User u = users.next();
+ * // consume user
+ * }
+ *
+ *
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ * @since 3.1
+ */
+public class WindowIterator implements Iterator {
+
+ private final Function> windowFunction;
+
+ private ScrollPosition currentPosition;
+
+ private @Nullable Window currentWindow;
+
+ private @Nullable Iterator currentIterator;
+
+ /**
+ * Entrypoint to create a new {@link WindowIterator} for the given windowFunction.
+ *
+ * @param windowFunction must not be {@literal null}.
+ * @param
+ * @return new instance of {@link WindowIteratorBuilder}.
+ */
+ public static WindowIteratorBuilder of(Function> windowFunction) {
+ return new WindowIteratorBuilder<>(windowFunction);
+ }
+
+ WindowIterator(Function> windowFunction, ScrollPosition position) {
+
+ this.windowFunction = windowFunction;
+ this.currentPosition = position;
+ }
+
+ @Override
+ public boolean hasNext() {
+
+ // use while loop instead of recursion to fetch the next window.
+ do {
+ if (currentWindow == null) {
+ currentWindow = windowFunction.apply(currentPosition);
+ }
+
+ if (currentIterator == null) {
+ if (currentWindow != null) {
+ currentIterator = currentWindow.iterator();
+ }
+ }
+
+ if (currentIterator != null) {
+
+ if (currentIterator.hasNext()) {
+ return true;
+ }
+
+ if (currentWindow != null && currentWindow.hasNext()) {
+
+ currentPosition = currentWindow.positionAt(currentWindow.size() - 1);
+ currentIterator = null;
+ currentWindow = null;
+ continue;
+ }
+ }
+
+ return false;
+ } while (true);
+ }
+
+ @Override
+ public T next() {
+
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+
+ return currentIterator.next();
+ }
+
+ /**
+ * Builder API to construct a {@link WindowIterator}.
+ *
+ * @param
+ * @author Christoph Strobl
+ * @since 3.1
+ */
+ public static class WindowIteratorBuilder {
+
+ private final Function> windowFunction;
+
+ WindowIteratorBuilder(Function> windowFunction) {
+
+ Assert.notNull(windowFunction, "WindowFunction must not be null");
+
+ this.windowFunction = windowFunction;
+ }
+
+ /**
+ * Create a {@link WindowIterator} given {@link ScrollPosition}.
+ *
+ * @param position
+ * @return
+ */
+ public WindowIterator startingAt(ScrollPosition position) {
+
+ Assert.notNull(position, "ScrollPosition must not be null");
+
+ return new WindowIterator<>(windowFunction, position);
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/data/repository/query/FluentQuery.java b/src/main/java/org/springframework/data/repository/query/FluentQuery.java
index 909a0d7169..b36437dfbd 100644
--- a/src/main/java/org/springframework/data/repository/query/FluentQuery.java
+++ b/src/main/java/org/springframework/data/repository/query/FluentQuery.java
@@ -15,6 +15,7 @@
*/
package org.springframework.data.repository.query;
+import org.springframework.data.domain.Window;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -26,6 +27,7 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
@@ -44,10 +46,23 @@ public interface FluentQuery {
* @param sort the {@link Sort} specification to sort the results by, may be {@link Sort#unsorted()}, must not be
* {@literal null}.
* @return a new instance of {@link FluentQuery}.
- * @throws IllegalArgumentException if resultType is {@code null}.
+ * @throws IllegalArgumentException if {@code sort} is {@code null}.
*/
FluentQuery sortBy(Sort sort);
+ /**
+ * Define the query limit.
+ *
+ * @param limit the limit to apply to the query to limit results. Must not be negative.
+ * @return a new instance of {@link FluentQuery}.
+ * @throws IllegalArgumentException if {@code limit} is less than zero.
+ * @throws UnsupportedOperationException if not supported by the underlying implementation.
+ * @since 3.1
+ */
+ default FluentQuery limit(int limit) {
+ throw new UnsupportedOperationException("Limit not supported");
+ }
+
/**
* Define the target type the result should be mapped to. Skip this step if you are only interested in the original
* domain type.
@@ -55,7 +70,7 @@ public interface FluentQuery {
* @param resultType must not be {@code null}.
* @param result type.
* @return a new instance of {@link FluentQuery}.
- * @throws IllegalArgumentException if resultType is {@code null}.
+ * @throws IllegalArgumentException if {@code resultType} is {@code null}.
*/
FluentQuery as(Class resultType);
@@ -64,7 +79,7 @@ public interface FluentQuery {
*
* @param properties must not be {@code null}.
* @return a new instance of {@link FluentQuery}.
- * @throws IllegalArgumentException if fields is {@code null}.
+ * @throws IllegalArgumentException if {@code properties} is {@code null}.
*/
default FluentQuery project(String... properties) {
return project(Arrays.asList(properties));
@@ -75,7 +90,7 @@ default FluentQuery project(String... properties) {
*
* @param properties must not be {@code null}.
* @return a new instance of {@link FluentQuery}.
- * @throws IllegalArgumentException if fields is {@code null}.
+ * @throws IllegalArgumentException if {@code properties} is {@code null}.
*/
FluentQuery project(Collection properties);
@@ -90,6 +105,11 @@ interface FetchableFluentQuery extends FluentQuery {
@Override
FetchableFluentQuery sortBy(Sort sort);
+ @Override
+ default FetchableFluentQuery limit(int limit) {
+ throw new UnsupportedOperationException("Limit not supported");
+ }
+
@Override
FetchableFluentQuery as(Class resultType);
@@ -144,12 +164,27 @@ default Optional first() {
*/
List all();
+ /**
+ * Get all matching elements as {@link Window} to start result scrolling or resume scrolling at
+ * {@code scrollPosition}.
+ *
+ * @param scrollPosition must not be {@literal null}.
+ * @return
+ * @throws IllegalArgumentException if {@code scrollPosition} is {@literal null}.
+ * @throws UnsupportedOperationException if not supported by the underlying implementation.
+ * @since 3.1
+ */
+ default Window scroll(ScrollPosition scrollPosition) {
+ throw new UnsupportedOperationException("Scrolling not supported");
+ }
+
/**
* Get a page of matching elements for {@link Pageable}.
*
* @param pageable the pageable to request a paged result, can be {@link Pageable#unpaged()}, must not be
* {@literal null}. The given {@link Pageable} will override any previously specified {@link Sort sort} if
- * the {@link Sort} object is not {@link Sort#isUnsorted()}.
+ * the {@link Sort} object is not {@link Sort#isUnsorted()}. Any potentially specified {@link #limit(int)}
+ * will be overridden by {@link Pageable#getPageSize()}.
* @return
*/
Page page(Pageable pageable);
@@ -187,6 +222,11 @@ interface ReactiveFluentQuery extends FluentQuery {
@Override
ReactiveFluentQuery sortBy(Sort sort);
+ @Override
+ default ReactiveFluentQuery limit(int limit) {
+ throw new UnsupportedOperationException("Limit not supported");
+ }
+
@Override
ReactiveFluentQuery as(Class resultType);
@@ -220,12 +260,27 @@ default ReactiveFluentQuery project(String... properties) {
*/
Flux all();
+ /**
+ * Get all matching elements as {@link Window} to start result scrolling or resume scrolling at
+ * {@code scrollPosition}.
+ *
+ * @param scrollPosition must not be {@literal null}.
+ * @return
+ * @throws IllegalArgumentException if {@code scrollPosition} is {@literal null}.
+ * @throws UnsupportedOperationException if not supported by the underlying implementation.
+ * @since 3.1
+ */
+ default Mono> scroll(ScrollPosition scrollPosition) {
+ throw new UnsupportedOperationException("Scrolling not supported");
+ }
+
/**
* Get a page of matching elements for {@link Pageable}.
*
* @param pageable the pageable to request a paged result, can be {@link Pageable#unpaged()}, must not be
* {@literal null}. The given {@link Pageable} will override any previously specified {@link Sort sort} if
- * the {@link Sort} object is not {@link Sort#isUnsorted()}.
+ * the {@link Sort} object is not {@link Sort#isUnsorted()}. Any potentially specified {@link #limit(int)}
+ * will be overridden by {@link Pageable#getPageSize()}.
* @return
*/
Mono> page(Pageable pageable);
diff --git a/src/main/java/org/springframework/data/repository/query/Parameter.java b/src/main/java/org/springframework/data/repository/query/Parameter.java
index 304772e50c..c751db67c5 100644
--- a/src/main/java/org/springframework/data/repository/query/Parameter.java
+++ b/src/main/java/org/springframework/data/repository/query/Parameter.java
@@ -26,6 +26,7 @@
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.util.ClassUtils;
import org.springframework.data.repository.util.QueryExecutionConverters;
@@ -57,7 +58,7 @@ public class Parameter {
static {
- List> types = new ArrayList<>(Arrays.asList(Pageable.class, Sort.class));
+ List> types = new ArrayList<>(Arrays.asList(ScrollPosition.class, Pageable.class, Sort.class));
// consider Kotlin Coroutines Continuation a special parameter. That parameter is synthetic and should not get
// bound to any query.
@@ -192,6 +193,16 @@ public String toString() {
return format("%s:%s", isNamedParameter() ? getName() : "#" + getIndex(), getType().getName());
}
+ /**
+ * Returns whether the {@link Parameter} is a {@link ScrollPosition} parameter.
+ *
+ * @return
+ * @since 3.1
+ */
+ boolean isScrollPosition() {
+ return ScrollPosition.class.isAssignableFrom(getType());
+ }
+
/**
* Returns whether the {@link Parameter} is a {@link Pageable} parameter.
*
diff --git a/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java b/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java
index f5ccd7ec3d..369c781a56 100644
--- a/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java
+++ b/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java
@@ -18,6 +18,7 @@
import java.util.Iterator;
import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
@@ -29,6 +30,14 @@
*/
public interface ParameterAccessor extends Iterable