This is an automated email from the ASF dual-hosted git repository.
roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new d0567913ee [#14060] feat(core): Batch policy resolution for metadata
objects (#14194)
d0567913ee is described below
commit d0567913eeb0c8dc608e5d159c46d187162ef3ad
Author: roryqi <[email protected]>
AuthorDate: Wed Oct 28 18:26:14 2026 +0800
[#14060] feat(core): Batch policy resolution for metadata objects (#14194)
### What changes were proposed in this pull request?
- Add a core API to resolve effective policies for multiple metadata
objects in one call, sharing ancestor tag reads and policy association
reads.
- Batch column ID and `TAG_METADATA_OBJECT_REL` lookups in the
relational store.
- Resolve an Iceberg table and its columns together when loading read
restrictions.
### Why are the changes needed?
Wide Iceberg tables currently repeat tag and policy lookups for every
column. This makes storage queries grow with the column count.
Fix: #14060. Depends on #13778.
### Does this PR introduce _any_ user-facing change?
Adds `PolicyDispatcher.listPolicyInfosForMetadataObjects` to the core
API. No REST API or configuration changes.
### How was this patch tested?
- Added unit tests for shared ancestor resolution, selector matching,
policy deduplication, and policy events.
- Extended the H2 policy manager test to cover batched table and column
reads.
- Updated the experimental Iceberg loader test to verify one batch call.
- Ran the targeted core and Iceberg tests and Spotless checks.
---
.../gravitino/hook/PolicyHookDispatcher.java | 8 ++
.../gravitino/listener/PolicyEventDispatcher.java | 34 +++++
.../gravitino/policy/ObjectPolicyResolver.java | 99 +++++++++++---
.../apache/gravitino/policy/PolicyDispatcher.java | 18 +++
.../org/apache/gravitino/policy/PolicyManager.java | 13 ++
.../gravitino/storage/relational/JDBCBackend.java | 2 +
.../relational/mapper/TableColumnMapper.java | 13 ++
.../mapper/TableColumnSQLProviderFactory.java | 6 +
.../mapper/TagMetadataObjectRelMapper.java | 15 ++
.../TagMetadataObjectRelSQLProviderFactory.java | 8 ++
.../provider/base/TableColumnBaseSQLProvider.java | 16 +++
.../base/TagMetadataObjectRelBaseSQLProvider.java | 21 +++
.../storage/relational/po/TagRelationTagPO.java | 38 ++++++
.../relational/service/TableColumnMetaService.java | 24 ++++
.../storage/relational/service/TagMetaService.java | 71 ++++++++++
.../apache/gravitino/tag/EffectiveTagResolver.java | 103 ++++++++++++++
.../apache/gravitino/utils/MetadataObjectUtil.java | 38 ++++++
.../listener/api/event/TestPolicyEvent.java | 34 +++++
.../gravitino/policy/TestObjectPolicyResolver.java | 126 +++++++++++++++++
.../apache/gravitino/policy/TestPolicyManager.java | 21 +++
.../service/TestTableColumnMetaService.java | 109 +++++++++++++++
.../relational/service/TestTagMetaService.java | 152 +++++++++++++++++++++
.../gravitino/tag/TestEffectiveTagResolver.java | 77 +++++++++++
.../gravitino/utils/TestMetadataObjectUtil.java | 104 ++++++++++++++
.../ExperimentalIcebergReadRestrictionLoader.java | 17 ++-
...stExperimentalIcebergReadRestrictionLoader.java | 12 +-
26 files changed, 1152 insertions(+), 27 deletions(-)
diff --git
a/core/src/main/java/org/apache/gravitino/hook/PolicyHookDispatcher.java
b/core/src/main/java/org/apache/gravitino/hook/PolicyHookDispatcher.java
index 1d08d80f53..d853b842cf 100644
--- a/core/src/main/java/org/apache/gravitino/hook/PolicyHookDispatcher.java
+++ b/core/src/main/java/org/apache/gravitino/hook/PolicyHookDispatcher.java
@@ -18,6 +18,8 @@
package org.apache.gravitino.hook;
import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
import org.apache.gravitino.Entity;
import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.MetadataObject;
@@ -123,4 +125,10 @@ public class PolicyHookDispatcher implements
PolicyDispatcher {
String metalake, MetadataObject metadataObject) {
return dispatcher.listPolicyInfosForMetadataObject(metalake,
metadataObject);
}
+
+ @Override
+ public Map<MetadataObject, PolicyEntity[]> listPolicyInfosForMetadataObjects(
+ String metalake, List<MetadataObject> metadataObjects) {
+ return dispatcher.listPolicyInfosForMetadataObjects(metalake,
metadataObjects);
+ }
}
diff --git
a/core/src/main/java/org/apache/gravitino/listener/PolicyEventDispatcher.java
b/core/src/main/java/org/apache/gravitino/listener/PolicyEventDispatcher.java
index 8869222622..f721e08105 100644
---
a/core/src/main/java/org/apache/gravitino/listener/PolicyEventDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/listener/PolicyEventDispatcher.java
@@ -18,6 +18,8 @@
*/
package org.apache.gravitino.listener;
+import java.util.List;
+import java.util.Map;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.RelationalEntity;
@@ -284,6 +286,38 @@ public class PolicyEventDispatcher implements
PolicyDispatcher {
}
}
+ @Override
+ public Map<MetadataObject, PolicyEntity[]> listPolicyInfosForMetadataObjects(
+ String metalake, List<MetadataObject> metadataObjects) {
+ metadataObjects.forEach(
+ object ->
+ eventBus.dispatchEvent(
+ new ListPolicyInfosForMetadataObjectPreEvent(
+ PrincipalUtils.getCurrentUserName(), metalake, object)));
+ try {
+ Map<MetadataObject, PolicyEntity[]> results =
+ dispatcher.listPolicyInfosForMetadataObjects(metalake,
metadataObjects);
+ metadataObjects.forEach(
+ object -> {
+ PolicyEntity[] policies = results.get(object);
+ eventBus.dispatchEvent(
+ new ListPolicyInfosForMetadataObjectEvent(
+ PrincipalUtils.getCurrentUserName(),
+ metalake,
+ object,
+ policies != null ? policies.length : -1));
+ });
+ return results;
+ } catch (Exception e) {
+ metadataObjects.forEach(
+ object ->
+ eventBus.dispatchEvent(
+ new ListPolicyInfosForMetadataObjectFailureEvent(
+ PrincipalUtils.getCurrentUserName(), metalake, object,
e)));
+ throw e;
+ }
+ }
+
private PolicyInfo toPolicyInfo(PolicyEntity policy) {
return new PolicyInfo(
policy.name(),
diff --git
a/core/src/main/java/org/apache/gravitino/policy/ObjectPolicyResolver.java
b/core/src/main/java/org/apache/gravitino/policy/ObjectPolicyResolver.java
index 1a244fd068..73b22b013a 100644
--- a/core/src/main/java/org/apache/gravitino/policy/ObjectPolicyResolver.java
+++ b/core/src/main/java/org/apache/gravitino/policy/ObjectPolicyResolver.java
@@ -80,12 +80,7 @@ public class ObjectPolicyResolver {
* @return Enabled policies selected by the object's effective tags.
*/
public PolicyEntity[] resolve(String metalake, MetadataObject
metadataObject) {
- Match[] matches = resolveMatches(metalake, metadataObject);
- PolicyEntity[] policies = new PolicyEntity[matches.length];
- for (int i = 0; i < matches.length; i++) {
- policies[i] = matches[i].policy();
- }
- return policies;
+ return policies(resolveMatches(metalake, metadataObject));
}
/**
@@ -104,18 +99,73 @@ public class ObjectPolicyResolver {
return new Match[0];
}
- Map<String, ResolvedTag> tagsByName =
- Arrays.stream(effectiveTags)
- .collect(
- Collectors.toMap(
- resolvedTag -> resolvedTag.tag().name(),
- resolvedTag -> resolvedTag,
- (left, right) -> left,
- LinkedHashMap::new));
List<NameIdentifier> tagIdentifiers =
- tagsByName.keySet().stream()
+ Arrays.stream(effectiveTags)
+ .map(resolvedTag -> resolvedTag.tag().name())
+ .distinct()
.map(tagName -> NameIdentifierUtil.ofTag(metalake, tagName))
.collect(Collectors.toList());
+ return selectMatches(effectiveTags, loadRelations(tagIdentifiers,
metadataObject));
+ }
+
+ /**
+ * Resolves effective policies for multiple objects using shared tag and
policy reads.
+ *
+ * @param metalake the metalake name
+ * @param metadataObjects the objects to resolve
+ * @return policies keyed by requested object
+ */
+ public Map<MetadataObject, PolicyEntity[]> resolve(
+ String metalake, List<MetadataObject> metadataObjects) {
+ if (metadataObjects.isEmpty()) {
+ return new LinkedHashMap<>();
+ }
+ Map<MetadataObject, ResolvedTag[]> tagsByObject =
+ effectiveTagResolver.resolveWithSources(metalake, metadataObjects);
+ Map<String, NameIdentifier> tagIdentifiers = new LinkedHashMap<>();
+ tagsByObject
+ .values()
+ .forEach(
+ tags -> {
+ for (ResolvedTag resolvedTag : tags) {
+ TagEntity tag = resolvedTag.tag();
+ tagIdentifiers.putIfAbsent(
+ tag.name(), NameIdentifierUtil.ofTag(metalake,
tag.name()));
+ }
+ });
+ List<RelationalEntity<?>> relations =
+ tagIdentifiers.isEmpty()
+ ? Collections.emptyList()
+ : loadRelations(new ArrayList<>(tagIdentifiers.values()),
metadataObjects);
+ Map<String, List<Integer>> relationPositionsByTag = new LinkedHashMap<>();
+ for (int i = 0; i < relations.size(); i++) {
+ relationPositionsByTag
+ .computeIfAbsent(relations.get(i).source().name(), ignored -> new
ArrayList<>())
+ .add(i);
+ }
+ Map<MetadataObject, PolicyEntity[]> result = new LinkedHashMap<>();
+ tagsByObject.forEach(
+ (object, tags) -> {
+ // Retain relation order while visiting only relations for this
object's effective tags.
+ List<RelationalEntity<?>> objectRelations =
+ Arrays.stream(tags)
+ .map(resolvedTag -> resolvedTag.tag().name())
+ .distinct()
+ .flatMap(
+ name ->
+ relationPositionsByTag
+ .getOrDefault(name, Collections.emptyList())
+ .stream())
+ .sorted()
+ .map(relations::get)
+ .collect(Collectors.toList());
+ result.put(object, policies(selectMatches(tags, objectRelations)));
+ });
+ return result;
+ }
+
+ private List<RelationalEntity<?>> loadRelations(
+ List<NameIdentifier> tagIdentifiers, Object request) {
List<RelationalEntity<?>> relations;
try {
@@ -127,9 +177,26 @@ public class ObjectPolicyResolver {
tagIdentifiers,
Entity.EntityType.TAG);
} catch (IOException e) {
- LOG.error("Failed to resolve policies for metadata object {}",
metadataObject, e);
+ LOG.error("Failed to resolve policies for metadata objects {}", request,
e);
throw new RuntimeException(e);
}
+ return relations;
+ }
+
+ private static PolicyEntity[] policies(Match[] matches) {
+ return
Arrays.stream(matches).map(Match::policy).toArray(PolicyEntity[]::new);
+ }
+
+ private static Match[] selectMatches(
+ ResolvedTag[] effectiveTags, List<RelationalEntity<?>> relations) {
+ Map<String, ResolvedTag> tagsByName =
+ Arrays.stream(effectiveTags)
+ .collect(
+ Collectors.toMap(
+ resolvedTag -> resolvedTag.tag().name(),
+ resolvedTag -> resolvedTag,
+ (left, right) -> left,
+ LinkedHashMap::new));
Map<Long, MatchState> matchStates = new LinkedHashMap<>();
for (RelationalEntity<?> relation : relations) {
diff --git
a/core/src/main/java/org/apache/gravitino/policy/PolicyDispatcher.java
b/core/src/main/java/org/apache/gravitino/policy/PolicyDispatcher.java
index 0f1ee9203a..199af5ed45 100644
--- a/core/src/main/java/org/apache/gravitino/policy/PolicyDispatcher.java
+++ b/core/src/main/java/org/apache/gravitino/policy/PolicyDispatcher.java
@@ -20,6 +20,9 @@
package org.apache.gravitino.policy;
import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.RelationalEntity;
import org.apache.gravitino.annotation.Evolving;
@@ -163,4 +166,19 @@ public interface PolicyDispatcher {
* @return The array of policies associated with the specified metadata
object.
*/
PolicyEntity[] listPolicyInfosForMetadataObject(String metalake,
MetadataObject metadataObject);
+
+ /**
+ * Lists effective policies for several metadata objects in the same
metalake.
+ *
+ * @param metalake the metalake name
+ * @param metadataObjects the objects to resolve
+ * @return policies keyed by metadata object, including empty results
+ */
+ default Map<MetadataObject, PolicyEntity[]>
listPolicyInfosForMetadataObjects(
+ String metalake, List<MetadataObject> metadataObjects) {
+ Map<MetadataObject, PolicyEntity[]> result = new LinkedHashMap<>();
+ metadataObjects.forEach(
+ object -> result.put(object,
listPolicyInfosForMetadataObject(metalake, object)));
+ return result;
+ }
}
diff --git a/core/src/main/java/org/apache/gravitino/policy/PolicyManager.java
b/core/src/main/java/org/apache/gravitino/policy/PolicyManager.java
index 52d8d91c5c..55e623c9d7 100644
--- a/core/src/main/java/org/apache/gravitino/policy/PolicyManager.java
+++ b/core/src/main/java/org/apache/gravitino/policy/PolicyManager.java
@@ -27,6 +27,9 @@ import java.io.IOException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
import org.apache.gravitino.Entity;
import org.apache.gravitino.EntityAlreadyExistsException;
import org.apache.gravitino.EntityStore;
@@ -233,6 +236,16 @@ public class PolicyManager implements PolicyDispatcher {
return objectPolicyResolver.resolve(metalake, metadataObject);
}
+ @Override
+ public Map<MetadataObject, PolicyEntity[]> listPolicyInfosForMetadataObjects(
+ String metalake, List<MetadataObject> metadataObjects) {
+ if (metadataObjects.isEmpty()) {
+ return new LinkedHashMap<>();
+ }
+ checkMetalake(NameIdentifier.of(metalake), entityStore);
+ return objectPolicyResolver.resolve(metalake, metadataObjects);
+ }
+
private PolicyEntity loadPolicyEntity(String metalake, String policyName) {
try {
return entityStore.get(
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
b/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
index 83b6c2409f..e25d804426 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
@@ -730,6 +730,8 @@ public class JDBCBackend implements RelationalBackend,
SupportsOrphanedRelationC
return OwnerMetaService.getInstance().batchGetOwner(nameIdentifiers,
identType);
case POLICY_TAG_REL:
return
PolicyTagRelService.getInstance().listRelations(nameIdentifiers, identType);
+ case TAG_METADATA_OBJECT_REL:
+ return
TagMetaService.getInstance().listTagsForMetadataObjects(nameIdentifiers,
identType);
default:
throw new IllegalArgumentException(
String.format("Doesn't support the relation type %s", relType));
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TableColumnMapper.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TableColumnMapper.java
index 906ad01c8a..3c4824c50c 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TableColumnMapper.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TableColumnMapper.java
@@ -73,6 +73,19 @@ public interface TableColumnMapper {
Long selectColumnIdByTableIdAndName(
@Param("tableId") Long tableId, @Param("columnName") String name);
+ /**
+ * Selects live column IDs for a table and a set of names.
+ *
+ * @param tableId the table ID
+ * @param columnNames the column names
+ * @return the matching column rows
+ */
+ @SelectProvider(
+ type = TableColumnSQLProviderFactory.class,
+ method = "selectColumnIdsByTableIdAndNames")
+ List<ColumnPO> selectColumnIdsByTableIdAndNames(
+ @Param("tableId") Long tableId, @Param("columnNames") List<String>
columnNames);
+
@SelectProvider(type = TableColumnSQLProviderFactory.class, method =
"selectColumnPOById")
ColumnPO selectColumnPOById(@Param("columnId") Long columnId);
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TableColumnSQLProviderFactory.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TableColumnSQLProviderFactory.java
index c00fe1554c..ed3fb3bf1a 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TableColumnSQLProviderFactory.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TableColumnSQLProviderFactory.java
@@ -92,6 +92,12 @@ public class TableColumnSQLProviderFactory {
return getProvider().selectColumnIdByTableIdAndName(tableId, name);
}
+ /** Returns the SQL for a batched active-column lookup. */
+ public static String selectColumnIdsByTableIdAndNames(
+ @Param("tableId") Long tableId, @Param("columnNames") List<String>
columnNames) {
+ return getProvider().selectColumnIdsByTableIdAndNames(tableId,
columnNames);
+ }
+
public static String selectColumnPOById(@Param("columnId") Long columnId) {
return getProvider().selectColumnPOById(columnId);
}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TagMetadataObjectRelMapper.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TagMetadataObjectRelMapper.java
index d375184121..50b4f68117 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TagMetadataObjectRelMapper.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TagMetadataObjectRelMapper.java
@@ -21,6 +21,7 @@ package org.apache.gravitino.storage.relational.mapper;
import java.util.List;
import org.apache.gravitino.storage.relational.po.TagMetadataObjectRelPO;
import org.apache.gravitino.storage.relational.po.TagPO;
+import org.apache.gravitino.storage.relational.po.TagRelationTagPO;
import org.apache.ibatis.annotations.DeleteProvider;
import org.apache.ibatis.annotations.InsertProvider;
import org.apache.ibatis.annotations.Param;
@@ -37,6 +38,20 @@ public interface TagMetadataObjectRelMapper {
@Param("metadataObjectId") Long metadataObjectId,
@Param("metadataObjectType") String metadataObjectType);
+ /**
+ * Selects tag assignment rows for multiple objects of one type.
+ *
+ * @param metadataObjectIds the source object IDs
+ * @param metadataObjectType the source object type
+ * @return the tag rows with source IDs
+ */
+ @SelectProvider(
+ type = TagMetadataObjectRelSQLProviderFactory.class,
+ method = "listTagPOsByMetadataObjectIdsAndType")
+ List<TagRelationTagPO> listTagPOsByMetadataObjectIdsAndType(
+ @Param("metadataObjectIds") List<Long> metadataObjectIds,
+ @Param("metadataObjectType") String metadataObjectType);
+
@SelectProvider(
type = TagMetadataObjectRelSQLProviderFactory.class,
method = "getTagPOsByMetadataObjectAndTagName")
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TagMetadataObjectRelSQLProviderFactory.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TagMetadataObjectRelSQLProviderFactory.java
index db6f12e938..d65bb86d66 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TagMetadataObjectRelSQLProviderFactory.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/TagMetadataObjectRelSQLProviderFactory.java
@@ -58,6 +58,14 @@ public class TagMetadataObjectRelSQLProviderFactory {
return getProvider().listTagPOsByMetadataObjectIdAndType(metadataObjectId,
metadataObjectType);
}
+ /** Returns the SQL for a batched tag assignment lookup. */
+ public static String listTagPOsByMetadataObjectIdsAndType(
+ @Param("metadataObjectIds") List<Long> metadataObjectIds,
+ @Param("metadataObjectType") String metadataObjectType) {
+ return getProvider()
+ .listTagPOsByMetadataObjectIdsAndType(metadataObjectIds,
metadataObjectType);
+ }
+
public static String getTagPOsByMetadataObjectAndTagName(
@Param("metadataObjectId") Long metadataObjectId,
@Param("metadataObjectType") String metadataObjectType,
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/TableColumnBaseSQLProvider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/TableColumnBaseSQLProvider.java
index fa6e2fdd83..d1123f3081 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/TableColumnBaseSQLProvider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/TableColumnBaseSQLProvider.java
@@ -141,6 +141,22 @@ public class TableColumnBaseSQLProvider {
+ " ORDER BY c.table_version DESC, c.id DESC LIMIT 1";
}
+ /** Returns the SQL for a batched active-column lookup. */
+ public String selectColumnIdsByTableIdAndNames(Long tableId, List<String>
columnNames) {
+ return "<script>SELECT c.column_id AS columnId, c.column_name AS
columnName FROM "
+ + TableColumnMapper.COLUMN_TABLE_NAME
+ + " c JOIN (SELECT column_id, MAX(table_version) AS max_version FROM "
+ + TableColumnMapper.COLUMN_TABLE_NAME
+ + " WHERE table_id = #{tableId} AND deleted_at = 0 GROUP BY column_id)
latest"
+ + " ON c.column_id = latest.column_id AND c.table_version =
latest.max_version"
+ + " WHERE c.table_id = #{tableId} AND c.column_name IN "
+ + "<foreach collection='columnNames' item='name' open='('
separator=',' close=')'>"
+ + "#{name}</foreach>"
+ + " AND c.deleted_at = 0 AND c.column_op_type <> "
+ + ColumnPO.ColumnOpType.DELETE.value()
+ + "</script>";
+ }
+
public String selectColumnPOById(@Param("columnId") Long columnId) {
return "SELECT column_id AS columnId, column_name AS columnName,"
+ " column_position AS columnPosition, metalake_id AS metalakeId,
catalog_id AS catalogId,"
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/TagMetadataObjectRelBaseSQLProvider.java
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/TagMetadataObjectRelBaseSQLProvider.java
index ea7d11a56c..c1893694d7 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/TagMetadataObjectRelBaseSQLProvider.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/TagMetadataObjectRelBaseSQLProvider.java
@@ -58,6 +58,27 @@ public class TagMetadataObjectRelBaseSQLProvider {
+ " AND tm.deleted_at = 0";
}
+ /** Returns the SQL for a batched tag assignment lookup. */
+ public String listTagPOsByMetadataObjectIdsAndType(
+ List<Long> metadataObjectIds, String metadataObjectType) {
+ return "<script>SELECT te.metadata_object_id as metadataObjectId,"
+ + " tm.tag_id as tagId, tm.tag_name as tagName,"
+ + " tm.metalake_id as metalakeId, tm.tag_comment as comment,
tm.properties as properties,"
+ + " tm.allowed_values as allowedValues, NULLIF(te.tag_value, '') as
assignmentValue,"
+ + " tm.audit_info as auditInfo, tm.current_version as currentVersion,"
+ + " tm.last_version as lastVersion, tm.deleted_at as deletedAt"
+ + " FROM "
+ + TagMetaMapper.TAG_TABLE_NAME
+ + " tm JOIN "
+ + TagMetadataObjectRelMapper.TAG_METADATA_OBJECT_RELATION_TABLE_NAME
+ + " te ON tm.tag_id = te.tag_id"
+ + " WHERE te.metadata_object_id IN "
+ + "<foreach collection='metadataObjectIds' item='id' open='('
separator=',' close=')'>"
+ + "#{id}</foreach>"
+ + " AND te.metadata_object_type = #{metadataObjectType}"
+ + " AND te.deleted_at = 0 AND tm.deleted_at = 0</script>";
+ }
+
public String getTagPOsByMetadataObjectAndTagName(
@Param("metadataObjectId") Long metadataObjectId,
@Param("metadataObjectType") String metadataObjectType,
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/po/TagRelationTagPO.java
b/core/src/main/java/org/apache/gravitino/storage/relational/po/TagRelationTagPO.java
new file mode 100644
index 0000000000..b96cf77f20
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/po/TagRelationTagPO.java
@@ -0,0 +1,38 @@
+/*
+ * 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.storage.relational.po;
+
+/** A tag assignment row with the source metadata object ID. */
+public class TagRelationTagPO extends TagPO {
+ private Long metadataObjectId;
+
+ /**
+ * @return the source metadata object ID
+ */
+ public Long getMetadataObjectId() {
+ return metadataObjectId;
+ }
+
+ /**
+ * @param metadataObjectId the source metadata object ID
+ */
+ public void setMetadataObjectId(Long metadataObjectId) {
+ this.metadataObjectId = metadataObjectId;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/service/TableColumnMetaService.java
b/core/src/main/java/org/apache/gravitino/storage/relational/service/TableColumnMetaService.java
index fd64a42b80..467f537d0a 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/service/TableColumnMetaService.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/service/TableColumnMetaService.java
@@ -91,6 +91,30 @@ public class TableColumnMetaService {
return columnId;
}
+ /** Returns active column IDs by name using batched reads of one table. */
+ public Map<String, Long> getColumnIdsByTableIdAndNames(Long tableId,
List<String> columnNames) {
+ if (columnNames.isEmpty()) {
+ return new HashMap<>();
+ }
+ Map<String, Long> ids = new HashMap<>();
+ for (List<String> batch : Lists.partition(columnNames, 500)) {
+ List<ColumnPO> rows =
+ SessionUtils.getWithoutCommit(
+ TableColumnMapper.class,
+ mapper -> mapper.selectColumnIdsByTableIdAndNames(tableId,
batch));
+ rows.forEach(row -> ids.put(row.getColumnName(), row.getColumnId()));
+ }
+ for (String name : columnNames) {
+ if (!ids.containsKey(name)) {
+ throw new NoSuchEntityException(
+ NoSuchEntityException.NO_SUCH_ENTITY_MESSAGE,
+ Entity.EntityType.COLUMN.name().toLowerCase(Locale.ROOT),
+ name);
+ }
+ }
+ return ids;
+ }
+
@Monitored(
metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
baseMetricName = "getColumnPOById")
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/service/TagMetaService.java
b/core/src/main/java/org/apache/gravitino/storage/relational/service/TagMetaService.java
index fdfa3c4f20..1c84f7027e 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/service/TagMetaService.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/service/TagMetaService.java
@@ -46,6 +46,8 @@ import org.apache.gravitino.HasIdentifier;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
+import org.apache.gravitino.RelationalEntity;
+import org.apache.gravitino.SupportsRelationOperations;
import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.exceptions.NoSuchTagException;
import org.apache.gravitino.json.JsonUtils;
@@ -62,6 +64,7 @@ import
org.apache.gravitino.storage.relational.mapper.TagMetadataObjectRelMapper
import org.apache.gravitino.storage.relational.po.MetalakePO;
import org.apache.gravitino.storage.relational.po.TagMetadataObjectRelPO;
import org.apache.gravitino.storage.relational.po.TagPO;
+import org.apache.gravitino.storage.relational.po.TagRelationTagPO;
import org.apache.gravitino.storage.relational.utils.ExceptionUtils;
import org.apache.gravitino.storage.relational.utils.POConverters;
import org.apache.gravitino.storage.relational.utils.SessionUtils;
@@ -213,6 +216,74 @@ public class TagMetaService {
return tagPOsToTagEntities(tagPOs, NamespaceUtil.ofTag(metalake));
}
+ /**
+ * Lists direct tag assignments for metadata objects of one type in one
metalake.
+ *
+ * @param objectIdentifiers source identifiers
+ * @param objectType source entity type
+ * @return tag relation edges with their source identifiers
+ */
+ public List<RelationalEntity<?>> listTagsForMetadataObjects(
+ List<NameIdentifier> objectIdentifiers, Entity.EntityType objectType) {
+ if (objectIdentifiers.isEmpty()) {
+ return new ArrayList<>();
+ }
+ String metalake = objectIdentifiers.get(0).namespace().level(0);
+ Map<Long, NameIdentifier> identifiersById = new LinkedHashMap<>();
+ Map<NameIdentifier, List<NameIdentifier>> columnsByTable = new
LinkedHashMap<>();
+ for (NameIdentifier identifier : objectIdentifiers) {
+ Preconditions.checkArgument(
+ metalake.equals(identifier.namespace().level(0)),
+ "All metadata objects must belong to the same metalake");
+ if (objectType == Entity.EntityType.COLUMN) {
+ columnsByTable
+ .computeIfAbsent(
+ NameIdentifier.of(identifier.namespace().levels()), ignored ->
new ArrayList<>())
+ .add(identifier);
+ } else {
+ identifiersById.put(EntityIdService.getEntityId(identifier,
objectType), identifier);
+ }
+ }
+ columnsByTable.forEach(
+ (table, columns) -> {
+ Long tableId = EntityIdService.getEntityId(table,
Entity.EntityType.TABLE);
+ Map<String, Long> ids =
+ TableColumnMetaService.getInstance()
+ .getColumnIdsByTableIdAndNames(
+ tableId,
+
columns.stream().map(NameIdentifier::name).collect(Collectors.toList()));
+ columns.forEach(column ->
identifiersById.put(ids.get(column.name()), column));
+ });
+ List<TagRelationTagPO> rows = new ArrayList<>();
+ String metadataObjectType =
+ NameIdentifierUtil.toMetadataObject(objectIdentifiers.get(0),
objectType).type().toString();
+ for (List<Long> batch : Lists.partition(new
ArrayList<>(identifiersById.keySet()), 500)) {
+ rows.addAll(
+ SessionUtils.getWithoutCommit(
+ TagMetadataObjectRelMapper.class,
+ mapper -> mapper.listTagPOsByMetadataObjectIdsAndType(batch,
metadataObjectType)));
+ }
+ Map<Long, List<TagPO>> rowsByObject = new LinkedHashMap<>();
+ rows.forEach(
+ row ->
+ rowsByObject
+ .computeIfAbsent(row.getMetadataObjectId(), ignored -> new
ArrayList<>())
+ .add(row));
+ List<RelationalEntity<?>> result = new ArrayList<>();
+ rowsByObject.forEach(
+ (id, tagRows) ->
+ tagPOsToTagEntities(tagRows, NamespaceUtil.ofTag(metalake))
+ .forEach(
+ tag ->
+ result.add(
+ new RelationalEntity<>(
+
SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+ identifiersById.get(id),
+ objectType,
+ tag))));
+ return result;
+ }
+
@Monitored(
metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
baseMetricName = "getTagForMetadataObject")
diff --git
a/core/src/main/java/org/apache/gravitino/tag/EffectiveTagResolver.java
b/core/src/main/java/org/apache/gravitino/tag/EffectiveTagResolver.java
index 65b4dbe0db..83b5f4ca9d 100644
--- a/core/src/main/java/org/apache/gravitino/tag/EffectiveTagResolver.java
+++ b/core/src/main/java/org/apache/gravitino/tag/EffectiveTagResolver.java
@@ -20,14 +20,20 @@ package org.apache.gravitino.tag;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumMap;
import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import org.apache.gravitino.Entity;
import org.apache.gravitino.EntityStore;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.RelationalEntity;
import org.apache.gravitino.SupportsRelationOperations;
import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.exceptions.NoSuchMetadataObjectException;
@@ -177,4 +183,101 @@ public class EffectiveTagResolver {
return "ResolvedTag{tag=" + tag + ", source=" + source + '}';
}
}
+
+ /**
+ * Resolves effective tags for multiple objects, reading each distinct
ancestor only once.
+ *
+ * @param metalake the metalake name
+ * @param metadataObjects the objects to resolve
+ * @return effective tags keyed by requested object
+ */
+ public Map<MetadataObject, TagEntity[]> resolve(
+ String metalake, List<MetadataObject> metadataObjects) {
+ Map<MetadataObject, TagEntity[]> result = new LinkedHashMap<>();
+ resolveWithSources(metalake, metadataObjects)
+ .forEach(
+ (object, tags) ->
+ result.put(
+ object,
Arrays.stream(tags).map(ResolvedTag::tag).toArray(TagEntity[]::new)));
+ return result;
+ }
+
+ /**
+ * Resolves effective tags with their sources, reading each distinct
ancestor only once.
+ *
+ * @param metalake the metalake name
+ * @param metadataObjects the objects to resolve
+ * @return effective tags with sources keyed by requested object
+ */
+ public Map<MetadataObject, ResolvedTag[]> resolveWithSources(
+ String metalake, List<MetadataObject> metadataObjects) {
+ if (metadataObjects.isEmpty()) {
+ return new LinkedHashMap<>();
+ }
+ MetadataObjectUtil.checkMetadataObjects(metalake, metadataObjects);
+ Map<MetadataObject, List<MetadataObject>> resolutionOrders = new
LinkedHashMap<>();
+ Map<Entity.EntityType, Set<MetadataObject>> objectsByType =
+ new EnumMap<>(Entity.EntityType.class);
+ for (MetadataObject requested : metadataObjects) {
+ List<MetadataObject> order = new ArrayList<>();
+ order.add(requested);
+ order.addAll(MetadataObjectUtil.getParentMetadataObjects(requested));
+ resolutionOrders.put(requested, order);
+ for (MetadataObject object : order) {
+ objectsByType
+ .computeIfAbsent(
+ MetadataObjectUtil.toEntityType(object), ignored -> new
LinkedHashSet<>())
+ .add(object);
+ }
+ }
+
+ Map<MetadataObject, List<TagEntity>> directTags = new LinkedHashMap<>();
+ for (Map.Entry<Entity.EntityType, Set<MetadataObject>> entry :
objectsByType.entrySet()) {
+ List<NameIdentifier> identifiers = new ArrayList<>();
+ Map<NameIdentifier, MetadataObject> objectsByIdentifier = new
LinkedHashMap<>();
+ for (MetadataObject object : entry.getValue()) {
+ NameIdentifier identifier = MetadataObjectUtil.toEntityIdent(metalake,
object);
+ identifiers.add(identifier);
+ objectsByIdentifier.put(identifier, object);
+ }
+ try {
+ for (RelationalEntity<?> relation :
+ entityStore
+ .relationOperations()
+ .batchListEntitiesByRelation(
+ SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+ identifiers,
+ entry.getKey())) {
+ directTags
+ .computeIfAbsent(
+ objectsByIdentifier.get(relation.source()), ignored -> new
ArrayList<>())
+ .add((TagEntity) relation.targetEntity());
+ }
+ } catch (NoSuchEntityException e) {
+ throw new NoSuchMetadataObjectException(
+ e, "Failed to resolve effective tags for metadata objects due to
not found");
+ } catch (IOException e) {
+ LOG.error("Failed to resolve effective tags for metadata objects", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ Map<MetadataObject, ResolvedTag[]> result = new LinkedHashMap<>();
+ resolutionOrders.forEach(
+ (requested, order) -> {
+ Map<String, ResolvedTag> effectiveTags = new LinkedHashMap<>();
+ for (int index = 0; index < order.size(); index++) {
+ boolean inherited = index > 0;
+ MetadataObject source = order.get(index);
+ directTags
+ .getOrDefault(source, Collections.emptyList())
+ .forEach(
+ tag ->
+ effectiveTags.putIfAbsent(
+ tag.name(), new
ResolvedTag(tag.copyWithInherited(inherited), source)));
+ }
+ result.put(requested, effectiveTags.values().toArray(new
ResolvedTag[0]));
+ });
+ return result;
+ }
}
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 47f056498e..70af3cf295 100644
--- a/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java
+++ b/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java
@@ -25,8 +25,10 @@ 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.HashSet;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
import java.util.function.Supplier;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Entity;
@@ -342,6 +344,42 @@ public class MetadataObjectUtil {
}
}
+ /**
+ * Checks metadata objects, checking each distinct table only once for
tables and their columns.
+ *
+ * <p>Every object's identifier is validated. Successful table checks are
shared only within this
+ * call. Like {@link #checkMetadataObject(String, MetadataObject)}, this
must be called outside
+ * the tree lock because checking an object may load it into the entity
store.
+ *
+ * @param metalake The metalake name.
+ * @param objects The metadata objects to check, in order.
+ * @throws NoSuchMetadataObjectException if a metadata object does not exist.
+ */
+ public static void checkMetadataObjects(String metalake,
List<MetadataObject> objects) {
+ Set<NameIdentifier> checkedTables = new HashSet<>();
+ for (MetadataObject object : objects) {
+ Preconditions.checkArgument(object != null, "metadataObject cannot be
null");
+ if (object.type() != MetadataObject.Type.TABLE
+ && object.type() != MetadataObject.Type.COLUMN) {
+ checkMetadataObject(metalake, object);
+ continue;
+ }
+
+ NameIdentifier identifier = toEntityIdent(metalake, object);
+ NameIdentifier tableIdentifier;
+ if (object.type() == MetadataObject.Type.COLUMN) {
+ NameIdentifierUtil.checkColumn(identifier);
+ tableIdentifier = NameIdentifier.of(identifier.namespace().levels());
+ } else {
+ NameIdentifierUtil.checkTable(identifier);
+ tableIdentifier = identifier;
+ }
+ if (checkedTables.add(tableIdentifier)) {
+ checkMetadataObject(metalake, object);
+ }
+ }
+ }
+
private static void check(
final boolean expression, Supplier<? extends RuntimeException>
exceptionToThrowSupplier) {
if (!expression) {
diff --git
a/core/src/test/java/org/apache/gravitino/listener/api/event/TestPolicyEvent.java
b/core/src/test/java/org/apache/gravitino/listener/api/event/TestPolicyEvent.java
index 49c9b05fe0..efc9248351 100644
---
a/core/src/test/java/org/apache/gravitino/listener/api/event/TestPolicyEvent.java
+++
b/core/src/test/java/org/apache/gravitino/listener/api/event/TestPolicyEvent.java
@@ -26,6 +26,8 @@ import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
import java.util.Objects;
import org.apache.gravitino.Entity;
import org.apache.gravitino.MetadataObject;
@@ -307,6 +309,38 @@ public class TestPolicyEvent {
Assertions.assertEquals(2, ((ListPolicyInfosForMetadataObjectEvent)
postevent).resultCount());
}
+ @Test
+ void testBatchListPolicyInfosEmitsEventsForEachObject() {
+ MetadataObject catalog =
+ NameIdentifierUtil.toMetadataObject(
+ NameIdentifierUtil.ofCatalog("metalake", "catalog_for_test"),
+ Entity.EntityType.CATALOG);
+ MetadataObject schema =
+ NameIdentifierUtil.toMetadataObject(
+ NameIdentifierUtil.ofSchema("metalake", "catalog_for_test",
"schema_for_test"),
+ Entity.EntityType.SCHEMA);
+ PolicyDispatcher delegate = mock(PolicyDispatcher.class);
+ when(delegate.listPolicyInfosForMetadataObjects("metalake",
List.of(catalog, schema)))
+ .thenReturn(Map.of(catalog, new PolicyEntity[] {policy}, schema, new
PolicyEntity[0]));
+ DummyEventListener listener = new DummyEventListener();
+ PolicyEventDispatcher batchDispatcher =
+ new PolicyEventDispatcher(new EventBus(List.of(listener)), delegate);
+
+ Map<MetadataObject, PolicyEntity[]> result =
+ batchDispatcher.listPolicyInfosForMetadataObjects("metalake",
List.of(catalog, schema));
+
+ Assertions.assertEquals(1, result.get(catalog).length);
+ Assertions.assertEquals(0, result.get(schema).length);
+ Assertions.assertEquals(
+ ListPolicyInfosForMetadataObjectPreEvent.class,
listener.popPreEvent().getClass());
+ Assertions.assertEquals(
+ ListPolicyInfosForMetadataObjectPreEvent.class,
listener.popPreEvent().getClass());
+ Assertions.assertEquals(
+ 0, ((ListPolicyInfosForMetadataObjectEvent)
listener.popPostEvent()).resultCount());
+ Assertions.assertEquals(
+ 1, ((ListPolicyInfosForMetadataObjectEvent)
listener.popPostEvent()).resultCount());
+ }
+
@Test
@SuppressWarnings("deprecation")
void
testListPolicyInfosForMetadataObjectEventDeprecatedConstructorReturnsNegativeCount()
{
diff --git
a/core/src/test/java/org/apache/gravitino/policy/TestObjectPolicyResolver.java
b/core/src/test/java/org/apache/gravitino/policy/TestObjectPolicyResolver.java
index a787ca0532..2520d7e40d 100644
---
a/core/src/test/java/org/apache/gravitino/policy/TestObjectPolicyResolver.java
+++
b/core/src/test/java/org/apache/gravitino/policy/TestObjectPolicyResolver.java
@@ -19,18 +19,25 @@
package org.apache.gravitino.policy;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.time.Instant;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import org.apache.gravitino.Entity;
import org.apache.gravitino.EntityStore;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.RelationalEntity;
import org.apache.gravitino.SupportsRelationOperations;
import org.apache.gravitino.json.PolicyAssociationSelectorSerde;
@@ -189,6 +196,125 @@ public class TestObjectPolicyResolver {
Assertions.assertEquals(0, resolver.resolveMatches(METALAKE,
OBJECT).length);
}
+ @Test
+ public void testBatchSharesPolicyReadAndPreservesSelectors() throws
Exception {
+ MetadataObject column =
+ MetadataObjects.of(
+ Arrays.asList("catalog", "schema", "table", "column"),
MetadataObject.Type.COLUMN);
+ TagEntity tableTag = tag(1L, "domain", TagAssignment.ofValues("finance"));
+ TagEntity columnTag = tag(1L, "domain",
TagAssignment.ofValues("engineering"));
+ PolicyEntity policy = policy(10L, "selected", true);
+ Map<MetadataObject, ResolvedTag[]> tags = new LinkedHashMap<>();
+ tags.put(
+ OBJECT, new ResolvedTag[] {new
ResolvedTag(tableTag.copyWithInherited(false), OBJECT)});
+ tags.put(
+ column, new ResolvedTag[] {new
ResolvedTag(columnTag.copyWithInherited(true), CATALOG)});
+ when(effectiveTagResolver.resolveWithSources(METALAKE, List.of(OBJECT,
column)))
+ .thenReturn(tags);
+ when(relationOperations.batchListEntitiesByRelation(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ List.of(NameIdentifierUtil.ofTag(METALAKE, "domain")),
+ Entity.EntityType.TAG))
+ .thenReturn(List.of(relation(tableTag, policy,
TagValueSelector.of("finance"))));
+
+ Map<MetadataObject, PolicyEntity[]> result =
+ resolver.resolve(METALAKE, List.of(OBJECT, column));
+
+ Assertions.assertEquals(1, result.get(OBJECT).length);
+ Assertions.assertFalse(result.get(OBJECT)[0].inherited().orElseThrow());
+ Assertions.assertEquals(0, result.get(column).length);
+ verify(relationOperations, times(1))
+ .batchListEntitiesByRelation(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ List.of(NameIdentifierUtil.ofTag(METALAKE, "domain")),
+ Entity.EntityType.TAG);
+ }
+
+ /** Verifies distinct column tags do not cause every column to scan all
policy relations. */
+ @Test
+ public void testBatchVisitsOnlyRelationsForEffectiveTags() throws Exception {
+ List<MetadataObject> objects = new ArrayList<>();
+ Map<MetadataObject, ResolvedTag[]> tags = new LinkedHashMap<>();
+ List<NameIdentifier> tagIdentifiers = new ArrayList<>();
+ List<RelationalEntity<?>> relations = new ArrayList<>();
+ List<PolicyEntity> expectedPolicies = new ArrayList<>();
+ for (int i = 0; i < 1000; i++) {
+ MetadataObject column =
+ MetadataObjects.of(
+ List.of("catalog", "schema", "table", "column_" + i),
MetadataObject.Type.COLUMN);
+ TagEntity tag = tag(i, "tag_" + i,
TagAssignment.noValue()).copyWithInherited(false);
+ PolicyEntity policy = policy(1000L + i, "policy_" + i, true);
+ objects.add(column);
+ tags.put(column, new ResolvedTag[] {new ResolvedTag(tag, column)});
+ tagIdentifiers.add(tag.nameIdentifier());
+ relations.add(spy(relation(tag, policy, AllValuesSelector.get())));
+ expectedPolicies.add(policy);
+ }
+ MetadataObject untagged =
+ MetadataObjects.of(
+ List.of("catalog", "schema", "table", "untagged"),
MetadataObject.Type.COLUMN);
+ objects.add(untagged);
+ tags.put(untagged, new ResolvedTag[0]);
+ when(effectiveTagResolver.resolveWithSources(METALAKE,
objects)).thenReturn(tags);
+ when(relationOperations.batchListEntitiesByRelation(
+ SupportsRelationOperations.Type.POLICY_TAG_REL, tagIdentifiers,
Entity.EntityType.TAG))
+ .thenReturn(relations);
+
+ Map<MetadataObject, PolicyEntity[]> result = resolver.resolve(METALAKE,
objects);
+
+ Assertions.assertEquals(objects.size(), result.size());
+ for (int i = 0; i < expectedPolicies.size(); i++) {
+ Assertions.assertArrayEquals(
+ new PolicyEntity[] {expectedPolicies.get(i)},
result.get(objects.get(i)));
+ // One visit to build the index and one visit for the column that owns
this tag.
+ verify(relations.get(i), times(2)).source();
+ }
+ Assertions.assertEquals(0, result.get(untagged).length);
+ verify(relationOperations, times(1))
+ .batchListEntitiesByRelation(
+ SupportsRelationOperations.Type.POLICY_TAG_REL, tagIdentifiers,
Entity.EntityType.TAG);
+ }
+
+ /**
+ * Verifies indexed matching retains relation order, deduplication and
direct-match precedence.
+ */
+ @Test
+ public void testBatchIndexPreservesPolicyOrderAndInheritance() throws
Exception {
+ TagEntity direct = tag(1L, "direct", TagAssignment.ofValues("finance"));
+ TagEntity inherited = tag(2L, "inherited",
TagAssignment.noValue()).copyWithInherited(true);
+ TagEntity unrelated = tag(3L, "unrelated", TagAssignment.noValue());
+ PolicyEntity first = policy(10L, "first", true);
+ PolicyEntity second = policy(11L, "second", true);
+ PolicyEntity disabled = policy(12L, "disabled", false);
+ Map<MetadataObject, ResolvedTag[]> tags = new LinkedHashMap<>();
+ tags.put(
+ OBJECT,
+ new ResolvedTag[] {new ResolvedTag(direct, OBJECT), new
ResolvedTag(inherited, CATALOG)});
+ tags.put(CATALOG, new ResolvedTag[] {new ResolvedTag(unrelated, CATALOG)});
+ when(effectiveTagResolver.resolveWithSources(METALAKE, List.of(OBJECT,
CATALOG)))
+ .thenReturn(tags);
+ when(relationOperations.batchListEntitiesByRelation(
+ SupportsRelationOperations.Type.POLICY_TAG_REL,
+ List.of(
+ direct.nameIdentifier(), inherited.nameIdentifier(),
unrelated.nameIdentifier()),
+ Entity.EntityType.TAG))
+ .thenReturn(
+ List.of(
+ relation(inherited, first, AllValuesSelector.get()),
+ relation(unrelated, second, AllValuesSelector.get()),
+ relation(direct, second, TagValueSelector.of("finance")),
+ relation(direct, first, TagValueSelector.of("finance")),
+ relation(direct, disabled, AllValuesSelector.get())));
+
+ Map<MetadataObject, PolicyEntity[]> result =
+ resolver.resolve(METALAKE, List.of(OBJECT, CATALOG));
+
+ Assertions.assertArrayEquals(new PolicyEntity[] {first, second},
result.get(OBJECT));
+ Assertions.assertFalse(result.get(OBJECT)[0].inherited().orElseThrow());
+ Assertions.assertFalse(result.get(OBJECT)[1].inherited().orElseThrow());
+ Assertions.assertArrayEquals(new PolicyEntity[] {second},
result.get(CATALOG));
+ }
+
private static RelationalEntity<PolicyEntity> relation(
TagEntity tag, PolicyEntity policy, PolicyAssociationSelector selector) {
return new RelationalEntity<>(
diff --git
a/core/src/test/java/org/apache/gravitino/policy/TestPolicyManager.java
b/core/src/test/java/org/apache/gravitino/policy/TestPolicyManager.java
index 51887d8b05..cf74141a2b 100644
--- a/core/src/test/java/org/apache/gravitino/policy/TestPolicyManager.java
+++ b/core/src/test/java/org/apache/gravitino/policy/TestPolicyManager.java
@@ -750,6 +750,27 @@ public class TestPolicyManager {
Assertions.assertEquals(
ImmutableSet.of(policy1, policy2, policy3),
ImmutableSet.copyOf(policiesInfo2));
+ MetadataObject columnObject =
+ NameIdentifierUtil.toMetadataObject(
+ NameIdentifierUtil.ofColumn(METALAKE, CATALOG, SCHEMA, TABLE,
COLUMN),
+ Entity.EntityType.COLUMN);
+ associatePolicyThroughTag(policy1, columnObject);
+ Map<MetadataObject, PolicyEntity[]> batched =
+ policyManager.listPolicyInfosForMetadataObjects(
+ METALAKE, List.of(tableObject, columnObject));
+ Assertions.assertEquals(3, batched.get(tableObject).length);
+ Assertions.assertEquals(3, batched.get(columnObject).length);
+ Assertions.assertFalse(
+ Arrays.stream(batched.get(columnObject))
+ .filter(policy -> policy.name().equals(policy1.name()))
+ .findFirst()
+ .orElseThrow()
+ .inherited()
+ .orElseThrow());
+ Assertions.assertEquals(
+
ImmutableSet.copyOf(policyManager.listPolicyInfosForMetadataObject(METALAKE,
columnObject)),
+ ImmutableSet.copyOf(batched.get(columnObject)));
+
// List policies for non-existent metadata object
MetadataObject nonExistentObject =
NameIdentifierUtil.toMetadataObject(
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestTableColumnMetaService.java
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestTableColumnMetaService.java
index f53defa175..681aec9566 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestTableColumnMetaService.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestTableColumnMetaService.java
@@ -190,6 +190,115 @@ public class TestTableColumnMetaService extends
TestJDBCBackend {
compareTwoColumns(createdTable.columns(), retrievedTable.columns());
}
+ /** Verifies batched column ID reads across query boundaries on every
backend. */
+ @TestTemplate
+ public void testBatchGetColumnIdsForWideTable() throws IOException {
+ createParentEntities(METALAKE_NAME, "catalog1", "schema1", AUDIT_INFO);
+ List<ColumnEntity> columns = new ArrayList<>();
+ for (int i = 0; i < WIDE_TABLE_COLUMN_COUNT; i++) {
+ columns.add(newIntColumn("column_" + i, i, "comment"));
+ }
+ TableEntity table =
+ TableEntity.builder()
+ .withId(RandomIdGenerator.INSTANCE.nextId())
+ .withName("wide_table")
+ .withNamespace(Namespace.of(METALAKE_NAME, "catalog1", "schema1"))
+ .withColumns(columns)
+ .withAuditInfo(AUDIT_INFO)
+ .build();
+ TableMetaService.getInstance().insertTable(table, false);
+ TableColumnMetaService service = TableColumnMetaService.getInstance();
+ for (int count : new int[] {0, 1, 499, 500, 501, 1001,
WIDE_TABLE_COLUMN_COUNT}) {
+ List<ColumnEntity> requested = columns.subList(0, count);
+ Map<String, Long> expected =
+ requested.stream().collect(Collectors.toMap(ColumnEntity::name,
ColumnEntity::id));
+ Assertions.assertEquals(
+ expected,
+ service.getColumnIdsByTableIdAndNames(
+ table.id(),
requested.stream().map(ColumnEntity::name).collect(Collectors.toList())));
+ }
+ List<String> names =
columns.stream().map(ColumnEntity::name).collect(Collectors.toList());
+ names.add(columns.get(0).name());
+ Assertions.assertEquals(
+ WIDE_TABLE_COLUMN_COUNT,
service.getColumnIdsByTableIdAndNames(table.id(), names).size());
+ names.add("missing_column");
+ Assertions.assertThrows(
+ NoSuchEntityException.class,
+ () -> service.getColumnIdsByTableIdAndNames(table.id(), names));
+ }
+
+ /** Verifies batch lookups follow renames, deletions and replacement IDs
within one table. */
+ @TestTemplate
+ public void testBatchGetColumnIdsFollowsColumnLifecycle() throws IOException
{
+ createParentEntities(METALAKE_NAME, "catalog1", "schema1", AUDIT_INFO);
+ ColumnEntity dropped = newIntColumn("dropped", 0, "comment");
+ ColumnEntity renamed = newIntColumn("renamed", 1, "comment");
+ ColumnEntity kept = newIntColumn("kept", 2, "comment");
+ TableEntity table =
+ TableEntity.builder()
+ .withId(RandomIdGenerator.INSTANCE.nextId())
+ .withName("table1")
+ .withNamespace(Namespace.of(METALAKE_NAME, "catalog1", "schema1"))
+ .withColumns(Lists.newArrayList(dropped, renamed, kept))
+ .withAuditInfo(AUDIT_INFO)
+ .build();
+ TableMetaService.getInstance().insertTable(table, false);
+ TableColumnMetaService service = TableColumnMetaService.getInstance();
+ Assertions.assertEquals(
+ Map.of("dropped", dropped.id(), "renamed", renamed.id(), "kept",
kept.id()),
+ service.getColumnIdsByTableIdAndNames(table.id(), List.of("dropped",
"renamed", "kept")));
+
+ ColumnEntity renamedColumn = newIntColumn("new_name", 1, "comment",
renamed.id());
+ TableEntity updated =
+ TableEntity.builder()
+ .withId(table.id())
+ .withName(table.name())
+ .withNamespace(table.namespace())
+ .withColumns(Lists.newArrayList(renamedColumn, kept))
+ .withAuditInfo(AUDIT_INFO)
+ .build();
+ TableMetaService.getInstance().updateTable(table.nameIdentifier(), ignored
-> updated);
+ Assertions.assertEquals(
+ Map.of("new_name", renamed.id(), "kept", kept.id()),
+ service.getColumnIdsByTableIdAndNames(table.id(), List.of("new_name",
"kept")));
+ for (String missing : List.of("dropped", "renamed", "missing")) {
+ Assertions.assertThrows(
+ NoSuchEntityException.class,
+ () -> service.getColumnIdsByTableIdAndNames(table.id(),
List.of("kept", missing)));
+ }
+
+ ColumnEntity replacement = newIntColumn("dropped", 0, "replacement");
+ TableEntity replaced =
+ TableEntity.builder()
+ .withId(table.id())
+ .withName(table.name())
+ .withNamespace(table.namespace())
+ .withColumns(Lists.newArrayList(replacement, renamedColumn, kept))
+ .withAuditInfo(AUDIT_INFO)
+ .build();
+ TableMetaService.getInstance().updateTable(table.nameIdentifier(), ignored
-> replaced);
+ Assertions.assertEquals(
+ Map.of("dropped", replacement.id(), "new_name", renamed.id(), "kept",
kept.id()),
+ service.getColumnIdsByTableIdAndNames(table.id(), List.of("dropped",
"new_name", "kept")));
+
+ ColumnEntity otherColumn = newIntColumn("kept", 0, "other table");
+ TableEntity otherTable =
+ TableEntity.builder()
+ .withId(RandomIdGenerator.INSTANCE.nextId())
+ .withName("table2")
+ .withNamespace(table.namespace())
+ .withColumns(Lists.newArrayList(otherColumn))
+ .withAuditInfo(AUDIT_INFO)
+ .build();
+ TableMetaService.getInstance().insertTable(otherTable, false);
+ Assertions.assertEquals(
+ Map.of("kept", kept.id()),
+ service.getColumnIdsByTableIdAndNames(table.id(), List.of("kept")));
+ Assertions.assertEquals(
+ Map.of("kept", otherColumn.id()),
+ service.getColumnIdsByTableIdAndNames(otherTable.id(),
List.of("kept")));
+ }
+
@TestTemplate
public void testDropColumnsRemovesTheirRelationsInBatches() throws Exception
{
String catalogName = "catalog1";
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestTagMetaService.java
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestTagMetaService.java
index 7781afbd51..7194e5dae3 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestTagMetaService.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestTagMetaService.java
@@ -33,9 +33,11 @@ import java.sql.Statement;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -51,6 +53,7 @@ import org.apache.gravitino.Namespace;
import org.apache.gravitino.RelationEdgeTarget;
import org.apache.gravitino.RelationQuery;
import org.apache.gravitino.RelationUpdate;
+import org.apache.gravitino.RelationalEntity;
import org.apache.gravitino.SupportsRelationOperations;
import org.apache.gravitino.authorization.AuthorizationUtils;
import org.apache.gravitino.authorization.Privileges;
@@ -1476,6 +1479,103 @@ public class TestTagMetaService extends TestJDBCBackend
{
Assertions.assertThrows(SQLException.class, () ->
insertDuplicateActiveTagRel(tagEntity.id()));
}
+ /**
+ * Verifies batch boundaries and per-source tag values, including columns
from different tables.
+ */
+ @TestTemplate
+ public void testBatchListTagsForMetadataObjects() throws IOException {
+ createAndInsertMakeLake(METALAKE_NAME);
+ createAndInsertCatalog(METALAKE_NAME, "catalog1");
+ createAndInsertSchema(METALAKE_NAME, "catalog1", "schema1");
+ List<ColumnEntity> columns = new ArrayList<>();
+ for (int i = 0; i < 502; i++) {
+ columns.add(newColumn("column_" + i, i));
+ }
+ TableEntity table =
+ TableEntity.builder()
+ .withId(RandomIdGenerator.INSTANCE.nextId())
+ .withName("table1")
+ .withNamespace(Namespace.of(METALAKE_NAME, "catalog1", "schema1"))
+ .withColumns(columns)
+ .withAuditInfo(AUDIT_INFO)
+ .build();
+ backend.insert(table, false);
+ TableEntity otherTable =
+ TableEntity.builder()
+ .withId(RandomIdGenerator.INSTANCE.nextId())
+ .withName("table2")
+ .withNamespace(table.namespace())
+ .withColumns(Lists.newArrayList(newColumn("column_0", 0)))
+ .withAuditInfo(AUDIT_INFO)
+ .build();
+ backend.insert(otherTable, false);
+ TagMetaService service = TagMetaService.getInstance();
+ for (String name : List.of("stage", "flag")) {
+ service.insertTag(
+ TagEntity.builder()
+ .withId(RandomIdGenerator.INSTANCE.nextId())
+ .withName(name)
+ .withNamespace(NamespaceUtil.ofTag(METALAKE_NAME))
+ .withAuditInfo(AUDIT_INFO)
+ .build(),
+ false);
+ }
+ List<NameIdentifier> identifiers = new ArrayList<>();
+ Map<NameIdentifier, Map<String, Set<String>>> expected = new
LinkedHashMap<>();
+ for (int i = 0; i < 501; i++) {
+ NameIdentifier identifier = columnIdent(table, columns.get(i).name());
+ identifiers.add(identifier);
+ service.associateTagValuesWithMetadataObject(
+ identifier,
+ Entity.EntityType.COLUMN,
+ new TagValue[] {TagValue.of("stage", "common"), TagValue.of("stage",
"value_" + i)},
+ new TagValue[0]);
+ expected.put(identifier, Map.of("stage", Set.of("common", "value_" +
i)));
+ }
+ service.associateTagValuesWithMetadataObject(
+ identifiers.get(0),
+ Entity.EntityType.COLUMN,
+ new TagValue[] {TagValue.noValue("flag")},
+ new TagValue[0]);
+ expected.put(
+ identifiers.get(0), Map.of("stage", Set.of("common", "value_0"),
"flag", Set.of()));
+ for (int count : new int[] {0, 1, 499, 500, 501}) {
+ List<NameIdentifier> requested = identifiers.subList(0, count);
+ assertBatchTagAssignments(
+ service,
+ requested,
+ Entity.EntityType.COLUMN,
+ requested.stream().collect(Collectors.toMap(identifier ->
identifier, expected::get)));
+ }
+
+ NameIdentifier otherColumn = columnIdent(otherTable, "column_0");
+ service.associateTagValuesWithMetadataObject(
+ otherColumn,
+ Entity.EntityType.COLUMN,
+ new TagValue[] {TagValue.of("stage", "other_table")},
+ new TagValue[0]);
+ expected.put(otherColumn, Map.of("stage", Set.of("other_table")));
+ identifiers.add(otherColumn);
+ identifiers.add(columnIdent(table, "column_501")); // An existing column
without tags.
+ identifiers.add(identifiers.get(0)); // Duplicate requests must not
duplicate relation edges.
+ assertBatchTagAssignments(service, identifiers, Entity.EntityType.COLUMN,
expected);
+
+ service.associateTagValuesWithMetadataObject(
+ table.nameIdentifier(),
+ Entity.EntityType.TABLE,
+ new TagValue[] {TagValue.of("stage", "table_value")},
+ new TagValue[0]);
+ assertBatchTagAssignments(
+ service,
+ List.of(table.nameIdentifier(), otherTable.nameIdentifier()),
+ Entity.EntityType.TABLE,
+ Map.of(table.nameIdentifier(), Map.of("stage",
Set.of("table_value"))));
+ identifiers.add(columnIdent(table, "missing"));
+ Assertions.assertThrows(
+ NoSuchEntityException.class,
+ () -> service.listTagsForMetadataObjects(identifiers,
Entity.EntityType.COLUMN));
+ }
+
@TestTemplate
public void testListTagsForMetadataObject() throws IOException {
testAssociateAndDisassociateTagsWithMetadataObject();
@@ -2098,6 +2198,22 @@ public class TestTagMetaService extends TestJDBCBackend {
// The dropped column's relation row is removed, not just hidden.
Assertions.assertEquals(2, countActiveTagRel(tag.id()));
+ assertBatchTagAssignments(
+ tagMetaService,
+ List.of(columnIdent(table, "c2_new"), columnIdent(table, "c3")),
+ Entity.EntityType.COLUMN,
+ Map.of(
+ columnIdent(table, "c2_new"), Map.of(tag.name(), Set.of()),
+ columnIdent(table, "c3"), Map.of(tag.name(), Set.of())));
+ for (String missing : List.of("c1", "c2", "missing")) {
+ Assertions.assertThrows(
+ NoSuchEntityException.class,
+ () ->
+ tagMetaService.listTagsForMetadataObjects(
+ List.of(columnIdent(table, "c3"), columnIdent(table,
missing)),
+ Entity.EntityType.COLUMN));
+ }
+
Assertions.assertEquals(
1,
tagMetaService
@@ -2127,6 +2243,13 @@ public class TestTagMetaService extends TestJDBCBackend {
.isEmpty());
Assertions.assertEquals(
2,
tagMetaService.listAssociatedMetadataObjectsForTag(tag.nameIdentifier()).size());
+ assertBatchTagAssignments(
+ tagMetaService,
+ List.of(columnIdent(table, "c1"), columnIdent(table, "c2_new"),
columnIdent(table, "c3")),
+ Entity.EntityType.COLUMN,
+ Map.of(
+ columnIdent(table, "c2_new"), Map.of(tag.name(), Set.of()),
+ columnIdent(table, "c3"), Map.of(tag.name(), Set.of())));
}
private static ColumnEntity newColumn(String name, int position) {
@@ -2179,6 +2302,35 @@ public class TestTagMetaService extends TestJDBCBackend {
() -> tagMetaService.getTagIdByTagName(metalakeId, "missing_tag"));
}
+ private void assertBatchTagAssignments(
+ TagMetaService service,
+ List<NameIdentifier> identifiers,
+ Entity.EntityType objectType,
+ Map<NameIdentifier, Map<String, Set<String>>> expected) {
+ List<RelationalEntity<?>> relations =
+ service.listTagsForMetadataObjects(identifiers, objectType);
+ relations.forEach(
+ relation -> {
+
assertEquals(SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
relation.type());
+ assertEquals(objectType, relation.sourceType());
+ assertEquals(NamespaceUtil.ofTag(METALAKE_NAME),
relation.targetEntity().namespace());
+ });
+ Map<NameIdentifier, Map<String, Set<String>>> actual =
+ relations.stream()
+ .collect(
+ Collectors.groupingBy(
+ RelationalEntity::source,
+ Collectors.toMap(
+ relation -> relation.targetEntity().name(),
+ relation -> {
+ TagEntity tag = (TagEntity) relation.targetEntity();
+ assertTrue(tag.assignment().isPresent());
+ return new LinkedHashSet<>(
+ Arrays.asList(tag.assignment().get().values()));
+ })));
+ assertEquals(expected, actual);
+ }
+
private TagEntity copyTagWithComment(TagEntity tag, String comment) {
return copyTag(tag, tag.name(), comment);
}
diff --git
a/core/src/test/java/org/apache/gravitino/tag/TestEffectiveTagResolver.java
b/core/src/test/java/org/apache/gravitino/tag/TestEffectiveTagResolver.java
index 0736256ace..7b66d309f0 100644
--- a/core/src/test/java/org/apache/gravitino/tag/TestEffectiveTagResolver.java
+++ b/core/src/test/java/org/apache/gravitino/tag/TestEffectiveTagResolver.java
@@ -18,18 +18,26 @@
*/
package org.apache.gravitino.tag;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
+import java.util.List;
+import java.util.Map;
import org.apache.gravitino.Entity;
import org.apache.gravitino.EntityStore;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.RelationalEntity;
import org.apache.gravitino.SupportsRelationOperations;
import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.exceptions.NoSuchMetadataObjectException;
@@ -66,6 +74,9 @@ public class TestEffectiveTagResolver {
metadataObjectUtil
.when(() -> MetadataObjectUtil.checkMetadataObject(METALAKE, TABLE))
.thenAnswer(invocation -> null);
+ metadataObjectUtil
+ .when(() -> MetadataObjectUtil.checkMetadataObjects(eq(METALAKE),
anyList()))
+ .thenAnswer(invocation -> null);
}
@AfterEach
@@ -151,6 +162,72 @@ public class TestEffectiveTagResolver {
Assertions.assertSame(exception, actual.getCause());
}
+ @Test
+ void testBatchSharesAncestorsAndPreservesNearestAssignment() throws
Exception {
+ MetadataObject first =
+ MetadataObjects.of(
+ Arrays.asList("catalog", "schema", "table", "first"),
MetadataObject.Type.COLUMN);
+ MetadataObject second =
+ MetadataObjects.of(
+ Arrays.asList("catalog", "schema", "table", "second"),
MetadataObject.Type.COLUMN);
+ TagEntity inherited = tag(1L, "domain", TagAssignment.ofValues("finance"));
+ TagEntity direct = tag(1L, "domain",
TagAssignment.ofValues("engineering"));
+ when(relationOperations.batchListEntitiesByRelation(any(), any(), any()))
+ .thenAnswer(
+ invocation -> {
+ Entity.EntityType type = invocation.getArgument(2);
+ if (type == Entity.EntityType.TABLE) {
+ return Collections.singletonList(
+ new RelationalEntity<>(
+
SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+ MetadataObjectUtil.toEntityIdent(METALAKE, TABLE),
+ type,
+ inherited));
+ }
+ if (type == Entity.EntityType.COLUMN) {
+ return Collections.singletonList(
+ new RelationalEntity<>(
+
SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+ MetadataObjectUtil.toEntityIdent(METALAKE, first),
+ type,
+ direct));
+ }
+ return Collections.emptyList();
+ });
+
+ Map<MetadataObject, TagEntity[]> result = resolver.resolve(METALAKE,
List.of(first, second));
+
+ metadataObjectUtil.verify(
+ () -> MetadataObjectUtil.checkMetadataObjects(METALAKE, List.of(first,
second)));
+
+ Assertions.assertArrayEquals(
+ new String[] {"engineering"},
result.get(first)[0].assignment().orElseThrow().values());
+ Assertions.assertFalse(result.get(first)[0].inherited().orElseThrow());
+ Assertions.assertArrayEquals(
+ new String[] {"finance"},
result.get(second)[0].assignment().orElseThrow().values());
+ Assertions.assertTrue(result.get(second)[0].inherited().orElseThrow());
+ verify(relationOperations, times(1))
+ .batchListEntitiesByRelation(
+ SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+ List.of(MetadataObjectUtil.toEntityIdent(METALAKE, TABLE)),
+ Entity.EntityType.TABLE);
+
+ Map<MetadataObject, EffectiveTagResolver.ResolvedTag[]> withSources =
+ resolver.resolveWithSources(METALAKE, List.of(first, second));
+ Assertions.assertEquals(first, withSources.get(first)[0].source());
+ Assertions.assertEquals(TABLE, withSources.get(second)[0].source());
+ Assertions.assertArrayEquals(
+ result.get(first),
+ Arrays.stream(withSources.get(first))
+ .map(EffectiveTagResolver.ResolvedTag::tag)
+ .toArray(TagEntity[]::new));
+ Assertions.assertArrayEquals(
+ result.get(second),
+ Arrays.stream(withSources.get(second))
+ .map(EffectiveTagResolver.ResolvedTag::tag)
+ .toArray(TagEntity[]::new));
+ }
+
private static TagEntity tag(long id, String name, TagAssignment assignment)
{
return TagEntity.builder()
.withId(id)
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 4cd8916e04..7ecd04fa24 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,7 @@
*/
package org.apache.gravitino.utils;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
@@ -25,6 +26,7 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.gravitino.Entity;
@@ -42,6 +44,8 @@ import org.apache.gravitino.catalog.SemanticModelDispatcher;
import org.apache.gravitino.catalog.TableDispatcher;
import org.apache.gravitino.catalog.TopicDispatcher;
import org.apache.gravitino.catalog.ViewDispatcher;
+import org.apache.gravitino.exceptions.IllegalNamespaceException;
+import org.apache.gravitino.exceptions.NoSuchMetadataObjectException;
import org.apache.gravitino.job.JobOperationDispatcher;
import org.apache.gravitino.metalake.MetalakeDispatcher;
import org.apache.gravitino.policy.PolicyDispatcher;
@@ -361,6 +365,106 @@ public class TestMetadataObjectUtil {
verify(jobDispatcher).getJobTemplate("metalake", "template");
}
+ @Test
+ void testBatchChecksEachTableOnceAndDoesNotReuseChecksAcrossCalls() {
+ GravitinoEnv env = mock(GravitinoEnv.class);
+ TableDispatcher dispatcher = mock(TableDispatcher.class);
+ when(env.internalTableDispatcher()).thenReturn(dispatcher);
+ NameIdentifier tableIdentifier = NameIdentifier.of("metalake", "catalog",
"schema", "table");
+ when(dispatcher.tableExists(tableIdentifier)).thenReturn(true);
+ MetadataObject table = MetadataObjects.of("catalog.schema", "table",
MetadataObject.Type.TABLE);
+ MetadataObject first =
+ MetadataObjects.of("catalog.schema.table", "first",
MetadataObject.Type.COLUMN);
+ MetadataObject second =
+ MetadataObjects.of("catalog.schema.table", "second",
MetadataObject.Type.COLUMN);
+ try (MockedStatic<GravitinoEnv> mockedEnv =
mockStatic(GravitinoEnv.class)) {
+ mockedEnv.when(GravitinoEnv::getInstance).thenReturn(env);
+ MetadataObjectUtil.checkMetadataObjects("metalake", List.of(table,
first, second, table));
+ verify(dispatcher, times(1)).tableExists(tableIdentifier);
+ MetadataObjectUtil.checkMetadataObjects("metalake", List.of(first,
second, table));
+ verify(dispatcher, times(2)).tableExists(tableIdentifier);
+
+ when(dispatcher.tableExists(tableIdentifier)).thenReturn(false);
+ NoSuchMetadataObjectException exception =
+ Assertions.assertThrows(
+ NoSuchMetadataObjectException.class,
+ () -> MetadataObjectUtil.checkMetadataObjects("metalake",
List.of(first, table)));
+ Assertions.assertEquals(
+ "Metadata object catalog.schema.table.first type COLUMN doesn't
exist",
+ exception.getMessage());
+ verify(dispatcher, times(3)).tableExists(tableIdentifier);
+ }
+ }
+
+ @Test
+ void testBatchChecksTablesWithTheSameNameInDifferentNamespaces() {
+ GravitinoEnv env = mock(GravitinoEnv.class);
+ TableDispatcher dispatcher = mock(TableDispatcher.class);
+ when(env.internalTableDispatcher()).thenReturn(dispatcher);
+ when(dispatcher.tableExists(any())).thenReturn(true);
+ try (MockedStatic<GravitinoEnv> mockedEnv =
mockStatic(GravitinoEnv.class)) {
+ mockedEnv.when(GravitinoEnv::getInstance).thenReturn(env);
+ MetadataObjectUtil.checkMetadataObjects(
+ "metalake",
+ List.of(
+ MetadataObjects.of("catalog.schema.table", "first",
MetadataObject.Type.COLUMN),
+ MetadataObjects.of("catalog.other.table", "second",
MetadataObject.Type.COLUMN),
+ MetadataObjects.of("other.schema", "table",
MetadataObject.Type.TABLE)));
+ verify(dispatcher).tableExists(NameIdentifier.of("metalake", "catalog",
"schema", "table"));
+ verify(dispatcher).tableExists(NameIdentifier.of("metalake", "catalog",
"other", "table"));
+ verify(dispatcher).tableExists(NameIdentifier.of("metalake", "other",
"schema", "table"));
+ }
+ }
+
+ @Test
+ void testBatchStillChecksNonTableObjects() {
+ GravitinoEnv env = mock(GravitinoEnv.class);
+ SchemaDispatcher dispatcher = mock(SchemaDispatcher.class);
+ when(env.internalSchemaDispatcher()).thenReturn(dispatcher);
+ NameIdentifier schemaIdentifier = NameIdentifier.of("metalake", "catalog",
"schema");
+ when(dispatcher.schemaExists(schemaIdentifier)).thenReturn(false);
+ MetadataObject schema = MetadataObjects.of("catalog", "schema",
MetadataObject.Type.SCHEMA);
+ try (MockedStatic<GravitinoEnv> mockedEnv =
mockStatic(GravitinoEnv.class)) {
+ mockedEnv.when(GravitinoEnv::getInstance).thenReturn(env);
+ Assertions.assertThrows(
+ NoSuchMetadataObjectException.class,
+ () -> MetadataObjectUtil.checkMetadataObjects("metalake",
List.of(schema)));
+ verify(dispatcher).schemaExists(schemaIdentifier);
+ }
+ }
+
+ @Test
+ void testBatchRejectsNullObject() {
+ IllegalArgumentException exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ MetadataObjectUtil.checkMetadataObjects(
+ "metalake", Collections.singletonList(null)));
+ Assertions.assertEquals("metadataObject cannot be null",
exception.getMessage());
+ }
+
+ @Test
+ void testBatchRejectsMalformedColumnIdentifier() {
+ GravitinoEnv env = mock(GravitinoEnv.class);
+ TableDispatcher dispatcher = mock(TableDispatcher.class);
+ when(env.internalTableDispatcher()).thenReturn(dispatcher);
+ NameIdentifier tableIdentifier = NameIdentifier.of("metalake", "catalog",
"schema", "table");
+ when(dispatcher.tableExists(tableIdentifier)).thenReturn(true);
+ MetadataObject table = MetadataObjects.of("catalog.schema", "table",
MetadataObject.Type.TABLE);
+ MetadataObject malformedColumn = mock(MetadataObject.class);
+ when(malformedColumn.type()).thenReturn(MetadataObject.Type.COLUMN);
+
when(malformedColumn.fullName()).thenReturn("catalog.schema.table.column.extra");
+ try (MockedStatic<GravitinoEnv> mockedEnv =
mockStatic(GravitinoEnv.class)) {
+ mockedEnv.when(GravitinoEnv::getInstance).thenReturn(env);
+ Assertions.assertThrows(
+ IllegalNamespaceException.class,
+ () ->
+ MetadataObjectUtil.checkMetadataObjects("metalake",
List.of(table, malformedColumn)));
+ verify(dispatcher, times(1)).tableExists(tableIdentifier);
+ }
+ }
+
private static List<String> describe(List<MetadataObject> objects) {
return objects.stream().map(o -> o.type() + ":" +
o.fullName()).collect(Collectors.toList());
}
diff --git
a/iceberg/iceberg-rest-server-experiment/src/main/java/org/apache/gravitino/iceberg/service/restriction/ExperimentalIcebergReadRestrictionLoader.java
b/iceberg/iceberg-rest-server-experiment/src/main/java/org/apache/gravitino/iceberg/service/restriction/ExperimentalIcebergReadRestrictionLoader.java
index b67adf17aa..19b13ac185 100644
---
a/iceberg/iceberg-rest-server-experiment/src/main/java/org/apache/gravitino/iceberg/service/restriction/ExperimentalIcebergReadRestrictionLoader.java
+++
b/iceberg/iceberg-rest-server-experiment/src/main/java/org/apache/gravitino/iceberg/service/restriction/ExperimentalIcebergReadRestrictionLoader.java
@@ -104,10 +104,10 @@ public final class
ExperimentalIcebergReadRestrictionLoader
metalake, context.catalogName(), tableName.namespace().level(2)),
Entity.EntityType.SCHEMA);
Schema schema = response.tableMetadata().schema();
- PolicyEntity[] rowPolicies =
- policyDispatcher.listPolicyInfosForMetadataObject(
- metalake, NameIdentifierUtil.toMetadataObject(tableName,
Entity.EntityType.TABLE));
- Map<Integer, PolicyEntity[]> maskPolicies = new LinkedHashMap<>();
+ MetadataObject table = NameIdentifierUtil.toMetadataObject(tableName,
Entity.EntityType.TABLE);
+ List<MetadataObject> objects = new ArrayList<>();
+ objects.add(table);
+ Map<Integer, MetadataObject> columns = new LinkedHashMap<>();
for (Types.NestedField field : schema.columns()) {
NameIdentifier columnName =
NameIdentifierUtil.ofColumn(
@@ -118,9 +118,14 @@ public final class ExperimentalIcebergReadRestrictionLoader
field.name());
MetadataObject column =
NameIdentifierUtil.toMetadataObject(columnName,
Entity.EntityType.COLUMN);
- maskPolicies.put(
- field.fieldId(),
policyDispatcher.listPolicyInfosForMetadataObject(metalake, column));
+ objects.add(column);
+ columns.put(field.fieldId(), column);
}
+ Map<MetadataObject, PolicyEntity[]> policies =
+ policyDispatcher.listPolicyInfosForMetadataObjects(metalake, objects);
+ PolicyEntity[] rowPolicies = policies.get(table);
+ Map<Integer, PolicyEntity[]> maskPolicies = new LinkedHashMap<>();
+ columns.forEach((fieldId, column) -> maskPolicies.put(fieldId,
policies.get(column)));
IdentityBindingContext bindingContext =
new IdentityBindingContext(context.userName(), metalake,
accessControlDispatcher);
diff --git
a/iceberg/iceberg-rest-server-experiment/src/test/java/org/apache/gravitino/iceberg/service/restriction/TestExperimentalIcebergReadRestrictionLoader.java
b/iceberg/iceberg-rest-server-experiment/src/test/java/org/apache/gravitino/iceberg/service/restriction/TestExperimentalIcebergReadRestrictionLoader.java
index 4376efa863..2b4b5893cf 100644
---
a/iceberg/iceberg-rest-server-experiment/src/test/java/org/apache/gravitino/iceberg/service/restriction/TestExperimentalIcebergReadRestrictionLoader.java
+++
b/iceberg/iceberg-rest-server-experiment/src/test/java/org/apache/gravitino/iceberg/service/restriction/TestExperimentalIcebergReadRestrictionLoader.java
@@ -28,6 +28,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -177,7 +178,7 @@ public class TestExperimentalIcebergReadRestrictionLoader {
@Test
void testLoadWithoutApplicablePoliciesQueriesTableAndColumns() throws
Exception {
- PolicyDispatcher dispatcher = mock(PolicyDispatcher.class);
+ PolicyDispatcher dispatcher = mock(PolicyDispatcher.class,
CALLS_REAL_METHODS);
when(dispatcher.listPolicyInfosForMetadataObject(eq(METALAKE),
any(MetadataObject.class)))
.thenReturn(new PolicyEntity[0]);
LoadTableResponse response = response(SCHEMA);
@@ -189,6 +190,7 @@ public class TestExperimentalIcebergReadRestrictionLoader {
assertFalse(result.etagSeed().isPresent());
ArgumentCaptor<MetadataObject> objects =
ArgumentCaptor.forClass(MetadataObject.class);
verify(dispatcher,
times(4)).listPolicyInfosForMetadataObject(eq(METALAKE), objects.capture());
+ verify(dispatcher,
times(1)).listPolicyInfosForMetadataObjects(eq(METALAKE), any());
assertEquals(MetadataObject.Type.TABLE,
objects.getAllValues().get(0).type());
for (int index = 1; index < 4; index++) {
assertEquals(MetadataObject.Type.COLUMN,
objects.getAllValues().get(index).type());
@@ -197,7 +199,7 @@ public class TestExperimentalIcebergReadRestrictionLoader {
@Test
void testLoadBuildsRestrictedResponseAndSeed() throws Exception {
- PolicyDispatcher dispatcher = mock(PolicyDispatcher.class);
+ PolicyDispatcher dispatcher = mock(PolicyDispatcher.class,
CALLS_REAL_METHODS);
when(dispatcher.listPolicyInfosForMetadataObject(eq(METALAKE),
any(MetadataObject.class)))
.thenAnswer(
invocation -> {
@@ -239,7 +241,7 @@ public class TestExperimentalIcebergReadRestrictionLoader {
@Test
void testLoadRejectsDottedTableNameWithPolicyDispatcher() throws Exception {
- PolicyDispatcher dispatcher = mock(PolicyDispatcher.class);
+ PolicyDispatcher dispatcher = mock(PolicyDispatcher.class,
CALLS_REAL_METHODS);
IllegalNameIdentifierException exception =
assertThrows(
@@ -254,7 +256,7 @@ public class TestExperimentalIcebergReadRestrictionLoader {
@Test
void testLoadRejectsDottedColumnNameWithPolicyDispatcher() throws Exception {
Schema schema = new Schema(Types.NestedField.optional(1, "a.b",
Types.StringType.get()));
- PolicyDispatcher dispatcher = mock(PolicyDispatcher.class);
+ PolicyDispatcher dispatcher = mock(PolicyDispatcher.class,
CALLS_REAL_METHODS);
when(dispatcher.listPolicyInfosForMetadataObject(eq(METALAKE),
any(MetadataObject.class)))
.thenReturn(new PolicyEntity[0]);
@@ -268,7 +270,7 @@ public class TestExperimentalIcebergReadRestrictionLoader {
@Test
void testLoadRejectsDottedAncestorNamesWithPolicyDispatcher() throws
Exception {
- PolicyDispatcher dispatcher = mock(PolicyDispatcher.class);
+ PolicyDispatcher dispatcher = mock(PolicyDispatcher.class,
CALLS_REAL_METHODS);
TableIdentifier table = TableIdentifier.of("schema", "table");
IllegalNameIdentifierException metalakeError =