Summary
Keyset scrolling has the right shape: a Scrollable request, a Window result with navigation tokens, and windows to iterate. Four places do not keep the promise the types make, and one ordinary case is missing.
- Direction and navigation are one flag.
Scrollable.isForward means both "sort descending" and "move to the previous window", so window.previous() on a forward window fetches descending and the docs tell the caller to reverse the list for display. hasPrevious means "this window was fetched with a cursor", a fact about the request rather than the data.
- Tokens depend on the result type.
Window.next() is null for selectRef(), for a custom select type and for scroll(int), even when the window has content, because the cursor values are read from the mapped result object. The docs carry a section explaining how to rebuild the token by hand.
- One sort column.
Scrollable.of(key, sort, size) allows one non-unique sort field before the key. Sorting by last name, then first name, then id is ordinary.
- Windows are not iterable. Every loop writes
window.content().
pageRef has no scrolling counterpart.
Proposal
Order(field, descending) becomes a top-level type shared by Pageable and Scrollable.
Scrollable(key, keyDescending, sort: List<Order>, size, position) with descending(), sortBy, sortByDescending, after(values...), before(values...) and from(cursor). backward() goes: a descending feed is a descending sort navigated forward.
- A window reached through
previous() is fetched with every direction flipped and reversed before it is returned, so every window is in sort order. hasNext and hasPrevious say whether rows exist after or before the window in sort order: for a window from the start, hasPrevious is false; after a cursor, hasPrevious is true because the anchor row precedes; before a cursor, hasNext is true because the anchor row follows; the open side is decided by the size + 1 fetch.
- The sort and key columns are selected alongside the result and the tokens are read from the row, so refs, projections read as another type and custom select types are navigable, and the "Window Type Parameters" section disappears.
- The keyset predicate expands to any number of sort fields as the OR chain the single-field form already emits, with the comparison operator following each field's own direction, which stays portable to SQL Server and Oracle.
scroll(int) becomes slice(int) and returns Slice<R>, the interface that exists today and nothing returns; hasPrevious there follows the builder's offset.
Slice<R> extends Iterable<R> with size(), isEmpty() and stream().
- The cursor string carries the position only: values and an after-or-before flag under the fingerprint of the ordering. The request carries ordering and size, so a client can change the page size between requests, and
st.orm.scrollable.maxSize goes away; the application validates its own size parameter.
windows(scrollable) accepts a start of after or none and refuses before.
scrollRef(scrollable) on both repositories.
Sort fields must be non-nullable, checked the way the key is, because WHERE field > ? drops NULL rows silently.
Upgrade notes
backward() is replaced by descending() for a descending sort and by previous() for navigation. Cursor strings issued by 1.14.0 are refused by the version check, so a client holding one starts over from the first window. scroll(int) is renamed slice(int) and returns Slice.
Builds on #552, which adds windows.
Summary
Keyset scrolling has the right shape: a
Scrollablerequest, aWindowresult with navigation tokens, andwindowsto iterate. Four places do not keep the promise the types make, and one ordinary case is missing.Scrollable.isForwardmeans both "sort descending" and "move to the previous window", sowindow.previous()on a forward window fetches descending and the docs tell the caller to reverse the list for display.hasPreviousmeans "this window was fetched with a cursor", a fact about the request rather than the data.Window.next()is null forselectRef(), for a custom select type and forscroll(int), even when the window has content, because the cursor values are read from the mapped result object. The docs carry a section explaining how to rebuild the token by hand.Scrollable.of(key, sort, size)allows one non-unique sort field before the key. Sorting by last name, then first name, then id is ordinary.window.content().pageRefhas no scrolling counterpart.Proposal
Order(field, descending)becomes a top-level type shared byPageableandScrollable.Scrollable(key, keyDescending, sort: List<Order>, size, position)withdescending(),sortBy,sortByDescending,after(values...),before(values...)andfrom(cursor).backward()goes: a descending feed is a descending sort navigated forward.previous()is fetched with every direction flipped and reversed before it is returned, so every window is in sort order.hasNextandhasPrevioussay whether rows exist after or before the window in sort order: for a window from the start,hasPreviousis false; after a cursor,hasPreviousis true because the anchor row precedes; before a cursor,hasNextis true because the anchor row follows; the open side is decided by the size + 1 fetch.scroll(int)becomesslice(int)and returnsSlice<R>, the interface that exists today and nothing returns;hasPreviousthere follows the builder's offset.Slice<R>extendsIterable<R>withsize(),isEmpty()andstream().st.orm.scrollable.maxSizegoes away; the application validates its own size parameter.windows(scrollable)accepts a start ofafteror none and refusesbefore.scrollRef(scrollable)on both repositories.Sort fields must be non-nullable, checked the way the key is, because
WHERE field > ?drops NULL rows silently.Upgrade notes
backward()is replaced bydescending()for a descending sort and byprevious()for navigation. Cursor strings issued by 1.14.0 are refused by the version check, so a client holding one starts over from the first window.scroll(int)is renamedslice(int)and returnsSlice.Builds on #552, which adds
windows.