plugins/elixir-phoenix/skills/ecto-patterns/SKILL.md
Ecto patterns — schemas, changesets, queries, migrations, Multi, associations, preloads, upserts. Use when editing Repo calls, Ecto.Query, or schema fields. Skip for Ash.
npx skillsauth add oliver-kriska/claude-elixir-phoenix ecto-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Reference for working with Ecto schemas, queries, and migrations.
cast/4 for user/API input, change/2 or put_change/3 for internal trusted data:float FOR MONEY — Always use :decimal or :integer (cents)u.name == ^user_input is safe, string interpolation causes SQL injectionhas_many, JOIN FOR belongs_to — Avoids row multiplicationfrom(a in A, b in B) without on: creates Cartesian productcast_assoc WITH SHARED DATA — When multiple parents share child data, deduplicate child records BEFORE building changesets. Dedup only works within a single changesetdefmodule MyApp.Context.Entity do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "entities" do
field :name, :string
field :status, Ecto.Enum, values: [:draft, :active, :archived]
field :amount_cents, :integer # Never :float for money!
belongs_to :user, MyApp.Accounts.User
timestamps(type: :utc_datetime_usec)
end
def changeset(entity, attrs) do
entity
|> cast(attrs, [:name, :status, :amount_cents])
|> validate_required([:name])
|> foreign_key_constraint(:user_id)
end
end
| Function | Use When |
|----------|----------|
| cast/4 | External data (user input, API) |
| put_change/3 | Internal trusted data (timestamps, computed) |
| change/2 | Internal data from existing struct |
| Relationship | Strategy |
|--------------|----------|
| belongs_to | JOIN (single query) |
| has_many | Separate queries (avoid row multiplication) |
| Wrong | Right |
|-------|-------|
| field :amount, :float | field :amount_cents, :integer |
| "SELECT * WHERE name = '#{name}'" | from(u in User, where: u.name == ^name) |
| Repo.all(User) \|> Enum.filter(& &1.active) | from(u in User, where: u.active) |
| Preloading in loops | Repo.preload(posts, :comments) |
| Repo.get!(User, user_id) with user input | Repo.get(User, id) + handle nil |
| {:ok, _} = Repo.update(cs) inside Repo.transaction | case/with + Repo.rollback(cs) (see transactions.md) |
For detailed patterns, see:
${CLAUDE_SKILL_DIR}/references/changesets.md - cast vs put_change, custom validations, prepare_changes${CLAUDE_SKILL_DIR}/references/queries.md - Composable queries, dynamic, subqueries, preloading${CLAUDE_SKILL_DIR}/references/migrations.md - Safe migrations, concurrent indexes, NOT NULL${CLAUDE_SKILL_DIR}/references/transactions.md - Repo.transact, Ecto.Multi, upsertstools
Compatibility alias for the Elixir/Phoenix plugin's LiveView assigns audit. Invoke explicitly with /lv:assigns.
development
Trace Elixir call trees from entry points via mix xref. Use when debugging data flow, planning signature changes, or understanding how a bug reaches code.
tools
Compatibility alias for the Elixir/Phoenix plugin's N+1 query checker. Invoke explicitly with /ecto:n1-check.
tools
Compatibility alias for the Elixir/Phoenix plugin's Ecto constraint debugger. Invoke explicitly with /ecto:constraint-debug.