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

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


The following commit(s) were added to refs/heads/main by this push:
     new 13d1684dd [#4370]feat(iceberg): support view interface for Iceberg 
REST server (#4937)
13d1684dd is described below

commit 13d1684dd5c5a38b1f57a34851f24024718e3a64
Author: theoryxu <[email protected]>
AuthorDate: Tue Oct 8 16:39:13 2024 +0800

    [#4370]feat(iceberg): support view interface for Iceberg REST server (#4937)
    
    ### What changes were proposed in this pull request?
    
    support view interface for Iceberg REST server
    
    ### Why are the changes needed?
    
    Fix: #4370
    
    ### Does this PR introduce _any_ user-facing change?
    
    no
    
    ### How was this patch tested?
    
    1. add UT
    2. manual test
    
    ---------
    
    Co-authored-by: theoryxu <[email protected]>
---
 docs/iceberg-rest-service.md                       |  10 +-
 .../iceberg/common/ops/IcebergCatalogWrapper.java  |  41 +++
 .../iceberg/service/IcebergExceptionMapper.java    |   2 +
 .../service/rest/IcebergViewOperations.java        | 149 ++++++++++
 .../service/rest/IcebergViewRenameOperations.java  |  62 ++++
 .../integration/test/IcebergRESTJdbcCatalogIT.java |   2 +
 .../integration/test/IcebergRESTServiceBaseIT.java |   8 +
 .../integration/test/IcebergRESTServiceIT.java     | 139 +++++++++
 .../iceberg/service/rest/IcebergRestTestUtil.java  |   4 +
 .../iceberg/service/rest/IcebergTestBase.java      |  14 +
 .../service/rest/TestIcebergViewOperations.java    | 311 +++++++++++++++++++++
 11 files changed, 741 insertions(+), 1 deletion(-)

diff --git a/docs/iceberg-rest-service.md b/docs/iceberg-rest-service.md
index 0e6fecd19..393845d64 100644
--- a/docs/iceberg-rest-service.md
+++ b/docs/iceberg-rest-service.md
@@ -14,7 +14,6 @@ The Apache Gravitino Iceberg REST Server follows the [Apache 
Iceberg REST API sp
 
 - Supports the Apache Iceberg REST API defined in Iceberg 1.5, and supports 
all namespace and table interfaces. The following interfaces are not 
implemented yet:
   - token
-  - view
   - multi table transaction
   - pagination
 - Works as a catalog proxy, supporting `Hive` and `JDBC` as catalog backend.
@@ -214,6 +213,15 @@ You must download the corresponding JDBC driver to the 
`iceberg-rest-server/libs
 
 If you want to use a custom Iceberg Catalog as `catalog-backend`, you can add 
a corresponding jar file to the classpath and load a custom Iceberg Catalog 
implementation by specifying the `catalog-backend-impl` property.
 
+#### View support
+
+You could access the view interface if using JDBC backend and enable 
`jdbc.schema-version` property.
+
+| Configuration item                              | Description                
                                                                | Default value 
| Required | Since Version |
+|-------------------------------------------------|--------------------------------------------------------------------------------------------|---------------|----------|---------------|
+| `gravitino.iceberg-rest.jdbc.schema-version`    | The schema version of JDBC 
catalog backend, setting to `V1` if supporting view operations. | (none)        
| NO       | 0.7.0         |
+
+
 #### Multi catalog support
 
 The Gravitino Iceberg REST server supports multiple catalogs and offers a 
configuration-based catalog management system.
diff --git 
a/iceberg/iceberg-common/src/main/java/org/apache/gravitino/iceberg/common/ops/IcebergCatalogWrapper.java
 
b/iceberg/iceberg-common/src/main/java/org/apache/gravitino/iceberg/common/ops/IcebergCatalogWrapper.java
index 0c7c2914b..6ff4bf2ce 100644
--- 
a/iceberg/iceberg-common/src/main/java/org/apache/gravitino/iceberg/common/ops/IcebergCatalogWrapper.java
+++ 
b/iceberg/iceberg-common/src/main/java/org/apache/gravitino/iceberg/common/ops/IcebergCatalogWrapper.java
@@ -43,9 +43,11 @@ import org.apache.iceberg.catalog.Catalog;
 import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.SupportsNamespaces;
 import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.catalog.ViewCatalog;
 import org.apache.iceberg.rest.CatalogHandlers;
 import org.apache.iceberg.rest.requests.CreateNamespaceRequest;
 import org.apache.iceberg.rest.requests.CreateTableRequest;
+import org.apache.iceberg.rest.requests.CreateViewRequest;
 import org.apache.iceberg.rest.requests.RegisterTableRequest;
 import org.apache.iceberg.rest.requests.RenameTableRequest;
 import org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest;
@@ -55,6 +57,7 @@ import org.apache.iceberg.rest.responses.GetNamespaceResponse;
 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.LoadViewResponse;
 import org.apache.iceberg.rest.responses.UpdateNamespacePropertiesResponse;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -116,6 +119,13 @@ public class IcebergCatalogWrapper implements 
AutoCloseable {
     }
   }
 
+  private ViewCatalog getViewCatalog() {
+    if (!(catalog instanceof ViewCatalog)) {
+      throw new UnsupportedOperationException(catalog.name() + " is not 
support view");
+    }
+    return (ViewCatalog) catalog;
+  }
+
   public CreateNamespaceResponse createNamespace(CreateNamespaceRequest 
request) {
     validateNamespace(Optional.of(request.namespace()));
     return CatalogHandlers.createNamespace(asNamespaceCatalog, request);
@@ -203,6 +213,37 @@ public class IcebergCatalogWrapper implements 
AutoCloseable {
     return loadTable(icebergTableChange.getTableIdentifier());
   }
 
+  public LoadViewResponse createView(Namespace namespace, CreateViewRequest 
request) {
+    request.validate();
+    return CatalogHandlers.createView(getViewCatalog(), namespace, request);
+  }
+
+  public LoadViewResponse updateView(TableIdentifier viewIdentifier, 
UpdateTableRequest request) {
+    request.validate();
+    return CatalogHandlers.updateView(getViewCatalog(), viewIdentifier, 
request);
+  }
+
+  public LoadViewResponse loadView(TableIdentifier viewIdentifier) {
+    return CatalogHandlers.loadView(getViewCatalog(), viewIdentifier);
+  }
+
+  public void dropView(TableIdentifier viewIdentifier) {
+    CatalogHandlers.dropView(getViewCatalog(), viewIdentifier);
+  }
+
+  public void renameView(RenameTableRequest request) {
+    request.validate();
+    CatalogHandlers.renameView(getViewCatalog(), request);
+  }
+
+  public boolean existView(TableIdentifier viewIdentifier) {
+    return getViewCatalog().viewExists(viewIdentifier);
+  }
+
+  public ListTablesResponse listView(Namespace namespace) {
+    return CatalogHandlers.listViews(getViewCatalog(), namespace);
+  }
+
   @Override
   public void close() throws Exception {
     if (catalog instanceof AutoCloseable) {
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/IcebergExceptionMapper.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/IcebergExceptionMapper.java
index 95c7bf91a..f880f7f7a 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/IcebergExceptionMapper.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/IcebergExceptionMapper.java
@@ -32,6 +32,7 @@ import 
org.apache.iceberg.exceptions.NamespaceNotEmptyException;
 import org.apache.iceberg.exceptions.NoSuchIcebergTableException;
 import org.apache.iceberg.exceptions.NoSuchNamespaceException;
 import org.apache.iceberg.exceptions.NoSuchTableException;
+import org.apache.iceberg.exceptions.NoSuchViewException;
 import org.apache.iceberg.exceptions.NotAuthorizedException;
 import org.apache.iceberg.exceptions.ServiceUnavailableException;
 import org.apache.iceberg.exceptions.UnprocessableEntityException;
@@ -57,6 +58,7 @@ public class IcebergExceptionMapper implements 
ExceptionMapper<Exception> {
           .put(NoSuchTableException.class, 404)
           .put(NoSuchIcebergTableException.class, 404)
           .put(UnsupportedOperationException.class, 406)
+          .put(NoSuchViewException.class, 404)
           .put(AlreadyExistsException.class, 409)
           .put(CommitFailedException.class, 409)
           .put(UnprocessableEntityException.class, 422)
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewOperations.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewOperations.java
new file mode 100644
index 000000000..3e46257e2
--- /dev/null
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewOperations.java
@@ -0,0 +1,149 @@
+/*
+ * 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.gravitino.iceberg.service.rest;
+
+import com.codahale.metrics.annotation.ResponseMetered;
+import com.codahale.metrics.annotation.Timed;
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.HEAD;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.iceberg.service.IcebergCatalogWrapperManager;
+import org.apache.gravitino.iceberg.service.IcebergRestUtils;
+import org.apache.gravitino.metrics.MetricNames;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.rest.RESTUtil;
+import org.apache.iceberg.rest.requests.CreateViewRequest;
+import org.apache.iceberg.rest.requests.UpdateTableRequest;
+import org.apache.iceberg.rest.responses.ListTablesResponse;
+import org.apache.iceberg.rest.responses.LoadViewResponse;
+
+@Path("/v1/{prefix:([^/]*/)?}namespaces/{namespace}/views")
+@Consumes(MediaType.APPLICATION_JSON)
+@Produces(MediaType.APPLICATION_JSON)
+public class IcebergViewOperations {
+
+  private IcebergCatalogWrapperManager icebergCatalogWrapperManager;
+
+  @SuppressWarnings("UnusedVariable")
+  @Context
+  private HttpServletRequest httpRequest;
+
+  @Inject
+  public IcebergViewOperations(IcebergCatalogWrapperManager 
icebergCatalogWrapperManager) {
+    this.icebergCatalogWrapperManager = icebergCatalogWrapperManager;
+  }
+
+  @GET
+  @Produces(MediaType.APPLICATION_JSON)
+  @Timed(name = "list-view." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
+  @ResponseMetered(name = "list-view", absolute = true)
+  public Response listView(
+      @PathParam("prefix") String prefix, @PathParam("namespace") String 
namespace) {
+    ListTablesResponse response =
+        
icebergCatalogWrapperManager.getOps(prefix).listView(RESTUtil.decodeNamespace(namespace));
+    return IcebergRestUtils.ok(response);
+  }
+
+  @POST
+  @Produces(MediaType.APPLICATION_JSON)
+  @Timed(name = "create-view." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
+  @ResponseMetered(name = "create-view", absolute = true)
+  public Response createView(
+      @PathParam("prefix") String prefix,
+      @PathParam("namespace") String namespace,
+      CreateViewRequest request) {
+    LoadViewResponse response =
+        icebergCatalogWrapperManager
+            .getOps(prefix)
+            .createView(RESTUtil.decodeNamespace(namespace), request);
+    return IcebergRestUtils.ok(response);
+  }
+
+  @GET
+  @Path("{view}")
+  @Produces(MediaType.APPLICATION_JSON)
+  @Timed(name = "load-view." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
+  @ResponseMetered(name = "load-view", absolute = true)
+  public Response loadView(
+      @PathParam("prefix") String prefix,
+      @PathParam("namespace") String namespace,
+      @PathParam("view") String view) {
+    TableIdentifier viewIdentifier = 
TableIdentifier.of(RESTUtil.decodeNamespace(namespace), view);
+    LoadViewResponse response =
+        icebergCatalogWrapperManager.getOps(prefix).loadView(viewIdentifier);
+    return IcebergRestUtils.ok(response);
+  }
+
+  @POST
+  @Path("{view}")
+  @Produces(MediaType.APPLICATION_JSON)
+  @Timed(name = "replace-view." + MetricNames.HTTP_PROCESS_DURATION, absolute 
= true)
+  @ResponseMetered(name = "replace-view", absolute = true)
+  public Response replaceView(
+      @PathParam("prefix") String prefix,
+      @PathParam("namespace") String namespace,
+      @PathParam("view") String view,
+      UpdateTableRequest request) {
+    TableIdentifier viewIdentifier = 
TableIdentifier.of(RESTUtil.decodeNamespace(namespace), view);
+    LoadViewResponse response =
+        icebergCatalogWrapperManager.getOps(prefix).updateView(viewIdentifier, 
request);
+    return IcebergRestUtils.ok(response);
+  }
+
+  @DELETE
+  @Path("{view}")
+  @Produces(MediaType.APPLICATION_JSON)
+  @Timed(name = "drop-view." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
+  @ResponseMetered(name = "drop-view", absolute = true)
+  public Response dropView(
+      @PathParam("prefix") String prefix,
+      @PathParam("namespace") String namespace,
+      @PathParam("view") String view) {
+    TableIdentifier viewIdentifier = 
TableIdentifier.of(RESTUtil.decodeNamespace(namespace), view);
+    icebergCatalogWrapperManager.getOps(prefix).dropView(viewIdentifier);
+    return IcebergRestUtils.noContent();
+  }
+
+  @HEAD
+  @Path("{view}")
+  @Produces(MediaType.APPLICATION_JSON)
+  @Timed(name = "view-exists." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
+  @ResponseMetered(name = "view-exits", absolute = true)
+  public Response viewExists(
+      @PathParam("prefix") String prefix,
+      @PathParam("namespace") String namespace,
+      @PathParam("view") String view) {
+    TableIdentifier tableIdentifier = 
TableIdentifier.of(RESTUtil.decodeNamespace(namespace), view);
+    if 
(icebergCatalogWrapperManager.getOps(prefix).existView(tableIdentifier)) {
+      return IcebergRestUtils.noContent();
+    } else {
+      return IcebergRestUtils.notExists();
+    }
+  }
+}
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewRenameOperations.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewRenameOperations.java
new file mode 100644
index 000000000..128689d33
--- /dev/null
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewRenameOperations.java
@@ -0,0 +1,62 @@
+/*
+ * 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.gravitino.iceberg.service.rest;
+
+import com.codahale.metrics.annotation.ResponseMetered;
+import com.codahale.metrics.annotation.Timed;
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.iceberg.service.IcebergCatalogWrapperManager;
+import org.apache.gravitino.iceberg.service.IcebergRestUtils;
+import org.apache.gravitino.metrics.MetricNames;
+import org.apache.iceberg.rest.requests.RenameTableRequest;
+
+@Path("/v1/{prefix:([^/]*/)?}views/rename")
+@Consumes(MediaType.APPLICATION_JSON)
+@Produces(MediaType.APPLICATION_JSON)
+public class IcebergViewRenameOperations {
+
+  @SuppressWarnings("UnusedVariable")
+  @Context
+  private HttpServletRequest httpRequest;
+
+  private IcebergCatalogWrapperManager icebergCatalogWrapperManager;
+
+  @Inject
+  public IcebergViewRenameOperations(IcebergCatalogWrapperManager 
icebergCatalogWrapperManager) {
+    this.icebergCatalogWrapperManager = icebergCatalogWrapperManager;
+  }
+
+  @POST
+  @Produces(MediaType.APPLICATION_JSON)
+  @Timed(name = "rename-view." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
+  @ResponseMetered(name = "rename-view", absolute = true)
+  public Response renameView(@PathParam("prefix") String prefix, 
RenameTableRequest request) {
+    icebergCatalogWrapperManager.getOps(prefix).renameView(request);
+    return IcebergRestUtils.noContent();
+  }
+}
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTJdbcCatalogIT.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTJdbcCatalogIT.java
index 1dc758a15..d53f80220 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTJdbcCatalogIT.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTJdbcCatalogIT.java
@@ -68,6 +68,8 @@ public class IcebergRESTJdbcCatalogIT extends 
IcebergRESTServiceIT {
     configMap.put(
         IcebergConfig.ICEBERG_CONFIG_PREFIX + 
IcebergConfig.JDBC_INIT_TABLES.getKey(), "true");
 
+    configMap.put(IcebergConfig.ICEBERG_CONFIG_PREFIX + "jdbc.schema-version", 
"V1");
+
     configMap.put(
         IcebergConfig.ICEBERG_CONFIG_PREFIX + 
IcebergConfig.CATALOG_WAREHOUSE.getKey(),
         GravitinoITUtils.genRandomName(
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTServiceBaseIT.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTServiceBaseIT.java
index e562e2783..0ba781cab 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTServiceBaseIT.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTServiceBaseIT.java
@@ -76,6 +76,10 @@ public abstract class IcebergRESTServiceBaseIT {
     return !catalogType.equals(IcebergCatalogBackend.MEMORY);
   }
 
+  boolean isSupportsViewCatalog() {
+    return !catalogType.equals(IcebergCatalogBackend.HIVE);
+  }
+
   abstract void initEnv();
 
   abstract Map<String, String> getCatalogConfig();
@@ -175,6 +179,10 @@ public abstract class IcebergRESTServiceBaseIT {
     return convertToStringMap(sql("desc table extended " + tableName));
   }
 
+  protected Map<String, String> getViewInfo(String viewName) {
+    return convertToStringMap(sql("desc extended " + viewName));
+  }
+
   protected List<String> getTableColumns(String tableName) {
     List<Object[]> objects = sql("desc table extended " + tableName);
     List<String> columns = new ArrayList<>();
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTServiceIT.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTServiceIT.java
index eb196b3a4..9b4900f4d 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTServiceIT.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTServiceIT.java
@@ -30,6 +30,7 @@ import org.apache.spark.sql.AnalysisException;
 import org.apache.spark.sql.catalyst.analysis.NamespaceAlreadyExistsException;
 import org.apache.spark.sql.catalyst.analysis.NoSuchNamespaceException;
 import org.apache.spark.sql.catalyst.analysis.NoSuchTableException;
+import org.apache.spark.sql.catalyst.analysis.NoSuchViewException;
 import org.apache.spark.sql.catalyst.analysis.TableAlreadyExistsException;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.Assertions;
@@ -557,4 +558,142 @@ public abstract class IcebergRESTServiceIT extends 
IcebergRESTServiceBaseIT {
     result = convertToStringMap(sql("SELECT * FROM 
iceberg_rest_table_test.register_foo2"));
     Assertions.assertEquals(ImmutableMap.of("1", "a", "2", "b"), result);
   }
+
+  @Test
+  @EnabledIf("isSupportsViewCatalog")
+  void testCreateViewAndDisplayView() {
+    String originTableName = "iceberg_rest_table_test.create_table_for_view_1";
+    String viewName = "iceberg_rest_table_test.test_create_view";
+
+    sql(
+        String.format(
+            "CREATE TABLE %s ( id bigint, data string, ts timestamp) USING 
iceberg",
+            originTableName));
+    sql(String.format("CREATE VIEW %s AS SELECT * FROM %s", viewName, 
originTableName));
+
+    Map<String, String> viewInfo = getViewInfo(viewName);
+    Map<String, String> m =
+        ImmutableMap.of(
+            "id", "bigint",
+            "data", "string",
+            "ts", "timestamp");
+
+    checkMapContains(m, viewInfo);
+  }
+
+  @Test
+  @EnabledIf("isSupportsViewCatalog")
+  void testViewProperties() {
+    String originTableName = "iceberg_rest_table_test.create_table_for_view_2";
+    String viewName = 
"iceberg_rest_table_test.test_create_view_with_properties";
+    sql(
+        String.format(
+            "CREATE TABLE %s ( id bigint, data string, ts timestamp) USING 
iceberg",
+            originTableName));
+
+    // test create view with properties
+    sql(
+        String.format(
+            "CREATE VIEW %s TBLPROPERTIES ('key1' = 'val1') AS SELECT * FROM 
%s",
+            viewName, originTableName));
+
+    Map<String, String> viewInfo = getViewInfo(viewName);
+    Assertions.assertTrue(viewInfo.getOrDefault("View Properties", 
"").contains("'key1' = 'val1'"));
+    Assertions.assertFalse(
+        viewInfo.getOrDefault("View Properties", "").contains("'key2' = 
'val2'"));
+
+    // test set properties
+    sql(
+        String.format(
+            "ALTER VIEW %s SET TBLPROPERTIES ('key1' = 'val1', 'key2' = 
'val2')", viewName));
+
+    viewInfo = getViewInfo(viewName);
+    Assertions.assertTrue(viewInfo.getOrDefault("View Properties", 
"").contains("'key1' = 'val1'"));
+    Assertions.assertTrue(viewInfo.getOrDefault("View Properties", 
"").contains("'key2' = 'val2'"));
+
+    // test unset properties
+    sql(String.format("ALTER VIEW %s UNSET TBLPROPERTIES ('key1', 'key2')", 
viewName));
+
+    viewInfo = getViewInfo(viewName);
+    Assertions.assertFalse(
+        viewInfo.getOrDefault("View Properties", "").contains("'key1' = 
'val1'"));
+    Assertions.assertFalse(
+        viewInfo.getOrDefault("View Properties", "").contains("'key2' = 
'val2'"));
+  }
+
+  @Test
+  @EnabledIf("isSupportsViewCatalog")
+  void testDropView() {
+    String originTableName = "iceberg_rest_table_test.create_table_for_view_3";
+    String viewName = "iceberg_rest_table_test.test_drop_view";
+
+    sql(
+        String.format(
+            "CREATE TABLE %s ( id bigint, data string, ts timestamp) USING 
iceberg",
+            originTableName));
+    sql(String.format("CREATE VIEW %s AS SELECT * FROM %s", viewName, 
originTableName));
+    sql(String.format("DROP VIEW %s", viewName));
+
+    Assertions.assertThrowsExactly(AnalysisException.class, () -> 
getViewInfo(viewName));
+    Assertions.assertThrowsExactly(
+        NoSuchViewException.class, () -> sql(String.format("DROP VIEW %s", 
viewName)));
+  }
+
+  @Test
+  @EnabledIf("isSupportsViewCatalog")
+  void testReplaceView() {
+    String originTableName = "iceberg_rest_table_test.create_table_for_view_4";
+    String viewName = "iceberg_rest_table_test.test_replace_view";
+
+    sql(
+        String.format(
+            "CREATE TABLE %s (id bigint, data string, ts timestamp) USING 
iceberg",
+            originTableName));
+    sql(String.format("CREATE VIEW %s AS SELECT * FROM %s", viewName, 
originTableName));
+    sql(
+        String.format(
+            "CREATE OR REPLACE VIEW %s (updated_id COMMENT 'updated ID') 
TBLPROPERTIES ('key1' = 'new_val1') AS SELECT id FROM %s",
+            viewName, originTableName));
+
+    Map<String, String> viewInfo = getViewInfo(viewName);
+    Assertions.assertTrue(
+        viewInfo.getOrDefault("View Properties", "").contains("'key1' = 
'new_val1'"));
+    Assertions.assertTrue(viewInfo.containsKey("updated_id"));
+  }
+
+  @Test
+  @EnabledIf("isSupportsViewCatalog")
+  void testShowAvailableViews() {
+    String originTableName = "iceberg_rest_table_test.create_table_for_view_5";
+    String viewName1 = "iceberg_rest_table_test.show_available_views_1";
+    String viewName2 = "iceberg_rest_table_test.show_available_views_2";
+
+    sql(
+        String.format(
+            "CREATE TABLE %s (id bigint, data string, ts timestamp) USING 
iceberg",
+            originTableName));
+    sql(String.format("CREATE VIEW %s AS SELECT * FROM %s", viewName1, 
originTableName));
+    sql(String.format("CREATE VIEW %s AS SELECT * FROM %s", viewName2, 
originTableName));
+
+    List<Object[]> views = sql("SHOW VIEWS IN iceberg_rest_table_test");
+    Assertions.assertEquals(2, views.size());
+  }
+
+  @Test
+  @EnabledIf("isSupportsViewCatalog")
+  void testShowCreateStatementView() {
+    String originTableName = "iceberg_rest_table_test.create_table_for_view_6";
+    String viewName = "iceberg_rest_table_test.show_create_statement_view";
+
+    sql(
+        String.format(
+            "CREATE TABLE %s (id bigint, data string, ts timestamp) USING 
iceberg",
+            originTableName));
+    sql(String.format("CREATE VIEW %s AS SELECT * FROM %s", viewName, 
originTableName));
+
+    List<Object[]> result = sql(String.format("SHOW CREATE TABLE %s", 
viewName));
+    Assertions.assertEquals(1, result.size());
+    Assertions.assertTrue(
+        
Arrays.stream(result.get(0)).findFirst().orElse("").toString().contains(viewName));
+  }
 }
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/IcebergRestTestUtil.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/IcebergRestTestUtil.java
index 8bccdab7c..4fc645132 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/IcebergRestTestUtil.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/IcebergRestTestUtil.java
@@ -44,7 +44,11 @@ public class IcebergRestTestUtil {
   public static final String UPDATE_NAMESPACE_POSTFIX = "properties";
   public static final String TEST_NAMESPACE_NAME = "gravitino-test";
   public static final String TABLE_PATH = NAMESPACE_PATH + "/" + 
TEST_NAMESPACE_NAME + "/tables";
+
+  public static final String VIEW_PATH = NAMESPACE_PATH + "/" + 
TEST_NAMESPACE_NAME + "/views";
   public static final String RENAME_TABLE_PATH = V_1 + "/tables/rename";
+
+  public static final String RENAME_VIEW_PATH = V_1 + "/views/rename";
   public static final String REPORT_METRICS_POSTFIX = "metrics";
 
   public static final boolean DEBUG_SERVER_LOG_ENABLED = true;
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/IcebergTestBase.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/IcebergTestBase.java
index 7d1d80b54..03d9a49eb 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/IcebergTestBase.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/IcebergTestBase.java
@@ -45,16 +45,30 @@ public class IcebergTestBase extends JerseyTest {
     return getIcebergClientBuilder(IcebergRestTestUtil.RENAME_TABLE_PATH, 
Optional.empty());
   }
 
+  public Invocation.Builder getRenameViewClientBuilder() {
+    return getIcebergClientBuilder(IcebergRestTestUtil.RENAME_VIEW_PATH, 
Optional.empty());
+  }
+
   public Invocation.Builder getTableClientBuilder() {
     return getTableClientBuilder(Optional.empty());
   }
 
+  public Invocation.Builder getViewClientBuilder() {
+    return getViewClientBuilder(Optional.empty());
+  }
+
   public Invocation.Builder getTableClientBuilder(Optional<String> name) {
     String path =
         Joiner.on("/").skipNulls().join(IcebergRestTestUtil.TABLE_PATH, 
name.orElseGet(() -> null));
     return getIcebergClientBuilder(path, Optional.empty());
   }
 
+  public Invocation.Builder getViewClientBuilder(Optional<String> name) {
+    String path =
+        Joiner.on("/").skipNulls().join(IcebergRestTestUtil.VIEW_PATH, 
name.orElseGet(() -> null));
+    return getIcebergClientBuilder(path, Optional.empty());
+  }
+
   public Invocation.Builder getReportMetricsClientBuilder(String name) {
     String path =
         Joiner.on("/")
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergViewOperations.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergViewOperations.java
new file mode 100644
index 000000000..9ec2dc66f
--- /dev/null
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergViewOperations.java
@@ -0,0 +1,311 @@
+/*
+ * 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.gravitino.iceberg.service.rest;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.UpdateRequirements;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.rest.requests.CreateViewRequest;
+import org.apache.iceberg.rest.requests.ImmutableCreateViewRequest;
+import org.apache.iceberg.rest.requests.RenameTableRequest;
+import org.apache.iceberg.rest.requests.UpdateTableRequest;
+import org.apache.iceberg.rest.responses.ListTablesResponse;
+import org.apache.iceberg.rest.responses.LoadViewResponse;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.view.ImmutableSQLViewRepresentation;
+import org.apache.iceberg.view.ImmutableViewVersion;
+import org.apache.iceberg.view.ViewMetadata;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+public class TestIcebergViewOperations extends TestIcebergNamespaceOperations {
+  private static final Schema viewSchema =
+      new Schema(Types.NestedField.of(1, false, "foo_string", 
Types.StringType.get()));
+
+  private static final Schema newViewSchema =
+      new Schema(Types.NestedField.of(2, false, "foo_string1", 
Types.StringType.get()));
+
+  private static final String VIEW_QUERY = "select 1";
+
+  @Override
+  protected Application configure() {
+    ResourceConfig resourceConfig =
+        
IcebergRestTestUtil.getIcebergResourceConfig(IcebergViewOperations.class);
+    // create namespace before each view test
+    resourceConfig.register(IcebergNamespaceOperations.class);
+    resourceConfig.register(IcebergViewRenameOperations.class);
+
+    return resourceConfig;
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = {"", IcebergRestTestUtil.PREFIX})
+  void testListViews(String prefix) {
+    setUrlPathWithPrefix(prefix);
+    verifyListViewFail(404);
+
+    verifyCreateNamespaceSucc(IcebergRestTestUtil.TEST_NAMESPACE_NAME);
+    verifyCreateViewSucc("list_foo1");
+    verifyCreateViewSucc("list_foo2");
+    verifyLisViewSucc(ImmutableSet.of("list_foo1", "list_foo2"));
+  }
+
+  @Test
+  void testCreateView() {
+    verifyCreateViewFail("create_foo1", 404);
+
+    verifyCreateNamespaceSucc(IcebergRestTestUtil.TEST_NAMESPACE_NAME);
+
+    verifyCreateViewSucc("create_foo1");
+
+    verifyCreateViewFail("create_foo1", 409);
+    verifyCreateViewFail("", 400);
+  }
+
+  @Test
+  void testLoadView() {
+    verifyLoadViewFail("load_foo1", 404);
+
+    verifyCreateNamespaceSucc(IcebergRestTestUtil.TEST_NAMESPACE_NAME);
+    verifyCreateViewSucc("load_foo1");
+    verifyLoadViewSucc("load_foo1");
+
+    verifyLoadViewFail("load_foo2", 404);
+  }
+
+  @Test
+  void testReplaceView() {
+    verifyCreateNamespaceSucc(IcebergRestTestUtil.TEST_NAMESPACE_NAME);
+    verifyCreateViewSucc("replace_foo1");
+    ViewMetadata metadata = getViewMeta("replace_foo1");
+    verifyReplaceSucc("replace_foo1", metadata);
+
+    verifyDropViewSucc("replace_foo1");
+    verifyUpdateViewFail("replace_foo1", 404, metadata);
+
+    verifyDropNamespaceSucc(IcebergRestTestUtil.TEST_NAMESPACE_NAME);
+    verifyUpdateViewFail("replace_foo1", 404, metadata);
+  }
+
+  @Test
+  void testDropView() {
+    verifyDropViewFail("drop_foo1", 404);
+    verifyCreateNamespaceSucc(IcebergRestTestUtil.TEST_NAMESPACE_NAME);
+    verifyDropViewFail("drop_foo1", 404);
+
+    verifyCreateViewSucc("drop_foo1");
+    verifyDropViewSucc("drop_foo1");
+    verifyLoadViewFail("drop_foo1", 404);
+  }
+
+  @Test
+  void testViewExits() {
+    verifyViewExistsStatusCode("exists_foo2", 404);
+    verifyCreateNamespaceSucc(IcebergRestTestUtil.TEST_NAMESPACE_NAME);
+    verifyViewExistsStatusCode("exists_foo2", 404);
+
+    verifyCreateViewSucc("exists_foo1");
+    verifyViewExistsStatusCode("exists_foo1", 204);
+    verifyLoadViewSucc("exists_foo1");
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = {"", IcebergRestTestUtil.PREFIX})
+  void testRenameTable(String prefix) {
+    setUrlPathWithPrefix(prefix);
+    // namespace not exits
+    verifyRenameViewFail("rename_foo1", "rename_foo3", 404);
+
+    verifyCreateNamespaceSucc(IcebergRestTestUtil.TEST_NAMESPACE_NAME);
+    verifyCreateViewSucc("rename_foo1");
+    // rename
+    verifyRenameViewSucc("rename_foo1", "rename_foo2");
+    verifyLoadViewFail("rename_foo1", 404);
+    verifyLoadViewSucc("rename_foo2");
+
+    // source view not exists
+    verifyRenameViewFail("rename_foo1", "rename_foo3", 404);
+
+    // dest view exists
+    verifyCreateViewSucc("rename_foo3");
+    verifyRenameViewFail("rename_foo2", "rename_foo3", 409);
+  }
+
+  private Response doCreateView(String name) {
+    CreateViewRequest createViewRequest =
+        ImmutableCreateViewRequest.builder()
+            .name(name)
+            .schema(viewSchema)
+            .viewVersion(
+                ImmutableViewVersion.builder()
+                    .versionId(1)
+                    .timestampMillis(System.currentTimeMillis())
+                    .schemaId(1)
+                    
.defaultNamespace(Namespace.of(IcebergRestTestUtil.TEST_NAMESPACE_NAME))
+                    .addRepresentations(
+                        ImmutableSQLViewRepresentation.builder()
+                            .sql(VIEW_QUERY)
+                            .dialect("spark")
+                            .build())
+                    .build())
+            .build();
+    return getViewClientBuilder()
+        .post(Entity.entity(createViewRequest, 
MediaType.APPLICATION_JSON_TYPE));
+  }
+
+  private Response doLoadView(String name) {
+    return getViewClientBuilder(Optional.of(name)).get();
+  }
+
+  private void verifyLoadViewSucc(String name) {
+    Response response = doLoadView(name);
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response.getStatus());
+
+    LoadViewResponse loadViewResponse = 
response.readEntity(LoadViewResponse.class);
+    Assertions.assertEquals(viewSchema.columns(), 
loadViewResponse.metadata().schema().columns());
+  }
+
+  private void verifyCreateViewFail(String name, int status) {
+    Response response = doCreateView(name);
+    Assertions.assertEquals(status, response.getStatus());
+  }
+
+  private void verifyCreateViewSucc(String name) {
+    Response response = doCreateView(name);
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response.getStatus());
+    LoadViewResponse loadViewResponse = 
response.readEntity(LoadViewResponse.class);
+    Schema schema = loadViewResponse.metadata().schema();
+    Assertions.assertEquals(schema.columns(), viewSchema.columns());
+  }
+
+  private void verifyLoadViewFail(String name, int status) {
+    Response response = doLoadView(name);
+    Assertions.assertEquals(status, response.getStatus());
+  }
+
+  private void verifyReplaceSucc(String name, ViewMetadata base) {
+    Response response = doReplaceView(name, base);
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response.getStatus());
+    LoadViewResponse loadViewResponse = 
response.readEntity(LoadViewResponse.class);
+    Assertions.assertEquals(
+        newViewSchema.columns(), 
loadViewResponse.metadata().schema().columns());
+  }
+
+  private Response doReplaceView(String name, ViewMetadata base) {
+    ViewMetadata.Builder builder =
+        ViewMetadata.buildFrom(base).setCurrentVersion(base.currentVersion(), 
newViewSchema);
+    ViewMetadata replacement = builder.build();
+    UpdateTableRequest updateTableRequest =
+        UpdateTableRequest.create(
+            null,
+            UpdateRequirements.forReplaceView(base, replacement.changes()),
+            replacement.changes());
+    return getViewClientBuilder(Optional.of(name))
+        .post(Entity.entity(updateTableRequest, 
MediaType.APPLICATION_JSON_TYPE));
+  }
+
+  private ViewMetadata getViewMeta(String viewName) {
+    Response response = doLoadView(viewName);
+    LoadViewResponse loadViewResponse = 
response.readEntity(LoadViewResponse.class);
+    return loadViewResponse.metadata();
+  }
+
+  private void verifyUpdateViewFail(String name, int status, ViewMetadata 
base) {
+    Response response = doReplaceView(name, base);
+    Assertions.assertEquals(status, response.getStatus());
+  }
+
+  private void verifyDropViewSucc(String name) {
+    Response response = doDropView(name);
+    Assertions.assertEquals(Response.Status.NO_CONTENT.getStatusCode(), 
response.getStatus());
+  }
+
+  private Response doDropView(String name) {
+    return getViewClientBuilder(Optional.of(name)).delete();
+  }
+
+  private void verifyDropViewFail(String name, int status) {
+    Response response = doDropView(name);
+    Assertions.assertEquals(status, response.getStatus());
+  }
+
+  private void verifyViewExistsStatusCode(String name, int status) {
+    Response response = doViewExists(name);
+    Assertions.assertEquals(status, response.getStatus());
+  }
+
+  private Response doViewExists(String name) {
+    return getViewClientBuilder(Optional.of(name)).head();
+  }
+
+  private void verifyListViewFail(int status) {
+    Response response = doListView();
+    Assertions.assertEquals(status, response.getStatus());
+  }
+
+  private Response doListView() {
+    return getViewClientBuilder().get();
+  }
+
+  private void verifyLisViewSucc(Set<String> expectedTableNames) {
+    Response response = doListView();
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response.getStatus());
+    ListTablesResponse listTablesResponse = 
response.readEntity(ListTablesResponse.class);
+    Set<String> tableNames =
+        listTablesResponse.identifiers().stream()
+            .map(identifier -> identifier.name())
+            .collect(Collectors.toSet());
+    Assertions.assertEquals(expectedTableNames, tableNames);
+  }
+
+  private void verifyRenameViewFail(String source, String dest, int status) {
+    Response response = doRenameView(source, dest);
+    Assertions.assertEquals(status, response.getStatus());
+  }
+
+  private Response doRenameView(String source, String dest) {
+    RenameTableRequest renameTableRequest =
+        RenameTableRequest.builder()
+            .withSource(
+                
TableIdentifier.of(Namespace.of(IcebergRestTestUtil.TEST_NAMESPACE_NAME), 
source))
+            .withDestination(
+                
TableIdentifier.of(Namespace.of(IcebergRestTestUtil.TEST_NAMESPACE_NAME), dest))
+            .build();
+    return getRenameViewClientBuilder()
+        .post(Entity.entity(renameTableRequest, 
MediaType.APPLICATION_JSON_TYPE));
+  }
+
+  private void verifyRenameViewSucc(String source, String dest) {
+    Response response = doRenameView(source, dest);
+    Assertions.assertEquals(Response.Status.NO_CONTENT.getStatusCode(), 
response.getStatus());
+  }
+}


Reply via email to