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

yuqi1129 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 fc2b7c6eef [#10915] feat(cache): Add auth cache mappers (#10913)
fc2b7c6eef is described below

commit fc2b7c6eefd3e0bf756f1356e0cd76b0a6093cc6
Author: Qi Yu <[email protected]>
AuthorDate: Fri May 8 11:09:02 2026 +0800

    [#10915] feat(cache): Add auth cache mappers (#10913)
    
    ### What changes were proposed in this pull request?
    
    Add auth-related mapper methods, provider SQL, auth PO classes,
    write-path updated_at touches, and focused mapper tests.
    
    This PR is stacked on #10912.
    
    ### Why are the changes needed?
    
    This is the auth mapper foundation for cache improvement. It depends on
    the 1.3.0 schema PR and is independent from the entity change log PR.
    
    Fix: #10915
    
    ### Does this PR introduce _any_ user-facing change?
    
    No user-facing API change. This adds internal relational mapper support
    for auth cache consistency.
    
    ### How was this patch tested?
    
    ./gradlew :core:test --tests
    org.apache.gravitino.storage.relational.mapper.provider.base.TestAuthMappers
---
 .../storage/relational/mapper/OwnerMetaMapper.java |  12 +
 .../mapper/OwnerMetaSQLProviderFactory.java        |  10 +
 .../storage/relational/mapper/RoleMetaMapper.java  |   7 +
 .../mapper/RoleMetaSQLProviderFactory.java         |   9 +
 .../storage/relational/mapper/UserMetaMapper.java  |   8 +
 .../mapper/UserMetaSQLProviderFactory.java         |   9 +
 .../provider/base/OwnerMetaBaseSQLProvider.java    |  26 +-
 .../provider/base/RoleMetaBaseSQLProvider.java     |  21 ++
 .../provider/base/UserMetaBaseSQLProvider.java     |  21 ++
 .../postgresql/RoleMetaPostgreSQLProvider.java     |   8 +
 .../postgresql/UserMetaPostgreSQLProvider.java     |   8 +
 .../storage/relational/po/OwnerRelPO.java          |   7 +
 .../relational/po/auth/ChangedOwnerInfo.java       |  35 ++
 .../storage/relational/po/auth/OwnerInfo.java      |  34 ++
 .../storage/relational/po/auth/RoleUpdatedAt.java  |  40 ++
 .../storage/relational/po/auth/UserUpdatedAt.java  |  34 ++
 .../relational/service/RoleMetaService.java        |   5 +-
 .../relational/service/UserMetaService.java        |   6 +-
 .../storage/relational/utils/POConverters.java     |   1 +
 .../mapper/provider/base/TestAuthMappers.java      | 406 +++++++++++++++++++++
 .../relational/service/TestRoleMetaService.java    |  18 +
 .../relational/service/TestUserMetaService.java    |  18 +
 22 files changed, 739 insertions(+), 4 deletions(-)

diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaMapper.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaMapper.java
index 17247163e2..3a2d0c459b 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaMapper.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaMapper.java
@@ -23,6 +23,8 @@ import org.apache.gravitino.storage.relational.po.GroupPO;
 import org.apache.gravitino.storage.relational.po.OwnerRelPO;
 import org.apache.gravitino.storage.relational.po.UserOwnerRelPO;
 import org.apache.gravitino.storage.relational.po.UserPO;
+import org.apache.gravitino.storage.relational.po.auth.ChangedOwnerInfo;
+import org.apache.gravitino.storage.relational.po.auth.OwnerInfo;
 import org.apache.ibatis.annotations.InsertProvider;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.SelectProvider;
@@ -95,4 +97,14 @@ public interface OwnerMetaMapper {
       method = "deleteOwnerMetasByLegacyTimeline")
   Integer deleteOwnerMetasByLegacyTimeline(
       @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit);
+
+  @SelectProvider(
+      type = OwnerMetaSQLProviderFactory.class,
+      method = "selectOwnerByMetadataObjectIdAndType")
+  OwnerInfo selectOwnerByMetadataObjectIdAndType(
+      @Param("metadataObjectId") long metadataObjectId,
+      @Param("metadataObjectType") String metadataObjectType);
+
+  @SelectProvider(type = OwnerMetaSQLProviderFactory.class, method = 
"selectChangedOwners")
+  List<ChangedOwnerInfo> selectChangedOwners(@Param("updatedAtFrom") long 
updatedAtFrom);
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaSQLProviderFactory.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaSQLProviderFactory.java
index 0dfcdc5bed..a710583abf 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaSQLProviderFactory.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaSQLProviderFactory.java
@@ -104,4 +104,14 @@ public class OwnerMetaSQLProviderFactory {
     return getProvider()
         .batchSelectUserOwnerMetaByMetadataObjectIdAndType(metadataObjectIds, 
metadataObjectType);
   }
+
+  public static String selectOwnerByMetadataObjectIdAndType(
+      @Param("metadataObjectId") long metadataObjectId,
+      @Param("metadataObjectType") String metadataObjectType) {
+    return 
getProvider().selectOwnerByMetadataObjectIdAndType(metadataObjectId, 
metadataObjectType);
+  }
+
+  public static String selectChangedOwners(@Param("updatedAtFrom") long 
updatedAtFrom) {
+    return getProvider().selectChangedOwners(updatedAtFrom);
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/RoleMetaMapper.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/RoleMetaMapper.java
index e32cdbeec5..78fcc921d2 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/RoleMetaMapper.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/RoleMetaMapper.java
@@ -21,6 +21,7 @@ package org.apache.gravitino.storage.relational.mapper;
 
 import java.util.List;
 import org.apache.gravitino.storage.relational.po.RolePO;
+import org.apache.gravitino.storage.relational.po.auth.RoleUpdatedAt;
 import org.apache.ibatis.annotations.DeleteProvider;
 import org.apache.ibatis.annotations.InsertProvider;
 import org.apache.ibatis.annotations.Param;
@@ -93,4 +94,10 @@ public interface RoleMetaMapper {
       method = "deleteRoleMetasByLegacyTimeline")
   Integer deleteRoleMetasByLegacyTimeline(
       @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit);
+
+  @UpdateProvider(type = RoleMetaSQLProviderFactory.class, method = 
"touchRoleUpdatedAt")
+  void touchRoleUpdatedAt(@Param("roleId") long roleId);
+
+  @SelectProvider(type = RoleMetaSQLProviderFactory.class, method = 
"batchGetRoleUpdatedAt")
+  List<RoleUpdatedAt> batchGetRoleUpdatedAt(@Param("roleIds") List<Long> 
roleIds);
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/RoleMetaSQLProviderFactory.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/RoleMetaSQLProviderFactory.java
index 04eb348d87..649c897f68 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/RoleMetaSQLProviderFactory.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/RoleMetaSQLProviderFactory.java
@@ -19,6 +19,7 @@
 package org.apache.gravitino.storage.relational.mapper;
 
 import com.google.common.collect.ImmutableMap;
+import java.util.List;
 import java.util.Map;
 import org.apache.gravitino.storage.relational.JDBCBackend.JDBCBackendType;
 import 
org.apache.gravitino.storage.relational.mapper.provider.base.RoleMetaBaseSQLProvider;
@@ -101,4 +102,12 @@ public class RoleMetaSQLProviderFactory {
       @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit) 
{
     return getProvider().deleteRoleMetasByLegacyTimeline(legacyTimeline, 
limit);
   }
+
+  public static String touchRoleUpdatedAt(@Param("roleId") long roleId) {
+    return getProvider().touchRoleUpdatedAt(roleId);
+  }
+
+  public static String batchGetRoleUpdatedAt(@Param("roleIds") List<Long> 
roleIds) {
+    return getProvider().batchGetRoleUpdatedAt(roleIds);
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaMapper.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaMapper.java
index 19bbb4eddd..498a90020f 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaMapper.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaMapper.java
@@ -22,6 +22,7 @@ package org.apache.gravitino.storage.relational.mapper;
 import java.util.List;
 import org.apache.gravitino.storage.relational.po.ExtendedUserPO;
 import org.apache.gravitino.storage.relational.po.UserPO;
+import org.apache.gravitino.storage.relational.po.auth.UserUpdatedAt;
 import org.apache.ibatis.annotations.DeleteProvider;
 import org.apache.ibatis.annotations.InsertProvider;
 import org.apache.ibatis.annotations.Param;
@@ -88,4 +89,11 @@ public interface UserMetaMapper {
       method = "deleteUserMetasByLegacyTimeline")
   Integer deleteUserMetasByLegacyTimeline(
       @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit);
+
+  @UpdateProvider(type = UserMetaSQLProviderFactory.class, method = 
"touchUserUpdatedAt")
+  void touchUserUpdatedAt(@Param("userId") long userId);
+
+  @SelectProvider(type = UserMetaSQLProviderFactory.class, method = 
"getUserUpdatedAt")
+  UserUpdatedAt getUserUpdatedAt(
+      @Param("metalakeName") String metalakeName, @Param("userName") String 
userName);
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaSQLProviderFactory.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaSQLProviderFactory.java
index 1d7fdb45a9..0c164899dd 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaSQLProviderFactory.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaSQLProviderFactory.java
@@ -97,4 +97,13 @@ public class UserMetaSQLProviderFactory {
       @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit) 
{
     return getProvider().deleteUserMetasByLegacyTimeline(legacyTimeline, 
limit);
   }
+
+  public static String touchUserUpdatedAt(@Param("userId") long userId) {
+    return getProvider().touchUserUpdatedAt(userId);
+  }
+
+  public static String getUserUpdatedAt(
+      @Param("metalakeName") String metalakeName, @Param("userName") String 
userName) {
+    return getProvider().getUserUpdatedAt(metalakeName, userName);
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/OwnerMetaBaseSQLProvider.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/OwnerMetaBaseSQLProvider.java
index 478aa8f618..27bcea1b92 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/OwnerMetaBaseSQLProvider.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/OwnerMetaBaseSQLProvider.java
@@ -109,7 +109,7 @@ public class OwnerMetaBaseSQLProvider {
     return "INSERT INTO "
         + OWNER_TABLE_NAME
         + " (metalake_id, metadata_object_id, metadata_object_type, owner_id, 
owner_type,"
-        + " audit_info, current_version, last_version, deleted_at)"
+        + " audit_info, current_version, last_version, deleted_at, updated_at)"
         + " VALUES ("
         + " #{ownerRelPO.metalakeId},"
         + " #{ownerRelPO.metadataObjectId},"
@@ -119,7 +119,8 @@ public class OwnerMetaBaseSQLProvider {
         + " #{ownerRelPO.auditInfo},"
         + " #{ownerRelPO.currentVersion},"
         + " #{ownerRelPO.lastVersion},"
-        + " #{ownerRelPO.deletedAt}"
+        + " #{ownerRelPO.deletedAt},"
+        + " #{ownerRelPO.updatedAt}"
         + ")";
   }
 
@@ -247,4 +248,25 @@ public class OwnerMetaBaseSQLProvider {
         + OWNER_TABLE_NAME
         + " WHERE deleted_at > 0 AND deleted_at < #{legacyTimeline} LIMIT 
#{limit}";
   }
+
+  public String selectOwnerByMetadataObjectIdAndType(
+      @Param("metadataObjectId") long metadataObjectId,
+      @Param("metadataObjectType") String metadataObjectType) {
+    return "SELECT owner_id as ownerId, owner_type as ownerType FROM "
+        + OWNER_TABLE_NAME
+        + " WHERE metadata_object_id = #{metadataObjectId}"
+        + " AND metadata_object_type = #{metadataObjectType}"
+        + " AND deleted_at = 0"
+        + " ORDER BY updated_at DESC, id DESC LIMIT 1";
+  }
+
+  public String selectChangedOwners(@Param("updatedAtFrom") long 
updatedAtFrom) {
+    return "SELECT metadata_object_id as metadataObjectId,"
+        + " metadata_object_type as metadataObjectType,"
+        + " updated_at as updatedAt"
+        + " FROM "
+        + OWNER_TABLE_NAME
+        + " WHERE deleted_at = 0 AND updated_at >= #{updatedAtFrom}"
+        + " ORDER BY updated_at, id LIMIT 1000";
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/RoleMetaBaseSQLProvider.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/RoleMetaBaseSQLProvider.java
index 1035665d5e..d3a7127a32 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/RoleMetaBaseSQLProvider.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/RoleMetaBaseSQLProvider.java
@@ -22,6 +22,7 @@ import static 
org.apache.gravitino.storage.relational.mapper.RoleMetaMapper.GROU
 import static 
org.apache.gravitino.storage.relational.mapper.RoleMetaMapper.ROLE_TABLE_NAME;
 import static 
org.apache.gravitino.storage.relational.mapper.RoleMetaMapper.USER_ROLE_RELATION_TABLE_NAME;
 
+import java.util.List;
 import org.apache.gravitino.storage.relational.mapper.MetalakeMetaMapper;
 import org.apache.gravitino.storage.relational.mapper.SecurableObjectMapper;
 import org.apache.gravitino.storage.relational.po.RolePO;
@@ -192,4 +193,24 @@ public class RoleMetaBaseSQLProvider {
         + ROLE_TABLE_NAME
         + " WHERE deleted_at > 0 AND deleted_at < #{legacyTimeline} LIMIT 
#{limit}";
   }
+
+  public String touchRoleUpdatedAt(@Param("roleId") long roleId) {
+    return "UPDATE "
+        + ROLE_TABLE_NAME
+        + " SET updated_at = (UNIX_TIMESTAMP() * 1000.0)"
+        + " + EXTRACT(MICROSECOND FROM CURRENT_TIMESTAMP(3)) / 1000"
+        + " WHERE role_id = #{roleId} AND deleted_at = 0";
+  }
+
+  public String batchGetRoleUpdatedAt(@Param("roleIds") List<Long> roleIds) {
+    if (roleIds == null || roleIds.isEmpty()) {
+      return "SELECT role_id as roleId, role_name as roleName, updated_at as 
updatedAt FROM "
+          + ROLE_TABLE_NAME
+          + " WHERE 1 = 0";
+    }
+    return "<script>SELECT role_id as roleId, role_name as roleName, 
updated_at as updatedAt FROM "
+        + ROLE_TABLE_NAME
+        + " WHERE role_id IN <foreach item='id' collection='roleIds' open='(' 
separator=',' close=')'>#{id}</foreach>"
+        + " AND deleted_at = 0</script>";
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/UserMetaBaseSQLProvider.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/UserMetaBaseSQLProvider.java
index 260a6c2188..2842edd332 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/UserMetaBaseSQLProvider.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/UserMetaBaseSQLProvider.java
@@ -187,4 +187,25 @@ public class UserMetaBaseSQLProvider {
         + USER_TABLE_NAME
         + " WHERE deleted_at > 0 AND deleted_at < #{legacyTimeline} LIMIT 
#{limit}";
   }
+
+  public String touchUserUpdatedAt(@Param("userId") long userId) {
+    return "UPDATE "
+        + USER_TABLE_NAME
+        + " SET updated_at = (UNIX_TIMESTAMP() * 1000.0)"
+        + " + EXTRACT(MICROSECOND FROM CURRENT_TIMESTAMP(3)) / 1000"
+        + " WHERE user_id = #{userId} AND deleted_at = 0";
+  }
+
+  public String getUserUpdatedAt(
+      @Param("metalakeName") String metalakeName, @Param("userName") String 
userName) {
+    return "SELECT um.user_id as userId, um.updated_at as updatedAt"
+        + " FROM "
+        + USER_TABLE_NAME
+        + " um"
+        + " JOIN "
+        + MetalakeMetaMapper.TABLE_NAME
+        + " mm ON um.metalake_id = mm.metalake_id AND mm.deleted_at = 0"
+        + " WHERE mm.metalake_name = #{metalakeName} AND um.user_name = 
#{userName}"
+        + " AND um.deleted_at = 0";
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/RoleMetaPostgreSQLProvider.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/RoleMetaPostgreSQLProvider.java
index 676b3e47fe..44de0a3bab 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/RoleMetaPostgreSQLProvider.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/RoleMetaPostgreSQLProvider.java
@@ -76,4 +76,12 @@ public class RoleMetaPostgreSQLProvider extends 
RoleMetaBaseSQLProvider {
         + ROLE_TABLE_NAME
         + " WHERE deleted_at > 0 AND deleted_at < #{legacyTimeline} LIMIT 
#{limit})";
   }
+
+  @Override
+  public String touchRoleUpdatedAt(@Param("roleId") long roleId) {
+    return "UPDATE "
+        + ROLE_TABLE_NAME
+        + " SET updated_at = CAST(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000 
AS BIGINT)"
+        + " WHERE role_id = #{roleId} AND deleted_at = 0";
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/UserMetaPostgreSQLProvider.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/UserMetaPostgreSQLProvider.java
index 73730cafef..a85a1610ab 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/UserMetaPostgreSQLProvider.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/UserMetaPostgreSQLProvider.java
@@ -104,4 +104,12 @@ public class UserMetaPostgreSQLProvider extends 
UserMetaBaseSQLProvider {
         + USER_TABLE_NAME
         + " WHERE deleted_at > 0 AND deleted_at < #{legacyTimeline} LIMIT 
#{limit})";
   }
+
+  @Override
+  public String touchUserUpdatedAt(@Param("userId") long userId) {
+    return "UPDATE "
+        + USER_TABLE_NAME
+        + " SET updated_at = CAST(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000 
AS BIGINT)"
+        + " WHERE user_id = #{userId} AND deleted_at = 0";
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/po/OwnerRelPO.java 
b/core/src/main/java/org/apache/gravitino/storage/relational/po/OwnerRelPO.java
index 615fe592c1..000f3bce08 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/po/OwnerRelPO.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/po/OwnerRelPO.java
@@ -35,6 +35,7 @@ public class OwnerRelPO {
   private Long currentVersion;
   private Long lastVersion;
   private Long deletedAt;
+  private Long updatedAt;
 
   private OwnerRelPO() {}
 
@@ -94,6 +95,11 @@ public class OwnerRelPO {
       return this;
     }
 
+    public Builder withUpdatedAt(Long updatedAt) {
+      ownerRelPO.updatedAt = updatedAt;
+      return this;
+    }
+
     public OwnerRelPO build() {
       validate();
       return ownerRelPO;
@@ -111,6 +117,7 @@ public class OwnerRelPO {
       Preconditions.checkArgument(ownerRelPO.currentVersion != null, "Current 
version is required");
       Preconditions.checkArgument(ownerRelPO.lastVersion != null, "Last 
version is required");
       Preconditions.checkArgument(ownerRelPO.deletedAt != null, "Deleted at is 
required");
+      Preconditions.checkArgument(ownerRelPO.updatedAt != null, "Updated at is 
required");
     }
   }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/po/auth/ChangedOwnerInfo.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/po/auth/ChangedOwnerInfo.java
new file mode 100644
index 0000000000..16f93d5fd8
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/po/auth/ChangedOwnerInfo.java
@@ -0,0 +1,35 @@
+/*
+ * 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.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/** Owner change poller result -- one row per changed owner_meta entry. */
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class ChangedOwnerInfo {
+  private long metadataObjectId;
+  private String metadataObjectType;
+  private long updatedAt;
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/po/auth/OwnerInfo.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/po/auth/OwnerInfo.java
new file mode 100644
index 0000000000..4ac4e0c9ef
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/po/auth/OwnerInfo.java
@@ -0,0 +1,34 @@
+/*
+ * 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.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/** Owner identity for a single metadata object. */
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class OwnerInfo {
+  private long ownerId;
+  private String ownerType;
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/po/auth/RoleUpdatedAt.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/po/auth/RoleUpdatedAt.java
new file mode 100644
index 0000000000..c2a55b674c
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/po/auth/RoleUpdatedAt.java
@@ -0,0 +1,40 @@
+/*
+ * 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.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/** Role identity and privilege-list staleness sentinel. */
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class RoleUpdatedAt {
+  private long roleId;
+  private String roleName;
+  private long updatedAt;
+
+  public RoleUpdatedAt(long roleId, long updatedAt) {
+    this.roleId = roleId;
+    this.updatedAt = updatedAt;
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/po/auth/UserUpdatedAt.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/po/auth/UserUpdatedAt.java
new file mode 100644
index 0000000000..d572a30449
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/po/auth/UserUpdatedAt.java
@@ -0,0 +1,34 @@
+/*
+ * 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.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/** User identity + role-list staleness sentinel. */
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class UserUpdatedAt {
+  private long userId;
+  private long updatedAt;
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/RoleMetaService.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/RoleMetaService.java
index d0fe1db7dd..803d4949df 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/RoleMetaService.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/RoleMetaService.java
@@ -259,7 +259,10 @@ public class RoleMetaService {
             SessionUtils.doWithoutCommit(
                 SecurableObjectMapper.class,
                 mapper -> 
mapper.batchInsertSecurableObjects(insertSecurableObjectPOs));
-          });
+          },
+          () ->
+              SessionUtils.doWithoutCommit(
+                  RoleMetaMapper.class, mapper -> 
mapper.touchRoleUpdatedAt(rolePO.getRoleId())));
 
       return newRoleEntity;
     } catch (RuntimeException re) {
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/UserMetaService.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/UserMetaService.java
index 709545a53b..bb810b1811 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/UserMetaService.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/UserMetaService.java
@@ -252,7 +252,11 @@ public class UserMetaService {
                 mapper ->
                     mapper.softDeleteUserRoleRelByUserAndRoles(
                         newEntity.id(), Lists.newArrayList(deleteRoleIds)));
-          });
+          },
+          () ->
+              SessionUtils.doWithoutCommit(
+                  UserMetaMapper.class,
+                  mapper -> mapper.touchUserUpdatedAt(oldUserPO.getUserId())));
     } catch (RuntimeException re) {
       ExceptionUtils.checkSQLException(
           re, Entity.EntityType.USER, newEntity.nameIdentifier().toString());
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/utils/POConverters.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/utils/POConverters.java
index 40cd0301e6..ae69ce2797 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/utils/POConverters.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/utils/POConverters.java
@@ -1575,6 +1575,7 @@ public class POConverters {
           .withCurrentVersion(INIT_VERSION)
           .withLastVersion(INIT_VERSION)
           .withDeleteAt(DEFAULT_DELETED_AT)
+          .withUpdatedAt(System.currentTimeMillis())
           .build();
     } catch (JsonProcessingException e) {
       throw new RuntimeException("Failed to serialize json object:", e);
diff --git 
a/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestAuthMappers.java
 
b/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestAuthMappers.java
new file mode 100644
index 0000000000..cd4e6fcf98
--- /dev/null
+++ 
b/core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestAuthMappers.java
@@ -0,0 +1,406 @@
+/*
+ * 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.mapper.provider.base;
+
+import com.google.common.collect.Lists;
+import java.io.File;
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+import org.apache.commons.io.FileUtils;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.Configs;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.storage.relational.JDBCBackend;
+import org.apache.gravitino.storage.relational.mapper.GroupMetaMapper;
+import org.apache.gravitino.storage.relational.mapper.MetalakeMetaMapper;
+import org.apache.gravitino.storage.relational.mapper.OwnerMetaMapper;
+import org.apache.gravitino.storage.relational.mapper.RoleMetaMapper;
+import org.apache.gravitino.storage.relational.mapper.UserMetaMapper;
+import org.apache.gravitino.storage.relational.po.GroupPO;
+import org.apache.gravitino.storage.relational.po.MetalakePO;
+import org.apache.gravitino.storage.relational.po.OwnerRelPO;
+import org.apache.gravitino.storage.relational.po.RolePO;
+import org.apache.gravitino.storage.relational.po.UserPO;
+import org.apache.gravitino.storage.relational.po.auth.ChangedOwnerInfo;
+import org.apache.gravitino.storage.relational.po.auth.OwnerInfo;
+import org.apache.gravitino.storage.relational.po.auth.RoleUpdatedAt;
+import org.apache.gravitino.storage.relational.po.auth.UserUpdatedAt;
+import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper;
+import org.apache.ibatis.session.SqlSession;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+public class TestAuthMappers {
+
+  private static final String JDBC_STORE_PATH =
+      "/tmp/gravitino_jdbc_authMappers_" + 
UUID.randomUUID().toString().replace("-", "");
+  private static final String DB_DIR = JDBC_STORE_PATH + "/testdb";
+  private static JDBCBackend backend;
+  private static RoleMetaMapper roleMetaMapper;
+  private static UserMetaMapper userMetaMapper;
+  private static GroupMetaMapper groupMetaMapper;
+  private static OwnerMetaMapper ownerMetaMapper;
+  private static MetalakeMetaMapper metalakeMetaMapper;
+  private static SqlSession sharedSession;
+
+  @BeforeAll
+  public static void setup() throws Exception {
+    File dir = new File(DB_DIR);
+    if (dir.exists()) {
+      FileUtils.deleteDirectory(dir);
+    }
+    dir.mkdirs();
+
+    Config config = Mockito.mock(Config.class);
+    Mockito.when(config.get(Configs.ENTITY_STORE)).thenReturn("relational");
+    Mockito.when(config.get(Configs.ENTITY_RELATIONAL_STORE)).thenReturn("h2");
+    Mockito.when(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL))
+        
.thenReturn(String.format("jdbc:h2:file:%s;DB_CLOSE_DELAY=-1;MODE=MYSQL", 
DB_DIR));
+    
Mockito.when(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_USER)).thenReturn("gravitino");
+    Mockito.when(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD))
+        .thenReturn("gravitino");
+    Mockito.when(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER))
+        .thenReturn("org.h2.Driver");
+    
Mockito.when(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS)).thenReturn(20);
+    
Mockito.when(config.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS))
+        .thenReturn(1000L);
+
+    backend = new JDBCBackend();
+    backend.initialize(config);
+
+    sharedSession = 
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
+    roleMetaMapper = sharedSession.getMapper(RoleMetaMapper.class);
+    userMetaMapper = sharedSession.getMapper(UserMetaMapper.class);
+    groupMetaMapper = sharedSession.getMapper(GroupMetaMapper.class);
+    ownerMetaMapper = sharedSession.getMapper(OwnerMetaMapper.class);
+    metalakeMetaMapper = sharedSession.getMapper(MetalakeMetaMapper.class);
+  }
+
+  @AfterAll
+  public static void tearDown() throws IOException {
+    if (sharedSession != null) {
+      sharedSession.close();
+    }
+    if (backend != null) {
+      backend.close();
+    }
+    File dir = new File(JDBC_STORE_PATH);
+    if (dir.exists()) {
+      FileUtils.deleteDirectory(dir);
+    }
+  }
+
+  @BeforeEach
+  public void truncateTables() {
+    try (SqlSession sqlSession =
+        
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true)) 
{
+      try (Connection connection = sqlSession.getConnection()) {
+        try (Statement statement = connection.createStatement()) {
+          statement.execute("TRUNCATE TABLE entity_change_log");
+          statement.execute("TRUNCATE TABLE owner_meta");
+          statement.execute("TRUNCATE TABLE user_role_rel");
+          statement.execute("TRUNCATE TABLE group_role_rel");
+          statement.execute("TRUNCATE TABLE role_meta_securable_object");
+          statement.execute("TRUNCATE TABLE user_meta");
+          statement.execute("TRUNCATE TABLE group_meta");
+          statement.execute("TRUNCATE TABLE role_meta");
+          statement.execute("TRUNCATE TABLE metalake_meta");
+        }
+      }
+    } catch (SQLException e) {
+      throw new RuntimeException("Truncate table failed", e);
+    }
+  }
+
+  @Test
+  void testRoleMetaTouchUpdatedAt() {
+    insertMetalake(1L, "metalake1");
+    insertRole(10L, "role10", 1L);
+
+    long before = queryUpdatedAt("role_meta", "role_id", 10L);
+    Assertions.assertEquals(0L, before);
+
+    long jvmBefore = System.currentTimeMillis();
+    roleMetaMapper.touchRoleUpdatedAt(10L);
+    long jvmAfter = System.currentTimeMillis();
+
+    long after = queryUpdatedAt("role_meta", "role_id", 10L);
+    // updated_at is set by the DB, so it should be close to JVM time but not 
exactly equal.
+    Assertions.assertTrue(
+        after >= jvmBefore - 1000L && after <= jvmAfter + 1000L,
+        "expected DB-time updated_at within 1s of JVM clock, got " + after);
+  }
+
+  @Test
+  void testRoleMetaBatchGetUpdatedAt() {
+    insertMetalake(1L, "metalake1");
+    insertRole(11L, "role11", 1L);
+    insertRole(12L, "role12", 1L);
+
+    roleMetaMapper.touchRoleUpdatedAt(11L);
+    roleMetaMapper.touchRoleUpdatedAt(12L);
+
+    List<Long> roleIds = Lists.newArrayList(11L, 12L);
+    List<RoleUpdatedAt> results = 
roleMetaMapper.batchGetRoleUpdatedAt(roleIds);
+
+    Assertions.assertEquals(2, results.size());
+    results.sort((a, b) -> Long.compare(a.getRoleId(), b.getRoleId()));
+    Assertions.assertEquals(11L, results.get(0).getRoleId());
+    Assertions.assertEquals(12L, results.get(1).getRoleId());
+    Assertions.assertTrue(results.get(0).getUpdatedAt() > 0L);
+    Assertions.assertTrue(results.get(1).getUpdatedAt() > 0L);
+
+    
Assertions.assertTrue(roleMetaMapper.batchGetRoleUpdatedAt(Collections.emptyList()).isEmpty());
+    
Assertions.assertTrue(roleMetaMapper.batchGetRoleUpdatedAt(null).isEmpty());
+  }
+
+  @Test
+  void testRoleMetaTouchUpdatedAtSkipsSoftDeleted() {
+    insertMetalake(1L, "metalake1");
+    insertRole(13L, "role13", 1L);
+    roleMetaMapper.softDeleteRoleMetaByRoleId(13L);
+
+    long beforeUpdatedAt = queryUpdatedAt("role_meta", "role_id", 13L);
+    roleMetaMapper.touchRoleUpdatedAt(13L);
+    long afterUpdatedAt = queryUpdatedAt("role_meta", "role_id", 13L);
+
+    Assertions.assertEquals(beforeUpdatedAt, afterUpdatedAt);
+  }
+
+  @Test
+  void testUserMetaTouchUpdatedAt() {
+    insertMetalake(1L, "metalake1");
+    insertUser(20L, "user20", 1L);
+
+    long before = queryUpdatedAt("user_meta", "user_id", 20L);
+    Assertions.assertEquals(0L, before);
+
+    long jvmBefore = System.currentTimeMillis();
+    userMetaMapper.touchUserUpdatedAt(20L);
+    long jvmAfter = System.currentTimeMillis();
+
+    long after = queryUpdatedAt("user_meta", "user_id", 20L);
+    Assertions.assertTrue(
+        after >= jvmBefore - 1000L && after <= jvmAfter + 1000L,
+        "expected DB-time updated_at within 1s of JVM clock, got " + after);
+  }
+
+  @Test
+  void testUserMetaTouchUpdatedAtSkipsSoftDeleted() {
+    insertMetalake(1L, "metalake1");
+    insertUser(22L, "user22", 1L);
+    userMetaMapper.softDeleteUserMetaByUserId(22L);
+
+    long beforeUpdatedAt = queryUpdatedAt("user_meta", "user_id", 22L);
+    userMetaMapper.touchUserUpdatedAt(22L);
+    long afterUpdatedAt = queryUpdatedAt("user_meta", "user_id", 22L);
+
+    Assertions.assertEquals(beforeUpdatedAt, afterUpdatedAt);
+  }
+
+  @Test
+  void testUserMetaGetUserUpdatedAt() {
+    insertMetalake(1L, "metalake1");
+    insertUser(21L, "user21", 1L);
+
+    userMetaMapper.touchUserUpdatedAt(21L);
+    long expected = queryUpdatedAt("user_meta", "user_id", 21L);
+
+    UserUpdatedAt info = userMetaMapper.getUserUpdatedAt("metalake1", 
"user21");
+    Assertions.assertNotNull(info);
+    Assertions.assertEquals(21L, info.getUserId());
+    Assertions.assertEquals(expected, info.getUpdatedAt());
+  }
+
+  @Test
+  void testOwnerMetaSelectOwnerByMetadataObjectIdAndType() {
+    insertMetalake(1L, "metalake1");
+    insertUser(50L, "user50", 1L);
+    AuditInfo auditInfo = buildAuditInfo();
+
+    OwnerRelPO ownerRelPO =
+        OwnerRelPO.builder()
+            .withMetalakeId(1L)
+            .withOwnerId(50L)
+            .withOwnerType("USER")
+            .withMetadataObjectId(100L)
+            .withMetadataObjectType("TABLE")
+            .withAuditIfo(auditInfo.toString())
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeleteAt(0L)
+            .withUpdatedAt(0L)
+            .build();
+    ownerMetaMapper.insertOwnerRel(ownerRelPO);
+
+    OwnerInfo info = 
ownerMetaMapper.selectOwnerByMetadataObjectIdAndType(100L, "TABLE");
+    Assertions.assertNotNull(info);
+    Assertions.assertEquals(50L, info.getOwnerId());
+    Assertions.assertEquals("USER", info.getOwnerType());
+
+    // ID collision with different type is filtered out.
+    
Assertions.assertNull(ownerMetaMapper.selectOwnerByMetadataObjectIdAndType(100L,
 "SCHEMA"));
+  }
+
+  @Test
+  void testOwnerMetaSelectChangedOwners() {
+    insertMetalake(1L, "metalake1");
+    insertUser(60L, "user60", 1L);
+    AuditInfo auditInfo = buildAuditInfo();
+
+    OwnerRelPO ownerRelPO =
+        OwnerRelPO.builder()
+            .withMetalakeId(1L)
+            .withOwnerId(60L)
+            .withOwnerType("USER")
+            .withMetadataObjectId(200L)
+            .withMetadataObjectType("SCHEMA")
+            .withAuditIfo(auditInfo.toString())
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeleteAt(0L)
+            .withUpdatedAt(0L)
+            .build();
+    ownerMetaMapper.insertOwnerRel(ownerRelPO);
+
+    // Set updated_at = 100 via direct SQL
+    try (SqlSession sqlSession =
+        
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true)) 
{
+      try (Connection connection = sqlSession.getConnection()) {
+        try (Statement statement = connection.createStatement()) {
+          statement.execute(
+              "UPDATE owner_meta SET updated_at = 100 WHERE metadata_object_id 
= 200");
+        }
+      }
+    } catch (SQLException e) {
+      throw new RuntimeException("Update failed", e);
+    }
+
+    List<ChangedOwnerInfo> changed = ownerMetaMapper.selectChangedOwners(50L);
+    Assertions.assertEquals(1, changed.size());
+    Assertions.assertEquals(200L, changed.get(0).getMetadataObjectId());
+    Assertions.assertEquals("SCHEMA", changed.get(0).getMetadataObjectType());
+    Assertions.assertEquals(100L, changed.get(0).getUpdatedAt());
+
+    // With the same timestamp, the row is returned again for timestamp-only 
polling.
+    List<ChangedOwnerInfo> sameTimestamp = 
ownerMetaMapper.selectChangedOwners(100L);
+    Assertions.assertEquals(1, sameTimestamp.size());
+  }
+
+  private AuditInfo buildAuditInfo() {
+    return 
AuditInfo.builder().withCreator("test").withCreateTime(Instant.now()).build();
+  }
+
+  private MetalakePO insertMetalake(long metalakeId, String metalakeName) {
+    AuditInfo auditInfo = buildAuditInfo();
+    MetalakePO metalakePO =
+        MetalakePO.builder()
+            .withMetalakeId(metalakeId)
+            .withMetalakeName(metalakeName)
+            .withAuditInfo(auditInfo.toString())
+            .withSchemaVersion("1.0")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(0L)
+            .build();
+    metalakeMetaMapper.insertMetalakeMeta(metalakePO);
+    return metalakePO;
+  }
+
+  private UserPO insertUser(long userId, String userName, long metalakeId) {
+    AuditInfo auditInfo = buildAuditInfo();
+    UserPO userPO =
+        UserPO.builder()
+            .withUserId(userId)
+            .withUserName(userName)
+            .withMetalakeId(metalakeId)
+            .withAuditInfo(auditInfo.toString())
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(0L)
+            .build();
+    userMetaMapper.insertUserMeta(userPO);
+    return userPO;
+  }
+
+  private RolePO insertRole(long roleId, String roleName, long metalakeId) {
+    AuditInfo auditInfo = buildAuditInfo();
+    RolePO rolePO =
+        RolePO.builder()
+            .withRoleId(roleId)
+            .withRoleName(roleName)
+            .withMetalakeId(metalakeId)
+            .withAuditInfo(auditInfo.toString())
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(0L)
+            .build();
+    roleMetaMapper.insertRoleMeta(rolePO);
+    return rolePO;
+  }
+
+  private GroupPO insertGroup(long groupId, String groupName, long metalakeId) 
{
+    AuditInfo auditInfo = buildAuditInfo();
+    GroupPO groupPO =
+        GroupPO.builder()
+            .withGroupId(groupId)
+            .withGroupName(groupName)
+            .withMetalakeId(metalakeId)
+            .withAuditInfo(auditInfo.toString())
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(0L)
+            .build();
+    groupMetaMapper.insertGroupMeta(groupPO);
+    return groupPO;
+  }
+
+  private long queryUpdatedAt(String table, String idColumn, long idValue) {
+    try (SqlSession sqlSession =
+        
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true)) 
{
+      try (Connection connection = sqlSession.getConnection()) {
+        String query = "SELECT updated_at FROM " + table + " WHERE " + 
idColumn + " = ?";
+        try (PreparedStatement statement = connection.prepareStatement(query)) 
{
+          statement.setLong(1, idValue);
+          try (ResultSet rs = statement.executeQuery()) {
+            if (rs.next()) {
+              return rs.getLong(1);
+            }
+            return -1;
+          }
+        }
+      }
+    } catch (SQLException e) {
+      throw new RuntimeException("SQL execution failed", e);
+    }
+  }
+}
diff --git 
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestRoleMetaService.java
 
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestRoleMetaService.java
index fbd17a31d7..287b346413 100644
--- 
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestRoleMetaService.java
+++ 
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestRoleMetaService.java
@@ -72,6 +72,22 @@ class TestRoleMetaService extends TestJDBCBackend {
 
   private static final String METALAKE_NAME = "metalake_for_role_test";
 
+  private long queryRoleUpdatedAt(long roleId) {
+    try (SqlSession sqlSession =
+        
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true)) 
{
+      try (Connection connection = sqlSession.getConnection();
+          Statement statement = connection.createStatement();
+          ResultSet resultSet =
+              statement.executeQuery(
+                  "SELECT updated_at FROM role_meta WHERE role_id = " + 
roleId)) {
+        Assertions.assertTrue(resultSet.next());
+        return resultSet.getLong(1);
+      }
+    } catch (SQLException se) {
+      throw new RuntimeException("Query role updated_at failed", se);
+    }
+  }
+
   @TestTemplate
   public void testMetaLifeCycleFromCreationToDeletion() throws IOException {
     BaseMetalake metalake = createAndInsertMakeLake(METALAKE_NAME);
@@ -661,6 +677,7 @@ class TestRoleMetaService extends TestJDBCBackend {
             AUDIT_INFO,
             "catalog");
     roleMetaService.insertRole(roleEntity, false);
+    long updatedAtBeforeGrant = queryRoleUpdatedAt(roleEntity.id());
 
     // grant privileges to the role
     Function<RoleEntity, RoleEntity> grantUpdater =
@@ -689,6 +706,7 @@ class TestRoleMetaService extends TestJDBCBackend {
         };
 
     
Assertions.assertNotNull(roleMetaService.updateRole(roleEntity.nameIdentifier(),
 grantUpdater));
+    Assertions.assertTrue(queryRoleUpdatedAt(roleEntity.id()) > 
updatedAtBeforeGrant);
     RoleEntity grantRole = 
roleMetaService.getRoleByIdentifier(roleEntity.nameIdentifier());
 
     assertEquals(grantRole.id(), roleEntity.id());
diff --git 
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestUserMetaService.java
 
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestUserMetaService.java
index d174434ce3..6033fc52a1 100644
--- 
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestUserMetaService.java
+++ 
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestUserMetaService.java
@@ -67,6 +67,22 @@ class TestUserMetaService extends TestJDBCBackend {
 
   private final String metalakeName = "metalake_for_user_test";
 
+  private long queryUserUpdatedAt(long userId) {
+    try (SqlSession sqlSession =
+        
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true)) 
{
+      try (Connection connection = sqlSession.getConnection();
+          Statement statement = connection.createStatement();
+          ResultSet resultSet =
+              statement.executeQuery(
+                  "SELECT updated_at FROM user_meta WHERE user_id = " + 
userId)) {
+        Assertions.assertTrue(resultSet.next());
+        return resultSet.getLong(1);
+      }
+    } catch (SQLException se) {
+      throw new RuntimeException("Query user updated_at failed", se);
+    }
+  }
+
   @TestTemplate
   public void testMetaLifeCycleFromCreationToDeletion() throws IOException {
     BaseMetalake metalake = createAndInsertMakeLake(metalakeName);
@@ -683,6 +699,7 @@ class TestUserMetaService extends TestJDBCBackend {
             auditInfo,
             "catalog");
     roleMetaService.insertRole(role3, false);
+    long updatedAtBeforeGrant = queryUserUpdatedAt(user1.id());
 
     // update user (grant)
     Function<UserEntity, UserEntity> grantUpdater =
@@ -711,6 +728,7 @@ class TestUserMetaService extends TestJDBCBackend {
         };
 
     
Assertions.assertNotNull(userMetaService.updateUser(user1.nameIdentifier(), 
grantUpdater));
+    Assertions.assertTrue(queryUserUpdatedAt(user1.id()) > 
updatedAtBeforeGrant);
     UserEntity grantUser =
         
UserMetaService.getInstance().getUserByIdentifier(user1.nameIdentifier());
     Assertions.assertEquals(user1.id(), grantUser.id());

Reply via email to