class AddStatusToPosts < ActiveRecord::Migration[6.1]
# Use change for automatic rollback
def change
# Add column without default to avoid table lock
add_column :posts, :status, :string
# Add index concurrently (PostgreSQL)
add_index :posts, :status, algorithm: :concurrently
end
end
class BackfillPostStatus < ActiveRecord::Migration[6.1]
disable_ddl_transaction! # Required for concurrent index
def up
# Backfill in batches to avoid locking
Post.where(status: nil).in_batches(of: 1000) do |batch|
batch.update_all(status: 'draft')
sleep 0.1 # Throttle to reduce database load
end
# Add NOT NULL constraint after backfill
change_column_null :posts, :status, false
end
def down
change_column_null :posts, :status, true
end
end
class SplitUserName < ActiveRecord::Migration[6.1]
def change
add_column :users, :first_name, :string
add_column :users, :last_name, :string
reversible do |dir|
dir.up do
User.reset_column_information
User.find_each do |user|
parts = user.name.to_s.split(' ', 2)
user.update_columns(
first_name: parts[0],
last_name: parts[1]
)
end
end
dir.down do
User.reset_column_information
User.find_each do |user|
user.update_column(:name, "#{user.first_name} #{user.last_name}".strip)
end
end
end
end
end
Migrations evolve database schema safely across environments. I follow strict conventions: one logical change per migration, descriptive names, reversible operations. Using change instead of up/down enables automatic rollback for most operations. For data migrations, I use reversible blocks or raise ActiveRecord::IrreversibleMigration. Adding indexes happens in separate migrations with algorithm: :concurrently for PostgreSQL to avoid table locks. I never edit committed migrations—create new ones to fix issues. Schema changes deploy before code to support zero-downtime deployments. For large tables, I add columns without defaults, then backfill in batches. Strong migration gem catches dangerous operations before production.