Part 23 of 24

Troubleshooting: 10 Common Liquibase Errors and How to Fix Them

Liquibase errors tend to cluster around the same ten problems. You will hit most of them at least once. This article gives you the exact error message to search for, the root cause, and the fastest fix — so you spend minutes recovering, not hours debugging.


Error 1: DATABASECHANGELOGLOCK — “Waiting for changelog lock”

Symptoms

Waiting for changelog lock....
Waiting for changelog lock....
Waiting for changelog lock....
liquibase.exception.LockException: Could not acquire change log lock.

Or the application hangs at startup and never finishes booting.

Root Cause

When Liquibase runs, it sets LOCKED=1 in the DATABASECHANGELOGLOCK table. When the run completes, it sets it back to LOCKED=0. If the process is killed mid-run (OOM kill, network timeout, Ctrl+C), the lock is never released — and every subsequent run waits forever.

Fix

Option A — Use the release-locks command (safest):

liquibase release-locks \
  --url=jdbc:mysql://localhost:3306/ecommerce \
  --username=liquibase_user \
  --password=secret

Option B — Direct SQL (if release-locks can’t run):

UPDATE DATABASECHANGELOGLOCK
SET LOCKED = 0, LOCKGRANTED = NULL, LOCKEDBY = NULL
WHERE ID = 1;

Verify the lock is clear:

SELECT * FROM DATABASECHANGELOGLOCK;
-- LOCKED should be 0

Prevention

  • Set a lock wait timeout so Liquibase gives up after N minutes instead of waiting forever:
    spring.liquibase.database-change-log-lock-wait-time=2m
    
  • In Kubernetes, add a pre-stop hook that runs liquibase release-locks before the pod is terminated
  • Never kill a running migration process — let it complete or fail cleanly

Error 2: Checksum Validation Failed

Symptoms

Validation Failed:
  1 change(s) have invalid checksums
    db/changelog/v1.0/1.0-create-users.yaml::001-create-users::abhay was:
    8:a1b2c3d4... but is now: 8:e5f6g7h8...

Root Cause

Liquibase stores a checksum of each changeset when it runs. If the changeset file is modified after it has been applied — even a whitespace change — the next run sees a different checksum and refuses to proceed.

Common causes:

  • Editor auto-formatting changed indentation
  • Line ending conversion (CRLF vs LF)
  • Property substitution changed a value
  • Someone edited a deployed changeset “just to fix a typo”

Fix

Option A — If the modification was accidental (whitespace/encoding only), revert it:

git diff db/changelog/v1.0/1.0-create-users.yaml
git checkout db/changelog/v1.0/1.0-create-users.yaml

Option B — If the change was intentional and you want to accept the new checksum:

Update the checksum to NULL in the tracking table (Liquibase will recalculate):

UPDATE DATABASECHANGELOG
SET MD5SUM = NULL
WHERE ID = '001-create-users'
  AND AUTHOR = 'abhay'
  AND FILENAME = 'db/changelog/v1.0/1.0-create-users.yaml';

Or use the clearCheckSums command (clears all checksums — use with care):

liquibase clearCheckSums \
  --url=jdbc:mysql://localhost:3306/ecommerce \
  --username=liquibase_user \
  --password=secret

Option C — Add validCheckSum to the changeset (permanent bypass):

- changeSet:
    id: 001-create-users
    author: abhay
    validCheckSum:
      - ANY          # accept any checksum forever
    changes:
      - createTable:
          tableName: users

Use ANY only when you’re certain the changeset hasn’t changed semantically.

Option D — Calculate the current checksum to add explicitly:

liquibase calculate-checksum \
  --changeset-identifier="db/changelog/v1.0/1.0-create-users.yaml::001-create-users::abhay"

Then add that specific value to validCheckSum instead of ANY.

Prevention

  • Never modify a deployed changeset. Add a new changeset instead.
  • Configure .editorconfig to enforce consistent line endings across your team:
    [*.yaml]
    end_of_line = lf
    
  • In CI, run liquibase validate as the first pipeline step to catch checksum mismatches before attempting deployment

Error 3: “Class was not specified and could not be determined”

