
Data
Database Migration Tools: Rollback, Review and Evidence
Database migration tool choice turns on rollback behaviour, reviewability and the record left behind. Flyway, Liquibase, Atlas and Sqitch, compared.
Migration tooling is the least-revisited decision in a codebase and the one most likely to be named in an incident review. It is argued on an axis — versioned SQL against declarative diff — that predicts almost nothing about the night it matters.
The axis everyone argues about decides the least
That axis describes how you author a change, not what happens when one goes wrong at 02:00. Both styles apply DDL to a live database and are bounded by the same engine semantics. Pick the one your reviewers read fluently.
Three axes do separate them. Where the licence boundary falls — per command, not per product. What the tool writes into your database as a record of what happened, which outlives the tool. And whether its rollback story survives a migration that failed halfway, where the vendors' own documentation is more candid than their comparison pages.
Some of this is already public: Flyway's undo sitting behind Teams, Liquibase Community moving to the Functional Source License at 5.0 with a two-year conversion back to Apache 2.0, and the broad shape of Atlas's reduced Community build against its default binary are all in the listicles ranking for this query. What is not out there is the per-command split, the three Atlas pages that contradict each other and are quoted below unreconciled, what each tool leaves behind after a revert, and the CNCF Governing Board's vote on the downstream consequence.
There are no throughput numbers below — benchmarks for schema tools measure the engine, not the tool. Every fact here was fetched on 1 October 2026, and vendor edition pages change without a version bump, so re-check any gating claim before it becomes load-bearing.
Where each tool's licence boundary actually falls
"Rollback is a paid feature" describes none of these four accurately, and acting on it means either paying for something you already have or planning an incident response around a command you may not run.
Flyway. The undo-migrations page is badged EDITION: TEAMS and the command reference reads "Undo Description Flyway Teams" — the floor, not an exclusive tier, since undo is included in Enterprise too. What sits outside Community is the undo command, not the ability to reverse a change: a Community user can hand-write a forward migration that undoes an earlier one.
Liquibase. The rollback table splits along a line almost nobody quotes correctly. rollback, rollback-to-date and rollback-count are documented as all editions; only rollback-one-changeset and rollback-one-update are Liquibase Secure. Ordinary sequential rollback is not paywalled.
Atlas. Here the boundary is a build, not a price. Its community-edition page: "The Community Edition of Atlas is licensed under the Apache 2.0 license. The standard distribution is built from the same open-source codebase plus some additional proprietary features, and is free to download and use under the terms of the Atlas MSA." The core repository's LICENSE is indeed Apache 2.0. But the same page lists migrate down — with migrate lint, migrate checkpoint and migrate rebase — among the commands "the Community Edition does not support". The revert command is absent from the Apache-2.0 build and present in a distribution that is free but not Apache 2.0. What the MSA provides is not asserted here, only that it is not that licence.
Atlas Pro is a third thing, layered on that boundary rather than cutting through it, and Atlas's pages name it inconsistently enough to quote rather than summarise. The migrate down page: Pro users "can protect this flow by configuring Atlas to require approval from one or more reviewers". The community-edition page: "Running atlas login unlocks Atlas Pro features such as migration linting, testing, and CI/CD integration", with a 30-day trial "after which a license is required". The feature-compatibility page lists Pre-migration checks and Migration Linting as Pro, "exclusively available to those on the Atlas Pro Plan". Three framings, unreconciled because Atlas does not reconcile them.
Sqitch is the short entry: revert belongs to a project distributed under the MIT licence, with no edition to check.
# migrate diff / apply / status are marked present in BOTH columns of Atlas's
# community-edition capability table: Apache-2.0 Community and standard.
atlas migrate diff add_orders_index \
--dir "file://migrations" \
--to "file://schema.sql" \
--dev-url "docker://postgres/17/dev"
# migrate lint is listed among the commands the Community Edition does not
# support. Separately, the feature-compatibility page marks Migration Linting
# as Pro and says Pro features are "exclusively available to those on the Atlas
# Pro Plan" -- while that same page's RULE table marks backward-incompatible,
# destructive and data-dependent changes as Open. Both are printed as found.
atlas migrate lint \
--dir "file://migrations" \
--dev-url "docker://postgres/17/dev" \
--latest 1
atlas migrate apply --dir "file://migrations" --url "$DATABASE_URL"
atlas migrate status --dir "file://migrations" --url "$DATABASE_URL"
# migrate down is also on the Community Edition's unsupported list, and ships
# in the standard distribution. It computes the plan from the live database, so
# it needs a Dev Database "that will be used to plan and analyze the migration".
atlas migrate down \
--dir "file://migrations" \
--url "$DATABASE_URL" \
--dev-url "docker://postgres/17/dev" \
--to-version 20260930120000The rollback you planned for will not save a half-applied migration
Flyway publishes the most honest page any of these vendors has, and it argues against Flyway's own paid feature. Undo migrations "assume the whole migration succeeded and should now be undone", which "does not help with failed versioned migrations on databases without DDL transactions" — because "A migration can fail at any point."
Which engine you run decides whether that applies. Where the database "cleanly supports DDL statements within a transaction, failed migrations will always be rolled back (unless they were marked as non-transactional)"; where it does not, "Flyway won't be able to perform a clean rollback in case of failure". MariaDB's manual confirms the edge from the other side, for the ordinary non-temporary DDL a migration runs: "even if the statement fails with an error, the transaction is committed." Not every DDL statement — that page excepts CREATE TABLE and DROP TABLE with TEMPORARY, though TRUNCATE TABLE commits even on a temporary table.
Atlas comes at it from the opposite direction, and the fencing is not optional. Its migrate down page says Atlas "computes a migration plan based on the current state of the database" and reverts "regardless of the state of the latest applied migration — whether it succeeded, failed, or was partially applied". The same page bounds that twice: Atlas "generates and executes a set of pre-migration checks to ensure the computed plan does not introduce destructive changes or deletes data", and "non-schema logic such as data migrations or other business-specific operations that Atlas cannot infer automatically" is what a manual down.sql exists for. So it reverts schema state and declines to delete data; it does not repair a half-applied data change — and it needs a --dev-url Dev Database to plan against.
On the checks themselves Atlas's pages disagree. The down page says Atlas "still generates and runs all defined pre-migration checks before applying a down migration, whether it's manual or automatic", skippable only with --skip-checks; the feature-compatibility page lists Pre-migration checks as Pro; the community-edition table marks them excluded. Settle on neither.
Sqitch is not computed recovery either: "sqitch revert runs each revert script in turn until the target change is reached." Which leaves Flyway's own recommended alternative as the one that works everywhere — "maintain backwards compatibility between the DB and all versions of the code currently deployed in production", "complemented with a proper, well tested, backup and restore strategy". Disaster-recovery testing arrives at the same place: the restore you have rehearsed is the rollback you own.
Liquibase moved the licence line under its users, and Keycloak is still stuck
At Liquibase Community 5.0 — release notes dated September 2025 — the licence changed. The README now reads "Liquibase Community is licensed under the Functional Source License (FSL)", with Liquibase Secure "commercially licensed" separately; the repository LICENSE.txt is "Functional Source License, Version 1.1, ALv2 Future License". The release also introduced a one-time Contributor License Agreement.
For most self-hosted teams the terms are survivable. "A Permitted Purpose is any purpose other than a Competing Use", and permitted purposes "specifically include using the Software ... for your internal use and access". The licence also grants its own exit ramp: "We hereby irrevocably grant you an additional license to use the Software under the Apache License, Version 2.0 that is effective on the second anniversary of the date we make the Software available." Every release becomes Apache 2.0 two years after it ships — which changes the shape of the exposure rather than removing it. Standing on code two years old is a different proposition for a migration tool than for a library.
Where it bites is transitively, because migration tools sit embedded inside other products. On 13 October 2025 Keycloak opened an issue recording the consequence: "CNCF does not permit source available licenses, and an exception is unlikely to be given, as such an alternative solution has to be found." Its options — switch component, freeze at the pre-change version, fork and maintain, or request an exception — are all, bar the exception, "far from ideal and will require significant effort."
That exception is not pending. The CNCF request was closed on 21 November 2025: "the Governing Board voted to reject this license exception request, following the recommendation of the Legal Committee, because FSL is not an open source license." As of 1 October 2026 the Keycloak issue remains open. If your estate includes a self-hosted identity provider, this is a licence change you inherited without choosing a migration tool at all — the transitive pattern that produced OpenBao and OpenTofu, arriving at the schema layer.
Reviewability: what a human sees in the pull request
Ask what a reviewer is actually reading and the four diverge sharply. With Flyway it is a versioned SQL file — the statements that will run, in order — and integrity is checked rather than assumed: validate "checks if the migration locally still has the same checksum as the migration already executed in the database".
With Liquibase the reviewed artefact depends on the changelog format, and the trade-off is not the one usually described: "If you use modeled changelogs (XML, YAML, JSON), you can define custom rollback logic for your changesets. Custom rollback definitions are not supported in formatted SQL changelogs." That does not mean formatted SQL has no rollback — it has an inline --rollback <SQL> syntax; only the file form, rollbackSqlFile, needs a Secure licence key.
databaseChangeLog:
- changeSet:
id: 0042-add-orders-customer-created-index
author: platform
comment: Index orders for the customer dashboard query.
changes:
- createIndex:
tableName: orders
indexName: idx_orders_customer_created
columns:
- column:
name: customer_id
- column:
name: created_at
rollback:
- dropIndex:
tableName: orders
indexName: idx_orders_customer_createdWith Atlas the reviewed artefact is generated, so the question is what checks run over it — and here the documentation conflicts, so all four statements belong side by side. The feature-compatibility page says "Atlas community version includes a basic set of linting rules, while the Pro Plan provides a more advanced suite", and its rule table marks backward-incompatible, destructive and data-dependent changes as Open. The CLI table on that same page marks Migration Linting | Pro. That page also states Pro-marked features are "exclusively available to those on the Atlas Pro Plan". And the community-edition page lists "migrate lint - Lint the migration directory for destructive or unsafe changes" under "The Community Edition does not support the following commands and integrations".
Sqitch asks the most of the reviewer and hides the least: a deploy script, a revert script and a verify script, all hand-written SQL, all in the pull request. Nothing about the reverse path is inferred.
lock_timeout bounds the wait, not the build
The migration that takes a service down is usually not a wrong migration; it is a correct one that queued behind a lock. Two settings get confused for each other here, and only one of them does the job. lock_timeout aborts a statement "that waits longer than the specified amount of time while attempting to acquire a lock", and "this timeout can only occur while waiting for locks"; it defaults to zero, meaning disabled. statement_timeout aborts the statement itself, so wrapping one around a long concurrent index build arms a killer rather than a guard.
-- PostgreSQL. CREATE INDEX CONCURRENTLY cannot run inside a transaction
-- block, so this file must execute outside one -- see the per-tool switches
-- below for how to arrange that in Flyway, Liquibase and Atlas.
--
-- lock_timeout aborts a statement ONLY while it is waiting to acquire a lock.
-- It defaults to 0, which disables it. The value below is an EXAMPLE: choose
-- yours from your own write-latency budget. PostgreSQL's documentation also
-- advises against setting lock_timeout in postgresql.conf, because that would
-- affect all sessions.
SET lock_timeout = '3s';
-- Deliberately NO statement_timeout. It aborts the statement itself once the
-- statement has run too long, not the lock wait, and it also defaults to 0.
-- Around a long concurrent build it would kill the build rather than bound the
-- wait. The two are not interchangeable knobs: PostgreSQL's docs note that if
-- statement_timeout is nonzero, it is "rather pointless to set lock_timeout to
-- the same or larger value, since the statement timeout would always trigger
-- first" -- so lock_timeout only does useful work below it.
-- A normal build locks the table against writes while reads continue.
-- CONCURRENTLY avoids locking out writes at the cost of two table scans and
-- more total work. PostgreSQL documents it as taking SHARE UPDATE EXCLUSIVE
-- rather than the ACCESS EXCLUSIVE lock "on the index as a whole"; the same
-- mode is held on the TABLE for the duration of the build, which is why other
-- DDL on this table waits even though writes do not.
CREATE INDEX CONCURRENTLY idx_orders_customer_created
ON orders (customer_id, created_at DESC);
-- This is not fail-clean. A failure during the scan leaves an index marked
-- INVALID: "ignored for querying purposes because it might be incomplete;
-- however it will still consume update overhead". For a UNIQUE index whose
-- SECOND scan fails, the invalid index "continues to enforce its uniqueness
-- constraint afterwards". Recovery: DROP INDEX then retry, or
-- REINDEX INDEX CONCURRENTLY.Getting that statement out of a transaction is per-tool. Flyway's executeInTransaction in the project TOML or via FLYWAY_EXECUTE_IN_TRANSACTION configures the run; the per-migration form is a sibling script configuration file, whose parameters are "effective at the level of a specific migration rather than globally". It "does not apply to Native Connectors".
# Sits in the same folder as V2__add_orders_index.sql. The file name is the
# migration file name with ".conf" appended. Flyway's script-configuration
# reference lists executeInTransaction as a Community-tier override meaning
# "Whether to execute this migration in a transaction."
executeInTransaction=falseLiquibase's runInTransaction defaults to true, with a warning worth repeating verbatim: "If runInTransaction is set to false and an error occurs part way through running a changeset that contains multiple statements, the Liquibase DATABASECHANGELOG table will be left in an invalid state." No Sqitch transaction flag is named here, because none is sourced. Atlas "executes each migration file in a transaction" by default; --tx-mode all wraps every pending file in one, which is not the mechanism here, since a concurrent build cannot run in a transaction block at all. The documented way is the file-level directive:
-- atlas:txmode none
CREATE INDEX CONCURRENTLY name_idx ON users (name);What each tool writes down, and what that record cannot prove
Each of these tools keeps its state in the database it manages, in a table of its own design: Flyway a schema history table whose name is a configurable setting defaulting to flyway_schema_history, Liquibase DATABASECHANGELOG, Atlas atlas_schema_revisions. Sqitch keeps two — its PostgreSQL registry comments one as tracking "the changes currently deployed to the database", and a separate events table as the "full history of all deployment events".
-- Flyway. The table NAME is a configurable setting; flyway_schema_history is
-- only its documented default, so check your project configuration first.
SELECT * FROM flyway_schema_history;
-- Liquibase. A rollback removes the corresponding row from this table. If a
-- Secure licence key has enabled DATABASECHANGELOGHISTORY (4.27.0+, disabled
-- by default), that is where the rollback itself is recorded.
SELECT * FROM databasechangelog;
-- Atlas. Named on the migrate down page as the migration history table.
SELECT * FROM atlas_schema_revisions;
-- Sqitch, PostgreSQL registry. Two tables, not one: what is deployed now, and
-- the full history of events. Substitute your own registry schema name.
SELECT * FROM sqitch.changes;
SELECT * FROM sqitch.events;An assessor's question is rarely which tool you use. It is: show me every change applied to this database in the audit period, who authorised it, and what happened to the ones you reversed. Judge each record by what it cannot prove.
Liquibase's gap is structural and documented: a rollback "removes the corresponding row from the DATABASECHANGELOG table", so the primary table does not evidence the reversal. DATABASECHANGELOGHISTORY records rollbacks instead — but it is "available in Liquibase 4.27.0 and later", "only available to Liquibase Secure users", and "disabled by default". Even switched on, the page declines the role: "It is not necessarily designed to meet security or regulatory needs." That is the vendor telling you not to present it as control evidence.
Flyway's post-undo record is a state on a row — Undone, "This versioned migration succeeded but has since been undone" — a durable fact about the migration, not an attribution chain. For Atlas the honest answer is a blank cell: nothing in the migrate down documentation says what the command does to rows in atlas_schema_revisions. Treat it as untested rather than inferring either answer.
Sqitch is the only one of the four whose registry was designed as an event log. In its PostgreSQL registry script the event column is constrained by a CHECK to 'deploy', 'revert', 'fail' and 'merge', so failures are rows rather than absences; the primary key (change_id, committed_at) means a redeploy appends instead of overwriting; and planner_name beside committer_name separates who planned a change from who ran it. That separation exists in none of the other three registries. One caveat: the MySQL registry script models event as an ENUM of the same four values and creates no CHECK.
-- Sqitch, PostgreSQL registry. Substitute your own registry schema name.
-- The event column is constrained by a CHECK to deploy, revert, fail, merge,
-- so a failed deploy is a row rather than a gap. The primary key is
-- (change_id, committed_at): a redeploy appends instead of overwriting.
SELECT
e.committed_at,
e.event,
e.change,
e.planner_name,
e.planned_at,
e.committer_name
FROM sqitch.events AS e
WHERE e.committed_at >= now() - interval '90 days'
ORDER BY e.committed_at DESC;Exit ramps: what a tool swap leaves stranded
Two facts determine what leaving costs, and neither appears on a comparison page. The first: the history artefact is tool-specific and does not travel. The four tables named above are four different schemas, and none of these tools reads another's — so whatever you move to starts with no history of what came before, unless you carry it across yourself.
The second: whether the changes survive as replayable SQL. Flyway's versioned SQL migrations and Sqitch's operator-written deploy and revert scripts are files a successor tool — or a human — can read and run. Liquibase's modeled changelogs carry rollback logic inside the model, though formatted SQL does have the inline --rollback form, with only rollbackSqlFile requiring a Secure licence key. Atlas's down plans are by default computed from live state rather than stored, unless a migration file carries a manual down.sql section.
No commands for the move are prescribed here; that depends on what your history table actually contains. What matters at selection time is the shape of the dependency: plain SQL files plus a readable history table is a cheap exit, a proprietary changelog model plus a computed revert an expensive one. That is the calculation exit-cost modelling applies to data gravity, pushed down to the schema layer — and if your database already runs on Kubernetes with an operator, the migration tool is one of the last pieces of the data path you still choose independently.
The long game: outliving the vendor that wrote the tool
Schema history outlives tool choices, usually by several of them. Weight the decision accordingly: licence durability and readability of the record above feature count, with the revert command you intend to rely on checked against the edition you are entitled to run — per command, not per product.
On that rule Sqitch scores unusually well: MIT, plain SQL scripts, and the only registry of the four designed as an event log. The counterarguments belong in the same paragraph. It is a Perl project whose README copyright names one principal author beside a corporate line that stopped in 2021 — a bus-factor risk of a different shape to a licence risk, not an absence of risk. And its revert is only as good as the revert scripts someone wrote; the manual says so in as many words.
The pattern underneath this has reached three layers of the stack in three years: secrets, infrastructure-as-code, and now the schema — and, through Keycloak, identity with it. A licence line moves under a tool nobody chose deliberately, and the cost lands on whoever cannot show what their systems did. Keep the history in a table you can read with SELECT, keep the changes in files a human can replay, and the tool becomes replaceable — the only property that has ever survived a vendor. The discipline that makes infrastructure state portable applies here, one layer down.
§FAQ/Common questions
Frequently asked
Which database migration tool has the best rollback?
None of them has a rollback that repairs a half-applied migration, and that is the honest framing. Flyway's own documentation says undo migrations "assume the whole migration succeeded and should now be undone" and that this "does not help with failed versioned migrations on databases without DDL transactions", because a migration can fail at any statement. Atlas computes a down plan from the live database and documents it as working "regardless of the state of the latest applied migration", but bounds that twice: by default it generates pre-migration checks "to ensure the computed plan does not introduce destructive changes or deletes data", and non-schema logic such as data migrations is what "Atlas cannot infer automatically", which is why teams write a manual down.sql. Sqitch's revert runs "each revert script in turn" that an operator wrote. So the real rollback is Flyway's own recommendation: backward compatibility between the database and every deployed version of the application code, plus a tested backup and restore strategy.
Is database rollback a paid feature in Flyway, Liquibase or Atlas?
The boundary is per command and sits somewhere different in each project. Flyway's undo-migrations page is badged EDITION: TEAMS and its command reference reads "Undo Description Flyway Teams" — marked Flyway Teams, and included in Enterprise. Liquibase documents rollback, rollback-to-date and rollback-count as available in all editions; only rollback-one-changeset and rollback-one-update are Liquibase Secure. Atlas lists migrate down among the commands "the Community Edition does not support", while the standard distribution that does include it is described on the same page as "free to download and use under the terms of the Atlas MSA" — a build boundary rather than a price, since the Community Edition is the Apache-2.0 one. Sqitch's revert is MIT with no edition to check. A Flyway Community user can still hand-write a forward migration that reverses an earlier one.
What does the Liquibase Functional Source License change mean for self-hosted users?
Liquibase Community moved to the Functional Source License at 5.0, release notes dated September 2025, and the repository LICENSE.txt is FSL-1.1-ALv2. For internal self-hosted use the terms are generally survivable: "A Permitted Purpose is any purpose other than a Competing Use", and permitted purposes "specifically include using the Software ... for your internal use and access". The licence also grants an additional Apache License 2.0 "effective on the second anniversary of the date we make the Software available", so each release becomes Apache 2.0 two years after shipping. The sharper problem is transitive. Keycloak opened an issue on 13 October 2025 noting that "CNCF does not permit source available licenses" and that "an alternative solution has to be found"; the CNCF licence-exception request was closed on 21 November 2025 after "the Governing Board voted to reject this license exception request ... because FSL is not an open source license". As of 1 October 2026 the Keycloak issue is still open.
What record does each migration tool leave in the database after a rollback?
Different records, with different limits. Liquibase documents that "When Liquibase rolls back a changeset, it removes the corresponding row from the DATABASECHANGELOG table"; rollbacks are recorded in DATABASECHANGELOGHISTORY instead, which is "available in Liquibase 4.27.0 and later", "only available to Liquibase Secure users", "disabled by default", and whose own page says it "is not necessarily designed to meet security or regulatory needs". Flyway's schema history table — default name flyway_schema_history, a configurable setting — documents an Undone state meaning "This versioned migration succeeded but has since been undone". Sqitch's PostgreSQL registry keeps a separate events table commented as the "full history of all deployment events", with its event column CHECK-constrained to deploy, revert, fail and merge, a primary key of (change_id, committed_at) so redeploys append, and planner_name beside committer_name. For Atlas, nothing in the migrate down documentation states what the command does to rows in atlas_schema_revisions, so treat that as untested rather than assumed.
How do I run CREATE INDEX CONCURRENTLY from a migration tool?
You need the statement outside a transaction block, because PostgreSQL documents that "a regular CREATE INDEX command can be performed within a transaction block, but CREATE INDEX CONCURRENTLY cannot". In Flyway, use a per-migration script configuration file — V2__name.sql.conf beside V2__name.sql — containing executeInTransaction=false; the project TOML [flyway] executeInTransaction and FLYWAY_EXECUTE_IN_TRANSACTION settings configure the whole run instead, and the parameter "does not apply to Native Connectors". In Atlas, write the file-level directive "-- atlas:txmode none" directly above the statement, or use the run-level --tx-mode none which "will not create any transaction"; --tx-mode all is the opposite of what you want here. Liquibase's runInTransaction attribute exists but carries a documented warning that setting it to false can leave DATABASECHANGELOG "in an invalid state" if a multi-statement changeset fails part way. Bound the lock wait with lock_timeout, which aborts only while waiting for a lock and defaults to 0; do not set a statement_timeout around the build, which would abort the build itself.
Further reading
- Postgres vs MariaDB: Replication, Upgrades, On-Call
- Postgres vs SQL Server: Standard, Enterprise, or Neither
- PostgreSQL on Kubernetes: CloudNativePG and Sovereign State
- Keycloak vs Authentik vs Zitadel: Self-Hosted Identity
- OpenBao: Exiting a Licence Change Without Losing Your Secrets
- OpenTofu Migration: The Registry Is the Hard Part
- Terraform vs Ansible: State You Own, State You Converge
- DR Testing: Failover Without a Maintenance Window
- Vendor Lock-In in the Cloud: Pricing Your Exit as a Number
- Postgres vs MongoDB: You May Already Have a Document Store
Executive Briefing
Thirty minutes to clarify your infrastructure risk
Walk us through your vendor footprint and regulatory constraints. We will tell you honestly where sovereignty creates leverage — and where it does not. No pitch deck. No obligation.