Claude-elixir-phoenix ecto-patterns
Ecto patterns — schemas, changesets, queries, migrations, Multi, associations, preloads, upserts. Use when editing Repo calls, Ecto.Query, or schema fields. Skip for Ash.
install
source · Clone the upstream repo
git clone https://github.com/oliver-kriska/claude-elixir-phoenix
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/oliver-kriska/claude-elixir-phoenix "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/elixir-phoenix/skills/ecto-patterns" ~/.claude/skills/oliver-kriska-claude-elixir-phoenix-ecto-patterns && rm -rf "$T"
manifest:
plugins/elixir-phoenix/skills/ecto-patterns/SKILL.mdsource content
Ecto Patterns Reference
Reference for working with Ecto schemas, queries, and migrations.
Iron Laws — Never Violate These
- CHANGESETS ARE FOR EXTERNAL DATA — Use
for user/API input,cast/4
orchange/2
for internal trusted dataput_change/3 - NEVER USE
FOR MONEY — Always use:float
or:decimal
(cents):integer - NO RAILS-STYLE POLYMORPHIC ASSOCIATIONS — They break foreign key constraints; use multiple nullable FKs or separate join tables
- ALWAYS PIN VALUES IN QUERIES —
is safe, string interpolation causes SQL injectionu.name == ^user_input - PRELOAD COLLECTIONS, NOT INDIVIDUALS — Preloading in loops = N+1 queries
- CONSTRAINTS BEAT VALIDATIONS FOR RACE CONDITIONS — Validations provide quick feedback, constraints provide DB-level safety
- SEPARATE QUERIES FOR
, JOIN FORhas_many
— Avoids row multiplicationbelongs_to - NO IMPLICIT CROSS JOINS —
withoutfrom(a in A, b in B)
creates Cartesian producton: - DEDUP BEFORE
WITH SHARED DATA — When multiple parents share child data, deduplicate child records BEFORE building changesets. Dedup only works within a single changesetcast_assoc
Quick Schema Template
defmodule 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
Quick Decisions
cast vs put_change vs change
| Function | Use When |
|---|---|
| External data (user input, API) |
| Internal trusted data (timestamps, computed) |
| Internal data from existing struct |
Preload Strategy
| Relationship | Strategy |
|---|---|
| JOIN (single query) |
| Separate queries (avoid row multiplication) |
Common Anti-patterns
| Wrong | Right |
|---|---|
| |
| |
| |
| Preloading in loops | |
with user input | + handle nil |
References
For detailed patterns, see:
- cast vs put_change, custom validations, prepare_changes${CLAUDE_SKILL_DIR}/references/changesets.md
- Composable queries, dynamic, subqueries, preloading${CLAUDE_SKILL_DIR}/references/queries.md
- Safe migrations, concurrent indexes, NOT NULL${CLAUDE_SKILL_DIR}/references/migrations.md
- Repo.transact, Ecto.Multi, upserts${CLAUDE_SKILL_DIR}/references/transactions.md