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

jerryshao pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git

commit 666b3b01d4222ac29762578d80c51fb2ebc05e59
Author: Zhiguo Wu <[email protected]>
AuthorDate: Tue Jul 14 14:24:28 2026 +0800

    [#11902] feat(client-java): Support tags for views and functions (#11903)
    
    This PR adds Java client support for tag operations on views and
    functions.
    
    Changes include:
    
    - Add default `supportsTags()` methods to the `View` and `Function` API
    interfaces.
    - Add `GenericView` and `GenericFunction` client wrappers backed by
    `MetadataObjectTagOperations`.
    - Return tag-capable wrappers from Java client view and function
    operations.
    - Add Java client tests for view/function tag support.
    
    metadata objects. The Java client should expose the same capability so
    users can manage tags for views and functions through the typed client
    API, consistent with other tag-capable metadata objects.
    
    Fix: #11902
    Related: #11844
    
    Yes.
    
    Java client users can now call `supportsTags()` on `View` and `Function`
    objects returned by the client, for example:
    
    ```java
    view.supportsTags().associateTags(...);
    function.supportsTags().listTags();
    ```
    
    - `./gradlew :clients:client-java:test -PskipITs`
    
    ---------
    
    Co-authored-by: Jerry Shao <[email protected]>
    (cherry picked from commit e71af9ca988f70b0f11c76c4bbc5416343ddcbb5)
---
 .../org/apache/gravitino/function/Function.java    |   9 +
 .../main/java/org/apache/gravitino/rel/View.java   |   9 +
 .../client/FunctionCatalogOperations.java          |  10 +-
 .../apache/gravitino/client/GenericFunction.java   | 131 ++++++++++++
 .../org/apache/gravitino/client/GenericView.java   | 141 +++++++++++++
 .../apache/gravitino/client/RelationalCatalog.java |   6 +-
 .../gravitino/client/TestFunctionCatalog.java      |  46 +++++
 .../apache/gravitino/client/TestSupportTags.java   | 107 ++++++++++
 .../gravitino/client/integration/test/TagIT.java   | 227 +++++++++++++++++++++
 9 files changed, 679 insertions(+), 7 deletions(-)

diff --git a/api/src/main/java/org/apache/gravitino/function/Function.java 
b/api/src/main/java/org/apache/gravitino/function/Function.java
index a55c9eab69..536c520d12 100644
--- a/api/src/main/java/org/apache/gravitino/function/Function.java
+++ b/api/src/main/java/org/apache/gravitino/function/Function.java
@@ -22,6 +22,7 @@ import javax.annotation.Nullable;
 import org.apache.gravitino.Auditable;
 import org.apache.gravitino.Namespace;
 import org.apache.gravitino.annotation.Evolving;
+import org.apache.gravitino.tag.SupportsTags;
 
 /**
  * An interface representing a user-defined function under a schema {@link 
Namespace}. A function is
@@ -65,4 +66,12 @@ public interface Function extends Auditable {
    * @return The definitions of the function.
    */
   FunctionDefinition[] definitions();
+
+  /**
+   * @return The {@link SupportsTags} if the function supports tag operations.
+   * @throws UnsupportedOperationException If the function does not support 
tag operations.
+   */
+  default SupportsTags supportsTags() {
+    throw new UnsupportedOperationException("Function does not support tag 
operations.");
+  }
 }
diff --git a/api/src/main/java/org/apache/gravitino/rel/View.java 
b/api/src/main/java/org/apache/gravitino/rel/View.java
index c4bf41440d..5b6d91a431 100644
--- a/api/src/main/java/org/apache/gravitino/rel/View.java
+++ b/api/src/main/java/org/apache/gravitino/rel/View.java
@@ -25,6 +25,7 @@ import javax.annotation.Nullable;
 import org.apache.gravitino.Auditable;
 import org.apache.gravitino.Namespace;
 import org.apache.gravitino.annotation.Unstable;
+import org.apache.gravitino.tag.SupportsTags;
 
 /**
  * An interface representing a logical view in a {@link Namespace}. A view is 
a named query whose
@@ -123,4 +124,12 @@ public interface View extends Auditable {
   default Map<String, String> properties() {
     return Collections.emptyMap();
   }
+
+  /**
+   * @return The {@link SupportsTags} if the view supports tag operations.
+   * @throws UnsupportedOperationException If the view does not support tag 
operations.
+   */
+  default SupportsTags supportsTags() {
+    throw new UnsupportedOperationException("View does not support tag 
operations.");
+  }
 }
diff --git 
a/clients/client-java/src/main/java/org/apache/gravitino/client/FunctionCatalogOperations.java
 
b/clients/client-java/src/main/java/org/apache/gravitino/client/FunctionCatalogOperations.java
index b7dc36d1ed..d8d8b6381e 100644
--- 
a/clients/client-java/src/main/java/org/apache/gravitino/client/FunctionCatalogOperations.java
+++ 
b/clients/client-java/src/main/java/org/apache/gravitino/client/FunctionCatalogOperations.java
@@ -110,7 +110,9 @@ class FunctionCatalogOperations implements FunctionCatalog {
             Collections.emptyMap(),
             ErrorHandlers.functionErrorHandler());
     resp.validate();
-    return resp.getFunctions();
+    return Arrays.stream(resp.getFunctions())
+        .map(function -> new GenericFunction(function, restClient, 
fullNamespace))
+        .toArray(Function[]::new);
   }
 
   /**
@@ -134,7 +136,7 @@ class FunctionCatalogOperations implements FunctionCatalog {
             ErrorHandlers.functionErrorHandler());
     resp.validate();
 
-    return resp.getFunction();
+    return new GenericFunction(resp.getFunction(), restClient, fullNamespace);
   }
 
   /**
@@ -180,7 +182,7 @@ class FunctionCatalogOperations implements FunctionCatalog {
             ErrorHandlers.functionErrorHandler());
     resp.validate();
 
-    return resp.getFunction();
+    return new GenericFunction(resp.getFunction(), restClient, fullNamespace);
   }
 
   /**
@@ -214,7 +216,7 @@ class FunctionCatalogOperations implements FunctionCatalog {
             ErrorHandlers.functionErrorHandler());
     resp.validate();
 
-    return resp.getFunction();
+    return new GenericFunction(resp.getFunction(), restClient, fullNamespace);
   }
 
   /**
diff --git 
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFunction.java
 
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFunction.java
new file mode 100644
index 0000000000..fd3bcb459b
--- /dev/null
+++ 
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFunction.java
@@ -0,0 +1,131 @@
+/*
+ * 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.client;
+
+import com.google.common.collect.Lists;
+import java.util.List;
+import org.apache.gravitino.Audit;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.dto.function.FunctionDTO;
+import org.apache.gravitino.exceptions.NoSuchTagException;
+import org.apache.gravitino.exceptions.TagAlreadyAssociatedException;
+import org.apache.gravitino.function.Function;
+import org.apache.gravitino.function.FunctionDefinition;
+import org.apache.gravitino.function.FunctionType;
+import org.apache.gravitino.tag.SupportsTags;
+import org.apache.gravitino.tag.Tag;
+
+/** Represents a generic function. */
+class GenericFunction implements Function, SupportsTags {
+
+  private final FunctionDTO functionDTO;
+
+  private final MetadataObjectTagOperations objectTagOperations;
+
+  GenericFunction(FunctionDTO functionDTO, RESTClient restClient, Namespace 
functionNs) {
+    this.functionDTO = functionDTO;
+    List<String> functionFullName =
+        Lists.newArrayList(functionNs.level(1), functionNs.level(2), 
functionDTO.name());
+    MetadataObject functionObject =
+        MetadataObjects.of(functionFullName, MetadataObject.Type.FUNCTION);
+    this.objectTagOperations =
+        new MetadataObjectTagOperations(functionNs.level(0), functionObject, 
restClient);
+  }
+
+  @Override
+  public Audit auditInfo() {
+    return functionDTO.auditInfo();
+  }
+
+  @Override
+  public String name() {
+    return functionDTO.name();
+  }
+
+  @Override
+  public FunctionType functionType() {
+    return functionDTO.functionType();
+  }
+
+  @Override
+  public boolean deterministic() {
+    return functionDTO.deterministic();
+  }
+
+  @Override
+  public String comment() {
+    return functionDTO.comment();
+  }
+
+  @Override
+  public FunctionDefinition[] definitions() {
+    return functionDTO.definitions();
+  }
+
+  @Override
+  public SupportsTags supportsTags() {
+    return this;
+  }
+
+  @Override
+  public String[] listTags() {
+    return objectTagOperations.listTags();
+  }
+
+  @Override
+  public Tag[] listTagsInfo() {
+    return objectTagOperations.listTagsInfo();
+  }
+
+  @Override
+  public Tag getTag(String name) throws NoSuchTagException {
+    return objectTagOperations.getTag(name);
+  }
+
+  @Override
+  public String[] associateTags(String[] tagsToAdd, String[] tagsToRemove)
+      throws TagAlreadyAssociatedException {
+    return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof GenericFunction)) {
+      return false;
+    }
+
+    GenericFunction that = (GenericFunction) obj;
+    return functionDTO.equals(that.functionDTO);
+  }
+
+  @Override
+  public int hashCode() {
+    return functionDTO.hashCode();
+  }
+
+  @Override
+  public String toString() {
+    return "GenericFunction{" + "functionDTO=" + functionDTO.toString() + '}';
+  }
+}
diff --git 
a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericView.java
 
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericView.java
new file mode 100644
index 0000000000..a38228806a
--- /dev/null
+++ 
b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericView.java
@@ -0,0 +1,141 @@
+/*
+ * 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.client;
+
+import com.google.common.collect.Lists;
+import java.util.List;
+import java.util.Map;
+import org.apache.gravitino.Audit;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.dto.rel.ViewDTO;
+import org.apache.gravitino.exceptions.NoSuchTagException;
+import org.apache.gravitino.exceptions.TagAlreadyAssociatedException;
+import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.Representation;
+import org.apache.gravitino.rel.View;
+import org.apache.gravitino.tag.SupportsTags;
+import org.apache.gravitino.tag.Tag;
+
+/** Represents a generic view. */
+class GenericView implements View, SupportsTags {
+
+  private final ViewDTO viewDTO;
+
+  private final MetadataObjectTagOperations objectTagOperations;
+
+  GenericView(ViewDTO viewDTO, RESTClient restClient, Namespace viewNs) {
+    this.viewDTO = viewDTO;
+    List<String> viewFullName =
+        Lists.newArrayList(viewNs.level(1), viewNs.level(2), viewDTO.name());
+    MetadataObject viewObject = MetadataObjects.of(viewFullName, 
MetadataObject.Type.VIEW);
+    this.objectTagOperations =
+        new MetadataObjectTagOperations(viewNs.level(0), viewObject, 
restClient);
+  }
+
+  @Override
+  public Audit auditInfo() {
+    return viewDTO.auditInfo();
+  }
+
+  @Override
+  public String name() {
+    return viewDTO.name();
+  }
+
+  @Override
+  public String comment() {
+    return viewDTO.comment();
+  }
+
+  @Override
+  public Column[] columns() {
+    return viewDTO.columns();
+  }
+
+  @Override
+  public Representation[] representations() {
+    return viewDTO.representations();
+  }
+
+  @Override
+  public String defaultCatalog() {
+    return viewDTO.defaultCatalog();
+  }
+
+  @Override
+  public String defaultSchema() {
+    return viewDTO.defaultSchema();
+  }
+
+  @Override
+  public Map<String, String> properties() {
+    return viewDTO.properties();
+  }
+
+  @Override
+  public SupportsTags supportsTags() {
+    return this;
+  }
+
+  @Override
+  public String[] listTags() {
+    return objectTagOperations.listTags();
+  }
+
+  @Override
+  public Tag[] listTagsInfo() {
+    return objectTagOperations.listTagsInfo();
+  }
+
+  @Override
+  public Tag getTag(String name) throws NoSuchTagException {
+    return objectTagOperations.getTag(name);
+  }
+
+  @Override
+  public String[] associateTags(String[] tagsToAdd, String[] tagsToRemove)
+      throws TagAlreadyAssociatedException {
+    return objectTagOperations.associateTags(tagsToAdd, tagsToRemove);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof GenericView)) {
+      return false;
+    }
+
+    GenericView that = (GenericView) obj;
+    return viewDTO.equals(that.viewDTO);
+  }
+
+  @Override
+  public int hashCode() {
+    return viewDTO.hashCode();
+  }
+
+  @Override
+  public String toString() {
+    return "GenericView{" + "viewDTO=" + viewDTO.toString() + '}';
+  }
+}
diff --git 
a/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalCatalog.java
 
