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

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


The following commit(s) were added to refs/heads/branch-1.3 by this push:
     new 61c1a501ed [Cherry-pick to branch-1.3] [#11639] fix(server): inherit 
tags/policies through multi-level schemas (#11658) (#11678)
61c1a501ed is described below

commit 61c1a501edf441a61c58bb6b383a92aae3698c00
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Jun 16 18:47:01 2026 +0800

    [Cherry-pick to branch-1.3] [#11639] fix(server): inherit tags/policies 
through multi-level schemas (#11658) (#11678)
    
    **Cherry-pick Information:**
    - Original commit: e19ebbff853d1a15a71d4a142e2d58abcbf8ed29
    - Target branch: `branch-1.3`
    - Status: ✅ Clean cherry-pick (no conflicts)
    
    Co-authored-by: roryqi <[email protected]>
    Co-authored-by: Claude Opus 4.8 <[email protected]>
---
 .../test/HierarchicalSchemaTagPolicyIT.java        | 446 +++++++++++++++++++++
 .../apache/gravitino/utils/MetadataObjectUtil.java |  67 ++++
 .../gravitino/utils/TestMetadataObjectUtil.java    |  51 +++
 docs/manage-policies-in-gravitino.md               |   8 +-
 docs/manage-tags-in-gravitino.md                   |   7 +-
 .../web/rest/MetadataObjectPolicyOperations.java   |  14 +-
 .../web/rest/MetadataObjectTagOperations.java      |  14 +-
 .../rest/TestMetadataObjectPolicyOperations.java   |  98 +++++
 .../web/rest/TestMetadataObjectTagOperations.java  | 106 +++++
 9 files changed, 795 insertions(+), 16 deletions(-)

diff --git 
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/HierarchicalSchemaTagPolicyIT.java
 
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/HierarchicalSchemaTagPolicyIT.java
new file mode 100644
index 0000000000..c782a7d119
--- /dev/null
+++ 
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/HierarchicalSchemaTagPolicyIT.java
@@ -0,0 +1,446 @@
+/*
+ * 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.integration.test;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.Configs;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Schema;
+import org.apache.gravitino.client.GravitinoMetalake;
+import org.apache.gravitino.integration.test.util.BaseIT;
+import org.apache.gravitino.integration.test.util.GravitinoITUtils;
+import org.apache.gravitino.policy.PolicyContent;
+import org.apache.gravitino.policy.PolicyContents;
+import org.apache.gravitino.policy.SupportsPolicies;
+import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.Table;
+import org.apache.gravitino.rel.types.Types;
+import org.apache.gravitino.tag.SupportsTags;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Integration test that verifies tags and policies assigned to a parent 
schema are inherited along
+ * a multi-level (hierarchical) schema hierarchy, including by the tables and 
columns under it.
+ *
+ * <p>A hierarchical schema {@code A:B:C} (using the configured {@code ":"} 
separator) has the
+ * intermediate schemas {@code A:B} and {@code A} as ancestors. Listing the 
tags/policies of {@code
+ * A:B:C}, or of a table/column under it, must therefore include the 
tags/policies assigned to
+ * {@code A:B}, {@code A} and the catalog as inherited (see issue #11639). The 
catalog is an Iceberg
+ * catalog backed by an in-memory H2 database, because {@code ":"} 
hierarchical schema names are
+ * only supported by Iceberg catalogs accessed through the Gravitino REST 
server with a configured
+ * schema separator.
+ *
+ * <p>This test runs entirely against the embedded server and an in-memory H2 
database, so it needs
+ * no Docker container and is intentionally not tagged {@code 
gravitino-docker-test}; tagging it as
+ * such would exclude it from the standard (non-Docker) integration-test runs.
+ */
+public class HierarchicalSchemaTagPolicyIT extends BaseIT {
+
+  private static final String METALAKE =
+      GravitinoITUtils.genRandomName("hierarchical_tag_policy_metalake");
+  private static final String CATALOG = "hierarchical_tag_policy_catalog";
+
+  private static final String ROOT_A = "A";
+  private static final String SCHEMA_AB = "A:B";
+  private static final String SCHEMA_ABC = "A:B:C";
+
+  // A table directly under the deepest schema A:B:C and one under the 
intermediate schema A:B, used
+  // to verify that tables/columns inherit tags and policies according to 
their position in the
+  // hierarchy.
+  private static final String LEAF_TABLE = "leaf_table";
+  private static final String MID_TABLE = "mid_table";
+  private static final String COLUMN_NAME = "col1";
+
+  private static GravitinoMetalake metalake;
+  private static Catalog catalog;
+
+  @BeforeAll
+  @Override
+  public void startIntegrationTest() throws Exception {
+    // Configure the hierarchical schema separator before the server starts.
+    customConfigs.put(Configs.SCHEMA_SEPARATOR.getKey(), ":");
+    super.startIntegrationTest();
+
+    metalake = client.createMetalake(METALAKE, "comment", new HashMap<>());
+
+    // Create an Iceberg catalog backed by an in-memory H2 database; it is 
lightweight, needs no
+    // extra container, and supports ':' hierarchical schema names. H2 is NOT 
a supported production
+    // Iceberg backend. The warehouse path and the H2 database name are 
derived from the randomized
+    // metalake name so concurrent runs on the same host do not share state 
and interfere.
+    Map<String, String> catalogProperties = new HashMap<>();
+    catalogProperties.put("catalog-backend", "jdbc");
+    catalogProperties.put("warehouse", "/tmp/" + METALAKE);
+    catalogProperties.put("uri", "jdbc:h2:mem:" + METALAKE + 
";DB_CLOSE_DELAY=-1;MODE=MYSQL");
+    catalogProperties.put("jdbc-driver", "org.h2.Driver");
+    catalogProperties.put("jdbc-initialize", "true");
+    catalog =
+        metalake.createCatalog(
+            CATALOG, Catalog.Type.RELATIONAL, "lakehouse-iceberg", "comment", 
catalogProperties);
+
+    // Creating "A:B:C" auto-creates the parent chain "A" and "A:B".
+    catalog.asSchemas().createSchema(SCHEMA_ABC, "hierarchical schema", new 
HashMap<>());
+    Assertions.assertEquals(SCHEMA_AB, 
catalog.asSchemas().loadSchema(SCHEMA_AB).name());
+    Assertions.assertEquals(ROOT_A, 
catalog.asSchemas().loadSchema(ROOT_A).name());
+
+    // A table under the deepest schema A:B:C and one under the intermediate 
schema A:B, so that
+    // table/column tag and policy inheritance through the hierarchy can be 
verified.
+    Column[] columns = new Column[] {Column.of(COLUMN_NAME, 
Types.IntegerType.get())};
+    catalog
+        .asTableCatalog()
+        .createTable(
+            NameIdentifier.of(SCHEMA_ABC, LEAF_TABLE),
+            columns,
+            "table under A:B:C",
+            new HashMap<>());
+    catalog
+        .asTableCatalog()
+        .createTable(
+            NameIdentifier.of(SCHEMA_AB, MID_TABLE), columns, "table under 
A:B", new HashMap<>());
+  }
+
+  @AfterAll
+  public void tearDown() {
+    if (metalake != null) {
+      metalake.dropCatalog(CATALOG, true);
+      client.dropMetalake(METALAKE, true);
+    }
+    // Note: this intentionally does not call super.stopIntegrationTest(). 
JUnit 5 invokes @AfterAll
+    // methods of both this subclass and the parent BaseIT, so 
BaseIT.stopIntegrationTest() (which
+    // stops the embedded server and clears customConfigs) still runs. Closing 
the client and
+    // nulling it here prevents BaseIT from double-closing it afterwards.
+    if (client != null) {
+      client.close();
+      client = null;
+    }
+  }
+
+  @Test
+  public void testTagInheritanceThroughHierarchicalSchema() {
+    String catalogTag = GravitinoITUtils.genRandomName("h_catalog_tag");
+    String rootTag = GravitinoITUtils.genRandomName("h_root_tag");
+    String midTag = GravitinoITUtils.genRandomName("h_mid_tag");
+    String leafTag = GravitinoITUtils.genRandomName("h_leaf_tag");
+    for (String tag : new String[] {catalogTag, rootTag, midTag, leafTag}) {
+      metalake.createTag(tag, "comment", Collections.emptyMap());
+    }
+
+    catalog.supportsTags().associateTags(new String[] {catalogTag}, null);
+    catalog
+        .asSchemas()
+        .loadSchema(ROOT_A)
+        .supportsTags()
+        .associateTags(new String[] {rootTag}, null);
+    catalog
+        .asSchemas()
+        .loadSchema(SCHEMA_AB)
+        .supportsTags()
+        .associateTags(new String[] {midTag}, null);
+    Schema leafSchema = catalog.asSchemas().loadSchema(SCHEMA_ABC);
+    leafSchema.supportsTags().associateTags(new String[] {leafTag}, null);
+
+    Map<String, Boolean> inheritedByName = 
tagInheritanceByName(leafSchema.supportsTags());
+
+    // The leaf schema's own tag plus the tags inherited from the intermediate 
schema A:B, the
+    // ancestor schema A, and the catalog. Before the fix, A:B and A were 
skipped.
+    Assertions.assertTrue(inheritedByName.containsKey(leafTag), "leaf tag 
should be present");
+    Assertions.assertTrue(
+        inheritedByName.containsKey(midTag), "tag on intermediate schema A:B 
should be inherited");
+    Assertions.assertTrue(
+        inheritedByName.containsKey(rootTag), "tag on ancestor schema A should 
be inherited");
+    Assertions.assertTrue(
+        inheritedByName.containsKey(catalogTag), "catalog tag should be 
inherited");
+
+    Assertions.assertFalse(inheritedByName.get(leafTag));
+    Assertions.assertTrue(inheritedByName.get(midTag));
+    Assertions.assertTrue(inheritedByName.get(rootTag));
+    Assertions.assertTrue(inheritedByName.get(catalogTag));
+
+    // Clean up associations and tags so the test is repeatable.
+    catalog.supportsTags().associateTags(null, new String[] {catalogTag});
+    catalog
+        .asSchemas()
+        .loadSchema(ROOT_A)
+        .supportsTags()
+        .associateTags(null, new String[] {rootTag});
+    catalog
+        .asSchemas()
+        .loadSchema(SCHEMA_AB)
+        .supportsTags()
+        .associateTags(null, new String[] {midTag});
+    leafSchema.supportsTags().associateTags(null, new String[] {leafTag});
+    for (String tag : new String[] {catalogTag, rootTag, midTag, leafTag}) {
+      metalake.deleteTag(tag);
+    }
+  }
+
+  @Test
+  public void testPolicyInheritanceThroughHierarchicalSchema() {
+    Set<MetadataObject.Type> types =
+        ImmutableSet.of(MetadataObject.Type.CATALOG, 
MetadataObject.Type.SCHEMA);
+    PolicyContent content = PolicyContents.custom(ImmutableMap.of("rule", 
"value"), types, null);
+
+    String catalogPolicy = GravitinoITUtils.genRandomName("h_catalog_policy");
+    String rootPolicy = GravitinoITUtils.genRandomName("h_root_policy");
+    String midPolicy = GravitinoITUtils.genRandomName("h_mid_policy");
+    String leafPolicy = GravitinoITUtils.genRandomName("h_leaf_policy");
+    for (String policy : new String[] {catalogPolicy, rootPolicy, midPolicy, 
leafPolicy}) {
+      metalake.createPolicy(policy, "custom", "comment", true, content);
+    }
+
+    catalog.supportsPolicies().associatePolicies(new String[] {catalogPolicy}, 
null);
+    catalog
+        .asSchemas()
+        .loadSchema(ROOT_A)
+        .supportsPolicies()
+        .associatePolicies(new String[] {rootPolicy}, null);
+    catalog
+        .asSchemas()
+        .loadSchema(SCHEMA_AB)
+        .supportsPolicies()
+        .associatePolicies(new String[] {midPolicy}, null);
+    Schema leafSchema = catalog.asSchemas().loadSchema(SCHEMA_ABC);
+    leafSchema.supportsPolicies().associatePolicies(new String[] {leafPolicy}, 
null);
+
+    Map<String, Boolean> inheritedByName = 
policyInheritanceByName(leafSchema.supportsPolicies());
+
+    Assertions.assertTrue(inheritedByName.containsKey(leafPolicy), "leaf 
policy should be present");
+    Assertions.assertTrue(
+        inheritedByName.containsKey(midPolicy),
+        "policy on intermediate schema A:B should be inherited");
+    Assertions.assertTrue(
+        inheritedByName.containsKey(rootPolicy), "policy on ancestor schema A 
should be inherited");
+    Assertions.assertTrue(
+        inheritedByName.containsKey(catalogPolicy), "catalog policy should be 
inherited");
+
+    Assertions.assertFalse(inheritedByName.get(leafPolicy));
+    Assertions.assertTrue(inheritedByName.get(midPolicy));
+    Assertions.assertTrue(inheritedByName.get(rootPolicy));
+    Assertions.assertTrue(inheritedByName.get(catalogPolicy));
+
+    // Clean up associations and policies so the test is repeatable.
+    catalog.supportsPolicies().associatePolicies(null, new String[] 
{catalogPolicy});
+    catalog
+        .asSchemas()
+        .loadSchema(ROOT_A)
+        .supportsPolicies()
+        .associatePolicies(null, new String[] {rootPolicy});
+    catalog
+        .asSchemas()
+        .loadSchema(SCHEMA_AB)
+        .supportsPolicies()
+        .associatePolicies(null, new String[] {midPolicy});
+    leafSchema.supportsPolicies().associatePolicies(null, new String[] 
{leafPolicy});
+    for (String policy : new String[] {catalogPolicy, rootPolicy, midPolicy, 
leafPolicy}) {
+      metalake.deletePolicy(policy);
+    }
+  }
+
+  @Test
+  public void testTagInheritanceForTableAndColumnUnderHierarchicalSchema() {
+    String catalogTag = GravitinoITUtils.genRandomName("ht_catalog_tag");
+    String rootTag = GravitinoITUtils.genRandomName("ht_root_tag");
+    String midTag = GravitinoITUtils.genRandomName("ht_mid_tag");
+    String leafTag = GravitinoITUtils.genRandomName("ht_leaf_tag");
+    for (String tag : new String[] {catalogTag, rootTag, midTag, leafTag}) {
+      metalake.createTag(tag, "comment", Collections.emptyMap());
+    }
+
+    catalog.supportsTags().associateTags(new String[] {catalogTag}, null);
+    catalog
+        .asSchemas()
+        .loadSchema(ROOT_A)
+        .supportsTags()
+        .associateTags(new String[] {rootTag}, null);
+    catalog
+        .asSchemas()
+        .loadSchema(SCHEMA_AB)
+        .supportsTags()
+        .associateTags(new String[] {midTag}, null);
+    catalog
+        .asSchemas()
+        .loadSchema(SCHEMA_ABC)
+        .supportsTags()
+        .associateTags(new String[] {leafTag}, null);
+
+    // A table under the deepest schema A:B:C inherits the whole chain: A:B:C, 
A:B, A and the
+    // catalog.
+    Table leafTable = 
catalog.asTableCatalog().loadTable(NameIdentifier.of(SCHEMA_ABC, LEAF_TABLE));
+    Map<String, Boolean> tableTags = 
tagInheritanceByName(leafTable.supportsTags());
+    for (String tag : new String[] {catalogTag, rootTag, midTag, leafTag}) {
+      Assertions.assertTrue(tableTags.containsKey(tag), tag + " should be 
inherited by the table");
+      Assertions.assertTrue(tableTags.get(tag), tag + " on an ancestor must be 
marked inherited");
+    }
+
+    // A column of that table inherits the same chain.
+    Map<String, Boolean> columnTags =
+        tagInheritanceByName(columnByName(leafTable, 
COLUMN_NAME).supportsTags());
+    for (String tag : new String[] {catalogTag, rootTag, midTag, leafTag}) {
+      Assertions.assertTrue(
+          columnTags.containsKey(tag), tag + " should be inherited by the 
column");
+      Assertions.assertTrue(columnTags.get(tag), tag + " on an ancestor must 
be marked inherited");
+    }
+
+    // A table under the intermediate schema A:B inherits A:B, A and the 
catalog, but not the tag
+    // assigned only to the deeper sibling schema A:B:C.
+    Map<String, Boolean> midTableTags =
+        tagInheritanceByName(
+            catalog
+                .asTableCatalog()
+                .loadTable(NameIdentifier.of(SCHEMA_AB, MID_TABLE))
+                .supportsTags());
+    Assertions.assertTrue(midTableTags.containsKey(catalogTag));
+    Assertions.assertTrue(midTableTags.containsKey(rootTag));
+    Assertions.assertTrue(midTableTags.containsKey(midTag));
+    Assertions.assertFalse(
+        midTableTags.containsKey(leafTag), "tag on A:B:C must not leak to a 
table under A:B");
+
+    // Clean up associations and tags so the test is repeatable.
+    catalog.supportsTags().associateTags(null, new String[] {catalogTag});
+    catalog
+        .asSchemas()
+        .loadSchema(ROOT_A)
+        .supportsTags()
+        .associateTags(null, new String[] {rootTag});
+    catalog
+        .asSchemas()
+        .loadSchema(SCHEMA_AB)
+        .supportsTags()
+        .associateTags(null, new String[] {midTag});
+    catalog
+        .asSchemas()
+        .loadSchema(SCHEMA_ABC)
+        .supportsTags()
+        .associateTags(null, new String[] {leafTag});
+    for (String tag : new String[] {catalogTag, rootTag, midTag, leafTag}) {
+      metalake.deleteTag(tag);
+    }
+  }
+
+  @Test
+  public void testPolicyInheritanceForTableUnderHierarchicalSchema() {
+    Set<MetadataObject.Type> types =
+        ImmutableSet.of(
+            MetadataObject.Type.CATALOG, MetadataObject.Type.SCHEMA, 
MetadataObject.Type.TABLE);
+    PolicyContent content = PolicyContents.custom(ImmutableMap.of("rule", 
"value"), types, null);
+
+    String catalogPolicy = GravitinoITUtils.genRandomName("ht_catalog_policy");
+    String rootPolicy = GravitinoITUtils.genRandomName("ht_root_policy");
+    String midPolicy = GravitinoITUtils.genRandomName("ht_mid_policy");
+    String leafPolicy = GravitinoITUtils.genRandomName("ht_leaf_policy");
+    for (String policy : new String[] {catalogPolicy, rootPolicy, midPolicy, 
leafPolicy}) {
+      metalake.createPolicy(policy, "custom", "comment", true, content);
+    }
+
+    catalog.supportsPolicies().associatePolicies(new String[] {catalogPolicy}, 
null);
+    catalog
+        .asSchemas()
+        .loadSchema(ROOT_A)
+        .supportsPolicies()
+        .associatePolicies(new String[] {rootPolicy}, null);
+    catalog
+        .asSchemas()
+        .loadSchema(SCHEMA_AB)
+        .supportsPolicies()
+        .associatePolicies(new String[] {midPolicy}, null);
+    catalog
+        .asSchemas()
+        .loadSchema(SCHEMA_ABC)
+        .supportsPolicies()
+        .associatePolicies(new String[] {leafPolicy}, null);
+
+    // A table under the deepest schema A:B:C inherits policies through the 
whole chain.
+    Map<String, Boolean> tablePolicies =
+        policyInheritanceByName(
+            catalog
+                .asTableCatalog()
+                .loadTable(NameIdentifier.of(SCHEMA_ABC, LEAF_TABLE))
+                .supportsPolicies());
+    for (String policy : new String[] {catalogPolicy, rootPolicy, midPolicy, 
leafPolicy}) {
+      Assertions.assertTrue(
+          tablePolicies.containsKey(policy), policy + " should be inherited by 
the table");
+      Assertions.assertTrue(
+          tablePolicies.get(policy), policy + " on an ancestor must be marked 
inherited");
+    }
+
+    // A table under the intermediate schema A:B does not inherit the policy 
assigned only to the
+    // deeper sibling schema A:B:C.
+    Map<String, Boolean> midTablePolicies =
+        policyInheritanceByName(
+            catalog
+                .asTableCatalog()
+                .loadTable(NameIdentifier.of(SCHEMA_AB, MID_TABLE))
+                .supportsPolicies());
+    Assertions.assertTrue(midTablePolicies.containsKey(catalogPolicy));
+    Assertions.assertTrue(midTablePolicies.containsKey(rootPolicy));
+    Assertions.assertTrue(midTablePolicies.containsKey(midPolicy));
+    Assertions.assertFalse(
+        midTablePolicies.containsKey(leafPolicy),
+        "policy on A:B:C must not leak to a table under A:B");
+
+    // Clean up associations and policies so the test is repeatable.
+    catalog.supportsPolicies().associatePolicies(null, new String[] 
{catalogPolicy});
+    catalog
+        .asSchemas()
+        .loadSchema(ROOT_A)
+        .supportsPolicies()
+        .associatePolicies(null, new String[] {rootPolicy});
+    catalog
+        .asSchemas()
+        .loadSchema(SCHEMA_AB)
+        .supportsPolicies()
+        .associatePolicies(null, new String[] {midPolicy});
+    catalog
+        .asSchemas()
+        .loadSchema(SCHEMA_ABC)
+        .supportsPolicies()
+        .associatePolicies(null, new String[] {leafPolicy});
+    for (String policy : new String[] {catalogPolicy, rootPolicy, midPolicy, 
leafPolicy}) {
+      metalake.deletePolicy(policy);
+    }
+  }
+
+  private static Map<String, Boolean> tagInheritanceByName(SupportsTags 
supportsTags) {
+    return Stream.of(supportsTags.listTagsInfo())
+        .collect(Collectors.toMap(tag -> tag.name(), tag -> 
tag.inherited().get()));
+  }
+
+  private static Map<String, Boolean> policyInheritanceByName(SupportsPolicies 
supportsPolicies) {
+    return Stream.of(supportsPolicies.listPolicyInfos())
+        .collect(Collectors.toMap(policy -> policy.name(), policy -> 
policy.inherited().get()));
+  }
+
+  private static Column columnByName(Table table, String columnName) {
+    return Stream.of(table.columns())
+        .filter(column -> column.name().equals(columnName))
+        .findFirst()
+        .orElseThrow(() -> new IllegalStateException("Column not found: " + 
columnName));
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java 
b/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java
index 42128cf287..b14845b669 100644
--- a/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java
+++ b/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java
@@ -24,12 +24,15 @@ import com.google.common.base.Joiner;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.ImmutableBiMap;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Optional;
 import java.util.function.Supplier;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.Entity;
 import org.apache.gravitino.GravitinoEnv;
 import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.authorization.AuthorizationUtils;
 import org.apache.gravitino.exceptions.IllegalMetadataObjectException;
@@ -139,6 +142,70 @@ public class MetadataObjectUtil {
     }
   }
 
+  /**
+   * Returns the ancestor metadata objects from which the given metadata 
object inherits tags and
+   * policies, ordered from the nearest ancestor to the outermost (the 
metadata object itself is not
+   * included).
+   *
+   * <p>This is similar to repeatedly calling {@link 
MetadataObjects#parent(MetadataObject)}, but it
+   * additionally expands hierarchical (multi-level) schemas: a schema name 
such as {@code a:b:c}
+   * (using the configured schema separator) has the intermediate schemas 
{@code a:b} and {@code a}
+   * as ancestors. Without this expansion, inheritance would jump directly 
from {@code a:b:c} to the
+   * catalog and skip the intermediate parent schemas, so tags/policies 
assigned to a parent schema
+   * would not be inherited by its child schemas and the objects within them.
+   *
+   * <p>The hierarchical schema separator is resolved from {@link
+   * HierarchicalSchemaUtil#schemaSeparator()}, which reads the server config 
from {@link
+   * org.apache.gravitino.GravitinoEnv} and falls back to the default {@code 
":"} ({@link
+   * org.apache.gravitino.Configs#SCHEMA_SEPARATOR}) when the config is not 
available (for example
+   * in unit tests that do not boot the server). Tests that need a 
deterministic separator without
+   * booting the server should call {@link 
#getParentMetadataObjects(MetadataObject, String)} with
+   * an explicit separator instead.
+   *
+   * @param object The metadata object
+   * @return The ancestor metadata objects, nearest first
+   */
+  public static List<MetadataObject> getParentMetadataObjects(MetadataObject 
object) {
+    return getParentMetadataObjects(object, 
HierarchicalSchemaUtil.schemaSeparator());
+  }
+
+  /**
+   * Returns the ancestor metadata objects of the given metadata object using 
the provided
+   * hierarchical schema separator. See {@link 
#getParentMetadataObjects(MetadataObject)}.
+   *
+   * @param object The metadata object
+   * @param separator The external hierarchical schema separator
+   * @return The ancestor metadata objects, nearest first
+   */
+  static List<MetadataObject> getParentMetadataObjects(MetadataObject object, 
String separator) {
+    Preconditions.checkArgument(object != null, "metadataObject cannot be 
null");
+
+    List<MetadataObject> parents = new ArrayList<>();
+    MetadataObject current = object;
+    while (current != null) {
+      // When the current object is a hierarchical (multi-level) schema, its 
intermediate parent
+      // schemas are ancestors too. Add them (nearest first) before walking up 
to the catalog.
+      if (current.type() == MetadataObject.Type.SCHEMA
+          && HierarchicalSchemaUtil.isHierarchical(current.name(), separator)) 
{
+        String catalogName = current.parent();
+        List<String> ancestorSchemas =
+            HierarchicalSchemaUtil.getAncestorNames(current.name(), separator);
+        for (int i = ancestorSchemas.size() - 1; i >= 0; i--) {
+          parents.add(
+              MetadataObjects.of(catalogName, ancestorSchemas.get(i), 
MetadataObject.Type.SCHEMA));
+        }
+      }
+
+      MetadataObject parent = MetadataObjects.parent(current);
+      if (parent != null) {
+        parents.add(parent);
+      }
+      current = parent;
+    }
+
+    return parents;
+  }
+
   /**
    * This method will check if the entity is existed explicitly, internally 
this check will load the
    * entity from underlying sources to entity store if not stored, and will 
allocate an uid for this
diff --git 
a/core/src/test/java/org/apache/gravitino/utils/TestMetadataObjectUtil.java 
b/core/src/test/java/org/apache/gravitino/utils/TestMetadataObjectUtil.java
index 4471a1e0ec..94e93bba66 100644
--- a/core/src/test/java/org/apache/gravitino/utils/TestMetadataObjectUtil.java
+++ b/core/src/test/java/org/apache/gravitino/utils/TestMetadataObjectUtil.java
@@ -18,6 +18,8 @@
  */
 package org.apache.gravitino.utils;
 
+import java.util.List;
+import java.util.stream.Collectors;
 import org.apache.gravitino.Entity;
 import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.MetadataObjects;
@@ -146,4 +148,53 @@ public class TestMetadataObjectUtil {
         MetadataObjectUtil.toEntityIdent(
             "metalake", MetadataObjects.of("catalog.schema", "view", 
MetadataObject.Type.VIEW)));
   }
+
+  @Test
+  public void testGetParentMetadataObjectsForFlatSchema() {
+    // A table under a flat (single-level) schema inherits from [schema, 
catalog].
+    MetadataObject table = MetadataObjects.of("catalog.schema", "table", 
MetadataObject.Type.TABLE);
+    Assertions.assertEquals(
+        List.of("SCHEMA:catalog.schema", "CATALOG:catalog"),
+        describe(MetadataObjectUtil.getParentMetadataObjects(table, ":")));
+
+    // A flat schema inherits only from its catalog.
+    MetadataObject schema = MetadataObjects.of("catalog", "schema", 
MetadataObject.Type.SCHEMA);
+    Assertions.assertEquals(
+        List.of("CATALOG:catalog"),
+        describe(MetadataObjectUtil.getParentMetadataObjects(schema, ":")));
+
+    // A catalog has no ancestors.
+    MetadataObject catalog = MetadataObjects.of(null, "catalog", 
MetadataObject.Type.CATALOG);
+    Assertions.assertTrue(MetadataObjectUtil.getParentMetadataObjects(catalog, 
":").isEmpty());
+  }
+
+  @Test
+  public void testGetParentMetadataObjectsForHierarchicalSchema() {
+    // A table under hierarchical schema a:b:c inherits from the schema and 
all its ancestor
+    // schemas (nearest first), then the catalog.
+    MetadataObject table = MetadataObjects.of("catalog.a:b:c", "table", 
MetadataObject.Type.TABLE);
+    Assertions.assertEquals(
+        List.of(
+            "SCHEMA:catalog.a:b:c", "SCHEMA:catalog.a:b", "SCHEMA:catalog.a", 
"CATALOG:catalog"),
+        describe(MetadataObjectUtil.getParentMetadataObjects(table, ":")));
+
+    // The hierarchical schema itself inherits from its ancestor schemas and 
the catalog, but not
+    // from itself.
+    MetadataObject schema = MetadataObjects.of("catalog", "a:b:c", 
MetadataObject.Type.SCHEMA);
+    Assertions.assertEquals(
+        List.of("SCHEMA:catalog.a:b", "SCHEMA:catalog.a", "CATALOG:catalog"),
+        describe(MetadataObjectUtil.getParentMetadataObjects(schema, ":")));
+
+    // A column under a hierarchical schema walks table -> schema -> ancestor 
schemas -> catalog.
+    MetadataObject column =
+        MetadataObjects.of("catalog.a:b.table", "col", 
MetadataObject.Type.COLUMN);
+    Assertions.assertEquals(
+        List.of(
+            "TABLE:catalog.a:b.table", "SCHEMA:catalog.a:b", 
"SCHEMA:catalog.a", "CATALOG:catalog"),
+        describe(MetadataObjectUtil.getParentMetadataObjects(column, ":")));
+  }
+
+  private static List<String> describe(List<MetadataObject> objects) {
+    return objects.stream().map(o -> o.type() + ":" + 
o.fullName()).collect(Collectors.toList());
+  }
 }
diff --git a/docs/manage-policies-in-gravitino.md 
b/docs/manage-policies-in-gravitino.md
index 5137a50d98..f195018780 100644
--- a/docs/manage-policies-in-gravitino.md
+++ b/docs/manage-policies-in-gravitino.md
@@ -29,7 +29,10 @@ Javadoc and REST API documentation.
    associated with policies. 
 3. Policies in Gravitino are inheritable, so listing policies of a metadata 
object will also list the
    policies of its parent metadata objects. For example, listing policies of a 
`Table` will also list
-   the policies of its parent `Schema` and `Catalog`.
+   the policies of its parent `Schema` and `Catalog`. For catalogs that 
support multi-level
+   (hierarchical) schemas, such as a schema named `a:b:c` (using the 
configured schema separator),
+   the intermediate parent schemas `a:b` and `a` are also part of the 
hierarchy, so their policies
+   are inherited as well.
 4. The same policy can be associated with both parent and child metadata 
objects. But when you list the
    associated policies of a child metadata object, this policy will be 
included only once in the result
    list with `inherited` value `false`.
@@ -381,7 +384,8 @@ schema.supportsPolicies().associatePolicies(new String[] 
{"policy1"}, null);
 ### List Associated Policies for a Metadata Object
 
 List all the policies associated with a metadata object. If a policy is 
inheritable, 
-listing policies of a metadata object will also list the policies of its 
parent metadata objects.
+listing policies of a metadata object will also list the policies of its 
parent metadata objects,
+including the intermediate parent schemas of a multi-level (hierarchical) 
schema.
 
 The request path for REST API is 
`/api/metalakes/{metalake}/objects/{metadataObjectType}/{metadataObjectFullName}/policies`.
 
diff --git a/docs/manage-tags-in-gravitino.md b/docs/manage-tags-in-gravitino.md
index 7c5aae7433..2714d9749a 100644
--- a/docs/manage-tags-in-gravitino.md
+++ b/docs/manage-tags-in-gravitino.md
@@ -31,7 +31,10 @@ the future versions.
 2`CATALOG`, `SCHEMA`, `TABLE`, `FILESET`, `TOPIC`, `MODEL`, and `COLUMN` 
objects can be tagged.
 3. Tags in Gravitino is inheritable, so listing tags of a metadata object will 
also list the
    tags of its parent metadata objects. For example, listing tags of a `Table` 
will also list
-   the tags of its parent `Schema` and `Catalog`.
+   the tags of its parent `Schema` and `Catalog`. For catalogs that support 
multi-level
+   (hierarchical) schemas, such as a schema named `a:b:c` (using the 
configured schema
+   separator), the intermediate parent schemas `a:b` and `a` are also part of 
the hierarchy, so
+   their tags are inherited as well.
 4. The same tag can be associated with both parent and child metadata objects. 
But when you list the
    associated tags of a child metadata object, this tag will be included only 
once in the result
    list with `inherited` value `false`.
@@ -249,7 +252,7 @@ schema1.supportsTags().associateTags(new String[] {"tag1"}, 
null);
 
 List all the tags associated with a metadata object. The tags in Gravitino are
 inheritable, so listing tags of a metadata object will also list the tags of 
its parent metadata
-objects.
+objects, including the intermediate parent schemas of a multi-level 
(hierarchical) schema.
 
 The request path for REST API is 
`/api/metalakes/{metalake}/objects/{metadataObjectType}/{metadataObjectName}/tags`.
 
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectPolicyOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectPolicyOperations.java
index defc9335e0..78a03570fb 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectPolicyOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectPolicyOperations.java
@@ -60,6 +60,7 @@ import 
org.apache.gravitino.server.authorization.annotations.AuthorizationObject
 import 
org.apache.gravitino.server.authorization.annotations.AuthorizationRequest;
 import 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants;
 import org.apache.gravitino.server.web.Utils;
+import org.apache.gravitino.utils.MetadataObjectUtil;
 import org.apache.gravitino.utils.NameIdentifierUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -110,11 +111,13 @@ public class MetadataObjectPolicyOperations {
             Optional<PolicyDTO> policyDTO =
                 policyEntity.map(t -> PolicyOperations.toDTO(t, 
Optional.of(false)));
 
-            MetadataObject parentObject = MetadataObjects.parent(object);
-            while (!policyEntity.isPresent() && parentObject != null) {
+            for (MetadataObject parentObject :
+                MetadataObjectUtil.getParentMetadataObjects(object)) {
+              if (policyEntity.isPresent()) {
+                break;
+              }
               policyEntity = getPolicyForObject(metalake, parentObject, 
policyName);
               policyDTO = policyEntity.map(t -> PolicyOperations.toDTO(t, 
Optional.of(true)));
-              parentObject = MetadataObjects.parent(parentObject);
             }
 
             if (!policyDTO.isPresent()) {
@@ -194,8 +197,8 @@ public class MetadataObjectPolicyOperations {
                       .toArray(PolicyDTO[]::new));
             }
 
-            MetadataObject parentObject = MetadataObjects.parent(object);
-            while (parentObject != null) {
+            for (MetadataObject parentObject :
+                MetadataObjectUtil.getParentMetadataObjects(object)) {
               PolicyEntity[] inheritedPolicies =
                   policyDispatcher.listPolicyInfosForMetadataObject(metalake, 
parentObject);
               if (ArrayUtils.isNotEmpty(inheritedPolicies)) {
@@ -205,7 +208,6 @@ public class MetadataObjectPolicyOperations {
                         .map(t -> PolicyOperations.toDTO(t, Optional.of(true)))
                         .toArray(PolicyDTO[]::new));
               }
-              parentObject = MetadataObjects.parent(parentObject);
             }
 
             if (verbose) {
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectTagOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectTagOperations.java
index c6cd5c6340..d96e655d30 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectTagOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectTagOperations.java
@@ -62,6 +62,7 @@ import 
org.apache.gravitino.server.authorization.expression.AuthorizationExpress
 import org.apache.gravitino.server.web.Utils;
 import org.apache.gravitino.tag.Tag;
 import org.apache.gravitino.tag.TagDispatcher;
+import org.apache.gravitino.utils.MetadataObjectUtil;
 import org.apache.gravitino.utils.NameIdentifierUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -114,11 +115,13 @@ public class MetadataObjectTagOperations {
             Optional<Tag> tag = getTagForObject(metalake, object, tagName);
             Optional<TagDTO> tagDTO = tag.map(t -> DTOConverters.toDTO(t, 
Optional.of(false)));
 
-            MetadataObject parentObject = MetadataObjects.parent(object);
-            while (!tag.isPresent() && parentObject != null) {
+            for (MetadataObject parentObject :
+                MetadataObjectUtil.getParentMetadataObjects(object)) {
+              if (tag.isPresent()) {
+                break;
+              }
               tag = getTagForObject(metalake, parentObject, tagName);
               tagDTO = tag.map(t -> DTOConverters.toDTO(t, Optional.of(true)));
-              parentObject = MetadataObjects.parent(parentObject);
             }
 
             if (!tagDTO.isPresent()) {
@@ -190,8 +193,8 @@ public class MetadataObjectTagOperations {
                       .toArray(TagDTO[]::new));
             }
 
-            MetadataObject parentObject = MetadataObjects.parent(object);
-            while (parentObject != null) {
+            for (MetadataObject parentObject :
+                MetadataObjectUtil.getParentMetadataObjects(object)) {
               Tag[] inheritedTags =
                   tagDispatcher.listTagsInfoForMetadataObject(metalake, 
parentObject);
               if (ArrayUtils.isNotEmpty(inheritedTags)) {
@@ -201,7 +204,6 @@ public class MetadataObjectTagOperations {
                         .map(t -> DTOConverters.toDTO(t, Optional.of(true)))
                         .toArray(TagDTO[]::new));
               }
-              parentObject = MetadataObjects.parent(parentObject);
             }
 
             if (verbose) {
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectPolicyOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectPolicyOperations.java
index fe28a0616c..e95b06fb88 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectPolicyOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectPolicyOperations.java
@@ -311,6 +311,104 @@ public class TestMetadataObjectPolicyOperations extends 
JerseyTest {
     Assertions.assertFalse(resultPolicies8.get("policy0").inherited().get());
   }
 
+  @Test
+  public void testListPoliciesForObjectUnderHierarchicalSchema() {
+    // Hierarchical (multi-level) schema "a:b:c" using the default separator 
":". Its ancestor
+    // schemas are "a" and "a:b". A table under it must inherit policies from 
the schema itself,
+    // all ancestor schemas, and the catalog. The REST operation resolves the 
separator via
+    // MetadataObjectUtil.getParentMetadataObjects(MetadataObject), which 
falls back to the default
+    // ":" here because GravitinoEnv config is not booted in this unit test.
+    MetadataObject catalog = MetadataObjects.parse("hcat", 
MetadataObject.Type.CATALOG);
+    MetadataObject schemaA = MetadataObjects.parse("hcat.a", 
MetadataObject.Type.SCHEMA);
+    MetadataObject schemaB = MetadataObjects.parse("hcat.a:b", 
MetadataObject.Type.SCHEMA);
+    MetadataObject schemaC = MetadataObjects.parse("hcat.a:b:c", 
MetadataObject.Type.SCHEMA);
+    MetadataObject table = MetadataObjects.parse("hcat.a:b:c.tbl", 
MetadataObject.Type.TABLE);
+
+    when(policyManager.listPolicyInfosForMetadataObject(metalake, catalog))
+        .thenReturn(new PolicyEntity[] {createPolicy("catalogPolicy")});
+    when(policyManager.listPolicyInfosForMetadataObject(metalake, schemaA))
+        .thenReturn(new PolicyEntity[] {createPolicy("schemaAPolicy")});
+    when(policyManager.listPolicyInfosForMetadataObject(metalake, schemaB))
+        .thenReturn(new PolicyEntity[] {createPolicy("schemaBPolicy")});
+    when(policyManager.listPolicyInfosForMetadataObject(metalake, schemaC))
+        .thenReturn(new PolicyEntity[] {createPolicy("schemaCPolicy")});
+    when(policyManager.listPolicyInfosForMetadataObject(metalake, table))
+        .thenReturn(new PolicyEntity[] {createPolicy("tablePolicy")});
+
+    Response response =
+        target(basePath(metalake))
+            .path(table.type().toString())
+            .path(table.fullName())
+            .path("policies")
+            .queryParam("details", true)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response.getStatus());
+    PolicyListResponse policyListResponse = 
response.readEntity(PolicyListResponse.class);
+    Assertions.assertEquals(0, policyListResponse.getCode());
+    Assertions.assertEquals(5, policyListResponse.getPolicies().length);
+
+    Map<String, Policy> resultPolicies =
+        Arrays.stream(policyListResponse.getPolicies())
+            .collect(Collectors.toMap(Policy::name, Function.identity()));
+
+    
Assertions.assertFalse(resultPolicies.get("tablePolicy").inherited().get());
+    
Assertions.assertTrue(resultPolicies.get("schemaCPolicy").inherited().get());
+    
Assertions.assertTrue(resultPolicies.get("schemaBPolicy").inherited().get());
+    
Assertions.assertTrue(resultPolicies.get("schemaAPolicy").inherited().get());
+    
Assertions.assertTrue(resultPolicies.get("catalogPolicy").inherited().get());
+  }
+
+  @Test
+  public void testGetPolicyForObjectUnderHierarchicalSchema() {
+    // Hierarchical (multi-level) schema "a:b:c" using the default separator 
":". The REST operation
+    // resolves the separator via 
MetadataObjectUtil.getParentMetadataObjects(MetadataObject), which
+    // falls back to the default ":" here because GravitinoEnv config is not 
booted in this unit
+    // test.
+    MetadataObject schemaA = MetadataObjects.parse("hcat.a", 
MetadataObject.Type.SCHEMA);
+    MetadataObject schemaB = MetadataObjects.parse("hcat.a:b", 
MetadataObject.Type.SCHEMA);
+    MetadataObject table = MetadataObjects.parse("hcat.a:b:c.tbl", 
MetadataObject.Type.TABLE);
+
+    PolicyEntity schemaAPolicy = createPolicy("schemaAPolicy");
+    PolicyEntity schemaBPolicy = createPolicy("schemaBPolicy");
+    when(policyManager.getPolicyForMetadataObject(metalake, schemaA, 
"schemaAPolicy"))
+        .thenReturn(schemaAPolicy);
+    when(policyManager.getPolicyForMetadataObject(metalake, schemaB, 
"schemaBPolicy"))
+        .thenReturn(schemaBPolicy);
+
+    // A policy on the intermediate schema "a:b" is inherited by a table under 
"a:b:c".
+    Response responseB =
+        target(basePath(metalake))
+            .path(table.type().toString())
+            .path(table.fullName())
+            .path("policies")
+            .path("schemaBPolicy")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
responseB.getStatus());
+    Policy respPolicyB = 
responseB.readEntity(PolicyResponse.class).getPolicy();
+    Assertions.assertEquals("schemaBPolicy", respPolicyB.name());
+    Assertions.assertTrue(respPolicyB.inherited().get());
+
+    // A policy on the ancestor schema "a" is also inherited by a table under 
"a:b:c".
+    Response responseA =
+        target(basePath(metalake))
+            .path(table.type().toString())
+            .path(table.fullName())
+            .path("policies")
+            .path("schemaAPolicy")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
responseA.getStatus());
+    Policy respPolicyA = 
responseA.readEntity(PolicyResponse.class).getPolicy();
+    Assertions.assertEquals("schemaAPolicy", respPolicyA.name());
+    Assertions.assertTrue(respPolicyA.inherited().get());
+  }
+
   @Test
   public void testGetPolicyForObject() {
     PolicyEntity policy1 = createPolicy("policy1");
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectTagOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectTagOperations.java
index f138539fbe..158324bb4b 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectTagOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectTagOperations.java
@@ -380,6 +380,112 @@ public class TestMetadataObjectTagOperations extends 
BaseOperationsTest {
     Assertions.assertFalse(resultTags8.get("tag0").inherited().get());
   }
 
+  @Test
+  public void testListTagsForObjectUnderHierarchicalSchema() {
+    // Hierarchical (multi-level) schema "a:b:c" using the default separator 
":". Its ancestor
+    // schemas are "a" and "a:b". A table under it must inherit tags from the 
schema itself, all
+    // ancestor schemas, and the catalog. The REST operation resolves the 
separator via
+    // MetadataObjectUtil.getParentMetadataObjects(MetadataObject), which 
falls back to the default
+    // ":" here because GravitinoEnv config is not booted in this unit test.
+    MetadataObject catalog = MetadataObjects.parse("hcat", 
MetadataObject.Type.CATALOG);
+    MetadataObject schemaA = MetadataObjects.parse("hcat.a", 
MetadataObject.Type.SCHEMA);
+    MetadataObject schemaB = MetadataObjects.parse("hcat.a:b", 
MetadataObject.Type.SCHEMA);
+    MetadataObject schemaC = MetadataObjects.parse("hcat.a:b:c", 
MetadataObject.Type.SCHEMA);
+    MetadataObject table = MetadataObjects.parse("hcat.a:b:c.tbl", 
MetadataObject.Type.TABLE);
+
+    when(tagManager.listTagsInfoForMetadataObject(metalake, catalog))
+        .thenReturn(tagInfos("catalogTag"));
+    when(tagManager.listTagsInfoForMetadataObject(metalake, schemaA))
+        .thenReturn(tagInfos("schemaATag"));
+    when(tagManager.listTagsInfoForMetadataObject(metalake, schemaB))
+        .thenReturn(tagInfos("schemaBTag"));
+    when(tagManager.listTagsInfoForMetadataObject(metalake, schemaC))
+        .thenReturn(tagInfos("schemaCTag"));
+    when(tagManager.listTagsInfoForMetadataObject(metalake, table))
+        .thenReturn(tagInfos("tableTag"));
+
+    Response response =
+        target(basePath(metalake))
+            .path(table.type().toString())
+            .path(table.fullName())
+            .path("tags")
+            .queryParam("details", true)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
response.getStatus());
+    TagListResponse tagListResponse = 
response.readEntity(TagListResponse.class);
+    Assertions.assertEquals(0, tagListResponse.getCode());
+    Assertions.assertEquals(5, tagListResponse.getTags().length);
+
+    Map<String, Tag> resultTags =
+        Arrays.stream(tagListResponse.getTags())
+            .collect(Collectors.toMap(Tag::name, Function.identity()));
+
+    Assertions.assertFalse(resultTags.get("tableTag").inherited().get());
+    Assertions.assertTrue(resultTags.get("schemaCTag").inherited().get());
+    Assertions.assertTrue(resultTags.get("schemaBTag").inherited().get());
+    Assertions.assertTrue(resultTags.get("schemaATag").inherited().get());
+    Assertions.assertTrue(resultTags.get("catalogTag").inherited().get());
+  }
+
+  private Tag[] tagInfos(String name) {
+    return new Tag[] {
+      
TagEntity.builder().withName(name).withId(1L).withAuditInfo(testAuditInfo1).build()
+    };
+  }
+
+  @Test
+  public void testGetTagForObjectUnderHierarchicalSchema() {
+    // Hierarchical (multi-level) schema "a:b:c" using the default separator 
":". The REST operation
+    // resolves the separator via 
MetadataObjectUtil.getParentMetadataObjects(MetadataObject), which
+    // falls back to the default ":" here because GravitinoEnv config is not 
booted in this unit
+    // test.
+    MetadataObject schemaA = MetadataObjects.parse("hcat.a", 
MetadataObject.Type.SCHEMA);
+    MetadataObject schemaB = MetadataObjects.parse("hcat.a:b", 
MetadataObject.Type.SCHEMA);
+    MetadataObject table = MetadataObjects.parse("hcat.a:b:c.tbl", 
MetadataObject.Type.TABLE);
+
+    TagEntity schemaATag =
+        
TagEntity.builder().withName("schemaATag").withId(1L).withAuditInfo(testAuditInfo1).build();
+    TagEntity schemaBTag =
+        
TagEntity.builder().withName("schemaBTag").withId(2L).withAuditInfo(testAuditInfo1).build();
+    when(tagManager.getTagForMetadataObject(metalake, schemaA, "schemaATag"))
+        .thenReturn(schemaATag);
+    when(tagManager.getTagForMetadataObject(metalake, schemaB, "schemaBTag"))
+        .thenReturn(schemaBTag);
+
+    // A tag on the intermediate schema "a:b" is inherited by a table under 
"a:b:c".
+    Response responseB =
+        target(basePath(metalake))
+            .path(table.type().toString())
+            .path(table.fullName())
+            .path("tags")
+            .path("schemaBTag")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
responseB.getStatus());
+    Tag respTagB = responseB.readEntity(TagResponse.class).getTag();
+    Assertions.assertEquals("schemaBTag", respTagB.name());
+    Assertions.assertTrue(respTagB.inherited().get());
+
+    // A tag on the ancestor schema "a" is also inherited by a table under 
"a:b:c".
+    Response responseA =
+        target(basePath(metalake))
+            .path(table.type().toString())
+            .path(table.fullName())
+            .path("tags")
+            .path("schemaATag")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
responseA.getStatus());
+    Tag respTagA = responseA.readEntity(TagResponse.class).getTag();
+    Assertions.assertEquals("schemaATag", respTagA.name());
+    Assertions.assertTrue(respTagA.inherited().get());
+  }
+
   @Test
   public void testGetTagForObject() {
     TagEntity tag1 =

Reply via email to