Symptoms

Unexpected error running Liquibase: liquibase.exception.DatabaseException:
  Class was not specified and could not be determined.

Or:

liquibase.exception.DatabaseException: Cannot find database driver:
  com.mysql.cj.jdbc.Driver

Root Cause

The MySQL JDBC driver JAR is not on the classpath. This happens when:

  • Running Liquibase CLI without placing the driver in the lib/ directory
  • Maven/Gradle dependency scope is runtime but the build doesn’t package it correctly
  • Using the wrong driver class name

Fix

For Liquibase CLI:

Download mysql-connector-java-8.x.x.jar and place it in:

liquibase/lib/mysql-connector-java-8.x.x.jar

For Maven:

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.3.0</version>
    <scope>runtime</scope>
</dependency>

For the Maven Liquibase plugin specifically:

<plugin>
    <groupId>org.liquibase</groupId>
    <artifactId>liquibase-maven-plugin</artifactId>
    <version>4.27.0</version>
    <dependencies>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.3.0</version>
        </dependency>
    </dependencies>
</plugin>

The plugin needs the driver as its own dependency — the project dependency doesn’t automatically carry over.

Verify the driver class name:

MySQL VersionDriver Class
MySQL 8.x (modern)com.mysql.cj.jdbc.Driver
MySQL 5.x (legacy)com.mysql.jdbc.Driver

In liquibase.properties:

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/ecommerce?useSSL=false&serverTimezone=UTC

Error 4: Duplicate Changeset Identifiers

Symptoms

[ERROR] Validation Failed
  1 change(s) have duplicate identifiers
    db/changelog/v1.1/feature-a.yaml::001-add-column::developer1
    db/changelog/v1.1/feature-b.yaml::001-add-column::developer1

Root Cause

Two changesets have the same id + author + filepath combination. This often happens when two developers copy-paste a template on the same day and don’t update the ID.

Fix

Rename the duplicate in the newer file:

# Before (duplicate)
- changeSet:
    id: 001-add-column
    author: developer1

# After (fixed)
- changeSet:
    id: 2026-05-10-001-add-preferences-column
    author: developer1

If the duplicate is already deployed (one of them ran successfully), mark the other as ignored:

- changeSet:
    id: 001-add-column
    author: developer1
    ignore: true   # Liquibase skips this entirely

Prevention

Use timestamp-based IDs. The format YYYY-MM-DD-NNN-description almost guarantees uniqueness:

2026-05-10-001-add-user-preferences
2026-05-10-002-add-product-tags

Two developers working on the same day can increment the NNN counter. In code review, a duplicate ID is immediately visible.


Error 5: Changeset Exists in DB but Not in Changelog

Symptoms

liquibase.exception.MigrationFailedException: Migration failed for changeset
  db/changelog/v1.0/1.0-baseline.yaml::001-create-users::abhay:
  Reason: liquibase.exception.DatabaseException: Table 'users' already exists

The table exists in the database, but the changeset doesn’t have a precondition to handle it.

Root Cause

This happens during adoption (the table was created before Liquibase) or when someone manually ran SQL against the database without creating a changeset.

Fix

Option A — Mark the changeset as already ran (without executing it):

liquibase mark-next-changeset-ran \
  --url=jdbc:mysql://localhost:3306/ecommerce \
  --username=liquibase_user \
  --password=secret \
  --changelog-file=db/changelog/db.changelog-master.yaml

Or target a specific changeset:

liquibase mark-changeset-ran \
  --changeset-identifier="db/changelog/v1.0/1.0-baseline.yaml::001-create-users::abhay"

Option B — Add a precondition to the changeset itself:

- changeSet:
    id: 001-create-users
    author: abhay
    preConditions:
      onFail: MARK_RAN
      not:
        - tableExists:
            tableName: users
    changes:
      - createTable:
          tableName: users
          # ...

Prevention

During database adoption (Article 22), always add onFail: MARK_RAN preconditions to every baseline changeset. For new changesets, use onFail: HALT (the default) so failures are explicit.


Error 6: Rollback Not Supported

Symptoms

liquibase.exception.RollbackImpossibleException:
  No inverse to sql created for Change.

