diff --git a/src/main/asciidoc/repositories-scrolling.adoc b/src/main/asciidoc/repositories-scrolling.adoc index 85c81f369..3e06ed779 100644 --- a/src/main/asciidoc/repositories-scrolling.adoc +++ b/src/main/asciidoc/repositories-scrolling.adoc @@ -6,8 +6,36 @@ Scrolling consists of a stable sort, a scroll type (Offset- or Keyset-based scro 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 `Scroll` that allows obtaining the scroll position to resume to obtain the next `Scroll` 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 next `ScrollPosition` through `Scroll.lastScrollPosition()`. +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.isLast()); +---- + +`WindowIterator` provides a specialized implementation that simplifies consumption of multiple `Window` instances 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()) { + users.next().forEach(user -> { + // consume the user + }); +} +---- [[repositories.scrolling.offset]] ===== Scrolling using Offset @@ -22,21 +50,13 @@ However, most databases require materializing the full query result before your ---- interface UserRepository extends Repository { - Scroll findFirst10ByLastnameOrderByFirstname(String lastname, OffsetScrollPosition position); + Window findFirst10ByLastnameOrderByFirstname(String lastname, OffsetScrollPosition position); } -Scroll users = repository.findFirst10ByLastnameOrderByFirstname("Doe", OffsetScrollPosition.initial()); - -do { - - for (User u : users) { - // consume the user - } - - // obtain the next Scroll - users = repository.findFirst10ByLastnameOrderByFirstname("Doe", users.lastScrollPosition()); -} while (!users.isEmpty() && !users.isLast()); +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]] @@ -50,31 +70,30 @@ This approach maintains a set of keys to resume scrolling by passing keys into t 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 `Scroll`. +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 properties 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 { - Scroll findFirst10ByLastnameOrderByFirstname(String lastname, KeysetScrollPosition position); + Window findFirst10ByLastnameOrderByFirstname(String lastname, KeysetScrollPosition position); } -Scroll users = repository.findFirst10ByLastnameOrderByFirstname("Doe", KeysetScrollPosition.initial()); - -do { - - for (User u : users) { - // consume the user - } - - // obtain the next Scroll - users = repository.findFirst10ByLastnameOrderByFirstname("Doe", users.lastScrollPosition()); -} while (!users.isEmpty() && !users.isLast()); +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. diff --git a/src/main/java/org/springframework/data/domain/Scroll.java b/src/main/java/org/springframework/data/domain/Window.java similarity index 51% rename from src/main/java/org/springframework/data/domain/Scroll.java rename to src/main/java/org/springframework/data/domain/Window.java index 82271fc67..d27e2249e 100644 --- a/src/main/java/org/springframework/data/domain/Scroll.java +++ b/src/main/java/org/springframework/data/domain/Window.java @@ -23,65 +23,66 @@ import java.util.function.IntFunction; import org.springframework.data.util.Streamable; /** - * A scroll of data consumed from an underlying query result. A scroll is similar to {@link Slice} in the sense that it - * contains a subset of the actual query results for easier consumption of large result sets. The scroll is less + * 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 consumption of 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 Scroll extends Streamable { +public interface Window extends Streamable { /** - * Construct a {@link Scroll}. + * Construct a {@link Window}. * * @param items the list of data. * @param positionFunction the list of data. - * @return the {@link Scroll}. + * @return the {@link Window}. * @param */ - static Scroll from(List items, IntFunction positionFunction) { - return new ScrollImpl<>(items, positionFunction, false); + static Window from(List items, IntFunction positionFunction) { + return new WindowImpl<>(items, positionFunction, false); } /** - * Construct a {@link Scroll}. + * Construct a {@link Window}. * * @param items the list of data. * @param positionFunction the list of data. * @param hasNext - * @return the {@link Scroll}. + * @return the {@link Window}. * @param */ - static Scroll from(List items, IntFunction positionFunction, boolean hasNext) { - return new ScrollImpl<>(items, positionFunction, hasNext); + static Window from(List items, IntFunction positionFunction, boolean hasNext) { + return new WindowImpl<>(items, positionFunction, hasNext); } /** - * Returns the number of elements in this scroll. + * Returns the number of elements in this window. * - * @return the number of elements in this scroll. + * @return the number of elements in this window. */ int size(); /** - * Returns {@code true} if this scroll contains no elements. + * Returns {@code true} if this window contains no elements. * - * @return {@code true} if this scroll contains no elements + * @return {@code true} if this window contains no elements */ boolean isEmpty(); /** - * Returns the scroll content as {@link List}. + * Returns the windows content as {@link List}. * * @return */ List getContent(); /** - * Returns whether the current scroll is the last one. + * Returns whether the current window is the last one. * * @return */ @@ -90,9 +91,9 @@ public interface Scroll extends Streamable { } /** - * Returns if there is a next scroll. + * Returns if there is a next window. * - * @return if there is a next scroll window. + * @return if there is a next window window. */ boolean hasNext(); @@ -123,45 +124,12 @@ public interface Scroll extends Streamable { return positionAt(index); } - // TODO: First and last seem to conflict with first/last scroll or first/last position of elements. - // these methods should rather express the position of the first element within this scroll and the scroll position - // to be used to get the next Scroll. /** - * Returns the first {@link ScrollPosition} or throw {@link NoSuchElementException} if the list is empty. - * - * @return the first {@link ScrollPosition}. - * @throws NoSuchElementException if this result is empty. - */ - default ScrollPosition firstPosition() { - - if (size() == 0) { - throw new NoSuchElementException(); - } - - return positionAt(0); - } - - /** - * Returns the last {@link ScrollPosition} or throw {@link NoSuchElementException} if the list is empty. - * - * @return the last {@link ScrollPosition}. - * @throws NoSuchElementException if this result is empty. - */ - default ScrollPosition lastPosition() { - - if (size() == 0) { - throw new NoSuchElementException(); - } - - return positionAt(size() - 1); - } - - /** - * Returns a new {@link Scroll} with the content of the current one mapped by the given {@code converter}. + * 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 Scroll} with the content of the current one mapped by the given {@code converter}. + * @return a new {@link Window} with the content of the current one mapped by the given {@code converter}. */ - Scroll map(Function converter); + Window map(Function converter); } diff --git a/src/main/java/org/springframework/data/domain/ScrollImpl.java b/src/main/java/org/springframework/data/domain/WindowImpl.java similarity index 89% rename from src/main/java/org/springframework/data/domain/ScrollImpl.java rename to src/main/java/org/springframework/data/domain/WindowImpl.java index 538849670..2272447f4 100644 --- a/src/main/java/org/springframework/data/domain/ScrollImpl.java +++ b/src/main/java/org/springframework/data/domain/WindowImpl.java @@ -26,19 +26,19 @@ import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** - * Default {@link Scroll} implementation. + * Default {@link Window} implementation. * * @author Mark Paluch * @since 3.1 */ -class ScrollImpl implements Scroll { +class WindowImpl implements Window { private final List items; private final IntFunction positionFunction; private final boolean hasNext; - ScrollImpl(List items, IntFunction positionFunction, boolean hasNext) { + WindowImpl(List items, IntFunction positionFunction, boolean hasNext) { Assert.notNull(items, "List of items must not be null"); Assert.notNull(positionFunction, "Position function must not be null"); @@ -79,11 +79,11 @@ class ScrollImpl implements Scroll { } @Override - public Scroll map(Function converter) { + public Window map(Function converter) { Assert.notNull(converter, "Function must not be null"); - return new ScrollImpl<>(stream().map(converter).collect(Collectors.toList()), positionFunction, hasNext); + return new WindowImpl<>(stream().map(converter).collect(Collectors.toList()), positionFunction, hasNext); } @NotNull @@ -98,7 +98,7 @@ class ScrollImpl implements Scroll { return true; if (o == null || getClass() != o.getClass()) return false; - ScrollImpl that = (ScrollImpl) o; + WindowImpl that = (WindowImpl) o; return ObjectUtils.nullSafeEquals(items, that.items) && ObjectUtils.nullSafeEquals(positionFunction, that.positionFunction) && ObjectUtils.nullSafeEquals(hasNext, that.hasNext); 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 000000000..f4c06833b --- /dev/null +++ b/src/main/java/org/springframework/data/domain/WindowIterator.java @@ -0,0 +1,117 @@ +/* + * 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.ArrayList; +import java.util.Iterator; +import java.util.List; +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} returning the Window {@link Window#getContent() content} on {@link #next()}. + *
+ * WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10By...("spring", position))
+ *   .startingAt(OffsetScrollPosition.initial());
+ * while (users.hasNext()) {
+ *   users.next().forEach(user -> {
+ *     // consume the user
+ *   });
+ * }
+ * 
+ * + * @author Christoph Strobl + * @since 3.1 + */ +public class WindowIterator implements Iterator> { + + private final Function> windowFunction; + private ScrollPosition currentPosition; + + @Nullable // + private Window currentWindow; + + /** + * 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; + this.currentWindow = doScroll(); + } + + @Override + public boolean hasNext() { + return currentWindow != null; + } + + @Override + public List next() { + + List toReturn = new ArrayList<>(currentWindow.getContent()); + currentPosition = currentWindow.positionAt(currentWindow.size() -1); + currentWindow = doScroll(); + return toReturn; + } + + @Nullable + Window doScroll() { + + if (currentWindow != null && !currentWindow.hasNext()) { + return null; + } + + Window window = windowFunction.apply(currentPosition); + if (window.isEmpty() && window.isLast()) { + return null; + } + return window; + } + + /** + * Builder API to construct a {@link WindowIterator}. + * + * @param + * @author Christoph Strobl + * @since 3.1 + */ + public static class WindowIteratorBuilder { + + private Function> windowFunction; + + WindowIteratorBuilder(Function> windowFunction) { + this.windowFunction = windowFunction; + } + + public WindowIterator startingAt(ScrollPosition position) { + + Assert.state(windowFunction != null, "WindowFunction cannot 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 7d3513bf5..b36437dfb 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,7 +27,6 @@ import java.util.stream.Stream; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Scroll; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; import org.springframework.lang.Nullable; @@ -165,7 +165,7 @@ public interface FluentQuery { List all(); /** - * Get all matching elements as {@link Scroll} to start result scrolling or resume scrolling at + * Get all matching elements as {@link Window} to start result scrolling or resume scrolling at * {@code scrollPosition}. * * @param scrollPosition must not be {@literal null}. @@ -174,7 +174,7 @@ public interface FluentQuery { * @throws UnsupportedOperationException if not supported by the underlying implementation. * @since 3.1 */ - default Scroll scroll(ScrollPosition scrollPosition) { + default Window scroll(ScrollPosition scrollPosition) { throw new UnsupportedOperationException("Scrolling not supported"); } @@ -261,7 +261,7 @@ public interface FluentQuery { Flux all(); /** - * Get all matching elements as {@link Scroll} to start result scrolling or resume scrolling at + * Get all matching elements as {@link Window} to start result scrolling or resume scrolling at * {@code scrollPosition}. * * @param scrollPosition must not be {@literal null}. @@ -270,7 +270,7 @@ public interface FluentQuery { * @throws UnsupportedOperationException if not supported by the underlying implementation. * @since 3.1 */ - default Mono> scroll(ScrollPosition scrollPosition) { + default Mono> scroll(ScrollPosition scrollPosition) { throw new UnsupportedOperationException("Scrolling not supported"); } diff --git a/src/main/java/org/springframework/data/repository/query/QueryMethod.java b/src/main/java/org/springframework/data/repository/query/QueryMethod.java index 23db75a5d..1d12f533d 100644 --- a/src/main/java/org/springframework/data/repository/query/QueryMethod.java +++ b/src/main/java/org/springframework/data/repository/query/QueryMethod.java @@ -24,7 +24,7 @@ import java.util.stream.Stream; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Scroll; +import org.springframework.data.domain.Window; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; @@ -97,7 +97,7 @@ public class QueryMethod { } if (hasParameterOfType(method, ScrollPosition.class)) { - assertReturnTypeAssignable(method, Collections.singleton(Scroll.class)); + assertReturnTypeAssignable(method, Collections.singleton(Window.class)); } Assert.notNull(this.parameters, @@ -203,13 +203,13 @@ public class QueryMethod { } /** - * Returns whether the query method will return a {@link Scroll}. + * Returns whether the query method will return a {@link Window}. * * @return * @since 3.1 */ public boolean isScrollQuery() { - return org.springframework.util.ClassUtils.isAssignable(Scroll.class, unwrappedReturnType); + return org.springframework.util.ClassUtils.isAssignable(Window.class, unwrappedReturnType); } /** diff --git a/src/main/java/org/springframework/data/repository/query/ResultProcessor.java b/src/main/java/org/springframework/data/repository/query/ResultProcessor.java index ce1cc2a39..4083df7c5 100644 --- a/src/main/java/org/springframework/data/repository/query/ResultProcessor.java +++ b/src/main/java/org/springframework/data/repository/query/ResultProcessor.java @@ -26,7 +26,7 @@ import org.springframework.core.CollectionFactory; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.data.domain.Scroll; +import org.springframework.data.domain.Window; import org.springframework.data.domain.Slice; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.util.ReactiveWrapperConverters; @@ -144,8 +144,8 @@ public class ResultProcessor { ChainingConverter converter = ChainingConverter.of(type.getReturnedType(), preparingConverter).and(this.converter); - if (source instanceof Scroll && method.isScrollQuery()) { - return (T) ((Scroll) source).map(converter::convert); + if (source instanceof Window && method.isScrollQuery()) { + return (T) ((Window) source).map(converter::convert); } if (source instanceof Slice && (method.isPageQuery() || method.isSliceQuery())) { diff --git a/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java b/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java index e14e8c654..b900480c2 100644 --- a/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java +++ b/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java @@ -36,7 +36,7 @@ import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.domain.Page; -import org.springframework.data.domain.Scroll; +import org.springframework.data.domain.Window; import org.springframework.data.domain.Slice; import org.springframework.data.geo.GeoResults; import org.springframework.data.util.CustomCollections; @@ -98,7 +98,7 @@ public abstract class QueryExecutionConverters { ALLOWED_PAGEABLE_TYPES.add(Slice.class); ALLOWED_PAGEABLE_TYPES.add(Page.class); ALLOWED_PAGEABLE_TYPES.add(List.class); - ALLOWED_PAGEABLE_TYPES.add(Scroll.class); + ALLOWED_PAGEABLE_TYPES.add(Window.class); WRAPPER_TYPES.add(NullableWrapperToCompletableFutureConverter.getWrapperType()); diff --git a/src/test/java/org/springframework/data/domain/WindowIteratorUnitTests.java b/src/test/java/org/springframework/data/domain/WindowIteratorUnitTests.java new file mode 100644 index 000000000..a8f525df7 --- /dev/null +++ b/src/test/java/org/springframework/data/domain/WindowIteratorUnitTests.java @@ -0,0 +1,131 @@ +/* + * 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.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.data.domain.WindowIterator.WindowIteratorBuilder; + +/** + * @author Christoph Strobl + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class WindowIteratorUnitTests { + + @Mock Function> fkt; + + @Mock Window window; + + @Mock ScrollPosition scrollPosition; + + @Captor ArgumentCaptor scrollCaptor; + + @BeforeEach + void beforeEach() { + when(fkt.apply(any())).thenReturn(window); + } + + @Test // GH-2151 + void loadsDataOnCreation() { + + WindowIteratorBuilder of = WindowIterator.of(fkt); + verifyNoInteractions(fkt); + + of.startingAt(scrollPosition); + verify(fkt).apply(eq(scrollPosition)); + } + + @Test // GH-2151 + void hasNextReturnsFalseIfNoDataAvailable() { + + when(window.isLast()).thenReturn(true); + when(window.isEmpty()).thenReturn(true); + + assertThat(WindowIterator.of(fkt).startingAt(scrollPosition).hasNext()).isFalse(); + } + + @Test // GH-2151 + void hasNextReturnsTrueIfDataAvailableButOnlyOnePage() { + + when(window.isLast()).thenReturn(true); + when(window.isEmpty()).thenReturn(false); + + assertThat(WindowIterator.of(fkt).startingAt(scrollPosition).hasNext()).isTrue(); + } + + @Test // GH-2151 + void allowsToIterateAllWindows() { + + ScrollPosition p1 = mock(ScrollPosition.class); + ScrollPosition p2 = mock(ScrollPosition.class); + + when(window.isEmpty()).thenReturn(false, false, false); + when(window.isLast()).thenReturn(false, false, true); + when(window.hasNext()).thenReturn(true, true, false); + when(window.size()).thenReturn(1, 1, 1); + when(window.positionAt(anyInt())).thenReturn(p1, p2); + when(window.getContent()).thenReturn(List.of((T) "0"), List.of((T) "1"), List.of((T) "2")); + + WindowIterator iterator = WindowIterator.of(fkt).startingAt(scrollPosition); + List capturedResult = new ArrayList<>(3); + while (iterator.hasNext()) { + capturedResult.addAll(iterator.next()); + } + + verify(fkt, times(3)).apply(scrollCaptor.capture()); + assertThat(scrollCaptor.getAllValues()).containsExactly(scrollPosition, p1, p2); + assertThat(capturedResult).containsExactly((T) "0", (T) "1", (T) "2"); + } + + @Test // GH-2151 + void stopsAfterFirstPageIfOnlyOneWindowAvailable() { + + ScrollPosition p1 = mock(ScrollPosition.class); + + when(window.isEmpty()).thenReturn(false); + when(window.isLast()).thenReturn(true); + when(window.hasNext()).thenReturn(false); + when(window.size()).thenReturn(1); + when(window.positionAt(anyInt())).thenReturn(p1); + when(window.getContent()).thenReturn(List.of((T) "0")); + + WindowIterator iterator = WindowIterator.of(fkt).startingAt(scrollPosition); + List capturedResult = new ArrayList<>(1); + while (iterator.hasNext()) { + capturedResult.addAll(iterator.next()); + } + + verify(fkt).apply(scrollCaptor.capture()); + assertThat(scrollCaptor.getAllValues()).containsExactly(scrollPosition); + assertThat(capturedResult).containsExactly((T) "0"); + } +} diff --git a/src/test/java/org/springframework/data/domain/ScrollUnitTests.java b/src/test/java/org/springframework/data/domain/WindowUnitTests.java similarity index 66% rename from src/test/java/org/springframework/data/domain/ScrollUnitTests.java rename to src/test/java/org/springframework/data/domain/WindowUnitTests.java index 5ffbc3f17..210fc9327 100644 --- a/src/test/java/org/springframework/data/domain/ScrollUnitTests.java +++ b/src/test/java/org/springframework/data/domain/WindowUnitTests.java @@ -23,31 +23,32 @@ import java.util.function.IntFunction; import org.junit.jupiter.api.Test; /** - * Unit tests for {@link Scroll}. + * Unit tests for {@link Window}. * * @author Mark Paluch + * @author Christoph Strobl */ -class ScrollUnitTests { +class WindowUnitTests { @Test // GH-2151 void equalsAndHashCode() { IntFunction positionFunction = OffsetScrollPosition.positionFunction(0); - Scroll one = Scroll.from(List.of(1, 2, 3), positionFunction); - Scroll two = Scroll.from(List.of(1, 2, 3), positionFunction); + Window one = Window.from(List.of(1, 2, 3), positionFunction); + Window two = Window.from(List.of(1, 2, 3), positionFunction); assertThat(one).isEqualTo(two).hasSameHashCodeAs(two); assertThat(one.equals(two)).isTrue(); - assertThat(Scroll.from(List.of(1, 2, 3), positionFunction, true)).isNotEqualTo(two).doesNotHaveSameHashCodeAs(two); + assertThat(Window.from(List.of(1, 2, 3), positionFunction, true)).isNotEqualTo(two).doesNotHaveSameHashCodeAs(two); } @Test // GH-2151 void allowsIteration() { - Scroll scroll = Scroll.from(List.of(1, 2, 3), OffsetScrollPosition.positionFunction(0)); + Window window = Window.from(List.of(1, 2, 3), OffsetScrollPosition.positionFunction(0)); - for (Integer integer : scroll) { + for (Integer integer : window) { assertThat(integer).isBetween(1, 3); } } @@ -55,15 +56,15 @@ class ScrollUnitTests { @Test // GH-2151 void shouldCreateCorrectPositions() { - Scroll scroll = Scroll.from(List.of(1, 2, 3), OffsetScrollPosition.positionFunction(0)); + Window window = Window.from(List.of(1, 2, 3), OffsetScrollPosition.positionFunction(0)); - assertThat(scroll.firstPosition()).isEqualTo(OffsetScrollPosition.of(1)); - assertThat(scroll.lastPosition()).isEqualTo(OffsetScrollPosition.of(3)); + assertThat(window.positionAt(0)).isEqualTo(OffsetScrollPosition.of(1)); + assertThat(window.positionAt(window.size() - 1)).isEqualTo(OffsetScrollPosition.of(3)); // by index - assertThat(scroll.positionAt(1)).isEqualTo(OffsetScrollPosition.of(2)); + assertThat(window.positionAt(1)).isEqualTo(OffsetScrollPosition.of(2)); // by object - assertThat(scroll.positionAt(Integer.valueOf(1))).isEqualTo(OffsetScrollPosition.of(1)); + assertThat(window.positionAt(Integer.valueOf(1))).isEqualTo(OffsetScrollPosition.of(1)); } } diff --git a/src/test/java/org/springframework/data/repository/query/ParametersUnitTests.java b/src/test/java/org/springframework/data/repository/query/ParametersUnitTests.java index a62071f6e..bea7d7b78 100755 --- a/src/test/java/org/springframework/data/repository/query/ParametersUnitTests.java +++ b/src/test/java/org/springframework/data/repository/query/ParametersUnitTests.java @@ -28,7 +28,7 @@ import org.reactivestreams.Publisher; import org.springframework.data.domain.OffsetScrollPosition; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Scroll; +import org.springframework.data.domain.Window; import org.springframework.data.domain.Sort; import org.springframework.test.util.ReflectionTestUtils; @@ -231,7 +231,7 @@ class ParametersUnitTests { Page customPageable(SomePageable pageable); - Scroll customScrollPosition(OffsetScrollPosition request); + Window customScrollPosition(OffsetScrollPosition request); } interface SomePageable extends Pageable {} diff --git a/src/test/java/org/springframework/data/repository/query/QueryMethodUnitTests.java b/src/test/java/org/springframework/data/repository/query/QueryMethodUnitTests.java index 7d76e1cf5..e5ad94eef 100755 --- a/src/test/java/org/springframework/data/repository/query/QueryMethodUnitTests.java +++ b/src/test/java/org/springframework/data/repository/query/QueryMethodUnitTests.java @@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.*; import io.vavr.collection.Seq; import io.vavr.control.Option; +import org.springframework.data.domain.Window; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -32,7 +33,6 @@ import org.eclipse.collections.api.list.ImmutableList; import org.junit.jupiter.api.Test; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Scroll; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Slice; import org.springframework.data.projection.ProjectionFactory; @@ -351,15 +351,15 @@ class QueryMethodUnitTests { ImmutableList returnsEclipseCollection(); - Scroll cursorWindow(ScrollPosition cursorRequest); + Window cursorWindow(ScrollPosition cursorRequest); - Mono> reactiveCursorWindow(ScrollPosition cursorRequest); + Mono> reactiveCursorWindow(ScrollPosition cursorRequest); - Flux> invalidReactiveCursorWindow(ScrollPosition cursorRequest); + Flux> invalidReactiveCursorWindow(ScrollPosition cursorRequest); Page cursorWindowMethodWithInvalidReturnType(ScrollPosition cursorRequest); - Scroll cursorWindowWithoutScrollPosition(); + Window cursorWindowWithoutScrollPosition(); } class User {