CtrlK
CommunityDocumentationLog inGet started
Tessl Logo

database-migrations

tessl i github:ThibautBaissac/rails_ai_agents --skill database-migrations

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.

86%

Overall

SKILL.md
Review
Evals

Database Migration Patterns for Rails 8

Overview

Safe database migrations are critical for production stability:

  • Zero-downtime deployments
  • Reversible migrations
  • Proper indexing
  • Data integrity constraints
  • Performance considerations

Quick Start

# 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

Safety Checklist

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

Safe Migration Patterns

Pattern 1: Add Column (Safe)

# 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

Pattern 2: Add Column with NOT NULL (Two-Step)

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

Pattern 3: Add Index (Production Safe)

# 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

Pattern 4: Add Foreign Key with Index

# 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

Pattern 5: Rename Column (Safe)

# db/migrate/20240115000001_rename_name_to_title_on_events.rb
class RenameNameToTitleOnEvents < ActiveRecord::Migration[8.0]
  def change
    rename_column :events, :name, :title
  end
end

Pattern 6: Remove Column (Safe)

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

Pattern 7: Add Enum Column

# db/migrate/20240115000001_add_status_enum_to_orders.rb
class AddStatusEnumToOrders < ActiveRecord::Migration[8.0]
  def change
    # Use integer for Rails enum
    add_column :orders, :status, :integer, default: 0, null: false

    # Add index for queries
    add_index :orders, :status
  end
end

In model:

class Order < ApplicationRecord
  enum :status, { pending: 0, confirmed: 1, shipped: 2, delivered: 3, cancelled: 4 }
end

Dangerous Operations (Avoid)

DON'T: Change Column Type Directly

# 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

DO: Add New Column, Migrate Data, Remove Old

# 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

Data Migrations

Safe Backfill Pattern

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

Using Background Job for Large Tables

# 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

Index Strategies

Composite Indexes

# 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 = ?

Partial Indexes

# 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 Indexes

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

# Unique within scope
add_index :event_vendors, [:event_id, :vendor_id], unique: true

Foreign Keys

Adding Foreign Keys

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

Foreign Key Options

# 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

Strong Migrations Gem

Installation

# Gemfile
gem 'strong_migrations'

Configuration

# 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

Handling Warnings

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

Reversible Migrations

Using change (Automatic Reversal)

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

Using up/down (Manual Reversal)

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

Irreversible Migrations

class DropLegacyTable < ActiveRecord::Migration[8.0]
  def up
    drop_table :legacy_events
  end

  def down
    raise ActiveRecord::IrreversibleMigration, "Cannot restore dropped table"
  end
end

Testing Migrations

Test Rollback

# Migrate and rollback
bin/rails db:migrate
bin/rails db:rollback
bin/rails db:migrate

# Check for issues
bin/rails db:migrate:status

Schema Check

# 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

Performance Tips

Avoid Table Locks

# DON'T - Locks entire table
add_index :large_table, :column

# DO - Non-blocking
disable_ddl_transaction!
add_index :large_table, :column, algorithm: :concurrently

Batch Operations

# 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

Checklist

  • Migration is reversible
  • Indexes on foreign keys
  • Concurrent index creation for large tables
  • NOT NULL added safely (two-step)
  • Data migrations use batching
  • Tested rollback locally
  • strong_migrations gem checks pass
  • No table locks during deploy
Repository
github.com/ThibautBaissac/rails_ai_agents
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.