Add db-migrations-flyway-liquibase: Flyway vs Liquibase migrations, rollbacks and baselines on Spring Boot 4.1
Companion code for the Flyway vs Liquibase article: checksum validation, out-of-order and repeatable migrations, baselining an existing schema, Flyway Community's undo/diff/deploy stubs, concurrent-startup locking for both tools, Liquibase changeset identity and rollback (auto-generated vs explicit), a verified Liquibase 5.0.3 filename-caching defect, the new OSS license service, the FSL license change, and running both tools against one database. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Q6XdRjtsp4862EM44T7i9a
This commit is contained in:
+99
@@ -0,0 +1,99 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user