Skill v1.0.0
currentAutomated scan100/100version: "1.0.0" name: ecto-essentials description: MANDATORY for ALL database work. Invoke before modifying schemas, queries, or migrations. file_patterns:
- "/schemas//*.ex"
- "/migrations//*.exs"
- "**/repo.ex"
- "**/*_context.ex"
auto_suggest: true
Ecto Essentials
RULES — Follow these with no exceptions
- Always use changesets for inserts and updates — never pass raw maps to Repo
- Preload associations before accessing them — avoid N+1 queries
- Use transactions for multi-step operations that must succeed together
- Add database constraints (unique_index, foreign_key, check_constraint) AND changeset validations
- Use contexts for database access — never call Repo directly from web layer
- Add indexes on foreign keys and frequently queried fields
- Use timestamps() in every schema — track when records were created/updated
Schema Definition
Define schemas with proper types and associations.
defmodule MyApp.Media.Image douse Ecto.Schemaimport Ecto.Changesetschema "images" dofield :title, :stringfield :description, :stringfield :filename, :stringfield :file_path, :stringfield :content_type, :stringfield :file_size, :integerbelongs_to :folder, MyApp.Media.Foldertimestamps()endend
Changesets
Always use changesets for data validation and casting.
def changeset(image, attrs) doimage|> 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.
import Ecto.Querydef list_images_by_folder(folder_id) doImage|> where([i], i.folder_id == ^folder_id)|> order_by([i], desc: i.inserted_at)|> Repo.all()enddef search_images(query_string) dosearch = "%#{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:
images = Repo.all(Image)# Later accessing image.folder causes N queriesEnum.each(images, fn image -> image.folder.name end)
Good:
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.
def transfer_images(image_ids, from_folder_id, to_folder_id) doRepo.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)endend)end
Insert and Update
Use Repo.insert and Repo.update with changesets.
def create_image(attrs) do%Image{}|> Image.changeset(attrs)|> Repo.insert()enddef update_image(%Image{} = image, attrs) doimage|> Image.changeset(attrs)|> Repo.update()end
Upsert Operations
Use on_conflict for upsert behavior.
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.
# Parent schemadefmodule MyApp.Media.Folder douse Ecto.Schemaschema "folders" dofield :name, :stringhas_many :images, MyApp.Media.Imagetimestamps()endend# Child schemadefmodule MyApp.Media.Image douse Ecto.Schemaschema "images" dofield :title, :stringbelongs_to :folder, MyApp.Media.Foldertimestamps()endend
Building Associations
Use Ecto.build_assoc to create associated records.
def add_image_to_folder(folder, image_attrs) dofolder|> Ecto.build_assoc(:images)|> Image.changeset(image_attrs)|> Repo.insert()end
Casting Associations
Use cast_assoc when working with nested data.
def changeset(folder, attrs) dofolder|> cast(attrs, [:name])|> cast_assoc(:images, with: &Image.changeset/2)|> validate_required([:name])end
Dynamic Queries
Build queries dynamically based on filters.
def list_images(filters) doImage|> apply_filters(filters)|> Repo.all()enddefp apply_filters(query, filters) doEnum.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 ->queryend)end
Aggregations
Use aggregation functions for statistics.
def count_images_by_folder doImage|> group_by([i], i.folder_id)|> select([i], {i.folder_id, count(i.id)})|> Repo.all()|> Map.new()enddef total_storage_used doImage|> select([i], sum(i.file_size))|> Repo.one()end
Repo Functions
Common Repo operations:
# Fetch single recordRepo.get(Image, id) # Returns record or nilRepo.get!(Image, id) # Returns record or raisesRepo.get_by(Image, title: "Photo")# Fetch all recordsRepo.all(Image)# InsertRepo.insert(changeset) # Returns {:ok, record} or {:error, changeset}Repo.insert!(changeset) # Returns record or raises# UpdateRepo.update(changeset)Repo.update!(changeset)# DeleteRepo.delete(record)Repo.delete!(record)# Delete all matchingRepo.delete_all(Image)Repo.delete_all(where(Image, [i], i.folder_id == ^folder_id))
Migrations
Write clear, reversible migrations.
defmodule MyApp.Repo.Migrations.CreateImages douse Ecto.Migrationdef change docreate table(:images) doadd :title, :string, null: falseadd :description, :textadd :filename, :string, null: falseadd :file_path, :string, null: falseadd :content_type, :string, null: falseadd :file_size, :integer, null: falseadd :folder_id, references(:folders, on_delete: :nilify_all)timestamps()endcreate index(:images, [:folder_id])create index(:images, [:inserted_at])endend
Unique Constraints
Add unique constraints in schema and migration.
# Migrationcreate unique_index(:folders, [:name])# Schema changesetdef changeset(folder, attrs) dofolder|> cast(attrs, [:name])|> validate_required([:name])|> unique_constraint(:name)end
Virtual Fields
Use virtual fields for computed or temporary data.
schema "images" dofield :title, :stringfield :file_path, :stringfield :url, :string, virtual: truetimestamps()enddef with_url(%Image{} = image) do%{image | url: "/uploads/#{Path.basename(image.file_path)}"}end
Custom Types
Define custom Ecto types for special data.
defmodule MyApp.FileSize douse Ecto.Typedef type, do: :integerdef cast(size) when is_integer(size) and size >= 0, do: {:ok, size}def cast(_), do: :errordef load(size), do: {:ok, size}def dump(size), do: {:ok, size}end
Context Pattern
Organize database operations in contexts.
defmodule MyApp.Media doalias MyApp.Media.{Image, Folder}alias MyApp.Repodef 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()enddef update_image(%Image{} = image, attrs) doimage|> Image.changeset(attrs)|> Repo.update()enddef delete_image(%Image{} = image) doRepo.delete(image)endend
Testing
When writing tests for Ecto schemas, changesets, or contexts, invoke elixir-phoenix-guide:testing-essentials before writing any _test.exs file.