This is an automated email from the ASF dual-hosted git repository.
deniskuzZ pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hive.git
The following commit(s) were added to refs/heads/master by this push:
new 9d9a51e103f HIVE-29878: Support pagination for list endpoints in HMS
REST Catalog and remove redundant type casting in REST Catalog (#6750)
9d9a51e103f is described below
commit 9d9a51e103fddfeb855e46e1606b36cab770a56e
Author: Raghav Aggarwal <[email protected]>
AuthorDate: Sat Sep 26 19:27:00 2026 +0530
HIVE-29878: Support pagination for list endpoints in HMS REST Catalog and
remove redundant type casting in REST Catalog (#6750)
---
.../org/apache/iceberg/rest/HMSCatalogAdapter.java | 106 ++++++++---------
.../rest/TestHMSCatalogAdapterPagination.java | 127 +++++++++++++++++++++
2 files changed, 177 insertions(+), 56 deletions(-)
diff --git
a/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogAdapter.java
b/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogAdapter.java
index 127b0ac6663..aa3813e533f 100644
---
a/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogAdapter.java
+++
b/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogAdapter.java
@@ -37,6 +37,7 @@
import org.apache.iceberg.catalog.Namespace;
import org.apache.iceberg.catalog.SupportsNamespaces;
import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.commons.lang3.math.NumberUtils;
import org.apache.iceberg.catalog.ViewCatalog;
import org.apache.iceberg.exceptions.AlreadyExistsException;
import org.apache.iceberg.exceptions.CommitFailedException;
@@ -89,6 +90,10 @@ public class HMSCatalogAdapter implements Closeable {
private static final Logger LOG =
LoggerFactory.getLogger(HMSCatalogAdapter.class);
private static final Splitter SLASH = Splitter.on('/').omitEmptyStrings();
+ private static final String PAGE_TOKEN = "pageToken";
+ private static final String PAGE_SIZE = "pageSize";
+ private static final String PARENT = "parent";
+
private static final String PREFIX_VAR = "prefix";
private static final String PREFIX_PLACEHOLDER = "{" + PREFIX_VAR + "}";
@@ -275,23 +280,40 @@ public Class<? extends RESTRequest> requestClass() {
private ConfigResponse config() {
final List<Endpoint> endpoints = Arrays.stream(Route.values())
.map(r -> Endpoint.create(r.method.name(), r.pathTemplate)).toList();
- return castResponse(ConfigResponse.class,
ConfigResponse.builder().withEndpoints(endpoints).build());
+ return ConfigResponse.builder().withEndpoints(endpoints).build();
}
- private ListNamespacesResponse listNamespaces(Map<String, String> vars) {
- Namespace namespace;
- if (vars.containsKey("parent")) {
- namespace =
Namespace.of(RESTUtil.NAMESPACE_SPLITTER.splitToStream(vars.get("parent")).toArray(String[]::new));
- } else {
- namespace = Namespace.empty();
+ /**
+ * Paging parameters of a list request. Without pageSize the listing is
unpaged, expressed as a
+ * single unbounded first page. This relies on CatalogHandlers.paginate
treating a null token as
+ * the first page and returning a null next-page-token once the list is
exhausted.
+ */
+ private record PageRequest(String token, String size) {
+ private static final PageRequest UNPAGED =
+ new PageRequest(null, String.valueOf(Integer.MAX_VALUE));
+
+ static PageRequest from(Map<String, String> vars) {
+ String size = vars.get(PAGE_SIZE);
+ if (size == null) {
+ return UNPAGED; // token ignored, as upstream; keeping it would
overflow token + MAX_VALUE
+ }
+ Preconditions.checkArgument(NumberUtils.toInt(size, 0) > 0,
+ "Invalid %s: %s, must be a positive integer", PAGE_SIZE, size);
+ return new PageRequest(vars.get(PAGE_TOKEN), size);
}
- return castResponse(ListNamespacesResponse.class,
CatalogHandlers.listNamespaces(asNamespaceCatalog, namespace));
+ }
+
+ private ListNamespacesResponse listNamespaces(Map<String, String> vars) {
+ Namespace parent = vars.containsKey(PARENT)
+ ? RESTUtil.namespaceFromQueryParam(vars.get(PARENT))
+ : Namespace.empty();
+ PageRequest page = PageRequest.from(vars);
+ return CatalogHandlers.listNamespaces(asNamespaceCatalog, parent,
page.token(), page.size());
}
private CreateNamespaceResponse createNamespace(Object body) {
CreateNamespaceRequest request = castRequest(CreateNamespaceRequest.class,
body);
- return castResponse(
- CreateNamespaceResponse.class,
CatalogHandlers.createNamespace(asNamespaceCatalog, request));
+ return CatalogHandlers.createNamespace(asNamespaceCatalog, request);
}
private RESTResponse namespaceExists(Map<String, String> vars) {
@@ -302,8 +324,7 @@ private RESTResponse namespaceExists(Map<String, String>
vars) {
private GetNamespaceResponse loadNamespace(Map<String, String> vars) {
Namespace namespace = namespaceFromPathVars(vars);
- return castResponse(
- GetNamespaceResponse.class,
CatalogHandlers.loadNamespace(asNamespaceCatalog, namespace));
+ return CatalogHandlers.loadNamespace(asNamespaceCatalog, namespace);
}
private RESTResponse dropNamespace(Map<String, String> vars) {
@@ -315,29 +336,25 @@ private UpdateNamespacePropertiesResponse
updateNamespace(Map<String, String> va
Namespace namespace = namespaceFromPathVars(vars);
UpdateNamespacePropertiesRequest request =
castRequest(UpdateNamespacePropertiesRequest.class, body);
- return castResponse(
- UpdateNamespacePropertiesResponse.class,
- CatalogHandlers.updateNamespaceProperties(asNamespaceCatalog,
namespace, request));
+ return CatalogHandlers.updateNamespaceProperties(asNamespaceCatalog,
namespace, request);
}
private ListTablesResponse listTables(Map<String, String> vars) {
Namespace namespace = namespaceFromPathVars(vars);
- return castResponse(ListTablesResponse.class,
CatalogHandlers.listTables(catalog, namespace));
+ PageRequest page = PageRequest.from(vars);
+ return CatalogHandlers.listTables(catalog, namespace, page.token(),
page.size());
}
private LoadTableResponse createTable(Map<String, String> vars, Object body)
{
- final Class<LoadTableResponse> responseType = LoadTableResponse.class;
Namespace namespace = namespaceFromPathVars(vars);
CreateTableRequest request = castRequest(CreateTableRequest.class, body);
request.validate();
if (request.stageCreate()) {
Map<String, String> namespaceMetadata =
asNamespaceCatalog.loadNamespaceMetadata(namespace);
icebergAuthorizer.validateStageCreateTable(catalogName, namespace,
namespaceMetadata, request);
- return castResponse(
- responseType, CatalogHandlers.stageTableCreate(catalog,
namespace, request));
+ return CatalogHandlers.stageTableCreate(catalog, namespace, request);
} else {
- return castResponse(
- responseType, CatalogHandlers.createTable(catalog, namespace,
request));
+ return CatalogHandlers.createTable(catalog, namespace, request);
}
}
@@ -358,19 +375,19 @@ private RESTResponse tableExists(Map<String, String>
vars) {
private LoadTableResponse loadTable(Map<String, String> vars) {
TableIdentifier ident = identFromPathVars(vars);
- return castResponse(LoadTableResponse.class,
CatalogHandlers.loadTable(catalog, ident));
+ return CatalogHandlers.loadTable(catalog, ident);
}
private LoadTableResponse registerTable(Map<String, String> vars, Object
body) {
- Namespace namespace = namespaceFromPathVars(vars);
- RegisterTableRequest request = castRequest(RegisterTableRequest.class,
body);
- return castResponse(LoadTableResponse.class,
CatalogHandlers.registerTable(catalog, namespace, request));
+ Namespace namespace = namespaceFromPathVars(vars);
+ RegisterTableRequest request = castRequest(RegisterTableRequest.class,
body);
+ return CatalogHandlers.registerTable(catalog, namespace, request);
}
private LoadTableResponse updateTable(Map<String, String> vars, Object body)
{
TableIdentifier ident = identFromPathVars(vars);
UpdateTableRequest request = castRequest(UpdateTableRequest.class, body);
- return castResponse(LoadTableResponse.class,
CatalogHandlers.updateTable(catalog, ident, request));
+ return CatalogHandlers.updateTable(catalog, ident, request);
}
private RESTResponse renameTable(Object body) {
@@ -395,23 +412,14 @@ private RESTResponse commitTransaction(Object body) {
private ListTablesResponse listViews(Map<String, String> vars) {
Namespace namespace = namespaceFromPathVars(vars);
- String pageToken = PropertyUtil.propertyAsString(vars, "pageToken", null);
- String pageSize = PropertyUtil.propertyAsString(vars, "pageSize", null);
- if (pageSize != null) {
- return castResponse(
- ListTablesResponse.class,
- CatalogHandlers.listViews(asViewCatalog, namespace, pageToken,
pageSize));
- } else {
- return castResponse(
- ListTablesResponse.class, CatalogHandlers.listViews(asViewCatalog,
namespace));
- }
+ PageRequest page = PageRequest.from(vars);
+ return CatalogHandlers.listViews(asViewCatalog, namespace, page.token(),
page.size());
}
private LoadViewResponse createView(Map<String, String> vars, Object body) {
Namespace namespace = namespaceFromPathVars(vars);
CreateViewRequest request = castRequest(CreateViewRequest.class, body);
- return castResponse(
- LoadViewResponse.class, CatalogHandlers.createView(asViewCatalog,
namespace, request));
+ return CatalogHandlers.createView(asViewCatalog, namespace, request);
}
private RESTResponse viewExists(Map<String, String> vars) {
@@ -422,14 +430,13 @@ private RESTResponse viewExists(Map<String, String> vars)
{
private LoadViewResponse loadView(Map<String, String> vars) {
TableIdentifier ident = viewIdentFromPathVars(vars);
- return castResponse(LoadViewResponse.class,
CatalogHandlers.loadView(asViewCatalog, ident));
+ return CatalogHandlers.loadView(asViewCatalog, ident);
}
private LoadViewResponse updateView(Map<String, String> vars, Object body) {
TableIdentifier ident = viewIdentFromPathVars(vars);
UpdateTableRequest request = castRequest(UpdateTableRequest.class, body);
- return castResponse(
- LoadViewResponse.class, CatalogHandlers.updateView(asViewCatalog,
ident, request));
+ return CatalogHandlers.updateView(asViewCatalog, ident, request);
}
private RESTResponse renameView(Object body) {
@@ -446,8 +453,7 @@ private RESTResponse dropView(Map<String, String> vars) {
private LoadViewResponse registerView(Map<String, String> vars, Object body)
{
Namespace namespace = namespaceFromPathVars(vars);
RegisterViewRequest request = castRequest(RegisterViewRequest.class, body);
- return castResponse(
- LoadViewResponse.class, CatalogHandlers.registerView(asViewCatalog,
namespace, request));
+ return CatalogHandlers.registerView(asViewCatalog, namespace, request);
}
/**
@@ -554,12 +560,6 @@ public void close() {
}
}
- private static class BadResponseType extends RuntimeException {
- private BadResponseType(Class<?> responseType, Object response) {
- super(
- String.format("Invalid response object, not a %s: %s",
responseType.getName(), response));
- }
- }
private static class BadRequestType extends RuntimeException {
private BadRequestType(Class<?> requestType, Object request) {
@@ -574,12 +574,6 @@ public static <T> T castRequest(Class<T> requestType,
Object request) {
throw new BadRequestType(requestType, request);
}
- public static <T extends RESTResponse> T castResponse(Class<T> responseType,
Object response) {
- if (responseType.isInstance(response)) {
- return responseType.cast(response);
- }
- throw new BadResponseType(responseType, response);
- }
public static void configureResponseFromException(
Exception exc, ErrorResponse.Builder errorBuilder) {
@@ -595,7 +589,7 @@ public static void configureResponseFromException(
}
private static Namespace namespaceFromPathVars(Map<String, String> pathVars)
{
- return RESTUtil.decodeNamespace(pathVars.get("namespace"));
+ return RESTUtil.decodeNamespace(pathVars.get("namespace"), "%1F");
}
private static TableIdentifier identFromPathVars(Map<String, String>
pathVars) {
diff --git
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestHMSCatalogAdapterPagination.java
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestHMSCatalogAdapterPagination.java
new file mode 100644
index 00000000000..1579cfe59f3
--- /dev/null
+++
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestHMSCatalogAdapterPagination.java
@@ -0,0 +1,127 @@
+/*
+ * 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.iceberg.rest;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.apache.iceberg.catalog.Catalog;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.SupportsNamespaces;
+import org.apache.iceberg.catalog.ViewCatalog;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.rest.responses.ListNamespacesResponse;
+import org.apache.iceberg.rest.HTTPRequest.HTTPMethod;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import javax.servlet.http.HttpServletResponse;
+
+class TestHMSCatalogAdapterPagination {
+
+ private HMSCatalogAdapter adapter;
+ private HttpServletResponse response;
+ private StringWriter stringWriter;
+
+ @BeforeEach
+ void setup() throws Exception {
+ Catalog catalog =
+ Mockito.mock(
+ Catalog.class,
+ Mockito.withSettings().extraInterfaces(SupportsNamespaces.class,
ViewCatalog.class));
+ SupportsNamespaces nsCatalog = (SupportsNamespaces) catalog;
+ List<Namespace> fakeNamespaces =
+ Arrays.asList(
+ Namespace.of("db1"),
+ Namespace.of("db2"),
+ Namespace.of("db3"),
+ Namespace.of("db4"),
+ Namespace.of("db5"));
+ Mockito.when(nsCatalog.listNamespaces()).thenReturn(fakeNamespaces);
+
Mockito.when(nsCatalog.listNamespaces(Mockito.any())).thenReturn(fakeNamespaces);
+
+ adapter = new HMSCatalogAdapter("test", catalog, null,
Collections.emptyList());
+
+ response = Mockito.mock(HttpServletResponse.class);
+ stringWriter = new StringWriter();
+ Mockito.when(response.getWriter()).thenReturn(new
PrintWriter(stringWriter));
+ }
+
+ @AfterEach
+ void tearDown() {
+ if (adapter != null) {
+ adapter.close();
+ }
+ }
+
+ @Test
+ void testUnpaginatedRequest() throws Exception {
+ // Missing pageSize (should call unpaginated and succeed without
NumberFormatException)
+ Map<String, String> vars = ImmutableMap.of("pageToken", "0");
+ ListNamespacesResponse res =
+ adapter.execute(HTTPMethod.GET, "v1/namespaces", vars, null, response);
+
+ if (res == null) {
+ System.err.println("Error output: " + stringWriter);
+ }
+ Assertions.assertNotNull(res, "Response should not be null");
+ Assertions.assertEquals(5, res.namespaces().size(), "Should return all 5
unpaginated");
+ }
+
+ @Test
+ void testPaginatedRequest() throws Exception {
+ // Both pageToken and pageSize (should call paginated and slice without
errors)
+ Map<String, String> vars = ImmutableMap.of("pageToken", "0", "pageSize",
"2");
+ ListNamespacesResponse res =
+ adapter.execute(HTTPMethod.GET, "v1/namespaces", vars, null, response);
+
+ Assertions.assertNotNull(res, "Response should not be null");
+ Assertions.assertEquals(2, res.namespaces().size(), "Should return exactly
2 paginated items");
+ }
+
+ @Test
+ void testPaginatedRequestWithoutToken() throws Exception {
+ // pageSize without pageToken
+ Map<String, String> vars = ImmutableMap.of("pageSize", "2");
+ ListNamespacesResponse res =
+ adapter.execute(HTTPMethod.GET, "v1/namespaces", vars, null, response);
+
+ Assertions.assertNotNull(res, "Response should not be null");
+ Assertions.assertEquals(2, res.namespaces().size(), "Should return exactly
2 paginated items");
+ }
+
+ @Test
+ void testInvalidPageSize() throws Exception {
+ // Invalid pageSize (should return null and write error to response)
+ Map<String, String> vars = ImmutableMap.of("pageSize", "invalid");
+ ListNamespacesResponse res =
+ adapter.execute(HTTPMethod.GET, "v1/namespaces", vars, null, response);
+
+ Assertions.assertNull(res, "Response should be null because it encountered
an error");
+ Assertions.assertTrue(
+ stringWriter.toString().contains("must be a positive integer"),
+ "Error output should contain the Preconditions error message");
+ }
+}