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 =