package com.ankurm.dbmigrations; import java.nio.file.Path; import java.sql.Connection; import javax.sql.DataSource; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration; import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration; import org.springframework.boot.liquibase.autoconfigure.LiquibaseAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; /** * Both starters, both enabled, one database — the question every "which one do I add" thread * on Stack Overflow eventually asks in a comment. The naive answer is no: Boot wires Liquibase's * {@code SpringLiquibase} bean before Flyway's {@code FlywayMigrationInitializer} in this * configuration (an artifact of bean registration order here, not a documented contract), so by * the time Flyway runs, Liquibase has already created tables, and Flyway's own safety check — * "I found tables I don't recognize and no schema history table" — refuses to start. Turning on * {@code baselineOnMigrate} alone is not enough here: its default {@code baselineVersion} (1) * tells Flyway "pretend V1 already ran", which only makes sense if the existing schema really * matches what V1 would have built. Liquibase's changelog built an unrelated {@code product} * table, not {@code customer}, so V1 also has to be told to baseline-version 0 (nothing has run * yet, from Flyway's point of view) so it runs its own migration set from scratch alongside * whatever Liquibase already put there. See docs/14-running-both-at-once.md. */ class BothTogetherTest { @Test void enablingBothNaivelyFailsUntilFlywayIsToldToBaseline(@TempDir Path tmp) throws Exception { Transcript t = Transcript.start("15-both-together-same-datasource", "Flyway and Liquibase, both enabled: the naive failure, then the fix"); Path naiveDb = tmp.resolve("both-together-naive"); t.section("naive: both enabled, no other configuration"); new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, FlywayAutoConfiguration.class, LiquibaseAutoConfiguration.class)) .withPropertyValues( "spring.datasource.url=jdbc:h2:file:" + naiveDb, "spring.datasource.username=sa", "spring.flyway.locations=classpath:db/migration", "spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.yaml") .run(ctx -> { assertThat(ctx).hasFailed(); String message = rootMessage(ctx.getStartupFailure()); t.line("context failed to start: " + message); assertThat(message).containsIgnoringCase("non-empty").containsIgnoringCase("baseline"); }); Path fixedDb = tmp.resolve("both-together-fixed"); t.section("fixed: spring.flyway.baseline-on-migrate=true, spring.flyway.baseline-version=0"); new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, FlywayAutoConfiguration.class, LiquibaseAutoConfiguration.class)) .withPropertyValues( "spring.datasource.url=jdbc:h2:file:" + fixedDb, "spring.datasource.username=sa", "spring.flyway.locations=classpath:db/migration", "spring.flyway.baseline-on-migrate=true", "spring.flyway.baseline-version=0", "spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.yaml") .run(ctx -> { assertThat(ctx).hasNotFailed(); DataSource ds = ctx.getBean(DataSource.class); try (Connection conn = ds.getConnection()) { t.line(DbDump.table(conn, "select table_name from information_schema.tables where table_schema='PUBLIC' order by table_name")); t.section("flyway_schema_history — baseline row at 0, then V1/V2 ran for real"); String flywayHistory = DbDump.table(conn, "select \"version\", \"description\", \"type\", \"success\" from \"flyway_schema_history\" order by \"installed_rank\""); t.line(flywayHistory); t.section("databasechangelog — Liquibase's own bookkeeping, untouched by Flyway"); t.line(DbDump.table(conn, "select id, author, exectype from databasechangelog order by orderexecuted")); String tables = DbDump.table(conn, "select table_name from information_schema.tables where table_schema='PUBLIC' order by table_name"); assertThat(tables).contains("CUSTOMER").contains("PRODUCT").contains("DATABASECHANGELOG"); assertThat(flywayHistory).contains("create customer").contains("seed customer"); } }); t.write(); } private static String rootMessage(Throwable t) { Throwable cause = t; while (cause.getCause() != null) { cause = cause.getCause(); } return cause.getClass().getSimpleName() + ": " + cause.getMessage(); } }