---
name: ecto-essentials
description: Use when defining schemas, writing queries, or creating migrations — schema design, Repo usage, indexes, query composition.
file_patterns:
  - "**/schemas/**/*.ex"
  - "**/migrations/**/*.exs"
  - "**/repo.ex"
  - "**/*_context.ex"
auto_suggest: true
---

# Ecto Essentials

## RULES — Follow these with no exceptions

1. **Always use changesets** for inserts and updates — never pass raw maps to Repo
2. **Preload associations** before accessing them — avoid N+1 queries
3. **Use transactions** for multi-step operations that must succeed together
4. **Add database constraints** (unique_index, foreign_key, check_constraint) AND changeset validations
5. **Use contexts** for database access — never call Repo directly from web layer
6. **Add indexes** on foreign keys and frequently queried fields
7. **Use timestamps()** in every schema — track when records were created/updated

---

## Schema Definition

Define schemas with proper types and associations.

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

  schema "images" do
    field :title, :string
    field :description, :string
    field :filename, :string
    field :file_path, :string
    field :content_type, :string
    field :file_size, :integer

    belongs_to :folder, MyApp.Media.Folder

    timestamps()
  end
end
```

## Changesets

Always use changesets for data validation and casting.

```elixir
def changeset(image, attrs) do
  image
  |> cast(attrs, [:title, :description, :filename, :file_path, :content_type, :file_size, :folder_id])
  |> validate_required([:title, :filename, :file_path, :content_type, :file_size])
  |> validate_length(:title, min: 1, max: 255)
  |> validate_inclusion(:content_type, ["image/jpeg", "image/png", "image/gif"])
  |> validate_number(:file_size, greater_than: 0, less_than: 10_000_000)
  |> foreign_key_constraint(:folder_id)
end
```

## Query Composition

Build queries composably using `Ecto.Query`.

```elixir
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) or ilike(i.description, ^search))
  |> Repo.all()
end
```

## Preloading Associations

Use `preload` to avoid N+1 queries.

**Bad:**
```elixir
images = Repo.all(Image)
# Later accessing image.folder causes N queries
Enum.each(images, fn image -> image.folder.name end)
```

**Good:**
```elixir
images =
  Image
  |> preload(:folder)
  |> Repo.all()

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

## Transactions

Use `Repo.transaction` for operations that must succeed together.

```elixir
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
      # Repo.transaction wraps the return — don't return {:ok, _} from inside
      count
    else
      {:error, reason} -> Repo.rollback(reason)
      _ -> Repo.rollback(:unknown_error)
    end
  end)
end

# Repo.transaction/1 always wraps the fn's return in {:ok, _}.
# Returning {:ok, count} from inside would yield {:ok, {:ok, count}} to the caller.
# Callers see {:ok, count} or {:error, reason}:
case transfer_images(ids, from_id, to_id) do
  # count is the number of images transferred
  {:ok, count} -> Logger.info("Images transferred", count: count)
  # transaction rolled back
  {:error, reason} -> Logger.error("Transfer failed", reason: inspect(reason))
end
```

## Insert and Update

Use `Repo.insert` and `Repo.update` with changesets.

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

def update_image(%Image{} = image, attrs) do
  image
  |> Image.changeset(attrs)
  |> Repo.update()
end
```

## Upsert Operations

Use `on_conflict` for upsert behavior.

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

## Associations

Define associations properly in schemas.

```elixir
# Parent schema
defmodule MyApp.Media.Folder do
  use Ecto.Schema

  schema "folders" do
    field :name, :string
    has_many :images, MyApp.Media.Image

    timestamps()
  end
end

# Child schema
defmodule MyApp.Media.Image do
  use Ecto.Schema

  schema "images" do
    field :title, :string
    belongs_to :folder, MyApp.Media.Folder

    timestamps()
  end
end
```

## Building Associations

Use `Ecto.build_assoc` to create associated records.

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

## Casting Associations

> `cast_assoc` usage, the FK-in-child-changeset pitfall, and nested-data patterns: see the
> **ecto-changeset-patterns** and **ecto-nested-associations** skills.

## Dynamic Queries

Build queries dynamically based on filters.

```elixir
def list_images(filters) do
  Image
  |> apply_filters(filters)
  |> Repo.all()
end

defp apply_filters(query, filters) do
  Enum.reduce(filters, query, fn
    {:folder_id, folder_id}, query ->
      where(query, [i], i.folder_id == ^folder_id)

    {:search, term}, query ->
      where(query, [i], ilike(i.title, ^"%#{term}%"))

    {:min_size, size}, query ->
      where(query, [i], i.file_size >= ^size)

    _, query ->
      query
  end)
end
```

## Aggregations

Use aggregation functions for statistics.

```elixir
def count_images_by_folder do
  Image
  |> group_by([i], i.folder_id)
  |> select([i], {i.folder_id, count(i.id)})
  |> Repo.all()
  |> Map.new()
end

def total_storage_used do
  Image
  |> select([i], sum(i.file_size))
  |> Repo.one()
end
```

## Repo Functions

Common `Repo` operations:

```elixir
# Fetch single record
Repo.get(Image, id)           # Returns record or nil
Repo.get!(Image, id)          # Returns record or raises
Repo.get_by(Image, title: "Photo")

# Fetch all records
Repo.all(Image)

# Insert
Repo.insert(changeset)        # Returns {:ok, record} or {:error, changeset}
Repo.insert!(changeset)       # Returns record or raises

# Update
Repo.update(changeset)
Repo.update!(changeset)

# Delete
Repo.delete(record)
Repo.delete!(record)

# Delete all matching
Repo.delete_all(Image)
Repo.delete_all(where(Image, [i], i.folder_id == ^folder_id))
```

## Migrations

Write clear, reversible migrations.

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

  def change do
    create table(:images) do
      add :title, :string, null: false
      add :description, :text
      add :filename, :string, null: false
      add :file_path, :string, null: false
      add :content_type, :string, null: false
      add :file_size, :integer, 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
```

## Unique Constraints

Add unique constraints in schema and migration.

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

# Schema changeset
def changeset(folder, attrs) do
  folder
  |> cast(attrs, [:name])
  |> validate_required([:name])
  |> unique_constraint(:name)
end
```

## Virtual Fields

Use virtual fields for computed or temporary data.

```elixir
schema "images" do
  field :title, :string
  field :file_path, :string
  field :url, :string, virtual: true

  timestamps()
end

def with_url(%Image{} = image) do
  %{image | url: "/uploads/#{Path.basename(image.file_path)}"}
end
```

## Custom Types

Define custom Ecto types for special data.

```elixir
defmodule MyApp.FileSize do
  use Ecto.Type

  def type, do: :integer

  def cast(size) when is_integer(size) and size >= 0, do: {:ok, size}
  def cast(_), do: :error

  def load(size), do: {:ok, size}
  def dump(size), do: {:ok, size}
end
```

## Context Pattern

Organize database operations in contexts.

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

  def list_images, do: Repo.all(Image)

  def get_image!(id), do: Repo.get!(Image, id)

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

  def update_image(%Image{} = image, attrs) do
    image
    |> Image.changeset(attrs)
    |> Repo.update()
  end

  def delete_image(%Image{} = image) do
    Repo.delete(image)
  end
end
```

## Testing

When writing tests for Ecto schemas, changesets, or contexts, invoke `elixir-phoenix-guide:testing-essentials` before writing any `_test.exs` file.
