DSL

Single transform

defmodule MyApp.Transforms.BackfillDetails do
  use Pollard

  transform "Backfill missing details" do
    from(s in "shops", where: is_nil(s.details))
    |> MyApp.Repo.update_all(set: [details: %{}])
  end
end

Multiple transforms (separate transactions)

defmodule MyApp.Transforms.SeedTypes do
  use Pollard

  transform "Seed product type" do
    MyApp.Repo.insert_all("types", [%{name: "product"}], on_conflict: :nothing)
  end

  transform "Seed variant type" do
    MyApp.Repo.insert_all("types", [%{name: "variant"}], on_conflict: :nothing)
  end
end

Mix Tasks

Generate a transform

mix pollard.gen backfill_shop_details
# => priv/repo/transforms/20260401120000_backfill_shop_details.exs

Run pending transforms

mix pollard.run
mix pollard.run --migration-source custom_table
mix pollard.run --repo MyApp.Repo

Generate tracking table migration

mix pollard.gen.migration
mix ecto.migrate

Runner

Basic usage

Pollard.Runner.run(MyApp.Repo, "priv/repo/transforms")

Options

OptionDefaultDescription
:migration_source"transforms"Tracking table name
:lockPollard.Lock.PostgresLock strategy module
:logtrueLog to stdout

Lock Strategies

Postgres (default)

Pollard.Runner.run(repo, path)
Pollard.Runner.run(repo, path, lock: Pollard.Lock.Postgres, lock_key: 12345)

None (single-node / SQLite)

Pollard.Runner.run(repo, path, lock: Pollard.Lock.None)

Custom

defmodule MyApp.Lock.Redis do
  @behaviour Pollard.Lock

  @impl true
  def acquire(_repo, _opts), do: :ok

  @impl true
  def release(_repo, _opts), do: :ok
end

Idempotency Patterns

Insert with conflict handling

Repo.insert_all("table", rows, on_conflict: :nothing)

Guarded update

from(r in "rows", where: is_nil(r.migrated_at))
|> Repo.update_all(set: [migrated_at: DateTime.utc_now()])

Upsert

Repo.insert(%Schema{id: id, name: name},
  on_conflict: {:replace, [:name]},
  conflict_target: :id
)

State check before update

from(r in "rows", where: r.status == "pending")
|> Repo.update_all(set: [status: "active"])

Release Support

defmodule MyApp.Release do
  def transform do
    Pollard.Runner.run(MyApp.Repo, transforms_path())
  end

  defp transforms_path do
    Application.app_dir(:my_app, "priv/repo/transforms")
  end
end

Deploy order

bin/migrate      # schema migrations
bin/transform    # data transforms
bin/server       # start app