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 15a72dc829 Core: Implement register view for REST catalog (#14870)
15a72dc829 is described below
commit 15a72dc829844a4ca2f004139c9acc5f1a922578
Author: Ajantha Bhat <[email protected]>
AuthorDate: Thu Jan 22 14:34:50 2026 +0530
Core: Implement register view for REST catalog (#14870)
This adds:
- registerView method to ViewSessionCatalog and BaseViewSessionCatalog
- REST endpoint handling in CatalogHandlers
- Client-side implementation in RESTCatalog and RESTSessionCatalog
- Resource path support for register-view endpoint
- Tests for the complete registration flow
---
.../apache/iceberg/catalog/ViewSessionCatalog.java | 15 ++++++++
.../iceberg/catalog/BaseViewSessionCatalog.java | 5 +++
.../org/apache/iceberg/rest/CatalogHandlers.java | 13 +++++++
.../java/org/apache/iceberg/rest/Endpoint.java | 2 +
.../java/org/apache/iceberg/rest/RESTCatalog.java | 5 +++
.../apache/iceberg/rest/RESTSessionCatalog.java | 45 ++++++++++++++++++++++
.../org/apache/iceberg/rest/ResourcePaths.java | 5 +++
.../apache/iceberg/rest/RESTCatalogAdapter.java | 12 ++++++
.../test/java/org/apache/iceberg/rest/Route.java | 6 +++
.../TestRESTViewCatalogWithAssumedViewSupport.java | 29 ++++++++++++++
.../org/apache/iceberg/rest/TestResourcePaths.java | 7 ++++
.../org/apache/iceberg/view/ViewCatalogTests.java | 17 ++------
12 files changed, 147 insertions(+), 14 deletions(-)
diff --git
a/api/src/main/java/org/apache/iceberg/catalog/ViewSessionCatalog.java
b/api/src/main/java/org/apache/iceberg/catalog/ViewSessionCatalog.java
index 106e20d3bc..0e195665f3 100644
--- a/api/src/main/java/org/apache/iceberg/catalog/ViewSessionCatalog.java
+++ b/api/src/main/java/org/apache/iceberg/catalog/ViewSessionCatalog.java
@@ -106,6 +106,21 @@ public interface ViewSessionCatalog {
*/
default void invalidateView(SessionCatalog.SessionContext context,
TableIdentifier identifier) {}
+ /**
+ * Register a view if it does not exist.
+ *
+ * @param context session context
+ * @param ident a view identifier
+ * @param metadataFileLocation the location of a metadata file
+ * @return a View instance
+ * @throws AlreadyExistsException if a table/view with the same identifier
already exists in the
+ * catalog.
+ */
+ default View registerView(
+ SessionCatalog.SessionContext context, TableIdentifier ident, String
metadataFileLocation) {
+ throw new UnsupportedOperationException("Registering views is not
supported");
+ }
+
/**
* Initialize a view catalog given a custom name and a map of catalog
properties.
*
diff --git
a/core/src/main/java/org/apache/iceberg/catalog/BaseViewSessionCatalog.java
b/core/src/main/java/org/apache/iceberg/catalog/BaseViewSessionCatalog.java
index 10895e1de9..ce76481d15 100644
--- a/core/src/main/java/org/apache/iceberg/catalog/BaseViewSessionCatalog.java
+++ b/core/src/main/java/org/apache/iceberg/catalog/BaseViewSessionCatalog.java
@@ -83,6 +83,11 @@ public abstract class BaseViewSessionCatalog extends
BaseSessionCatalog
BaseViewSessionCatalog.this.invalidateView(context, identifier);
}
+ @Override
+ public View registerView(TableIdentifier identifier, String
metadataFileLocation) {
+ return BaseViewSessionCatalog.this.registerView(context, identifier,
metadataFileLocation);
+ }
+
@Override
public void initialize(String name, Map<String, String> properties) {
throw new UnsupportedOperationException(
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 18de8493f4..310738895e 100644
--- a/core/src/main/java/org/apache/iceberg/rest/CatalogHandlers.java
+++ b/core/src/main/java/org/apache/iceberg/rest/CatalogHandlers.java
@@ -84,6 +84,7 @@ import org.apache.iceberg.rest.requests.CreateViewRequest;
import org.apache.iceberg.rest.requests.FetchScanTasksRequest;
import org.apache.iceberg.rest.requests.PlanTableScanRequest;
import org.apache.iceberg.rest.requests.RegisterTableRequest;
+import org.apache.iceberg.rest.requests.RegisterViewRequest;
import org.apache.iceberg.rest.requests.RenameTableRequest;
import org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest;
import org.apache.iceberg.rest.requests.UpdateTableRequest;
@@ -746,6 +747,18 @@ public class CatalogHandlers {
}
}
+ public static LoadViewResponse registerView(
+ ViewCatalog catalog, Namespace namespace, RegisterViewRequest request) {
+ request.validate();
+
+ TableIdentifier identifier = TableIdentifier.of(namespace, request.name());
+ View view = catalog.registerView(identifier, request.metadataLocation());
+ return ImmutableLoadViewResponse.builder()
+ .metadata(asBaseView(view).operations().current())
+ .metadataLocation(request.metadataLocation())
+ .build();
+ }
+
static ViewMetadata commit(ViewOperations ops, UpdateTableRequest request) {
AtomicBoolean isRetry = new AtomicBoolean(false);
try {
diff --git a/core/src/main/java/org/apache/iceberg/rest/Endpoint.java
b/core/src/main/java/org/apache/iceberg/rest/Endpoint.java
index b4b617b8ec..c2369a0fa5 100644
--- a/core/src/main/java/org/apache/iceberg/rest/Endpoint.java
+++ b/core/src/main/java/org/apache/iceberg/rest/Endpoint.java
@@ -86,6 +86,8 @@ public class Endpoint {
public static final Endpoint V1_DELETE_VIEW = Endpoint.create("DELETE",
ResourcePaths.V1_VIEW);
public static final Endpoint V1_RENAME_VIEW =
Endpoint.create("POST", ResourcePaths.V1_VIEW_RENAME);
+ public static final Endpoint V1_REGISTER_VIEW =
+ Endpoint.create("POST", ResourcePaths.V1_VIEW_REGISTER);
private static final Splitter ENDPOINT_SPLITTER = Splitter.on(" ");
private static final Joiner ENDPOINT_JOINER = Joiner.on(" ");
diff --git a/core/src/main/java/org/apache/iceberg/rest/RESTCatalog.java
b/core/src/main/java/org/apache/iceberg/rest/RESTCatalog.java
index f4c75d1050..895336b1ad 100644
--- a/core/src/main/java/org/apache/iceberg/rest/RESTCatalog.java
+++ b/core/src/main/java/org/apache/iceberg/rest/RESTCatalog.java
@@ -329,4 +329,9 @@ public class RESTCatalog
public void invalidateView(TableIdentifier identifier) {
viewSessionCatalog.invalidateView(identifier);
}
+
+ @Override
+ public View registerView(TableIdentifier identifier, String
metadataFileLocation) {
+ return viewSessionCatalog.registerView(identifier, metadataFileLocation);
+ }
}
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 0c4f8a39bf..beb350ef03 100644
--- a/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
+++ b/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
@@ -64,6 +64,7 @@ import org.apache.iceberg.metrics.MetricsReporter;
import org.apache.iceberg.metrics.MetricsReporters;
import
org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.relocated.com.google.common.base.Strings;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableSet;
@@ -81,7 +82,9 @@ import org.apache.iceberg.rest.requests.CreateTableRequest;
import org.apache.iceberg.rest.requests.CreateViewRequest;
import org.apache.iceberg.rest.requests.ImmutableCreateViewRequest;
import org.apache.iceberg.rest.requests.ImmutableRegisterTableRequest;
+import org.apache.iceberg.rest.requests.ImmutableRegisterViewRequest;
import org.apache.iceberg.rest.requests.RegisterTableRequest;
+import org.apache.iceberg.rest.requests.RegisterViewRequest;
import org.apache.iceberg.rest.requests.RenameTableRequest;
import org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest;
import org.apache.iceberg.rest.requests.UpdateTableRequest;
@@ -1457,6 +1460,48 @@ public class RESTSessionCatalog extends
BaseViewSessionCatalog
.post(paths.renameView(), request, null, mutationHeaders,
ErrorHandlers.viewErrorHandler());
}
+ @Override
+ public View registerView(
+ SessionContext context, TableIdentifier ident, String
metadataFileLocation) {
+ Endpoint.check(endpoints, Endpoint.V1_REGISTER_VIEW);
+ checkViewIdentifierIsValid(ident);
+
+ Preconditions.checkArgument(
+ !Strings.isNullOrEmpty(metadataFileLocation),
+ "Invalid metadata file location: %s",
+ metadataFileLocation);
+
+ RegisterViewRequest request =
+ ImmutableRegisterViewRequest.builder()
+ .name(ident.name())
+ .metadataLocation(metadataFileLocation)
+ .build();
+
+ AuthSession contextualSession = authManager.contextualSession(context,
catalogAuth);
+ LoadViewResponse response =
+ client
+ .withAuthSession(contextualSession)
+ .post(
+ paths.registerView(ident.namespace()),
+ request,
+ LoadViewResponse.class,
+ mutationHeaders,
+ ErrorHandlers.viewErrorHandler());
+
+ AuthSession tableSession =
+ authManager.tableSession(ident, response.config(), contextualSession);
+ RESTViewOperations ops =
+ newViewOps(
+ client.withAuthSession(tableSession),
+ paths.view(ident),
+ Map::of,
+ mutationHeaders,
+ response.metadata(),
+ endpoints);
+
+ return new BaseView(ops, ViewUtil.fullViewName(name(), ident));
+ }
+
private static Map<String, String> headersForLoadTable(TableWithETag
tableWithETag) {
if (tableWithETag == null) {
return Map.of();
diff --git a/core/src/main/java/org/apache/iceberg/rest/ResourcePaths.java
b/core/src/main/java/org/apache/iceberg/rest/ResourcePaths.java
index 231a966f80..0fc55c1a44 100644
--- a/core/src/main/java/org/apache/iceberg/rest/ResourcePaths.java
+++ b/core/src/main/java/org/apache/iceberg/rest/ResourcePaths.java
@@ -49,6 +49,7 @@ public class ResourcePaths {
public static final String V1_VIEWS =
"/v1/{prefix}/namespaces/{namespace}/views";
public static final String V1_VIEW =
"/v1/{prefix}/namespaces/{namespace}/views/{view}";
public static final String V1_VIEW_RENAME = "/v1/{prefix}/views/rename";
+ public static final String V1_VIEW_REGISTER =
"/v1/{prefix}/namespaces/{namespace}/register-view";
public static ResourcePaths forCatalogProperties(Map<String, String>
properties) {
return new ResourcePaths(
@@ -151,6 +152,10 @@ public class ResourcePaths {
return SLASH.join("v1", prefix, "views", "rename");
}
+ public String registerView(Namespace ns) {
+ return SLASH.join("v1", prefix, "namespaces", pathEncode(ns),
"register-view");
+ }
+
public String planTableScan(TableIdentifier ident) {
return SLASH.join(
"v1",
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 0600ef5515..5c9e8fe6d4 100644
--- a/core/src/test/java/org/apache/iceberg/rest/RESTCatalogAdapter.java
+++ b/core/src/test/java/org/apache/iceberg/rest/RESTCatalogAdapter.java
@@ -70,6 +70,7 @@ import org.apache.iceberg.rest.requests.CreateViewRequest;
import org.apache.iceberg.rest.requests.FetchScanTasksRequest;
import org.apache.iceberg.rest.requests.PlanTableScanRequest;
import org.apache.iceberg.rest.requests.RegisterTableRequest;
+import org.apache.iceberg.rest.requests.RegisterViewRequest;
import org.apache.iceberg.rest.requests.RenameTableRequest;
import org.apache.iceberg.rest.requests.ReportMetricsRequest;
import org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest;
@@ -516,6 +517,17 @@ public class RESTCatalogAdapter extends BaseHTTPClient {
break;
}
+ case REGISTER_VIEW:
+ {
+ if (null != asViewCatalog) {
+ Namespace namespace = namespaceFromPathVars(vars);
+ RegisterViewRequest request =
castRequest(RegisterViewRequest.class, body);
+ return castResponse(
+ responseType, CatalogHandlers.registerView(asViewCatalog,
namespace, request));
+ }
+ break;
+ }
+
default:
if (responseType == OAuthTokenResponse.class) {
return castResponse(responseType, handleOAuthRequest(body));
diff --git a/core/src/test/java/org/apache/iceberg/rest/Route.java
b/core/src/test/java/org/apache/iceberg/rest/Route.java
index eedb2615ad..8680915bff 100644
--- a/core/src/test/java/org/apache/iceberg/rest/Route.java
+++ b/core/src/test/java/org/apache/iceberg/rest/Route.java
@@ -29,6 +29,7 @@ import org.apache.iceberg.rest.requests.CreateViewRequest;
import org.apache.iceberg.rest.requests.FetchScanTasksRequest;
import org.apache.iceberg.rest.requests.PlanTableScanRequest;
import org.apache.iceberg.rest.requests.RegisterTableRequest;
+import org.apache.iceberg.rest.requests.RegisterViewRequest;
import org.apache.iceberg.rest.requests.RenameTableRequest;
import org.apache.iceberg.rest.requests.ReportMetricsRequest;
import org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest;
@@ -115,6 +116,11 @@ enum Route {
RENAME_VIEW(
HTTPRequest.HTTPMethod.POST, ResourcePaths.V1_VIEW_RENAME,
RenameTableRequest.class, null),
DROP_VIEW(HTTPRequest.HTTPMethod.DELETE, ResourcePaths.V1_VIEW),
+ REGISTER_VIEW(
+ HTTPRequest.HTTPMethod.POST,
+ ResourcePaths.V1_VIEW_REGISTER,
+ RegisterViewRequest.class,
+ LoadViewResponse.class),
PLAN_TABLE_SCAN(
HTTPRequest.HTTPMethod.POST,
ResourcePaths.V1_TABLE_SCAN_PLAN_SUBMIT,
diff --git
a/core/src/test/java/org/apache/iceberg/rest/TestRESTViewCatalogWithAssumedViewSupport.java
b/core/src/test/java/org/apache/iceberg/rest/TestRESTViewCatalogWithAssumedViewSupport.java
index 2ac0828443..1ba340cc56 100644
---
a/core/src/test/java/org/apache/iceberg/rest/TestRESTViewCatalogWithAssumedViewSupport.java
+++
b/core/src/test/java/org/apache/iceberg/rest/TestRESTViewCatalogWithAssumedViewSupport.java
@@ -18,6 +18,8 @@
*/
package org.apache.iceberg.rest;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
import java.io.File;
import java.net.InetAddress;
import java.net.InetSocketAddress;
@@ -107,4 +109,31 @@ public class TestRESTViewCatalogWithAssumedViewSupport
extends TestRESTViewCatal
CatalogProperties.VIEW_OVERRIDE_PREFIX + "key4",
"catalog-override-key4"));
}
+
+ @Override
+ public void registerView() {
+ // Older client doesn't support the newer endpoint.
+ assertThatThrownBy(super::registerView)
+ .isInstanceOf(UnsupportedOperationException.class)
+ .hasMessageStartingWith(
+ "Server does not support endpoint: POST
/v1/{prefix}/namespaces/{namespace}/register-view");
+ }
+
+ @Override
+ public void registerExistingView() {
+ // Older client doesn't support the newer endpoint.
+ assertThatThrownBy(super::registerExistingView)
+ .isInstanceOf(UnsupportedOperationException.class)
+ .hasMessageStartingWith(
+ "Server does not support endpoint: POST
/v1/{prefix}/namespaces/{namespace}/register-view");
+ }
+
+ @Override
+ public void registerViewThatAlreadyExistsAsTable() {
+ // Older client doesn't support the newer endpoint.
+ assertThatThrownBy(super::registerViewThatAlreadyExistsAsTable)
+ .isInstanceOf(UnsupportedOperationException.class)
+ .hasMessageStartingWith(
+ "Server does not support endpoint: POST
/v1/{prefix}/namespaces/{namespace}/register-view");
+ }
}
diff --git a/core/src/test/java/org/apache/iceberg/rest/TestResourcePaths.java
b/core/src/test/java/org/apache/iceberg/rest/TestResourcePaths.java
index 1f6306eab0..1a1018be95 100644
--- a/core/src/test/java/org/apache/iceberg/rest/TestResourcePaths.java
+++ b/core/src/test/java/org/apache/iceberg/rest/TestResourcePaths.java
@@ -267,6 +267,13 @@ public class TestResourcePaths {
assertThat(withoutPrefix.view(ident)).isEqualTo("v1/namespaces/n%1Fs/views/view-name");
}
+ @Test
+ public void testRegisterView() {
+ Namespace ns = Namespace.of("ns");
+
assertThat(withPrefix.registerView(ns)).isEqualTo("v1/ws/catalog/namespaces/ns/register-view");
+
assertThat(withoutPrefix.registerView(ns)).isEqualTo("v1/namespaces/ns/register-view");
+ }
+
@Test
public void planEndpointPath() {
TableIdentifier tableId = TableIdentifier.of("test_namespace",
"test_table");
diff --git a/core/src/test/java/org/apache/iceberg/view/ViewCatalogTests.java
b/core/src/test/java/org/apache/iceberg/view/ViewCatalogTests.java
index 160897d7a4..00926ca73c 100644
--- a/core/src/test/java/org/apache/iceberg/view/ViewCatalogTests.java
+++ b/core/src/test/java/org/apache/iceberg/view/ViewCatalogTests.java
@@ -44,7 +44,6 @@ import org.apache.iceberg.exceptions.NoSuchNamespaceException;
import org.apache.iceberg.exceptions.NoSuchTableException;
import org.apache.iceberg.exceptions.NoSuchViewException;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
-import org.apache.iceberg.rest.RESTCatalog;
import org.apache.iceberg.types.Types;
import org.apache.iceberg.util.LocationUtil;
import org.junit.jupiter.api.Test;
@@ -2013,10 +2012,6 @@ public abstract class ViewCatalogTests<C extends
ViewCatalog & SupportsNamespace
public void registerView() {
C catalog = catalog();
- assumeThat(catalog)
- .as("Registering a view is not yet supported for the REST catalog")
- .isNotInstanceOf(RESTCatalog.class);
-
TableIdentifier identifier = TableIdentifier.of("ns", "view");
if (requiresNamespaceCreate()) {
@@ -2040,7 +2035,9 @@ public abstract class ViewCatalogTests<C extends
ViewCatalog & SupportsNamespace
assertThat(catalog.viewExists(identifier)).as("View must not
exist").isFalse();
// view metadata should still exist after dropping the view as gc is
disabled
- assertThat(((BaseViewOperations)
ops).io().newInputFile(metadataLocation).exists()).isTrue();
+ if (ops instanceof BaseViewOperations) {
+ assertThat(((BaseViewOperations)
ops).io().newInputFile(metadataLocation).exists()).isTrue();
+ }
View registeredView = catalog.registerView(identifier, metadataLocation);
@@ -2085,10 +2082,6 @@ public abstract class ViewCatalogTests<C extends
ViewCatalog & SupportsNamespace
public void registerExistingView() {
C catalog = catalog();
- assumeThat(catalog)
- .as("Registering a view is not yet supported for the REST catalog")
- .isNotInstanceOf(RESTCatalog.class);
-
TableIdentifier identifier = TableIdentifier.of("ns", "view");
if (requiresNamespaceCreate()) {
@@ -2117,10 +2110,6 @@ public abstract class ViewCatalogTests<C extends
ViewCatalog & SupportsNamespace
public void registerViewThatAlreadyExistsAsTable() {
C catalog = catalog();
- assumeThat(catalog)
- .as("Registering a view is not yet supported for the REST catalog")
- .isNotInstanceOf(RESTCatalog.class);
-
TableIdentifier identifier = TableIdentifier.of("ns", "view");
if (requiresNamespaceCreate()) {