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:
2026-09-15 07:08:57 +00:00
co-authored by Claude Sonnet 5
parent b02fbe1416
commit 3908331431
61 changed files with 3128 additions and 0 deletions
@@ -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();
}
}