Ecto patterns for Postgres-backed Elixir/Phoenix apps: schemas, changesets (per-operation, composition, validations), associations, cast_assoc/cast_embed, Ecto.Multi, transactions, migrations, and query performance (N+1, indexes).
How this skill is triggered — by the user, by Claude, or both
Slash command
/elixir-phoenix:ectoWhen to use
Use when writing or reviewing Ecto schemas, changesets, associations (`cast_assoc`/`cast_embed`), `Ecto.Multi` transactions, migrations, or `Repo`/query code, including preloads, N+1, and indexing.
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Pairs with `elixir-conventions`. Database errors a caller can act on (validation, conflict) are values; anything that "can't happen" should crash.
Pairs with elixir-conventions. Database errors a caller can act on (validation, conflict) are values; anything that "can't happen" should crash.
registration_changeset, profile_changeset, admin_changeset: each casts only its own fields. Don't toggle behavior with option flags.cast the fields you accept; never cast everything. The cast allowlist is your mass-assignment boundary.unsafe_validate_unique with a DB unique_constraint. The first gives a friendly form error; the second is the source of truth that catches the concurrent insert the validation can't see.cast_assoc/cast_embed require the association preloaded on the struct you're updating, and a changeset on the child that casts its own fields (including any required FKs). Set on_replace: explicitly.Ecto.Multi, not nested Repo calls. You get one transaction and a {:error, failed_step, value, changes_so_far} you can branch on.Repo.insert!/Repo.get! etc. when a failure means a bug, not a user-facing error (see elixir-conventions §7 and §8).# Don't: defensive option-juggling inside one changeset
def changeset(user, attrs, opts \\ []) do
user
|> cast(attrs, [:email, :password])
|> then(fn cs -> if Keyword.get(opts, :validate_unique, true), do: unsafe_validate_unique(cs, :email, Repo), else: cs end)
end
# Do: one changeset per operation; the caller picks
def registration_changeset(user, attrs) do
user
|> cast(attrs, [:email, :password])
|> validate_required([:email, :password])
|> validate_format(:email, ~r/@/)
|> unsafe_validate_unique(:email, Repo)
|> unique_constraint(:email)
end
Compose shared validation as plain changeset->changeset functions and pipe them; don't reach for with/else inside changesets.
order
|> Repo.preload(:line_items) # required before cast_assoc
|> Ecto.Changeset.cast(attrs, [:status])
|> Ecto.Changeset.cast_assoc(:line_items,
with: &LineItem.changeset/2,
on_replace: :delete) # be explicit: :delete | :nilify | :raise
cast_assoc when parent and children share a lifecycle and arrive in one payload. When they have independent lifecycles, manage them separately with Ecto.Multi instead.cast_embed for embedded schemas (no separate table); same preload/on_replace discipline.Ecto.Multi.new()
|> Ecto.Multi.insert(:order, Order.changeset(%Order{}, attrs))
|> Ecto.Multi.insert_all(:items, LineItem, &build_items(&1.order, attrs))
|> Ecto.Multi.run(:charge, fn _repo, %{order: order} -> Billing.charge(order) end)
|> Repo.transaction()
|> case do
{:ok, %{order: order}} -> {:ok, order}
{:error, :charge, reason, _changes} -> {:error, reason} # branch only on steps you can act on
end
Don't write a generic catch-all else over every step. Match the steps whose failure the caller can handle; let genuinely unexpected failures raise.
create index(...) on FKs and filter/sort columns. Consider concurrently: true (with @disable_ddl_transaction true) for large tables.Repo.all(from u in User, where: ..., preload: [:posts]) over looping Repo.preload per row.select: to avoid loading whole rows when you need a few fields; Repo.aggregate/3 for counts/sums.Run mix format, mix compile --warnings-as-errors, and your migrations against a scratch DB (mix ecto.migrate / ecto.rollback) so reversibility is real, not assumed.
npx claudepluginhub ariesclark/skills --plugin elixir-phoenixBlocks Edit/Write/Bash actions until Claude investigates importers, data schemas, and user instructions. Improves output quality by forcing concrete facts before edits.