PhoenixMountable.Backfill behaviour (PhoenixMountable v0.2.1)

Copy Markdown View Source

Data migrations, kept apart from schema changes and run in batches.

Filling a new column on a table with millions of rows inside a migration holds locks for minutes and blocks the deploy. A backfill runs in batches instead: inline in the migration for small tables, or deferred to a process in the running app after the deploy, recording its progress after every batch so it resumes where it stopped after a crash or a restart.

defmodule MyLib.Backfills.Slugs do
  use PhoenixMountable.Backfill,
    table: "my_lib_items",
    where: "slug IS NULL",
    set: "slug = LOWER(title)",
    batch_size: 1_000
end

# in a migration version
def up(ctx) do
  alter table(:my_lib_items, prefix: ctx.prefix), do: add(:slug, :string)
  backfill(ctx, MyLib.Backfills.Slugs)
end

A later contract version that relies on the data declares requires_backfills: [MyLib.Backfills.Slugs], and refuses to run until the backfill has finished.

Options

  • :mode — :inline (the default) runs every batch inside the migration; :deferred records the backfill as pending, to be run by PhoenixMountable.Backfill.Runner (or mix phoenix_mountable.backfill). The host overrides it for a migration with up(version: n, backfills: :deferred).
  • :batch_size — rows per batch. Defaults to 1_000.
  • :throttle — milliseconds to wait between batches (deferred mode). Defaults to 0.

For the common case, an UPDATE keyed by an ordered column:

  • :table — the table.
  • :key — the column to paginate by. Defaults to "id".
  • :where — an SQL condition selecting the rows still to fill.
  • :set — the SQL assignments.

For anything else, implement batch/3 instead.

Summary

Callbacks

Processes one batch after cursor (nil for the first), returning the cursor to continue from and the number of rows processed, or :done.

Callbacks

batch(t, cursor, batch_size)

@callback batch(
  PhoenixMountable.Migrations.Context.t(),
  cursor :: term(),
  batch_size :: pos_integer()
) ::
  {:cont, term(), non_neg_integer()} | :done

Processes one batch after cursor (nil for the first), returning the cursor to continue from and the number of rows processed, or :done.

Runs in a transaction together with the recording of the new cursor, so a batch is either fully done and recorded, or not at all.