Files
spring-boot-demo/db-migrations-flyway-liquibase/docs/10-rollback-auto-generated-vs-explicit.md
T
asmhatreandClaude Sonnet 5 67e1b2d8a9 db-migrations-flyway-liquibase: fix dead Redgate/Liquibase doc links and back the lock-defaults claim with a real transcript
Several documentation.red-gate.com and liquibase.com URLs added in the previous commit had
since moved (Redgate restructured its docs under /flyway/reference/, Liquibase Pro pricing
moved to /pricing); this repoints them at the current, verified-200 pages. Also captures
docs/output/16-liquibase-lock-defaults-javap.txt, the trimmed javap output backing chapter
11's claim about Liquibase's default lock-poll and lock-wait settings, which chapter 11
previously asserted without a committed artifact to back it.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Q6XdRjtsp4862EM44T7i9a
2026-09-15 07:31:49 +00:00

84 lines
4.5 KiB
Markdown

# 10. Rollback: auto-generated vs explicit
[← 9. Liquibase: anatomy of an update](09-liquibase-anatomy-of-an-update.md) · [Index](../README.md) · Next: [11. Liquibase locking →](11-liquibase-locking.md)
Chapter [7](07-why-there-is-no-undo.md) 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`](../src/test/java/com/ankurm/dbmigrations/liquibase/LiquibaseRollbackAutoTest.java)
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`](output/10-liquibase-rollback-auto.txt))
## When it can't
`insert` is not in that list.
[`LiquibaseRollbackFailTest`](../src/test/java/com/ankurm/dbmigrations/liquibase/LiquibaseRollbackFailTest.java)
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`](output/11-liquibase-rollback-no-inverse.txt))
## The fix: write the inverse yourself
[`LiquibaseRollbackExplicitTest`](../src/test/java/com/ankurm/dbmigrations/liquibase/LiquibaseRollbackExplicitTest.java)
is the same `insert` changeset, this time carrying its own `rollback:` block:
```yaml
- 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`](output/12-liquibase-rollback-explicit.txt))
<svg viewBox="0 0 700 160" xmlns="http://www.w3.org/2000/svg" font-family="monospace" font-size="13">
<rect x="20" y="20" width="200" height="50" rx="6" fill="#dcfce7" stroke="#16a34a"/>
<text x="120" y="42" text-anchor="middle">createTable</text>
<text x="120" y="58" text-anchor="middle" font-size="11">auto-invertible → DROP TABLE</text>
<rect x="250" y="20" width="200" height="50" rx="6" fill="#fee2e2" stroke="#dc2626"/>
<text x="350" y="42" text-anchor="middle">insert, no rollback:</text>
<text x="350" y="58" text-anchor="middle" font-size="11">RollbackImpossibleException</text>
<rect x="480" y="20" width="200" height="50" rx="6" fill="#dcfce7" stroke="#16a34a"/>
<text x="580" y="42" text-anchor="middle">insert + rollback:</text>
<text x="580" y="58" text-anchor="middle" font-size="11">works, runs the delete</text>
<text x="20" y="110" font-size="12" fill="#475569">Whether rollback() works is a property of the CHANGE TYPE and whether you wrote a</text>
<text x="20" y="128" font-size="12" fill="#475569">rollback: block — not a global Liquibase capability you can rely on by default.</text>
</svg>
## Going deeper
- [Which change types Liquibase can auto-generate a rollback for](https://docs.liquibase.com/community/user-guide-5-0/what-automatic-rollbacks-does-liquibase-support) (`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.