Repository: jakartaee/data (java)

Issue:
[Bug]: PageRequest doesn't know whether it's a Builder
### Specification Version

1.0.0-M3

### Bug report

`PageRequest` has multiple operations capable of producing an object in an inconsistent state. The most dangerous of these is `page(long)`, which looks harmless enough but actually results in something completely broken if you call it an a `PageRequest` with a `Cursor`. And there's no warning of this in the Javadoc, even. On the other hand, it also has operations like `next()` which are written to be called by the application. 

It is in no way obvious to the user that these operations `page(long)` and `next()` are so different in nature. So much so that even I have tripped up twice over the difference and I am far from a casual user of this API.

We need to remove these dangerous operations to a `Builder`-type object, or do what I suggested a while ago, and expose the constructor of `Pagination` to Jakarta Data implementations and simply remove the "dangerous" methods. (This is the simplest solution.)

### Additional information

_No response_

Pull request description:
PageRequest.page(num) method can cause wrong usage
fixes #672

This adds onto the change to remove next()/previous() (first commit) and also removes page(number) from PageRequest (second commit).  All of these methods can mislead applications into writing code that causes wrong behavior.

Patch:
diff --git a/api/src/main/java/jakarta/data/Order.java b/api/src/main/java/jakarta/data/Order.java
index 1e8db342..3cc99a50 100644
--- a/api/src/main/java/jakarta/data/Order.java
+++ b/api/src/main/java/jakarta/data/Order.java
@@ -40,7 +40,7 @@ import jakarta.data.repository.OrderBy;
  * Page&lt;Employee>&gt; findByYearHired(int year, PageRequest pageRequest, Order&lt;Employee&gt;);
  * ...
  * page1 = employees.findByYearHired(Year.now(),
- *                                   PageRequest.ofSize(10).page(1),
+ *                                   PageRequest.ofSize(10),
  *                                   Order.by(_Employee.salary.desc(),
  *                                            _Employee.lastName.asc(),
  *                                            _Employee.firstName.asc()));
diff --git a/api/src/main/java/jakarta/data/page/CursoredPage.java b/api/src/main/java/jakarta/data/page/CursoredPage.java
index 5e7d17bb..6bc61000 100644
--- a/api/src/main/java/jakarta/data/page/CursoredPage.java
+++ b/api/src/main/java/jakarta/data/page/CursoredPage.java
@@ -18,6 +18,7 @@
 package jakarta.data.page;
 
 import jakarta.data.repository.OrderBy;
+import jakarta.data.Order;
 import jakarta.data.Sort;
 import java.util.NoSuchElementException;
 
@@ -78,12 +79,12 @@ import java.util.NoSuchElementException;
  * in an instance of {@link PageRequest.Cursor Cursor} and identifying the last
  * result on the current page.</p>
  *
