# 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)) 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](https://docs.liquibase.com/workflows/liquibase-community/how-to-apply-or-revert-changes.html) (`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.