Ruby on Rails stores all Active Record migrations in the db/migrate folder. New migrations are generated with a simple command:

bin/rails generate migration NameForMigration

This generates the following no-op migration file to fill in:

class NameForMigration < ActiveRecord::Migration[6.1]
  def change
  end
end

In addition to no-op migrations, Rails can auto-generate simple migrations (e.g. adding columns or foreign keys) simply by augmenting the basic command and following the established conventions for naming. A command might look like this:

bin/rails generate migration AddColumnsToTable column1:string column2:decimal

Which produces this migration file:

class AddColumnsToTable < ActiveRecord::Migration[6.1]
  def change
    add_column :table, :column1, :string
    add_column :table, :column2, :decimal
  end
end

For such migrations, Rails will auto-magically know how to handle both the apply and rollback steps. However, for more complex migrations, human intervention is needed. When filling in the no-op file by hand, Rails will only know how to apply or rollback a migration if both directions are implemented. This can be done using the reversible feature documented here, but I prefer to switch out the default change method for up and down instead.

Why bother implementing the rollback?

Rollbacks aren't used often, but they're great for emergencies. If a code upgrade that requires a database migration is pushed to production and that code upgrade is later found to be buggy, it may need to be reverted. To do this and preserve compatibility between the codebase and the database, the database will need the appropriate migration to be rolled back as well. In these cases, having a working bi-directional migration file already written can help minimize the downtime.

Example

Imagine a table with a zip_code column that is an integer when it should be a string. This kind of migration is too complex for Ruby to auto-generate. Instead, use up and down like this:

class ChangeZipCodeForTable < ActiveRecord::Migration[6.1]
  def up
    change_column :table, :zip_code, :string, using: "LPAD(code::VARCHAR, 5, '0')"
  end

  def down
    change_column :table, :zip_code, :integer, using: "code::INTEGER"
  end
end

And that's it! Pretty simple, right?

Oh, and don't forget to test the rollback step before submitting the PR:

bin/rails db:rollback