CtrlK
BlogDocsLog inGet started
Tessl Logo

igmarin/elixir-phoenix-skills

Curated library of 38 atomic skills, 7 personas, and 1 orchestrator for Elixir and Phoenix development. Organized by category: fundamentals, phoenix, database, testing, auth, infrastructure, quality, security, integrations, tooling, frameworks, personas, and orchestration. Covers core Elixir patterns, Phoenix LiveView, Ecto, OTP, Oban, testing, security, deployment, real-time, and modern tooling (Req, Swoosh, Cachex, Broadway, Ash).

91

1.37x
Quality

91%

Does it follow best practices?

Impact

91%

1.37x

Average score across 56 eval scenarios

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

SKILL.mdskills/database/ecto-essentials/

name:
ecto-essentials
type:
atomic
tags:
atomic
license:
MIT
description:
MANDATORY for ALL Elixir database work. Invoke before modifying schemas, queries, or migrations. Covers schema definition, changesets, query composition, preloading, transactions, associations, migrations, upserts, dynamic queries, and the context pattern. Trigger words: Ecto, schema, changeset, migration, Repo, query, preload, association, belongs_to, has_many, Elixir database.

Ecto Essentials

Use this skill before modifying ANY schema, query, or migration.

RULES — Follow these with no exceptions

  1. Add database constraints (unique_index, foreign_key, check_constraint) AND changeset validations — both layers are required
  2. Add indexes on foreign keys and frequently queried fields — never omit indexes on foreign keys
  3. Parameterize all user input in queries — never interpolate values into SQL fragments, always use ^
  4. Never combine schema changes and data backfill in the same migration

Schema Definition

defmodule MyApp.Media.Image do
  use Ecto.Schema
  import Ecto.Changeset

  schema "images" do
    field :title, :string
    field :filename, :string
    field :content_type, :string

    belongs_to :folder, MyApp.Media.Folder  # parent uses has_many :images, MyApp.Media.Image

    timestamps()
  end

  def changeset(image, attrs) do
    image
    |> cast(attrs, [:title, :filename, :content_type, :folder_id])
    |> validate_required([:title, :filename, :content_type])
    |> validate_length(:title, min: 1, max: 255)
    |> validate_inclusion(:content_type, ["image/jpeg", "image/png", "image/gif"])
    |> foreign_key_constraint(:folder_id)
  end
end

See assets/changeset_snippets.ex for copy-paste changeset templates (validations, associations, and error formatting).

Query Composition

import Ecto.Query

def list_images_by_folder(folder_id) do
  Image
  |> where([i], i.folder_id == ^folder_id)
  |> order_by([i], desc: i.inserted_at)
  |> Repo.all()
end

def search_images(query_string) do
  search = "%#{query_string}%"

  Image
  |> where([i], ilike(i.title, ^search))
  |> Repo.all()
end

Preloading Associations

N+1 — avoid:

images = Repo.all(Image)
Enum.each(images, fn image -> image.folder.name end)

Single query with preload:

images =
  Image
  |> preload(:folder)
  |> Repo.all()

Enum.each(images, fn image -> image.folder.name end)

Transactions

def transfer_images(image_ids, from_folder_id, to_folder_id) do
  Repo.transaction(fn ->
    with {:ok, from_folder} <- get_folder(from_folder_id),
         {:ok, to_folder} <- get_folder(to_folder_id),
         {count, nil} <- update_images(image_ids, to_folder_id) do
      {:ok, count}
    else
      {:error, reason} -> Repo.rollback(reason)
      _ -> Repo.rollback(:unknown_error)
    end
  end)
end

Ecto.Multi for Complex Operations

def create_user_with_profile(user_attrs, profile_attrs) do
  Ecto.Multi.new()
  |> Ecto.Multi.insert(:user, User.changeset(%User{}, user_attrs))
  |> Ecto.Multi.insert(:profile, fn %{user: user} ->
    Profile.changeset(%Profile{}, Map.put(profile_attrs, :user_id, user.id))
  end)
  |> Repo.transaction()
end

On failure, the error tuple identifies the named step: {:error, :user, changeset, _changes} or {:error, :profile, changeset, _changes}.

Building Associations

def add_image_to_folder(folder, image_attrs) do
  folder
  |> Ecto.build_assoc(:images)
  |> Image.changeset(image_attrs)
  |> Repo.insert()
end

Upsert Operations

def create_or_update_folder(attrs) do
  %Folder{}
  |> Folder.changeset(attrs)
  |> Repo.insert(
    on_conflict: {:replace, [:name, :updated_at]},
    conflict_target: :name
  )
end

Dynamic Queries

def list_images(filters) do
  Enum.reduce(filters, Image, fn
    {:folder_id, id}, q -> where(q, [i], i.folder_id == ^id)
    {:search, term}, q -> where(q, [i], ilike(i.title, ^"%#{term}%"))
    {:content_type, ct}, q -> where(q, [i], i.content_type == ^ct)
    _, q -> q
  end)
  |> Repo.all()
end

Migrations

defmodule MyApp.Repo.Migrations.CreateImages do
  use Ecto.Migration

  def change do
    create table(:images) do
      add :title, :string, null: false
      add :filename, :string, null: false
      add :content_type, :string, null: false
      add :folder_id, references(:folders, on_delete: :nilify_all)

      timestamps()
    end

    create index(:images, [:folder_id])
    create index(:images, [:inserted_at])
  end
end

Migration validation workflow:

  1. Run mix ecto.migrate — confirm it applies without errors
  2. Run mix ecto.rollback — confirm it reverses cleanly
  3. Run mix ecto.migrate again — confirm re-applying succeeds

See assets/migration_checklist.md for the full safe-migration and expand-contract checklist.

Unique Constraints

Add unique constraints in migration AND schema changeset.

# Migration
create unique_index(:folders, [:name])

Context Pattern

Never call Repo from the web layer (LiveViews, controllers) — all database operations belong in context modules.

defmodule MyApp.Media do
  alias MyApp.Media.{Image, Folder}
  alias MyApp.Repo

  def create_image(attrs) do
    %Image{}
    |> Image.changeset(attrs)
    |> Repo.insert()
  end
end

All standard CRUD functions (list_*, get_*!, update_*, delete_*) follow the same pattern.

Related Skills

When Not to Use

  • Simple read-only schema inspection — use mix ecto.schema directly
  • Raw SQL queries that bypass Ecto entirely — write these in a dedicated repo method, not inline in contexts
  • Advanced nested association patterns — use ecto-changeset-patterns instead
  • Migration orchestration planning — use ecto-migration persona instead

Common Pitfalls

❌ Don't✅ Do
Interpolate user input into a query fragmentBind values with ^ so Ecto parameterizes them
Add a changeset validation but skip the DB constraintPair validate_* / unique_constraint with unique_index / references
Leave foreign keys unindexedcreate index(:images, [:folder_id]) in the migration
Access image.folder inside Enum.each (N+1)preload(:folder) before Repo.all/1
Call Repo directly from a LiveView or controllerRoute all queries through a context module
Combine schema changes and data backfill in one migrationSplit into separate migrations (expand → backfill → contract)

Integration

PredecessorThis SkillSuccessor
elixir-essentialsecto-essentialsecto-changeset-patterns
elixir-essentialsecto-essentialstesting-essentials

Companion skills: ecto-changeset-patterns, ecto-nested-associations, ecto-migration, testing-essentials

skills

database

ecto-essentials

.mcp.json

README.md

tile.json