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 89523946e6 [#11068] feat(authn): Add built-in IdP group meta storage 
model (#11127)
89523946e6 is described below

commit 89523946e6b20d391700d303f3f8cf9515072404
Author: MaSai <[email protected]>
AuthorDate: Tue May 19 01:17:31 2026 +0800

    [#11068] feat(authn): Add built-in IdP group meta storage model (#11127)
    
    ### What changes were proposed in this pull request?
    
    This PR adds the built-in IdP group metadata relational storage model
    under `plugins:idp-basic`.
    
    It introduces:
    - the `IdpGroupPO` persistent object for built-in IdP groups
    - the `IdpGroupMetaMapper` MyBatis mapper for querying, inserting, soft
    deleting, and cleaning legacy group metadata
    - the `IdpGroupMetaSQLProviderFactory` together with base, H2, and
    PostgreSQL SQL providers for group metadata
    - mapper package registration updates so the built-in IdP group mapper
    is loaded by the `idp-basic` plugin
    - tests covering the group PO, SQL providers, and storage behavior
    
    This PR keeps the new group metadata storage implementation aligned with
    the existing built-in IdP user metadata code by:
    - following the same PO, mapper, and SQL provider structure
    - keeping MySQL in the base provider and using dedicated H2 and
    PostgreSQL providers for dialect differences
    - using the shared storage test base and mapper-driven test setup
    already used by the user metadata tests
    
    
    ### Why are the changes needed?
    
    The built-in IdP needs a relational group metadata model so group
    records can be persisted and queried consistently across the supported
    JDBC backends.
    
    Part of apache/gravitino#11068
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    ```bash
    ./gradlew --no-daemon :plugins:idp-basic:test -PskipITs 
-PskipDockerTests=false \
      --tests org.apache.gravitino.idp.storage.po.TestIdpGroupPO \
      --tests 
org.apache.gravitino.idp.storage.mapper.TestIdpBasicMapperPackageProvider \
      --tests 
org.apache.gravitino.idp.storage.mapper.TestIdpGroupMetaSQLProviderFactory \
      --tests 
org.apache.gravitino.idp.storage.mapper.provider.base.TestIdpGroupMetaBaseSQLProvider
 \
      --tests 
org.apache.gravitino.idp.storage.mapper.provider.h2.TestIdpGroupMetaH2Provider \
      --tests 
org.apache.gravitino.idp.storage.mapper.provider.postgresql.TestIdpGroupMetaPostgreSQLProvider
 \
      --tests org.apache.gravitino.idp.storage.mapper.TestIdpGroupMetaStorage
    ```
    
    `TestIdpGroupMetaStorage` covers H2, MySQL, and PostgreSQL when
    `-PskipDockerTests=false` is set.
---
 ...UserMetaMapper.java => IdpGroupMetaMapper.java} |  40 ++--
 .../mapper/IdpGroupMetaSQLProviderFactory.java     |  79 ++++++++
 .../idp/storage/mapper/IdpUserMetaMapper.java      |   4 +
 .../mapper/IdpUserMetaSQLProviderFactory.java      |  88 +++------
 .../storage/mapper/SQLProviderFactoryHelper.java   |  68 +++++++
 .../provider/IdpBasicMapperPackageProvider.java    |   3 +-
 .../provider/base/IdpGroupMetaBaseSQLProvider.java |  84 ++++++++
 .../IdpGroupMetaH2Provider.java}                   |  16 +-
 .../postgresql/IdpGroupMetaPostgreSQLProvider.java |  42 ++++
 .../IdpGroupPO.java}                               |  32 +--
 .../storage/mapper/AbstractIdpMetaStorageTest.java |  66 +------
 .../mapper/TestIdpBasicMapperPackageProvider.java  |  54 +++++
 .../storage/mapper/TestIdpGroupMetaStorage.java    | 220 +++++++++++++++++++++
 .../idp/storage/mapper/TestIdpUserMetaStorage.java | 158 +++++++++++++--
 .../mapper/TestSQLProviderFactoryHelper.java       |  68 +++++++
 .../base/TestIdpGroupMetaBaseSQLProvider.java      | 163 +++++++++++++++
 .../provider/h2/TestIdpGroupMetaH2Provider.java}   |  22 ++-
 .../TestIdpGroupMetaPostgreSQLProvider.java        |  45 +++++
 .../gravitino/idp/storage/po/TestIdpGroupPO.java   |  84 ++++++++
 19 files changed, 1141 insertions(+), 195 deletions(-)

diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaMapper.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpGroupMetaMapper.java
similarity index 55%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaMapper.java
copy to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpGroupMetaMapper.java
index 9aa677ebe2..88419f8349 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaMapper.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpGroupMetaMapper.java
@@ -20,7 +20,7 @@
 package org.apache.gravitino.idp.storage.mapper;
 
 import java.util.List;
-import org.apache.gravitino.idp.storage.po.IdpUserPO;
+import org.apache.gravitino.idp.storage.po.IdpGroupPO;
 import org.apache.ibatis.annotations.DeleteProvider;
 import org.apache.ibatis.annotations.InsertProvider;
 import org.apache.ibatis.annotations.Param;
@@ -28,35 +28,35 @@ import org.apache.ibatis.annotations.SelectProvider;
 import org.apache.ibatis.annotations.UpdateProvider;
 
 /**
- * A MyBatis mapper for built-in IdP user metadata operations.
+ * A MyBatis mapper for built-in IdP group metadata operations.
  *
- * <p>This interface defines the SQL statements MyBatis executes for the 
built-in IdP user metadata
+ * <p>This interface defines the SQL statements MyBatis executes for the 
built-in IdP group metadata
  * store. The SQLs are provided through {@code *Provider} annotations on this 
mapper interface. See
  * the <a href="https://mybatis.org/mybatis-3/getting-started.html";>MyBatis 
getting started
  * guide</a>.
  */
