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

etudenhoefner pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git


The following commit(s) were added to refs/heads/main by this push:
     new 7600ba74fa Core: Add pagination when listing namespaces/tables/views 
(#9782)
7600ba74fa is described below

commit 7600ba74faef15b4d5593e9ada255e6dc999d0ed
Author: Rahil C <[email protected]>
AuthorDate: Thu May 2 23:35:38 2024 -0700

    Core: Add pagination when listing namespaces/tables/views (#9782)
---
 .../org/apache/iceberg/rest/CatalogHandlers.java   |  58 +++++++++
 .../apache/iceberg/rest/RESTSessionCatalog.java    |  99 ++++++++++----
 .../rest/responses/ListNamespacesResponse.java     |  21 ++-
 .../iceberg/rest/responses/ListTablesResponse.java |  21 ++-
 .../apache/iceberg/rest/RESTCatalogAdapter.java    |  32 ++++-
 .../org/apache/iceberg/rest/TestRESTCatalog.java   | 145 +++++++++++++++++++++
 .../apache/iceberg/rest/TestRESTViewCatalog.java   |  85 ++++++++++++
 .../rest/responses/TestListNamespacesResponse.java |  29 ++++-
 .../rest/responses/TestListTablesResponse.java     |  29 ++++-
 9 files changed, 476 insertions(+), 43 deletions(-)

diff --git a/core/src/main/java/org/apache/iceberg/rest/CatalogHandlers.java 
b/core/src/main/java/org/apache/iceberg/rest/CatalogHandlers.java
index e4e3c065fb..746da5ffca 100644
--- a/core/src/main/java/org/apache/iceberg/rest/CatalogHandlers.java
+++ b/core/src/main/java/org/apache/iceberg/rest/CatalogHandlers.java
@@ -80,6 +80,7 @@ import org.apache.iceberg.view.ViewRepresentation;
 
 public class CatalogHandlers {
   private static final Schema EMPTY_SCHEMA = new Schema();
+  private static final String INTIAL_PAGE_TOKEN = "";
 
   private CatalogHandlers() {}
 
@@ -117,6 +118,29 @@ public class CatalogHandlers {
     return ListNamespacesResponse.builder().addAll(results).build();
   }
 
+  public static ListNamespacesResponse listNamespaces(
+      SupportsNamespaces catalog, Namespace parent, String pageToken, String 
pageSize) {
+    List<Namespace> results;
+    List<Namespace> subResults;
+
+    if (parent.isEmpty()) {
+      results = catalog.listNamespaces();
+    } else {
+      results = catalog.listNamespaces(parent);
+    }
+
+    int start = INTIAL_PAGE_TOKEN.equals(pageToken) ? 0 : 
Integer.parseInt(pageToken);
+    int end = start + Integer.parseInt(pageSize);
+    subResults = results.subList(start, end);
+    String nextToken = String.valueOf(end);
+
+    if (end >= results.size()) {
+      nextToken = null;
+    }
+
+    return 
ListNamespacesResponse.builder().addAll(subResults).nextPageToken(nextToken).build();
+  }
+
   public static CreateNamespaceResponse createNamespace(
       SupportsNamespaces catalog, CreateNamespaceRequest request) {
     Namespace namespace = request.namespace();
@@ -174,6 +198,23 @@ public class CatalogHandlers {
     return ListTablesResponse.builder().addAll(idents).build();
   }
 
+  public static ListTablesResponse listTables(
+      Catalog catalog, Namespace namespace, String pageToken, String pageSize) 
{
+    List<TableIdentifier> results = catalog.listTables(namespace);
+    List<TableIdentifier> subResults;
+
+    int start = INTIAL_PAGE_TOKEN.equals(pageToken) ? 0 : 
Integer.parseInt(pageToken);
+    int end = start + Integer.parseInt(pageSize);
+    subResults = results.subList(start, end);
+    String nextToken = String.valueOf(end);
+
+    if (end >= results.size()) {
+      nextToken = null;
+    }
+
+    return 
ListTablesResponse.builder().addAll(subResults).nextPageToken(nextToken).build();
+  }
+
   public static LoadTableResponse stageTableCreate(
       Catalog catalog, Namespace namespace, CreateTableRequest request) {
     request.validate();
@@ -397,6 +438,23 @@ public class CatalogHandlers {
     return 
ListTablesResponse.builder().addAll(catalog.listViews(namespace)).build();
   }
 
+  public static ListTablesResponse listViews(
+      ViewCatalog catalog, Namespace namespace, String pageToken, String 
pageSize) {
+    List<TableIdentifier> results = catalog.listViews(namespace);
+    List<TableIdentifier> subResults;
+
+    int start = INTIAL_PAGE_TOKEN.equals(pageToken) ? 0 : 
Integer.parseInt(pageToken);
+    int end = start + Integer.parseInt(pageSize);
+    subResults = results.subList(start, end);
+    String nextToken = String.valueOf(end);
+
+    if (end >= results.size()) {
+      nextToken = null;
+    }
+
+    return 
ListTablesResponse.builder().addAll(subResults).nextPageToken(nextToken).build();
+  }
+
   public static LoadViewResponse createView(
       ViewCatalog catalog, Namespace namespace, CreateViewRequest request) {
     request.validate();
diff --git a/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java 
b/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
index 94f3057f9f..dcf92289df 100644
--- a/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
+++ b/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
@@ -114,6 +114,7 @@ public class RESTSessionCatalog extends 
BaseViewSessionCatalog
   private static final String DEFAULT_FILE_IO_IMPL = 
"org.apache.iceberg.io.ResolvingFileIO";
   private static final String REST_METRICS_REPORTING_ENABLED = 
"rest-metrics-reporting-enabled";
   private static final String REST_SNAPSHOT_LOADING_MODE = 
"snapshot-loading-mode";
+  public static final String REST_PAGE_SIZE = "rest-page-size";
   private static final List<String> TOKEN_PREFERENCE_ORDER =
       ImmutableList.of(
           OAuth2Properties.ID_TOKEN_TYPE,
@@ -136,6 +137,7 @@ public class RESTSessionCatalog extends 
BaseViewSessionCatalog
   private FileIO io = null;
   private MetricsReporter reporter = null;
   private boolean reportingViaRestEnabled;
+  private Integer pageSize = null;
   private CloseableGroup closeables = null;
 
   // a lazy thread pool for token refresh
@@ -228,6 +230,12 @@ public class RESTSessionCatalog extends 
BaseViewSessionCatalog
               client, tokenRefreshExecutor(name), token, 
expiresAtMillis(mergedProps), catalogAuth);
     }
 
+    this.pageSize = PropertyUtil.propertyAsNullableInt(mergedProps, 
REST_PAGE_SIZE);
+    if (pageSize != null) {
+      Preconditions.checkArgument(
+          pageSize > 0, "Invalid value for %s, must be a positive integer", 
REST_PAGE_SIZE);
+    }
+
     this.io = newFileIO(SessionContext.createEmpty(), mergedProps);
 
     this.fileIOCloser = newFileIOCloser();
@@ -278,14 +286,27 @@ public class RESTSessionCatalog extends 
BaseViewSessionCatalog
   @Override
   public List<TableIdentifier> listTables(SessionContext context, Namespace 
ns) {
     checkNamespaceIsValid(ns);
+    Map<String, String> queryParams = Maps.newHashMap();
+    ImmutableList.Builder<TableIdentifier> tables = ImmutableList.builder();
+    String pageToken = "";
+    if (pageSize != null) {
+      queryParams.put("pageSize", String.valueOf(pageSize));
+    }
 
-    ListTablesResponse response =
-        client.get(
-            paths.tables(ns),
-            ListTablesResponse.class,
-            headers(context),
-            ErrorHandlers.namespaceErrorHandler());
-    return response.identifiers();
+    do {
+      queryParams.put("pageToken", pageToken);
+      ListTablesResponse response =
+          client.get(
+              paths.tables(ns),
+              queryParams,
+              ListTablesResponse.class,
+              headers(context),
+              ErrorHandlers.namespaceErrorHandler());
+      pageToken = response.nextPageToken();
+      tables.addAll(response.identifiers());
+    } while (pageToken != null);
+
+    return tables.build();
   }
 
   @Override
@@ -494,22 +515,31 @@ public class RESTSessionCatalog extends 
BaseViewSessionCatalog
 
   @Override
   public List<Namespace> listNamespaces(SessionContext context, Namespace 
namespace) {
-    Map<String, String> queryParams;
-    if (namespace.isEmpty()) {
-      queryParams = ImmutableMap.of();
-    } else {
-      // query params should be unescaped
-      queryParams = ImmutableMap.of("parent", 
RESTUtil.NAMESPACE_JOINER.join(namespace.levels()));
+    Map<String, String> queryParams = Maps.newHashMap();
+    if (!namespace.isEmpty()) {
+      queryParams.put("parent", 
RESTUtil.NAMESPACE_JOINER.join(namespace.levels()));
     }
 
-    ListNamespacesResponse response =
-        client.get(
-            paths.namespaces(),
-            queryParams,
-            ListNamespacesResponse.class,
-            headers(context),
-            ErrorHandlers.namespaceErrorHandler());
-    return response.namespaces();
+    ImmutableList.Builder<Namespace> namespaces = ImmutableList.builder();
+    String pageToken = "";
+    if (pageSize != null) {
+      queryParams.put("pageSize", String.valueOf(pageSize));
+    }
+
+    do {
+      queryParams.put("pageToken", pageToken);
+      ListNamespacesResponse response =
+          client.get(
+              paths.namespaces(),
+              queryParams,
+              ListNamespacesResponse.class,
+              headers(context),
+              ErrorHandlers.namespaceErrorHandler());
+      pageToken = response.nextPageToken();
+      namespaces.addAll(response.namespaces());
+    } while (pageToken != null);
+
+    return namespaces.build();
   }
 
   @Override
@@ -1048,14 +1078,27 @@ public class RESTSessionCatalog extends 
BaseViewSessionCatalog
   @Override
   public List<TableIdentifier> listViews(SessionContext context, Namespace 
namespace) {
     checkNamespaceIsValid(namespace);
+    Map<String, String> queryParams = Maps.newHashMap();
+    ImmutableList.Builder<TableIdentifier> views = ImmutableList.builder();
+    String pageToken = "";
+    if (pageSize != null) {
+      queryParams.put("pageSize", String.valueOf(pageSize));
+    }
 
-    ListTablesResponse response =
-        client.get(
-            paths.views(namespace),
-            ListTablesResponse.class,
-            headers(context),
-            ErrorHandlers.namespaceErrorHandler());
-    return response.identifiers();
+    do {
+      queryParams.put("pageToken", pageToken);
+      ListTablesResponse response =
+          client.get(
+              paths.views(namespace),
+              queryParams,
+              ListTablesResponse.class,
+              headers(context),
+              ErrorHandlers.namespaceErrorHandler());
+      pageToken = response.nextPageToken();
+      views.addAll(response.identifiers());
+    } while (pageToken != null);
+
+    return views.build();
   }
 
   @Override
diff --git 
a/core/src/main/java/org/apache/iceberg/rest/responses/ListNamespacesResponse.java
 
b/core/src/main/java/org/apache/iceberg/rest/responses/ListNamespacesResponse.java
index 13a599e1a7..8feeda6f2b 100644
--- 
a/core/src/main/java/org/apache/iceberg/rest/responses/ListNamespacesResponse.java
+++ 
b/core/src/main/java/org/apache/iceberg/rest/responses/ListNamespacesResponse.java
@@ -29,13 +29,15 @@ import org.apache.iceberg.rest.RESTResponse;
 public class ListNamespacesResponse implements RESTResponse {
 
   private List<Namespace> namespaces;
+  private String nextPageToken;
 
   public ListNamespacesResponse() {
     // Required for Jackson deserialization
   }
 
-  private ListNamespacesResponse(List<Namespace> namespaces) {
+  private ListNamespacesResponse(List<Namespace> namespaces, String 
nextPageToken) {
     this.namespaces = namespaces;
+    this.nextPageToken = nextPageToken;
     validate();
   }
 
@@ -48,9 +50,16 @@ public class ListNamespacesResponse implements RESTResponse {
     return namespaces != null ? namespaces : ImmutableList.of();
   }
 
+  public String nextPageToken() {
+    return nextPageToken;
+  }
+
   @Override
   public String toString() {
-    return MoreObjects.toStringHelper(this).add("namespaces", 
namespaces()).toString();
+    return MoreObjects.toStringHelper(this)
+        .add("namespaces", namespaces())
+        .add("next-page-token", nextPageToken())
+        .toString();
   }
 
   public static Builder builder() {
@@ -59,6 +68,7 @@ public class ListNamespacesResponse implements RESTResponse {
 
   public static class Builder {
     private final ImmutableList.Builder<Namespace> namespaces = 
ImmutableList.builder();
+    private String nextPageToken;
 
     private Builder() {}
 
@@ -75,8 +85,13 @@ public class ListNamespacesResponse implements RESTResponse {
       return this;
     }
 
+    public Builder nextPageToken(String pageToken) {
+      nextPageToken = pageToken;
+      return this;
+    }
+
     public ListNamespacesResponse build() {
-      return new ListNamespacesResponse(namespaces.build());
+      return new ListNamespacesResponse(namespaces.build(), nextPageToken);
     }
   }
 }
diff --git 
a/core/src/main/java/org/apache/iceberg/rest/responses/ListTablesResponse.java 
b/core/src/main/java/org/apache/iceberg/rest/responses/ListTablesResponse.java
index 3c99c12c90..1db05709b4 100644
--- 
a/core/src/main/java/org/apache/iceberg/rest/responses/ListTablesResponse.java
+++ 
b/core/src/main/java/org/apache/iceberg/rest/responses/ListTablesResponse.java
@@ -30,13 +30,15 @@ import org.apache.iceberg.rest.RESTResponse;
 public class ListTablesResponse implements RESTResponse {
 
   private List<TableIdentifier> identifiers;
+  private String nextPageToken;
 
   public ListTablesResponse() {
     // Required for Jackson deserialization
   }
 
-  private ListTablesResponse(List<TableIdentifier> identifiers) {
+  private ListTablesResponse(List<TableIdentifier> identifiers, String 
nextPageToken) {
     this.identifiers = identifiers;
+    this.nextPageToken = nextPageToken;
     validate();
   }
 
@@ -49,9 +51,16 @@ public class ListTablesResponse implements RESTResponse {
     return identifiers != null ? identifiers : ImmutableList.of();
   }
 
+  public String nextPageToken() {
+    return nextPageToken;
+  }
+
   @Override
   public String toString() {
-    return MoreObjects.toStringHelper(this).add("identifiers", 
identifiers).toString();
+    return MoreObjects.toStringHelper(this)
+        .add("identifiers", identifiers)
+        .add("next-page-token", nextPageToken())
+        .toString();
   }
 
   public static Builder builder() {
@@ -60,6 +69,7 @@ public class ListTablesResponse implements RESTResponse {
 
   public static class Builder {
     private final ImmutableList.Builder<TableIdentifier> identifiers = 
ImmutableList.builder();
+    private String nextPageToken;
 
     private Builder() {}
 
@@ -76,8 +86,13 @@ public class ListTablesResponse implements RESTResponse {
       return this;
     }
 
+    public Builder nextPageToken(String pageToken) {
+      nextPageToken = pageToken;
+      return this;
+    }
+
     public ListTablesResponse build() {
-      return new ListTablesResponse(identifiers.build());
+      return new ListTablesResponse(identifiers.build(), nextPageToken);
     }
   }
 }
diff --git a/core/src/test/java/org/apache/iceberg/rest/RESTCatalogAdapter.java 
b/core/src/test/java/org/apache/iceberg/rest/RESTCatalogAdapter.java
index 7fccc4e974..357b05e85c 100644
--- a/core/src/test/java/org/apache/iceberg/rest/RESTCatalogAdapter.java
+++ b/core/src/test/java/org/apache/iceberg/rest/RESTCatalogAdapter.java
@@ -298,7 +298,17 @@ public class RESTCatalogAdapter implements RESTClient {
             ns = Namespace.empty();
           }
 
-          return castResponse(responseType, 
CatalogHandlers.listNamespaces(asNamespaceCatalog, ns));
+          String pageToken = PropertyUtil.propertyAsString(vars, "pageToken", 
null);
+          String pageSize = PropertyUtil.propertyAsString(vars, "pageSize", 
null);
+
+          if (pageSize != null) {
+            return castResponse(
+                responseType,
+                CatalogHandlers.listNamespaces(asNamespaceCatalog, ns, 
pageToken, pageSize));
+          } else {
+            return castResponse(
+                responseType, 
CatalogHandlers.listNamespaces(asNamespaceCatalog, ns));
+          }
         }
         break;
 
@@ -339,7 +349,14 @@ public class RESTCatalogAdapter implements RESTClient {
       case LIST_TABLES:
         {
           Namespace namespace = namespaceFromPathVars(vars);
-          return castResponse(responseType, 
CatalogHandlers.listTables(catalog, namespace));
+          String pageToken = PropertyUtil.propertyAsString(vars, "pageToken", 
null);
+          String pageSize = PropertyUtil.propertyAsString(vars, "pageSize", 
null);
+          if (pageSize != null) {
+            return castResponse(
+                responseType, CatalogHandlers.listTables(catalog, namespace, 
pageToken, pageSize));
+          } else {
+            return castResponse(responseType, 
CatalogHandlers.listTables(catalog, namespace));
+          }
         }
 
       case CREATE_TABLE:
@@ -412,7 +429,16 @@ public class RESTCatalogAdapter implements RESTClient {
         {
           if (null != asViewCatalog) {
             Namespace namespace = namespaceFromPathVars(vars);
-            return castResponse(responseType, 
CatalogHandlers.listViews(asViewCatalog, namespace));
+            String pageToken = PropertyUtil.propertyAsString(vars, 
"pageToken", null);
+            String pageSize = PropertyUtil.propertyAsString(vars, "pageSize", 
null);
+            if (pageSize != null) {
+              return castResponse(
+                  responseType,
+                  CatalogHandlers.listViews(asViewCatalog, namespace, 
pageToken, pageSize));
+            } else {
+              return castResponse(
+                  responseType, CatalogHandlers.listViews(asViewCatalog, 
namespace));
+            }
           }
           break;
         }
diff --git a/core/src/test/java/org/apache/iceberg/rest/TestRESTCatalog.java 
b/core/src/test/java/org/apache/iceberg/rest/TestRESTCatalog.java
index 18d832b3cd..95380424e7 100644
--- a/core/src/test/java/org/apache/iceberg/rest/TestRESTCatalog.java
+++ b/core/src/test/java/org/apache/iceberg/rest/TestRESTCatalog.java
@@ -77,7 +77,10 @@ import org.apache.iceberg.rest.auth.OAuth2Properties;
 import org.apache.iceberg.rest.auth.OAuth2Util;
 import org.apache.iceberg.rest.requests.UpdateTableRequest;
 import org.apache.iceberg.rest.responses.ConfigResponse;
+import org.apache.iceberg.rest.responses.CreateNamespaceResponse;
 import org.apache.iceberg.rest.responses.ErrorResponse;
+import org.apache.iceberg.rest.responses.ListNamespacesResponse;
+import org.apache.iceberg.rest.responses.ListTablesResponse;
 import org.apache.iceberg.rest.responses.LoadTableResponse;
 import org.apache.iceberg.rest.responses.OAuthTokenResponse;
 import org.apache.iceberg.types.Types;
@@ -2329,6 +2332,148 @@ public class TestRESTCatalog extends 
CatalogTests<RESTCatalog> {
     assertThat(schema2.columns()).hasSize(1);
   }
 
+  @Test
+  public void testInvalidPageSize() {
+    RESTCatalogAdapter adapter = Mockito.spy(new 
RESTCatalogAdapter(backendCatalog));
+    RESTCatalog catalog =
+        new RESTCatalog(SessionCatalog.SessionContext.createEmpty(), (config) 
-> adapter);
+    Assertions.assertThatThrownBy(
+            () ->
+                catalog.initialize(
+                    "test", ImmutableMap.of(RESTSessionCatalog.REST_PAGE_SIZE, 
"-1")))
+        .isInstanceOf(IllegalArgumentException.class)
+        .hasMessage(
+            String.format(
+                "Invalid value for %s, must be a positive integer",
+                RESTSessionCatalog.REST_PAGE_SIZE));
+  }
+
+  @Test
+  public void testPaginationForListNamespaces() {
+    RESTCatalogAdapter adapter = Mockito.spy(new 
RESTCatalogAdapter(backendCatalog));
+    RESTCatalog catalog =
+        new RESTCatalog(SessionCatalog.SessionContext.createEmpty(), (config) 
-> adapter);
+    catalog.initialize("test", 
ImmutableMap.of(RESTSessionCatalog.REST_PAGE_SIZE, "10"));
+    int numberOfItems = 30;
+    String namespaceName = "newdb";
+
+    // create several namespaces for listing and verify
+    for (int i = 0; i < numberOfItems; i++) {
+      String nameSpaceName = namespaceName + i;
+      catalog.createNamespace(Namespace.of(nameSpaceName));
+    }
+
+    assertThat(catalog.listNamespaces()).hasSize(numberOfItems);
+
+    Mockito.verify(adapter)
+        .execute(
+            eq(HTTPMethod.GET),
+            eq("v1/config"),
+            any(),
+            any(),
+            eq(ConfigResponse.class),
+            any(),
+            any());
+
+    Mockito.verify(adapter, times(numberOfItems))
+        .execute(
+            eq(HTTPMethod.POST),
+            eq("v1/namespaces"),
+            any(),
+            any(),
+            eq(CreateNamespaceResponse.class),
+            any(),
+            any());
+
+    // verify initial request with empty pageToken
+    Mockito.verify(adapter)
+        .handleRequest(
+            eq(RESTCatalogAdapter.Route.LIST_NAMESPACES),
+            eq(ImmutableMap.of("pageToken", "", "pageSize", "10")),
+            any(),
+            eq(ListNamespacesResponse.class));
+
+    // verify second request with updated pageToken
+    Mockito.verify(adapter)
+        .handleRequest(
+            eq(RESTCatalogAdapter.Route.LIST_NAMESPACES),
+            eq(ImmutableMap.of("pageToken", "10", "pageSize", "10")),
+            any(),
+            eq(ListNamespacesResponse.class));
+
+    // verify third request with update pageToken
+    Mockito.verify(adapter)
+        .handleRequest(
+            eq(RESTCatalogAdapter.Route.LIST_NAMESPACES),
+            eq(ImmutableMap.of("pageToken", "20", "pageSize", "10")),
+            any(),
+            eq(ListNamespacesResponse.class));
+  }
+
+  @Test
+  public void testPaginationForListTables() {
+    RESTCatalogAdapter adapter = Mockito.spy(new 
RESTCatalogAdapter(backendCatalog));
+    RESTCatalog catalog =
+        new RESTCatalog(SessionCatalog.SessionContext.createEmpty(), (config) 
-> adapter);
+    catalog.initialize("test", 
ImmutableMap.of(RESTSessionCatalog.REST_PAGE_SIZE, "10"));
+    int numberOfItems = 30;
+    String namespaceName = "newdb";
+    String tableName = "newtable";
+    catalog.createNamespace(Namespace.of(namespaceName));
+
+    // create several tables under namespace for listing and verify
+    for (int i = 0; i < numberOfItems; i++) {
+      TableIdentifier tableIdentifier = TableIdentifier.of(namespaceName, 
tableName + i);
+      catalog.createTable(tableIdentifier, SCHEMA);
+    }
+
+    
assertThat(catalog.listTables(Namespace.of(namespaceName))).hasSize(numberOfItems);
+
+    Mockito.verify(adapter)
+        .execute(
+            eq(HTTPMethod.GET),
+            eq("v1/config"),
+            any(),
+            any(),
+            eq(ConfigResponse.class),
+            any(),
+            any());
+
+    Mockito.verify(adapter, times(numberOfItems))
+        .execute(
+            eq(HTTPMethod.POST),
+            eq(String.format("v1/namespaces/%s/tables", namespaceName)),
+            any(),
+            any(),
+            eq(LoadTableResponse.class),
+            any(),
+            any());
+
+    // verify initial request with empty pageToken
+    Mockito.verify(adapter)
+        .handleRequest(
+            eq(RESTCatalogAdapter.Route.LIST_TABLES),
+            eq(ImmutableMap.of("pageToken", "", "pageSize", "10", "namespace", 
namespaceName)),
+            any(),
+            eq(ListTablesResponse.class));
+
+    // verify second request with updated pageToken
+    Mockito.verify(adapter)
+        .handleRequest(
+            eq(RESTCatalogAdapter.Route.LIST_TABLES),
+            eq(ImmutableMap.of("pageToken", "10", "pageSize", "10", 
"namespace", namespaceName)),
+            any(),
+            eq(ListTablesResponse.class));
+
+    // verify third request with update pageToken
+    Mockito.verify(adapter)
+        .handleRequest(
+            eq(RESTCatalogAdapter.Route.LIST_TABLES),
+            eq(ImmutableMap.of("pageToken", "20", "pageSize", "10", 
"namespace", namespaceName)),
+            any(),
+            eq(ListTablesResponse.class));
+  }
+
   @Test
   public void testCleanupUncommitedFilesForCleanableFailures() {
     RESTCatalogAdapter adapter = Mockito.spy(new 
RESTCatalogAdapter(backendCatalog));
diff --git 
a/core/src/test/java/org/apache/iceberg/rest/TestRESTViewCatalog.java 
b/core/src/test/java/org/apache/iceberg/rest/TestRESTViewCatalog.java
index 0b29da7042..f67c4b078e 100644
--- a/core/src/test/java/org/apache/iceberg/rest/TestRESTViewCatalog.java
+++ b/core/src/test/java/org/apache/iceberg/rest/TestRESTViewCatalog.java
@@ -18,20 +18,31 @@
  */
 package org.apache.iceberg.rest;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import java.io.File;
 import java.nio.file.Path;
+import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 import java.util.function.Consumer;
 import org.apache.iceberg.CatalogProperties;
 import org.apache.iceberg.catalog.Catalog;
+import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.SessionCatalog;
+import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.iceberg.inmemory.InMemoryCatalog;
 import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
 import org.apache.iceberg.rest.RESTCatalogAdapter.HTTPMethod;
+import org.apache.iceberg.rest.responses.ConfigResponse;
 import org.apache.iceberg.rest.responses.ErrorResponse;
+import org.apache.iceberg.rest.responses.ListTablesResponse;
+import org.apache.iceberg.rest.responses.LoadViewResponse;
 import org.apache.iceberg.view.ViewCatalogTests;
 import org.eclipse.jetty.server.Server;
 import org.eclipse.jetty.server.handler.gzip.GzipHandler;
@@ -39,7 +50,9 @@ import org.eclipse.jetty.servlet.ServletContextHandler;
 import org.eclipse.jetty.servlet.ServletHolder;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
+import org.mockito.Mockito;
 
 public class TestRESTViewCatalog extends ViewCatalogTests<RESTCatalog> {
   private static final ObjectMapper MAPPER = RESTObjectMapper.mapper();
@@ -144,6 +157,78 @@ public class TestRESTViewCatalog extends 
ViewCatalogTests<RESTCatalog> {
     }
   }
 
+  @Test
+  public void testPaginationForListViews() {
+    RESTCatalogAdapter adapter = Mockito.spy(new 
RESTCatalogAdapter(backendCatalog));
+    RESTCatalog catalog =
+        new RESTCatalog(SessionCatalog.SessionContext.createEmpty(), (config) 
-> adapter);
+    catalog.initialize("test", 
ImmutableMap.of(RESTSessionCatalog.REST_PAGE_SIZE, "10"));
+
+    int numberOfItems = 30;
+    String namespaceName = "newdb";
+    String viewName = "newview";
+
+    // create initial namespace
+    catalog().createNamespace(Namespace.of(namespaceName));
+
+    // create several views under namespace, based off a table for listing and 
verify
+    for (int i = 0; i < numberOfItems; i++) {
+      TableIdentifier viewIndentifier = TableIdentifier.of(namespaceName, 
viewName + i);
+      catalog
+          .buildView(viewIndentifier)
+          .withSchema(SCHEMA)
+          .withDefaultNamespace(viewIndentifier.namespace())
+          .withQuery("spark", "select * from ns.tbl")
+          .create();
+    }
+    List<TableIdentifier> views = 
catalog.listViews(Namespace.of(namespaceName));
+    assertThat(views).hasSize(numberOfItems);
+
+    Mockito.verify(adapter)
+        .execute(
+            eq(HTTPMethod.GET),
+            eq("v1/config"),
+            any(),
+            any(),
+            eq(ConfigResponse.class),
+            any(),
+            any());
+
+    Mockito.verify(adapter, times(numberOfItems))
+        .execute(
+            eq(HTTPMethod.POST),
+            eq(String.format("v1/namespaces/%s/views", namespaceName)),
+            any(),
+            any(),
+            eq(LoadViewResponse.class),
+            any(),
+            any());
+
+    // verify initial request with empty pageToken
+    Mockito.verify(adapter)
+        .handleRequest(
+            eq(RESTCatalogAdapter.Route.LIST_VIEWS),
+            eq(ImmutableMap.of("pageToken", "", "pageSize", "10", "namespace", 
namespaceName)),
+            any(),
+            eq(ListTablesResponse.class));
+
+    // verify second request with update pageToken
+    Mockito.verify(adapter)
+        .handleRequest(
+            eq(RESTCatalogAdapter.Route.LIST_VIEWS),
+            eq(ImmutableMap.of("pageToken", "10", "pageSize", "10", 
"namespace", namespaceName)),
+            any(),
+            eq(ListTablesResponse.class));
+
+    // verify third request with update pageToken
+    Mockito.verify(adapter)
+        .handleRequest(
+            eq(RESTCatalogAdapter.Route.LIST_VIEWS),
+            eq(ImmutableMap.of("pageToken", "20", "pageSize", "10", 
"namespace", namespaceName)),
+            any(),
+            eq(ListTablesResponse.class));
+  }
+
   @Override
   protected RESTCatalog catalog() {
     return restCatalog;
diff --git 
a/core/src/test/java/org/apache/iceberg/rest/responses/TestListNamespacesResponse.java
 
b/core/src/test/java/org/apache/iceberg/rest/responses/TestListNamespacesResponse.java
index bfe5a662b2..d9ed801de0 100644
--- 
a/core/src/test/java/org/apache/iceberg/rest/responses/TestListNamespacesResponse.java
+++ 
b/core/src/test/java/org/apache/iceberg/rest/responses/TestListNamespacesResponse.java
@@ -34,11 +34,11 @@ public class TestListNamespacesResponse extends 
RequestResponseTestBase<ListName
 
   @Test
   public void testRoundTripSerDe() throws JsonProcessingException {
-    String fullJson = "{\"namespaces\":[[\"accounting\"],[\"tax\"]]}";
+    String fullJson = 
"{\"namespaces\":[[\"accounting\"],[\"tax\"]],\"next-page-token\":null}";
     ListNamespacesResponse fullValue = 
ListNamespacesResponse.builder().addAll(NAMESPACES).build();
     assertRoundTripSerializesEquallyFrom(fullJson, fullValue);
 
-    String emptyNamespaces = "{\"namespaces\":[]}";
+    String emptyNamespaces = "{\"namespaces\":[],\"next-page-token\":null}";
     assertRoundTripSerializesEquallyFrom(emptyNamespaces, 
ListNamespacesResponse.builder().build());
   }
 
@@ -83,9 +83,32 @@ public class TestListNamespacesResponse extends 
RequestResponseTestBase<ListName
         .hasMessage("Invalid namespace: null");
   }
 
+  @Test
+  public void testWithNullPaginationToken() throws JsonProcessingException {
+    String jsonWithNullPageToken =
+        
"{\"namespaces\":[[\"accounting\"],[\"tax\"]],\"next-page-token\":null}";
+    ListNamespacesResponse response =
+        
ListNamespacesResponse.builder().addAll(NAMESPACES).nextPageToken(null).build();
+    assertRoundTripSerializesEquallyFrom(jsonWithNullPageToken, response);
+    Assertions.assertThat(response.nextPageToken()).isNull();
+    Assertions.assertThat(response.namespaces()).isEqualTo(NAMESPACES);
+  }
+
+  @Test
+  public void testWithPaginationToken() throws JsonProcessingException {
+    String pageToken = "token";
+    String jsonWithPageToken =
+        
"{\"namespaces\":[[\"accounting\"],[\"tax\"]],\"next-page-token\":\"token\"}";
+    ListNamespacesResponse response =
+        
ListNamespacesResponse.builder().addAll(NAMESPACES).nextPageToken(pageToken).build();
+    assertRoundTripSerializesEquallyFrom(jsonWithPageToken, response);
+    Assertions.assertThat(response.nextPageToken()).isEqualTo("token");
+    Assertions.assertThat(response.namespaces()).isEqualTo(NAMESPACES);
+  }
+
   @Override
   public String[] allFieldsFromSpec() {
-    return new String[] {"namespaces"};
+    return new String[] {"namespaces", "next-page-token"};
   }
 
   @Override
diff --git 
a/core/src/test/java/org/apache/iceberg/rest/responses/TestListTablesResponse.java
 
b/core/src/test/java/org/apache/iceberg/rest/responses/TestListTablesResponse.java
index 116d43a6d1..d46228f188 100644
--- 
a/core/src/test/java/org/apache/iceberg/rest/responses/TestListTablesResponse.java
+++ 
b/core/src/test/java/org/apache/iceberg/rest/responses/TestListTablesResponse.java
@@ -36,11 +36,11 @@ public class TestListTablesResponse extends 
RequestResponseTestBase<ListTablesRe
   @Test
   public void testRoundTripSerDe() throws JsonProcessingException {
     String fullJson =
-        
"{\"identifiers\":[{\"namespace\":[\"accounting\",\"tax\"],\"name\":\"paid\"}]}";
+        
"{\"identifiers\":[{\"namespace\":[\"accounting\",\"tax\"],\"name\":\"paid\"}],\"next-page-token\":null}";
     assertRoundTripSerializesEquallyFrom(
         fullJson, ListTablesResponse.builder().addAll(IDENTIFIERS).build());
 
-    String emptyIdentifiers = "{\"identifiers\":[]}";
+    String emptyIdentifiers = "{\"identifiers\":[],\"next-page-token\":null}";
     assertRoundTripSerializesEquallyFrom(emptyIdentifiers, 
ListTablesResponse.builder().build());
   }
 
@@ -105,9 +105,32 @@ public class TestListTablesResponse extends 
RequestResponseTestBase<ListTablesRe
         .hasMessage("Invalid table identifier: null");
   }
 
+  @Test
+  public void testWithNullPaginationToken() throws JsonProcessingException {
+    String jsonWithNullPageToken =
+        
"{\"identifiers\":[{\"namespace\":[\"accounting\",\"tax\"],\"name\":\"paid\"}],\"next-page-token\":null}";
+    ListTablesResponse response =
+        
ListTablesResponse.builder().addAll(IDENTIFIERS).nextPageToken(null).build();
+    assertRoundTripSerializesEquallyFrom(jsonWithNullPageToken, response);
+    Assertions.assertThat(response.nextPageToken()).isNull();
+    Assertions.assertThat(response.identifiers()).isEqualTo(IDENTIFIERS);
+  }
+
+  @Test
+  public void testWithPaginationToken() throws JsonProcessingException {
+    String pageToken = "token";
+    String jsonWithPageToken =
+        
"{\"identifiers\":[{\"namespace\":[\"accounting\",\"tax\"],\"name\":\"paid\"}],\"next-page-token\":\"token\"}";
+    ListTablesResponse response =
+        
ListTablesResponse.builder().addAll(IDENTIFIERS).nextPageToken(pageToken).build();
+    assertRoundTripSerializesEquallyFrom(jsonWithPageToken, response);
+    Assertions.assertThat(response.nextPageToken()).isEqualTo("token");
+    Assertions.assertThat(response.identifiers()).isEqualTo(IDENTIFIERS);
+  }
+
   @Override
   public String[] allFieldsFromSpec() {
-    return new String[] {"identifiers"};
+    return new String[] {"identifiers", "next-page-token"};
   }
 
   @Override

Reply via email to