Sqitch vs Flyway: Dependency-Based vs Version-Ordered Migrations

Two feature branches each added a migration numbered V58. One merged first; the other now fails Flyway validation with a version conflict, and renumbering it to V59 means its checksum changes on every database where someone already ran it locally. A colleague points out that Sqitch would not have this problem, because it orders changes by declared dependencies rather than by version numbers, and every change ships with its own revert and verify script. That is true, and it is also a different operational model with its own costs. This guide compares the two approaches on the questions that matter for zero-downtime delivery — ordering under parallel development, rollback, verification and tooling fit — so you can choose deliberately. It extends Migration Tool Comparison.

Two Ways to Order Changes Two panels. Flyway: changes ordered by version number V56, V57, V58; each is a forward SQL file; order is global and total. Sqitch: changes named in a plan file with explicit requires; add_region requires orders_table; each change has deploy, revert and verify scripts; order is the plan order constrained by dependencies. Two Ways to Order Changes Flyway version-ordered V56__orders_table.sql V57__customers_email_idx.sql V58__add_region.sql order = version number, global simple; numbers collide across branches Sqitch plan + dependencies orders_table customers_email_idx add_region [orders_table] deploy/, revert/, verify/ per change explicit deps; plan file merges instead
Flyway's order is a number in the file name; Sqitch's order is a plan file plus declared dependencies, which is why parallel branches collide differently.

Symptom / Error Signatures

Teams start comparing the two when they hit problems like these:

  • Flyway: Validate failed: Detected resolved migration not applied to database: 58 or Found more than one migration with version 58 after a merge. The outOfOrder setting helps but makes environments apply changes in different orders.
  • Flyway Community lacks undo migrations (U files require a paid edition), so rollback relies on hand-written forward fixes.
  • No built-in way to verify that a migration’s effect is present, beyond the history table saying it ran.
  • Sqitch, for teams already using it: plan file conflict in git on every merge, because every branch appends to the same sqitch.plan lines, and changes that must be reworked with sqitch rework confuse newcomers.

Root Cause Analysis

The tools answer “in what order do changes apply?” differently. Flyway sorts migrations by version number; a database is described by the highest version applied, and every environment applies the same total order. That makes Flyway easy to reason about and easy to integrate — a directory of SQL files, a JVM or native CLI, plugins for Maven, Gradle and Spring Boot. The cost is that parallel branches compete for the next number.

Sqitch orders changes by a plan file, sqitch.plan, in which each change is named and may declare requires and conflicts on other changes. Sqitch refuses to deploy a change whose requirements are not deployed. Each change is three scripts: deploy/ makes the change, revert/ undoes it, and verify/ proves it is present, typically by selecting from the new object in a way that fails if it is missing. Sqitch records deployments in a registry schema and uses the plan’s hashes to detect tampering. Parallel branches do not collide on numbers, but they do both append to the plan file, which produces a textual merge conflict that is usually trivial to resolve.

Dimension Flyway Sqitch
Ordering version number plan order + requires
Branch conflicts duplicate versions; renumbering changes checksums plan-file merge conflicts; easy to resolve
Rollback undo migrations in paid editions; otherwise forward fixes revert script for every change, sqitch revert --to
Verification none built in verify script per change, sqitch verify
Transaction handling per-file transaction where supported each script manages its own BEGIN/COMMIT
Ecosystem JVM plugins, Spring Boot auto-run, wide adoption CLI (Perl), language-agnostic, smaller community
Engines very broad PostgreSQL, MySQL, SQLite, Oracle, Snowflake and others
Which Model Fits Your Team? Decision tree. If the application is JVM-based and wants migrations to run on startup, choose Flyway. Otherwise, if many parallel branches add schema changes and you want revert and verify scripts for every change, choose Sqitch. If not, either works; prefer the one matching existing tooling. Which Model Fits Your Team? JVM app that runs migrations on startup? yes no Flyway (native integration) Many parallel branches + want revert/verify? yes no Sqitch Either — match existing tooling
The decisive questions are ecosystem fit and how much you value per-change revert and verify scripts; both handle zero-downtime DDL equally well.

Immediate Mitigation

If you are stuck on a Flyway version collision today:

