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 106e20d3bce1..0e195665f3c2 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 @@ default boolean viewExists(SessionCatalog.SessionContext context, TableIdentifie */ 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 10895e1de9e6..ce76481d159e 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 void invalidateView(TableIdentifier identifier) { 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 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 18de8493f49d..310738895e60 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.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 static void dropView(ViewCatalog catalog, TableIdentifier viewIdentifier) } } + 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 b4b617b8ec5e..c2369a0fa57d 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 f4c75d1050d7..895336b1ad3f 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 boolean viewExists(TableIdentifier identifier) { 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 0c4f8a39bfd9..beb350ef031f 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.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.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 void renameView(SessionContext context, TableIdentifier from, TableIdenti .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 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 231a966f8062..0fc55c1a44d8 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 properties) { return new ResourcePaths( @@ -151,6 +152,10 @@ public String renameView() { 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 0600ef551582..5c9e8fe6d42b 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.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 T handleRequest( 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 eedb2615ad64..8680915bff64 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.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 2ac08284433d..1ba340cc56c2 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 T handleRequest( 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 1f6306eab0a2..1a1018be95ea 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 void viewWithMultipartNamespace() { 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 160897d7a4bb..00926ca73cc7 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.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 void registerTableThatAlreadyExistsAsView() { 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 void registerView() { 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 void registerView() { 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 void registerExistingView() { 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()) {