This is an automated email from the ASF dual-hosted git repository.

mchades pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/branch-1.3 by this push:
     new 54c0e3bdc2 [Cherry-pick to branch-1.3] [#10669] feat(iceberg-rest): 
Add pagination support for list endpoints (#10671) (#11585)
54c0e3bdc2 is described below

commit 54c0e3bdc22cad3c1143c5a875ac4dd1b188ac50
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Jun 11 13:00:19 2026 +0800

    [Cherry-pick to branch-1.3] [#10669] feat(iceberg-rest): Add pagination 
support for list endpoints (#10671) (#11585)
    
    **Cherry-pick Information:**
    - Original commit: b98b4be9f88232d994409d3bc9f617b31c2174d0
    - Target branch: `branch-1.3`
    - Status: ✅ Clean cherry-pick (no conflicts)
    
    Co-authored-by: Akshay Thorat <[email protected]>
---
 .../service/rest/IcebergNamespaceOperations.java   |   8 +-
 .../service/rest/IcebergPaginationHelper.java      | 155 +++++++++++++++
 .../service/rest/IcebergTableOperations.java       |   9 +-
 .../service/rest/IcebergViewOperations.java        |  11 +-
 .../rest/TestIcebergNamespaceOperations.java       |  47 +++++
 .../service/rest/TestIcebergPaginationHelper.java  | 216 +++++++++++++++++++++
 .../service/rest/TestIcebergTableOperations.java   |  43 ++++
 .../service/rest/TestIcebergViewOperations.java    |  44 +++++
 8 files changed, 530 insertions(+), 3 deletions(-)

diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergNamespaceOperations.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergNamespaceOperations.java
index ca5807e2f4..9d33d5ddfd 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergNamespaceOperations.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergNamespaceOperations.java
@@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.annotations.VisibleForTesting;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import java.util.regex.Pattern;
 import javax.inject.Inject;
 import javax.servlet.http.HttpServletRequest;
@@ -101,7 +102,9 @@ public class IcebergNamespaceOperations {
       accessMetadataType = MetadataObject.Type.CATALOG)
   public Response listNamespaces(
       @DefaultValue("") @Encoded() @QueryParam("parent") String parent,
-      @AuthorizationMetadata(type = Entity.EntityType.CATALOG) 
@PathParam("prefix") String prefix) {
+      @AuthorizationMetadata(type = Entity.EntityType.CATALOG) 
@PathParam("prefix") String prefix,
+      @QueryParam("pageToken") String pageToken,
+      @QueryParam("pageSize") Integer pageSize) {
     String catalogName = IcebergRESTUtils.getCatalogName(prefix);
     Namespace parentNamespace =
         parent.isEmpty()
@@ -124,6 +127,9 @@ public class IcebergNamespaceOperations {
               response =
                   filterListNamespacesResponse(response, 
authContext.metalakeName(), catalogName);
             }
+            response =
+                IcebergPaginationHelper.paginateNamespaces(
+                    response, Optional.ofNullable(pageToken), 
Optional.ofNullable(pageSize));
             return IcebergRESTUtils.ok(response);
           });
     } catch (Exception e) {
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergPaginationHelper.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergPaginationHelper.java
new file mode 100644
index 0000000000..625a342562
--- /dev/null
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergPaginationHelper.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *  http://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.apache.gravitino.iceberg.service.rest;
+
+import com.google.common.base.Preconditions;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.rest.responses.ListNamespacesResponse;
+import org.apache.iceberg.rest.responses.ListTablesResponse;
+
+/**
+ * Utility for applying cursor-based in-memory pagination to Iceberg list 
responses.
+ *
+ * <p>Items are sorted deterministically by name and the page token is the 
name of the last item on
+ * the current page. On the next request, items strictly after the cursor (by 
string comparison) are
+ * returned. This is more resilient than offset-based pagination when the 
underlying catalog is
+ * modified between page requests.
+ *
+ * <p><b>Known limitations:</b>
+ *
+ * <ul>
+ *   <li>This is in-memory pagination: the full item list is materialized from 
the underlying
+ *       catalog on every page request, so server-side memory usage is 
unchanged. Paging through N
+ *       items is O(N²) total work. True server-push-down pagination would 
require catalog
+ *       implementations to add native support.
+ *   <li>Items created after the cursor but alphabetically before the last 
item on the current page
+ *       will be missed on subsequent pages. This is the same behavior as most 
keyset pagination
+ *       implementations and is generally acceptable.
+ *   <li>When {@code pageToken} is provided without {@code pageSize}, the 
response returns all items
+ *       after the cursor with no {@code nextPageToken}.
+ * </ul>
+ */
+class IcebergPaginationHelper {
+
+  private IcebergPaginationHelper() {}
+
+  /**
+   * Paginate a {@link ListNamespacesResponse}.
+   *
+   * @param response the full (unpaginated) response
+   * @param pageToken cursor-based page token (name of last item on previous 
page), or empty for the
+   *     first page
+   * @param pageSize maximum items per page, or empty for no limit
+   * @return a new response containing only the requested page
+   */
+  static ListNamespacesResponse paginateNamespaces(
+      ListNamespacesResponse response, Optional<String> pageToken, 
Optional<Integer> pageSize) {
+    PaginatedPage<Namespace> page =
+        paginate(response.namespaces(), pageToken, pageSize, 
Namespace::toString);
+    ListNamespacesResponse.Builder builder = 
ListNamespacesResponse.builder().addAll(page.items);
+    page.nextPageToken.ifPresent(builder::nextPageToken);
+    return builder.build();
+  }
+
+  /**
+   * Paginate a {@link ListTablesResponse}. Works for both table and view list 
responses.
+   *
+   * @param response the full (unpaginated) response
+   * @param pageToken cursor-based page token (name of last item on previous 
page), or empty for the
+   *     first page
+   * @param pageSize maximum items per page, or empty for no limit
+   * @return a new response containing only the requested page
+   */
+  static ListTablesResponse paginateTables(
+      ListTablesResponse response, Optional<String> pageToken, 
Optional<Integer> pageSize) {
+    PaginatedPage<TableIdentifier> page =
+        paginate(response.identifiers(), pageToken, pageSize, 
TableIdentifier::toString);
+    ListTablesResponse.Builder builder = 
ListTablesResponse.builder().addAll(page.items);
+    page.nextPageToken.ifPresent(builder::nextPageToken);
+    return builder.build();
+  }
+
+  /**
+   * Core pagination logic shared by all list endpoints.
+   *
+   * <p>Items are sorted by {@code keyExtractor} to produce a stable ordering, 
then sliced using the
+   * cursor token. The next-page token is the key of the last item on the 
returned page.
+   *
+   * @param items the complete list of items to paginate
+   * @param pageToken cursor string (key of last item on previous page), or 
empty for the first page
+   * @param pageSize maximum items per page, or empty for no limit
+   * @param keyExtractor function to extract a comparable cursor key from each 
item
+   * @return a {@link PaginatedPage} containing the requested page of items 
and optional next token
+   */
+  static <T> PaginatedPage<T> paginate(
+      List<T> items,
+      Optional<String> pageToken,
+      Optional<Integer> pageSize,
+      Function<T, String> keyExtractor) {
+    String token = pageToken.orElse("");
+    if (!pageSize.isPresent() && token.isEmpty()) {
+      return new PaginatedPage<>(items, Optional.empty());
+    }
+
+    pageSize.ifPresent(
+        size -> Preconditions.checkArgument(size > 0, "pageSize must be 
positive, got: %s", size));
+
+    List<T> sorted =
+        
items.stream().sorted(Comparator.comparing(keyExtractor)).collect(Collectors.toList());
+
+    int startIdx = 0;
+    if (!token.isEmpty()) {
+      startIdx = sorted.size();
+      for (int i = 0; i < sorted.size(); i++) {
+        if (keyExtractor.apply(sorted.get(i)).compareTo(token) > 0) {
+          startIdx = i;
+          break;
+        }
+      }
+    }
+
+    int limit = pageSize.orElse(sorted.size());
+    int end = Math.min(startIdx + limit, sorted.size());
+    List<T> page = sorted.subList(startIdx, end);
+
+    Optional<String> nextToken = Optional.empty();
+    if (end < sorted.size() && !page.isEmpty()) {
+      nextToken = Optional.of(keyExtractor.apply(page.get(page.size() - 1)));
+    }
+
+    return new PaginatedPage<>(page, nextToken);
+  }
+
+  /** Holds a page of items and an optional token for the next page. */
+  static class PaginatedPage<T> {
+    final List<T> items;
+    final Optional<String> nextPageToken;
+
+    PaginatedPage(List<T> items, Optional<String> nextPageToken) {
+      this.items = items;
+      this.nextPageToken = nextPageToken;
+    }
+  }
+}
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergTableOperations.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergTableOperations.java
index a3eec15dd0..029722cfd8 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergTableOperations.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergTableOperations.java
@@ -123,7 +123,9 @@ public class IcebergTableOperations {
   public Response listTable(
       @AuthorizationMetadata(type = Entity.EntityType.CATALOG) 
@PathParam("prefix") String prefix,
       @AuthorizationMetadata(type = EntityType.SCHEMA) @Encoded() 
@PathParam("namespace")
-          String namespace) {
+          String namespace,
+      @QueryParam("pageToken") String pageToken,
+      @QueryParam("pageSize") Integer pageSize) {
     String catalogName = IcebergRESTUtils.getCatalogName(prefix);
     Namespace icebergNS =
         RESTUtil.decodeNamespace(namespace, 
IcebergRESTUtils.NAMESPACE_SEPARATOR_URLENCODED_UTF_8);
@@ -143,6 +145,11 @@ public class IcebergTableOperations {
                   filterListTablesResponse(
                       listTablesResponse, authContext.metalakeName(), 
catalogName);
             }
+            listTablesResponse =
+                IcebergPaginationHelper.paginateTables(
+                    listTablesResponse,
+                    Optional.ofNullable(pageToken),
+                    Optional.ofNullable(pageSize));
             return IcebergRESTUtils.ok(listTablesResponse);
           });
     } catch (Exception e) {
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewOperations.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewOperations.java
index ff01399e0a..b33a53b2e6 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewOperations.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewOperations.java
@@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.annotations.VisibleForTesting;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import javax.inject.Inject;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.Consumes;
@@ -36,6 +37,7 @@ import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
@@ -97,7 +99,9 @@ public class IcebergViewOperations {
   public Response listView(
       @AuthorizationMetadata(type = Entity.EntityType.CATALOG) 
@PathParam("prefix") String prefix,
       @AuthorizationMetadata(type = EntityType.SCHEMA) @Encoded() 
@PathParam("namespace")
-          String namespace) {
+          String namespace,
+      @QueryParam("pageToken") String pageToken,
+      @QueryParam("pageSize") Integer pageSize) {
     String catalogName = IcebergRESTUtils.getCatalogName(prefix);
     Namespace icebergNS =
         RESTUtil.decodeNamespace(namespace, 
IcebergRESTUtils.NAMESPACE_SEPARATOR_URLENCODED_UTF_8);
@@ -117,6 +121,11 @@ public class IcebergViewOperations {
                   filterListViewsResponse(
                       listTablesResponse, authContext.metalakeName(), 
catalogName);
             }
+            listTablesResponse =
+                IcebergPaginationHelper.paginateTables(
+                    listTablesResponse,
+                    Optional.ofNullable(pageToken),
+                    Optional.ofNullable(pageSize));
             return IcebergRESTUtils.ok(listTablesResponse);
           });
     } catch (Exception e) {
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergNamespaceOperations.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergNamespaceOperations.java
index c7c8f95bfb..dab90563fc 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergNamespaceOperations.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergNamespaceOperations.java
@@ -18,8 +18,12 @@
  */
 package org.apache.gravitino.iceberg.service.rest;
 
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import java.util.Arrays;
 import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.client.Entity;
 import javax.ws.rs.core.Application;
@@ -58,6 +62,7 @@ import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.rest.requests.CreateTableRequest;
 import org.apache.iceberg.rest.requests.ImmutableRegisterTableRequest;
 import org.apache.iceberg.rest.requests.RegisterTableRequest;
+import org.apache.iceberg.rest.responses.ListNamespacesResponse;
 import org.apache.iceberg.rest.responses.LoadTableResponse;
 import org.apache.iceberg.types.Types.NestedField;
 import org.apache.iceberg.types.Types.StringType;
@@ -307,6 +312,48 @@ public class TestIcebergNamespaceOperations extends 
IcebergNamespaceTestBase {
     verifyListNamespaceFail(Optional.of(Namespace.of("list_foo3", "a", "x")), 
404);
   }
 
+  @ParameterizedTest
+  @ValueSource(strings = {"", IcebergRestTestUtil.PREFIX})
+  void testListNamespaceWithPagination(String prefix) {
+    setUrlPathWithPrefix(prefix);
+    dropAllExistingNamespace();
+
+    doCreateNamespace(Namespace.of("page_ns1"));
+    doCreateNamespace(Namespace.of("page_ns2"));
+    doCreateNamespace(Namespace.of("page_ns3"));
+
+    // First page: pageSize=2, no pageToken
+    Response firstPageResponse =
+        getNamespaceClientBuilder(
+                Optional.empty(), Optional.empty(), 
Optional.of(ImmutableMap.of("pageSize", "2")))
+            .get();
+    Assertions.assertEquals(Status.OK.getStatusCode(), 
firstPageResponse.getStatus());
+    ListNamespacesResponse firstPage = 
firstPageResponse.readEntity(ListNamespacesResponse.class);
+    Assertions.assertEquals(2, firstPage.namespaces().size());
+    Assertions.assertNotNull(firstPage.nextPageToken());
+
+    // Second page using the nextPageToken
+    Response secondPageResponse =
+        getNamespaceClientBuilder(
+                Optional.empty(),
+                Optional.empty(),
+                Optional.of(
+                    ImmutableMap.of("pageToken", firstPage.nextPageToken(), 
"pageSize", "2")))
+            .get();
+    Assertions.assertEquals(Status.OK.getStatusCode(), 
secondPageResponse.getStatus());
+    ListNamespacesResponse secondPage = 
secondPageResponse.readEntity(ListNamespacesResponse.class);
+    Assertions.assertEquals(1, secondPage.namespaces().size());
+    Assertions.assertNull(secondPage.nextPageToken());
+
+    // Verify combined results
+    Set<String> paginatedNames =
+        java.util.stream.Stream.concat(
+                firstPage.namespaces().stream(), 
secondPage.namespaces().stream())
+            .map(Namespace::toString)
+            .collect(Collectors.toSet());
+    Assertions.assertEquals(ImmutableSet.of("page_ns1", "page_ns2", 
"page_ns3"), paginatedNames);
+  }
+
   @Test
   @SuppressWarnings("deprecation")
   void 
testIcebergListNamespacesEventDeprecatedConstructorReturnsNegativeCount() {
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergPaginationHelper.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergPaginationHelper.java
new file mode 100644
index 0000000000..63804c9752
--- /dev/null
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergPaginationHelper.java
@@ -0,0 +1,216 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *  http://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.apache.gravitino.iceberg.service.rest;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.rest.responses.ListNamespacesResponse;
+import org.apache.iceberg.rest.responses.ListTablesResponse;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestIcebergPaginationHelper {
+
+  @Test
+  void testPaginateNamespacesNoPagination() {
+    ListNamespacesResponse response =
+        
ListNamespacesResponse.builder().add(Namespace.of("ns1")).add(Namespace.of("ns2")).build();
+    ListNamespacesResponse result =
+        IcebergPaginationHelper.paginateNamespaces(response, Optional.empty(), 
Optional.empty());
+    Assertions.assertEquals(2, result.namespaces().size());
+    Assertions.assertNull(result.nextPageToken());
+  }
+
+  @Test
+  void testPaginateNamespacesFirstPage() {
+    ListNamespacesResponse response =
+        ListNamespacesResponse.builder()
+            .add(Namespace.of("ns1"))
+            .add(Namespace.of("ns2"))
+            .add(Namespace.of("ns3"))
+            .build();
+    ListNamespacesResponse result =
+        IcebergPaginationHelper.paginateNamespaces(response, Optional.empty(), 
Optional.of(2));
+    Assertions.assertEquals(2, result.namespaces().size());
+    Assertions.assertNotNull(result.nextPageToken());
+  }
+
+  @Test
+  void testPaginateNamespacesSecondPage() {
+    ListNamespacesResponse response =
+        ListNamespacesResponse.builder()
+            .add(Namespace.of("ns1"))
+            .add(Namespace.of("ns2"))
+            .add(Namespace.of("ns3"))
+            .build();
+    // First page
+    ListNamespacesResponse firstPage =
+        IcebergPaginationHelper.paginateNamespaces(response, Optional.empty(), 
Optional.of(2));
+    // Second page using cursor from first page
+    ListNamespacesResponse secondPage =
+        IcebergPaginationHelper.paginateNamespaces(
+            response, Optional.ofNullable(firstPage.nextPageToken()), 
Optional.of(2));
+    Assertions.assertEquals(1, secondPage.namespaces().size());
+    Assertions.assertNull(secondPage.nextPageToken());
+  }
+
+  @Test
+  void testPaginateNamespacesSortsDeterministically() {
+    // Items added in reverse order should still paginate in sorted order
+    ListNamespacesResponse response =
+        ListNamespacesResponse.builder()
+            .add(Namespace.of("ns3"))
+            .add(Namespace.of("ns1"))
+            .add(Namespace.of("ns2"))
+            .build();
+    ListNamespacesResponse result =
+        IcebergPaginationHelper.paginateNamespaces(response, Optional.empty(), 
Optional.of(2));
+    List<String> names =
+        
result.namespaces().stream().map(Namespace::toString).collect(Collectors.toList());
+    Assertions.assertEquals(Arrays.asList("ns1", "ns2"), names);
+  }
+
+  @Test
+  void testPaginateNamespacesCursorBeyondAllItems() {
+    ListNamespacesResponse response =
+        
ListNamespacesResponse.builder().add(Namespace.of("ns1")).add(Namespace.of("ns2")).build();
+    // Cursor after all items alphabetically
+    ListNamespacesResponse result =
+        IcebergPaginationHelper.paginateNamespaces(response, 
Optional.of("zzzz"), Optional.of(10));
+    Assertions.assertEquals(0, result.namespaces().size());
+    Assertions.assertNull(result.nextPageToken());
+  }
+
+  @Test
+  void testPaginateNamespacesPageTokenWithoutPageSize() {
+    ListNamespacesResponse response =
+        ListNamespacesResponse.builder()
+            .add(Namespace.of("ns1"))
+            .add(Namespace.of("ns2"))
+            .add(Namespace.of("ns3"))
+            .build();
+    // pageToken without pageSize returns all items after cursor
+    ListNamespacesResponse result =
+        IcebergPaginationHelper.paginateNamespaces(response, 
Optional.of("ns1"), Optional.empty());
+    List<String> names =
+        
result.namespaces().stream().map(Namespace::toString).collect(Collectors.toList());
+    Assertions.assertEquals(Arrays.asList("ns2", "ns3"), names);
+    Assertions.assertNull(result.nextPageToken());
+  }
+
+  @Test
+  void testPaginateNamespacesZeroPageSizeThrows() {
+    ListNamespacesResponse response =
+        ListNamespacesResponse.builder().add(Namespace.of("ns1")).build();
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            IcebergPaginationHelper.paginateNamespaces(response, 
Optional.empty(), Optional.of(0)));
+  }
+
+  @Test
+  void testPaginateNamespacesNegativePageSizeThrows() {
+    ListNamespacesResponse response =
+        ListNamespacesResponse.builder().add(Namespace.of("ns1")).build();
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            IcebergPaginationHelper.paginateNamespaces(
+                response, Optional.empty(), Optional.of(-1)));
+  }
+
+  @Test
+  void testPaginateNamespacesEmptyList() {
+    ListNamespacesResponse response = ListNamespacesResponse.builder().build();
+    ListNamespacesResponse result =
+        IcebergPaginationHelper.paginateNamespaces(response, Optional.empty(), 
Optional.of(10));
+    Assertions.assertEquals(0, result.namespaces().size());
+    Assertions.assertNull(result.nextPageToken());
+  }
+
+  @Test
+  void testPaginateTablesFirstPage() {
+    Namespace ns = Namespace.of("db");
+    ListTablesResponse response =
+        ListTablesResponse.builder()
+            .add(TableIdentifier.of(ns, "t1"))
+            .add(TableIdentifier.of(ns, "t2"))
+            .add(TableIdentifier.of(ns, "t3"))
+            .build();
+    ListTablesResponse result =
+        IcebergPaginationHelper.paginateTables(response, Optional.empty(), 
Optional.of(2));
+    Assertions.assertEquals(2, result.identifiers().size());
+    Assertions.assertNotNull(result.nextPageToken());
+  }
+
+  @Test
+  void testPaginateTablesFullWalk() {
+    Namespace ns = Namespace.of("db");
+    ListTablesResponse response =
+        ListTablesResponse.builder()
+            .add(TableIdentifier.of(ns, "t1"))
+            .add(TableIdentifier.of(ns, "t2"))
+            .add(TableIdentifier.of(ns, "t3"))
+            .add(TableIdentifier.of(ns, "t4"))
+            .add(TableIdentifier.of(ns, "t5"))
+            .build();
+
+    // Walk all pages with pageSize=2
+    ListTablesResponse page1 =
+        IcebergPaginationHelper.paginateTables(response, Optional.empty(), 
Optional.of(2));
+    Assertions.assertEquals(2, page1.identifiers().size());
+    Assertions.assertNotNull(page1.nextPageToken());
+
+    ListTablesResponse page2 =
+        IcebergPaginationHelper.paginateTables(
+            response, Optional.ofNullable(page1.nextPageToken()), 
Optional.of(2));
+    Assertions.assertEquals(2, page2.identifiers().size());
+    Assertions.assertNotNull(page2.nextPageToken());
+
+    ListTablesResponse page3 =
+        IcebergPaginationHelper.paginateTables(
+            response, Optional.ofNullable(page2.nextPageToken()), 
Optional.of(2));
+    Assertions.assertEquals(1, page3.identifiers().size());
+    Assertions.assertNull(page3.nextPageToken());
+
+    // Verify all 5 items were returned
+    int totalItems =
+        page1.identifiers().size() + page2.identifiers().size() + 
page3.identifiers().size();
+    Assertions.assertEquals(5, totalItems);
+  }
+
+  @Test
+  void testPaginateTablesExactPageSize() {
+    Namespace ns = Namespace.of("db");
+    ListTablesResponse response =
+        ListTablesResponse.builder()
+            .add(TableIdentifier.of(ns, "t1"))
+            .add(TableIdentifier.of(ns, "t2"))
+            .build();
+    // pageSize equals total items
+    ListTablesResponse result =
+        IcebergPaginationHelper.paginateTables(response, Optional.empty(), 
Optional.of(2));
+    Assertions.assertEquals(2, result.identifiers().size());
+    Assertions.assertNull(result.nextPageToken());
+  }
+}
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergTableOperations.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergTableOperations.java
index 13accb3c73..14d2782b4f 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergTableOperations.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergTableOperations.java
@@ -310,6 +310,49 @@ public class TestIcebergTableOperations extends 
IcebergNamespaceTestBase {
     Assertions.assertEquals(2, ((IcebergListTableEvent) 
listTablePostEvent).resultCount());
   }
 
+  @ParameterizedTest
+  @MethodSource(
+      
"org.apache.gravitino.iceberg.service.rest.IcebergRestTestUtil#testPrefixesAndNamespaces")
+  void testListTablesWithPagination(String prefix, Namespace namespace) {
+    setUrlPathWithPrefix(prefix);
+    verifyCreateNamespaceSucc(namespace);
+    verifyCreateTableSucc(namespace, "page_t1");
+    verifyCreateTableSucc(namespace, "page_t2");
+    verifyCreateTableSucc(namespace, "page_t3");
+
+    dummyEventListener.clearEvent();
+
+    // First page: pageSize=2
+    String tablePath =
+        IcebergRestTestUtil.NAMESPACE_PATH + "/" + 
RESTUtil.encodeNamespace(namespace) + "/tables";
+    Response firstPageResponse =
+        getIcebergClientBuilder(tablePath, 
Optional.of(ImmutableMap.of("pageSize", "2"))).get();
+    Assertions.assertEquals(Status.OK.getStatusCode(), 
firstPageResponse.getStatus());
+    ListTablesResponse firstPage = 
firstPageResponse.readEntity(ListTablesResponse.class);
+    Assertions.assertEquals(2, firstPage.identifiers().size());
+    Assertions.assertNotNull(firstPage.nextPageToken());
+
+    // Second page using nextPageToken
+    Response secondPageResponse =
+        getIcebergClientBuilder(
+                tablePath,
+                Optional.of(
+                    ImmutableMap.of("pageToken", firstPage.nextPageToken(), 
"pageSize", "2")))
+            .get();
+    Assertions.assertEquals(Status.OK.getStatusCode(), 
secondPageResponse.getStatus());
+    ListTablesResponse secondPage = 
secondPageResponse.readEntity(ListTablesResponse.class);
+    Assertions.assertEquals(1, secondPage.identifiers().size());
+    Assertions.assertNull(secondPage.nextPageToken());
+
+    // Verify combined results
+    Set<String> paginatedNames =
+        java.util.stream.Stream.concat(
+                firstPage.identifiers().stream(), 
secondPage.identifiers().stream())
+            .map(id -> id.name())
+            .collect(Collectors.toSet());
+    Assertions.assertEquals(ImmutableSet.of("page_t1", "page_t2", "page_t3"), 
paginatedNames);
+  }
+
   @ParameterizedTest
   
@MethodSource("org.apache.gravitino.iceberg.service.rest.IcebergRestTestUtil#testNamespaces")
   void testTableExits(Namespace namespace) {
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergViewOperations.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergViewOperations.java
index 2c47a44739..4cb65b64ce 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergViewOperations.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergViewOperations.java
@@ -19,6 +19,7 @@
 
 package org.apache.gravitino.iceberg.service.rest;
 
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import java.util.Arrays;
 import java.util.Optional;
@@ -134,6 +135,49 @@ public class TestIcebergViewOperations extends 
IcebergNamespaceTestBase {
     Assertions.assertEquals(2, ((IcebergListViewEvent) 
listViewPostEvent).resultCount());
   }
 
+  @ParameterizedTest
+  @MethodSource(
+      
"org.apache.gravitino.iceberg.service.rest.IcebergRestTestUtil#testPrefixesAndNamespaces")
+  void testListViewsWithPagination(String prefix, Namespace namespace) {
+    setUrlPathWithPrefix(prefix);
+    verifyCreateNamespaceSucc(namespace);
+    verifyCreateViewSucc(namespace, "page_v1");
+    verifyCreateViewSucc(namespace, "page_v2");
+    verifyCreateViewSucc(namespace, "page_v3");
+
+    dummyEventListener.clearEvent();
+
+    // First page: pageSize=2
+    String viewPath =
+        IcebergRestTestUtil.NAMESPACE_PATH + "/" + 
RESTUtil.encodeNamespace(namespace) + "/views";
+    Response firstPageResponse =
+        getIcebergClientBuilder(viewPath, 
Optional.of(ImmutableMap.of("pageSize", "2"))).get();
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
firstPageResponse.getStatus());
+    ListTablesResponse firstPage = 
firstPageResponse.readEntity(ListTablesResponse.class);
+    Assertions.assertEquals(2, firstPage.identifiers().size());
+    Assertions.assertNotNull(firstPage.nextPageToken());
+
+    // Second page using nextPageToken
+    Response secondPageResponse =
+        getIcebergClientBuilder(
+                viewPath,
+                Optional.of(
+                    ImmutableMap.of("pageToken", firstPage.nextPageToken(), 
"pageSize", "2")))
+            .get();
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
secondPageResponse.getStatus());
+    ListTablesResponse secondPage = 
secondPageResponse.readEntity(ListTablesResponse.class);
+    Assertions.assertEquals(1, secondPage.identifiers().size());
+    Assertions.assertNull(secondPage.nextPageToken());
+
+    // Verify combined results
+    Set<String> paginatedNames =
+        java.util.stream.Stream.concat(
+                firstPage.identifiers().stream(), 
secondPage.identifiers().stream())
+            .map(id -> id.name())
+            .collect(Collectors.toSet());
+    Assertions.assertEquals(ImmutableSet.of("page_v1", "page_v2", "page_v3"), 
paginatedNames);
+  }
+
   @ParameterizedTest
   
@MethodSource("org.apache.gravitino.iceberg.service.rest.IcebergRestTestUtil#testNamespaces")
   void testCreateView(Namespace namespace) {

Reply via email to