Creates safe database migrations with proper indexes and rollback strategies. Use when creating tables, adding columns, creating indexes, handling zero-downtime migrations, or when user mentions migrations, schema changes, or database structure.
Safe database migrations are critical for production stability:
# Generate migration
bin/rails generate migration AddStatusToEvents status:integer
# Run migrations
bin/rails db:migrate
# Rollback
bin/rails db:rollback
# Check status
bin/rails db:migrate:status
Migration Safety:
- [ ] Migration is reversible (has down or uses change)
- [ ] Large tables use batching for updates
- [ ] Indexes added concurrently (if needed)
- [ ] Foreign keys have indexes
- [ ] NOT NULL added in two steps (for existing columns)
- [ ] Default values don't lock table
- [ ] Tested rollback locally
# db/migrate/20240115000001_add_status_to_events.rb
class AddStatusToEvents < ActiveRecord::Migration[8.0]
def change
add_column :events, :status, :integer, default: 0, null: false
end
end
For existing tables with data, add NOT NULL in two migrations:
# Step 1: Add column with default (allows NULL temporarily)
# db/migrate/20240115000001_add_priority_to_tasks.rb
class AddPriorityToTasks < ActiveRecord::Migration[8.0]
def change
add_column :tasks, :priority, :integer, default: 0
end
end
# Step 2: Add NOT NULL constraint after backfill
# db/migrate/20240115000002_add_not_null_to_tasks_priority.rb
class AddNotNullToTasksPriority < ActiveRecord::Migration[8.0]
def change
change_column_null :tasks, :priority, false
end
end
# db/migrate/20240115000001_add_index_to_events_status.rb
class AddIndexToEventsStatus < ActiveRecord::Migration[8.0]
disable_ddl_transaction!
def change
add_index :events, :status, algorithm: :concurrently, if_not_exists: true
end
end
# db/migrate/20240115000001_add_account_to_events.rb
class AddAccountToEvents < ActiveRecord::Migration[8.0]
def change
add_reference :events, :account, null: false, foreign_key: true, index: true
end
end
rename_column breaks production code if deployed while the old column name is still referenced. Always use a multi-step approach:
# Step 1: Add new column (deploy)
class AddTitleToEvents < ActiveRecord::Migration[8.0]
def change
add_column :events, :title, :string
end
end
# Step 2: Backfill + update code to use new column (deploy)
# Keep old column readable during transition
# Step 3: Remove old column once code no longer references it (deploy)
class RemoveNameFromEvents < ActiveRecord::Migration[8.0]
def change
safety_assured { remove_column :events, :name, :string }
end
end
⚠️ Never use
rename_columnon production tables in use — it breaks any code referencing the old column until the next deploy.
First, remove references in code, then migrate:
# db/migrate/20240115000001_remove_legacy_field_from_events.rb
class RemoveLegacyFieldFromEvents < ActiveRecord::Migration[8.0]
def change
# safety_assured tells strong_migrations this is intentional
safety_assured { remove_column :events, :legacy_field, :string }
end
end
# db/migrate/20240115000001_add_status_enum_to_orders.rb
class AddStatusEnumToOrders < ActiveRecord::Migration[8.0]
disable_ddl_transaction!
def change
# Use integer for Rails enum
add_column :orders, :status, :integer, default: 0, null: false
# Concurrent index — safe for large tables
add_index :orders, :status, algorithm: :concurrently
end
end
In model:
class Order < ApplicationRecord
enum :status, { pending: 0, confirmed: 1, shipped: 2, delivered: 3, cancelled: 4 }
end
# DANGEROUS - can lose data or lock table
class ChangeColumnType < ActiveRecord::Migration[8.0]
def change
change_column :events, :budget, :decimal # DON'T DO THIS
end
end
# Step 1: Add new column
class AddBudgetDecimalToEvents < ActiveRecord::Migration[8.0]
def change
add_column :events, :budget_decimal, :decimal, precision: 10, scale: 2
end
end
# Step 2: Backfill data (in a rake task or separate migration)
class BackfillEventsBudget < ActiveRecord::Migration[8.0]
disable_ddl_transaction!
def up
Event.in_batches.update_all("budget_decimal = budget")
end
def down
# Data migration, no rollback needed
end
end
# Step 3: Remove old column (after code updated)
class RemoveOldBudgetFromEvents < ActiveRecord::Migration[8.0]
def change
safety_assured { remove_column :events, :budget, :integer }
rename_column :events, :budget_decimal, :budget
end
end
class BackfillEventStatus < ActiveRecord::Migration[8.0]
disable_ddl_transaction!
def up
Event.unscoped.in_batches(of: 1000) do |batch|
batch.where(status: nil).update_all(status: 0)
sleep(0.1) # Reduce database load
end
end
def down
# No rollback for data migration
end
end
# Migration just adds column
class AddProcessedAtToEvents < ActiveRecord::Migration[8.0]
def change
add_column :events, :processed_at, :datetime
end
end
# Separate job for backfill
class BackfillProcessedAtJob < ApplicationJob
def perform(start_id, end_id)
Event.where(id: start_id..end_id, processed_at: nil)
.update_all(processed_at: Time.current)
end
end
# Rake task to enqueue
# lib/tasks/backfill.rake
namespace :backfill do
task processed_at: :environment do
Event.in_batches(of: 10_000) do |batch|
BackfillProcessedAtJob.perform_later(batch.minimum(:id), batch.maximum(:id))
end
end
end
# For queries: WHERE account_id = ? AND status = ?
add_index :events, [:account_id, :status]
# Order matters! This index helps:
# - WHERE account_id = ?
# - WHERE account_id = ? AND status = ?
# But NOT:
# - WHERE status = ?
# Index only active records
add_index :events, :event_date, where: "status = 0", name: "index_events_on_date_active"
# Index only non-null values
add_index :users, :reset_token, where: "reset_token IS NOT NULL"
# Unique constraint
add_index :users, :email, unique: true
# Unique within scope
add_index :event_vendors, [:event_id, :vendor_id], unique: true
class AddForeignKeys < ActiveRecord::Migration[8.0]
def change
# With automatic index
add_reference :events, :venue, foreign_key: true
# To existing column
add_foreign_key :events, :accounts
# With specific column name
add_foreign_key :events, :users, column: :organizer_id
end
end
# ON DELETE CASCADE (delete children when parent deleted)
add_foreign_key :comments, :posts, on_delete: :cascade
# ON DELETE NULLIFY (set to NULL when parent deleted)
add_foreign_key :posts, :users, column: :author_id, on_delete: :nullify
# ON DELETE RESTRICT (prevent parent deletion)
add_foreign_key :orders, :users, on_delete: :restrict
# Gemfile
gem 'strong_migrations'
# config/initializers/strong_migrations.rb
StrongMigrations.start_after = 20240101000000
# Target version for safe operations
StrongMigrations.target_version = 16 # PostgreSQL version
# Custom checks
StrongMigrations.add_check do |method, args|
if method == :add_column && args[1] == :events
stop! "Check with team before modifying events table"
end
end
class AddColumnWithDefault < ActiveRecord::Migration[8.0]
def change
# Tell strong_migrations this is safe
safety_assured do
add_column :events, :priority, :integer, default: 0, null: false
end
end
end
class CreateEvents < ActiveRecord::Migration[8.0]
def change
create_table :events do |t|
t.string :name, null: false
t.date :event_date
t.references :account, null: false, foreign_key: true
t.timestamps
end
add_index :events, [:account_id, :event_date]
end
end
class ChangeEventsStructure < ActiveRecord::Migration[8.0]
def up
# Complex change
execute <<-SQL
ALTER TABLE events ADD CONSTRAINT check_positive_budget
CHECK (budget_cents >= 0)
SQL
end
def down
execute <<-SQL
ALTER TABLE events DROP CONSTRAINT check_positive_budget
SQL
end
end
class DropLegacyTable < ActiveRecord::Migration[8.0]
def up
drop_table :legacy_events
end
def down
raise ActiveRecord::IrreversibleMigration, "Cannot restore dropped table"
end
end
# Migrate and rollback
bin/rails db:migrate
bin/rails db:rollback
bin/rails db:migrate
# Check for issues
bin/rails db:migrate:status
# spec/db/schema_spec.rb
RSpec.describe "Database Schema" do
it "has all foreign keys indexed" do
foreign_keys = ActiveRecord::Base.connection.foreign_keys(:events)
indexes = ActiveRecord::Base.connection.indexes(:events)
foreign_keys.each do |fk|
indexed = indexes.any? { |idx| idx.columns.first == fk.column }
expect(indexed).to be(true), "Missing index for #{fk.column}"
end
end
end
# DON'T - Locks entire table
add_index :large_table, :column
# DO - Non-blocking
disable_ddl_transaction!
add_index :large_table, :column, algorithm: :concurrently
# DON'T - Updates all at once
Event.update_all(status: 0)
# DO - Updates in batches
Event.in_batches(of: 1000) do |batch|
batch.update_all(status: 0)
end
npx skills add rivettidaniel/database-migrations下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer