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

jshao 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 8ddb17436c [#8710] feat(core): Support cache entities in relation 
operations. (#8712)
8ddb17436c is described below

commit 8ddb17436cfbaed7b46317da73471f489130a1c3
Author: Mini Yu <[email protected]>
AuthorDate: Thu Nov 20 23:56:45 2025 +0800

    [#8710] feat(core): Support cache entities in relation operations. (#8712)
    
    ### What changes were proposed in this pull request?
    
    Cache entities in relation operations.
    
    ### Why are the changes needed?
    
    It's a big improvement.
    
    Fix: #8710
    
    ### Does this PR introduce _any_ user-facing change?
    
    N/A
    
    ### How was this patch tested?
    
    UTs and ITs.
---
 .../gravitino/cache/CaffeineEntityCache.java       |  86 ++++++-
 .../apache/gravitino/cache/ReverseIndexCache.java  |   8 +
 .../apache/gravitino/cache/ReverseIndexRules.java  |  40 ++++
 .../org/apache/gravitino/policy/PolicyManager.java |   2 +-
 .../storage/relational/RelationalEntityStore.java  |  39 +++-
 .../java/org/apache/gravitino/tag/TagManager.java  |   2 +-
 .../gravitino/storage/TestEntityStorage.java       | 247 +++++++++++++++++++++
 7 files changed, 408 insertions(+), 16 deletions(-)

diff --git 
a/core/src/main/java/org/apache/gravitino/cache/CaffeineEntityCache.java 
b/core/src/main/java/org/apache/gravitino/cache/CaffeineEntityCache.java
index ceb1a3ef29..1ecc27dc4f 100644
--- a/core/src/main/java/org/apache/gravitino/cache/CaffeineEntityCache.java
+++ b/core/src/main/java/org/apache/gravitino/cache/CaffeineEntityCache.java
@@ -23,6 +23,7 @@ import com.github.benmanes.caffeine.cache.Cache;
 import com.github.benmanes.caffeine.cache.Caffeine;
 import com.github.benmanes.caffeine.cache.RemovalCause;
 import com.github.benmanes.caffeine.cache.stats.CacheStats;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
@@ -43,12 +44,15 @@ import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.Config;
 import org.apache.gravitino.Configs;
 import org.apache.gravitino.Entity;
 import org.apache.gravitino.HasIdentifier;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.SupportsRelationOperations;
+import org.apache.gravitino.meta.GenericEntity;
 import org.apache.gravitino.meta.ModelVersionEntity;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -90,6 +94,14 @@ public class CaffeineEntityCache extends BaseEntityCache {
 
   private ScheduledExecutorService scheduler;
 
+  private static final Set<SupportsRelationOperations.Type> RELATION_TYPES =
+      Sets.newHashSet(
+          SupportsRelationOperations.Type.METADATA_OBJECT_ROLE_REL,
+          SupportsRelationOperations.Type.ROLE_USER_REL,
+          SupportsRelationOperations.Type.ROLE_GROUP_REL,
+          SupportsRelationOperations.Type.POLICY_METADATA_OBJECT_REL,
+          SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL);
+
   /**
    * Constructs a new {@link CaffeineEntityCache}.
    *
@@ -133,6 +145,11 @@ public class CaffeineEntityCache extends BaseEntityCache {
     }
   }
 
+  @VisibleForTesting
+  public Cache<EntityCacheRelationKey, List<Entity>> getCacheData() {
+    return this.cacheData;
+  }
+
   /** {@inheritDoc} */
   @Override
   public <E extends Entity & HasIdentifier> Optional<List<E>> getIfPresent(
@@ -167,7 +184,10 @@ public class CaffeineEntityCache extends BaseEntityCache {
 
     return segmentedLock.withLock(
         EntityCacheRelationKey.of(ident, type, relType),
-        () -> invalidateEntities(ident, type, Optional.of(relType)));
+        () -> {
+          invalidateEntities(ident, type, Optional.of(relType));
+          return true;
+        });
   }
 
   /** {@inheritDoc} */
@@ -176,7 +196,47 @@ public class CaffeineEntityCache extends BaseEntityCache {
     checkArguments(ident, type);
     return segmentedLock.withLock(
         EntityCacheRelationKey.of(ident, type),
-        () -> invalidateEntities(ident, type, Optional.empty()));
+        () -> {
+          // Clear possible relation first, then clear the main entity cache.
+          // For example, if a tag has been updated, apart from invalidating 
the relation:
+          // metadata_object_to_tag_rel, we also need to invalidate the 
tag_to_metadata_object_rel.
+          // Assuming a tag "tag1" is related to a metadata object "catalog", 
when "catalog" is
+          // renamed to `catalog_new`, we need to invalidate both relations to 
avoid stale data.
+          // that is: before: tag1:TAG_METADATA_OBJECT_REL -> catalog,
+          // catalog:TAG_METADATA_OBJECT_REL -> tag1, after: 
tag1:TAG_METADATA_OBJECT_REL -> null,
+          // catalog:TAG_METADATA_OBJECT_REL -> null.
+          RELATION_TYPES.forEach(
+              relType -> {
+                List<Entity> relatedEntities =
+                    cacheData.getIfPresent(EntityCacheRelationKey.of(ident, 
type, relType));
+                if (relatedEntities != null) {
+                  relatedEntities.stream()
+                      .filter(e -> StringUtils.isNotBlank(((HasIdentifier) 
e).name()))
+                      .forEach(
+                          entity -> {
+                            NameIdentifier identifier = ((HasIdentifier) 
entity).nameIdentifier();
+                            if (entity instanceof GenericEntity) {
+                              String metalakeName = ident.namespace().level(0);
+                              String[] names =
+                                  ArrayUtils.addFirst(
+                                      identifier.namespace().levels(), 
metalakeName);
+                              names = ArrayUtils.add(names, identifier.name());
+                              identifier = NameIdentifier.of(names);
+                            }
+
+                            invalidateEntities(identifier, entity.type(), 
Optional.of(relType));
+                          });
+                }
+              });
+
+          RELATION_TYPES.forEach(
+              relType -> {
+                invalidateEntities(ident, type, Optional.of(relType));
+              });
+
+          invalidateEntities(ident, type, Optional.empty());
+          return true;
+        });
   }
 
   /** {@inheritDoc} */
@@ -222,10 +282,12 @@ public class CaffeineEntityCache extends BaseEntityCache {
     segmentedLock.withLock(
         entityCacheKey,
         () -> {
-          // We still need to cache the entities even if the list is empty, to 
avoid cache
-          // misses. Consider the scenario where a user queries for an 
entity's relations and the
-          // result is empty. If we don't cache this empty result, the next 
query will still hit the
-          // backend, this is not desired.
+          // Return directly if entities are empty. No need to put an empty 
list to cache, we will
+          // use another PR to resolve the performance problem.
+          if (entities.isEmpty()) {
+            return;
+          }
+
           syncEntitiesToCache(
               entityCacheKey, entities.stream().map(e -> (Entity) 
e).collect(Collectors.toList()));
         });
@@ -296,15 +358,13 @@ public class CaffeineEntityCache extends BaseEntityCache {
   }
 
   /**
-   * Syncs the entities to the cache, if entities is too big and can not put 
to the cache, then it
-   * will be removed from the cache and cacheIndex will not be updated.
+   * Syncs the entities to the cache, if entities are too big and cannot put 
to the cache, then it
+   * will be removed from the cache, and cacheIndex will not be updated.
    *
    * @param key The key of the entities.
    * @param newEntities The new entities to sync to the cache.
    */
   private void syncEntitiesToCache(EntityCacheRelationKey key, List<Entity> 
newEntities) {
-    if (key.relationType() != null) return;
-
     List<Entity> existingEntities = cacheData.getIfPresent(key);
 
     if (existingEntities != null && key.relationType() != null) {
@@ -377,11 +437,12 @@ public class CaffeineEntityCache extends BaseEntityCache {
       // For example, we have stored a role entity in the cache and entity to 
role mapping in the
       // reverse index. This is: cache data: role identifier -> role entity, 
reverse index:
       // the securable object -> role. When we update the securable object, we 
need to invalidate
-      // the
-      // role entity from the cache though the securable object is not in the 
cache data.
+      // the role entity from the cache though the securable object is not in 
the cache data.
       valueForExactKey = EntityCacheRelationKey.of(identifier, type, 
relTypeOpt.orElse(null));
     }
 
+    // The visited set to avoid processing the same key multiple times and 
thus causing infinite
+    // loop.
     Set<EntityCacheKey> visited = Sets.newHashSet();
     queue.offer(valueForExactKey);
     while (!queue.isEmpty()) {
@@ -405,6 +466,7 @@ public class CaffeineEntityCache extends BaseEntityCache {
           Lists.newArrayList(
               reverseIndex.getValuesForKeysStartingWith(
                   currentKeyToRemove.identifier().toString()));
+
       reverseKeysToRemove.forEach(
           key -> {
             // Remove from reverse index
diff --git 
a/core/src/main/java/org/apache/gravitino/cache/ReverseIndexCache.java 
b/core/src/main/java/org/apache/gravitino/cache/ReverseIndexCache.java
index 57ca31c974..16c0c87532 100644
--- a/core/src/main/java/org/apache/gravitino/cache/ReverseIndexCache.java
+++ b/core/src/main/java/org/apache/gravitino/cache/ReverseIndexCache.java
@@ -29,8 +29,11 @@ import java.util.Map;
 import org.apache.gravitino.Entity;
 import org.apache.gravitino.HasIdentifier;
 import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.meta.GenericEntity;
 import org.apache.gravitino.meta.GroupEntity;
+import org.apache.gravitino.meta.PolicyEntity;
 import org.apache.gravitino.meta.RoleEntity;
+import org.apache.gravitino.meta.TagEntity;
 import org.apache.gravitino.meta.UserEntity;
 
 /**
@@ -48,6 +51,10 @@ public class ReverseIndexCache {
     registerReverseRule(UserEntity.class, ReverseIndexRules.USER_REVERSE_RULE);
     registerReverseRule(GroupEntity.class, 
ReverseIndexRules.GROUP_REVERSE_RULE);
     registerReverseRule(RoleEntity.class, ReverseIndexRules.ROLE_REVERSE_RULE);
+    registerReverseRule(PolicyEntity.class, 
ReverseIndexRules.POLICY_REVERSE_RULE);
+    registerReverseRule(TagEntity.class, ReverseIndexRules.TAG_REVERSE_RULE);
+    registerReverseRule(
+        GenericEntity.class, 
ReverseIndexRules.GENERIC_METADATA_OBJECT_REVERSE_RULE);
   }
 
   public boolean remove(EntityCacheKey key) {
@@ -73,6 +80,7 @@ public class ReverseIndexCache {
   public void put(
       NameIdentifier nameIdentifier, Entity.EntityType type, 
EntityCacheRelationKey key) {
     EntityCacheKey entityCacheKey = EntityCacheKey.of(nameIdentifier, type);
+
     List<EntityCacheKey> existingKeys = 
reverseIndex.getValueForExactKey(entityCacheKey.toString());
     if (existingKeys == null) {
       reverseIndex.put(entityCacheKey.toString(), List.of(key));
diff --git 
a/core/src/main/java/org/apache/gravitino/cache/ReverseIndexRules.java 
b/core/src/main/java/org/apache/gravitino/cache/ReverseIndexRules.java
index d0839c5f6f..f568320860 100644
--- a/core/src/main/java/org/apache/gravitino/cache/ReverseIndexRules.java
+++ b/core/src/main/java/org/apache/gravitino/cache/ReverseIndexRules.java
@@ -18,11 +18,16 @@
  */
 package org.apache.gravitino.cache;
 
+import org.apache.commons.lang3.ArrayUtils;
 import org.apache.gravitino.Entity;
+import org.apache.gravitino.Entity.EntityType;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.Namespace;
+import org.apache.gravitino.meta.GenericEntity;
 import org.apache.gravitino.meta.GroupEntity;
+import org.apache.gravitino.meta.PolicyEntity;
 import org.apache.gravitino.meta.RoleEntity;
+import org.apache.gravitino.meta.TagEntity;
 import org.apache.gravitino.meta.UserEntity;
 import org.apache.gravitino.utils.NamespaceUtil;
 
@@ -142,4 +147,39 @@ public class ReverseIndexRules {
                   });
         }
       };
+
+  // Keep policies/tags to objects reverse index for metadata objects, so the 
key are objects and
+  // the values are policies/tags.
+  public static final ReverseIndexCache.ReverseIndexRule 
GENERIC_METADATA_OBJECT_REVERSE_RULE =
+      (entity, key, reverseIndexCache) -> {
+        // Name in GenericEntity contains no metalake.
+        GenericEntity genericEntity = (GenericEntity) entity;
+        EntityType type = entity.type();
+        if (genericEntity.name() != null) {
+          String[] levels = genericEntity.name().split("\\.");
+          String metalakeName = key.identifier().namespace().levels()[0];
+          NameIdentifier objectNameIdentifier =
+              NameIdentifier.of(ArrayUtils.addFirst(levels, metalakeName));
+          reverseIndexCache.put(objectNameIdentifier, type, key);
+        }
+      };
+
+  // Keep objects to policies reverse index for policy objects, so the key are 
policies and the
+  // values are objects.
+  public static final ReverseIndexCache.ReverseIndexRule POLICY_REVERSE_RULE =
+      (entity, key, reverseIndexCache) -> {
+        PolicyEntity policyEntity = (PolicyEntity) entity;
+        NameIdentifier nameIdentifier =
+            NameIdentifier.of(policyEntity.namespace(), policyEntity.name());
+        reverseIndexCache.put(nameIdentifier, Entity.EntityType.POLICY, key);
+      };
+
+  // Keep objects to tags reverse index for tag objects, so the key are tags 
and the
+  // values are objects.
+  public static final ReverseIndexCache.ReverseIndexRule TAG_REVERSE_RULE =
+      (entity, key, reverseIndexCache) -> {
+        TagEntity tagEntity = (TagEntity) entity;
+        NameIdentifier nameIdentifier = 
NameIdentifier.of(tagEntity.namespace(), tagEntity.name());
+        reverseIndexCache.put(nameIdentifier, Entity.EntityType.TAG, key);
+      };
 }
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 14deabbb48..fe39649b9d 100644
--- a/core/src/main/java/org/apache/gravitino/policy/PolicyManager.java
+++ b/core/src/main/java/org/apache/gravitino/policy/PolicyManager.java
@@ -387,7 +387,7 @@ public class PolicyManager implements PolicyDispatcher {
                     entityType,
                     policyIdent);
           } catch (NoSuchEntityException e) {
-            if (e.getMessage().contains("No such policy entity")) {
+            if (e.getMessage().contains("No such entity")) {
               throw new NoSuchPolicyException(
                   e, "Policy %s does not exist for metadata object %s", 
policyName, metadataObject);
             } else {
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
index 790624bc12..57de273755 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
@@ -20,6 +20,7 @@ package org.apache.gravitino.storage.relational;
 
 import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_STORE;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableMap;
 import java.io.IOException;
 import java.util.List;
@@ -58,6 +59,11 @@ public class RelationalEntityStore implements EntityStore, 
SupportsRelationOpera
   private RelationalGarbageCollector garbageCollector;
   private EntityCache cache;
 
+  @VisibleForTesting
+  public EntityCache getCache() {
+    return cache;
+  }
+
   @Override
   public void initialize(Config config) throws RuntimeException {
     this.backend = createRelationalEntityBackend(config);
@@ -196,8 +202,37 @@ public class RelationalEntityStore implements EntityStore, 
SupportsRelationOpera
       Entity.EntityType srcType,
       NameIdentifier destEntityIdent)
       throws IOException, NoSuchEntityException {
-    // todo: support cache
-    return backend.getEntityByRelation(relType, srcIdentifier, srcType, 
destEntityIdent);
+    return cache.withCacheLock(
+        EntityCacheRelationKey.of(srcIdentifier, srcType, relType),
+        () -> {
+          Optional<List<E>> entities = cache.getIfPresent(relType, 
srcIdentifier, srcType);
+          if (entities.isPresent()) {
+            return entities.get().stream()
+                .filter(e -> e.nameIdentifier().equals(destEntityIdent))
+                .findFirst()
+                .orElseThrow(
+                    () ->
+                        new NoSuchEntityException(
+                            "No such entity with ident: %s", destEntityIdent));
+          }
+
+          // Use allFields=true to cache complete entities
+          List<E> backendEntities =
+              backend.listEntitiesByRelation(relType, srcIdentifier, srcType, 
true);
+
+          E r =
+              backendEntities.stream()
+                  .filter(e -> e.nameIdentifier().equals(destEntityIdent))
+                  .findFirst()
+                  .orElseThrow(
+                      () ->
+                          new NoSuchEntityException(
+                              "No such entity with ident: %s", 
destEntityIdent));
+
+          cache.put(srcIdentifier, srcType, relType, backendEntities);
+
+          return r;
+        });
   }
 
   @Override
diff --git a/core/src/main/java/org/apache/gravitino/tag/TagManager.java 
b/core/src/main/java/org/apache/gravitino/tag/TagManager.java
index 18d258c1ee..867e49d1c1 100644
--- a/core/src/main/java/org/apache/gravitino/tag/TagManager.java
+++ b/core/src/main/java/org/apache/gravitino/tag/TagManager.java
@@ -280,7 +280,7 @@ public class TagManager implements TagDispatcher {
                     entityType,
                     tagIdent);
           } catch (NoSuchEntityException e) {
-            if (e.getMessage().contains("No such tag entity")) {
+            if (e.getMessage().contains("No such entity")) {
               throw new NoSuchTagException(
                   e, "Tag %s does not exist for metadata object %s", name, 
metadataObject);
             } else {
diff --git 
a/core/src/test/java/org/apache/gravitino/storage/TestEntityStorage.java 
b/core/src/test/java/org/apache/gravitino/storage/TestEntityStorage.java
index 5ff904eb31..d9044f322f 100644
--- a/core/src/test/java/org/apache/gravitino/storage/TestEntityStorage.java
+++ b/core/src/test/java/org/apache/gravitino/storage/TestEntityStorage.java
@@ -36,6 +36,7 @@ import static 
org.apache.gravitino.file.Fileset.LOCATION_NAME_UNKNOWN;
 import static 
org.apache.gravitino.storage.relational.TestJDBCBackend.createRoleEntity;
 import static 
org.apache.gravitino.storage.relational.TestJDBCBackend.createUserEntity;
 
+import com.github.benmanes.caffeine.cache.Cache;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -65,12 +66,15 @@ import org.apache.gravitino.EntityStore;
 import org.apache.gravitino.EntityStoreFactory;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.Namespace;
+import org.apache.gravitino.SupportsRelationOperations;
 import org.apache.gravitino.authorization.AuthorizationUtils;
 import org.apache.gravitino.authorization.Privilege.Condition;
 import org.apache.gravitino.authorization.Privileges;
 import org.apache.gravitino.authorization.Role;
 import org.apache.gravitino.authorization.SecurableObject;
 import org.apache.gravitino.authorization.SecurableObjects;
+import org.apache.gravitino.cache.CaffeineEntityCache;
+import org.apache.gravitino.cache.EntityCacheRelationKey;
 import org.apache.gravitino.exceptions.NoSuchEntityException;
 import org.apache.gravitino.exceptions.NonEmptyEntityException;
 import org.apache.gravitino.file.Fileset;
@@ -81,6 +85,7 @@ import org.apache.gravitino.meta.BaseMetalake;
 import org.apache.gravitino.meta.CatalogEntity;
 import org.apache.gravitino.meta.ColumnEntity;
 import org.apache.gravitino.meta.FilesetEntity;
+import org.apache.gravitino.meta.GenericEntity;
 import org.apache.gravitino.meta.GenericTableEntity;
 import org.apache.gravitino.meta.GroupEntity;
 import org.apache.gravitino.meta.ModelEntity;
@@ -89,6 +94,7 @@ import org.apache.gravitino.meta.RoleEntity;
 import org.apache.gravitino.meta.SchemaEntity;
 import org.apache.gravitino.meta.SchemaVersion;
 import org.apache.gravitino.meta.TableEntity;
+import org.apache.gravitino.meta.TagEntity;
 import org.apache.gravitino.meta.TopicEntity;
 import org.apache.gravitino.meta.UserEntity;
 import org.apache.gravitino.model.ModelVersion;
@@ -103,6 +109,7 @@ import 
org.apache.gravitino.storage.relational.converters.MySQLExceptionConverte
 import 
org.apache.gravitino.storage.relational.converters.PostgreSQLExceptionConverter;
 import 
org.apache.gravitino.storage.relational.converters.SQLExceptionConverterFactory;
 import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper;
+import org.apache.gravitino.utils.NameIdentifierUtil;
 import org.apache.gravitino.utils.NamespaceUtil;
 import org.apache.ibatis.session.SqlSession;
 import org.junit.jupiter.api.AfterEach;
@@ -2794,6 +2801,246 @@ public class TestEntityStorage {
     }
   }
 
+  @ParameterizedTest
+  @MethodSource("storageProvider")
+  void testTagRelationCache(String type) throws Exception {
+    Config config = Mockito.mock(Config.class);
+    init(type, config);
+
+    AuditInfo auditInfo =
+        
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+    try (EntityStore store = EntityStoreFactory.createEntityStore(config)) {
+      store.initialize(config);
+
+      BaseMetalake metalake =
+          createBaseMakeLake(RandomIdGenerator.INSTANCE.nextId(), "metalake", 
auditInfo);
+      store.put(metalake, false);
+
+      Namespace namespace = NameIdentifierUtil.ofTag("metalake", 
"tag1").namespace();
+      TagEntity tag1 =
+          TagEntity.builder()
+              .withId(RandomIdGenerator.INSTANCE.nextId())
+              .withNamespace(namespace)
+              .withName("tag1")
+              .withAuditInfo(auditInfo)
+              .withProperties(Collections.emptyMap())
+              .build();
+      CatalogEntity catalog =
+          createCatalog(
+              RandomIdGenerator.INSTANCE.nextId(),
+              NamespaceUtil.ofCatalog("metalake"),
+              "catalog",
+              auditInfo);
+
+      store.put(catalog, false);
+      store.put(tag1, false);
+
+      SupportsRelationOperations relationOperations = 
(SupportsRelationOperations) store;
+
+      relationOperations.updateEntityRelations(
+          SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+          catalog.nameIdentifier(),
+          EntityType.CATALOG,
+          new NameIdentifier[] {tag1.nameIdentifier()},
+          new NameIdentifier[] {});
+
+      // Now try to load the relation
+      List<TagEntity> tags =
+          relationOperations.listEntitiesByRelation(
+              SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+              catalog.nameIdentifier(),
+              EntityType.CATALOG,
+              true);
+      Assertions.assertEquals(1, tags.size());
+      Assertions.assertEquals(tag1, tags.get(0));
+
+      // Check whether tags exists in entity store cache
+      RelationalEntityStore relationalEntityStore = (RelationalEntityStore) 
store;
+      CaffeineEntityCache caffeineEntityCache =
+          (CaffeineEntityCache) relationalEntityStore.getCache();
+      Cache<EntityCacheRelationKey, List<Entity>> cache = 
caffeineEntityCache.getCacheData();
+
+      List<Entity> cachedTags =
+          cache.get(
+              EntityCacheRelationKey.of(
+                  catalog.nameIdentifier(),
+                  EntityType.CATALOG,
+                  SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL),
+              k -> null);
+
+      // Check cached tags is correct
+      Assertions.assertNotNull(cachedTags);
+      Assertions.assertEquals(1, cachedTags.size());
+      Assertions.assertEquals(tag1, cachedTags.get(0));
+
+      List<GenericEntity> genericEntities =
+          relationOperations.listEntitiesByRelation(
+              SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+              tag1.nameIdentifier(),
+              EntityType.TAG,
+              true);
+      Assertions.assertEquals(1, genericEntities.size());
+      Assertions.assertEquals(catalog.id(), genericEntities.get(0).id());
+      Assertions.assertEquals(catalog.name(), genericEntities.get(0).name());
+
+      // Now we are going to alter the catalog
+      CatalogEntity updatedCatalog =
+          CatalogEntity.builder()
+              .withId(catalog.id())
+              .withNamespace(catalog.namespace())
+              .withName("newCatalogName")
+              .withAuditInfo(auditInfo)
+              .withComment(catalog.getComment())
+              .withProperties(catalog.getProperties())
+              .withType(catalog.getType())
+              .withProvider(catalog.getProvider())
+              .build();
+      store.update(
+          catalog.nameIdentifier(),
+          CatalogEntity.class,
+          Entity.EntityType.CATALOG,
+          e -> updatedCatalog);
+      // Now try to load the relation again from cache, it should be empty.
+      List<Entity> cachedTagsAfterCatalogUpdate =
+          cache.get(
+              EntityCacheRelationKey.of(
+                  catalog.nameIdentifier(),
+                  EntityType.CATALOG,
+                  SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL),
+              k -> null);
+      Assertions.assertNull(cachedTagsAfterCatalogUpdate);
+
+      List<Entity> cachedTagsByTagAfterCatalogUpdate =
+          cache.get(
+              EntityCacheRelationKey.of(
+                  tag1.nameIdentifier(),
+                  EntityType.TAG,
+                  SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL),
+              k -> null);
+      Assertions.assertNull(cachedTagsByTagAfterCatalogUpdate);
+
+      // Load tags again, it should repopulate the cache
+      List<TagEntity> tagsAfterCatalogUpdate =
+          relationOperations.listEntitiesByRelation(
+              SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+              updatedCatalog.nameIdentifier(),
+              EntityType.CATALOG,
+              true);
+      Assertions.assertEquals(1, tagsAfterCatalogUpdate.size());
+      Assertions.assertEquals(tag1, tagsAfterCatalogUpdate.get(0));
+
+      List<Entity> cachedTagsAfterReload =
+          cache.get(
+              EntityCacheRelationKey.of(
+                  updatedCatalog.nameIdentifier(),
+                  EntityType.CATALOG,
+                  SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL),
+              k -> null);
+      Assertions.assertNotNull(cachedTagsAfterReload);
+      Assertions.assertEquals(1, cachedTagsAfterReload.size());
+      Assertions.assertEquals(tag1, cachedTagsAfterReload.get(0));
+
+      List<GenericEntity> genericEntitiesAfterCatalogUpdate =
+          relationOperations.listEntitiesByRelation(
+              SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+              tag1.nameIdentifier(),
+              EntityType.TAG,
+              true);
+      Assertions.assertEquals(1, genericEntitiesAfterCatalogUpdate.size());
+      Assertions.assertEquals(catalog.id(), 
genericEntitiesAfterCatalogUpdate.get(0).id());
+      Assertions.assertEquals(
+          updatedCatalog.name(), 
genericEntitiesAfterCatalogUpdate.get(0).name());
+
+      List<Entity> cachedTagsByTagAfterReload =
+          cache.get(
+              EntityCacheRelationKey.of(
+                  tag1.nameIdentifier(),
+                  EntityType.TAG,
+                  SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL),
+              k -> null);
+      Assertions.assertNotNull(cachedTagsByTagAfterReload);
+      Assertions.assertEquals(1, cachedTagsByTagAfterReload.size());
+      Assertions.assertEquals(
+          updatedCatalog.id(), ((GenericEntity) 
cachedTagsByTagAfterReload.get(0)).id());
+      Assertions.assertEquals(
+          updatedCatalog.name(), ((GenericEntity) 
cachedTagsByTagAfterReload.get(0)).name());
+
+      // Now try to alter the tag: rename tag1 -> tagChanged.
+      TagEntity updatedTag1 =
+          TagEntity.builder()
+              .withId(tag1.id())
+              .withNamespace(tag1.namespace())
+              .withName("tagChanged")
+              .withAuditInfo(auditInfo)
+              .withProperties(tag1.properties())
+              .build();
+      store.update(tag1.nameIdentifier(), TagEntity.class, 
Entity.EntityType.TAG, e -> updatedTag1);
+
+      // Now try to load the relation again from cache, it should be empty.
+      List<Entity> cachedTagsAfterTagUpdate =
+          cache.get(
+              EntityCacheRelationKey.of(
+                  updatedCatalog.nameIdentifier(),
+                  EntityType.CATALOG,
+                  SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL),
+              k -> null);
+      Assertions.assertNull(cachedTagsAfterTagUpdate);
+
+      List<Entity> cachedEntitiesAfterTagUpdate =
+          cache.get(
+              EntityCacheRelationKey.of(
+                  tag1.nameIdentifier(),
+                  EntityType.TAG,
+                  SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL),
+              k -> null);
+      Assertions.assertNull(cachedEntitiesAfterTagUpdate);
+
+      List<TagEntity> tagsAfterTagUpdate =
+          relationOperations.listEntitiesByRelation(
+              SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+              updatedCatalog.nameIdentifier(),
+              EntityType.CATALOG,
+              true);
+      Assertions.assertEquals(1, tagsAfterTagUpdate.size());
+      Assertions.assertEquals(updatedTag1, tagsAfterTagUpdate.get(0));
+
+      List<Entity> cachedTagsAfterTagReload =
+          cache.get(
+              EntityCacheRelationKey.of(
+                  updatedCatalog.nameIdentifier(),
+                  EntityType.CATALOG,
+                  SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL),
+              k -> null);
+      Assertions.assertNotNull(cachedTagsAfterTagReload);
+      Assertions.assertEquals(1, cachedTagsAfterTagReload.size());
+      Assertions.assertEquals(updatedTag1, cachedTagsAfterTagReload.get(0));
+
+      List<GenericEntity> genericEntitiesAfterTagUpdate =
+          relationOperations.listEntitiesByRelation(
+              SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL,
+              updatedTag1.nameIdentifier(),
+              EntityType.TAG,
+              true);
+      Assertions.assertEquals(1, genericEntitiesAfterTagUpdate.size());
+      Assertions.assertEquals(catalog.id(), 
genericEntitiesAfterTagUpdate.get(0).id());
+      Assertions.assertEquals(updatedCatalog.name(), 
genericEntitiesAfterTagUpdate.get(0).name());
+      List<Entity> cachedTagsByTagAfterTagReload =
+          cache.get(
+              EntityCacheRelationKey.of(
+                  updatedTag1.nameIdentifier(),
+                  EntityType.TAG,
+                  SupportsRelationOperations.Type.TAG_METADATA_OBJECT_REL),
+              k -> null);
+      Assertions.assertNotNull(cachedTagsByTagAfterTagReload);
+      Assertions.assertEquals(1, cachedTagsByTagAfterTagReload.size());
+      Assertions.assertEquals(
+          updatedCatalog.id(), ((GenericEntity) 
cachedTagsByTagAfterTagReload.get(0)).id());
+      Assertions.assertEquals(
+          updatedCatalog.name(), ((GenericEntity) 
cachedTagsByTagAfterTagReload.get(0)).name());
+    }
+  }
+
   @ParameterizedTest
   @MethodSource("storageProvider")
   void testLanceTableCreateAndUpdate(String type) {

Reply via email to