Guides

Migrations

A migration copies a database while your application keeps using it. This page covers the four phases, dry runs and rolling back.

How a migration runs

Every migration goes through the same four phases, in order. quarry status names the current one, and a run that stops in any phase can be resumed.

  1. PlanCompare schemas and size every table
  2. CopyBulk-copy rows in parallel ranges
  3. Catch-upReplay the writes made during the copy
  4. Cut-overPause writes and switch to the target

Plan

Quarry reads both schemas, checks that every table has a primary key or a unique index it can page through, and splits large tables into ranges. The plan is saved in .quarry/plan.json, so a resumed run copies exactly the same ranges.

Copy

Before the first batch, Quarry records the source’s current position in its change log, the start position, so that nothing written during the copy is missed. Workers then copy ranges in parallel (--parallel, 4 by default), each in batches of --batch-size rows.

Catch-up

When the copy finishes, Quarry replays every change made since the start position, oldest first, and keeps streaming new ones. The target is now a live replica of the source, and the lag shown by quarry status settles at a few hundred milliseconds.

Cut-over

quarry cutover makes the source read-only, waits until the target has applied the final change, and runs your switch-over hook. Writes are paused only for that window. Quarry then starts reverse replication, copying writes from the target back to the source, so that you can still roll back.

Dry runs

A dry run changes nothing. It connects with the same credentials as a real run and checks everything that could stop a migration halfway: permissions on both sides, a primary key on every table, free disk on the target, and whether the source can stream its changes. It then reads one sample batch per table into memory to time it.

Terminal
quarry run --dry-run
Dry run for shop · nothing will be written

  ✓ source  replication permission
  ✓ source  4 tables readable, all with primary keys
  ✓ target  schema compatible, 1 table to create
  ✓ target  212 GB free, 70.3 GB needed
  ! orders  sample batch took 1.8 s (expected under 1 s)

Estimated copy time 1 h 52 min · catch-up about 4 min
1 warning, 0 errors · safe to run

Warnings do not stop a real run; errors do. Fix the errors and dry-run again until the report ends in safe to run. A slow sample batch, like the one on orders above, usually means the target is missing an index that the source has.

Note

Dry-run against production, not a staging copy. The checks only mean something when they see the real permissions, table sizes and load.

Rolling back a migration

How you roll back depends on whether you have cut over yet. In both cases the command is quarry rollback; it reads the state of the migration and does the right thing.

Before cut-over

Your application still writes to the source, so nothing needs to move back. Stop quarry run with Ctrl+C and roll back:

Terminal
quarry rollback

This drops the tables Quarry created on the target and releases the replication slot on the source. Tables that already existed on the target are left alone; pass --keep-target to keep the copied ones too.

After cut-over

After a cut-over, reverse replication runs for 24 hours by default. While it runs, a rollback is a second cut-over in the other direction: writes pause briefly, the source catches up, and your rollback hook points the application back at it.

Terminal
quarry rollback --yes

Warning

Once reverse replication stops, a rollback can no longer bring the source up to date: writes made on the target after that point exist only there. If you need longer to decide, cut over with quarry cutover --keep-reverse 72h.

Troubleshooting

The copy stalls on one table

quarry status shows one worker busy and the rest idle. The table has no primary key, so Quarry cannot split it into ranges and copies it as one. Add a primary key, or name a unique index for Quarry to page through in quarry.toml.

Replication lag keeps growing

The target applies changes more slowly than the source produces them. Raise --apply-workers, and check the target for missing indexes on foreign keys: every replayed update has to find its row.

Cut-over times out

quarry cutover waits 30 seconds for the lag to reach zero. If a long-running transaction holds the source open, the cut-over is abandoned and the source is made writable again. Nothing is lost. Find the transaction with quarry status --verbose, end it, and cut over again.

Permission denied on the source

On PostgreSQL the source role needs the REPLICATION attribute and SELECT on every table in the plan. quarry run --dry-run names the exact permission that is missing.