Liquibase: Preconditions, Contexts & Labels
|
This section documents schema/database evolution using version-controlled migration tools — Liquibase (relational/SQL), Mongock (MongoDB, Java-native) and Flamingock (Mongock’s successor, generalized to any external system) — as described by each tool’s own official documentation: liquibase.org and docs.liquibase.com for Liquibase, docs.mongock.io for Mongock, and docs.flamingock.io for Flamingock — which are the references these pages are written and verified against. This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production. Mongock is now maintained under the Flamingock GitHub organization (github.com/flamingock/mongock), and Flamingock is positioned as its successor — see Coming from Mongock. Readers starting a new project should default to Flamingock unless they have a specific reason to stay on Mongock. This section’s bibliography lists the reference material consulted while preparing these pages. |
Preconditions
A precondition is a state check that gates whether a changeset is allowed to run — What are Preconditions?. Common
checks include tableExists, columnExists, rowCount and sqlCheck (an arbitrary query whose result must
match an expected value). A precondition failure can be configured to halt the whole changelog, skip just that
changeset, mark it as run without executing it, or warn — controlled by onFail:
- changeSet:
id: add-email-column
author: alberto
preConditions:
onFail: MARK_RAN
not:
- columnExists:
tableName: person
columnName: email
changes:
- addColumn:
tableName: person
columns:
- column: { name: email, type: varchar(255) }
This makes the changeset a no-op (recorded as applied, nothing executed) if email already exists — useful when
a column might have been added out-of-band, or when reconciling two changelogs that both add it.
Contexts
A context is a logical expression attached to a changeset, evaluated at update time against a context
expression the person running Liquibase supplies — What are
Contexts?:
- changeSet:
id: seed-test-data
author: alberto
context: "test and not production"
changes:
- insert: { tableName: person, columns: [ { column: { name: name, value: "Test User" } } ] }
liquibase --contexts=test update
The changeset only runs when the supplied --contexts value satisfies its expression. Contexts support boolean
operators (and, or, not), so a changeset can be scoped to exactly the environments it belongs in.
Labels
A label is a simpler tag, matched at run time the same way, but without contexts' boolean-expression semantics by default — What are Labels?:
- changeSet:
id: add-index-for-reporting
author: alberto
labels: reporting
changes:
- createIndex: { tableName: person, indexName: idx_person_name, columns: [ { column: { name: name } } ] }
liquibase --labels=reporting update
Contexts vs. Labels: Who Decides
The practical difference is who controls the intent:
-
Contexts — the changeset author decides the logic when writing the changelog (
test and not production). The person runningupdateonly supplies which contexts are currently active; they can’t change what the changeset’s own expression means. -
Labels — the person running
updatedecides which labelled changesets to include, by choosing which--labelsvalue to pass at invocation time. The changeset author just attaches a tag; the operational decision of when to apply it happens at deploy time.
Use contexts when the routing logic is a property of the change itself (test-only seed data, an environment this changeset must never touch); use labels when the same changelog needs to be run selectively depending on what a particular deployment run is for (e.g. "just the reporting-related changes this time").
Combining a Precondition with a Context
- changeSet:
id: backfill-legacy-status
author: alberto
context: "migration"
preConditions:
onFail: MARK_RAN
columnExists:
tableName: person
columnName: legacy_status
changes:
- update:
tableName: person
columns:
- column: { name: legacy_status, value: "UNKNOWN" }
where: "legacy_status IS NULL"
This changeset only even attempts to run under the migration context, and even then only if legacy_status
still exists — so it becomes a safe no-op once that column is eventually dropped by a later changeset, instead
of failing the whole changelog.