Make Optional Liquibase Migrations Environment-Aware with Preconditions
Use Liquibase preconditions to skip optional schema changes safely. Learn how tableExists, columnExists, and onFail strategies prevent deployment failures.
07 Jul 2026, 01:55 UTC

The Problem: Optional Changes That Break Deployments
Not every schema change is mandatory. A team might add a covering index to support a new report, or add a nullable column for a feature flag, but the same changelog runs against staging, production, and customer-managed instances that are on different versions. When Liquibase reaches a changeSet that assumes an object exists, and it does not, the update fails. The operator then faces a partially applied deployment and manual cleanup.
Preconditions solve this by letting a changeSet inspect the database before it runs. If the required schema state is absent, Liquibase can skip the changeSet instead of crashing the migration.
What a Precondition Actually Checks
A precondition is a logical block inside a changeSet. Liquibase evaluates it immediately before the changeSet's changes. Common checks include tableExists, columnExists, and indexExists. These tags query the database catalog, so the connecting user needs read access to the schema metadata.
Logical wrappers <and>, <or>, and <not> let you combine checks. The onFail attribute controls what happens when the precondition returns false:
HALT: stops the entire update. Use this when later changeSets depend on the current one.CONTINUE: skips the current changeSet and proceeds to the next. Useful for optional performance patches.MARK_RAN: skips the changeSet and records it inDATABASECHANGELOGso Liquibase will not try it again.
Worked Example: Conditional Index on a Large Table
Suppose you want to add an index on orders(customer_id) to speed up a dashboard query. The index should only be created if the orders table exists and the index is not already present. This changeSet is safe to run on older databases that lack the table, and on newer ones where a DBA already added the index manually.
<changeSet id='add-orders-customer-index' author='platform-team'>
<preConditions onFail='CONTINUE'>
<and>
<tableExists tableName='orders'/>
<not>
<indexExists tableName='orders' indexName='idx_orders_customer_id'/>
</not>
</and>
</preConditions>
<createIndex tableName='orders' indexName='idx_orders_customer_id'>
<column name='customer_id'/>
</createIndex>
</changeSet>
Where to run: From the service repository root, using the Liquibase CLI or build plugin: liquibase update --changelog-file=src/main/resources/db/changelog.xml --defaults-file=liquibase.properties
Permissions: The database user needs SELECT on the relevant catalog views (for example, information_schema.tables and information_schema.statistics on MySQL, or pg_indexes on PostgreSQL) plus CREATE or ALTER on the target table.
Expected check: If orders is missing, the precondition fails, onFail='CONTINUE' skips the index creation, and Liquibase proceeds to the next changeSet without a SQL error.
Risk: MARK_RAN can hide real problems. If the precondition fails because of a typo in the table name, Liquibase records the changeSet as executed and never retries it, even after you fix the changelog.
Trade-offs and Maintenance
Preconditions add safety but also noise. A changelog with dozens of <preConditions> blocks becomes harder to scan than a plain list of changes. Teams often extract shared checks into included changelogs or limit preconditions to optional changeSets only.
Strict preconditions can also cause false negatives. If your staging database uses a different table catalog or schema name than production, a tableExists check may pass in one environment and fail in another. Pair preconditions with context attributes when environment-specific logic is unavoidable.
Verifying Before Production
- Enable debug logging:
liquibase.logLevel=debug. The log shows each precondition result and the chosenonFailaction. - Run against a disposable test database that intentionally lacks the target table. Confirm the update skips the changeSet rather than throwing a SQL exception.
- Query
DATABASECHANGELOGafter the run. If you usedMARK_RAN, confirm the row exists; if you usedCONTINUE, confirm it does not.
Preconditions are most valuable for optional changes that should degrade gracefully. For required schema updates, let Liquibase fail loudly so the deployment stops before inconsistent data accumulates.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.