1. Renumber the unmerged migration before it reaches any shared environment. A migration that has only run on developer laptops can be renumbered; developers reset their local databases. Use timestamps for new versions (for example V20260918113000__add_region.sql) so collisions stop happening.

2. If both versions already ran somewhere shared, do not renumber. Enable outOfOrder=true for that deploy only, apply the missing migration, and record the decision. Out-of-order application is safe only when the two migrations are independent — they touch different objects — which the review must confirm.

# Shell · Flyway CLI · one-off deploy to apply an older-numbered migration that was skipped
# WARNING: only safe when the skipped migration is independent of migrations already applied.
flyway -url="$JDBC_URL" -user=migrator -outOfOrder=true migrate
flyway -url="$JDBC_URL" -user=migrator info   # confirm both versions show Success

If you are evaluating Sqitch, try it on one service first:

3. Scaffold a change with dependencies and all three scripts.

# Shell · repository root · Sqitch CLI installed
# WARNING: sqitch init writes sqitch.conf and sqitch.plan; commit both.
sqitch init app --engine pg
sqitch add orders_table -n "Create orders table"
sqitch add add_region --requires orders_table -n "Add region to orders"

4. Write deploy, revert and verify for the change. The verify script should fail when the change is absent.

-- PostgreSQL · deploy/add_region.sql · Sqitch runs each script as written; this one manages its own transaction
BEGIN;
SET LOCAL lock_timeout = '2s';
ALTER TABLE orders ADD COLUMN IF NOT EXISTS region text;
COMMIT;
-- revert/add_region.sql:  BEGIN; ALTER TABLE orders DROP COLUMN IF EXISTS region; COMMIT;
-- verify/add_region.sql:  SELECT region FROM orders WHERE false;   -- errors if the column is missing

Permanent Fix / Long-Term Pattern

Whichever tool you choose, the zero-downtime rules are identical: every change is additive-first, locks are bounded with lock_timeout, non-transactional statements live alone, and destructive steps wait for the contract phase of Expand and Contract Methodology. Neither tool enforces these; your review and linting do.

With Flyway, eliminate collisions structurally by using timestamp versions, and replace the missing undo feature with the discipline in writing safe down migrations for automated rollback — or accept forward-only fixes and design for them. With Sqitch, treat revert scripts as production code: test that deploy → verify → revert → deploy round-trips in CI, and remember that a revert which drops a column destroys data written since deploy, so production rollbacks should still prefer forward fixes once real data exists. Sqitch’s verify scripts are worth copying even into Flyway projects as post-deploy checks. If you need to switch tools, the approach in migrating from Flyway to Liquibase without downtime — baseline the new tool at the old tool’s end state — applies equally to Sqitch.

Sqitch Round-Trip Test in CI Five steps run against a scratch database for every pull request: deploy all changes, verify all, revert the newest change, verify that it is gone, and deploy it again. Sqitch Round-Trip Test in CI STEP 1 sqitch deploy scratch database STEP 2 sqitch verify all changes present STEP 3 sqitch revert --to @HEAD^ newest change removed STEP 4 Verify absent verify script must fail STEP 5 sqitch deploy re-apply cleanly
The round trip proves the revert script actually reverses the deploy and that verify detects both states — before anyone relies on them in an incident.

Verification Checklist

Frequently Asked Questions

Is Sqitch better for zero-downtime migrations than Flyway? Neither tool makes DDL safer by itself. Both execute the SQL you write; zero-downtime behaviour comes from the statements and their sequencing. Sqitch’s advantages are dependency-based ordering and built-in revert and verify scripts; Flyway’s are simplicity and ecosystem integration.

Can Flyway express dependencies between migrations? Not directly. Order is the version number. Teams approximate dependencies by keeping related changes in one migration or by reviewing order carefully, and use timestamp versions to reduce collisions between branches.

Does Sqitch wrap scripts in transactions automatically? No. Each deploy, revert and verify script is run as written, so you include BEGIN and COMMIT where you want atomicity and omit them for statements such as CREATE INDEX CONCURRENTLY that must run outside a transaction.

Should production rollbacks use Sqitch revert scripts? Only when the revert is non-destructive or no meaningful data has been written since the deploy. Reverting an added column drops whatever was written to it, so after real traffic, a forward fix is usually safer than a revert.