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
84 lines
4.5 KiB
Markdown
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.
|