-public interface IdpUserMetaMapper {
-  String IDP_USER_TABLE_NAME = "idp_user_meta";
+public interface IdpGroupMetaMapper {
+  String IDP_GROUP_TABLE_NAME = "idp_group_meta";
 
-  @SelectProvider(type = IdpUserMetaSQLProviderFactory.class, method = 
"selectIdpUser")
-  IdpUserPO selectIdpUser(@Param("username") String username);
+  @SelectProvider(type = IdpGroupMetaSQLProviderFactory.class, method = 
"selectIdpGroup")
+  IdpGroupPO selectIdpGroup(@Param("groupName") String groupName);
 
-  @SelectProvider(type = IdpUserMetaSQLProviderFactory.class, method = 
"selectIdpUsers")
-  List<IdpUserPO> selectIdpUsers(@Param("usernames") List<String> usernames);
+  /**
+   * Selects active groups by name. An empty list returns all active groups; 
pass null for an
+   * explicit error.
+   */
+  @SelectProvider(type = IdpGroupMetaSQLProviderFactory.class, method = 
"selectIdpGroups")
+  List<IdpGroupPO> selectIdpGroups(@Param("groupNames") List<String> 
groupNames);
 
-  @InsertProvider(type = IdpUserMetaSQLProviderFactory.class, method = 
"insertIdpUser")
-  void insertIdpUser(@Param("userMeta") IdpUserPO userPO);
+  @InsertProvider(type = IdpGroupMetaSQLProviderFactory.class, method = 
"insertIdpGroup")
+  void insertIdpGroup(@Param("groupMeta") IdpGroupPO groupPO);
 
-  @UpdateProvider(type = IdpUserMetaSQLProviderFactory.class, method = 
"updateIdpUserPassword")
-  Integer updateIdpUserPassword(
-      @Param("userId") Long userId, @Param("passwordHash") String 
passwordHash);
-
-  @UpdateProvider(type = IdpUserMetaSQLProviderFactory.class, method = 
"softDeleteIdpUser")
-  Integer softDeleteIdpUser(@Param("userId") Long userId);
+  @UpdateProvider(type = IdpGroupMetaSQLProviderFactory.class, method = 
"softDeleteIdpGroup")
+  Integer softDeleteIdpGroup(@Param("groupId") Long groupId);
 
   @DeleteProvider(
-      type = IdpUserMetaSQLProviderFactory.class,
-      method = "deleteIdpUserMetasByLegacyTimeline")
-  Integer deleteIdpUserMetasByLegacyTimeline(
+      type = IdpGroupMetaSQLProviderFactory.class,
+      method = "deleteIdpGroupMetasByLegacyTimeline")
+  Integer deleteIdpGroupMetasByLegacyTimeline(
       @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit);
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpGroupMetaSQLProviderFactory.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpGroupMetaSQLProviderFactory.java
new file mode 100644
index 0000000000..4241ffc598
--- /dev/null
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpGroupMetaSQLProviderFactory.java
@@ -0,0 +1,79 @@
+/*
+ * 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.idp.storage.mapper;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.List;
+import java.util.Map;
+import 
org.apache.gravitino.idp.storage.mapper.provider.base.IdpGroupMetaBaseSQLProvider;
+import 
org.apache.gravitino.idp.storage.mapper.provider.h2.IdpGroupMetaH2Provider;
+import 
org.apache.gravitino.idp.storage.mapper.provider.postgresql.IdpGroupMetaPostgreSQLProvider;
+import org.apache.gravitino.idp.storage.po.IdpGroupPO;
+import org.apache.gravitino.storage.relational.JDBCBackend.JDBCBackendType;
+import org.apache.ibatis.annotations.Param;
+
+public class IdpGroupMetaSQLProviderFactory {
+  private static final IdpGroupMetaBaseSQLProvider MYSQL_PROVIDER =
+      new IdpGroupMetaBaseSQLProvider();
+  private static final IdpGroupMetaBaseSQLProvider H2_PROVIDER = new 
IdpGroupMetaH2Provider();
+  private static final IdpGroupMetaBaseSQLProvider POSTGRESQL_PROVIDER =
+      new IdpGroupMetaPostgreSQLProvider();
+  private static final Map<JDBCBackendType, IdpGroupMetaBaseSQLProvider> 
PROVIDER_MAP =
+      ImmutableMap.of(
+          JDBCBackendType.MYSQL,
+          MYSQL_PROVIDER,
+          JDBCBackendType.H2,
+          H2_PROVIDER,
+          JDBCBackendType.POSTGRESQL,
+          POSTGRESQL_PROVIDER);
+
+  private IdpGroupMetaSQLProviderFactory() {}
+
+  private static IdpGroupMetaBaseSQLProvider currentProvider() {
+    return SQLProviderFactoryHelper.currentProvider(
+        PROVIDER_MAP, IdpGroupMetaSQLProviderFactory.class);
+  }
+
+  static IdpGroupMetaBaseSQLProvider getProvider(String databaseId) {
+    return SQLProviderFactoryHelper.getProvider(
+        databaseId, PROVIDER_MAP, IdpGroupMetaSQLProviderFactory.class);
+  }
+
+  public static String selectIdpGroup(@Param("groupName") String groupName) {
+    return currentProvider().selectIdpGroup(groupName);
+  }
+
+  public static String selectIdpGroups(@Param("groupNames") List<String> 
groupNames) {
+    return currentProvider().selectIdpGroups(groupNames);
+  }
+
+  public static String insertIdpGroup(@Param("groupMeta") IdpGroupPO groupPO) {
+    return currentProvider().insertIdpGroup(groupPO);
+  }
+
+  public static String softDeleteIdpGroup(@Param("groupId") Long groupId) {
+    return currentProvider().softDeleteIdpGroup(groupId);
+  }
+
+  public static String deleteIdpGroupMetasByLegacyTimeline(
+      @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit) 
{
+    return 
currentProvider().deleteIdpGroupMetasByLegacyTimeline(legacyTimeline, limit);
+  }
+}
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaMapper.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaMapper.java
index 9aa677ebe2..41c3476030 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaMapper.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaMapper.java
@@ -41,6 +41,10 @@ public interface IdpUserMetaMapper {
   @SelectProvider(type = IdpUserMetaSQLProviderFactory.class, method = 
"selectIdpUser")
   IdpUserPO selectIdpUser(@Param("username") String username);
 
+  /**
+   * Selects active users by name. An empty list returns all active users; 
pass null for an explicit
+   * error.
+   */
   @SelectProvider(type = IdpUserMetaSQLProviderFactory.class, method = 
"selectIdpUsers")
   List<IdpUserPO> selectIdpUsers(@Param("usernames") List<String> usernames);
 
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaSQLProviderFactory.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaSQLProviderFactory.java
index 27e77781ed..46057906d9 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaSQLProviderFactory.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaSQLProviderFactory.java
@@ -27,93 +27,57 @@ import 
org.apache.gravitino.idp.storage.mapper.provider.h2.IdpUserMetaH2Provider
 import 
org.apache.gravitino.idp.storage.mapper.provider.postgresql.IdpUserMetaPostgreSQLProvider;
 import org.apache.gravitino.idp.storage.po.IdpUserPO;
 import org.apache.gravitino.storage.relational.JDBCBackend.JDBCBackendType;
-import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper;
 import org.apache.ibatis.annotations.Param;
 
 public class IdpUserMetaSQLProviderFactory {
-  private static final IdpUserMetaBaseSQLProvider IDP_USER_META_BASE_PROVIDER =
-      new IdpUserMetaBaseSQLProvider();
-  private static final IdpUserMetaBaseSQLProvider IDP_USER_META_H2_PROVIDER =
-      new IdpUserMetaH2Provider();
-  private static final IdpUserMetaBaseSQLProvider 
IDP_USER_META_POSTGRESQL_PROVIDER =
+  private static final IdpUserMetaBaseSQLProvider MYSQL_PROVIDER = new 
IdpUserMetaBaseSQLProvider();
+  private static final IdpUserMetaBaseSQLProvider H2_PROVIDER = new 
IdpUserMetaH2Provider();
+  private static final IdpUserMetaBaseSQLProvider POSTGRESQL_PROVIDER =
       new IdpUserMetaPostgreSQLProvider();
-
-  private static final Map<JDBCBackendType, IdpUserMetaBaseSQLProvider>
-      IDP_USER_META_SQL_PROVIDER_MAP =
-          ImmutableMap.of(
-              JDBCBackendType.MYSQL, IDP_USER_META_BASE_PROVIDER,
-              JDBCBackendType.H2, IDP_USER_META_H2_PROVIDER,
-              JDBCBackendType.POSTGRESQL, IDP_USER_META_POSTGRESQL_PROVIDER);
-
-  static IdpUserMetaBaseSQLProvider getProvider(String databaseId) {
-    if (databaseId == null) {
-      throw new IllegalStateException(
-          "MyBatis databaseId is not configured for IdP user SQL providers.");
-    }
-
-    try {
-      JDBCBackendType jdbcBackendType = JDBCBackendType.fromString(databaseId);
-      IdpUserMetaBaseSQLProvider provider = 
IDP_USER_META_SQL_PROVIDER_MAP.get(jdbcBackendType);
-      if (provider != null) {
-        return provider;
-      }
-
-      throw new IllegalStateException(
-          String.format(
-              "No IdP user SQL provider registered for backend %s (databaseId: 
%s)",
-              jdbcBackendType, databaseId));
-    } catch (IllegalArgumentException e) {
-      throw new IllegalStateException(
-          String.format(
-              "Unsupported IdP user SQL provider databaseId: %s, supported 
backends: %s",
-              databaseId, IDP_USER_META_SQL_PROVIDER_MAP.keySet()),
-          e);
-    }
-  }
-
-  public static IdpUserMetaBaseSQLProvider h2Provider() {
-    return IDP_USER_META_H2_PROVIDER;
-  }
-
-  public static IdpUserMetaBaseSQLProvider mysqlProvider() {
-    return IDP_USER_META_BASE_PROVIDER;
+  private static final Map<JDBCBackendType, IdpUserMetaBaseSQLProvider> 
PROVIDER_MAP =
+      ImmutableMap.of(
+          JDBCBackendType.MYSQL,
+          MYSQL_PROVIDER,
+          JDBCBackendType.H2,
+          H2_PROVIDER,
+          JDBCBackendType.POSTGRESQL,
+          POSTGRESQL_PROVIDER);
+
+  private IdpUserMetaSQLProviderFactory() {}
+
+  private static IdpUserMetaBaseSQLProvider currentProvider() {
+    return SQLProviderFactoryHelper.currentProvider(
+        PROVIDER_MAP, IdpUserMetaSQLProviderFactory.class);
   }
 
-  public static IdpUserMetaBaseSQLProvider postgresqlProvider() {
-    return IDP_USER_META_POSTGRESQL_PROVIDER;
+  static IdpUserMetaBaseSQLProvider getProvider(String databaseId) {
+    return SQLProviderFactoryHelper.getProvider(
+        databaseId, PROVIDER_MAP, IdpUserMetaSQLProviderFactory.class);
   }
 
   public static String selectIdpUser(@Param("username") String username) {
-    return getProvider(currentDatabaseId()).selectIdpUser(username);
+    return currentProvider().selectIdpUser(username);
   }
 
   public static String selectIdpUsers(@Param("usernames") List<String> 
usernames) {
-    return getProvider(currentDatabaseId()).selectIdpUsers(usernames);
+    return currentProvider().selectIdpUsers(usernames);
   }
 
   public static String insertIdpUser(@Param("userMeta") IdpUserPO userPO) {
-    return getProvider(currentDatabaseId()).insertIdpUser(userPO);
+    return currentProvider().insertIdpUser(userPO);
   }
 
   public static String updateIdpUserPassword(
       @Param("userId") Long userId, @Param("passwordHash") String 
passwordHash) {
-    return getProvider(currentDatabaseId()).updateIdpUserPassword(userId, 
passwordHash);
+    return currentProvider().updateIdpUserPassword(userId, passwordHash);
   }
 
   public static String softDeleteIdpUser(@Param("userId") Long userId) {
-    return getProvider(currentDatabaseId()).softDeleteIdpUser(userId);
+    return currentProvider().softDeleteIdpUser(userId);
   }
 
   public static String deleteIdpUserMetasByLegacyTimeline(
       @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit) 
{
-    return getProvider(currentDatabaseId())
-        .deleteIdpUserMetasByLegacyTimeline(legacyTimeline, limit);
-  }
-
-  private static String currentDatabaseId() {
-    return SqlSessionFactoryHelper.getInstance()
-        .getSqlSessionFactory()
-        .getConfiguration()
-        .getDatabaseId();
+    return 
currentProvider().deleteIdpUserMetasByLegacyTimeline(legacyTimeline, limit);
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/SQLProviderFactoryHelper.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/SQLProviderFactoryHelper.java
new file mode 100644
index 0000000000..cbb6726c45
--- /dev/null
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/SQLProviderFactoryHelper.java
@@ -0,0 +1,68 @@
+/*
+ * 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.idp.storage.mapper;
+
+import java.util.Map;
+import org.apache.gravitino.storage.relational.JDBCBackend.JDBCBackendType;
+import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper;
+
+final class SQLProviderFactoryHelper {
+  private SQLProviderFactoryHelper() {}
+
+  static <T> T getProvider(
+      String databaseId, Map<JDBCBackendType, T> providerMap, Class<?> 
providerFactoryClass) {
+    if (databaseId == null) {
+      throw new IllegalStateException(
+          String.format(
+              "MyBatis databaseId is not configured for %s.",
+              providerFactoryClass.getSimpleName()));
+    }
+
+    try {
+      JDBCBackendType jdbcBackendType = JDBCBackendType.fromString(databaseId);
+      T provider = providerMap.get(jdbcBackendType);
+      if (provider != null) {
+        return provider;
+      }
+
+      throw new IllegalStateException(
+          String.format(
+              "No %s registered for backend %s (databaseId: %s)",
+              providerFactoryClass.getSimpleName(), jdbcBackendType, 
databaseId));
+    } catch (IllegalArgumentException e) {
+      throw new IllegalStateException(
+          String.format(
+              "Unsupported %s databaseId: %s, supported backends: %s",
+              providerFactoryClass.getSimpleName(), databaseId, 
providerMap.keySet()),
+          e);
+    }
+  }
+
+  static <T> T currentProvider(Map<JDBCBackendType, T> providerMap, Class<?> 
providerFactoryClass) {
+    return getProvider(currentDatabaseId(), providerMap, providerFactoryClass);
+  }
+
+  static String currentDatabaseId() {
+    return SqlSessionFactoryHelper.getInstance()
+        .getSqlSessionFactory()
+        .getConfiguration()
+        .getDatabaseId();
+  }
+}
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/IdpBasicMapperPackageProvider.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/IdpBasicMapperPackageProvider.java
index 3cf51de9fa..075c994067 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/IdpBasicMapperPackageProvider.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/IdpBasicMapperPackageProvider.java
@@ -20,6 +20,7 @@ package org.apache.gravitino.idp.storage.mapper.provider;
 
 import com.google.common.collect.ImmutableList;
 import java.util.List;
+import org.apache.gravitino.idp.storage.mapper.IdpGroupMetaMapper;
 import org.apache.gravitino.idp.storage.mapper.IdpUserMetaMapper;
 import 
org.apache.gravitino.storage.relational.mapper.provider.MapperPackageProvider;
 
@@ -28,6 +29,6 @@ public class IdpBasicMapperPackageProvider implements 
MapperPackageProvider {
 
   @Override
   public List<Class<?>> getMapperClasses() {
-    return ImmutableList.of(IdpUserMetaMapper.class);
+    return ImmutableList.of(IdpUserMetaMapper.class, IdpGroupMetaMapper.class);
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpGroupMetaBaseSQLProvider.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpGroupMetaBaseSQLProvider.java
new file mode 100644
index 0000000000..a99edacec2
--- /dev/null
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpGroupMetaBaseSQLProvider.java
@@ -0,0 +1,84 @@
+/*
+ * 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.idp.storage.mapper.provider.base;
+
+import java.util.List;
+import org.apache.gravitino.idp.storage.mapper.IdpGroupMetaMapper;
+import org.apache.gravitino.idp.storage.po.IdpGroupPO;
+import org.apache.ibatis.annotations.Param;
+
+public class IdpGroupMetaBaseSQLProvider {
+
+  public String selectIdpGroup(@Param("groupName") String groupName) {
+    return "SELECT group_id as groupId, group_name as groupName,"
+        + " current_version as currentVersion,"
+        + " last_version as lastVersion, deleted_at as deletedAt"
+        + " FROM "
+        + IdpGroupMetaMapper.IDP_GROUP_TABLE_NAME
+        + " WHERE group_name = #{groupName} AND deleted_at = 0";
+  }
+
+  public String selectIdpGroups(@Param("groupNames") List<String> groupNames) {
+    return "<script>"
+        + "SELECT group_id as groupId, group_name as groupName,"
+        + " current_version as currentVersion,"
+        + " last_version as lastVersion, deleted_at as deletedAt"
+        + " FROM "
+        + IdpGroupMetaMapper.IDP_GROUP_TABLE_NAME
+        + " WHERE deleted_at = 0 "
+        + "<foreach collection='groupNames' item='groupName'"
+        + " open='AND group_name IN (' separator=',' close=')'>"
+        + "#{groupName}"
+        + "</foreach>"
+        + "</script>";
+  }
+
+  public String insertIdpGroup(@Param("groupMeta") IdpGroupPO groupPO) {
+    return "INSERT INTO "
+        + IdpGroupMetaMapper.IDP_GROUP_TABLE_NAME
+        + " (group_id, group_name, current_version, last_version, deleted_at)"
+        + " VALUES ("
+        + " #{groupMeta.groupId},"
+        + " #{groupMeta.groupName},"
+        + " #{groupMeta.currentVersion},"
+        + " #{groupMeta.lastVersion},"
+        + " #{groupMeta.deletedAt}"
+        + " )";
+  }
+
+  public String softDeleteIdpGroup(@Param("groupId") Long groupId) {
+    return "UPDATE "
+        + IdpGroupMetaMapper.IDP_GROUP_TABLE_NAME
+        + " SET deleted_at = "
+        + currentTimeMillisExpression()
+        + " WHERE group_id = #{groupId} AND deleted_at = 0";
+  }
+
+  public String deleteIdpGroupMetasByLegacyTimeline(
+      @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit) 
{
+    return "DELETE FROM "
+        + IdpGroupMetaMapper.IDP_GROUP_TABLE_NAME
+        + " WHERE deleted_at > 0 AND deleted_at < #{legacyTimeline} LIMIT 
#{limit}";
+  }
+
+  protected String currentTimeMillisExpression() {
+    return "(UNIX_TIMESTAMP() * 1000.0)";
+  }
+}
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/IdpBasicMapperPackageProvider.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/h2/IdpGroupMetaH2Provider.java
similarity index 61%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/IdpBasicMapperPackageProvider.java
copy to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/h2/IdpGroupMetaH2Provider.java
index 3cf51de9fa..b87e0bee38 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/IdpBasicMapperPackageProvider.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/h2/IdpGroupMetaH2Provider.java
@@ -16,18 +16,16 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.idp.storage.mapper.provider;
 
-import com.google.common.collect.ImmutableList;
-import java.util.List;
-import org.apache.gravitino.idp.storage.mapper.IdpUserMetaMapper;
-import 
org.apache.gravitino.storage.relational.mapper.provider.MapperPackageProvider;
+package org.apache.gravitino.idp.storage.mapper.provider.h2;
 
-/** Supplies built-in IdP mapper classes from the idp-basic plugin. */
-public class IdpBasicMapperPackageProvider implements MapperPackageProvider {
+import 
org.apache.gravitino.idp.storage.mapper.provider.base.IdpGroupMetaBaseSQLProvider;
+
+/** SQL provider for IdP group metadata statements on H2 backends. */
+public class IdpGroupMetaH2Provider extends IdpGroupMetaBaseSQLProvider {
 
   @Override
-  public List<Class<?>> getMapperClasses() {
-    return ImmutableList.of(IdpUserMetaMapper.class);
+  protected String currentTimeMillisExpression() {
+    return "DATEDIFF('MILLISECOND', TIMESTAMP '1970-01-01 00:00:00', 
CURRENT_TIMESTAMP())";
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/IdpGroupMetaPostgreSQLProvider.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/IdpGroupMetaPostgreSQLProvider.java
new file mode 100644
index 0000000000..a808bc2472
--- /dev/null
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/IdpGroupMetaPostgreSQLProvider.java
@@ -0,0 +1,42 @@
+/*
+ * 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.idp.storage.mapper.provider.postgresql;
+
+import org.apache.gravitino.idp.storage.mapper.IdpGroupMetaMapper;
+import 
org.apache.gravitino.idp.storage.mapper.provider.base.IdpGroupMetaBaseSQLProvider;
+import org.apache.ibatis.annotations.Param;
+
+public class IdpGroupMetaPostgreSQLProvider extends 
IdpGroupMetaBaseSQLProvider {
+
+  @Override
+  protected String currentTimeMillisExpression() {
+    return "CAST(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000 AS BIGINT)";
+  }
+
+  @Override
+  public String deleteIdpGroupMetasByLegacyTimeline(
+      @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit) 
{
+    return "DELETE FROM "
+        + IdpGroupMetaMapper.IDP_GROUP_TABLE_NAME
+        + " WHERE group_id IN (SELECT group_id FROM "
+        + IdpGroupMetaMapper.IDP_GROUP_TABLE_NAME
+        + " WHERE deleted_at > 0 AND deleted_at < #{legacyTimeline} LIMIT 
#{limit})";
+  }
+}
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/IdpBasicMapperPackageProvider.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpGroupPO.java
similarity index 58%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/IdpBasicMapperPackageProvider.java
copy to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpGroupPO.java
index 3cf51de9fa..0aa518a1eb 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/IdpBasicMapperPackageProvider.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpGroupPO.java
@@ -16,18 +16,26 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.idp.storage.mapper.provider;
+package org.apache.gravitino.idp.storage.po;
 
-import com.google.common.collect.ImmutableList;
-import java.util.List;
-import org.apache.gravitino.idp.storage.mapper.IdpUserMetaMapper;
-import 
org.apache.gravitino.storage.relational.mapper.provider.MapperPackageProvider;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
 
-/** Supplies built-in IdP mapper classes from the idp-basic plugin. */
-public class IdpBasicMapperPackageProvider implements MapperPackageProvider {
-
-  @Override
-  public List<Class<?>> getMapperClasses() {
-    return ImmutableList.of(IdpUserMetaMapper.class);
-  }
+@Getter
+@EqualsAndHashCode
+@ToString
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@Builder(setterPrefix = "with")
+public class IdpGroupPO {
+  private Long groupId;
+  private String groupName;
+  private Long currentVersion;
+  private Long lastVersion;
+  private Long deletedAt;
 }
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/AbstractIdpMetaStorageTest.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/AbstractIdpMetaStorageTest.java
index 06f97b8288..f41f323835 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/AbstractIdpMetaStorageTest.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/AbstractIdpMetaStorageTest.java
@@ -19,15 +19,11 @@
 
 package org.apache.gravitino.idp.storage.mapper;
 
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.sql.Connection;
 import java.sql.DriverManager;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Statement;
 import java.util.Arrays;
@@ -38,7 +34,6 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.Config;
 import org.apache.gravitino.Configs;
 import org.apache.gravitino.config.ConfigConstants;
-import org.apache.gravitino.idp.storage.po.IdpUserPO;
 import org.apache.gravitino.integration.test.container.ContainerSuite;
 import org.apache.gravitino.integration.test.container.MySQLContainer;
 import org.apache.gravitino.integration.test.container.PostgreSQLContainer;
@@ -57,7 +52,6 @@ abstract class AbstractIdpMetaStorageTest {
 
   protected JDBCBackend backend;
   protected SqlSession sharedSession;
-  protected IdpUserMetaMapper idpUserMetaMapper;
 
   private Config config;
   private Path h2Path;
@@ -88,7 +82,7 @@ abstract class AbstractIdpMetaStorageTest {
     backend = new JDBCBackend();
     backend.initialize(config);
     sharedSession = 
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
-    idpUserMetaMapper = sharedSession.getMapper(IdpUserMetaMapper.class);
+    initializeMappers();
   }
 
   protected void restartBackend() throws IOException {
@@ -97,61 +91,7 @@ abstract class AbstractIdpMetaStorageTest {
     backend = new JDBCBackend();
     backend.initialize(config);
     sharedSession = 
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
-    idpUserMetaMapper = sharedSession.getMapper(IdpUserMetaMapper.class);
-  }
-
-  protected IdpUserPO insertUser(
-      long userId,
-      String userName,
-      String passwordHash,
-      long currentVersion,
-      long lastVersion,
-      long deletedAt) {
-    IdpUserPO userPO =
-        IdpUserPO.builder()
-            .withUserId(userId)
-            .withUserName(userName)
-            .withPasswordHash(passwordHash)
-            .withCurrentVersion(currentVersion)
-            .withLastVersion(lastVersion)
-            .withDeletedAt(deletedAt)
-            .build();
-    idpUserMetaMapper.insertIdpUser(userPO);
-    return userPO;
-  }
-
-  protected long queryLongValue(String table, String column, String idColumn, 
long idValue) {
-    try (SqlSession sqlSession =
-            
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
-        Connection connection = sqlSession.getConnection();
-        PreparedStatement statement =
-            connection.prepareStatement(
-                "SELECT " + column + " FROM " + table + " WHERE " + idColumn + 
" = ?")) {
-      statement.setLong(1, idValue);
-      try (ResultSet resultSet = statement.executeQuery()) {
-        assertTrue(resultSet.next());
-        return resultSet.getLong(1);
-      }
-    } catch (SQLException e) {
-      throw new RuntimeException("Query " + column + " from " + table + " 
failed", e);
-    }
-  }
-
-  protected int countRows(String table, String idColumn, long idValue) {
-    try (SqlSession sqlSession =
-            
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
-        Connection connection = sqlSession.getConnection();
-        PreparedStatement statement =
-            connection.prepareStatement(
-                "SELECT COUNT(1) FROM " + table + " WHERE " + idColumn + " = 
?")) {
-      statement.setLong(1, idValue);
-      try (ResultSet resultSet = statement.executeQuery()) {
-        assertTrue(resultSet.next());
-        return resultSet.getInt(1);
-      }
-    } catch (SQLException e) {
-      throw new RuntimeException("Count rows from " + table + " failed", e);
-    }
+    initializeMappers();
   }
 
   protected void closeSession() {
@@ -161,6 +101,8 @@ abstract class AbstractIdpMetaStorageTest {
     }
   }
 
+  protected abstract void initializeMappers();
+
   private Config createBackendConfig(String type) throws IOException {
     Config backendConfig = new Config(false) {};
     backendConfig.set(Configs.ENTITY_STORE, Configs.RELATIONAL_ENTITY_STORE);
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpBasicMapperPackageProvider.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpBasicMapperPackageProvider.java
new file mode 100644
index 0000000000..3fcf1b6a7b
--- /dev/null
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpBasicMapperPackageProvider.java
@@ -0,0 +1,54 @@
+/*
+ * 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.idp.storage.mapper;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.ServiceLoader;
+import 
org.apache.gravitino.idp.storage.mapper.provider.IdpBasicMapperPackageProvider;
+import 
org.apache.gravitino.storage.relational.mapper.provider.MapperPackageProvider;
+import org.junit.jupiter.api.Test;
+
+public class TestIdpBasicMapperPackageProvider {
+
+  @Test
+  public void testGetMapperClasses() {
+    MapperPackageProvider provider = new IdpBasicMapperPackageProvider();
+    List<Class<?>> mapperClasses = provider.getMapperClasses();
+
+    assertEquals(2, mapperClasses.size());
+    assertTrue(
+        mapperClasses.containsAll(List.of(IdpUserMetaMapper.class, 
IdpGroupMetaMapper.class)));
+  }
+
+  @Test
+  public void testServiceLoaderDiscoversProvider() {
+    List<MapperPackageProvider> providers =
+        ServiceLoader.load(MapperPackageProvider.class).stream()
+            .map(ServiceLoader.Provider::get)
+            .filter(provider -> provider instanceof 
IdpBasicMapperPackageProvider)
+            .toList();
+
+    assertEquals(1, providers.size());
+    assertTrue(providers.get(0) instanceof IdpBasicMapperPackageProvider);
+  }
+}
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpGroupMetaStorage.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpGroupMetaStorage.java
new file mode 100644
index 0000000000..48ad87d48a
--- /dev/null
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpGroupMetaStorage.java
@@ -0,0 +1,220 @@
+/*
+ * 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.idp.storage.mapper;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertIterableEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.util.Comparator;
+import java.util.List;
+import org.apache.gravitino.idp.storage.po.IdpGroupPO;
+import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper;
+import org.apache.ibatis.exceptions.PersistenceException;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+@Tag("gravitino-docker-test")
+class TestIdpGroupMetaStorage extends AbstractIdpMetaStorageTest {
+  private IdpGroupMetaMapper idpGroupMetaMapper;
+
+  @Override
+  protected void initializeMappers() {
+    idpGroupMetaMapper = sharedSession.getMapper(IdpGroupMetaMapper.class);
+  }
+
+  @ParameterizedTest
+  @MethodSource("storageProvider")
+  void testInsertIdpGroupAndSelectIdpGroup(String type) throws IOException {
+    init(type);
+    IdpGroupPO firstGroup =
+        IdpGroupPO.builder()
+            .withGroupId(1L)
+            .withGroupName("dev")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(0L)
+            .build();
+    idpGroupMetaMapper.insertIdpGroup(firstGroup);
+
+    assertEquals(firstGroup, idpGroupMetaMapper.selectIdpGroup("dev"));
+    assertNull(idpGroupMetaMapper.selectIdpGroup("unknown"));
+  }
+
+  @ParameterizedTest
+  @MethodSource("storageProvider")
+  void testSelectIdpGroups(String type) throws IOException {
+    init(type);
+    IdpGroupPO firstGroup =
+        IdpGroupPO.builder()
+            .withGroupId(1L)
+            .withGroupName("dev")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(0L)
+            .build();
+    IdpGroupPO secondGroup =
+        IdpGroupPO.builder()
+            .withGroupId(2L)
+            .withGroupName("ops")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(0L)
+            .build();
+    idpGroupMetaMapper.insertIdpGroup(firstGroup);
+    idpGroupMetaMapper.insertIdpGroup(secondGroup);
+
+    List<IdpGroupPO> groups = 
idpGroupMetaMapper.selectIdpGroups(List.of("ops", "dev"));
+    groups.sort(Comparator.comparing(IdpGroupPO::getGroupId));
+    assertIterableEquals(List.of(firstGroup, secondGroup), groups);
+    List<IdpGroupPO> groupsWithEmptyFilter = 
idpGroupMetaMapper.selectIdpGroups(List.of());
+    groupsWithEmptyFilter.sort(Comparator.comparing(IdpGroupPO::getGroupId));
+    assertIterableEquals(List.of(firstGroup, secondGroup), 
groupsWithEmptyFilter);
+    assertThrows(PersistenceException.class, () -> 
idpGroupMetaMapper.selectIdpGroups(null));
+  }
+
+  @ParameterizedTest
+  @MethodSource("storageProvider")
+  void testSelectIdpGroupIgnoresDeletedGroups(String type) throws IOException {
+    init(type);
+    IdpGroupPO activeGroup =
+        IdpGroupPO.builder()
+            .withGroupId(1L)
+            .withGroupName("dev")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(0L)
+            .build();
+    idpGroupMetaMapper.insertIdpGroup(activeGroup);
+    idpGroupMetaMapper.insertIdpGroup(
+        IdpGroupPO.builder()
+            .withGroupId(2L)
+            .withGroupName("ops")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(10L)
+            .build());
+
+    assertIterableEquals(
+        List.of(activeGroup), 
idpGroupMetaMapper.selectIdpGroups(List.of("dev", "ops")));
+    assertNull(idpGroupMetaMapper.selectIdpGroup("ops"));
+  }
+
+  @ParameterizedTest
+  @MethodSource("storageProvider")
+  void testSoftDeleteIdpGroup(String type) throws IOException {
+    init(type);
+    idpGroupMetaMapper.insertIdpGroup(
+        IdpGroupPO.builder()
+            .withGroupId(1L)
+            .withGroupName("dev")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(0L)
+            .build());
+
+    assertEquals(1, idpGroupMetaMapper.softDeleteIdpGroup(1L));
+    assertNull(idpGroupMetaMapper.selectIdpGroup("dev"));
+    assertEquals(0, idpGroupMetaMapper.softDeleteIdpGroup(1L));
+    assertEquals(1, 
idpGroupMetaMapper.deleteIdpGroupMetasByLegacyTimeline(Long.MAX_VALUE, 10));
+  }
+
+  @ParameterizedTest
+  @MethodSource("storageProvider")
+  void testDeleteIdpGroupMetasByLegacyTimeline(String type) throws IOException 
{
+    init(type);
+    idpGroupMetaMapper.insertIdpGroup(
+        IdpGroupPO.builder()
+            .withGroupId(1L)
+            .withGroupName("legacy-group")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(10L)
+            .build());
+    idpGroupMetaMapper.insertIdpGroup(
+        IdpGroupPO.builder()
+            .withGroupId(2L)
+            .withGroupName("new-group")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(30L)
+            .build());
+    idpGroupMetaMapper.insertIdpGroup(
+        IdpGroupPO.builder()
+            .withGroupId(3L)
+            .withGroupName("active-group")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(0L)
+            .build());
+
+    assertEquals(1, 
idpGroupMetaMapper.deleteIdpGroupMetasByLegacyTimeline(20L, 10));
+    assertEquals(0, 
idpGroupMetaMapper.deleteIdpGroupMetasByLegacyTimeline(20L, 10));
+    assertEquals(1, 
idpGroupMetaMapper.deleteIdpGroupMetasByLegacyTimeline(40L, 10));
+    assertEquals(0, 
idpGroupMetaMapper.deleteIdpGroupMetasByLegacyTimeline(Long.MAX_VALUE, 10));
+    assertEquals("active-group", 
idpGroupMetaMapper.selectIdpGroup("active-group").getGroupName());
+    assertNull(idpGroupMetaMapper.selectIdpGroup("legacy-group"));
+    assertNull(idpGroupMetaMapper.selectIdpGroup("new-group"));
+  }
+
+  @ParameterizedTest
+  @MethodSource("storageProvider")
+  void testRestart(String type) throws IOException {
+    init(type);
+    IdpGroupPO expectedActiveGroup =
+        IdpGroupPO.builder()
+            .withGroupId(1L)
+            .withGroupName("dev")
+            .withCurrentVersion(3L)
+            .withLastVersion(2L)
+            .withDeletedAt(0L)
+            .build();
+    idpGroupMetaMapper.insertIdpGroup(expectedActiveGroup);
+    idpGroupMetaMapper.insertIdpGroup(
+        IdpGroupPO.builder()
+            .withGroupId(2L)
+            .withGroupName("ops")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(10L)
+            .build());
+
+    assertPersistedGroups(expectedActiveGroup);
+
+    restartBackend();
+
+    assertPersistedGroups(expectedActiveGroup);
+  }
+
+  private void assertPersistedGroups(IdpGroupPO expectedActiveGroup) {
+    assertTrue(
+        SqlSessionFactoryHelper.getInstance()
+            .getSqlSessionFactory()
+            .getConfiguration()
+            .hasMapper(IdpGroupMetaMapper.class));
+
+    assertEquals(expectedActiveGroup, 
idpGroupMetaMapper.selectIdpGroup("dev"));
+    assertNull(idpGroupMetaMapper.selectIdpGroup("ops"));
+  }
+}
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpUserMetaStorage.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpUserMetaStorage.java
index 025b3c5fb0..f2d87f4341 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpUserMetaStorage.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpUserMetaStorage.java
@@ -37,12 +37,27 @@ import org.junit.jupiter.params.provider.MethodSource;
 
 @Tag("gravitino-docker-test")
 class TestIdpUserMetaStorage extends AbstractIdpMetaStorageTest {
+  private IdpUserMetaMapper idpUserMetaMapper;
+
+  @Override
+  protected void initializeMappers() {
+    idpUserMetaMapper = sharedSession.getMapper(IdpUserMetaMapper.class);
+  }
 
   @ParameterizedTest
   @MethodSource("storageProvider")
   void testInsertIdpUserAndSelectIdpUser(String type) throws IOException {
     init(type);
-    IdpUserPO firstUser = insertUser(1L, "alice", "hash-a", 1L, 0L, 0L);
+    IdpUserPO firstUser =
+        IdpUserPO.builder()
+            .withUserId(1L)
+            .withUserName("alice")
+            .withPasswordHash("hash-a")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(0L)
+            .build();
+    idpUserMetaMapper.insertIdpUser(firstUser);
 
     assertEquals(firstUser, idpUserMetaMapper.selectIdpUser("alice"));
     assertNull(idpUserMetaMapper.selectIdpUser("unknown"));
@@ -52,8 +67,26 @@ class TestIdpUserMetaStorage extends 
AbstractIdpMetaStorageTest {
   @MethodSource("storageProvider")
   void testSelectIdpUsers(String type) throws IOException {
     init(type);
-    IdpUserPO firstUser = insertUser(1L, "alice", "hash-a", 1L, 0L, 0L);
-    IdpUserPO secondUser = insertUser(2L, "bob", "hash-b", 1L, 0L, 0L);
+    IdpUserPO firstUser =
+        IdpUserPO.builder()
+            .withUserId(1L)
+            .withUserName("alice")
+            .withPasswordHash("hash-a")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(0L)
+            .build();
+    IdpUserPO secondUser =
+        IdpUserPO.builder()
+            .withUserId(2L)
+            .withUserName("bob")
+            .withPasswordHash("hash-b")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(0L)
+            .build();
+    idpUserMetaMapper.insertIdpUser(firstUser);
+    idpUserMetaMapper.insertIdpUser(secondUser);
 
     List<IdpUserPO> users = idpUserMetaMapper.selectIdpUsers(List.of("bob", 
"alice"));
     users.sort(Comparator.comparing(IdpUserPO::getUserId));
@@ -68,8 +101,25 @@ class TestIdpUserMetaStorage extends 
AbstractIdpMetaStorageTest {
   @MethodSource("storageProvider")
   void testSelectIdpUsersIgnoresDeletedUsers(String type) throws IOException {
     init(type);
-    IdpUserPO activeUser = insertUser(1L, "alice", "hash-a", 1L, 0L, 0L);
-    insertUser(2L, "bob", "hash-b", 1L, 0L, 10L);
+    IdpUserPO activeUser =
+        IdpUserPO.builder()
+            .withUserId(1L)
+            .withUserName("alice")
+            .withPasswordHash("hash-a")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(0L)
+            .build();
+    idpUserMetaMapper.insertIdpUser(activeUser);
+    idpUserMetaMapper.insertIdpUser(
+        IdpUserPO.builder()
+            .withUserId(2L)
+            .withUserName("bob")
+            .withPasswordHash("hash-b")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(10L)
+            .build());
 
     assertIterableEquals(
         List.of(activeUser), idpUserMetaMapper.selectIdpUsers(List.of("alice", 
"bob")));
@@ -80,7 +130,15 @@ class TestIdpUserMetaStorage extends 
AbstractIdpMetaStorageTest {
   @MethodSource("storageProvider")
   void testUpdateIdpUserPassword(String type) throws IOException {
     init(type);
-    insertUser(1L, "alice", "hash-a", 1L, 0L, 0L);
+    idpUserMetaMapper.insertIdpUser(
+        IdpUserPO.builder()
+            .withUserId(1L)
+            .withUserName("alice")
+            .withPasswordHash("hash-a")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(0L)
+            .build());
 
     assertEquals(1, idpUserMetaMapper.updateIdpUserPassword(1L, "hash-a-2"));
     assertEquals("hash-a-2", 
idpUserMetaMapper.selectIdpUser("alice").getPasswordHash());
@@ -92,7 +150,15 @@ class TestIdpUserMetaStorage extends 
AbstractIdpMetaStorageTest {
   @MethodSource("storageProvider")
   void testUpdateIdpUserPasswordKeepsVersionsUnchanged(String type) throws 
IOException {
     init(type);
-    insertUser(1L, "alice", "hash-a", 3L, 2L, 0L);
+    idpUserMetaMapper.insertIdpUser(
+        IdpUserPO.builder()
+            .withUserId(1L)
+            .withUserName("alice")
+            .withPasswordHash("hash-a")
+            .withCurrentVersion(3L)
+            .withLastVersion(2L)
+            .withDeletedAt(0L)
+            .build());
 
     assertEquals(1, idpUserMetaMapper.updateIdpUserPassword(1L, "hash-a-2"));
     assertEquals("hash-a-2", 
idpUserMetaMapper.selectIdpUser("alice").getPasswordHash());
@@ -104,35 +170,89 @@ class TestIdpUserMetaStorage extends 
AbstractIdpMetaStorageTest {
   @MethodSource("storageProvider")
   void testSoftDeleteIdpUser(String type) throws IOException {
     init(type);
-    insertUser(1L, "alice", "hash-a", 1L, 0L, 0L);
+    idpUserMetaMapper.insertIdpUser(
+        IdpUserPO.builder()
+            .withUserId(1L)
+            .withUserName("alice")
+            .withPasswordHash("hash-a")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(0L)
+            .build());
 
     assertEquals(1, idpUserMetaMapper.softDeleteIdpUser(1L));
     assertNull(idpUserMetaMapper.selectIdpUser("alice"));
-    assertTrue(queryLongValue("idp_user_meta", "deleted_at", "user_id", 1L) > 
0L);
-    assertEquals(1L, queryLongValue("idp_user_meta", "current_version", 
"user_id", 1L));
-    assertEquals(0L, queryLongValue("idp_user_meta", "last_version", 
"user_id", 1L));
+    assertIterableEquals(List.of(), 
idpUserMetaMapper.selectIdpUsers(List.of("alice")));
+    assertEquals(0, idpUserMetaMapper.softDeleteIdpUser(1L));
+    assertEquals(0, idpUserMetaMapper.updateIdpUserPassword(1L, "hash-a-2"));
+    assertEquals(1, 
idpUserMetaMapper.deleteIdpUserMetasByLegacyTimeline(Long.MAX_VALUE, 10));
+    assertEquals(0, 
idpUserMetaMapper.deleteIdpUserMetasByLegacyTimeline(Long.MAX_VALUE, 10));
   }
 
   @ParameterizedTest
   @MethodSource("storageProvider")
   void testDeleteIdpUserMetasByLegacyTimeline(String type) throws IOException {
     init(type);
-    insertUser(1L, "legacy-user", "hash", 1L, 0L, 10L);
-    insertUser(2L, "new-user", "hash", 1L, 0L, 30L);
-    insertUser(3L, "active-user", "hash", 1L, 0L, 0L);
+    idpUserMetaMapper.insertIdpUser(
+        IdpUserPO.builder()
+            .withUserId(1L)
+            .withUserName("legacy-user")
+            .withPasswordHash("hash")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(10L)
+            .build());
+    idpUserMetaMapper.insertIdpUser(
+        IdpUserPO.builder()
+            .withUserId(2L)
+            .withUserName("new-user")
+            .withPasswordHash("hash")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(30L)
+            .build());
+    idpUserMetaMapper.insertIdpUser(
+        IdpUserPO.builder()
+            .withUserId(3L)
+            .withUserName("active-user")
+            .withPasswordHash("hash")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(0L)
+            .build());
 
     assertEquals(1, idpUserMetaMapper.deleteIdpUserMetasByLegacyTimeline(20L, 
10));
-    assertEquals(0, countRows("idp_user_meta", "user_id", 1L));
-    assertEquals(1, countRows("idp_user_meta", "user_id", 2L));
-    assertEquals(1, countRows("idp_user_meta", "user_id", 3L));
+    assertEquals(0, idpUserMetaMapper.deleteIdpUserMetasByLegacyTimeline(20L, 
10));
+    assertEquals(1, idpUserMetaMapper.deleteIdpUserMetasByLegacyTimeline(40L, 
10));
+    assertEquals(0, 
idpUserMetaMapper.deleteIdpUserMetasByLegacyTimeline(Long.MAX_VALUE, 10));
+    assertEquals("active-user", 
idpUserMetaMapper.selectIdpUser("active-user").getUserName());
+    assertNull(idpUserMetaMapper.selectIdpUser("legacy-user"));
+    assertNull(idpUserMetaMapper.selectIdpUser("new-user"));
   }
 
   @ParameterizedTest
   @MethodSource("storageProvider")
   void testRestart(String type) throws IOException {
     init(type);
-    IdpUserPO expectedActiveUser = insertUser(1L, "alice", "hash-a", 3L, 2L, 
0L);
-    insertUser(2L, "bob", "hash-b", 1L, 0L, 10L);
+    IdpUserPO expectedActiveUser =
+        IdpUserPO.builder()
+            .withUserId(1L)
+            .withUserName("alice")
+            .withPasswordHash("hash-a")
+            .withCurrentVersion(3L)
+            .withLastVersion(2L)
+            .withDeletedAt(0L)
+            .build();
+    idpUserMetaMapper.insertIdpUser(expectedActiveUser);
+    idpUserMetaMapper.insertIdpUser(
+        IdpUserPO.builder()
+            .withUserId(2L)
+            .withUserName("bob")
+            .withPasswordHash("hash-b")
+            .withCurrentVersion(1L)
+            .withLastVersion(0L)
+            .withDeletedAt(10L)
+            .build());
     assertEquals(1, idpUserMetaMapper.updateIdpUserPassword(1L, "hash-a-2"));
 
     expectedActiveUser =
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestSQLProviderFactoryHelper.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestSQLProviderFactoryHelper.java
new file mode 100644
index 0000000000..585beeb51f
--- /dev/null
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestSQLProviderFactoryHelper.java
@@ -0,0 +1,68 @@
+/*
+ * 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.idp.storage.mapper;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.apache.gravitino.storage.relational.JDBCBackend.JDBCBackendType;
+import org.junit.jupiter.api.Test;
+
+public class TestSQLProviderFactoryHelper {
+  private static final Map<JDBCBackendType, String> PROVIDER_MAP =
+      ImmutableMap.of(
+          JDBCBackendType.MYSQL, "mysql",
+          JDBCBackendType.H2, "h2",
+          JDBCBackendType.POSTGRESQL, "postgresql");
+
+  @Test
+  void testGetProvider() {
+    assertEquals("mysql", provider("mysql"));
+    assertEquals("h2", provider("h2"));
+    assertEquals("postgresql", provider("postgresql"));
+  }
+
+  @Test
+  void testGetProviderWithNullDatabaseId() {
+    IllegalStateException exception =
+        assertThrows(IllegalStateException.class, () -> provider(null));
+    assertEquals(
+        "MyBatis databaseId is not configured for 
TestSQLProviderFactoryHelper.",
+        exception.getMessage());
+  }
+
+  @Test
+  void testGetProviderWithUnsupportedDatabaseId() {
+    IllegalStateException exception =
+        assertThrows(IllegalStateException.class, () -> provider("sqlite"));
+    assertEquals(
+        "Unsupported TestSQLProviderFactoryHelper databaseId: sqlite, 
supported backends: [MYSQL,"
+            + " H2,"
+            + " POSTGRESQL]",
+        exception.getMessage());
+  }
+
+  private String provider(String databaseId) {
+    return SQLProviderFactoryHelper.getProvider(
+        databaseId, PROVIDER_MAP, TestSQLProviderFactoryHelper.class);
+  }
+}
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/provider/base/TestIdpGroupMetaBaseSQLProvider.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/provider/base/TestIdpGroupMetaBaseSQLProvider.java
new file mode 100644
index 0000000000..2b98776e18
--- /dev/null
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/provider/base/TestIdpGroupMetaBaseSQLProvider.java
@@ -0,0 +1,163 @@
+/*
+ * 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.idp.storage.mapper.provider.base;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.gravitino.idp.storage.po.IdpGroupPO;
+import org.apache.ibatis.builder.BuilderException;
+import org.apache.ibatis.mapping.BoundSql;
+import org.apache.ibatis.mapping.SqlSource;
+import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
+import org.apache.ibatis.session.Configuration;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestIdpGroupMetaBaseSQLProvider {
+
+  protected IdpGroupMetaBaseSQLProvider createProvider() {
+    return new IdpGroupMetaBaseSQLProvider() {
+      @Override
+      protected String currentTimeMillisExpression() {
+        return "CURRENT_TIME_MILLIS()";
+      }
+    };
+  }
+
+  protected String expectedDeleteAtClause() {
+    return "deleted_at = CURRENT_TIME_MILLIS()";
+  }
+
+  protected String expectedDeleteIdpGroupMetasByLegacyTimelineSql() {
+    return "DELETE FROM idp_group_meta WHERE deleted_at > 0 AND deleted_at < 
#{legacyTimeline}"
+        + " LIMIT #{limit}";
+  }
+
+  @Test
+  void testSelectIdpGroup() {
+    String normalizedSql = 
createProvider().selectIdpGroup("group").replaceAll("\\s+", " ").trim();
+
+    Assertions.assertTrue(normalizedSql.contains("SELECT group_id as 
groupId"));
+    Assertions.assertTrue(normalizedSql.contains("FROM idp_group_meta"));
+    Assertions.assertTrue(
+        normalizedSql.contains("WHERE group_name = #{groupName} AND deleted_at 
= 0"));
+  }
+
+  @Test
+  void testSelectIdpGroups() {
+    String script = createProvider().selectIdpGroups(Arrays.asList("dev", 
"ops"));
+    Map<String, Object> params = new HashMap<>();
+    params.put("groupNames", Arrays.asList("dev", "ops"));
+
+    String normalizedSql = renderScript(script, params);
+
+    Assertions.assertTrue(normalizedSql.contains("SELECT group_id as 
groupId"));
+    Assertions.assertTrue(normalizedSql.contains("FROM idp_group_meta"));
+    Assertions.assertTrue(
+        normalizedSql.matches(".*group_name 
IN\\s*\\(\\s*\\?\\s*,\\s*\\?\\s*\\).*"));
+    Assertions.assertFalse(normalizedSql.matches(".*\\b1\\s*=\\s*0\\b.*"));
+  }
+
+  @Test
+  void testSelectIdpGroupsWithEmptyGroupNames() {
+    String script = createProvider().selectIdpGroups(Collections.emptyList());
+    Map<String, Object> params = new HashMap<>();
+    params.put("groupNames", Collections.emptyList());
+
+    String normalizedSql = renderScript(script, params);
+
+    Assertions.assertFalse(
+        normalizedSql.matches(".*\\bIN\\s*\\(\\s*\\).*"),
+        "Empty groupNames should not generate invalid SQL IN (...) with no 
values");
+    Assertions.assertFalse(normalizedSql.matches(".*\\b1\\s*=\\s*0\\b.*"));
+    Assertions.assertEquals(
+        "SELECT group_id as groupId, group_name as groupName, current_version 
as"
+            + " currentVersion, last_version as lastVersion, deleted_at as 
deletedAt FROM"
+            + " idp_group_meta WHERE deleted_at = 0",
+        normalizedSql);
+  }
+
+  @Test
+  void testSelectIdpGroupsWithNullGroupNames() {
+    String script = createProvider().selectIdpGroups(null);
+    Map<String, Object> params = new HashMap<>();
+    params.put("groupNames", null);
+
+    Assertions.assertThrows(BuilderException.class, () -> renderScript(script, 
params));
+  }
+
+  @Test
+  void testInsertIdpGroup() {
+    String normalizedSql =
+        createProvider().insertIdpGroup(newGroupPO()).replaceAll("\\s+", " 
").trim();
+
+    Assertions.assertTrue(normalizedSql.contains("INSERT INTO 
idp_group_meta"));
+    Assertions.assertTrue(
+        normalizedSql.contains(
+            "(group_id, group_name, current_version, last_version, 
deleted_at)"));
+    Assertions.assertTrue(
+        normalizedSql.contains(
+            "VALUES ( #{groupMeta.groupId}, #{groupMeta.groupName}, 
#{groupMeta.currentVersion},"
+                + " #{groupMeta.lastVersion}, #{groupMeta.deletedAt} )"));
+  }
+
+  @Test
+  void testSoftDeleteIdpGroup() {
+    String normalizedSql = 
createProvider().softDeleteIdpGroup(1L).replaceAll("\\s+", " ").trim();
+
+    Assertions.assertTrue(normalizedSql.contains("UPDATE idp_group_meta"));
+    Assertions.assertTrue(normalizedSql.contains(expectedDeleteAtClause()));
+    Assertions.assertTrue(normalizedSql.contains("WHERE group_id = #{groupId} 
AND deleted_at = 0"));
+  }
+
+  @Test
+  void testDeleteIdpGroupMetasByLegacyTimeline() {
+    String normalizedSql =
+        createProvider().deleteIdpGroupMetasByLegacyTimeline(1L, 
2).replaceAll("\\s+", " ").trim();
+
+    Assertions.assertEquals(expectedDeleteIdpGroupMetasByLegacyTimelineSql(), 
normalizedSql);
+  }
+
+  @Test
+  void testCurrentTimeMillisExpression() {
+    Assertions.assertEquals(
+        "(UNIX_TIMESTAMP() * 1000.0)",
+        new IdpGroupMetaBaseSQLProvider().currentTimeMillisExpression());
+  }
+
+  private IdpGroupPO newGroupPO() {
+    return IdpGroupPO.builder()
+        .withGroupId(1L)
+        .withGroupName("group")
+        .withCurrentVersion(1L)
+        .withLastVersion(1L)
+        .withDeletedAt(0L)
+        .build();
+  }
+
+  private String renderScript(String script, Map<String, Object> params) {
+    SqlSource sqlSource =
+        new XMLLanguageDriver().createSqlSource(new Configuration(), script, 
Map.class);
+    BoundSql boundSql = sqlSource.getBoundSql(params);
+    return boundSql.getSql().replaceAll("\\s+", " ").trim();
+  }
+}
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/IdpBasicMapperPackageProvider.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/provider/h2/TestIdpGroupMetaH2Provider.java
similarity index 60%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/IdpBasicMapperPackageProvider.java
copy to 
plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/provider/h2/TestIdpGroupMetaH2Provider.java
index 3cf51de9fa..fb6584795a 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/IdpBasicMapperPackageProvider.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/provider/h2/TestIdpGroupMetaH2Provider.java
@@ -16,18 +16,20 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.idp.storage.mapper.provider;
 
-import com.google.common.collect.ImmutableList;
-import java.util.List;
-import org.apache.gravitino.idp.storage.mapper.IdpUserMetaMapper;
-import 
org.apache.gravitino.storage.relational.mapper.provider.MapperPackageProvider;
+package org.apache.gravitino.idp.storage.mapper.provider.h2;
 
-/** Supplies built-in IdP mapper classes from the idp-basic plugin. */
-public class IdpBasicMapperPackageProvider implements MapperPackageProvider {
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
 
-  @Override
-  public List<Class<?>> getMapperClasses() {
-    return ImmutableList.of(IdpUserMetaMapper.class);
+class TestIdpGroupMetaH2Provider {
+
+  @Test
+  void testCurrentTimeMillisExpression() {
+    IdpGroupMetaH2Provider provider = new IdpGroupMetaH2Provider();
+
+    Assertions.assertEquals(
+        "DATEDIFF('MILLISECOND', TIMESTAMP '1970-01-01 00:00:00', 
CURRENT_TIMESTAMP())",
+        provider.currentTimeMillisExpression());
   }
 }
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/TestIdpGroupMetaPostgreSQLProvider.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/TestIdpGroupMetaPostgreSQLProvider.java
new file mode 100644
index 0000000000..bdc1b16987
--- /dev/null
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/TestIdpGroupMetaPostgreSQLProvider.java
@@ -0,0 +1,45 @@
+/*
+ * 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.idp.storage.mapper.provider.postgresql;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestIdpGroupMetaPostgreSQLProvider {
+
+  @Test
+  void testCurrentTimeMillisExpression() {
+    IdpGroupMetaPostgreSQLProvider provider = new 
IdpGroupMetaPostgreSQLProvider();
+
+    Assertions.assertEquals(
+        "CAST(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000 AS BIGINT)",
+        provider.currentTimeMillisExpression());
+  }
+
+  @Test
+  void testDeleteIdpGroupMetasByLegacyTimeline() {
+    IdpGroupMetaPostgreSQLProvider provider = new 
IdpGroupMetaPostgreSQLProvider();
+
+    Assertions.assertEquals(
+        "DELETE FROM idp_group_meta WHERE group_id IN (SELECT group_id FROM 
idp_group_meta"
+            + " WHERE deleted_at > 0 AND deleted_at < #{legacyTimeline} LIMIT 
#{limit})",
+        provider.deleteIdpGroupMetasByLegacyTimeline(1L, 2));
+  }
+}
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpGroupPO.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpGroupPO.java
new file mode 100644
index 0000000000..baaefb8e96
--- /dev/null
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpGroupPO.java
@@ -0,0 +1,84 @@
+/*
+ * 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.idp.storage.po;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestIdpGroupPO {
+
+  @Test
+  public void testIdpGroupPOBuilder() {
+    IdpGroupPO groupPO =
+        IdpGroupPO.builder()
+            .withGroupId(1L)
+            .withGroupName("engineering")
+            .withCurrentVersion(1L)
+            .withLastVersion(1L)
+            .withDeletedAt(0L)
+            .build();
+
+    Assertions.assertEquals(1L, groupPO.getGroupId());
+    Assertions.assertEquals("engineering", groupPO.getGroupName());
+    Assertions.assertEquals(1L, groupPO.getCurrentVersion());
+    Assertions.assertEquals(1L, groupPO.getLastVersion());
+    Assertions.assertEquals(0L, groupPO.getDeletedAt());
+  }
+
+  @Test
+  public void testEqualsAndHashCode() {
+    IdpGroupPO groupPO1 =
+        IdpGroupPO.builder()
+            .withGroupId(1L)
+            .withGroupName("engineering")
+            .withCurrentVersion(1L)
+            .withLastVersion(1L)
+            .withDeletedAt(0L)
+            .build();
+
+    IdpGroupPO groupPO2 =
+        IdpGroupPO.builder()
+            .withGroupId(1L)
+            .withGroupName("engineering")
+            .withCurrentVersion(1L)
+            .withLastVersion(1L)
+            .withDeletedAt(0L)
+            .build();
+
+    Assertions.assertEquals(groupPO1, groupPO2);
+    Assertions.assertEquals(groupPO1.hashCode(), groupPO2.hashCode());
+  }
+
+  @Test
+  public void testBuilderReuseDoesNotMutateBuiltObject() {
+    var builder =
+        IdpGroupPO.builder()
+            .withGroupId(1L)
+            .withGroupName("engineering")
+            .withCurrentVersion(1L)
+            .withLastVersion(1L)
+            .withDeletedAt(0L);
+
+    IdpGroupPO firstGroup = builder.build();
+    IdpGroupPO secondGroup = builder.withGroupName("platform").build();
+
+    Assertions.assertEquals("engineering", firstGroup.getGroupName());
+    Assertions.assertEquals("platform", secondGroup.getGroupName());
+  }
+}

Reply via email to