Or:

Unexpected error running Liquibase: No inverse to dropTable created

Root Cause

Liquibase cannot automatically generate rollback SQL for:

  • sql / sqlFile changesets (raw SQL)
  • dropTable, dropColumn, dropIndex
  • insert, update, delete data changes
  • Any change where the reverse operation requires knowing the prior state

Fix

Add an explicit rollback block to every changeset that needs it:

- changeSet:
    id: 2026-05-10-001-drop-legacy-column
    author: abhay
    changes:
      - dropColumn:
          tableName: users
          columnName: legacy_field
    rollback:
      - addColumn:
          tableName: users
          columns:
            - column:
                name: legacy_field
                type: VARCHAR(255)

For raw SQL changesets:

- changeSet:
    id: 2026-05-10-002-update-status-values
    author: abhay
    changes:
      - sql:
          sql: "UPDATE orders SET status = 'PENDING' WHERE status = 'NEW'"
    rollback:
      - sql:
          sql: "UPDATE orders SET status = 'NEW' WHERE status = 'PENDING'"

To explicitly mark a changeset as having no rollback (use sparingly):

    rollback:
      - empty: {}

Prevention

Write the rollback block at the same time you write the changes block — it’s much harder to reconstruct rollback logic after the fact. In CI, run liquibase future-rollback-sql to verify rollback works before deployment.


Error 7: Out-of-Order Changeset Execution

Symptoms

Changesets run in the wrong order, causing foreign key failures:

