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()) {

Reply via email to