Claude Skill

ecto-constraint-debug

Debug Ecto constraint violations - trace triggers, check migrations,

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download oliver-kriska-claude-elixir-phoenix-targets_amp_skills_ecto-constraint-debug-9767a82.zip · 3 KB
Part of oliver-kriska/claude-elixir-phoenix — 93 skills

Install

skills CLI npx skills add https://github.com/oliver-kriska/claude-elixir-phoenix/tree/main/targets/amp/skills/ecto-constraint-debug
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install oliver-kriska-claude-elixir-phoenix@llmmart
Git git clone https://github.com/oliver-kriska/claude-elixir-phoenix.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole oliver-kriska/claude-elixir-phoenix collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Ecto Constraint Debugging

Ash projects: Ash surfaces DB constraints through its own error DSL. Use the ash-framework skill — mix usage_rules.search_docs "constraint" -p ash_postgres.

Systematic approach to diagnosing constraint violations. Load when you see Ecto.ConstraintError, unique_constraint, foreign_key_constraint, or constraint-related changeset errors.

Iron Laws

  1. READ THE CONSTRAINT NAME — The constraint name (e.g., links_url_index) tells you exactly which index/constraint failed. Parse it from the error message first
  2. CHECK MIGRATION BEFORE CODE — Verify the constraint definition in priv/repo/migrations/ matches what the schema expects
  3. TRACE ALL INSERT PATHS — Find every code path that inserts into the constrained table. The bug is often in a path you didn't consider
  4. RACE CONDITION UNTIL PROVEN OTHERWISE — If validation passes but constraint fails, assume concurrent inserts until you prove a single-request cause

Step-by-Step Debugging

Step 1: Parse the Error

Extract from the error message:

  • Constraint name (e.g., users_email_index)
  • Table name (e.g., users)
  • Operation (insert, update, or delete)
  • Conflicting values (if available in logs)

Step 2: Find the Migration

Use Grep to search for the constraint name in priv/repo/migrations/. Also check for create unique_index, create index, add constraint.

Verify: Does the migration constraint match the schema's unique_constraint/3 or foreign_key_constraint/3 call?

Step 3: Find the Schema

Use Grep to find constraint handling in changesets (unique_constraint, foreign_key_constraint, check_constraint) in lib/.

Step 4: Trace Insert Paths

Find ALL callers that insert/update this schema:

Use Grep to find all insert/update paths (Repo.insert, Repo.update, Repo.insert_all, cast_assoc) in lib/.

Step 5: Identify the Cause

Symptom Likely Cause Fix Pattern
Same user triggers twice Race condition (double-click, retry) Upsert with on_conflict
Multiple parents share child cast_assoc doesn't dedup across changesets Dedup before building changesets
Concurrent API requests Missing transaction isolation Wrap in Repo.transaction or use upsert
Migration added constraint to existing data Data violates new constraint Backfill or clean data first

Step 6: Apply Fix

See references/constraint-patterns.md for detailed fix patterns.

Quick Fixes by Constraint Type

Unique violation → Upsert: Repo.insert(changeset, on_conflict: :replace_all, conflict_target: [:field])

Foreign key violation → Check: Does the referenced record exist? Was it deleted concurrently?

Check constraint → Validate: Does the value satisfy the constraint condition?

References

  • references/constraint-patterns.md - Detailed patterns for each constraint type