liquibase.exception.DatabaseException: Cannot add foreign key constraint
  (table 'order_items' references 'orders' which doesn't exist yet)

Or with includeAll, a new file runs before an older one it depends on.

Root Cause

includeAll processes files alphabetically. If you name files inconsistently, the order may not match logical dependencies.

Example of a broken naming scheme:

add-order-items.yaml      # runs first alphabetically
add-orders.yaml           # runs second — but order_items depends on orders!

Fix

Rename files with numeric prefixes:

001-create-orders.yaml
002-create-order-items.yaml   # now runs after orders

Or switch from includeAll to explicit include statements:

databaseChangeLog:
  - include:
      file: db/changelog/v1.1/001-create-orders.yaml
  - include:
      file: db/changelog/v1.1/002-create-order-items.yaml

Explicit includes always run in the listed order, regardless of filename.

Prevention

Adopt a strict naming convention from the start:

YYYY-MM-DD-NNN-description.yaml

Files sort correctly by date, and the NNN counter handles same-day ordering. Never use plain descriptive names like add-orders.yaml with includeAll.


Error 8: precondition Failure Blocking Migration

Symptoms

ERROR: Preconditions failed
  db/changelog/v1.1/001-add-index.yaml::001-add-index::abhay
  Precondition failed with message: index 'idx_users_email' already exists

Root Cause

A precondition with onFail: HALT (the default) blocks the entire migration when the precondition fails. Common scenario: an index already exists because it was created manually, and the changeset tries to create it again.

Fix

If the object already exists and the changeset should be skipped:

Change onFail to MARK_RAN:

preConditions:
  onFail: MARK_RAN
  not:
    - indexExists:
        tableName: users
        indexName: idx_users_email

If the object shouldn’t exist but does (real conflict):

Investigate whether someone ran DDL outside of Liquibase. If the object is correct, add the precondition. If it’s wrong, drop the object and let the changeset recreate it properly.

onFail values quick reference:

ValueBehavior
HALT (default)Stop everything, fail with error
CONTINUESkip this changeset, keep running others
MARK_RANRecord as executed, skip actual execution
WARNLog a warning, keep running

Use HALT for new migrations (failures should be loud). Use MARK_RAN for baseline/adoption changesets.


Error 9: includeAll Picks Up Wrong Files

Symptoms

Unexpected changesets running, or migration files being processed in a confusing order. Sometimes test fixture files get applied to production.

ERROR: Unexpected table 'test_users_fixture' in production database

Root Cause

includeAll without a filter includes every file in the directory matching the format. Test data files, README files, or anything that matches *.yaml will be included.

Fix

Option A — Use the filter attribute to restrict by filename pattern:

- includeAll:
    path: db/changelog/migrations/
    filter: .*migration.*\.yaml

Option B — Use resourceComparator to control ordering:

- includeAll:
    path: db/changelog/migrations/
    resourceComparator: liquibase.changelog.StandardChangeLogComparator

Option C — Move test/seed data to a separate directory and use contexts:

db/changelog/
├── migrations/          ← includeAll targets this
│   ├── 001-create-tables.yaml
│   └── 002-add-indexes.yaml
└── seed/                ← only included in dev/test contexts
    └── 001-test-data.yaml

Then in the master:

databaseChangeLog:
  - includeAll:
      path: db/changelog/migrations/
  - changeSet:
      id: seed-test-data
      author: abhay
      context: dev,test
      changes:
        # seed data here

Error 10: Spring Boot Startup Fails — Liquibase Can’t Connect

Symptoms

BeanCreationException: Error creating bean with name 'liquibase':
  Invocation of init method failed; nested exception is
  liquibase.exception.DatabaseException: Connection could not be acquired from driver

Root Cause

Spring Boot can’t establish a database connection before running Liquibase. Common causes:

  • Database is not yet available (startup race condition in Docker Compose / Kubernetes)
  • Wrong JDBC URL (typo, wrong port, wrong database name)
  • Missing credentials or wrong user/password
  • MySQL not accepting connections from the application host (GRANT not set up)

Fix

Step 1 — Verify the connection manually:

mysql -h localhost -P 3306 -u ecommerce_user -p ecommerce

If this fails, the credentials or network are the problem — not Liquibase.

Step 2 — Check the JDBC URL:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/ecommerce?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    username: ecommerce_user
    password: secret
    driver-class-name: com.mysql.cj.jdbc.Driver

Common URL mistakes:

  • Missing serverTimezone=UTC (causes timezone errors with MySQL 8)
  • Wrong port (3307 vs 3306)
  • Wrong database name
  • SSL settings conflicting with MySQL 8 defaults

Step 3 — In Docker Compose, add a health check so the app waits for MySQL:

services:
  app:
    depends_on:
      mysql:
        condition: service_healthy

  mysql:
    image: mysql:8.0
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      timeout: 3s
      retries: 10

Step 4 — In Kubernetes, use an init container:

initContainers:
  - name: wait-for-mysql
    image: busybox
    command:
      - sh
      - -c
      - "until nc -z mysql-service 3306; do echo waiting for mysql; sleep 2; done"

Step 5 — Temporarily disable Liquibase to diagnose the connection issue:

spring:
  liquibase:
    enabled: false

If the app starts, Liquibase configuration is the problem. If it still fails, the datasource itself is misconfigured.


Quick Reference: Fix Commands

# Release stuck lock
liquibase release-locks

# Clear all checksums (forces recalculation on next run)
liquibase clearCheckSums

# Calculate checksum for a specific changeset
liquibase calculate-checksum \
  --changeset-identifier="filepath::id::author"

# Mark a changeset as already ran
liquibase mark-changeset-ran \
  --changeset-identifier="filepath::id::author"

# Preview rollback SQL without executing
liquibase future-rollback-sql

# Validate all changesets (checksums, preconditions, syntax)
liquibase validate

# Show pending changesets
liquibase status --verbose

# Show all applied changesets
liquibase history

When None of These Match Your Error

  1. Run liquibase validate first — it catches the most common issues before attempting update
  2. Add --log-level=DEBUG to any Liquibase command for verbose output
  3. Check the DATABASECHANGELOG table directly — the EXECTYPE and DESCRIPTION columns often tell you exactly what happened
  4. Check the Spring Boot startup log for the full stack trace — the root cause is usually buried under several caused by layers

Golden Rules

  1. Never modify a deployed changeset — checksum errors are the consequence
  2. Never kill a running migration — stuck locks are the consequence
  3. Run liquibase validate in CI — catch problems before they hit production
  4. Write rollback blocks at the same time as changes — they’re impossible to reconstruct later
  5. Use explicit include over includeAll — ordering surprises are eliminated entirely