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 0d97d6433d [#11067] feat(authn): Add built-in IdP user meta storage
model (#11066)
0d97d6433d is described below
commit 0d97d6433d4da8929811f8d847a523c3ccaf5100
Author: MaSai <[email protected]>
AuthorDate: Fri May 15 20:34:08 2026 +0800
[#11067] feat(authn): Add built-in IdP user meta storage model (#11066)
### What changes were proposed in this pull request?
This PR adds the built-in IdP user metadata relational storage model
under `plugins:idp-basic`.
It introduces:
- the `IdpUserPO` persistent object for built-in IdP users
- the `IdpUserMetaMapper` MyBatis mapper for querying, inserting,
updating passwords, soft deleting, and cleaning legacy user metadata
- the `IdpUserMetaSQLProviderFactory` with MySQL, H2, and PostgreSQL SQL
providers
- mapper package registration for the built-in IdP user mapper in the
`idp-basic` plugin
- plugin dependency updates required by the new mapper and mapper tests
- unit tests covering the user PO, mapper package provider, SQL
providers, and mapper behavior
This PR also follows the existing project patterns used in the
relational storage layer:
- the `IdpUserPO` builder follows the same copy-on-build style already
used by `PolicyPO`
- the SQL provider factory and PostgreSQL cleanup SQL keep the same
style as existing relational provider implementations in `core`
- multi-database mapper execution is controlled by `dockerTest=true`,
consistent with the current backend test extension pattern
### Why are the changes needed?
The built-in IdP needs a relational user metadata model so user records
can be persisted and queried consistently across the supported JDBC
backends.
Fix: #11067
### 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.basic.storage.relational.po.TestIdpUserPO \
--tests
org.apache.gravitino.idp.basic.storage.relational.mapper.provider.base.TestIdpUserMetaBaseSQLProvider
\
--tests
org.apache.gravitino.idp.basic.storage.relational.mapper.provider.mysql.TestIdpUserMetaMySQLProvider
\
--tests
org.apache.gravitino.idp.basic.storage.relational.mapper.provider.postgresql.TestIdpUserMetaPostgreSQLProvider
\
--tests
org.apache.gravitino.idp.basic.storage.relational.mapper.TestIdpUserMetaStorage
```
`TestIdpUserMetaStorage` covers H2, MySQL, and PostgreSQL when
`-PskipDockerTests=false` is set.
---
plugins/idp-basic/build.gradle.kts | 21 ++
.../idp/storage/mapper/IdpUserMetaMapper.java | 62 +++++
.../mapper/IdpUserMetaSQLProviderFactory.java | 119 ++++++++
.../provider/IdpBasicMapperPackageProvider.java} | 26 +-
.../provider/base/IdpUserMetaBaseSQLProvider.java | 93 +++++++
.../mapper/provider/h2/IdpUserMetaH2Provider.java} | 24 +-
.../postgresql/IdpUserMetaPostgreSQLProvider.java | 42 +++
.../gravitino/idp/storage/po/IdpUserPO.java} | 39 +--
...elational.mapper.provider.MapperPackageProvider | 18 ++
.../storage/mapper/AbstractIdpMetaStorageTest.java | 301 +++++++++++++++++++++
.../idp/storage/mapper/TestIdpUserMetaStorage.java | 169 ++++++++++++
.../base/TestIdpUserMetaBaseSQLProvider.java | 169 ++++++++++++
.../provider/h2/TestIdpUserMetaH2Provider.java} | 28 +-
.../TestIdpUserMetaPostgreSQLProvider.java | 45 +++
.../gravitino/idp/storage/po/TestIdpUserPO.java | 105 +++++++
15 files changed, 1195 insertions(+), 66 deletions(-)
diff --git a/plugins/idp-basic/build.gradle.kts
b/plugins/idp-basic/build.gradle.kts
index e10209214c..3229f452c8 100644
--- a/plugins/idp-basic/build.gradle.kts
+++ b/plugins/idp-basic/build.gradle.kts
@@ -24,10 +24,31 @@ plugins {
}
dependencies {
+ annotationProcessor(libs.lombok)
+
+ implementation(project(":core"))
+
implementation(libs.bcprov.jdk18on)
implementation(libs.commons.lang3)
implementation(libs.guava)
+ implementation(libs.mybatis)
+
+ compileOnly(libs.lombok)
+
+ testImplementation(project(":common"))
+ testImplementation(project(":core"))
+ testImplementation(project(":integration-test-common", "testArtifacts"))
+
+ testImplementation(libs.awaitility)
testImplementation(libs.junit.jupiter.api)
+ testImplementation(libs.junit.jupiter.params)
+ testImplementation(libs.commons.io)
+ testImplementation(libs.mysql.driver)
+ testImplementation(libs.postgresql.driver)
+ testImplementation(libs.testcontainers)
+ testImplementation(libs.testcontainers.mysql)
+ testImplementation(libs.testcontainers.postgresql)
+
testRuntimeOnly(libs.junit.jupiter.engine)
}
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
new file mode 100644
index 0000000000..9aa677ebe2
--- /dev/null
+++
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaMapper.java
@@ -0,0 +1,62 @@
+/*
+ * 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.List;
+import org.apache.gravitino.idp.storage.po.IdpUserPO;
+import org.apache.ibatis.annotations.DeleteProvider;
+import org.apache.ibatis.annotations.InsertProvider;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.SelectProvider;
+import org.apache.ibatis.annotations.UpdateProvider;
+
+/**
+ * A MyBatis mapper for built-in IdP user metadata operations.
+ *
+ * <p>This interface defines the SQL statements MyBatis executes for the
built-in IdP user 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";
+
+ @SelectProvider(type = IdpUserMetaSQLProviderFactory.class, method =
"selectIdpUser")
+ IdpUserPO selectIdpUser(@Param("username") String username);
+
+ @SelectProvider(type = IdpUserMetaSQLProviderFactory.class, method =
"selectIdpUsers")
+ List<IdpUserPO> selectIdpUsers(@Param("usernames") List<String> usernames);
+
+ @InsertProvider(type = IdpUserMetaSQLProviderFactory.class, method =
"insertIdpUser")
+ void insertIdpUser(@Param("userMeta") IdpUserPO userPO);
+
+ @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);
+
+ @DeleteProvider(
+ type = IdpUserMetaSQLProviderFactory.class,
+ method = "deleteIdpUserMetasByLegacyTimeline")
+ Integer deleteIdpUserMetasByLegacyTimeline(
+ @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit);
+}
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
new file mode 100644
index 0000000000..27e77781ed
--- /dev/null
+++
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaSQLProviderFactory.java
@@ -0,0 +1,119 @@
+/*
+ * 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.IdpUserMetaBaseSQLProvider;
+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 =
+ 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;
+ }
+
+ public static IdpUserMetaBaseSQLProvider postgresqlProvider() {
+ return IDP_USER_META_POSTGRESQL_PROVIDER;
+ }
+
+ public static String selectIdpUser(@Param("username") String username) {
+ return getProvider(currentDatabaseId()).selectIdpUser(username);
+ }
+
+ public static String selectIdpUsers(@Param("usernames") List<String>
usernames) {
+ return getProvider(currentDatabaseId()).selectIdpUsers(usernames);
+ }
+
+ public static String insertIdpUser(@Param("userMeta") IdpUserPO userPO) {
+ return getProvider(currentDatabaseId()).insertIdpUser(userPO);
+ }
+
+ public static String updateIdpUserPassword(
+ @Param("userId") Long userId, @Param("passwordHash") String
passwordHash) {
+ return getProvider(currentDatabaseId()).updateIdpUserPassword(userId,
passwordHash);
+ }
+
+ public static String softDeleteIdpUser(@Param("userId") Long userId) {
+ return getProvider(currentDatabaseId()).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();
+ }
+}
diff --git a/plugins/idp-basic/build.gradle.kts
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/IdpBasicMapperPackageProvider.java
similarity index 60%
copy from plugins/idp-basic/build.gradle.kts
copy to
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/IdpBasicMapperPackageProvider.java
index e10209214c..3cf51de9fa 100644
--- a/plugins/idp-basic/build.gradle.kts
+++
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/IdpBasicMapperPackageProvider.java
@@ -16,24 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/
+package org.apache.gravitino.idp.storage.mapper.provider;
-plugins {
- `maven-publish`
- id("java")
- id("idea")
-}
+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;
-dependencies {
- implementation(libs.bcprov.jdk18on)
- implementation(libs.commons.lang3)
- implementation(libs.guava)
- testImplementation(libs.junit.jupiter.api)
- testRuntimeOnly(libs.junit.jupiter.engine)
-}
+/** Supplies built-in IdP mapper classes from the idp-basic plugin. */
+public class IdpBasicMapperPackageProvider implements MapperPackageProvider {
-tasks {
- test {
- environment("GRAVITINO_HOME", rootDir.path)
- environment("GRAVITINO_TEST", "true")
+ @Override
+ public List<Class<?>> getMapperClasses() {
+ return ImmutableList.of(IdpUserMetaMapper.class);
}
}
diff --git
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpUserMetaBaseSQLProvider.java
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpUserMetaBaseSQLProvider.java
new file mode 100644
index 0000000000..5c026d1db5
--- /dev/null
+++
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpUserMetaBaseSQLProvider.java
@@ -0,0 +1,93 @@
+/*
+ * 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.IdpUserMetaMapper;
+import org.apache.gravitino.idp.storage.po.IdpUserPO;
+import org.apache.ibatis.annotations.Param;
+
+public class IdpUserMetaBaseSQLProvider {
+ public String selectIdpUser(@Param("username") String username) {
+ return "SELECT user_id as userId, user_name as userName, password_hash as
passwordHash,"
+ + " current_version as currentVersion,"
+ + " last_version as lastVersion, deleted_at as deletedAt"
+ + " FROM "
+ + IdpUserMetaMapper.IDP_USER_TABLE_NAME
+ + " WHERE user_name = #{username} AND deleted_at = 0";
+ }
+
+ public String selectIdpUsers(@Param("usernames") List<String> usernames) {
+ return "<script>"
+ + "SELECT user_id as userId, user_name as userName, password_hash as
passwordHash,"
+ + " current_version as currentVersion,"
+ + " last_version as lastVersion, deleted_at as deletedAt"
+ + " FROM "
+ + IdpUserMetaMapper.IDP_USER_TABLE_NAME
+ + " WHERE deleted_at = 0 "
+ + "<foreach collection='usernames' item='username'"
+ + " open='AND user_name IN (' separator=',' close=')'>"
+ + "#{username}"
+ + "</foreach>"
+ + "</script>";
+ }
+
+ public String insertIdpUser(@Param("userMeta") IdpUserPO userPO) {
+ return "INSERT INTO "
+ + IdpUserMetaMapper.IDP_USER_TABLE_NAME
+ + " (user_id, user_name, password_hash, current_version, last_version,
deleted_at)"
+ + " VALUES ("
+ + " #{userMeta.userId},"
+ + " #{userMeta.userName},"
+ + " #{userMeta.passwordHash},"
+ + " #{userMeta.currentVersion},"
+ + " #{userMeta.lastVersion},"
+ + " #{userMeta.deletedAt}"
+ + " )";
+ }
+
+ public String updateIdpUserPassword(
+ @Param("userId") Long userId, @Param("passwordHash") String
passwordHash) {
+ return "UPDATE "
+ + IdpUserMetaMapper.IDP_USER_TABLE_NAME
+ + " SET password_hash = #{passwordHash}"
+ + " WHERE user_id = #{userId}"
+ + " AND deleted_at = 0";
+ }
+
+ public String softDeleteIdpUser(@Param("userId") Long userId) {
+ return "UPDATE "
+ + IdpUserMetaMapper.IDP_USER_TABLE_NAME
+ + " SET deleted_at = "
+ + currentTimeMillisExpression()
+ + " WHERE user_id = #{userId} AND deleted_at = 0";
+ }
+
+ public String deleteIdpUserMetasByLegacyTimeline(
+ @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit)
{
+ return "DELETE FROM "
+ + IdpUserMetaMapper.IDP_USER_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/build.gradle.kts
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/h2/IdpUserMetaH2Provider.java
similarity index 64%
copy from plugins/idp-basic/build.gradle.kts
copy to
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/h2/IdpUserMetaH2Provider.java
index e10209214c..8731a3354d 100644
--- a/plugins/idp-basic/build.gradle.kts
+++
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/h2/IdpUserMetaH2Provider.java
@@ -17,23 +17,15 @@
* under the License.
*/
-plugins {
- `maven-publish`
- id("java")
- id("idea")
-}
+package org.apache.gravitino.idp.storage.mapper.provider.h2;
-dependencies {
- implementation(libs.bcprov.jdk18on)
- implementation(libs.commons.lang3)
- implementation(libs.guava)
- testImplementation(libs.junit.jupiter.api)
- testRuntimeOnly(libs.junit.jupiter.engine)
-}
+import
org.apache.gravitino.idp.storage.mapper.provider.base.IdpUserMetaBaseSQLProvider;
+
+/** SQL provider for IdP user metadata statements on H2 backends. */
+public class IdpUserMetaH2Provider extends IdpUserMetaBaseSQLProvider {
-tasks {
- test {
- environment("GRAVITINO_HOME", rootDir.path)
- environment("GRAVITINO_TEST", "true")
+ @Override
+ 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/IdpUserMetaPostgreSQLProvider.java
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/IdpUserMetaPostgreSQLProvider.java
new file mode 100644
index 0000000000..c41dd86589
--- /dev/null
+++
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/IdpUserMetaPostgreSQLProvider.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.IdpUserMetaMapper;
+import
org.apache.gravitino.idp.storage.mapper.provider.base.IdpUserMetaBaseSQLProvider;
+import org.apache.ibatis.annotations.Param;
+
+public class IdpUserMetaPostgreSQLProvider extends IdpUserMetaBaseSQLProvider {
+
+ @Override
+ protected String currentTimeMillisExpression() {
+ return "CAST(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000 AS BIGINT)";
+ }
+
+ @Override
+ public String deleteIdpUserMetasByLegacyTimeline(
+ @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit)
{
+ return "DELETE FROM "
+ + IdpUserMetaMapper.IDP_USER_TABLE_NAME
+ + " WHERE user_id IN (SELECT user_id FROM "
+ + IdpUserMetaMapper.IDP_USER_TABLE_NAME
+ + " WHERE deleted_at > 0 AND deleted_at < #{legacyTimeline} LIMIT
#{limit})";
+ }
+}
diff --git a/plugins/idp-basic/build.gradle.kts
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserPO.java
similarity index 57%
copy from plugins/idp-basic/build.gradle.kts
copy to
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserPO.java
index e10209214c..cbe03a610b 100644
--- a/plugins/idp-basic/build.gradle.kts
+++
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserPO.java
@@ -16,24 +16,27 @@
* specific language governing permissions and limitations
* under the License.
*/
+package org.apache.gravitino.idp.storage.po;
-plugins {
- `maven-publish`
- id("java")
- id("idea")
-}
-
-dependencies {
- implementation(libs.bcprov.jdk18on)
- implementation(libs.commons.lang3)
- implementation(libs.guava)
- testImplementation(libs.junit.jupiter.api)
- testRuntimeOnly(libs.junit.jupiter.engine)
-}
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
-tasks {
- test {
- environment("GRAVITINO_HOME", rootDir.path)
- environment("GRAVITINO_TEST", "true")
- }
+@Getter
+@EqualsAndHashCode
+@ToString
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@Builder(setterPrefix = "with")
+public class IdpUserPO {
+ private Long userId;
+ private String userName;
+ private String passwordHash;
+ private Long currentVersion;
+ private Long lastVersion;
+ private Long deletedAt;
}
diff --git
a/plugins/idp-basic/src/main/resources/META-INF/services/org.apache.gravitino.storage.relational.mapper.provider.MapperPackageProvider
b/plugins/idp-basic/src/main/resources/META-INF/services/org.apache.gravitino.storage.relational.mapper.provider.MapperPackageProvider
new file mode 100644
index 0000000000..fac41a8f06
--- /dev/null
+++
b/plugins/idp-basic/src/main/resources/META-INF/services/org.apache.gravitino.storage.relational.mapper.provider.MapperPackageProvider
@@ -0,0 +1,18 @@
+# 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.
+
+org.apache.gravitino.idp.storage.mapper.provider.IdpBasicMapperPackageProvider
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
new file mode 100644
index 0000000000..06f97b8288
--- /dev/null
+++
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/AbstractIdpMetaStorageTest.java
@@ -0,0 +1,301 @@
+/*
+ * 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.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;
+import java.util.Comparator;
+import java.util.UUID;
+import java.util.stream.Stream;
+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;
+import org.apache.gravitino.integration.test.util.TestDatabaseName;
+import org.apache.gravitino.storage.relational.JDBCBackend;
+import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper;
+import org.apache.ibatis.session.SqlSession;
+import org.junit.jupiter.api.AfterEach;
+
+abstract class AbstractIdpMetaStorageTest {
+ private static final String H2_BACKEND = "h2";
+ private static final String MYSQL_BACKEND = "mysql";
+ private static final String POSTGRESQL_BACKEND = "postgresql";
+ private static final TestDatabaseName MYSQL_TEST_DATABASE =
TestDatabaseName.MYSQL_JDBC_BACKEND;
+ private static final TestDatabaseName POSTGRESQL_TEST_DATABASE =
TestDatabaseName.PG_JDBC_BACKEND;
+
+ protected JDBCBackend backend;
+ protected SqlSession sharedSession;
+ protected IdpUserMetaMapper idpUserMetaMapper;
+
+ private Config config;
+ private Path h2Path;
+
+ static Stream<String> storageProvider() {
+ return Stream.of(H2_BACKEND, MYSQL_BACKEND, POSTGRESQL_BACKEND);
+ }
+
+ @AfterEach
+ void closeSuite() throws IOException {
+ closeSession();
+ if (backend != null) {
+ backend.close();
+ backend = null;
+ }
+
+ SqlSessionFactoryHelper.getInstance().close();
+ ContainerSuite.getInstance().close();
+
+ if (h2Path != null && Files.exists(h2Path)) {
+ deleteDirectory(h2Path);
+ h2Path = null;
+ }
+ }
+
+ protected void init(String type) throws IOException {
+ config = createBackendConfig(type);
+ backend = new JDBCBackend();
+ backend.initialize(config);
+ sharedSession =
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
+ idpUserMetaMapper = sharedSession.getMapper(IdpUserMetaMapper.class);
+ }
+
+ protected void restartBackend() throws IOException {
+ closeSession();
+ backend.close();
+ 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);
+ }
+ }
+
+ protected void closeSession() {
+ if (sharedSession != null) {
+ sharedSession.close();
+ sharedSession = null;
+ }
+ }
+
+ private Config createBackendConfig(String type) throws IOException {
+ Config backendConfig = new Config(false) {};
+ backendConfig.set(Configs.ENTITY_STORE, Configs.RELATIONAL_ENTITY_STORE);
+ backendConfig.set(Configs.ENTITY_RELATIONAL_STORE, type);
+ backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS,
20);
+
backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS,
1000L);
+
+ switch (type) {
+ case MYSQL_BACKEND:
+ initializeMySQLBackend(backendConfig);
+ break;
+ case POSTGRESQL_BACKEND:
+ initializePostgreSQLBackend(backendConfig);
+ break;
+ case H2_BACKEND:
+ initializeH2Backend(backendConfig);
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported backend type: " +
type);
+ }
+
+ return backendConfig;
+ }
+
+ private void initializeMySQLBackend(Config backendConfig) throws IOException
{
+ ContainerSuite containerSuite = ContainerSuite.getInstance();
+ containerSuite.startMySQLContainer(MYSQL_TEST_DATABASE);
+ MySQLContainer mySQLContainer = containerSuite.getMySQLContainer();
+ String jdbcUrl = mySQLContainer.getJdbcUrl(MYSQL_TEST_DATABASE);
+
+ backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL, jdbcUrl);
+ backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_USER,
mySQLContainer.getUsername());
+ backendConfig.set(
+ Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD,
mySQLContainer.getPassword());
+ backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER,
"com.mysql.cj.jdbc.Driver");
+
+ try (Connection connection =
+ DriverManager.getConnection(
+ StringUtils.substringBeforeLast(jdbcUrl, "/"),
+ mySQLContainer.getUsername(),
+ mySQLContainer.getPassword());
+ Statement statement = connection.createStatement()) {
+ statement.execute("DROP DATABASE IF EXISTS " + MYSQL_TEST_DATABASE);
+ statement.execute("CREATE DATABASE " + MYSQL_TEST_DATABASE);
+ statement.execute("USE " + MYSQL_TEST_DATABASE);
+ executeSqlStatements(statement, loadSchemaStatements(MYSQL_BACKEND));
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed to initialize MySQL backend for IdP
user tests", e);
+ }
+ }
+
+ private void initializePostgreSQLBackend(Config backendConfig) throws
IOException {
+ ContainerSuite containerSuite = ContainerSuite.getInstance();
+ containerSuite.startPostgreSQLContainer(POSTGRESQL_TEST_DATABASE);
+ PostgreSQLContainer postgreSQLContainer =
containerSuite.getPostgreSQLContainer();
+ String schemaName = "idp_user_" +
UUID.randomUUID().toString().replace("-", "");
+ String jdbcUrl = postgreSQLContainer.getJdbcUrl(POSTGRESQL_TEST_DATABASE);
+
+ backendConfig.set(
+ Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL, jdbcUrl +
"?currentSchema=" + schemaName);
+ backendConfig.set(
+ Configs.ENTITY_RELATIONAL_JDBC_BACKEND_USER,
postgreSQLContainer.getUsername());
+ backendConfig.set(
+ Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD,
postgreSQLContainer.getPassword());
+ backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER,
"org.postgresql.Driver");
+
+ try (Connection connection =
+ DriverManager.getConnection(
+ jdbcUrl, postgreSQLContainer.getUsername(),
postgreSQLContainer.getPassword());
+ Statement statement = connection.createStatement()) {
+ statement.execute("DROP SCHEMA IF EXISTS " + schemaName + " CASCADE");
+ statement.execute("CREATE SCHEMA " + schemaName);
+ statement.execute("SET search_path TO " + schemaName);
+ executeSqlStatements(statement,
loadSchemaStatements(POSTGRESQL_BACKEND));
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed to initialize PostgreSQL backend for
IdP user tests", e);
+ }
+ }
+
+ private void initializeH2Backend(Config backendConfig) throws IOException {
+ h2Path = Files.createTempDirectory("gravitino_idp_basic_h2_");
+ backendConfig.set(
+ Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL,
+ String.format("jdbc:h2:file:%s;DB_CLOSE_DELAY=-1;MODE=MYSQL", h2Path));
+ backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_USER, "root");
+ backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD,
"123456");
+ backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER,
"org.h2.Driver");
+ }
+
+ private String[] loadSchemaStatements(String databaseType) throws
IOException {
+ Path scriptPath =
+ resolveProjectRoot()
+ .resolve("scripts")
+ .resolve(databaseType)
+ .resolve(
+ String.format(
+ "schema-%s-%s.sql",
ConfigConstants.CURRENT_SCRIPT_VERSION, databaseType));
+ return Arrays.stream(Files.readString(scriptPath).split(";"))
+ .map(String::trim)
+ .filter(sql -> !sql.isEmpty())
+ .toArray(String[]::new);
+ }
+
+ private Path resolveProjectRoot() {
+ String rootDir = System.getenv("GRAVITINO_ROOT_DIR");
+ if (StringUtils.isBlank(rootDir)) {
+ rootDir = System.getenv("GRAVITINO_HOME");
+ }
+
+ if (StringUtils.isBlank(rootDir)) {
+ throw new IllegalStateException(
+ "GRAVITINO_ROOT_DIR or GRAVITINO_HOME must be set for IdP user
storage tests");
+ }
+
+ return Path.of(rootDir);
+ }
+
+ private void executeSqlStatements(Statement statement, String[]
sqlStatements)
+ throws SQLException {
+ for (String sql : sqlStatements) {
+ statement.execute(sql);
+ }
+ }
+
+ private void deleteDirectory(Path directory) throws IOException {
+ try (Stream<Path> paths = Files.walk(directory)) {
+ paths.sorted(Comparator.reverseOrder()).forEach(this::deletePath);
+ }
+ }
+
+ private void deletePath(Path path) {
+ try {
+ Files.deleteIfExists(path);
+ } catch (IOException e) {
+ throw new RuntimeException("Delete path failed: " + path, e);
+ }
+ }
+}
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
new file mode 100644
index 0000000000..025b3c5fb0
--- /dev/null
+++
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpUserMetaStorage.java
@@ -0,0 +1,169 @@
+/*
+ * 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.IdpUserPO;
+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 TestIdpUserMetaStorage extends AbstractIdpMetaStorageTest {
+
+ @ParameterizedTest
+ @MethodSource("storageProvider")
+ void testInsertIdpUserAndSelectIdpUser(String type) throws IOException {
+ init(type);
+ IdpUserPO firstUser = insertUser(1L, "alice", "hash-a", 1L, 0L, 0L);
+
+ assertEquals(firstUser, idpUserMetaMapper.selectIdpUser("alice"));
+ assertNull(idpUserMetaMapper.selectIdpUser("unknown"));
+ }
+
+ @ParameterizedTest
+ @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);
+
+ List<IdpUserPO> users = idpUserMetaMapper.selectIdpUsers(List.of("bob",
"alice"));
+ users.sort(Comparator.comparing(IdpUserPO::getUserId));
+ assertIterableEquals(List.of(firstUser, secondUser), users);
+ List<IdpUserPO> usersWithEmptyFilter =
idpUserMetaMapper.selectIdpUsers(List.of());
+ usersWithEmptyFilter.sort(Comparator.comparing(IdpUserPO::getUserId));
+ assertIterableEquals(List.of(firstUser, secondUser), usersWithEmptyFilter);
+ assertThrows(PersistenceException.class, () ->
idpUserMetaMapper.selectIdpUsers(null));
+ }
+
+ @ParameterizedTest
+ @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);
+
+ assertIterableEquals(
+ List.of(activeUser), idpUserMetaMapper.selectIdpUsers(List.of("alice",
"bob")));
+ assertNull(idpUserMetaMapper.selectIdpUser("bob"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("storageProvider")
+ void testUpdateIdpUserPassword(String type) throws IOException {
+ init(type);
+ insertUser(1L, "alice", "hash-a", 1L, 0L, 0L);
+
+ assertEquals(1, idpUserMetaMapper.updateIdpUserPassword(1L, "hash-a-2"));
+ assertEquals("hash-a-2",
idpUserMetaMapper.selectIdpUser("alice").getPasswordHash());
+ assertEquals(1L,
idpUserMetaMapper.selectIdpUser("alice").getCurrentVersion());
+ assertEquals(0L,
idpUserMetaMapper.selectIdpUser("alice").getLastVersion());
+ }
+
+ @ParameterizedTest
+ @MethodSource("storageProvider")
+ void testUpdateIdpUserPasswordKeepsVersionsUnchanged(String type) throws
IOException {
+ init(type);
+ insertUser(1L, "alice", "hash-a", 3L, 2L, 0L);
+
+ assertEquals(1, idpUserMetaMapper.updateIdpUserPassword(1L, "hash-a-2"));
+ assertEquals("hash-a-2",
idpUserMetaMapper.selectIdpUser("alice").getPasswordHash());
+ assertEquals(3L,
idpUserMetaMapper.selectIdpUser("alice").getCurrentVersion());
+ assertEquals(2L,
idpUserMetaMapper.selectIdpUser("alice").getLastVersion());
+ }
+
+ @ParameterizedTest
+ @MethodSource("storageProvider")
+ void testSoftDeleteIdpUser(String type) throws IOException {
+ init(type);
+ insertUser(1L, "alice", "hash-a", 1L, 0L, 0L);
+
+ 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));
+ }
+
+ @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);
+
+ 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));
+ }
+
+ @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);
+ assertEquals(1, idpUserMetaMapper.updateIdpUserPassword(1L, "hash-a-2"));
+
+ expectedActiveUser =
+ IdpUserPO.builder()
+ .withUserId(expectedActiveUser.getUserId())
+ .withUserName(expectedActiveUser.getUserName())
+ .withPasswordHash("hash-a-2")
+ .withCurrentVersion(expectedActiveUser.getCurrentVersion())
+ .withLastVersion(expectedActiveUser.getLastVersion())
+ .withDeletedAt(expectedActiveUser.getDeletedAt())
+ .build();
+
+ assertPersistedUsers(expectedActiveUser);
+
+ restartBackend();
+
+ assertPersistedUsers(expectedActiveUser);
+ }
+
+ private void assertPersistedUsers(IdpUserPO expectedActiveUser) {
+ assertTrue(
+ SqlSessionFactoryHelper.getInstance()
+ .getSqlSessionFactory()
+ .getConfiguration()
+ .hasMapper(IdpUserMetaMapper.class));
+
+ assertEquals(expectedActiveUser, idpUserMetaMapper.selectIdpUser("alice"));
+ assertNull(idpUserMetaMapper.selectIdpUser("bob"));
+
+ List<IdpUserPO> users = idpUserMetaMapper.selectIdpUsers(List.of("bob",
"alice"));
+ users.sort(Comparator.comparing(IdpUserPO::getUserId));
+ assertIterableEquals(List.of(expectedActiveUser), users);
+ }
+}
diff --git
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/provider/base/TestIdpUserMetaBaseSQLProvider.java
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/provider/base/TestIdpUserMetaBaseSQLProvider.java
new file mode 100644
index 0000000000..b14e015428
--- /dev/null
+++
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/provider/base/TestIdpUserMetaBaseSQLProvider.java
@@ -0,0 +1,169 @@
+/*
+ * 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.IdpUserPO;
+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 TestIdpUserMetaBaseSQLProvider {
+
+ @Test
+ void testSelectIdpUser() {
+ String normalizedSql =
createProvider().selectIdpUser("tom").replaceAll("\\s+", " ").trim();
+
+ Assertions.assertTrue(normalizedSql.contains("SELECT user_id as userId"));
+ Assertions.assertTrue(normalizedSql.contains("FROM idp_user_meta"));
+ Assertions.assertTrue(
+ normalizedSql.contains("WHERE user_name = #{username} AND deleted_at =
0"));
+ }
+
+ @Test
+ void testSelectIdpUsers() {
+ String script = createProvider().selectIdpUsers(Arrays.asList("tom",
"jerry"));
+ Map<String, Object> params = new HashMap<>();
+ params.put("usernames", Arrays.asList("tom", "jerry"));
+
+ String normalizedSql = renderScript(script, params);
+
+ Assertions.assertTrue(normalizedSql.contains("SELECT user_id as userId"));
+ Assertions.assertTrue(normalizedSql.contains("FROM idp_user_meta"));
+ Assertions.assertTrue(normalizedSql.matches(".*user_name IN \\( \\? , \\?
\\).*"));
+ Assertions.assertFalse(normalizedSql.matches(".*\\b1\\s*=\\s*0\\b.*"));
+ }
+
+ @Test
+ void testSelectIdpUsersWithEmptyUserNames() {
+ String script = createProvider().selectIdpUsers(Collections.emptyList());
+ Map<String, Object> params = new HashMap<>();
+ params.put("usernames", Collections.emptyList());
+
+ String normalizedSql = renderScript(script, params);
+
+ Assertions.assertFalse(
+ normalizedSql.matches(".*\\bIN\\s*\\(\\s*\\).*"),
+ "Empty userNames should not generate invalid SQL IN (...) with no
values");
+ Assertions.assertFalse(normalizedSql.matches(".*\\b1\\s*=\\s*0\\b.*"));
+ Assertions.assertEquals(
+ "SELECT user_id as userId, user_name as userName, password_hash as
passwordHash,"
+ + " current_version as currentVersion, last_version as
lastVersion,"
+ + " deleted_at as deletedAt FROM idp_user_meta WHERE deleted_at =
0",
+ normalizedSql);
+ }
+
+ @Test
+ void testSelectIdpUsersWithNullUserNames() {
+ String script = createProvider().selectIdpUsers(null);
+ Map<String, Object> params = new HashMap<>();
+ params.put("usernames", null);
+
+ Assertions.assertThrows(BuilderException.class, () -> renderScript(script,
params));
+ }
+
+ @Test
+ void testInsertIdpUser() {
+ String normalizedSql =
+ createProvider().insertIdpUser(newUserPO()).replaceAll("\\s+", "
").trim();
+
+ Assertions.assertTrue(normalizedSql.contains("INSERT INTO idp_user_meta"));
+ Assertions.assertTrue(
+ normalizedSql.contains(
+ "(user_id, user_name, password_hash, current_version,
last_version, deleted_at)"));
+ Assertions.assertTrue(
+ normalizedSql.contains(
+ "VALUES ( #{userMeta.userId}, #{userMeta.userName},
#{userMeta.passwordHash},"
+ + " #{userMeta.currentVersion}, #{userMeta.lastVersion},"
+ + " #{userMeta.deletedAt} )"));
+ }
+
+ @Test
+ void testUpdateIdpUserPassword() {
+ String normalizedSql =
+ createProvider().updateIdpUserPassword(1L, "hash").replaceAll("\\s+",
" ").trim();
+
+ Assertions.assertTrue(normalizedSql.contains("UPDATE idp_user_meta"));
+ Assertions.assertTrue(normalizedSql.contains("SET password_hash =
#{passwordHash}"));
+ Assertions.assertTrue(normalizedSql.contains("WHERE user_id = #{userId}"));
+ Assertions.assertTrue(normalizedSql.contains("AND deleted_at = 0"));
+ }
+
+ @Test
+ void testSoftDeleteIdpUser() {
+ String normalizedSql =
createProvider().softDeleteIdpUser(1L).replaceAll("\\s+", " ").trim();
+
+ Assertions.assertTrue(normalizedSql.contains("UPDATE idp_user_meta"));
+ Assertions.assertTrue(normalizedSql.contains("CURRENT_TIME_MILLIS()"));
+ Assertions.assertTrue(normalizedSql.contains("WHERE user_id = #{userId}
AND deleted_at = 0"));
+ }
+
+ @Test
+ void testDeleteIdpUserMetasByLegacyTimeline() {
+ String normalizedSql =
+ createProvider().deleteIdpUserMetasByLegacyTimeline(1L,
2).replaceAll("\\s+", " ").trim();
+
+ Assertions.assertEquals(
+ "DELETE FROM idp_user_meta WHERE deleted_at > 0 AND deleted_at <
#{legacyTimeline}"
+ + " LIMIT #{limit}",
+ normalizedSql);
+ }
+
+ @Test
+ void testCurrentTimeMillisExpression() {
+ Assertions.assertEquals(
+ "(UNIX_TIMESTAMP() * 1000.0)",
+ new IdpUserMetaBaseSQLProvider().currentTimeMillisExpression());
+ }
+
+ private IdpUserMetaBaseSQLProvider createProvider() {
+ return new IdpUserMetaBaseSQLProvider() {
+ @Override
+ protected String currentTimeMillisExpression() {
+ return "CURRENT_TIME_MILLIS()";
+ }
+ };
+ }
+
+ 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();
+ }
+
+ private IdpUserPO newUserPO() {
+ return IdpUserPO.builder()
+ .withUserId(1L)
+ .withUserName("tom")
+ .withPasswordHash("hash")
+ .withCurrentVersion(1L)
+ .withLastVersion(1L)
+ .withDeletedAt(0L)
+ .build();
+ }
+}
diff --git a/plugins/idp-basic/build.gradle.kts
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/provider/h2/TestIdpUserMetaH2Provider.java
similarity index 64%
copy from plugins/idp-basic/build.gradle.kts
copy to
plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/provider/h2/TestIdpUserMetaH2Provider.java
index e10209214c..ab2c5f2ea7 100644
--- a/plugins/idp-basic/build.gradle.kts
+++
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/provider/h2/TestIdpUserMetaH2Provider.java
@@ -17,23 +17,19 @@
* under the License.
*/
-plugins {
- `maven-publish`
- id("java")
- id("idea")
-}
+package org.apache.gravitino.idp.storage.mapper.provider.h2;
-dependencies {
- implementation(libs.bcprov.jdk18on)
- implementation(libs.commons.lang3)
- implementation(libs.guava)
- testImplementation(libs.junit.jupiter.api)
- testRuntimeOnly(libs.junit.jupiter.engine)
-}
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestIdpUserMetaH2Provider {
+
+ @Test
+ void testCurrentTimeMillisExpression() {
+ IdpUserMetaH2Provider provider = new IdpUserMetaH2Provider();
-tasks {
- test {
- environment("GRAVITINO_HOME", rootDir.path)
- environment("GRAVITINO_TEST", "true")
+ 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/TestIdpUserMetaPostgreSQLProvider.java
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/TestIdpUserMetaPostgreSQLProvider.java
new file mode 100644
index 0000000000..21a16b074d
--- /dev/null
+++
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/TestIdpUserMetaPostgreSQLProvider.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 TestIdpUserMetaPostgreSQLProvider {
+
+ @Test
+ void testCurrentTimeMillisExpression() {
+ IdpUserMetaPostgreSQLProvider provider = new
IdpUserMetaPostgreSQLProvider();
+
+ Assertions.assertEquals(
+ "CAST(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000 AS BIGINT)",
+ provider.currentTimeMillisExpression());
+ }
+
+ @Test
+ void testDeleteIdpUserMetasByLegacyTimeline() {
+ IdpUserMetaPostgreSQLProvider provider = new
IdpUserMetaPostgreSQLProvider();
+
+ Assertions.assertEquals(
+ "DELETE FROM idp_user_meta WHERE user_id IN (SELECT user_id FROM
idp_user_meta"
+ + " WHERE deleted_at > 0 AND deleted_at < #{legacyTimeline} LIMIT
#{limit})",
+ provider.deleteIdpUserMetasByLegacyTimeline(1L, 2));
+ }
+}
diff --git
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpUserPO.java
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpUserPO.java
new file mode 100644
index 0000000000..8bdfa0f16c
--- /dev/null
+++
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpUserPO.java
@@ -0,0 +1,105 @@
+/*
+ * 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 TestIdpUserPO {
+
+ @Test
+ public void testIdpUserPOBuilder() {
+ IdpUserPO userPO =
+ IdpUserPO.builder()
+ .withUserId(1L)
+ .withUserName("alice")
+ .withPasswordHash("hash")
+ .withCurrentVersion(1L)
+ .withLastVersion(1L)
+ .withDeletedAt(0L)
+ .build();
+
+ Assertions.assertEquals(1L, userPO.getUserId());
+ Assertions.assertEquals("alice", userPO.getUserName());
+ Assertions.assertEquals("hash", userPO.getPasswordHash());
+ Assertions.assertEquals(1L, userPO.getCurrentVersion());
+ Assertions.assertEquals(1L, userPO.getLastVersion());
+ Assertions.assertEquals(0L, userPO.getDeletedAt());
+ }
+
+ @Test
+ public void testIdpUserPOBuilderAllowsPartialFields() {
+ IdpUserPO userPO =
+ IdpUserPO.builder()
+ .withUserId(1L)
+ .withUserName("alice")
+ .withCurrentVersion(1L)
+ .withLastVersion(1L)
+ .withDeletedAt(0L)
+ .build();
+
+ Assertions.assertEquals(1L, userPO.getUserId());
+ Assertions.assertEquals("alice", userPO.getUserName());
+ Assertions.assertNull(userPO.getPasswordHash());
+ }
+
+ @Test
+ public void testEqualsAndHashCode() {
+ IdpUserPO userPO1 =
+ IdpUserPO.builder()
+ .withUserId(1L)
+ .withUserName("alice")
+ .withPasswordHash("hash")
+ .withCurrentVersion(1L)
+ .withLastVersion(1L)
+ .withDeletedAt(0L)
+ .build();
+
+ IdpUserPO userPO2 =
+ IdpUserPO.builder()
+ .withUserId(1L)
+ .withUserName("alice")
+ .withPasswordHash("hash")
+ .withCurrentVersion(1L)
+ .withLastVersion(1L)
+ .withDeletedAt(0L)
+ .build();
+
+ Assertions.assertEquals(userPO1, userPO2);
+ Assertions.assertEquals(userPO1.hashCode(), userPO2.hashCode());
+ }
+
+ @Test
+ public void testBuilderReuseDoesNotMutateBuiltObject() {
+ var builder =
+ IdpUserPO.builder()
+ .withUserId(1L)
+ .withUserName("alice")
+ .withPasswordHash("hash")
+ .withCurrentVersion(1L)
+ .withLastVersion(1L)
+ .withDeletedAt(0L);
+
+ IdpUserPO firstUser = builder.build();
+ IdpUserPO secondUser = builder.withUserName("bob").build();
+
+ Assertions.assertEquals("alice", firstUser.getUserName());
+ Assertions.assertEquals("bob", secondUser.getUserName());
+ }
+}