b/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalCatalog.java
index c94d3bba2c..1ae0df08b5 100644
--- 
a/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalCatalog.java
+++ 
b/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalCatalog.java
@@ -359,7 +359,7 @@ class RelationalCatalog extends BaseSchemaCatalog
             ErrorHandlers.viewErrorHandler());
     resp.validate();
 
-    return resp.getView();
+    return new GenericView(resp.getView(), restClient, fullNamespace);
   }
 
   /**
@@ -415,7 +415,7 @@ class RelationalCatalog extends BaseSchemaCatalog
             ErrorHandlers.viewErrorHandler());
     resp.validate();
 
-    return resp.getView();
+    return new GenericView(resp.getView(), restClient, fullNamespace);
   }
 
   /**
@@ -449,7 +449,7 @@ class RelationalCatalog extends BaseSchemaCatalog
             ErrorHandlers.viewErrorHandler());
     resp.validate();
 
-    return resp.getView();
+    return new GenericView(resp.getView(), restClient, fullNamespace);
   }
 
   /**
diff --git 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestFunctionCatalog.java
 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestFunctionCatalog.java
index 8786c98945..c5644c5e1a 100644
--- 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestFunctionCatalog.java
+++ 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestFunctionCatalog.java
@@ -41,6 +41,7 @@ import org.apache.gravitino.dto.responses.CatalogResponse;
 import org.apache.gravitino.dto.responses.DropResponse;
 import org.apache.gravitino.dto.responses.EntityListResponse;
 import org.apache.gravitino.dto.responses.ErrorResponse;
+import org.apache.gravitino.dto.responses.FunctionListResponse;
 import org.apache.gravitino.dto.responses.FunctionResponse;
 import org.apache.gravitino.exceptions.FunctionAlreadyExistsException;
 import org.apache.gravitino.exceptions.NoSuchFunctionException;
@@ -151,6 +152,51 @@ public class TestFunctionCatalog extends TestBase {
         "internal error");
   }
 
+  @Test
+  public void testListFunctionInfos() throws JsonProcessingException {
+    NameIdentifier func1 = NameIdentifier.of("schema1", "func1");
+    String functionPath =
+        withSlash(formatFunctionRequestPath(Namespace.of(metalakeName, 
catalogName, "schema1")));
+    FunctionDTO[] mockFunctions =
+        new FunctionDTO[] {
+          mockFunctionDTO("func1", FunctionType.SCALAR, "mock comment1", true),
+          mockFunctionDTO("func2", FunctionType.SCALAR, "mock comment2", true)
+        };
+
+    FunctionListResponse resp = new FunctionListResponse(mockFunctions);
+    buildMockResource(
+        Method.GET, functionPath, ImmutableMap.of("details", "true"), null, 
resp, SC_OK);
+
+    Function[] functions = 
catalog.asFunctionCatalog().listFunctionInfos(func1.namespace());
+    Assertions.assertEquals(2, functions.length);
+    assertFunction(mockFunctions[0], functions[0]);
+    assertFunction(mockFunctions[1], functions[1]);
+
+    // Throw schema not found exception
+    ErrorResponse errResp =
+        ErrorResponse.notFound(NoSuchSchemaException.class.getSimpleName(), 
"schema not found");
+    buildMockResource(
+        Method.GET, functionPath, ImmutableMap.of("details", "true"), null, 
errResp, SC_NOT_FOUND);
+    Assertions.assertThrows(
+        NoSuchSchemaException.class,
+        () -> catalog.asFunctionCatalog().listFunctionInfos(func1.namespace()),
+        "schema not found");
+
+    // Throw Runtime exception
+    ErrorResponse errResp2 = ErrorResponse.internalError("internal error");
+    buildMockResource(
+        Method.GET,
+        functionPath,
+        ImmutableMap.of("details", "true"),
+        null,
+        errResp2,
+        SC_SERVER_ERROR);
+    Assertions.assertThrows(
+        RuntimeException.class,
+        () -> catalog.asFunctionCatalog().listFunctionInfos(func1.namespace()),
+        "internal error");
+  }
+
   @Test
   public void testGetFunction() throws JsonProcessingException {
     NameIdentifier func = NameIdentifier.of("schema1", "func1");
diff --git 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportTags.java
 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportTags.java
index 157f69191e..7ee356ebae 100644
--- 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportTags.java
+++ 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportTags.java
@@ -35,9 +35,13 @@ import org.apache.gravitino.Schema;
 import org.apache.gravitino.dto.AuditDTO;
 import org.apache.gravitino.dto.SchemaDTO;
 import org.apache.gravitino.dto.file.FilesetDTO;
+import org.apache.gravitino.dto.function.FunctionDTO;
+import org.apache.gravitino.dto.function.FunctionDefinitionDTO;
 import org.apache.gravitino.dto.messaging.TopicDTO;
 import org.apache.gravitino.dto.rel.ColumnDTO;
+import org.apache.gravitino.dto.rel.SQLRepresentationDTO;
 import org.apache.gravitino.dto.rel.TableDTO;
+import org.apache.gravitino.dto.rel.ViewDTO;
 import org.apache.gravitino.dto.requests.TagsAssociateRequest;
 import org.apache.gravitino.dto.responses.ErrorResponse;
 import org.apache.gravitino.dto.responses.NameListResponse;
@@ -47,9 +51,13 @@ import org.apache.gravitino.dto.tag.TagDTO;
 import org.apache.gravitino.exceptions.NoSuchTagException;
 import org.apache.gravitino.exceptions.NotFoundException;
 import org.apache.gravitino.file.Fileset;
+import org.apache.gravitino.function.Function;
+import org.apache.gravitino.function.FunctionType;
 import org.apache.gravitino.messaging.Topic;
 import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.Dialects;
 import org.apache.gravitino.rel.Table;
+import org.apache.gravitino.rel.View;
 import org.apache.gravitino.rel.types.Types;
 import org.apache.gravitino.tag.SupportsTags;
 import org.apache.gravitino.tag.Tag;
@@ -78,6 +86,10 @@ public class TestSupportTags extends TestBase {
 
   private static Topic genericTopic;
 
+  private static View genericView;
+
+  private static Function genericFunction;
+
   @BeforeAll
   public static void setUp() throws Exception {
     TestBase.setUp();
@@ -171,6 +183,41 @@ public class TestSupportTags extends TestBase {
                 .build(),
             client.restClient(),
             Namespace.of(METALAKE_NAME, "catalog1", "schema1"));
+
+    genericView =
+        new GenericView(
+            ViewDTO.builder()
+                .withName("view1")
+                .withComment("comment1")
+                .withColumns(new ColumnDTO[0])
+                .withRepresentations(
+                    new SQLRepresentationDTO[] {
+                      SQLRepresentationDTO.builder()
+                          .withDialect(Dialects.TRINO)
+                          .withSql("SELECT 1")
+                          .build()
+                    })
+                .withProperties(Collections.emptyMap())
+                .withAudit(AuditDTO.builder().withCreator("test").build())
+                .build(),
+            client.restClient(),
+            Namespace.of(METALAKE_NAME, "catalog1", "schema1"));
+
+    genericFunction =
+        new GenericFunction(
+            FunctionDTO.builder()
+                .withName("function1")
+                .withFunctionType(FunctionType.SCALAR)
+                .withComment("comment1")
+                .withDeterministic(true)
+                .withDefinitions(
+                    new FunctionDefinitionDTO[] {
+                      
FunctionDefinitionDTO.builder().withReturnType(Types.StringType.get()).build()
+                    })
+                .withAudit(AuditDTO.builder().withCreator("test").build())
+                .build(),
+            client.restClient(),
+            Namespace.of(METALAKE_NAME, "catalog1", "schema1"));
   }
 
   @Test
@@ -224,6 +271,21 @@ public class TestSupportTags extends TestBase {
         MetadataObjects.of("catalog1.schema1", genericTopic.name(), 
MetadataObject.Type.TOPIC));
   }
 
+  @Test
+  public void testListTagsForView() throws JsonProcessingException {
+    testListTags(
+        genericView.supportsTags(),
+        MetadataObjects.of("catalog1.schema1", genericView.name(), 
MetadataObject.Type.VIEW));
+  }
+
+  @Test
+  public void testListTagsForFunction() throws JsonProcessingException {
+    testListTags(
+        genericFunction.supportsTags(),
+        MetadataObjects.of(
+            "catalog1.schema1", genericFunction.name(), 
MetadataObject.Type.FUNCTION));
+  }
+
   @Test
   public void testListTagsInfoForCatalog() throws JsonProcessingException {
     testListTagsInfo(
@@ -275,6 +337,21 @@ public class TestSupportTags extends TestBase {
         MetadataObjects.of("catalog1.schema1", genericTopic.name(), 
MetadataObject.Type.TOPIC));
   }
 
+  @Test
+  public void testListTagsInfoForView() throws JsonProcessingException {
+    testListTagsInfo(
+        genericView.supportsTags(),
+        MetadataObjects.of("catalog1.schema1", genericView.name(), 
MetadataObject.Type.VIEW));
+  }
+
+  @Test
+  public void testListTagsInfoForFunction() throws JsonProcessingException {
+    testListTagsInfo(
+        genericFunction.supportsTags(),
+        MetadataObjects.of(
+            "catalog1.schema1", genericFunction.name(), 
MetadataObject.Type.FUNCTION));
+  }
+
   @Test
   public void testGetTagForCatalog() throws JsonProcessingException {
     testGetTag(
@@ -326,6 +403,21 @@ public class TestSupportTags extends TestBase {
         MetadataObjects.of("catalog1.schema1", genericTopic.name(), 
MetadataObject.Type.TOPIC));
   }
 
+  @Test
+  public void testGetTagForView() throws JsonProcessingException {
+    testGetTag(
+        genericView.supportsTags(),
+        MetadataObjects.of("catalog1.schema1", genericView.name(), 
MetadataObject.Type.VIEW));
+  }
+
+  @Test
+  public void testGetTagForFunction() throws JsonProcessingException {
+    testGetTag(
+        genericFunction.supportsTags(),
+        MetadataObjects.of(
+            "catalog1.schema1", genericFunction.name(), 
MetadataObject.Type.FUNCTION));
+  }
+
   @Test
   public void testAssociateTagsForCatalog() throws JsonProcessingException {
     testAssociateTags(
@@ -377,6 +469,21 @@ public class TestSupportTags extends TestBase {
         MetadataObjects.of("catalog1.schema1", genericTopic.name(), 
MetadataObject.Type.TOPIC));
   }
 
+  @Test
+  public void testAssociateTagsForView() throws JsonProcessingException {
+    testAssociateTags(
+        genericView.supportsTags(),
+        MetadataObjects.of("catalog1.schema1", genericView.name(), 
MetadataObject.Type.VIEW));
+  }
+
+  @Test
+  public void testAssociateTagsForFunction() throws JsonProcessingException {
+    testAssociateTags(
+        genericFunction.supportsTags(),
+        MetadataObjects.of(
+            "catalog1.schema1", genericFunction.name(), 
MetadataObject.Type.FUNCTION));
+  }
+
   private void testListTags(SupportsTags supportsTags, MetadataObject 
metadataObject)
       throws JsonProcessingException {
     String path =
diff --git 
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/TagIT.java
 
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/TagIT.java
index bea92fde3f..ffa59da404 100644
--- 
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/TagIT.java
+++ 
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/TagIT.java
@@ -33,13 +33,24 @@ import org.apache.gravitino.dto.tag.MetadataObjectDTO;
 import org.apache.gravitino.exceptions.NoSuchTagException;
 import org.apache.gravitino.exceptions.TagAlreadyAssociatedException;
 import org.apache.gravitino.exceptions.TagAlreadyExistsException;
+import org.apache.gravitino.function.Function;
+import org.apache.gravitino.function.FunctionDefinition;
+import org.apache.gravitino.function.FunctionDefinitions;
+import org.apache.gravitino.function.FunctionImpl;
+import org.apache.gravitino.function.FunctionImpls;
+import org.apache.gravitino.function.FunctionParam;
+import org.apache.gravitino.function.FunctionParams;
+import org.apache.gravitino.function.FunctionType;
 import org.apache.gravitino.integration.test.container.ContainerSuite;
 import org.apache.gravitino.integration.test.container.HiveContainer;
 import org.apache.gravitino.integration.test.util.BaseIT;
 import org.apache.gravitino.integration.test.util.GravitinoITUtils;
 import org.apache.gravitino.model.Model;
 import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.Dialects;
+import org.apache.gravitino.rel.SQLRepresentation;
 import org.apache.gravitino.rel.Table;
+import org.apache.gravitino.rel.View;
 import org.apache.gravitino.rel.types.Types;
 import org.apache.gravitino.tag.Tag;
 import org.apache.gravitino.tag.TagChange;
@@ -60,6 +71,8 @@ public class TagIT extends BaseIT {
   private static Catalog relationalCatalog;
   private static Schema schema;
   private static Table table;
+  private static View view;
+  private static Function function;
 
   private static Catalog modelCatalog;
   private static Schema modelSchema;
@@ -114,6 +127,51 @@ public class TagIT extends BaseIT {
                 Collections.emptyMap());
     column = Arrays.stream(table.columns()).filter(c -> 
c.name().equals("col1")).findFirst().get();
 
+    // Create view
+    String viewName = GravitinoITUtils.genRandomName("tag_it_view");
+    Assertions.assertFalse(
+        
relationalCatalog.asViewCatalog().viewExists(NameIdentifier.of(schemaName, 
viewName)));
+    view =
+        relationalCatalog
+            .asViewCatalog()
+            .createView(
+                NameIdentifier.of(schemaName, viewName),
+                "comment",
+                new Column[] {
+                  Column.of("col1", Types.IntegerType.get()),
+                  Column.of("col2", Types.StringType.get())
+                },
+                new SQLRepresentation[] {
+                  SQLRepresentation.builder()
+                      .withDialect(Dialects.HIVE)
+                      .withSql("SELECT col1, col2 FROM " + table.name())
+                      .build()
+                },
+                null,
+                null,
+                Collections.emptyMap());
+
+    // Create function
+    String functionName = GravitinoITUtils.genRandomName("tag_it_function");
+    Assertions.assertFalse(
+        relationalCatalog
+            .asFunctionCatalog()
+            .functionExists(NameIdentifier.of(schemaName, functionName)));
+    FunctionParam param = FunctionParams.of("x", Types.IntegerType.get());
+    FunctionImpl impl = FunctionImpls.ofSql(FunctionImpl.RuntimeType.SPARK, 
"SELECT x + 1");
+    FunctionDefinition definition =
+        FunctionDefinitions.of(
+            new FunctionParam[] {param}, Types.IntegerType.get(), new 
FunctionImpl[] {impl});
+    function =
+        relationalCatalog
+            .asFunctionCatalog()
+            .registerFunction(
+                NameIdentifier.of(schemaName, functionName),
+                "comment",
+                FunctionType.SCALAR,
+                true,
+                new FunctionDefinition[] {definition});
+
     // Create model catalog
     String modelCatalogName = 
GravitinoITUtils.genRandomName("tag_it_model_catalog");
     Assertions.assertFalse(metalake.catalogExists(modelCatalogName));
@@ -140,7 +198,11 @@ public class TagIT extends BaseIT {
 
   @AfterAll
   public void tearDown() {
+    
relationalCatalog.asViewCatalog().dropView(NameIdentifier.of(schema.name(), 
view.name()));
     
relationalCatalog.asTableCatalog().dropTable(NameIdentifier.of(schema.name(), 
table.name()));
+    relationalCatalog
+        .asFunctionCatalog()
+        .dropFunction(NameIdentifier.of(schema.name(), function.name()));
     relationalCatalog.asSchemas().dropSchema(schema.name(), true);
     metalake.dropCatalog(relationalCatalog.name(), true);
 
@@ -164,6 +226,12 @@ public class TagIT extends BaseIT {
 
   @AfterEach
   public void cleanUp() {
+    String[] viewTags = view.supportsTags().listTags();
+    view.supportsTags().associateTags(null, viewTags);
+
+    String[] functionTags = function.supportsTags().listTags();
+    function.supportsTags().associateTags(null, functionTags);
+
     String[] tableTags = table.supportsTags().listTags();
     table.supportsTags().associateTags(null, tableTags);
 
@@ -552,6 +620,82 @@ public class TagIT extends BaseIT {
         MetadataObject.Type.TABLE, 
tag3.associatedObjects().objects()[0].type());
   }
 
+  @Test
+  public void testAssociateTagsToView() {
+    Tag tag1 =
+        metalake.createTag(
+            GravitinoITUtils.genRandomName("tag_it_view_tag1"), "comment1", 
Collections.emptyMap());
+    Tag tag2 =
+        metalake.createTag(
+            GravitinoITUtils.genRandomName("tag_it_view_tag2"), "comment2", 
Collections.emptyMap());
+    Tag tag3 =
+        metalake.createTag(
+            GravitinoITUtils.genRandomName("tag_it_view_tag3"), "comment3", 
Collections.emptyMap());
+
+    // Associate tags to catalog
+    relationalCatalog.supportsTags().associateTags(new String[] {tag1.name()}, 
null);
+
+    // Associate tags to schema
+    schema.supportsTags().associateTags(new String[] {tag2.name()}, null);
+
+    // Associate tags to view
+    String[] tags = view.supportsTags().associateTags(new String[] 
{tag3.name()}, null);
+
+    Assertions.assertEquals(1, tags.length);
+    Assertions.assertEquals(tag3.name(), tags[0]);
+
+    // Test list associated tags for view
+    String[] tags1 = view.supportsTags().listTags();
+    Assertions.assertEquals(3, tags1.length);
+    Set<String> tagNames = Sets.newHashSet(tags1);
+    Assertions.assertTrue(tagNames.contains(tag1.name()));
+    Assertions.assertTrue(tagNames.contains(tag2.name()));
+    Assertions.assertTrue(tagNames.contains(tag3.name()));
+
+    // Test list associated tags with details for view
+    Tag[] tags2 = view.supportsTags().listTagsInfo();
+    Assertions.assertEquals(3, tags2.length);
+
+    Set<Tag> nonInheritedTags =
+        Arrays.stream(tags2).filter(tag -> 
!tag.inherited().get()).collect(Collectors.toSet());
+    Set<Tag> inheritedTags =
+        Arrays.stream(tags2).filter(tag -> 
tag.inherited().get()).collect(Collectors.toSet());
+
+    Assertions.assertEquals(1, nonInheritedTags.size());
+    Assertions.assertEquals(2, inheritedTags.size());
+    Assertions.assertTrue(nonInheritedTags.contains(tag3));
+    Assertions.assertTrue(inheritedTags.contains(tag1));
+    Assertions.assertTrue(inheritedTags.contains(tag2));
+
+    // Test get associated tag for view
+    Tag resultTag1 = view.supportsTags().getTag(tag1.name());
+    Assertions.assertEquals(tag1, resultTag1);
+    Assertions.assertTrue(resultTag1.inherited().get());
+
+    Tag resultTag2 = view.supportsTags().getTag(tag2.name());
+    Assertions.assertEquals(tag2, resultTag2);
+    Assertions.assertTrue(resultTag2.inherited().get());
+
+    Tag resultTag3 = view.supportsTags().getTag(tag3.name());
+    Assertions.assertEquals(tag3, resultTag3);
+    Assertions.assertFalse(resultTag3.inherited().get());
+
+    // Test get objects associated with tag
+    Assertions.assertEquals(1, tag1.associatedObjects().count());
+    Assertions.assertEquals(relationalCatalog.name(), 
tag1.associatedObjects().objects()[0].name());
+    Assertions.assertEquals(
+        MetadataObject.Type.CATALOG, 
tag1.associatedObjects().objects()[0].type());
+
+    Assertions.assertEquals(1, tag2.associatedObjects().count());
+    Assertions.assertEquals(schema.name(), 
tag2.associatedObjects().objects()[0].name());
+    Assertions.assertEquals(
+        MetadataObject.Type.SCHEMA, 
tag2.associatedObjects().objects()[0].type());
+
+    Assertions.assertEquals(1, tag3.associatedObjects().count());
+    Assertions.assertEquals(view.name(), 
tag3.associatedObjects().objects()[0].name());
+    Assertions.assertEquals(MetadataObject.Type.VIEW, 
tag3.associatedObjects().objects()[0].type());
+  }
+
   @Test
   public void testAssociateTagsToColumn() {
     Tag tag1 =
@@ -765,4 +909,87 @@ public class TagIT extends BaseIT {
     Assertions.assertEquals(
         MetadataObject.Type.MODEL, 
tag3.associatedObjects().objects()[0].type());
   }
+
+  @Test
+  public void testAssociateTagsToFunction() {
+    Tag tag1 =
+        metalake.createTag(
+            GravitinoITUtils.genRandomName("tag_it_function_tag1"),
+            "comment1",
+            Collections.emptyMap());
+    Tag tag2 =
+        metalake.createTag(
+            GravitinoITUtils.genRandomName("tag_it_function_tag2"),
+            "comment2",
+            Collections.emptyMap());
+    Tag tag3 =
+        metalake.createTag(
+            GravitinoITUtils.genRandomName("tag_it_function_tag3"),
+            "comment3",
+            Collections.emptyMap());
+
+    // Associate tags to catalog
+    relationalCatalog.supportsTags().associateTags(new String[] {tag1.name()}, 
null);
+
+    // Associate tags to schema
+    schema.supportsTags().associateTags(new String[] {tag2.name()}, null);
+
+    // Associate tags to function
+    String[] tags = function.supportsTags().associateTags(new String[] 
{tag3.name()}, null);
+
+    Assertions.assertEquals(1, tags.length);
+    Assertions.assertEquals(tag3.name(), tags[0]);
+
+    // Test list associated tags for function
+    String[] tags1 = function.supportsTags().listTags();
+    Assertions.assertEquals(3, tags1.length);
+    Set<String> tagNames = Sets.newHashSet(tags1);
+    Assertions.assertTrue(tagNames.contains(tag1.name()));
+    Assertions.assertTrue(tagNames.contains(tag2.name()));
+    Assertions.assertTrue(tagNames.contains(tag3.name()));
+
+    // Test list associated tags with details for function
+    Tag[] tags2 = function.supportsTags().listTagsInfo();
+    Assertions.assertEquals(3, tags2.length);
+
+    Set<Tag> nonInheritedTags =
+        Arrays.stream(tags2).filter(tag -> 
!tag.inherited().get()).collect(Collectors.toSet());
+    Set<Tag> inheritedTags =
+        Arrays.stream(tags2).filter(tag -> 
tag.inherited().get()).collect(Collectors.toSet());
+
+    Assertions.assertEquals(1, nonInheritedTags.size());
+    Assertions.assertEquals(2, inheritedTags.size());
+    Assertions.assertTrue(nonInheritedTags.contains(tag3));
+    Assertions.assertTrue(inheritedTags.contains(tag1));
+    Assertions.assertTrue(inheritedTags.contains(tag2));
+
+    // Test get associated tag for function
+    Tag resultTag1 = function.supportsTags().getTag(tag1.name());
+    Assertions.assertEquals(tag1, resultTag1);
+    Assertions.assertTrue(resultTag1.inherited().get());
+
+    Tag resultTag2 = function.supportsTags().getTag(tag2.name());
+    Assertions.assertEquals(tag2, resultTag2);
+    Assertions.assertTrue(resultTag2.inherited().get());
+
+    Tag resultTag3 = function.supportsTags().getTag(tag3.name());
+    Assertions.assertEquals(tag3, resultTag3);
+    Assertions.assertFalse(resultTag3.inherited().get());
+
+    // Test get objects associated with tag
+    Assertions.assertEquals(1, tag1.associatedObjects().count());
+    Assertions.assertEquals(relationalCatalog.name(), 
tag1.associatedObjects().objects()[0].name());
+    Assertions.assertEquals(
+        MetadataObject.Type.CATALOG, 
tag1.associatedObjects().objects()[0].type());
+
+    Assertions.assertEquals(1, tag2.associatedObjects().count());
+    Assertions.assertEquals(schema.name(), 
tag2.associatedObjects().objects()[0].name());
+    Assertions.assertEquals(
+        MetadataObject.Type.SCHEMA, 
tag2.associatedObjects().objects()[0].type());
+
+    Assertions.assertEquals(1, tag3.associatedObjects().count());
+    Assertions.assertEquals(function.name(), 
tag3.associatedObjects().objects()[0].name());
+    Assertions.assertEquals(
+        MetadataObject.Type.FUNCTION, 
tag3.associatedObjects().objects()[0].type());
+  }
 }

Reply via email to