Using Liquibase Preconditions to Safeguard Schema Migrations
Learn how Liquibase preconditions guard against unintended schema changes, see a concrete XML example, weigh trade‑offs, and get step‑by‑step guidance for safe, idempotent deployments.
19 Sept 2026, 06:32 UTC

Why Preconditions Matter
When you run a Liquibase changeset against a database, you expect the changes to apply only if the target environment is in the right state. A common pitfall is running a migration that assumes a table or column already exists, only to hit a runtime error in a new or partially‑deployed environment. Liquibase’s preconditions feature lets you declare those assumptions up front, so the migration will either skip or abort before touching the database.
What a Precondition Is
A precondition is a declarative check that Liquibase evaluates against the current database before executing the changes in a changeset. The check can be simple—tableExists or columnExists—or more complex using logical operators like and, or, and not. If the check fails, Liquibase can:
- Abort the entire migration (default behavior).
- Continue without recording the changeset in
DATABASECHANGELOG(useful for idempotent scripts). - Ignore the failure and proceed (set
failOnError="false").
Because preconditions are evaluated but never executed, they are safe to run in any environment, including read‑only replicas.
Concrete Example
Below is a minimal XML changeset that adds a new column email to a users table, but only if the table exists and the column does not. The precondition uses logical operators to combine checks.
<changeSet id="add-email-to-users" author="alice">
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="users"/>
<not>
<columnExists tableName="users" columnName="email"/>
</not>
</and>
</preConditions>
<addColumn tableName="users">
<column name="email" type="VARCHAR(255)"/>
</addColumn>
</changeSet>
Explanation of the key parts:
onFail="MARK_RAN"tells Liquibase to record the changeset as executed even if the precondition fails. This prevents the same changeset from running again on a database that already has the desired state.- The
andoperator requires both conditions to be true. Ifusersdoes not exist, the changeset is skipped. - The
notwrapper negates thecolumnExistscheck, ensuring we only add the column if it’s missing.
Running this changeset against a database that already has a users.email column will result in the precondition failing, Liquibase will mark the changeset as ran, and the addColumn command will not execute.
Testing the Behavior
- Create a test database (e.g.,
CREATE DATABASE testdb;). - Run Liquibase with the changeset using the
--url,--username, and--passwordparameters. Verify that theuserstable is absent; the changeset should be skipped and theDATABASECHANGELOGtable should contain a record withMARK_RANstatus. - Add the
userstable but omit theemailcolumn. Re‑run Liquibase. TheaddColumnshould now execute, and the changeset should be marked asEXECUTED. - Run Liquibase again. Since the column now exists, the precondition fails again, but the changeset is marked as
MARK_RAN, so no additional changes occur.
Trade‑offs and Limitations
- Masking Drift: If you overuse preconditions, you might inadvertently hide schema drift. For example, a precondition that always passes because a table exists will let a changeset run on a database that’s out of sync, potentially causing data loss.
- Pipeline Halts: The default
onFail="HALT"can stop CI/CD pipelines. Teams must decide whether to useMARK_RANorCONTINUEbased on risk appetite. - Complexity: Nested logical operators can become hard to read. Keep preconditions simple and document them.
- Database Specificity: Some preconditions (e.g.,
dbms) are database‑specific. Test against all target DBMS variants.
Best‑Practice Checklist
- Use
failOnError="false"only when you have a clear fallback strategy. - Prefer
MARK_RANfor idempotent migrations that should not rerun. - Document each precondition’s intent in the changelog comment.
- Run a dry‑run (
--dryRun) before production to verify precondition evaluation. - Inspect
DATABASECHANGELOGafter deployment to confirm the status of each changeset.
Actionable Next Steps
1. Audit existing changesets for missing preconditions. Add them where assumptions exist.
2. Define a policy for onFail handling that aligns with your release process.
3. Automate tests that create a clean database, run Liquibase, and assert that the expected changesets ran or were skipped.
4. Review DATABASECHANGELOG entries after each deployment to ensure the state matches expectations.
By integrating preconditions thoughtfully, you reduce the risk of accidental schema changes, keep migrations idempotent, and strengthen your deployment pipeline’s resilience.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.