Files
spring-boot-demo/db-migrations-flyway-liquibase/docs/10-rollback-auto-generated-vs-explicit.md
T
asmhatreandClaude Sonnet 5 3908331431 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
2026-09-15 07:08:57 +00:00

4.5 KiB

10. Rollback: auto-generated vs explicit

← 9. Liquibase: anatomy of an update · Index · Next: 11. Liquibase locking →

Chapter 7 showed Flyway Community has no working undo at all. Liquibase's answer is rollback() — and whether it works depends entirely on the change type.

When Liquibase can invert a change on its own

createTable is one of the change types Liquibase knows how to invert without being told how — it just runs DROP TABLE. LiquibaseRollbackAutoTest writes no rollback: block anywhere in its changelog, calls update(), confirms the session table exists, then calls rollback(1, ...):

after update(): session table exists = true
after rollback(1): session table exists = false

(from docs/output/10-liquibase-rollback-auto.txt)

When it can't

insert is not in that list. LiquibaseRollbackFailTest applies a createTable changeset followed by an insert changeset, then calls rollback(1, ...) — which only asks to roll back the most recent changeset, the insert. It fails for real:

after update(): both changesets applied
rollback(1) threw: liquibase.exception.CommandExecutionException
message: liquibase.exception.LiquibaseException: liquibase.exception.RollbackFailedException: liquibase.exception.RollbackImpossibleException: No inverse to liquibase.change.core.InsertDataChange created

(from docs/output/11-liquibase-rollback-no-inverse.txt)

The fix: write the inverse yourself

LiquibaseRollbackExplicitTest is the same insert changeset, this time carrying its own rollback: block:

- insert:
    tableName: audit_log
    columns:
      - {column: {name: event, value: 'system-start'}}
  rollback:
    - delete:
        tableName: audit_log
        where: event='system-start'

rollback(1, ...) now succeeds — the table stays, the row is gone:

audit_log after rollback(1) — table still exists, the row is gone:
ID | EVENT
---+------
(0 rows)

(from docs/output/12-liquibase-rollback-explicit.txt)

createTable auto-invertible → DROP TABLE insert, no rollback: RollbackImpossibleException insert + rollback: works, runs the delete Whether rollback() works is a property of the CHANGE TYPE and whether you wrote a rollback: block — not a global Liquibase capability you can rely on by default.

Going deeper

  • Which change types Liquibase can auto-generate a rollback for (rel="nofollow") — createTable, addColumn, and a handful of other structural changes; almost anything involving data (insert, update, delete, most sql: changes) needs an explicit rollback: block.
  • A team relying on rollback in production is really relying on discipline: every changeset that touches data needs its rollback written and tested at the time the changeset is written, not discovered missing during an actual incident — rollback(1, ...) failing is the worst possible moment to learn insert has no inverse.
  • Liquibase also supports rollbackCount, rollbackToDate and rolling back by tag — this module exercises only the single-changeset rollback(int, ...) overload.