- * <p>A {@link PageRequest} based on an explicit key may be constructed by
- * calling {@link PageRequest#afterKey(Object...)}. The arguments supplied
- * to this method must match the list of sorting criteria specified by
- * {@link OrderBy} annotations of the repository method, {@link Sort}
- * parameters of the page request, or {@code OrderBy} name pattern of
- * the repository method. For example:</p>
+ * <p>A {@link PageRequest} based on an explicit cursor may be constructed by
+ * calling {@link PageRequest#afterCursor(PageRequest.Cursor)}. The key component
+ * values of the cursor supplied to this method must match the list of sorting
+ * criteria specified by {@link OrderBy} annotations or {@code OrderBy} name
+ * pattern of the repository method and the {@link Sort} and {@link Order}
+ * parameters of the repository method. For example:</p>
  *
  * <pre>
  * Employee emp = ...
@@ -192,10 +193,9 @@ public interface CursoredPage<T> extends Page<T> {
      * when matching entities are added prior to the first page and the
      * previous page is requested) by assigning a page number of {@code 1}
      * to such pages. This means that there can be multiple consecutive pages
-     * numbered {@code 1} and that
-     * {@code currentPage.previousPageRequest().next().page()}
-     * cannot be relied upon to return a page number that is equal to the
-     * current page number.</p>
+     * numbered {@code 1} and that navigating to the previous page and then
+     * forward again cannot be relied upon to return a page number that is
+     * equal to the current page number.</p>
      *
      * @return pagination information for requesting the previous page.
      * @throws NoSuchElementException if the current page is empty or if
diff --git a/api/src/main/java/jakarta/data/page/Page.java b/api/src/main/java/jakarta/data/page/Page.java
index 9c458b5e..5be4f931 100644
--- a/api/src/main/java/jakarta/data/page/Page.java
+++ b/api/src/main/java/jakarta/data/page/Page.java
@@ -109,8 +109,8 @@ public interface Page<T> extends Iterable<T> {
 
 
     /**
-     * Returns a request for the {@linkplain PageRequest#next() next} page if
-     * {@link #hasNext()} indicates there might be a next page.
+     * Returns a request for the next page if {@link #hasNext()} indicates there
+     * might be a next page.
      *
      * @return a request for the next page.
      * @throws NoSuchElementException if it is known that there is no next page.
@@ -122,8 +122,8 @@ public interface Page<T> extends Iterable<T> {
 
 
     /**
-     * <p>Returns a request for the {@link PageRequest#previous() previous} page,
-     * if {@link #hasPrevious()} indicates there might be a previous page.</p>
+     * <p>Returns a request for the previous page, if {@link #hasPrevious()}
+     * indicates there might be a previous page.</p>
      *
      * @return a request for the previous page.
      * @throws NoSuchElementException if it is known that there is no previous page.
diff --git a/api/src/main/java/jakarta/data/page/PageRequest.java b/api/src/main/java/jakarta/data/page/PageRequest.java
index db6779b6..40ec4c7c 100644
--- a/api/src/main/java/jakarta/data/page/PageRequest.java
+++ b/api/src/main/java/jakarta/data/page/PageRequest.java
@@ -18,6 +18,8 @@
 package jakarta.data.page;
 
 import jakarta.data.Limit;
+import jakarta.data.Order;
+import jakarta.data.Sort;
 import jakarta.data.repository.OrderBy;
 
 import java.util.List;
@@ -78,6 +80,21 @@ public interface PageRequest {
         return new Pagination(pageNumber, 10, Mode.OFFSET, null, true);
     }
 
+    /**
+     * Creates a new page request without a cursor.
+     *
+     * @param pageNumber   The page number.
+     * @param maxPageSize  The number of query results in a full page.
+     * @param requestTotal Indicates whether to retrieve the
+     *                     {@linkplain Page#totalElements() total}
+     *                     number of elements available across all pages.
+     * @return a new instance of {@code PageRequest}. This method never returns {@code null}.
+     * @throws IllegalArgumentException when the page number is negative or zero.
+     */
+    static PageRequest ofPage(long pageNumber, int maxPageSize, boolean requestTotal) {
+        return new Pagination(pageNumber, maxPageSize, Mode.OFFSET, null, requestTotal);
+    }
+
     /**
      * Creates a new page request for requesting pages of the specified size,
      * starting with the first page number, which is 1.
@@ -94,30 +111,46 @@ public interface PageRequest {
      * <p>Requests {@linkplain CursoredPage cursor-based pagination} in the forward direction,
      * starting after the specified key.</p>
      *
-     * @param key values forming the key, the order and number of which must match the
-     *        {@link OrderBy} annotations, {@link jakarta.data.Sort} parameters, or {@code OrderBy}
-     *        name pattern of the repository method to which this pagination will be
-     *        applied.
+     * @param cursor       cursor with key values, the order and number of
+     *                     which must match the {@link OrderBy} annotations or
+     *                     {@code OrderBy} name pattern and the {@link Order} and
+     *                     {@link Sort} parameters of the repository method to
+     *                     which this page request will be supplied.
+     * @param pageNumber   The page number.
+     * @param maxPageSize  The number of query results in a full page.
+     * @param requestTotal Indicates whether to retrieve the
+     *                     {@linkplain Page#totalElements() total}
+     *                     number of elements available across all pages.
      * @return a new instance of {@code PageRequest} with forward cursor-based pagination.
      *         This method never returns {@code null}.
-     * @throws IllegalArgumentException if no values are provided for the key.
+     * @throws IllegalArgumentException if the cursor is null or has no values.
      */
-    PageRequest afterKey(Object... key);
+    static PageRequest afterCursor(Cursor cursor, long pageNumber, int maxPageSize, boolean requestTotal) {
+        return new Pagination(pageNumber, maxPageSize, Mode.CURSOR_NEXT, cursor, requestTotal);
+    }
 
     /**
      * <p>Requests {@linkplain CursoredPage cursor-based pagination} in the previous page
-     * direction relative to the specified key.</p>
+     * direction relative to the specified cursor.</p>
      *
-     * @param key values forming the key, the order and number of which must match the
-     *        {@link OrderBy} annotations, {@link jakarta.data.Sort} parameters, or {@code OrderBy}
-     *        name pattern of the repository method to which this pagination will be
-     *        applied.
+     * @param cursor       cursor with key values, the order and number of
+     *                     which must match the {@link OrderBy} annotations or
+     *                     {@code OrderBy} name pattern and the {@link Order} and
+     *                     {@link Sort} parameters of the repository method to
+     *                     which this page request will be supplied.
+     * @param pageNumber   The page number.
+     * @param maxPageSize  The number of query results in a full page.
+     * @param requestTotal Indicates whether to retrieve the
+     *                     {@linkplain Page#totalElements() total}
+     *                     number of elements available across all pages.
      * @return a new instance of {@code PageRequest} with cursor-based pagination
      *         in the previous page direction.
      *         This method never returns {@code null}.
-     * @throws IllegalArgumentException if no values are provided for the key.
+     * @throws IllegalArgumentException if the cursor is null or has no values.
      */
-    PageRequest beforeKey(Object... key);
+    static PageRequest beforeCursor(Cursor cursor, long pageNumber, int maxPageSize, boolean requestTotal) {
+        return new Pagination(pageNumber, maxPageSize, Mode.CURSOR_PREVIOUS, cursor, requestTotal);
+    }
 
     /**
      * <p>Requests {@linkplain CursoredPage cursor-based pagination} in the forward direction,
@@ -197,50 +230,6 @@ public interface PageRequest {
      */
     boolean requestTotal();
 
-
-    /**
-     * <p>Returns the {@code PageRequest} requesting the next page if
-     * using offset pagination.</p>
-     *
-     * <p>If using cursor-based pagination, traversal of pages must only be done
-     * via the {@link CursoredPage#nextPageRequest()},
-     * {@link CursoredPage#previousPageRequest()}, or
-     * {@linkplain CursoredPage#cursor(int) cursor},
-     * not with this method.</p>
-     *
-     * @return The next PageRequest.
-     * @throws UnsupportedOperationException if this {@code PageRequest}
-     *         has a {@link PageRequest.Cursor Cursor}.
-     */
-    PageRequest next();
-
-    /**
-     * <p>Returns the {@code PageRequest} requesting the previous page
-     * if using offset pagination, or null if this is the first page, that
-     * is, when {@link #page()} returns {@code 1}.</p>
-     *
-     * <p>If using cursor-based pagination, traversal of pages must only be done
-     * via the {@link CursoredPage#nextPageRequest()},
-     * {@link CursoredPage#previousPageRequest()}, or
-     * {@linkplain CursoredPage#cursor(int) cursor},
-     * not with this method.</p>
-     *
-     * @return The previous PageRequest, or null if this is the first page.
-     * @throws UnsupportedOperationException if this {@code PageRequest}
-     *         has a {@link PageRequest.Cursor Cursor}.
-     */
-    PageRequest previous();
-
-    /**
-     * <p>Creates a new page request with the same pagination information,
-     * but with the specified page number.</p>
-     *
-     * @param pageNumber The page number
-     * @return a new instance of {@code PageRequest}.
-     *         This method never returns {@code null}.
-     */
-    PageRequest page(long pageNumber);
-
     /**
      * <p>Creates a new page request with the same pagination information,
      * but with the specified maximum page size. When a page is retrieved
diff --git a/api/src/main/java/jakarta/data/page/Pagination.java b/api/src/main/java/jakarta/data/page/Pagination.java
index 26a67905..b80f4868 100644
--- a/api/src/main/java/jakarta/data/page/Pagination.java
+++ b/api/src/main/java/jakarta/data/page/Pagination.java
@@ -46,16 +46,6 @@ record Pagination(long page, int size, Mode mode, Cursor type, boolean requestTo
         return new Pagination(page, size, mode, type, true);
     }
 
-    @Override
-    public PageRequest afterKey(Object... key) {
-        return new Pagination(page, size, Mode.CURSOR_NEXT, new PageRequestCursor(key), requestTotal);
-    }
-
-    @Override
-    public PageRequest beforeKey(Object... key) {
-        return new Pagination(page, size, Mode.CURSOR_PREVIOUS, new PageRequestCursor(key), requestTotal);
-    }
-
     @Override
     public PageRequest afterCursor(Cursor cursor) {
         return new Pagination(page, size, Mode.CURSOR_NEXT, cursor, requestTotal);
@@ -71,26 +61,6 @@ record Pagination(long page, int size, Mode mode, Cursor type, boolean requestTo
         return Optional.ofNullable(type);
     }
 
-    @Override
-    public PageRequest next() {
-        if (mode == Mode.OFFSET) {
-            return new Pagination(page + 1, this.size, Mode.OFFSET, null, requestTotal);
-        } else {
-            throw new UnsupportedOperationException("Not supported for cursor-based pagination. Instead use afterKey or afterCursor " +
-                    "to provide a cursor or obtain the nextPageRequest from a CursoredPage.");
-        }
-    }
-
-    @Override
-    public PageRequest previous() {
-        if (mode == Mode.OFFSET) {
-            return page()<=1 ? null : new Pagination(page - 1, this.size, Mode.OFFSET, null, requestTotal);
-        } else {
-            throw new UnsupportedOperationException("Not supported for cursor-based pagination. Instead use beforeKey or beforeCursor " +
-                    "to provide a cursor or obtain the previousPageRequest from a CursoredPage.");
-        }
-    }
-
     @Override
     public String toString() {
         StringBuilder s = new StringBuilder(mode == Mode.OFFSET ? 100 : 150)
@@ -103,11 +73,6 @@ record Pagination(long page, int size, Mode mode, Cursor type, boolean requestTo
         return s.append("}").toString();
     }
 
-    @Override
-    public PageRequest page(long pageNumber) {
-        return new Pagination(pageNumber, size, mode, type, requestTotal);
-    }
-
     @Override
     public PageRequest size(int maxPageSize) {
         return new Pagination(page, maxPageSize, mode, type, requestTotal);
diff --git a/api/src/main/java/jakarta/data/page/impl/CursoredPageRecord.java b/api/src/main/java/jakarta/data/page/impl/CursoredPageRecord.java
index c7a6212e..728c14fb 100644
--- a/api/src/main/java/jakarta/data/page/impl/CursoredPageRecord.java
+++ b/api/src/main/java/jakarta/data/page/impl/CursoredPageRecord.java
@@ -61,12 +61,16 @@ public record CursoredPageRecord<T>
             (List<T> content, List<PageRequest.Cursor> cursors, long totalElements, PageRequest pageRequest,
             boolean firstPage, boolean lastPage) {
         this(content, cursors, totalElements, pageRequest,
-                lastPage ? null
-                        : pageRequest.page(pageRequest.page()+1)
-                                .afterCursor(cursors.get(cursors.size()-1)),
-                firstPage ? null
-                        : pageRequest.page(pageRequest.page()==1 ? 1 : pageRequest.page()-1)
-                                .beforeCursor(cursors.get(0)));
+                lastPage ? null : PageRequest.afterCursor(
+                        cursors.get(cursors.size() - 1),
+                        pageRequest.page() + 1,
+                        pageRequest.size(),
+                        pageRequest.requestTotal()),
+                firstPage ? null : PageRequest.beforeCursor(
+                        cursors.get(0),
+                        pageRequest.page() == 1 ? 1 : pageRequest.page() - 1,
+                        pageRequest.size(),
+                        pageRequest.requestTotal()));
     }
 
     @Override
diff --git a/api/src/main/java/jakarta/data/page/impl/PageRecord.java b/api/src/main/java/jakarta/data/page/impl/PageRecord.java
index 1d07fc7d..04671fed 100644
--- a/api/src/main/java/jakarta/data/page/impl/PageRecord.java
+++ b/api/src/main/java/jakarta/data/page/impl/PageRecord.java
@@ -84,7 +84,10 @@ public record PageRecord<T>(PageRequest pageRequest, List<T> content, long total
         if (!hasNext()) {
             throw new NoSuchElementException();
         }
-        return pageRequest.next();
+
+        return PageRequest.ofPage(pageRequest.page() + 1,
+                                  pageRequest.size(),
+                                  pageRequest.requestTotal());
     }
 
     @Override
@@ -97,7 +100,10 @@ public record PageRecord<T>(PageRequest pageRequest, List<T> content, long total
         if (!hasPrevious()) {
             throw new NoSuchElementException();
         }
-        return pageRequest.previous();
+
+        return PageRequest.ofPage(pageRequest.page() - 1,
+                pageRequest.size(),
+                pageRequest.requestTotal());
     }
 
     @Override
diff --git a/spec/src/main/asciidoc/repository.asciidoc b/spec/src/main/asciidoc/repository.asciidoc
index dcf51b1e..6aa93726 100644
--- a/spec/src/main/asciidoc/repository.asciidoc
+++ b/spec/src/main/asciidoc/repository.asciidoc
@@ -322,7 +322,7 @@ This `PageRequest` specifies a starting page and maximum page size:
 
 [source,java]
 ----
-PageRequest pageRequest = PageRequest.ofSize(20).page(1);
+PageRequest pageRequest = PageRequest.ofPage(1).size(20);
 List<Product> first20 = products.findByName(name, pageRequest,
                             Order.by(_Product.price.desc(),
                                      _Product.id.asc()));


Test patch:
diff --git a/api/src/test/java/jakarta/data/page/PageRequestCursorTest.java b/api/src/test/java/jakarta/data/page/PageRequestCursorTest.java
index cc6b3624..66b7ca04 100644
--- a/api/src/test/java/jakarta/data/page/PageRequestCursorTest.java
+++ b/api/src/test/java/jakarta/data/page/PageRequestCursorTest.java
@@ -32,11 +32,12 @@ class PageRequestCursorTest {
     @Test
     @DisplayName("Should include key values in next PageRequest")
     void shouldCreatePageRequestAfterKeys() {
-        PageRequest pageRequest = PageRequest.ofSize(20).afterKey("First", 2L, 3);
+        PageRequest pageRequest = PageRequest.afterCursor(PageRequest.Cursor.forKey("First", 2L, 3), 1L, 20, false);
 
         assertSoftly(softly -> {
             softly.assertThat(pageRequest.size()).isEqualTo(20);
             softly.assertThat(pageRequest.page()).isEqualTo(1L);
+            softly.assertThat(pageRequest.requestTotal()).isEqualTo(false);
             softly.assertThat(pageRequest.mode()).isEqualTo(PageRequest.Mode.CURSOR_NEXT);
             softly.assertThat(pageRequest.cursor()).get().extracting(PageRequest.Cursor::size).isEqualTo(3);
             softly.assertThat(pageRequest.cursor()).get().extracting(c -> c.get(0)).isEqualTo("First");
@@ -64,11 +65,13 @@ class PageRequestCursorTest {
     @Test
     @DisplayName("Should include key values in previous PageRequest")
     void shouldCreatePageRequestBeforeKey() {
-        PageRequest pageRequest = PageRequest.ofSize(30).beforeKey(1991, "123-45-6789").page(10);
+        PageRequest.Cursor cursor = PageRequest.Cursor.forKey(1991, "123-45-6789");
+        PageRequest pageRequest = PageRequest.beforeCursor(cursor, 10L, 30, false);
 
         assertSoftly(softly -> {
             softly.assertThat(pageRequest.size()).isEqualTo(30);
             softly.assertThat(pageRequest.page()).isEqualTo(10L);
+            softly.assertThat(pageRequest.requestTotal()).isEqualTo(false);            
             softly.assertThat(pageRequest.mode()).isEqualTo(PageRequest.Mode.CURSOR_PREVIOUS);
             softly.assertThat(pageRequest.cursor()).get().extracting(PageRequest.Cursor::size).isEqualTo(2);
             softly.assertThat(pageRequest.cursor()).get().extracting(c -> c.get(0)).isEqualTo(1991);
@@ -98,10 +101,10 @@ class PageRequestCursorTest {
     @Test
     @DisplayName("Should be usable in a hashing structure")
     void shouldHash() {
-        PageRequest pageRequest1 = PageRequest.ofSize(15).afterKey(1, '1', "1");
-        PageRequest pageRequest2A = PageRequest.ofSize(15).afterKey(2, '2', "2");
-        PageRequest pageRequest2B = PageRequest.ofSize(15).beforeKey(2, '2', "2");
-        PageRequest pageRequest2C = PageRequest.ofSize(15).beforeKey(2, '2', "2");
+        PageRequest pageRequest1 = PageRequest.ofSize(15).afterCursor(PageRequest.Cursor.forKey(1, '1', "1"));
+        PageRequest pageRequest2A = PageRequest.afterCursor(PageRequest.Cursor.forKey(2, '2', "2"), 1L, 15, true);
+        PageRequest pageRequest2B = PageRequest.beforeCursor(PageRequest.Cursor.forKey(2, '2', "2"), 1L, 15, true);
+        PageRequest pageRequest2C = PageRequest.ofSize(15).beforeCursor(PageRequest.Cursor.forKey(2, '2', "2"));
         Map<PageRequest, Integer> map = new HashMap<>();
 
         assertSoftly(softly -> {
@@ -121,8 +124,8 @@ class PageRequestCursorTest {
     @Test
     @DisplayName("Should be displayable as String with toString")
     void shouldPageRequestDisplayAsString() {
-        var afterKeySet = PageRequest.ofSize(200).afterKey("value1", 1);
-        var beforeKeySet = PageRequest.ofSize(100).beforeKey("Item1", 3456);
+        var afterKeySet = PageRequest.ofSize(200).afterCursor(PageRequest.Cursor.forKey("value1", 1));
+        var beforeKeySet = PageRequest.ofSize(100).beforeCursor(PageRequest.Cursor.forKey("Item1", 3456));
 
         assertSoftly(softly -> {
 
@@ -138,11 +141,11 @@ class PageRequestCursorTest {
     @Test
     @DisplayName("Should return true from equals if key values and other properties are equal")
     void shouldBeEqualWithSameKeyValues() {
-        PageRequest pageRequest25P1S0A1 = PageRequest.ofSize(25).afterKey("keyval1", '2', 3);
-        PageRequest pageRequest25P1S0B1 = PageRequest.ofSize(25).beforeKey("keyval1", '2', 3);
+        PageRequest pageRequest25P1S0A1 = PageRequest.ofSize(25).afterCursor(PageRequest.Cursor.forKey("keyval1", '2', 3));
+        PageRequest pageRequest25P1S0B1 = PageRequest.ofSize(25).beforeCursor(PageRequest.Cursor.forKey("keyval1", '2', 3));
         PageRequest pageRequest25P1S0A1Match = PageRequest.ofSize(25).afterCursor(new PageRequestCursor("keyval1", '2', 3));
         PageRequest pageRequest25P2S0A1 = PageRequest.ofPage(2).size(25).afterCursor(new PageRequestCursor("keyval1", '2', 3));
-        PageRequest pageRequest25P1S0A2 = PageRequest.ofSize(25).afterKey("keyval2", '2', 3);
+        PageRequest pageRequest25P1S0A2 = PageRequest.ofSize(25).afterCursor(PageRequest.Cursor.forKey("keyval2", '2', 3));
 
         PageRequest.Cursor cursor1 = new PageRequestCursor("keyval1", '2', 3);
         PageRequest.Cursor cursor2 = new PageRequestCursor("keyval2", '2', 3);
@@ -161,7 +164,7 @@ class PageRequestCursorTest {
             }
 
             @Override
-            public List elements() {
+            public List<?> elements() {
                 return List.of(keyset);
             }
         };
@@ -191,17 +194,15 @@ class PageRequestCursorTest {
     @Test
     @DisplayName("Should raise IllegalArgumentException when key values are absent")
     void shouldRaiseErrorForMissingKeysetValues() {
-        assertThatIllegalArgumentException().isThrownBy(() -> PageRequest.ofSize(60).afterKey((Object[]) null));
-        assertThatIllegalArgumentException().isThrownBy(() -> PageRequest.ofSize(70).afterKey(new Object[0]));
-        assertThatIllegalArgumentException().isThrownBy(() -> PageRequest.ofSize(80).beforeKey((Object[]) null));
-        assertThatIllegalArgumentException().isThrownBy(() -> PageRequest.ofSize(90).beforeKey(new Object[0]));
+        assertThatIllegalArgumentException().isThrownBy(() -> PageRequest.Cursor.forKey((Object[]) null));
+        assertThatIllegalArgumentException().isThrownBy(() -> PageRequest.Cursor.forKey(new Object[0]));
     }
 
     @Test
-    @DisplayName("Key should be replaced on new instance of PageRequest")
-    void shouldReplaceKey() {
-        PageRequest p1 = PageRequest.ofSize(30).afterKey("last1", "fname1", 100).page(12);
-        PageRequest p2 = p1.beforeKey("lname2", "fname2", 200);
+    @DisplayName("Cursor should be replaced on new instance of PageRequest")
+    void shouldReplaceCursor() {
+        PageRequest p1 = PageRequest.ofPage(12).size(30).afterCursor(PageRequest.Cursor.forKey("last1", "fname1", 100));
+        PageRequest p2 = p1.beforeCursor(PageRequest.Cursor.forKey("lname2", "fname2", 200));
 
         assertSoftly(softly -> {
             softly.assertThat(p1.mode()).isEqualTo(PageRequest.Mode.CURSOR_NEXT);
diff --git a/api/src/test/java/jakarta/data/page/PageRequestTest.java b/api/src/test/java/jakarta/data/page/PageRequestTest.java
index 66562ba3..446ebb1e 100644
--- a/api/src/test/java/jakarta/data/page/PageRequestTest.java
+++ b/api/src/test/java/jakarta/data/page/PageRequestTest.java
@@ -52,20 +52,6 @@ class PageRequestTest {
         });
     }
 
-    @Test
-    @DisplayName("Should navigate next")
-    void shouldNext() {
-        PageRequest pageRequest = PageRequest.ofSize(1).page(2);
-        PageRequest next = pageRequest.next();
-
-        assertSoftly(softly -> {
-            softly.assertThat(pageRequest.page()).isEqualTo(2L);
-            softly.assertThat(pageRequest.size()).isEqualTo(1);
-            softly.assertThat(next.page()).isEqualTo(3L);
-            softly.assertThat(next.size()).isEqualTo(1);
-        });
-    }
-
     @Test
     @DisplayName("Should create a new PageRequest at the given page with a default size of 10")
     void shouldCreatePage() {
@@ -100,7 +86,7 @@ class PageRequestTest {
             softly.assertThat(pageRequest1.requestTotal()).isEqualTo(true);
         });
 
-        PageRequest pageRequest2 = PageRequest.ofSize(80).withoutTotal().page(2);
+        PageRequest pageRequest2 = PageRequest.ofPage(2).size(80).withoutTotal();
 
         assertSoftly(softly -> {
             softly.assertThat(pageRequest2.page()).isEqualTo(2L);
@@ -122,21 +108,6 @@ class PageRequestTest {
         assertThatIllegalArgumentException().isThrownBy(() -> PageRequest.ofSize(-1));
     }
 
-
-    @Test
-    @DisplayName("Page number should be replaced on new instance of PageRequest")
-    void shouldReplacePage() {
-        PageRequest p6 = PageRequest.ofSize(75).page(6);
-        PageRequest p7 = p6.page(7);
-
-        assertSoftly(softly -> {
-            softly.assertThat(p7.page()).isEqualTo(7L);
-            softly.assertThat(p6.page()).isEqualTo(6L);
-            softly.assertThat(p7.size()).isEqualTo(75);
-            softly.assertThat(p6.size()).isEqualTo(75);
-        });
-    }
-
     @Test
     @DisplayName("Size should be replaced on new instance of PageRequest")
     void shouldReplaceSize() {
diff --git a/api/src/test/java/jakarta/data/page/PaginationTest.java b/api/src/test/java/jakarta/data/page/PaginationTest.java
index c50b3f6a..a561066a 100644
--- a/api/src/test/java/jakarta/data/page/PaginationTest.java
+++ b/api/src/test/java/jakarta/data/page/PaginationTest.java
@@ -20,10 +20,7 @@ package jakarta.data.page;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
-import java.util.Collections;
-
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 class PaginationTest {
 
@@ -35,16 +32,4 @@ class PaginationTest {
                 .isThrownBy(() -> new Pagination(1, 10, PageRequest.Mode.CURSOR_NEXT, null, true));
     }
 
-    @Test
-    @DisplayName("Should throw UnsupportedOperationException when key is not supported")
-    void shouldThrowExceptionWhenKeysetIsNotSupported() {
-        assertThatThrownBy(() -> {
-            Pagination pagination = new Pagination(1, 10,
-                                                        PageRequest.Mode.CURSOR_NEXT,
-                                                        new PageRequestCursor("me", 200),
-                                                        true);
-            pagination.next();
-        }).isInstanceOf(UnsupportedOperationException.class)
-                .hasMessage("Not supported for cursor-based pagination. Instead use afterKey or afterCursor to provide a cursor or obtain the nextPageRequest from a CursoredPage.");
-    }
 }
diff --git a/tck/src/main/java/ee/jakarta/tck/data/standalone/entity/EntityTests.java b/tck/src/main/java/ee/jakarta/tck/data/standalone/entity/EntityTests.java
index a76a8dba..bf8514f2 100644
--- a/tck/src/main/java/ee/jakarta/tck/data/standalone/entity/EntityTests.java
+++ b/tck/src/main/java/ee/jakarta/tck/data/standalone/entity/EntityTests.java
@@ -65,6 +65,7 @@ import jakarta.data.exceptions.EmptyResultException;
 import jakarta.data.exceptions.NonUniqueResultException;
 import jakarta.data.page.Page;
 import jakarta.data.page.PageRequest;
+import jakarta.data.page.PageRequest.Cursor;
 import jakarta.inject.Inject;
 
 /**
@@ -371,7 +372,7 @@ public class EntityTests {
 
     @Assertion(id = "133", strategy = "Request a Slice higher than the final Slice, expecting an empty Slice with 0 results.")
     public void testBeyondFinalSlice() {
-        PageRequest sixth = PageRequest.ofSize(5).page(6).withoutTotal();
+        PageRequest sixth = PageRequest.ofPage(6).size(5).withoutTotal();
         Page<NaturalNumber> page = numbers.findByNumTypeAndFloorOfSquareRootLessThanEqual(NumberType.PRIME, 8L,
                 sixth, Sort.desc("id"));
         assertEquals(0, page.numberOfElements());
@@ -499,7 +500,7 @@ public class EntityTests {
 
     @Assertion(id = "133", strategy = "Request the last Page of up to 10 results, expecting to find the final 3.")
     public void testFinalPageOfUpTo10() {
-        PageRequest fifthPageRequest = PageRequest.ofSize(10).page(5);
+        PageRequest fifthPageRequest = PageRequest.ofPage(5).size(10);
         Page<AsciiCharacter> page;
         try {
             page = characters.findByNumericValueBetween(48, 90, fifthPageRequest,
@@ -548,7 +549,7 @@ public class EntityTests {
 
     @Assertion(id = "133", strategy = "Request the last Slice of up to 5 results, expecting to find the final 2.")
     public void testFinalSliceOfUpTo5() {
-        PageRequest fifth = PageRequest.ofSize(5).page(5).withoutTotal();
+        PageRequest fifth = PageRequest.ofPage(5).size(5).withoutTotal();
         Page<NaturalNumber> page = numbers.findByNumTypeAndFloorOfSquareRootLessThanEqual(NumberType.PRIME, 8L,
                 fifth, Sort.desc("id"));
         assertEquals(true, page.hasContent());
@@ -941,8 +942,9 @@ public class EntityTests {
         //                                                                                  ^^^^^ next page ^^^^
 
         Order<NaturalNumber> order = Order.by(Sort.asc("floorOfSquareRoot"), Sort.desc("id"));
-        PageRequest middle7 = PageRequest.ofPage(4).size(7)
-                        .afterKey((short) 5, 5L, 26L); // 20th result is 26; it requires 5 bits and its square root rounds down to 5.
+        PageRequest middle7 = PageRequest.afterCursor(
+                Cursor.forKey((short) 5, 5L, 26L), // 20th result is 26; it requires 5 bits and its square root rounds down to 5.),
+                4L, 7, true);
 
         CursoredPage<NaturalNumber> page;
         try {
@@ -1047,8 +1049,9 @@ public class EntityTests {
         //                                  ^^^^^^^^ slice 2 ^^^^^^^^^
         //                                                                                        ^^^^^^^^ slice 3 ^^^^^^^^^
 
-        PageRequest middle9 = PageRequest.ofPage(4).size(9).withoutTotal()
-                             .afterKey(6L, 46L); // 20th result is 46; its square root rounds down to 6.
+        PageRequest middle9 = PageRequest.afterCursor(
+                Cursor.forKey(6L, 46L), // 20th result is 46; its square root rounds down to 6.
+                4L, 9, false);
         Order<NaturalNumber> order = Order.by(Sort.desc("floorOfSquareRoot"), Sort.asc("id"));
 
         CursoredPage<NaturalNumber> slice;
@@ -1104,7 +1107,7 @@ public class EntityTests {
     @Assertion(id = "133", strategy = "Request a CursoredPage of results where none match the query, expecting an empty CursoredPage with 0 results.")
     public void testCursoredPageWithoutTotalOfNothing() {
         // There are no numbers larger than 30 which have a square root that rounds down to 3.
-        PageRequest pagination = PageRequest.ofSize(33).afterKey(30L).withoutTotal();
+        PageRequest pagination = PageRequest.ofSize(33).afterCursor(Cursor.forKey(30L)).withoutTotal();
 
         CursoredPage<NaturalNumber> slice;
         try {