Skip to content
Back to skills

Create Migration

ASecurity

Generate a Rails migration with zero-downtime patterns, correct PostgreSQL types, proper indexes, and foreign-key constraints. TRIGGER when the user wants to add/change a column, table, index, or constraint, or asks for a database migration. Writes the file and explains the deploy sequence — it does NOT run the migration.

  • 2 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 5, 2026
ai-agentsgorubysqlexpressrailsdatabase

Security analysis

A100/100

Scanned September 5, 2026

npx -y skills add mik2win/foureyes --skill create-migration --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Create Migration?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Create Migration
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/mik2win-create-migration/badge)](https://www.skillsdirectory.com/skills/mik2win-create-migration)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: create-migration
description: >-
  Generate a Rails migration with zero-downtime patterns, correct PostgreSQL types, proper
  indexes, and foreign-key constraints.
  TRIGGER when the user wants to add/change a column, table, index, or constraint, or asks
  for a database migration. Writes the file and explains the deploy sequence — it does NOT
  run the migration.
allowed-tools:
  - Read
  - Grep
  - Glob
  - Write
  - Edit
---

# Create Migration Skill

You are a senior Ruby/Rails developer specializing in PostgreSQL. Generate migrations following project conventions defined in the `rails-database` rule.

## 1. Parse Arguments

Parse `$ARGUMENTS` to determine:

- **Migration name** (required): e.g., `AddStatusToOrders`, `CreatePayments`, `RemoveOldColumns`
- **Description** (optional): additional context about what the migration should do

If only a plain description is given (e.g., "add a status column to orders"), derive a proper migration name from it (`AddStatusToOrders`).

## 2. Read Existing Schema

Read `db/schema.rb` (or `db/structure.sql` if present) to understand:

- Existing tables and columns relevant to this migration
- Current index patterns used in the project
- Whether UUIDs are used as primary keys
- Whether `citext` or other PostgreSQL extensions are enabled
- The Rails migration version used (e.g., `[8.0]`)

Also check recent migrations in `db/migrate/` to match the project's migration style and version number.

## 3. Determine Zero-Downtime Pattern

Evaluate whether the migration requires a zero-downtime approach:

### Column Rename -- 3-step process (separate migrations)

Generate THREE separate migration files:

```ruby
# Step 1: Add new column
class AddFullNameToUsers < ActiveRecord::Migration[8.0]
  def change
    add_column :users, :full_name, :string
  end
end

# Step 2: Backfill data (separate deploy)
class BackfillFullName < ActiveRecord::Migration[8.0]
  def up
    User.in_batches.update_all('full_name = name')
  end

  def down; end
end

# Step 3: Remove old column (after step 2 is live)
class RemoveNameFromUsers < ActiveRecord::Migration[8.0]
  def change
    remove_column :users, :name, :string
  end
end
```

Warn the developer: "This requires 3 separate deploys. Deploy step 1 first, then step 2 after it is live, then step 3."

### Column Type Change

Generate separate migrations: add new column, backfill, swap reads, remove old.

### Adding NOT NULL to Existing Column -- 2-step process

```ruby
# Step 1: Add unvalidated check constraint
class AddNotNullConstraintToOrdersStatus < ActiveRecord::Migration[8.0]
  def change
    add_check_constraint :orders, 'status IS NOT NULL',
                         name: 'orders_status_not_null', validate: false
  end
end

# Step 2: Validate constraint and add formal NOT NULL
class ValidateNotNullOnOrdersStatus < ActiveRecord::Migration[8.0]
  def change
    validate_check_constraint :orders, name: 'orders_status_not_null'
    change_column_null :orders, :status, false
    remove_check_constraint :orders, name: 'orders_status_not_null'
  end
end
```

### Index on Large Table

```ruby
class AddIndexToOrdersEmail < ActiveRecord::Migration[8.0]
  disable_ddl_transaction!

  def change
    add_index :orders, :email, algorithm: :concurrently
  end
end
```

Always use `algorithm: :concurrently` + `disable_ddl_transaction!` for indexes on existing tables. The only exception is indexes inside `create_table` blocks (those are safe).

### Removing a Column

Warn the developer to add `self.ignored_columns += ['column_name']` to the model first, deploy, THEN run the removal migration.

## 4. Apply PostgreSQL Type Conventions

| Data | Use | Avoid |
|------|-----|-------|
| Short text | `:string` | -- |
| Long text | `:text` | `:string` with high limit |
| Money | `:decimal, precision: 12, scale: 2` | `:float` |
| Date + time | `:datetime` (maps to `timestamptz` in PG) | raw `timestamp` without tz |
| Boolean | `:boolean, null: false, default: false` | nullable booleans |
| JSON | `:jsonb, default: {}, null: false` | `:json` (not indexable) |
| Email | `:citext` (with `enable_extension 'citext'`) | `:string` (case-sensitive) |
| UUID PKs | `id: :uuid` (with `enable_extension 'pgcrypto'`) | -- |
| Enum | `:integer, default: 0, null: false` | PostgreSQL `CREATE TYPE` enum |
| Counter | `:integer, default: 0, null: false` | nullable counter |

### Column Naming Conventions

- Timestamps: `_at` suffix (`published_at`, `shipped_at`, `confirmed_at`)
- Booleans: no `is_` prefix (`active`, `visible`, `admin` -- generates `active?` predicate)
- Counters: `_count` suffix (`comments_count`, `orders_count`)

## 5. Add Appropriate Indexes

Apply these rules:

- **Foreign keys**: always indexed (Rails does NOT auto-index them)
- **Columns in WHERE clauses**: indexed
- **Columns in ORDER BY**: indexed
- **Unique constraints**: unique index
- **Partial indexes**: use `where:` for columns with many NULLs or common filter conditions
- **Composite indexes**: put the most selective column first; leftmost prefix rule applies
- **Expression indexes**: for computed queries (`lower(email)`)
- **GIN indexes**: for `jsonb` and array columns

```ruby
# Foreign key -- always indexed
add_reference :orders, :user, null: false, foreign_key: true

# Partial index
add_index :orders, :shipped_at, where: 'shipped_at IS NOT NULL', name: 'idx_orders_shipped'

# Composite -- column order matters
add_index :orders, [:user_id, :created_at]

# Unique
add_index :users, :email, unique: true

# GIN for jsonb
add_index :products, :metadata, using: :gin
```

## 6. Add Foreign Key Constraints

- Use `add_reference` with `foreign_key: true` for new FK columns (adds column + index + FK in one call)
- Specify `on_delete` behavior explicitly:

| on_delete | Use When |
|-----------|----------|
| `:cascade` | Child records should be deleted with parent (comments on a post) |
| `:nullify` | Optional relationship (user deletes account, keep orders) |
| `:restrict` | Prevent parent deletion (financial records) |

```ruby
add_reference :orders, :user, null: false, foreign_key: true
add_foreign_key :comments, :posts, on_delete: :cascade
add_foreign_key :payments, :orders, on_delete: :restrict
```

## 7. Generate the Migration File

### File naming

Use the format: `db/migrate/YYYYMMDDHHMMSS_migration_name.rb`

Generate a timestamp based on the current time. Use `Time.now.utc.strftime('%Y%m%d%H%M%S')` format.

### Structure

Use `change` method when possible (auto-reversible). Use `up`/`down` only for raw SQL or non-reversible operations. Use `reversible` block for hybrid cases.

```ruby
# frozen_string_literal: true

class AddStatusToOrders < ActiveRecord::Migration[8.0]
  def change
    add_column :orders, :status, :integer, default: 0, null: false
    add_index :orders, :status
    add_check_constraint :orders, 'status BETWEEN 0 AND 4', name: 'orders_valid_status'
  end
end
```

### One Logical Change Per Migration

Do not mix unrelated table changes. If the request involves multiple unrelated changes, generate separate migration files.

## 8. Post-Generation Rules

- **Never auto-run** the migration. Show the file and tell the developer to run `rails db:migrate` manually.
- If zero-downtime steps are needed, explain the deployment sequence clearly.
- If the migration adds a column that needs backfilling, recommend a separate rake task or `maintenance_tasks` task -- do NOT put data backfill logic in a schema migration.
- Add `add_check_constraint` for enum integer columns.
- If adding a boolean column, always include `null: false` and `default:`.
- Do NOT create test files unless explicitly asked.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.