Safeguard Your Production Migrations with Liquibase Preconditions
Use Liquibase preconditions to check database state before applying migrations. Learn how to embed them, test with --dryRun, and avoid common pitfalls in this concise guide.
20 Apr 2026, 21:29 UTC

Why Preconditions Matter
When a team pushes a new change‑set to a live database, a missing table or an unexpected column type can cause the migration to fail or, worse, corrupt data. Liquibase’s preconditions let you declare these checks declaratively so that the migration either runs safely or fails gracefully before any destructive action occurs.
What Are Preconditions?
A precondition is a logical test evaluated at the start of a ChangeSet. If the test fails, Liquibase can either stop the migration, skip the ChangeSet, or mark it as ran, depending on the onFail attribute. This behaviour transforms a potential runtime error into a controlled, predictable event.
Common Precondition Types
| Type | Description |
|---|---|
dbms | Checks the database engine (e.g., PostgreSQL, MySQL). |
tableExists | Verifies that a table is present. |
columnExists | Ensures a column exists in a given table. |
sqlCheck | Runs a custom SQL query and expects a non‑zero result. |
custom | Invokes a Java class that implements liquibase.precondition.Precondition. |
Embedding Preconditions in a ChangeSet
Below is a concrete example that adds a discount column to the orders table only if the table exists and the column is not already present. The onFail="MARK_RAN" attribute tells Liquibase to record the change‑set as executed without actually applying it when the precondition fails.
<changeSet id="add-discount-column" author="devops">
<preConditions onFail="MARK_RAN" onError="HALT">
<dbms type="postgresql"/>
<tableExists tableName="orders"/>
<not>
<columnExists tableName="orders" columnName="discount"/>
</not>
</preConditions>
<addColumn tableName="orders">
<column name="discount" type="numeric(5,2)"/>
</addColumn>
</changeSet>
When Liquibase runs this change‑set against a production database:
- If
ordersdoes not exist, the precondition fails and theChangeSetis marked as ran, preventing an error. - If the column already exists, the
notwrapper causes the precondition to fail, also marking the change‑set as ran. - Only when the table exists and the column is missing will the
addColumnstatement execute.
Testing Preconditions Before Deployment
Liquibase’s --dryRun flag simulates the migration without touching the database. It prints the SQL that would be executed and indicates whether any preconditions would cause the change‑set to be skipped. Running:
liquibase --url=jdbc:postgresql://db.example.com:5432/app --username=app_user --password=secret --changeLogFile=changelog.xml --dryRun=true update
will reveal the exact path Liquibase will take, allowing you to verify that preconditions behave as expected.
Trade‑Offs and Caveats
- Runtime Overhead: Each precondition triggers an additional query. In large migrations with many change‑sets, this can add noticeable latency.
- Masking Issues: Using
onFail="MARK_RAN"hides failures. If the precondition logic is wrong, the change‑set may be skipped silently, leading to missing schema changes. - Version Drift: In long‑running environments, a precondition that was true at migration time may become false later. Keep preconditions in sync with the evolving schema.
Practical Takeaway
Start by adding simple tableExists or columnExists preconditions to your most critical change‑sets. Test them with --dryRun, review the generated plan, and document the required database state in your migration checklist. This disciplined approach turns fragile migrations into reliable, auditable steps that protect your production data.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.