liquibase migration error: DatabaseException - column already exists on re-run
Fixes Liquibase re-running a changeset that fails with 'column already exists'. Use when a changeset was applied outside Liquibase or lost its DATABASECHANGELOG row, so Liquibase tries to apply DDL that is already there. Key trigger: a DatabaseException naming a column your changeset tries to add, on re-run.
TL;DR
Mark the already-applied changeset as ran with liquibase changelog-sync, or add a column-exists precondition so the changeset skips itself where the column is present. Liquibase fails because the changeset has no row in DATABASECHANGELOG, so it executes DDL that already exists. Syncing the bookkeeping tells Liquibase the work is done without re-running it.
liquibase.exception.DatabaseException: column already existsSteps
Confirm the column really exists with the expected definition. Inspect the table in your database client. Expected: the column is present with the right type.
Check DATABASECHANGELOG for the changeset's id, author, and filepath. Expected: no row, which confirms Liquibase believes it never ran.
Pick one fix. If the database state is correct and you only need the record fixed, run:
liquibase changelog-syncExpected: the sync reports success and the changeset now has a row in DATABASECHANGELOG.
If only this changeset is affected and you want it to self-heal on every environment, add a precondition to the changeset instead: a not/columnExists check with onFail set to MARK_RAN, so the changeset marks itself ran wherever the column already exists. Expected: the changelog now carries the guard; no manual sync needed per environment.
Re-run
liquibase update. Expected:Liquibase: Update has been successfulwith no DatabaseException.
Use this when
- Re-running a changelog fails on DDL that already exists in the database
- A changeset was applied by hand, by a script, or by a different tool outside Liquibase
- The DATABASECHANGELOG row was deleted or never written
- The same changelog works on fresh databases but fails on one long-lived database
Not for this skill when
- The column exists with the WRONG definition (fix the column or write a corrective changeset; do not just mark it ran)
- A genuinely new changeset fails (that is a bug in the changeset, not bookkeeping)
- Checksum validation errors on edited changesets (that needs clear-checksums, a different fix)
- The column is missing everywhere (then the changeset should simply run)
Variant phrasings
- liquibase changeset ran twice column exists
- liquibase DatabaseException column already exists on update
- liquibase mark changeset as ran without running it
- liquibase changelogsync vs precondition MARK_RAN
Why it happens
Liquibase decides what to run purely from DATABASECHANGELOG. If the DDL was applied by hand, by a Liquibase run using a different changelog path, or the row was wiped, Liquibase sees an unrun changeset and executes it. Databases reject duplicate DDL, hence the exception. Preconditions make changesets defensive against this; changelog-sync repairs the bookkeeping after the fact.
Edge cases
changelog-syncmarks EVERYTHING unrun as ran. On a partially-migrated database that can skip changesets that genuinely never ran. Prefer the precondition approach when only one changeset is affected.- Name casing matters: on Postgres, unquoted names fold to lowercase. Make sure precondition table and column names match the real casing or the check never matches.
- If the column exists but with a different type or default than the changeset declares, marking it ran bakes in the drift. Fix the column first.
- Never edit an already-ran changeset's SQL to work around this; that breaks checksums in every other environment.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_xIByctrpep50kgDEj5PJUg