diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java index 930c0596d1f..b27a063466e 100755 --- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java +++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java @@ -29,9 +29,9 @@ import java.util.Set; import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider; import org.keycloak.connections.jpa.updater.JpaUpdaterProvider; -import org.keycloak.connections.jpa.updater.liquibase.conn.CustomChangeLogHistoryService; import org.keycloak.connections.jpa.updater.liquibase.conn.KeycloakLiquibase; import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider; +import org.keycloak.connections.jpa.updater.liquibase.conn.MySQLCustomChangeLogHistoryService; import org.keycloak.connections.jpa.util.JpaUtils; import org.keycloak.models.KeycloakSession; @@ -215,6 +215,13 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider { // in org.keycloak.connections.jpa.updater.liquibase.lock.CustomLockService.init() called indirectly from // KeycloakApplication constructor (search for waitForLock() call). Hence it is not included in the creation script. + // For MySQL, add primary key to DATABASECHANGELOG table (handled by MySQLCustomChangeLogHistoryService at runtime) + ChangeLogHistoryService changeLogHistoryService = ChangeLogHistoryServiceFactory.getInstance().getChangeLogService(database); + if (changeLogHistoryService instanceof MySQLCustomChangeLogHistoryService customChangeLogHistoryService) { + loggingExecutor.comment("Add primary key to DATABASECHANGELOG table for MySQL"); + loggingExecutor.execute(customChangeLogHistoryService.getAddDatabaseChangeLogPKStatement()); + } + executorService.setExecutor(LiquibaseConstants.JDBC_EXECUTOR, database, oldTemplate); } @@ -286,7 +293,7 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider { private void resetLiquibaseServices(KeycloakLiquibase liquibase) { liquibase.resetServices(); - ChangeLogHistoryServiceFactory.getInstance().register(new CustomChangeLogHistoryService()); + ChangeLogHistoryServiceFactory.getInstance().register(new MySQLCustomChangeLogHistoryService()); } private List getLiquibaseUnrunChangeSets(Liquibase liquibase) throws LiquibaseException { diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/CustomChangeLogHistoryService.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/MySQLCustomChangeLogHistoryService.java similarity index 76% rename from model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/CustomChangeLogHistoryService.java rename to model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/MySQLCustomChangeLogHistoryService.java index 4a5445d1713..29207fe7f30 100644 --- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/CustomChangeLogHistoryService.java +++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/MySQLCustomChangeLogHistoryService.java @@ -16,11 +16,16 @@ */ package org.keycloak.connections.jpa.updater.liquibase.conn; +import org.keycloak.connections.jpa.updater.liquibase.LiquibaseConstants; + +import liquibase.Scope; import liquibase.change.ColumnConfig; import liquibase.changelog.StandardChangeLogHistoryService; import liquibase.database.Database; import liquibase.database.core.MySQLDatabase; import liquibase.exception.DatabaseException; +import liquibase.executor.ExecutorService; +import liquibase.executor.LoggingExecutor; import liquibase.executor.jvm.ChangelogJdbcMdcListener; import liquibase.snapshot.InvalidExampleException; import liquibase.snapshot.SnapshotGeneratorFactory; @@ -33,7 +38,7 @@ import liquibase.structure.core.Table; * * @author hmlnarik */ -public class CustomChangeLogHistoryService extends StandardChangeLogHistoryService { +public class MySQLCustomChangeLogHistoryService extends StandardChangeLogHistoryService { private boolean serviceInitialized; @@ -50,6 +55,13 @@ public class CustomChangeLogHistoryService extends StandardChangeLogHistoryServi serviceInitialized = true; + + // Skip execution in manual migration mode - the PK statement is added to the export by LiquibaseJpaUpdaterProvider + ExecutorService executorService = Scope.getCurrentScope().getSingleton(ExecutorService.class); + if (executorService.getExecutor(LiquibaseConstants.JDBC_EXECUTOR, getDatabase()) instanceof LoggingExecutor) { + return; + } + if (!existsDatabaseChangelogPK()) { createDatabaseChangelogPK(); } @@ -74,8 +86,7 @@ public class CustomChangeLogHistoryService extends StandardChangeLogHistoryServi } private void createDatabaseChangelogPK() throws DatabaseException { - AddPrimaryKeyStatement pkStatement = new AddPrimaryKeyStatement(getLiquibaseCatalogName(), getLiquibaseSchemaName(), getDatabaseChangeLogTableName(), - ColumnConfig.arrayFromNames("ID, AUTHOR, FILENAME"), "PK_DATABASECHANGELOG"); + AddPrimaryKeyStatement pkStatement = getAddDatabaseChangeLogPKStatement(); try { ChangelogJdbcMdcListener.execute(getDatabase(), ex -> ex.execute(pkStatement)); getDatabase().commit(); @@ -86,4 +97,9 @@ public class CustomChangeLogHistoryService extends StandardChangeLogHistoryServi } } } + + public AddPrimaryKeyStatement getAddDatabaseChangeLogPKStatement() { + return new AddPrimaryKeyStatement(getLiquibaseCatalogName(), getLiquibaseSchemaName(), getDatabaseChangeLogTableName(), + ColumnConfig.arrayFromNames("ID, AUTHOR, FILENAME"), "PK_DATABASECHANGELOG"); + } } diff --git a/model/jpa/src/main/resources/META-INF/services/liquibase.changelog.ChangeLogHistoryService b/model/jpa/src/main/resources/META-INF/services/liquibase.changelog.ChangeLogHistoryService index 4ff1eb6cb77..4ce26983908 100644 --- a/model/jpa/src/main/resources/META-INF/services/liquibase.changelog.ChangeLogHistoryService +++ b/model/jpa/src/main/resources/META-INF/services/liquibase.changelog.ChangeLogHistoryService @@ -15,4 +15,4 @@ # limitations under the License. # -org.keycloak.connections.jpa.updater.liquibase.conn.CustomChangeLogHistoryService \ No newline at end of file +org.keycloak.connections.jpa.updater.liquibase.conn.MySQLCustomChangeLogHistoryService \ No newline at end of file diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/QuarkusJpaUpdaterProvider.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/QuarkusJpaUpdaterProvider.java index 65494141e0b..5d6b99f7615 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/QuarkusJpaUpdaterProvider.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/QuarkusJpaUpdaterProvider.java @@ -32,9 +32,9 @@ import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider; import org.keycloak.connections.jpa.updater.JpaUpdaterProvider; import org.keycloak.connections.jpa.updater.liquibase.LiquibaseConstants; import org.keycloak.connections.jpa.updater.liquibase.ThreadLocalSessionContext; -import org.keycloak.connections.jpa.updater.liquibase.conn.CustomChangeLogHistoryService; import org.keycloak.connections.jpa.updater.liquibase.conn.KeycloakLiquibase; import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider; +import org.keycloak.connections.jpa.updater.liquibase.conn.MySQLCustomChangeLogHistoryService; import org.keycloak.connections.jpa.util.JpaUtils; import org.keycloak.models.KeycloakSession; @@ -217,6 +217,13 @@ public class QuarkusJpaUpdaterProvider implements JpaUpdaterProvider { // in org.keycloak.connections.jpa.updater.liquibase.lock.CustomLockService.init() called indirectly from // KeycloakApplication constructor (search for waitForLock() call). Hence it is not included in the creation script. + // For MySQL, add primary key to DATABASECHANGELOG table (handled by MySQLCustomChangeLogHistoryService at runtime) + ChangeLogHistoryService changeLogHistoryService = ChangeLogHistoryServiceFactory.getInstance().getChangeLogService(database); + if (changeLogHistoryService instanceof MySQLCustomChangeLogHistoryService customChangeLogHistoryService) { + loggingExecutor.comment("Add primary key to DATABASECHANGELOG table for MySQL"); + loggingExecutor.execute(customChangeLogHistoryService.getAddDatabaseChangeLogPKStatement()); + } + executorService.setExecutor(LiquibaseConstants.JDBC_EXECUTOR, database, oldTemplate); } @@ -283,7 +290,7 @@ public class QuarkusJpaUpdaterProvider implements JpaUpdaterProvider { private void resetLiquibaseServices(KeycloakLiquibase liquibase) { liquibase.resetServices(); - getChangeLogHistoryService().register(new CustomChangeLogHistoryService()); + getChangeLogHistoryService().register(new MySQLCustomChangeLogHistoryService()); } private ChangeLogHistoryServiceFactory getChangeLogHistoryService() { diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/BasicDatabaseTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/BasicDatabaseTest.java index 1e2ad2827d4..c5ea36cd50c 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/BasicDatabaseTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/BasicDatabaseTest.java @@ -85,7 +85,7 @@ public abstract class BasicDatabaseTest { cliResult.assertMessage("Import finished successfully"); } - public void assertManualDbInitialization(CLIResult cliResult, RawDistRootPath rawDistRootPath) { + public String assertManualDbInitialization(CLIResult cliResult, RawDistRootPath rawDistRootPath) { cliResult.assertMessage("Database not initialized, please initialize database with"); var output = readKeycloakDbUpdateScript(rawDistRootPath); @@ -94,6 +94,8 @@ public abstract class BasicDatabaseTest { assertThat(output, containsString("Keycloak database creation script - apply this script to empty DB")); assertThat(output, containsString("Change Log: META-INF/jpa-changelog-master.xml")); assertThat(output, containsString("Changeset META-INF/jpa-changelog-26.2.6.xml")); + + return output; } protected static String readKeycloakDbUpdateScript(RawDistRootPath path) { diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/dist/MySQLDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/dist/MySQLDistTest.java index 0dbbc2d2deb..d9dc59bb242 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/dist/MySQLDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/dist/MySQLDistTest.java @@ -11,6 +11,9 @@ import io.quarkus.test.junit.main.Launch; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; + @DistributionTest(removeBuildOptionsAfterBuild = true) @WithDatabase(alias = "mysql") public class MySQLDistTest extends MySQLTest { @@ -27,7 +30,11 @@ public class MySQLDistTest extends MySQLTest { @Test @Launch({"start", AbstractAutoBuildCommand.OPTIMIZED_BUILD_OPTION_LONG, "--spi-connections-jpa-quarkus-migration-strategy=manual", "--spi-connections-jpa-quarkus-initialize-empty=false", "--http-enabled=true", "--hostname-strict=false",}) public void testKeycloakDbUpdateScript(CLIResult cliResult, RawDistRootPath rawDistRootPath) { - assertManualDbInitialization(cliResult, rawDistRootPath); + String output = assertManualDbInitialization(cliResult, rawDistRootPath); + + // Verify MySQL primary key is included + assertThat(output, containsString("Add primary key to DATABASECHANGELOG table for MySQL")); + assertThat(output, containsString("ALTER TABLE keycloak.DATABASECHANGELOG ADD PRIMARY KEY (ID, AUTHOR, FILENAME);")); } @Tag(DistributionTest.STORAGE)