mcpbeat

Ecto Patterns

oliver-kriska/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.

7k tokens
context cost
the whole folder, loaded on every use
6
files
instructions only
0
copies elsewhere
how many repositories repackaged it
514
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/oliver-kriska/claude-elixir-phoenix --skill ecto-patterns

What comes with it

24 166 bytes besides the instruction
references/changesets.md
references/fulltext-search.md
references/migrations.md
references/queries.md
references/transactions.md

The instruction itself

8 sections, as written by the author

Ecto Patterns Reference

Reference for working with Ecto schemas, queries, and migrations.

Iron Laws — Never Violate These

  • CHANGESETS ARE FOR EXTERNAL DATA — Use cast/4 for user/API input, change/2 or put_change/3 for internal trusted data
  • NEVER USE :float FOR MONEY — Always use :decimal or :integer (cents)
  • NO RAILS-STYLE POLYMORPHIC ASSOCIATIONS — They break foreign key constraints; use multiple nullable FKs or separate join tables
  • ALWAYS PIN VALUES IN QUERIESu.name == ^user_input is safe, string interpolation causes SQL injection
  • 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 has_many, JOIN FOR belongs_to — Avoids row multiplication
  • NO IMPLICIT CROSS JOINSfrom(a in A, b in B) without on: creates Cartesian product
  • DEDUP BEFORE cast_assoc WITH SHARED DATA — When multiple parents share child data, deduplicate child records BEFORE building changesets. Dedup only works within a single changeset

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 |

|----------|----------|

| cast/4 | External data (user input, API) |

| put_change/3 | Internal trusted data (timestamps, computed) |

| change/2 | Internal data from existing struct |

Preload Strategy

| Relationship | Strategy |

|--------------|----------|

| belongs_to | JOIN (single query) |

| has_many | Separate queries (avoid row multiplication) |

Common Anti-patterns

| 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) |

References

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, upserts

How to use it

Copy the folder

Take oliver-kriska/ecto-patterns from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.