Claude-skill-registry ecto-migration-helper
Create, manage, and safely run Ecto database migrations with proper rollback handling and best practices. Use when working with database schema changes, adding columns, or modifying constraints.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/ecto-migration-helper" ~/.claude/skills/majiayu000-claude-skill-registry-ecto-migration-helper && rm -rf "$T"
manifest:
skills/data/ecto-migration-helper/SKILL.mdsource content
Ecto Migration Helper
This skill helps create and manage Ecto migrations safely with proper patterns and rollback support.
When to Use
- Creating new migrations
- Modifying existing tables
- Adding/removing indexes
- Changing constraints
- Data migrations
- Rolling back migrations
Creating Migrations
Generate Empty Migration
mix ecto.gen.migration add_email_to_users
Creates:
priv/repo/migrations/TIMESTAMP_add_email_to_users.exs
Migration Naming Conventions
- Creating new tablecreate_table_name
- Adding columnadd_field_to_table
- Removing columnremove_field_from_table
- Adding indexadd_index_to_table_on_field
- Changing column typemodify_field_in_table
- Adding constraintadd_constraint_to_table
Common Migration Patterns
Adding a Column
defmodule MyApp.Repo.Migrations.AddEmailToUsers do use Ecto.Migration def change do alter table(:users) do add :email, :string end end end
Adding Column with Default
def change do alter table(:users) do add :active, :boolean, default: true, null: false end end
Adding Column with Index
def change do alter table(:users) do add :email, :string end create unique_index(:users, [:email]) end
Adding Foreign Key
def change do alter table(:posts) do add :user_id, references(:users, on_delete: :delete_all), null: false end create index(:posts, [:user_id]) end
Removing a Column
def change do alter table(:users) do remove :old_field end end
WARNING: Removing columns is irreversible with
change. Use up/down:
def up do alter table(:users) do remove :old_field end end def down do alter table(:users) do add :old_field, :string end end
Modifying Column Type
def change do alter table(:products) do modify :price, :decimal, precision: 10, scale: 2 end end
Renaming Column
def change do rename table(:users), :username, to: :name end
Adding Composite Index
def change do create index(:posts, [:user_id, :published_at]) end
Adding Unique Constraint
def change do create unique_index(:users, [:email]) create unique_index(:users, [:organization_id, :email]) # Composite unique end
Adding Check Constraint
def change do create constraint(:products, :price_must_be_positive, check: "price > 0") end
Safe Migration Patterns
Making Columns NOT NULL
WRONG (will fail if existing NULLs):
def change do alter table(:users) do modify :email, :string, null: false # FAILS! end end
RIGHT (two-step approach):
# Migration 1: Add default, fill NULLs def change do # Set default for new rows alter table(:users) do modify :email, :string, default: "unknown@example.com" end # Fill existing NULLs execute( "UPDATE users SET email = 'unknown@example.com' WHERE email IS NULL", "" # No rollback needed ) end # Migration 2: Add NOT NULL constraint def change do alter table(:users) do modify :email, :string, null: false end end
Removing Columns Safely
Step 1: Deploy code that doesn't use the column Step 2: Run migration to remove column (after deployment)
# Deploy this migration AFTER code no longer references the field def up do alter table(:users) do remove :old_field end end def down do alter table(:users) do add :old_field, :string # Specify type for rollback end end
Large Data Migrations
Use batching to avoid locking:
def up do execute """ UPDATE users SET status = 'active' WHERE status IS NULL AND id IN (SELECT id FROM users WHERE status IS NULL LIMIT 1000) """ # Repeat in batches or use recursive function end
Data Migrations
Backfilling Data
defmodule MyApp.Repo.Migrations.BackfillUserDefaults do use Ecto.Migration import Ecto.Query alias MyApp.Repo alias MyApp.Accounts.User def up do # Use application code in migrations carefully User |> where([u], is_nil(u.status)) |> Repo.update_all(set: [status: "active"]) end def down do # Usually no rollback for data migrations :ok end end
Complex Data Migration (Separate Module)
defmodule MyApp.Repo.Migrations.MigrateUserData do use Ecto.Migration def up do MyApp.ReleaseTasks.migrate_user_data() end def down do :ok end end # In lib/my_app/release_tasks.ex defmodule MyApp.ReleaseTasks do def migrate_user_data do # Complex logic here end end
Running Migrations
Development
# Run all pending migrations mix ecto.migrate # Run to specific version mix ecto.migrate --to 20250101120000 # Rollback last migration mix ecto.rollback # Rollback last 3 migrations mix ecto.rollback --step 3 # Rollback to specific version mix ecto.rollback --to 20250101120000
Test Environment
# Create test database MIX_ENV=test mix ecto.create # Run migrations in test MIX_ENV=test mix ecto.migrate # Reset test database (drop, create, migrate) MIX_ENV=test mix ecto.reset
Production
# Run on production (typically via release task) bin/my_app eval "MyApp.ReleaseTasks.migrate()" # Or if mix is available MIX_ENV=prod mix ecto.migrate
Migration Status
# Check migration status mix ecto.migrations # Output shows: # Status Migration ID Migration Name # -------------------------------------------------- # up 20250101120000 create_users # up 20250101130000 add_email_to_users # down 20250101140000 add_profile_to_users
Reversible vs Non-Reversible
Reversible (use change
)
change- Adding columns
- Creating tables
- Adding indexes
- Adding references
Non-Reversible (use up
/down
)
updown- Removing columns (data loss)
- execute() with SQL
- Data transformations
- Dropping tables
Best Practices
1. One Logical Change Per Migration
# Good: Focused migration mix ecto.gen.migration add_email_to_users # Bad: Multiple unrelated changes mix ecto.gen.migration update_users_and_posts_and_comments
2. Always Add Indexes for Foreign Keys
add :user_id, references(:users) create index(:posts, [:user_id]) # Always add this!
3. Specify on_delete for Foreign Keys
# Be explicit about cascade behavior add :user_id, references(:users, on_delete: :delete_all) # Cascade add :user_id, references(:users, on_delete: :nilify_all) # Set NULL add :user_id, references(:users, on_delete: :restrict) # Prevent delete add :user_id, references(:users, on_delete: :nothing) # No action
4. Use Precision for Decimals
# Good add :price, :decimal, precision: 10, scale: 2 # Bad (database decides precision) add :price, :decimal
5. Make Constraints Explicit
# Email should be unique and not null add :email, :string, null: false create unique_index(:users, [:email])
6. Test Rollbacks Locally
# After creating migration mix ecto.migrate mix ecto.rollback mix ecto.migrate
Troubleshooting
Migration Fails
Column already exists:
# Check current schema mix ecto.migrations # Drop and recreate if in development mix ecto.drop && mix ecto.create && mix ecto.migrate
Can't rollback:
- Check if migration uses
vschange
/updown - Review the migration for non-reversible operations
- May need to write custom
functiondown
Lock timeout:
# Add timeout to migration @disable_ddl_transaction true # For operations that can't run in transaction @disable_migration_lock true # For long-running migrations def change do # Migration code end
Data Migration Issues
Timeout on large tables:
- Use batching
- Consider running outside of migration (Rails-style rake task)
- Use
@disable_ddl_transaction true
References to application code:
- Be careful with schema changes
- Application code might change, migration won't
- Consider using raw SQL for data migrations
Advanced Patterns
Concurrent Index Creation (PostgreSQL)
@disable_ddl_transaction true def change do create index(:posts, [:user_id], concurrently: true) end
Conditional Migrations
def change do if function_exported?(MyApp.Repo, :__adapter__, 0) do # Migration code end end
Timestamps Helper
create table(:users) do add :name, :string timestamps() # Adds inserted_at and updated_at end