In this guide (8 items)
Liquibase Guidelines
Liquibase Guidelines
Iron Rule: Never Modify an Already-Committed Changeset
Once a Liquibase changeset has been merged or deployed, its content is immutable. Liquibase
hashes every changeset and stores the checksum in DATABASECHANGELOG. If the file content
changes, the stored checksum no longer matches the recomputed one and Liquibase aborts the next
deploy with:
Validation Failed: N change sets check sum path/V<id>__<name>.sql::<id>::<author> was: 9:<oldHash> but is now: 9:<newHash>Production won’t start. Rollback is painful. The fix is process, not patch.
What “modify” means
Any of these break the checksum:
- Editing SQL (add/remove
IF EXISTS, change a column type, fix a typo). - Re-indenting, retabbing, changing line endings.
- Editing comments inside the changeset body.
- Re-ordering statements inside one changeset.
- Splitting one changeset into two without keeping the original id intact.
What “already-committed” means
A changeset is committed once it has been merged to the integration branch (develop/main) OR
deployed to any environment whose DATABASECHANGELOG you do not own. If it only ever ran on
your laptop, you may still edit — but always verify against a fresh DB before pushing.
The only safe path: add a new changeset
Need to change behavior? Write a new file with a later timestamp, give it a unique id, and let it correct or supersede the previous one.
-- V20260601000000__fix_buyer_email_backfill.sql-- changeset andres:20260601000000-01UPDATE tickets SET buyer_email = NULL WHERE ...-- rollback UPDATE tickets SET buyer_email = ... WHERE ...;Same applies to typos in column names, wrong constraints, missing indexes — fix forward, never back-edit.
Emergency escape hatches (use sparingly)
When forward-fixing is impossible (e.g. local-only changeset never reached prod and you need a clean history), one of these may be acceptable. Document the choice in the PR.
- Recompute checksum — run
liquibase clearCheckSumson every environment that already saw the changeset. Coordinates a manual step across every cluster. Avoid in prod. <validCheckSum>ANY</validCheckSum>— instructs Liquibase to skip checksum verification for the changeset. Hides drift forever, so the next reader can’t trust the changelog.- Delete + re-include with a new id — only on a never-deployed changeset, and only on a branch nobody else has pulled.
None of these substitute for the discipline of forward-only migrations.
Environment-specific seed data
Need a table that exists only in tests (e.g. a cross-service users placeholder)? Use a
precondition that no-ops in production instead of two divergent changelogs:
-- changeset andres:20260525235959-01-- preconditions onFail:MARK_RAN onError:MARK_RAN-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM sys.tables WHERE name = 'users'CREATE TABLE users (...);-- rollback DROP TABLE users;Production already has the table → precondition fails → changeset is marked MARK_RAN and never
executes. Tests start with an empty DB → precondition passes → table is created. One changelog,
one history.
Companion: zero-downtime / expand-contract
For column drops, type changes, or NOT NULL on populated tables, follow arch-zero-downtime-migration
(expand → backfill → contract). That skill assumes this rule — every phase is a brand-new changeset.
Pre-merge checklist
- Is the file I’m editing a changeset that has ever been merged or deployed? → STOP, add a new one.
- Does the new changeset have a unique id within the file and across the directory?
- Did I run the full migration locally against an empty DB?
- Did I include a
-- rollbackclause? - Did I avoid
validCheckSum: ANYandclearCheckSums?