{"slug":"ecto-patterns-2","title":"ecto-patterns","summary":"'Use when a task touches Ecto, even a how-to question: schemas, changesets,","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-10-04T15:14:21.820613Z","repo":{"url":"https://github.com/oliver-kriska/claude-elixir-phoenix","stars":560,"forks":44,"license":"MIT","updatedAt":"2026-10-02T04:11:38Z"},"bodyHtml":"<hr>\n<h2>name: ecto-patterns\ndescription: 'Use when a task touches Ecto, even a how-to question: schemas, changesets,\nvalidations, queries, preloads, Multi, migrations, constraints, money fields. Load\nit before writing Ecto code. Skip for Ash.'</h2>\n<h1>Ecto Patterns Reference</h1>\n<p>Reference for working with Ecto schemas, queries, and migrations.</p>\n<h2>Iron Laws — Never Violate These</h2>\n<ol>\n<li><strong>CHANGESETS ARE FOR EXTERNAL DATA</strong> — Use <code>cast/4</code> for user/API input, <code>change/2</code> or <code>put_change/3</code> for internal trusted data</li>\n<li><strong>NEVER USE <code>:float</code> FOR MONEY</strong> — Always use <code>:decimal</code> or <code>:integer</code> (cents)</li>\n<li><strong>NO RAILS-STYLE POLYMORPHIC ASSOCIATIONS</strong> — They break foreign key constraints; use multiple nullable FKs or separate join tables</li>\n<li><strong>ALWAYS PIN VALUES IN QUERIES</strong> — <code>u.name == ^user_input</code> is safe, string interpolation causes SQL injection</li>\n<li><strong>PRELOAD COLLECTIONS, NOT INDIVIDUALS</strong> — Preloading in loops = N+1 queries</li>\n<li><strong>CONSTRAINTS BEAT VALIDATIONS FOR RACE CONDITIONS</strong> — Validations provide quick feedback, constraints provide DB-level safety</li>\n<li><strong>SEPARATE QUERIES FOR <code>has_many</code>, JOIN FOR <code>belongs_to</code></strong> — Avoids row multiplication</li>\n<li><strong>NO IMPLICIT CROSS JOINS</strong> — <code>from(a in A, b in B)</code> without <code>on:</code> creates Cartesian product</li>\n<li><strong>DEDUP BEFORE <code>cast_assoc</code> WITH SHARED DATA</strong> — When multiple parents share child data, deduplicate child records BEFORE building changesets. Dedup only works within a single changeset</li>\n</ol>\n<h2>Quick Schema Template</h2>\n<pre><code>defmodule MyApp.Context.Entity do\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  @primary_key {:id, :binary_id, autogenerate: true}\n  @foreign_key_type :binary_id\n\n  schema \"entities\" do\n    field :name, :string\n    field :status, Ecto.Enum, values: [:draft, :active, :archived]\n    field :amount_cents, :integer  # Never :float for money!\n    belongs_to :user, MyApp.Accounts.User\n    timestamps(type: :utc_datetime_usec)\n  end\n\n  def changeset(entity, attrs) do\n    entity\n    |&gt; cast(attrs, [:name, :status, :amount_cents])\n    |&gt; validate_required([:name])\n    |&gt; foreign_key_constraint(:user_id)\n  end\nend\n</code></pre>\n<h2>Quick Decisions</h2>\n<h3>cast vs put_change vs change</h3>\n<table>\n<thead>\n<tr>\n<th>Function</th>\n<th>Use When</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>cast/4</code></td>\n<td>External data (user input, API)</td>\n</tr>\n<tr>\n<td><code>put_change/3</code></td>\n<td>Internal trusted data (timestamps, computed)</td>\n</tr>\n<tr>\n<td><code>change/2</code></td>\n<td>Internal data from existing struct</td>\n</tr>\n</tbody>\n</table>\n<h3>Preload Strategy</h3>\n<table>\n<thead>\n<tr>\n<th>Relationship</th>\n<th>Strategy</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>belongs_to</code></td>\n<td>JOIN (single query)</td>\n</tr>\n<tr>\n<td><code>has_many</code></td>\n<td>Separate queries (avoid row multiplication)</td>\n</tr>\n</tbody>\n</table>\n<h2>Common Anti-patterns</h2>\n<table>\n<thead>\n<tr>\n<th>Wrong</th>\n<th>Right</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>field :amount, :float</code></td>\n<td><code>field :amount_cents, :integer</code></td>\n</tr>\n<tr>\n<td><code>\"SELECT * WHERE name = '#{name}'\"</code></td>\n<td><code>from(u in User, where: u.name == ^name)</code></td>\n</tr>\n<tr>\n<td><code>Repo.all(User) \\|&gt; Enum.filter(&amp; &amp;1.active)</code></td>\n<td><code>from(u in User, where: u.active)</code></td>\n</tr>\n<tr>\n<td>Preloading in loops</td>\n<td><code>Repo.preload(posts, :comments)</code></td>\n</tr>\n<tr>\n<td><code>Repo.get!(User, user_id)</code> with user input</td>\n<td><code>Repo.get(User, id)</code> + handle nil</td>\n</tr>\n<tr>\n<td><code>{:ok, _} = Repo.update(cs)</code> inside <code>Repo.transaction</code></td>\n<td><code>case</code>/<code>with</code> + <code>Repo.rollback(cs)</code> (see transactions.md)</td>\n</tr>\n</tbody>\n</table>\n<h2>References</h2>\n<p>For detailed patterns, see:</p>\n<ul>\n<li><code>references/changesets.md</code> - cast vs put_change, custom validations, prepare_changes</li>\n<li><code>references/queries.md</code> - Composable queries, dynamic, subqueries, preloading</li>\n<li><code>references/migrations.md</code> - Safe migrations, concurrent indexes, NOT NULL</li>\n<li><code>references/transactions.md</code> - Repo.transact, Ecto.Multi, upserts</li>\n</ul>\n","files":[{"path":"references/changesets.md","sizeBytes":4273,"isText":true},{"path":"references/fulltext-search.md","sizeBytes":5746,"isText":true},{"path":"references/migrations.md","sizeBytes":4623,"isText":true},{"path":"references/queries.md","sizeBytes":4942,"isText":true},{"path":"references/transactions.md","sizeBytes":4554,"isText":true},{"path":"SKILL.md","sizeBytes":3454,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-10-04T15:16:13.813986Z","sha256":"FA0633AE976172E4651D91018BE6B2212FACE53916F71462E9FE285BC3324257","sizeBytes":12107},"review":null,"source":{"repositoryUrl":"https://github.com/oliver-kriska/claude-elixir-phoenix","path":"targets/amp/skills/ecto-patterns","license":"MIT","commit":"9767a82d24ddddad553e85f88efc2869a7fd7d88","subtreeSha":"FFA2A35F56C83A2213419B3DC93C312BED422B2D7ACA86C392FA53C237059A4F","lastSyncedAt":"2026-10-04T15:14:09.139242Z"},"reviewedAt":"2026-10-04T15:19:17.855458Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/oliver-kriska/claude-elixir-phoenix/tree/main/targets/amp/skills/ecto-patterns"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install oliver-kriska-claude-elixir-phoenix@llmmart"},{"target":"git","command":"git clone https://github.com/oliver-kriska/claude-elixir-phoenix.git"}]}