Files (claude-elixir-phoenix)
  • references
    • constraint-patterns.md 5.1 KB
      # Constraint Debugging Patterns
      
      ## Unique Constraint Violations
      
      ### Pattern 1: Race Condition (Double Submit)
      
      **Symptom**: `unique_constraint` error on user action, works on retry.
      
      **Root cause**: Two concurrent requests insert the same unique value.
      
      **Fix**: Upsert pattern
      
      ```elixir
      def create_or_update_link(attrs) do
        %Link{}
        |> Link.changeset(attrs)
        |> Repo.insert(
          on_conflict: {:replace, [:updated_at]},
          conflict_target: [:url],
          returning: true
        )
      end
      ```
      
      ### Pattern 2: Shared Data via `cast_assoc`
      
      **Symptom**: Inserting parent records that share child associations fails on the second parent.
      
      **Root cause**: `cast_assoc` builds separate INSERT for each parent's children. If two parents reference the same child (e.g., same URL), the second INSERT violates the unique constraint.
      
      **Fix**: Deduplicate before building changesets
      
      ```elixir
      # BAD: Each contact gets its own link changesets
      contacts
      |> Enum.map(fn contact ->
        Contact.changeset(contact, %{links: extract_links(contact.text)})
      end)
      
      # GOOD: Deduplicate links first, then associate
      all_links = contacts |> Enum.flat_map(&extract_links(&1.text)) |> Enum.uniq_by(& &1.url)
      {:ok, links} = Repo.insert_all(Link, all_links, on_conflict: :nothing, returning: true)
      link_map = Map.new(links, &{&1.url, &1.id})
      
      contacts
      |> Enum.map(fn contact ->
        link_ids = contact.text |> extract_links() |> Enum.map(&link_map[&1.url])
        Contact.changeset(contact, %{link_ids: link_ids})
      end)
      ```
      
      ### Pattern 3: Bulk Insert with Duplicates
      
      **Symptom**: `insert_all` fails when input data has duplicate values for a unique column.
      
      **Fix**: Deduplicate input or use `on_conflict: :nothing`
      
      ```elixir
      # Deduplicate input
      unique_records = Enum.uniq_by(records, & &1.email)
      
      # Or handle at DB level
      Repo.insert_all(User, records,
        on_conflict: :nothing,
        conflict_target: [:email]
      )
      ```
      
      ## Foreign Key Violations
      
      ### Pattern 1: Orphaned Reference
      
      **Symptom**: Insert/update fails because referenced record doesn't exist.
      
      **Root cause**: Parent record was deleted between validation and insert, or ID was passed incorrectly.
      
      **Fix**: Check existence in transaction
      
      ```elixir
      Repo.transact(fn ->
        case Repo.get(Parent, parent_id) do
          nil -> {:error, :parent_not_found}
          parent ->
            %Child{parent_id: parent.id}
            |> Child.changeset(attrs)
            |> Repo.insert()
        end
      end)
      ```
      
      ### Pattern 2: Cascade Delete Surprise
      
      **Symptom**: Deleting a parent silently deletes children (or fails if no cascade).
      
      **Check migration**: Look for `on_delete` option
      
      ```elixir
      # In migration
      add :parent_id, references(:parents, on_delete: :delete_all)  # CASCADE
      add :parent_id, references(:parents, on_delete: :restrict)      # BLOCK
      add :parent_id, references(:parents, on_delete: :nilify_all)    # SET NULL
      add :parent_id, references(:parents, on_delete: :nothing)       # DB DEFAULT
      ```
      
      ## Check Constraint Violations
      
      ### Pattern 1: Enum Mismatch
      
      **Symptom**: Insert fails on check constraint for an Ecto.Enum field.
      
      **Root cause**: Value not in the allowed list defined in migration.
      
      **Debug**: Compare schema enum values with migration constraint
      
      ```elixir
      # Schema
      field :status, Ecto.Enum, values: [:draft, :active, :archived]
      
      # Migration must match
      create constraint(:items, :status_must_be_valid,
        check: "status IN ('draft', 'active', 'archived')")
      ```
      
      ### Pattern 2: Range Violation
      
      **Symptom**: Value fails a range check constraint.
      
      **Debug**: Read the constraint definition in migration
      
      ```bash
      grep -r "create constraint.*table_name" priv/repo/migrations/
      ```
      
      ## Debugging Techniques
      
      ### Inspect the Changeset Error
      
      ```elixir
      case Repo.insert(changeset) do
        {:ok, record} -> {:ok, record}
        {:error, changeset} ->
          # constraint errors appear in changeset.errors
          IO.inspect(changeset.errors, label: "INSERT ERRORS")
          IO.inspect(changeset.changes, label: "ATTEMPTED CHANGES")
          {:error, changeset}
      end
      ```
      
      ### Check for Existing Data
      
      ```elixir
      # Find what's violating the unique constraint
      Repo.all(from r in Record, where: r.unique_field == ^value)
      ```
      
      ### Trace with Tidewave (when available)
      
      ```
      mcp__tidewave__execute_sql_query "SELECT * FROM table WHERE unique_col = 'value'"
      mcp__tidewave__project_eval "MyApp.Repo.all(from r in MyApp.Record, where: r.field == ^value)"
      ```
      
      ## Prevention Patterns
      
      ### Always Handle Constraint Errors
      
      ```elixir
      def create_entity(attrs) do
        %Entity{}
        |> Entity.changeset(attrs)
        |> Repo.insert()
        |> case do
          {:ok, entity} -> {:ok, entity}
          {:error, %{errors: [field: {_, [constraint: :unique]}]}} ->
            # Handle duplicate gracefully
            {:error, :already_exists}
          {:error, changeset} -> {:error, changeset}
        end
      end
      ```
      
      ### Use Upserts for Idempotency
      
      ```elixir
      def upsert_entity(attrs) do
        %Entity{}
        |> Entity.changeset(attrs)
        |> Repo.insert(
          on_conflict: {:replace, [:name, :updated_at]},
          conflict_target: [:external_id],
          returning: true
        )
      end
      ```
      
      ### Add Both Validation AND Constraint
      
      ```elixir
      def changeset(entity, attrs) do
        entity
        |> cast(attrs, [:email])
        |> validate_required([:email])
        |> unsafe_validate_unique(:email, MyApp.Repo)  # Quick feedback
        |> unique_constraint(:email)                     # DB safety
      end
      ```
      
  • SKILL.md 3.1 KB
    ---
    name: ecto-constraint-debug
    description: Debug Ecto constraint violations - trace triggers, check migrations,
      find duplicate data. Use when seeing unique_constraint, foreign_key_constraint,
      or check_constraint errors.
    ---
    
    # Ecto Constraint Debugging
    
    > **Ash projects**: Ash surfaces DB constraints through its own error DSL. Use the `ash-framework` skill — `mix usage_rules.search_docs "constraint" -p ash_postgres`.
    
    Systematic approach to diagnosing constraint violations. Load when you see `Ecto.ConstraintError`, `unique_constraint`, `foreign_key_constraint`, or constraint-related changeset errors.
    
    ## Iron Laws
    
    1. **READ THE CONSTRAINT NAME** — The constraint name (e.g., `links_url_index`) tells you exactly which index/constraint failed. Parse it from the error message first
    2. **CHECK MIGRATION BEFORE CODE** — Verify the constraint definition in `priv/repo/migrations/` matches what the schema expects
    3. **TRACE ALL INSERT PATHS** — Find every code path that inserts into the constrained table. The bug is often in a path you didn't consider
    4. **RACE CONDITION UNTIL PROVEN OTHERWISE** — If validation passes but constraint fails, assume concurrent inserts until you prove a single-request cause
    
    ## Step-by-Step Debugging
    
    ### Step 1: Parse the Error
    
    Extract from the error message:
    
    - **Constraint name** (e.g., `users_email_index`)
    - **Table name** (e.g., `users`)
    - **Operation** (insert, update, or delete)
    - **Conflicting values** (if available in logs)
    
    ### Step 2: Find the Migration
    
    Use Grep to search for the constraint name in `priv/repo/migrations/`. Also check for `create unique_index`, `create index`, `add constraint`.
    
    Verify: Does the migration constraint match the schema's `unique_constraint/3` or `foreign_key_constraint/3` call?
    
    ### Step 3: Find the Schema
    
    Use Grep to find constraint handling in changesets (`unique_constraint`, `foreign_key_constraint`, `check_constraint`) in `lib/`.
    
    ### Step 4: Trace Insert Paths
    
    Find ALL callers that insert/update this schema:
    
    Use Grep to find all insert/update paths (`Repo.insert`, `Repo.update`, `Repo.insert_all`, `cast_assoc`) in `lib/`.
    
    ### Step 5: Identify the Cause
    
    | Symptom | Likely Cause | Fix Pattern |
    |---------|-------------|-------------|
    | Same user triggers twice | Race condition (double-click, retry) | Upsert with `on_conflict` |
    | Multiple parents share child | `cast_assoc` doesn't dedup across changesets | Dedup before building changesets |
    | Concurrent API requests | Missing transaction isolation | Wrap in `Repo.transaction` or use upsert |
    | Migration added constraint to existing data | Data violates new constraint | Backfill or clean data first |
    
    ### Step 6: Apply Fix
    
    See `references/constraint-patterns.md` for detailed fix patterns.
    
    ## Quick Fixes by Constraint Type
    
    **Unique violation** → Upsert: `Repo.insert(changeset, on_conflict: :replace_all, conflict_target: [:field])`
    
    **Foreign key violation** → Check: Does the referenced record exist? Was it deleted concurrently?
    
    **Check constraint** → Validate: Does the value satisfy the constraint condition?
    
    ## References
    
    - `references/constraint-patterns.md` - Detailed patterns for each constraint type
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related