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 running update only supplies which contexts are currently active; they can’t change what the changeset’s own expression means.

  • Labels — the person running update decides which labelled changesets to include, by choosing which --labels value 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.