rubyguides

Migrations in Rails

Rails migrations give you a way to describe your database structure in Ruby code. Instead of writing raw SQL to create a table, you call a Ruby method. Rails translates that into the right SQL for whichever database you are using. Migration files live in db/migrate/ and are numbered in the order they run. That number is a timestamp, which keeps two developers from creating conflicting migration numbers.

Creating Tables

The most common migration you will write creates a table. The create_table method takes the table name and a block:

class CreateArticles < ActiveRecord::Migration[7.0]
  def change
    create_table :articles do |t|
      t.string :title, null: false
      t.text :body
      t.integer :views, default: 0
      t.boolean :published, default: false
      t.timestamps
    end
  end
end

The t.timestamps line adds created_at and updated_at columns automatically. Rails manages those columns for you whenever you save a record.

You can skip the auto-incrementing primary key with id: false, or supply a custom primary key name. You can also pass if_not_exists: true to prevent Rails from raising an error if the table already exists when running in production or a shared development environment.

Adding and removing columns

Once a table exists, you will often need to add a column later. After launching your articles table, you might decide you need a slug column for URL-friendly article titles:

class AddSlugToArticles < ActiveRecord::Migration[7.0]
  def change
    add_column :articles, :slug, :string
    add_index :articles, :slug, unique: true
  end
end

Removing a column works the same way:

remove_column :articles, :slug

You can rename columns if you misnamed something early on:

rename_column :users, :login_count, :sign_in_count

Changing a column is possible but requires a bit more care. change_column works inside up/down methods rather than change, because Rails cannot always reverse it automatically:

class ChangeTitleLimit < ActiveRecord::Migration[7.0]
  def up
    change_column :articles, :title, :string, limit: 500
  end

  def down
    change_column :articles, :title, :string, limit: 255
  end
end

The change Method vs up and down

Most simple migrations use change. Rails inspects the operation and knows how to reverse it. add_column reverses to remove_column, create_table reverses to drop_table, and add_index reverses to remove_index.

Some operations are not reversible, however. If you delete rows as part of a migration, Rails cannot know how to restore them. In those cases, write up and down explicitly:

class RemoveLegacyData < ActiveRecord::Migration[7.0]
  def up
    execute "DELETE FROM articles WHERE legacy = 1"
  end

  def down
    # You must restore the data manually if you ever roll back
    execute "INSERT INTO articles (legacy) VALUES (1)"
  end
end

When you need conditional logic inside a reversible migration, use the reversible block:

class ManageTrigger < ActiveRecord::Migration[7.0]
  def change
    reversible do |dir|
      dir.up   { execute "ENABLE TRIGGER audit_trigger" }
      dir.down { execute "DISABLE TRIGGER audit_trigger" }
    end
  end
end

References and foreign keys

Rails offers two ways to create a foreign key relationship. The table builder methods t.references and t.belongs_to are equivalent and create a column named <association>_id with an index:

create_table :comments do |t|
  t.references :article, foreign_key: true, null: false
  t.text :body
  t.timestamps
end

This produces an article_id column on comments with a foreign key constraint pointing back to articles. The polymorphic: true option is useful for comments that can belong to multiple models:

add_reference :comments, :commentable, polymorphic: true, index: true

This adds both commentable_type and commentable_id columns.

The standalone add_reference method creates the same column outside a table creation block:

add_reference :articles, :author, foreign_key: true, null: false

If you already have a column and want to add the foreign key constraint separately, use add_foreign_key:

add_foreign_key :articles, :authors, column: :author_id,
                                    name: :fk_articles_author,
                                    on_delete: :cascade

Running and rolling back migrations

The rake task rails db:migrate runs all pending migrations in order. To check what is running, rails db:migrate:status shows each migration with an up or down indicator:

rails db:migrate:status

If you made a mistake and need to undo the last migration, rails db:rollback drops the last batch. You can roll back more than one step with STEP=n:

rails db:rollback STEP=3

To migrate to a specific version, pass the timestamp:

rails db:migrate VERSION=20260329100000

If a migration fails partway through on PostgreSQL or MySQL, the entire migration rolls back automatically because those databases support transactional DDL. SQLite has limited support for transactional schema changes, so certain operations may leave partial results on failure.

Seeding Data

Migrations are for structure, not data. When you need initial data, use db/seeds.rb:

10.times do |i|
  Article.create!(
    title: "Article #{i + 1}",
    slug: "article-#{i + 1}",
    published: i.even?
  )
end

Run it with rails db:seed. It is not part of the migration chain, so it does not run automatically when you run rails db:migrate.

Common Gotchas

Changing a column with change_column requires up/down methods. Rails will warn you if you try to use it inside change.

Column order matters in some databases. Adding a non-null column without a default to an existing table with rows will fail on most databases because every existing row needs a value. Add the column first with a default or nullable, populate the data, then tighten the constraint in a separate migration.

Avoid editing migrations that have already run on other machines. Instead, write a new migration to correct the schema. Shared migration history is a shared contract.

See Also