How do I generate a migration?
Rails migrations are Ruby classes that modify your database schema over time.
Instead of writing raw SQL, you use a Ruby DSL that's database-agnostic and
version-controlled. To create a new migration, use the rails generate migration
command (or rails g migration for short).
Basic Migration Generator
Create an empty migration file with a descriptive name:
rails generate migration AddEmailToUsers
This creates a timestamped file in db/migrate/:
# db/migrate/20260106120000_add_email_to_users.rb
class AddEmailToUsers < ActiveRecord::Migration[7.1]
def change
end
end
Generate with Column Definitions
Rails can automatically generate the migration body if you follow naming conventions.
Use AddXxxToYyy or RemoveXxxFromYyy patterns:
# Add a single column
rails g migration AddEmailToUsers email:string
# Add multiple columns
rails g migration AddDetailsToUsers email:string age:integer
# Add column with index
rails g migration AddEmailToUsers email:string:index
# Add column with unique index
rails g migration AddEmailToUsers email:string:uniq
The generated migration includes the column definition:
class AddEmailToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :email, :string
add_index :users, :email, unique: true
end
end
Common Error
If you see this error when generating a migration:
Another migration is already named add_email_to_users: db/migrate/20260105_add_email_to_users.rb
Fix: Use a unique, descriptive name for your migration. Rails prevents duplicate migration names even with different timestamps.
How do I run migrations?
After creating migrations, you need to run them to apply the changes to your database.
Rails tracks which migrations have been run in the schema_migrations table,
so each migration only runs once.
Run All Pending Migrations
rails db:migrate
You'll see output like:
== 20260106120000 AddEmailToUsers: migrating ==================================
-- add_column(:users, :email, :string)
-> 0.0012s
== 20260106120000 AddEmailToUsers: migrated (0.0013s) =========================
Run Migrations in a Specific Environment
# Production
RAILS_ENV=production rails db:migrate
# Test
RAILS_ENV=test rails db:migrate
Common Error: Pending Migrations
If you try to start your Rails server with pending migrations:
Migrations are pending. To resolve this issue, run:
bin/rails db:migrate RAILS_ENV=development
ActiveRecord::PendingMigrationError
Fix: Run rails db:migrate to apply pending migrations before
starting your server.
Common Error: Migration Failed
If a migration fails partway through:
== 20260106120000 AddEmailToUsers: migrating ==================================
-- add_column(:users, :email, :string)
rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:
PG::DuplicateColumn: ERROR: column "email" of relation "users" already exists
Fix: The column already exists. Either remove the existing column first, or modify your migration to check for existence:
def change
add_column :users, :email, :string unless column_exists?(:users, :email)
end
How do I check migration status?
Before running or rolling back migrations, you often want to see which migrations
have been applied and which are pending. Rails provides the db:migrate:status
command for this.
View Migration Status
rails db:migrate:status
Output shows each migration's status:
database: myapp_development
Status Migration ID Migration Name
--------------------------------------------------
up 20260101120000 Create users
up 20260102120000 Add name to users
down 20260106120000 Add email to users
Status meanings:
up- Migration has been applied to the databasedown- Migration has not been run (pending)
Common Issue: Missing Migration File
If you see this in your status output:
Status Migration ID Migration Name
--------------------------------------------------
up 20260101120000 Create users
up 20260102120000 ********** NO FILE **********
up 20260103120000 Add name to users
This means a migration was run but the file no longer exists (perhaps deleted or
on another branch). The migration is recorded in schema_migrations
but Rails can't find the corresponding file.
Fix: If you're sure the migration should be removed from history, you can delete the record:
rails runner "ActiveRecord::SchemaMigration.where(version: '20260102120000').delete_all"
Check for Any Pending Migrations
In Rails console, you can programmatically check:
# Returns true if there are pending migrations
ActiveRecord::Migration.check_pending!
How do I rollback a migration?
Rolling back a migration reverses the changes it made to your database. This is useful during development when you need to modify a migration you just created, or when you need to undo changes.
Rollback the Last Migration
rails db:rollback
Output:
== 20260106120000 AddEmailToUsers: reverting ==================================
-- remove_column(:users, :email, :string)
-> 0.0015s
== 20260106120000 AddEmailToUsers: reverted (0.0234s) =========================
Rollback Multiple Migrations
# Rollback last 3 migrations
rails db:rollback STEP=3
Rollback to a Specific Version
# Rollback to (but not including) this migration
rails db:migrate VERSION=20260101120000
Common Error: IrreversibleMigration
If you try to rollback a migration that can't be reversed:
rails aborted!
ActiveRecord::IrreversibleMigration:
This migration uses remove_column, which is not automatically reversible.
To make the migration reversible you can either:
1. Define #up and #down methods in place of the #change method.
2. Use the #reversible method to define reversible behavior.
Fix: Some operations like remove_column in a change
method lose information (the column type) needed to reverse. Specify the column type:
# This is reversible - Rails knows how to re-add the column
def change
remove_column :users, :email, :string
end
# Or use up/down for complex cases
def up
remove_column :users, :email
end
def down
add_column :users, :email, :string
end
Common Error: Column Referenced by Foreign Key
PG::DependentObjectsStillExist: ERROR: cannot drop column email because other objects depend on it
Fix: Remove the foreign key constraint first, then the column:
def change
remove_foreign_key :orders, :users
remove_column :users, :email, :string
end
How do I rollback multiple migrations?
Sometimes you need to undo several migrations at once, either to return to a known good state or to rework a series of related changes.
Using STEP
# Rollback the last 3 migrations
rails db:rollback STEP=3
Using VERSION
Rollback all migrations after a specific version:
# Rollback to version 20260101120000 (this migration stays applied)
rails db:migrate VERSION=20260101120000
Redo Migrations
Rollback and re-run migrations in one command:
# Redo last migration
rails db:migrate:redo
# Redo last 3 migrations
rails db:migrate:redo STEP=3
Common Error: Partial Rollback Failure
If one migration in a series fails to rollback:
== 20260103120000 AddIndexToUsers: reverting ==================================
rails aborted!
ActiveRecord::IrreversibleMigration
Fix: You'll need to manually fix the problematic migration by adding
proper up/down methods, then continue the rollback.
How do I run a specific migration?
Sometimes you need to run or re-run a specific migration without affecting others. This is useful for debugging or when migrations get out of sync.
Run Up to a Specific Version
# Run all migrations up to and including this version
rails db:migrate VERSION=20260106120000
Run a Single Migration (Up or Down)
# Run a specific migration's "up" method
rails db:migrate:up VERSION=20260106120000
# Run a specific migration's "down" method
rails db:migrate:down VERSION=20260106120000
Re-run a Specific Migration
# Rollback and re-run a specific migration
rails db:migrate:redo VERSION=20260106120000
Common Error: Migration Already Run
== 20260106120000 AddEmailToUsers: migrating ==================================
(0.0ms) Migration 20260106120000 has already been run.
Fix: The migration is already in your schema_migrations table.
Use db:migrate:redo to rollback and re-run, or manually remove the version:
rails runner "ActiveRecord::SchemaMigration.where(version: '20260106120000').delete_all"
How do I run migrations in different environments?
Rails maintains separate databases for development, test, and production environments. Each environment tracks its own migration state independently.
Specify Environment
# Development (default)
rails db:migrate
# Test
RAILS_ENV=test rails db:migrate
# Production
RAILS_ENV=production rails db:migrate
Prepare Test Database
The test database is usually maintained automatically, but you can manually sync it:
# Load schema into test database
rails db:test:prepare
# Or reset test database completely
RAILS_ENV=test rails db:reset
Production Migrations
In production, you typically run migrations during deployment:
# Common deployment pattern
RAILS_ENV=production rails db:migrate
# Or with release phase (Heroku, Render, etc.)
# This runs automatically during deploy
Common Error: Wrong Database
PG::ConnectionBad: could not connect to server: Connection refused
Is the server running on host "localhost" and accepting
TCP/IP connections on port 5432?
Fix: Check your config/database.yml for the correct environment
settings and ensure the database server is running.
How do I create a table?
The create_table method is the foundation of Rails migrations. It creates
a new table with the columns you specify, plus automatic id,
created_at, and updated_at columns by default.
Using the Generator
# Generate a model with migration
rails g model User name:string email:string age:integer
# Generate just the migration
rails g migration CreateUsers name:string email:string
Basic create_table
class CreateUsers < ActiveRecord::Migration[7.1]
def change
create_table :users do |t|
t.string :name
t.string :email
t.integer :age
t.boolean :admin, default: false
t.text :bio
t.timestamps # adds created_at and updated_at
end
end
end
Common Column Types
create_table :products do |t|
t.string :name # VARCHAR(255)
t.text :description # TEXT
t.integer :quantity # INTEGER
t.bigint :views # BIGINT
t.float :rating # FLOAT
t.decimal :price, precision: 10, scale: 2 # DECIMAL(10,2)
t.boolean :active # BOOLEAN
t.date :release_date # DATE
t.time :available_at # TIME
t.datetime :published_at # DATETIME/TIMESTAMP
t.binary :image_data # BLOB/BYTEA
t.json :metadata # JSON (if supported)
t.timestamps
end
Common Error: Table Already Exists
PG::DuplicateTable: ERROR: relation "users" already exists
-- or --
Mysql2::Error: Table 'users' already exists
Fix: The table already exists. Either drop it first or use if_not_exists:
create_table :users, if_not_exists: true do |t|
# ...
end
How do I create a table only if it doesn't exist?
Sometimes you need to create a table conditionally, especially when working with existing databases or writing idempotent migrations.
Using if_not_exists Option
class CreateUsers < ActiveRecord::Migration[7.1]
def change
create_table :users, if_not_exists: true do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
Check Table Existence Manually
class CreateUsers < ActiveRecord::Migration[7.1]
def change
unless table_exists?(:users)
create_table :users do |t|
t.string :name
t.timestamps
end
end
end
end
Important Note on Reversibility
When using if_not_exists: true, Rails may not know whether it created
the table, which can affect rollback behavior. For cleaner migrations, prefer checking
explicitly:
def up
return if table_exists?(:users)
create_table :users do |t|
t.string :name
t.timestamps
end
end
def down
drop_table :users, if_exists: true
end
How do I create a table with a custom primary key?
By default, Rails creates an auto-incrementing id column as the primary key.
You can customize this to use a different column name, type, or disable it entirely.
Custom Primary Key Name
create_table :users, primary_key: :user_id do |t|
t.string :name
t.timestamps
end
String Primary Key
create_table :countries, id: false, primary_key: :code do |t|
t.string :code, null: false, primary_key: true
t.string :name
t.timestamps
end
No Primary Key
For join tables or special cases:
create_table :users_roles, id: false do |t|
t.references :user, null: false, foreign_key: true
t.references :role, null: false, foreign_key: true
end
# Add composite index instead
add_index :users_roles, [:user_id, :role_id], unique: true
Using bigint Instead of integer
# Rails 5.1+ uses bigint by default, but you can be explicit:
create_table :users, id: :bigint do |t|
t.string :name
t.timestamps
end
Common Error: Primary Key Mismatch
ActiveRecord::MismatchedForeignKey: Column `user_id` on table `posts`
does not match column `id` on `users`, which has type `bigint`.
Fix: Ensure foreign key columns match the primary key type:
# If users.id is bigint, use bigint for the foreign key
t.bigint :user_id
# or
t.references :user, type: :bigint, foreign_key: true
How do I create a table with UUID primary key?
UUIDs (Universally Unique Identifiers) are useful when you need globally unique IDs, want to hide record counts, or need to generate IDs before inserting records.
PostgreSQL with pgcrypto
# First, enable the extension (run once)
class EnableUuid < ActiveRecord::Migration[7.1]
def change
enable_extension 'pgcrypto'
end
end
# Then create tables with UUID
class CreateUsers < ActiveRecord::Migration[7.1]
def change
create_table :users, id: :uuid do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
Set UUID as Default for All Tables
In config/application.rb:
config.generators do |g|
g.orm :active_record, primary_key_type: :uuid
end
UUID Foreign Keys
create_table :posts, id: :uuid do |t|
t.references :user, type: :uuid, foreign_key: true
t.string :title
t.timestamps
end
Common Error: Extension Not Enabled
PG::UndefinedFunction: ERROR: function gen_random_uuid() does not exist
Fix: Enable the pgcrypto extension:
enable_extension 'pgcrypto'
How do I create a table with composite primary keys?
Composite primary keys use multiple columns together as the primary key. Rails 7.1+ has built-in support for composite primary keys.
Create Table with Composite Primary Key
class CreateTravelRoutes < ActiveRecord::Migration[7.1]
def change
create_table :travel_routes, primary_key: [:origin, :destination] do |t|
t.string :origin, null: false
t.string :destination, null: false
t.integer :distance
t.decimal :price, precision: 10, scale: 2
t.timestamps
end
end
end
In the Model
class TravelRoute < ApplicationRecord
self.primary_key = [:origin, :destination]
end
# Query using composite key
route = TravelRoute.find(["NYC", "LAX"])
Foreign Keys to Composite Primary Keys
class CreateBookings < ActiveRecord::Migration[7.1]
def change
create_table :bookings do |t|
t.string :route_origin, null: false
t.string :route_destination, null: false
t.datetime :departure_time
t.timestamps
end
add_foreign_key :bookings, :travel_routes,
column: [:route_origin, :route_destination],
primary_key: [:origin, :destination]
end
end
How do I create a join table?
Join tables (also called junction tables or bridge tables) connect two tables in a many-to-many relationship. Rails provides a dedicated method for creating them.
Using create_join_table
class CreateJoinTableUsersRoles < ActiveRecord::Migration[7.1]
def change
create_join_table :users, :roles do |t|
t.index [:user_id, :role_id]
t.index [:role_id, :user_id]
end
end
end
This creates a table named roles_users (alphabetical order) with user_id and role_id columns.
Custom Table Name
create_join_table :users, :roles, table_name: :user_roles do |t|
t.index [:user_id, :role_id], unique: true
end
With Additional Columns
create_join_table :users, :projects do |t|
t.string :role # e.g., "owner", "member", "viewer"
t.datetime :joined_at
t.index [:user_id, :project_id], unique: true
end
Manual Alternative
create_table :users_roles, id: false do |t|
t.references :user, null: false, foreign_key: true
t.references :role, null: false, foreign_key: true
end
add_index :users_roles, [:user_id, :role_id], unique: true
How do I add comments to a table or column?
Database comments help document your schema directly in the database. They're visible in database tools and can be useful for teams.
Comment on Table Creation
create_table :users, comment: 'Registered user accounts' do |t|
t.string :email, comment: 'Primary login identifier'
t.string :name, comment: 'Display name, not unique'
t.integer :status, comment: '0=pending, 1=active, 2=suspended'
t.timestamps
end
Add Comment to Existing Table
change_table_comment :users, from: nil, to: 'Registered user accounts'
# Or without tracking the previous value (not reversible)
change_table_comment :users, 'Registered user accounts'
Add Comment to Existing Column
change_column_comment :users, :status, from: nil, to: '0=pending, 1=active, 2=suspended'
View Comments
Comments appear in db/schema.rb:
create_table "users", comment: "Registered user accounts", force: :cascade do |t|
t.string "email", comment: "Primary login identifier"
# ...
end
Note: Comments are supported in PostgreSQL and MySQL. SQLite does not support comments.
How do I drop a table?
Use drop_table to remove a table from your database. Be careful—this
permanently deletes all data in the table.
Basic Drop Table
class DropLegacyUsers < ActiveRecord::Migration[7.1]
def change
drop_table :legacy_users
end
end
Reversible Drop (Recommended)
To make the migration reversible, provide the table schema:
class DropLegacyUsers < ActiveRecord::Migration[7.1]
def change
drop_table :legacy_users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
Using up/down
class DropLegacyUsers < ActiveRecord::Migration[7.1]
def up
drop_table :legacy_users
end
def down
create_table :legacy_users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
Common Error: Foreign Key Constraint
PG::DependentObjectsStillExist: ERROR: cannot drop table users because other objects depend on it
-- or --
Mysql2::Error: Cannot delete or update a parent row: a foreign key constraint fails
Fix: Remove foreign key constraints first, or use CASCADE:
# Remove specific foreign key
remove_foreign_key :posts, :users
drop_table :users
# Or use cascade (PostgreSQL)
drop_table :users, force: :cascade
How do I drop a table only if it exists?
When writing idempotent migrations or cleaning up uncertain state, you may want to drop a table only if it exists.
Using if_exists Option
class DropLegacyTables < ActiveRecord::Migration[7.1]
def change
drop_table :legacy_users, if_exists: true
drop_table :legacy_orders, if_exists: true
end
end
Check Manually
class DropLegacyUsers < ActiveRecord::Migration[7.1]
def change
drop_table :legacy_users if table_exists?(:legacy_users)
end
end
Important: Reversibility
Using if_exists: true without a block makes the migration irreversible
because Rails doesn't know the table schema:
# NOT reversible
drop_table :users, if_exists: true
# Reversible - provide the schema
drop_table :users, if_exists: true do |t|
t.string :name
t.timestamps
end
Why is my drop_table migration not reversible?
When you run drop_table without specifying the table schema, Rails
doesn't know how to recreate the table on rollback.
The Error
rails aborted!
ActiveRecord::IrreversibleMigration:
To avoid mistakes, drop_table is only reversible if given options or a block (to recreate the table).
Fix: Provide Table Schema
class DropUsers < ActiveRecord::Migration[7.1]
def change
# Pass a block with the table definition
drop_table :users do |t|
t.string :name, null: false
t.string :email, null: false, index: { unique: true }
t.string :password_digest
t.boolean :admin, default: false
t.timestamps
end
end
end
Alternative: Use up/down
class DropUsers < ActiveRecord::Migration[7.1]
def up
drop_table :users
end
def down
create_table :users do |t|
t.string :name, null: false
t.string :email, null: false
t.timestamps
end
add_index :users, :email, unique: true
end
end
Copy Schema from schema.rb
The easiest way to get the exact schema is to copy the table definition from
db/schema.rb before dropping.
How do I add a column to an existing table?
Use add_column to add a new column to an existing table. This is one
of the most common migration operations.
Using the Generator
# Rails recognizes the AddXxxToYyy pattern
rails g migration AddEmailToUsers email:string
# With multiple columns
rails g migration AddFieldsToUsers email:string age:integer admin:boolean
Basic add_column
class AddEmailToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :email, :string
end
end
With Options
add_column :users, :email, :string, null: false, default: ''
add_column :users, :age, :integer, limit: 2 # SMALLINT
add_column :users, :score, :decimal, precision: 5, scale: 2
add_column :users, :status, :integer, default: 0, null: false
add_column :users, :metadata, :jsonb, default: {}
Add Column with Index
class AddEmailToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :email, :string
add_index :users, :email, unique: true
end
end
Common Error: Column Already Exists
PG::DuplicateColumn: ERROR: column "email" of relation "users" already exists
Fix: Check if column exists first:
add_column :users, :email, :string unless column_exists?(:users, :email)
How do I add multiple columns at once?
You can add multiple columns in a single migration using multiple add_column
calls or using change_table for a cleaner syntax.
Multiple add_column Calls
class AddProfileFieldsToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :first_name, :string
add_column :users, :last_name, :string
add_column :users, :phone, :string
add_column :users, :bio, :text
add_column :users, :avatar_url, :string
end
end
Using change_table
class AddProfileFieldsToUsers < ActiveRecord::Migration[7.1]
def change
change_table :users do |t|
t.string :first_name
t.string :last_name
t.string :phone
t.text :bio
t.string :avatar_url
end
end
end
Using change_table with bulk: true
For databases that support it (MySQL), this combines changes into a single ALTER statement:
class AddProfileFieldsToUsers < ActiveRecord::Migration[7.1]
def change
change_table :users, bulk: true do |t|
t.string :first_name
t.string :last_name
t.string :phone
t.index :phone
end
end
end
Generator Shortcut
rails g migration AddProfileFieldsToUsers first_name:string last_name:string phone:string bio:text
How do I remove a column from a table?
Use remove_column to delete a column. To make it reversible,
you must specify the column type.
Using the Generator
# Rails recognizes RemoveXxxFromYyy pattern
rails g migration RemoveEmailFromUsers email:string
Basic remove_column (Reversible)
class RemoveEmailFromUsers < ActiveRecord::Migration[7.1]
def change
# Specify type to make it reversible
remove_column :users, :email, :string
end
end
With All Column Options
# Include all options for perfect rollback
remove_column :users, :email, :string, null: false, default: '', index: true
Remove Multiple Columns
class RemoveLegacyFieldsFromUsers < ActiveRecord::Migration[7.1]
def change
remove_column :users, :legacy_id, :integer
remove_column :users, :old_email, :string
remove_column :users, :deprecated_flag, :boolean
end
end
# Or using change_table
class RemoveLegacyFieldsFromUsers < ActiveRecord::Migration[7.1]
def change
change_table :users do |t|
t.remove :legacy_id, type: :integer
t.remove :old_email, type: :string
t.remove :deprecated_flag, type: :boolean
end
end
end
Common Error: Irreversible Migration
ActiveRecord::IrreversibleMigration:
remove_column is only reversible if given a type.
Fix: Always include the column type:
# Bad - not reversible
remove_column :users, :email
# Good - reversible
remove_column :users, :email, :string
How do I remove a column from multiple tables?
When refactoring your schema, you might need to remove the same column from several tables. Create a single migration that handles all the changes.
Remove Same Column from Multiple Tables
class RemoveLegacyTimestamps < ActiveRecord::Migration[7.1]
def change
# Remove deprecated_at from multiple tables
remove_column :users, :deprecated_at, :datetime
remove_column :posts, :deprecated_at, :datetime
remove_column :comments, :deprecated_at, :datetime
end
end
Using a Loop
class RemoveLegacyColumn < ActiveRecord::Migration[7.1]
TABLES = [:users, :posts, :comments, :orders, :products]
def change
TABLES.each do |table|
remove_column table, :legacy_id, :integer if column_exists?(table, :legacy_id)
end
end
end
With Different Column Types per Table
class CleanupLegacyColumns < ActiveRecord::Migration[7.1]
def change
remove_column :users, :old_email, :string
remove_column :posts, :view_count, :integer
remove_column :orders, :legacy_status, :string
end
end
Reversible with Full Schema
class RemoveSoftDeleteColumns < ActiveRecord::Migration[7.1]
def change
remove_column :users, :deleted_at, :datetime, index: true
remove_column :posts, :deleted_at, :datetime, index: true
remove_column :comments, :deleted_at, :datetime, index: true
end
end
How do I change a column's type?
Use change_column to modify a column's type, constraints, or options.
Note that changing column types can cause data loss if the types are incompatible.
Basic Type Change
class ChangeAgeToString < ActiveRecord::Migration[7.1]
def up
change_column :users, :age, :string
end
def down
change_column :users, :age, :integer
end
end
Why change_column Needs up/down
change_column is not automatically reversible because Rails doesn't
track the previous column state.
Using reversible
class ChangeStatusToInteger < ActiveRecord::Migration[7.1]
def change
reversible do |dir|
dir.up do
change_column :orders, :status, :integer, using: 'status::integer'
end
dir.down do
change_column :orders, :status, :string
end
end
end
end
String to Text
class ChangeBioToText < ActiveRecord::Migration[7.1]
def up
change_column :users, :bio, :text
end
def down
change_column :users, :bio, :string, limit: 255
end
end
Common Error: Data Truncation
PG::StringDataRightTruncation: ERROR: value too long for type character varying(50)
Fix: When shrinking column size, either clean up data first or use a larger limit:
# Clean up data first
User.where("LENGTH(name) > 50").update_all("name = LEFT(name, 50)")
# Then change the column
change_column :users, :name, :string, limit: 50
How do I rename a column?
Use rename_column to change a column's name. This operation is
automatically reversible.
Basic Rename
class RenameNameToFullName < ActiveRecord::Migration[7.1]
def change
rename_column :users, :name, :full_name
end
end
Rename Multiple Columns
class RenameUserColumns < ActiveRecord::Migration[7.1]
def change
rename_column :users, :fname, :first_name
rename_column :users, :lname, :last_name
rename_column :users, :mail, :email
end
end
After Renaming
Remember to update your code:
- Model validations and associations
- Controller parameters (strong params)
- Views and form fields
- API responses
- Tests and factories
Common Error: Index Name Mismatch
If the column had an index, the index name might now be inconsistent:
# After renaming email to primary_email, you might want to rename the index too
rename_index :users, 'index_users_on_email', 'index_users_on_primary_email'
Why doesn't rename_column rename my foreign key constraint?
When you rename a column that has a foreign key constraint, the constraint name doesn't automatically update. This is a known Rails behavior.
The Problem
# Original
add_reference :posts, :author, foreign_key: { to_table: :users }
# Creates FK named: fk_rails_xxx or posts_author_id_fk
# After renaming
rename_column :posts, :author_id, :creator_id
# FK constraint still references old column name internally
Fix: Recreate the Foreign Key
class RenameAuthorToCreator < ActiveRecord::Migration[7.1]
def change
# Remove old foreign key
remove_foreign_key :posts, column: :author_id
# Rename column
rename_column :posts, :author_id, :creator_id
# Add new foreign key
add_foreign_key :posts, :users, column: :creator_id
end
end
Check Foreign Key Names
# In Rails console
ActiveRecord::Base.connection.foreign_keys(:posts)
PostgreSQL: Rename Constraint Directly
class RenameAuthorToCreator < ActiveRecord::Migration[7.1]
def change
rename_column :posts, :author_id, :creator_id
# Rename the constraint in PostgreSQL
execute "ALTER TABLE posts RENAME CONSTRAINT fk_rails_xxx TO fk_rails_yyy"
end
end
What data types are available in Rails migrations?
Rails provides database-agnostic types that map to native database types. Here's a comprehensive reference.
Common Types
create_table :examples do |t|
# Strings
t.string :name # VARCHAR(255)
t.text :description # TEXT (unlimited length)
# Numbers
t.integer :count # INTEGER (4 bytes)
t.bigint :large_count # BIGINT (8 bytes)
t.float :rating # FLOAT
t.decimal :price, precision: 10, scale: 2 # DECIMAL
# Boolean
t.boolean :active # BOOLEAN
# Date/Time
t.date :birth_date # DATE
t.time :start_time # TIME
t.datetime :published_at # DATETIME/TIMESTAMP
t.timestamp :processed_at # Same as datetime
# Binary
t.binary :image_data # BLOB/BYTEA
# Special
t.json :metadata # JSON (native if supported)
t.jsonb :settings # JSONB (PostgreSQL only)
t.timestamps # created_at + updated_at
end
Database-Specific Types
# PostgreSQL
t.uuid :external_id
t.inet :ip_address
t.cidr :network
t.macaddr :mac_address
t.hstore :properties
t.jsonb :data
t.daterange :availability
t.tsrange :duration
t.point :location
t.line :path
t.polygon :area
t.interval :duration
# MySQL
t.unsigned_integer :count
t.unsigned_bigint :large_count
Type Modifiers
t.string :code, limit: 10 # VARCHAR(10)
t.decimal :amount, precision: 8, scale: 2
t.integer :age, limit: 2 # SMALLINT
t.integer :views, limit: 8 # BIGINT
# Array type (PostgreSQL)
t.string :tags, array: true, default: []
t.integer :scores, array: true
How do I add a string column with a specific length?
Use the limit option to specify the maximum character length for
a string column. This maps to VARCHAR(n) in the database.
Specify String Length
class AddCodeToProducts < ActiveRecord::Migration[7.1]
def change
add_column :products, :sku, :string, limit: 20
add_column :products, :country_code, :string, limit: 2
add_column :products, :postal_code, :string, limit: 10
end
end
In create_table
create_table :countries do |t|
t.string :code, limit: 2, null: false # ISO country code
t.string :code3, limit: 3 # 3-letter code
t.string :name, limit: 100
t.string :phone_code, limit: 5
t.timestamps
end
String vs Text
# Use string for short, fixed-ish length content
t.string :email, limit: 255 # VARCHAR(255)
t.string :phone, limit: 20 # VARCHAR(20)
# Use text for longer, variable content
t.text :bio # TEXT (unlimited)
t.text :description # TEXT
Change Existing Column Length
class IncreaseNameLength < ActiveRecord::Migration[7.1]
def up
change_column :users, :name, :string, limit: 500
end
def down
change_column :users, :name, :string, limit: 255
end
end
Database Differences
- PostgreSQL: VARCHAR(n) and TEXT perform identically
- MySQL: VARCHAR max is 65,535 bytes (varies with encoding)
- SQLite: Limit is advisory only (not enforced)
How do I add a boolean column?
Boolean columns store true/false values. Always set a default to avoid NULL values, which can cause unexpected behavior.
Basic Boolean Column
class AddAdminToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :admin, :boolean, default: false, null: false
end
end
Multiple Boolean Flags
class AddFlagsToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :active, :boolean, default: true, null: false
add_column :users, :verified, :boolean, default: false, null: false
add_column :users, :subscribed, :boolean, default: false, null: false
end
end
Why Use default: false, null: false?
# Without defaults, you get three states: true, false, nil
user.admin? # Could be true, false, or nil (confusing!)
# With default: false, null: false
user.admin? # Always true or false, never nil
Add Boolean to Existing Table with Data
class AddVerifiedToUsers < ActiveRecord::Migration[7.1]
def change
# Add column with default (applies to new AND existing rows in PostgreSQL 11+)
add_column :users, :verified, :boolean, default: false, null: false
end
end
# For older PostgreSQL or MySQL, you might need:
class AddVerifiedToUsers < ActiveRecord::Migration[7.1]
def up
add_column :users, :verified, :boolean
User.update_all(verified: false)
change_column_null :users, :verified, false
change_column_default :users, :verified, false
end
def down
remove_column :users, :verified
end
end
Database Representation
- PostgreSQL: Native BOOLEAN type
- MySQL: TINYINT(1) - 0 or 1
- SQLite: INTEGER - 0 or 1
How do I add a decimal column with precision?
Use decimal for precise numeric values like money, measurements, or
percentages. Unlike float, decimal avoids floating-point rounding errors.
Understanding Precision and Scale
# precision: total number of digits
# scale: number of digits after decimal point
# DECIMAL(10, 2) can store: -99999999.99 to 99999999.99
t.decimal :price, precision: 10, scale: 2
Common Use Cases
class CreateProducts < ActiveRecord::Migration[7.1]
def change
create_table :products do |t|
# Money (8 digits, 2 decimal places) - up to 999,999.99
t.decimal :price, precision: 8, scale: 2, null: false
# Percentage (5 digits, 2 decimal) - up to 100.00 or 999.99
t.decimal :discount_rate, precision: 5, scale: 2, default: 0
# Weight in kg (10 digits, 4 decimal) - high precision
t.decimal :weight, precision: 10, scale: 4
# Geographic coordinates
t.decimal :latitude, precision: 10, scale: 6
t.decimal :longitude, precision: 10, scale: 6
t.timestamps
end
end
end
Why Not Float?
# Float has rounding errors
0.1 + 0.2 # => 0.30000000000000004
# Decimal is exact
BigDecimal("0.1") + BigDecimal("0.2") # => 0.3
Common Error: Precision Exceeded
PG::NumericValueOutOfRange: ERROR: numeric field overflow
DETAIL: A field with precision 8, scale 2 must round to an absolute value less than 10^6.
Fix: Increase precision or check your data:
change_column :products, :price, :decimal, precision: 12, scale: 2
How do I add a datetime column?
DateTime columns store both date and time. Rails provides datetime,
timestamp, date, and time types.
DateTime vs Timestamp
# In Rails, these are typically equivalent:
t.datetime :published_at
t.timestamp :published_at
# timestamps helper adds both created_at and updated_at
t.timestamps
DateTime with Precision (Rails 7+)
# Microsecond precision (default in Rails 7+)
t.datetime :occurred_at, precision: 6
# Second precision only
t.datetime :occurred_at, precision: 0
# Millisecond precision
t.datetime :occurred_at, precision: 3
Different Time Types
create_table :events do |t|
t.date :event_date # Just the date: 2026-01-15
t.time :start_time # Just the time: 14:30:00
t.datetime :starts_at # Both: 2026-01-15 14:30:00
t.timestamps
end
With Default Value
# Default to current time (database function)
t.datetime :published_at, default: -> { 'CURRENT_TIMESTAMP' }
# Or in PostgreSQL
t.datetime :published_at, default: -> { 'NOW()' }
Common Issue: Timezone Handling
# In config/application.rb, set your timezone:
config.time_zone = 'Eastern Time (US & Canada)'
# Store times in UTC (recommended)
config.active_record.default_timezone = :utc
Migration Compatibility Note
Rails 7 changed the default datetime precision from nil to 6 (microseconds). Old migrations might behave differently:
# Rails 6 migration creates datetime without precision
class OldMigration < ActiveRecord::Migration[6.1]
# ...
end
# Rails 7 migration creates datetime with precision: 6
class NewMigration < ActiveRecord::Migration[7.1]
# ...
end
How do I use long integer (bigint) fields?
Bigint columns store large integers (8 bytes vs 4 bytes for integer). Rails 5.1+ uses bigint for primary keys by default.
When to Use Bigint
- Primary keys (default in Rails 5.1+)
- Foreign keys to bigint primary keys
- Counters that might exceed 2 billion
- External IDs from other systems
- Unix timestamps in milliseconds
Integer vs Bigint Range
# Integer (4 bytes): -2,147,483,648 to 2,147,483,647
# Bigint (8 bytes): -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
t.integer :small_count # INTEGER
t.bigint :large_count # BIGINT
# Or use limit
t.integer :count, limit: 8 # BIGINT
Create Table with Bigint Primary Key
# Rails 5.1+ does this by default
create_table :users do |t|
t.string :name
t.timestamps
end
# Explicitly specify
create_table :users, id: :bigint do |t|
t.string :name
t.timestamps
end
Foreign Keys to Bigint
# Reference column must match primary key type
create_table :posts do |t|
t.references :user, type: :bigint, foreign_key: true
# or
t.bigint :user_id
t.string :title
t.timestamps
end
Common Error: Type Mismatch
ActiveRecord::MismatchedForeignKey: Column `user_id` on table `posts`
does not match column `id` on `users`, which has type `bigint(8)`.
Change the type of the `user_id` column to `bigint`.
Fix: Ensure foreign key columns match the primary key type:
# Change existing column
change_column :posts, :user_id, :bigint
# Or when creating reference
t.references :user, type: :bigint, foreign_key: true
How do I add a JSON/JSONB column?
JSON columns store structured data without a fixed schema. PostgreSQL's
jsonb is preferred for performance as it stores data in binary format.
PostgreSQL JSONB (Recommended)
class AddMetadataToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :settings, :jsonb, default: {}
add_column :users, :preferences, :jsonb, default: {}
# Add index for faster queries
add_index :users, :settings, using: :gin
end
end
MySQL JSON
class AddMetadataToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :settings, :json
end
end
In create_table
create_table :products do |t|
t.string :name
t.jsonb :specifications, default: {}
t.jsonb :variants, default: []
t.timestamps
end
Querying JSON Data
# In your model
class User < ApplicationRecord
# Query JSON fields
scope :with_dark_mode, -> { where("settings->>'theme' = ?", 'dark') }
scope :newsletter_subscribers, -> { where("settings->>'newsletter' = ?", 'true') }
end
# PostgreSQL jsonb operators
User.where("settings @> ?", { theme: 'dark' }.to_json)
User.where("settings ? :key", key: 'newsletter')
JSON vs JSONB
- json: Stores exact text, preserves whitespace and key order
- jsonb: Binary format, faster queries, supports indexing, no duplicate keys
Common Error: Invalid JSON Default
PG::InvalidTextRepresentation: ERROR: invalid input syntax for type json
Fix: Use Ruby hash/array for defaults, not JSON strings:
# Correct
t.jsonb :settings, default: {}
t.jsonb :tags, default: []
# Wrong
t.jsonb :settings, default: '{}'
How do I add an array column (PostgreSQL)?
PostgreSQL supports native array columns, allowing you to store multiple values in a single field without a separate table.
Create Array Column
class AddTagsToArticles < ActiveRecord::Migration[7.1]
def change
add_column :articles, :tags, :string, array: true, default: []
add_column :articles, :scores, :integer, array: true, default: []
add_column :articles, :prices, :decimal, array: true, default: [], precision: 10, scale: 2
end
end
In create_table
create_table :products do |t|
t.string :name
t.string :categories, array: true, default: []
t.string :sizes, array: true, default: []
t.integer :related_ids, array: true, default: []
t.timestamps
end
# Add GIN index for array queries
add_index :products, :categories, using: :gin
Querying Arrays
# Find records containing a value
Product.where("'electronics' = ANY(categories)")
# Find records containing all values
Product.where("categories @> ARRAY[?]::varchar[]", ['electronics', 'gadgets'])
# Find records containing any of values
Product.where("categories && ARRAY[?]::varchar[]", ['electronics', 'clothing'])
Model Usage
class Product < ApplicationRecord
# Works like a regular Ruby array
end
product = Product.new(tags: ['ruby', 'rails', 'web'])
product.tags << 'api'
product.save
Important Note
Array columns are PostgreSQL-specific. For MySQL or SQLite, use a JSON column or a separate join table instead.
How do I add an enum column?
There are two approaches: use integer columns with Rails enums (database-agnostic) or use native database enum types (PostgreSQL).
Option 1: Integer Column with Rails Enum (Recommended)
# Migration
class AddStatusToOrders < ActiveRecord::Migration[7.1]
def change
add_column :orders, :status, :integer, default: 0, null: false
end
end
# Model
class Order < ApplicationRecord
enum status: {
pending: 0,
processing: 1,
shipped: 2,
delivered: 3,
cancelled: 4
}
end
# Usage
order.pending? # => true
order.shipped! # updates status to shipped
Order.delivered # scope for delivered orders
Option 2: PostgreSQL Native Enum
class AddStatusToOrders < ActiveRecord::Migration[7.1]
def up
# Create the enum type
execute <<-SQL
CREATE TYPE order_status AS ENUM ('pending', 'processing', 'shipped', 'delivered', 'cancelled');
SQL
add_column :orders, :status, :order_status, default: 'pending', null: false
end
def down
remove_column :orders, :status
execute <<-SQL
DROP TYPE order_status;
SQL
end
end
Rails 7+ create_enum Helper
class AddStatusToOrders < ActiveRecord::Migration[7.1]
def change
create_enum :order_status, ['pending', 'processing', 'shipped', 'delivered', 'cancelled']
add_column :orders, :status, :enum, enum_type: :order_status, default: 'pending', null: false
end
end
Why Integer is Often Better
- Database-agnostic (works everywhere)
- Easy to add new values
- Better performance (smaller storage)
- No migration needed to add values
Why is my enum migration not reversible?
PostgreSQL enum types require special handling for reversibility because you can't easily drop a type that's in use.
The Error
ActiveRecord::IrreversibleMigration
-- or on rollback --
PG::DependentObjectsStillExist: ERROR: cannot drop type order_status because other objects depend on it
Fix: Use up/down Methods
class AddStatusToOrders < ActiveRecord::Migration[7.1]
def up
create_enum :order_status, ['pending', 'processing', 'shipped']
add_column :orders, :status, :enum, enum_type: :order_status, default: 'pending'
end
def down
remove_column :orders, :status
execute "DROP TYPE order_status"
end
end
Adding Values to Existing Enum
class AddCancelledToOrderStatus < ActiveRecord::Migration[7.1]
def up
execute <<-SQL
ALTER TYPE order_status ADD VALUE 'cancelled';
SQL
end
def down
# PostgreSQL doesn't support removing enum values!
# You'd need to recreate the type
raise ActiveRecord::IrreversibleMigration
end
end
Alternative: Use Integer Enums
To avoid these complications, use integer columns with Rails enums instead. Adding new values is simply a model change, no migration required:
# No migration needed to add 'refunded'
enum status: {
pending: 0,
processing: 1,
shipped: 2,
delivered: 3,
cancelled: 4,
refunded: 5 # Just add to the hash
}
How do I set a default value for a column?
Default values are set automatically when creating new records without explicitly providing a value for that column.
When Creating Column
class AddFieldsToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :role, :string, default: 'member'
add_column :users, :active, :boolean, default: true
add_column :users, :credits, :integer, default: 0
add_column :users, :settings, :jsonb, default: {}
end
end
In create_table
create_table :products do |t|
t.string :name
t.string :status, default: 'draft'
t.integer :stock, default: 0
t.decimal :price, default: 0.0, precision: 10, scale: 2
t.boolean :featured, default: false
t.timestamps
end
Add Default to Existing Column
class AddDefaultToStatus < ActiveRecord::Migration[7.1]
def change
change_column_default :orders, :status, from: nil, to: 'pending'
end
end
Default with null: false
# Best practice: combine default with null: false
add_column :users, :locale, :string, default: 'en', null: false
Common Issue: Existing Records
Adding a default doesn't update existing records (they keep NULL or their current value). To update existing records:
class AddDefaultStatus < ActiveRecord::Migration[7.1]
def change
add_column :orders, :status, :string, default: 'pending'
# Update existing records
reversible do |dir|
dir.up do
Order.where(status: nil).update_all(status: 'pending')
end
end
end
end
How do I change a column's default value?
Use change_column_default to modify the default value of an existing column.
Reversible Change (Recommended)
class ChangeDefaultStatus < ActiveRecord::Migration[7.1]
def change
change_column_default :orders, :status, from: 'new', to: 'pending'
end
end
Non-Reversible Change
# If you don't know the previous default
class ChangeDefaultStatus < ActiveRecord::Migration[7.1]
def up
change_column_default :orders, :status, 'pending'
end
def down
change_column_default :orders, :status, 'new'
end
end
Change Multiple Defaults
class UpdateDefaults < ActiveRecord::Migration[7.1]
def change
change_column_default :users, :role, from: 'user', to: 'member'
change_column_default :users, :credits, from: 100, to: 0
change_column_default :users, :active, from: nil, to: true
end
end
Dynamic Default (Database Function)
class ChangeDefaultTimestamp < ActiveRecord::Migration[7.1]
def up
change_column_default :events, :occurred_at, -> { 'CURRENT_TIMESTAMP' }
end
def down
change_column_default :events, :occurred_at, nil
end
end
How do I remove a column's default value?
Set the default to nil to remove it, allowing the column to have
no default value.
Remove Default (Reversible)
class RemoveDefaultFromStatus < ActiveRecord::Migration[7.1]
def change
change_column_default :orders, :status, from: 'pending', to: nil
end
end
Remove Default (Non-Reversible)
class RemoveDefaultFromStatus < ActiveRecord::Migration[7.1]
def up
change_column_default :orders, :status, nil
end
def down
change_column_default :orders, :status, 'pending'
end
end
Using change_column
# This also works but changes more than just the default
class RemoveDefaultFromStatus < ActiveRecord::Migration[7.1]
def up
change_column :orders, :status, :string, default: nil
end
def down
change_column :orders, :status, :string, default: 'pending'
end
end
How do I set a dynamic default value (database function)?
Use a lambda/proc to specify a database function as the default value. This is evaluated by the database, not Ruby.
Current Timestamp
class AddTimestampDefaults < ActiveRecord::Migration[7.1]
def change
add_column :events, :occurred_at, :datetime, default: -> { 'CURRENT_TIMESTAMP' }
# PostgreSQL specific
add_column :events, :created_date, :date, default: -> { 'CURRENT_DATE' }
end
end
UUID Generation
# PostgreSQL with pgcrypto
class AddExternalId < ActiveRecord::Migration[7.1]
def change
enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto')
add_column :orders, :external_id, :uuid, default: -> { 'gen_random_uuid()' }, null: false
end
end
In create_table
create_table :audit_logs, id: :uuid, default: -> { 'gen_random_uuid()' } do |t|
t.string :action
t.jsonb :data
t.datetime :logged_at, default: -> { 'NOW()' }
t.timestamps
end
MySQL Examples
# MySQL current timestamp
add_column :events, :created_at, :datetime, default: -> { 'CURRENT_TIMESTAMP' }
# MySQL UUID (requires MySQL 8.0+)
add_column :orders, :uuid, :string, limit: 36, default: -> { 'UUID()' }
Important Note
The lambda syntax tells Rails to use the expression as-is in SQL, not as a Ruby value. Without the lambda, Rails would try to evaluate it as Ruby code.
Why did change_column_default remove my column options?
This is a common confusion. change_column_default only changes the default
value and shouldn't affect other options. However, change_column can
inadvertently reset options if you don't specify them.
The Problem with change_column
# Original column
add_column :users, :status, :string, limit: 20, null: false, default: 'active'
# This LOSES limit and null constraint!
change_column :users, :status, :string, default: 'pending'
# After: status is string with default 'pending', but limit is 255 and null is allowed
Solution: Use change_column_default
# This only changes the default, preserves everything else
change_column_default :users, :status, from: 'active', to: 'pending'
If You Must Use change_column
# Specify ALL options you want to keep
change_column :users, :status, :string, limit: 20, null: false, default: 'pending'
Check Current Column Definition
# In Rails console
ActiveRecord::Base.connection.columns(:users).find { |c| c.name == 'status' }
# Shows: #<Column name: "status", type: :string, limit: 20, null: false, default: "active">
How do I make a column NOT NULL?
Use change_column_null to add or remove a NOT NULL constraint.
If the column has existing NULL values, you must handle them first.
Add NOT NULL to New Column
add_column :users, :email, :string, null: false
Add NOT NULL to Existing Column
class MakeEmailNotNull < ActiveRecord::Migration[7.1]
def change
change_column_null :users, :email, false
end
end
With Existing NULL Values
class MakeEmailNotNull < ActiveRecord::Migration[7.1]
def change
# Provide a default for existing NULL values
change_column_null :users, :email, false, '[email protected]'
end
end
# Or handle manually
class MakeEmailNotNull < ActiveRecord::Migration[7.1]
def up
# Update NULLs first
User.where(email: nil).update_all(email: '[email protected]')
# Then add constraint
change_column_null :users, :email, false
end
def down
change_column_null :users, :email, true
end
end
Common Error: NULL Values Exist
PG::NotNullViolation: ERROR: column "email" of relation "users" contains null values
-- or --
Mysql2::Error: Invalid use of NULL value
Fix: Update or delete rows with NULL values before adding the constraint:
def up
# Option 1: Set a default value
User.where(email: nil).update_all(email: '[email protected]')
# Option 2: Delete rows with NULL
User.where(email: nil).delete_all
# Then add constraint
change_column_null :users, :email, false
end
Remove NOT NULL Constraint
class AllowNullEmail < ActiveRecord::Migration[7.1]
def change
change_column_null :users, :email, true
end
end
How do I allow NULL on an existing column?
Use change_column_null with true to remove a NOT NULL
constraint and allow NULL values.
Remove NOT NULL Constraint
class AllowNullOnEmail < ActiveRecord::Migration[7.1]
def change
change_column_null :users, :email, true
end
end
Multiple Columns
class AllowNullOnOptionalFields < ActiveRecord::Migration[7.1]
def change
change_column_null :users, :phone, true
change_column_null :users, :bio, true
change_column_null :users, :avatar_url, true
end
end
With Reversibility Consideration
class AllowNullOnEmail < ActiveRecord::Migration[7.1]
def up
change_column_null :users, :email, true
end
def down
# Fill in NULLs before re-adding constraint
User.where(email: nil).find_each do |user|
user.update_column(:email, "user_#{user.id}@placeholder.com")
end
change_column_null :users, :email, false
end
end
How do I add a NOT NULL column to a table with existing data?
Adding a NOT NULL column to a table with existing rows requires providing a default value, either in the column definition or by updating rows first.
Option 1: With Default Value (Simplest)
class AddStatusToOrders < ActiveRecord::Migration[7.1]
def change
# PostgreSQL 11+ applies default to existing rows automatically
add_column :orders, :status, :string, null: false, default: 'pending'
end
end
Option 2: Three-Step Process
class AddStatusToOrders < ActiveRecord::Migration[7.1]
def up
# 1. Add column allowing NULL
add_column :orders, :status, :string
# 2. Backfill existing records
Order.update_all(status: 'pending')
# Or for large tables:
# Order.in_batches.update_all(status: 'pending')
# 3. Add NOT NULL constraint
change_column_null :orders, :status, false
end
def down
remove_column :orders, :status
end
end
Option 3: Using SQL for Performance
class AddStatusToOrders < ActiveRecord::Migration[7.1]
def up
add_column :orders, :status, :string
# Direct SQL is faster for large tables
execute "UPDATE orders SET status = 'pending' WHERE status IS NULL"
change_column_null :orders, :status, false
change_column_default :orders, :status, 'pending'
end
def down
remove_column :orders, :status
end
end
For Very Large Tables
class AddStatusToOrders < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def up
add_column :orders, :status, :string
# Update in batches to avoid locking
Order.in_batches(of: 10_000) do |batch|
batch.update_all(status: 'pending')
end
change_column_null :orders, :status, false
end
def down
remove_column :orders, :status
end
end
How do I add an index?
Indexes improve query performance by allowing the database to find rows faster.
Use add_index to create an index on one or more columns.
Basic Index
class AddIndexToUsersEmail < ActiveRecord::Migration[7.1]
def change
add_index :users, :email
end
end
When Adding a Column
class AddEmailToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :email, :string
add_index :users, :email
end
end
# Or combine with generator
# rails g migration AddEmailToUsers email:string:index
Using References (Automatically Adds Index)
class AddUserToPosts < ActiveRecord::Migration[7.1]
def change
add_reference :posts, :user, foreign_key: true
# Automatically creates index on user_id
end
end
In create_table
create_table :users do |t|
t.string :email, index: true
t.string :username, index: { unique: true }
t.references :organization, index: true, foreign_key: true
t.timestamps
end
Common Error: Index Already Exists
PG::DuplicateTable: ERROR: relation "index_users_on_email" already exists
Fix: Check if index exists first:
add_index :users, :email unless index_exists?(:users, :email)
How do I add a unique index?
Unique indexes enforce uniqueness at the database level, preventing duplicate values even if application validations fail.
Basic Unique Index
class AddUniqueIndexToUsersEmail < ActiveRecord::Migration[7.1]
def change
add_index :users, :email, unique: true
end
end
Unique Index on Multiple Columns
# Ensure combination is unique
class AddUniqueIndexToMemberships < ActiveRecord::Migration[7.1]
def change
add_index :memberships, [:user_id, :organization_id], unique: true
end
end
Unique Index with Conditions (Partial)
# Only enforce uniqueness for active records
class AddUniqueIndexToActiveUsers < ActiveRecord::Migration[7.1]
def change
add_index :users, :email, unique: true, where: "deleted_at IS NULL"
end
end
Common Error: Duplicate Values Exist
PG::UniqueViolation: ERROR: could not create unique index "index_users_on_email"
DETAIL: Key (email)=([email protected]) is duplicated.
Fix: Remove duplicates before adding the unique index:
class AddUniqueIndexToUsersEmail < ActiveRecord::Migration[7.1]
def up
# Find and handle duplicates first
duplicates = User.group(:email).having('COUNT(*) > 1').pluck(:email)
duplicates.each do |email|
users = User.where(email: email).order(:created_at)
users.offset(1).destroy_all # Keep the first, remove others
end
add_index :users, :email, unique: true
end
def down
remove_index :users, :email
end
end
How do I add a composite index (multiple columns)?
Composite indexes (multi-column indexes) improve queries that filter or sort by multiple columns together.
Basic Composite Index
class AddIndexToOrdersStatusDate < ActiveRecord::Migration[7.1]
def change
add_index :orders, [:status, :created_at]
end
end
Column Order Matters
# This index helps queries that:
# - Filter by status alone
# - Filter by status AND user_id
# But NOT queries that only filter by user_id
add_index :orders, [:status, :user_id]
# For queries filtering only by user_id, add a separate index
add_index :orders, :user_id
Common Composite Index Patterns
class AddIndexesToOrders < ActiveRecord::Migration[7.1]
def change
# For: Order.where(user_id: x).order(created_at: :desc)
add_index :orders, [:user_id, :created_at]
# For: Order.where(status: x, user_id: y)
add_index :orders, [:status, :user_id]
# For polymorphic associations
add_index :comments, [:commentable_type, :commentable_id]
end
end
Unique Composite Index
# User can only have one role per organization
add_index :memberships, [:user_id, :organization_id, :role], unique: true
# Or simpler: user can only be in org once
add_index :memberships, [:user_id, :organization_id], unique: true
With Custom Name
# If auto-generated name is too long
add_index :orders, [:user_id, :status, :created_at],
name: 'idx_orders_user_status_date'
How do I add a partial index?
Partial indexes only include rows that match a condition. They're smaller and faster for queries that match the same condition.
Basic Partial Index
class AddPartialIndexToOrders < ActiveRecord::Migration[7.1]
def change
# Only index non-archived orders
add_index :orders, :status, where: "archived = false"
end
end
Unique Partial Index (Soft Delete)
# Enforce unique email only for non-deleted users
class AddUniqueEmailIndex < ActiveRecord::Migration[7.1]
def change
add_index :users, :email, unique: true, where: "deleted_at IS NULL"
end
end
Index Only Active Records
class AddPartialIndexes < ActiveRecord::Migration[7.1]
def change
# Index only active users
add_index :users, :email, where: "active = true", name: 'idx_active_users_email'
# Index only pending orders
add_index :orders, :created_at, where: "status = 'pending'", name: 'idx_pending_orders'
# Index only recent records
add_index :logs, :action, where: "created_at > '2025-01-01'", name: 'idx_recent_logs'
end
end
PostgreSQL Specific Conditions
# JSONB conditions
add_index :users, :email, where: "settings->>'notifications' = 'true'"
# NULL checks
add_index :orders, :shipped_at, where: "shipped_at IS NOT NULL"
add_index :orders, :user_id, where: "cancelled_at IS NULL"
Benefits of Partial Indexes
- Smaller index size
- Faster index updates
- Better query performance for matching queries
- Can enforce partial uniqueness
How do I add an index with a custom name?
Rails auto-generates index names, but they can exceed database limits
(63 characters for PostgreSQL). Use the name option for custom names.
Why Custom Names?
- Auto-generated names can be too long
- More readable names for debugging
- Match existing index names when adopting legacy schemas
Custom Index Name
class AddIndexToOrders < ActiveRecord::Migration[7.1]
def change
add_index :orders, [:user_id, :status, :created_at, :updated_at],
name: 'idx_orders_user_status_timestamps'
end
end
Common Error: Name Too Long
ArgumentError: Index name 'index_user_organization_memberships_on_user_id_and_organization_id_and_role_id' is too long; the limit is 63 characters
Fix: Use a shorter custom name:
add_index :user_organization_memberships,
[:user_id, :organization_id, :role_id],
name: 'idx_user_org_memberships_composite'
Removing Index by Name
# When removing, reference by name
remove_index :orders, name: 'idx_orders_user_status_timestamps'
Check Index Name
# In Rails console
ActiveRecord::Base.connection.indexes(:orders).map(&:name)
How do I remove an index?
Use remove_index to delete an index. You can reference it by
column(s) or by name.
Remove by Column
class RemoveIndexFromUsersEmail < ActiveRecord::Migration[7.1]
def change
remove_index :users, :email
end
end
Remove Composite Index
class RemoveIndexFromOrders < ActiveRecord::Migration[7.1]
def change
remove_index :orders, [:user_id, :status]
end
end
Remove by Name
class RemoveIndexFromOrders < ActiveRecord::Migration[7.1]
def change
remove_index :orders, name: 'idx_orders_user_status'
end
end
Making Removal Reversible
# For simple indexes, Rails can reverse automatically
remove_index :users, :email # Reversible
# For indexes with options, specify them for reversibility
remove_index :users, :email, unique: true, where: "active = true"
Check If Index Exists First
class RemoveIndexIfExists < ActiveRecord::Migration[7.1]
def change
remove_index :users, :email if index_exists?(:users, :email)
end
end
Common Error: Index Not Found
ArgumentError: Index name 'index_users_on_email' on table 'users' does not exist
Fix: Check the actual index name in your database or schema.rb:
# In Rails console
ActiveRecord::Base.connection.indexes(:users)
How do I check if an index exists?
Use index_exists? to check for an index before adding or removing it,
making your migrations idempotent.
Check by Column
class AddIndexSafely < ActiveRecord::Migration[7.1]
def change
add_index :users, :email unless index_exists?(:users, :email)
end
end
Check by Name
class AddIndexSafely < ActiveRecord::Migration[7.1]
def change
unless index_exists?(:users, name: 'index_users_on_email')
add_index :users, :email
end
end
end
Check Composite Index
if index_exists?(:orders, [:user_id, :status])
# Index exists
end
Check Unique Index
# Check if a unique index exists on email
index_exists?(:users, :email, unique: true)
List All Indexes
# In Rails console or migration
ActiveRecord::Base.connection.indexes(:users).each do |index|
puts "#{index.name}: #{index.columns.join(', ')} (unique: #{index.unique})"
end
Safe Add/Remove Pattern
class ManageIndexes < ActiveRecord::Migration[7.1]
def change
# Safe add
add_index :users, :email unless index_exists?(:users, :email)
# Safe remove
remove_index :users, :legacy_field if index_exists?(:users, :legacy_field)
end
end
Why can't I rollback my PostgreSQL expression index?
Expression indexes (indexes on functions or expressions) can't be automatically reversed because Rails doesn't store the expression used to create them.
The Error
ActiveRecord::IrreversibleMigration:
This migration uses remove_index to remove an expression index, but
add_index was not given an expression.
Problem: Expression Index
# These are not automatically reversible
add_index :users, 'LOWER(email)', name: 'index_users_on_lower_email'
add_index :users, "(data->>'name')", name: 'index_users_on_data_name'
Fix: Use up/down Methods
class AddLowerEmailIndex < ActiveRecord::Migration[7.1]
def up
execute <<-SQL
CREATE INDEX index_users_on_lower_email ON users (LOWER(email));
SQL
end
def down
execute <<-SQL
DROP INDEX index_users_on_lower_email;
SQL
end
end
Alternative: Use reversible
class AddLowerEmailIndex < ActiveRecord::Migration[7.1]
def change
reversible do |dir|
dir.up do
execute "CREATE INDEX index_users_on_lower_email ON users (LOWER(email))"
end
dir.down do
execute "DROP INDEX index_users_on_lower_email"
end
end
end
end
Common Expression Indexes
# Case-insensitive search
execute "CREATE INDEX idx_users_lower_email ON users (LOWER(email))"
# JSONB field
execute "CREATE INDEX idx_users_settings_theme ON users ((settings->>'theme'))"
# Date extraction
execute "CREATE INDEX idx_orders_year ON orders (EXTRACT(YEAR FROM created_at))"
# GIN index for full-text search
execute "CREATE INDEX idx_posts_search ON posts USING GIN (to_tsvector('english', title || ' ' || body))"
Why is my index name being truncated?
Databases have limits on identifier names (PostgreSQL: 63 characters, MySQL: 64). Rails auto-generates index names that may exceed these limits.
The Error
Index name 'index_user_organization_role_permissions_on_user_id_and_organization_id_and_role_id'
on table 'user_organization_role_permissions' is too long; the limit is 63 characters
Fix: Use Custom Name
class AddIndexToPermissions < ActiveRecord::Migration[7.1]
def change
add_index :user_organization_role_permissions,
[:user_id, :organization_id, :role_id],
name: 'idx_user_org_role_perms'
end
end
Common Long Name Scenarios
# Polymorphic association - auto name too long
add_index :comments, [:commentable_type, :commentable_id],
name: 'idx_comments_commentable'
# Join table with long names
add_index :user_organization_memberships, [:user_id, :organization_id],
name: 'idx_user_org_memberships', unique: true
# Multiple columns
add_index :audit_logs, [:auditable_type, :auditable_id, :action, :created_at],
name: 'idx_audit_logs_composite'
Rails 7.1+ Index Name Changes
Rails 7.1 changed how index names are generated for rename_table.
Older migrations may have different index names than newer ones:
# In migration version 7.0 and earlier
rename_table :users, :members
# Index names stay as index_users_on_*
# In migration version 7.1+
rename_table :users, :members
# Index names become index_members_on_*
How do I add a reference/belongs_to column?
Use add_reference (or add_belongs_to) to add a foreign key
column with an index. This is the preferred way to create associations.
Basic Reference
class AddUserToPosts < ActiveRecord::Migration[7.1]
def change
add_reference :posts, :user, foreign_key: true
end
end
# Creates:
# - user_id column (bigint)
# - index on user_id
# - foreign key constraint to users table
Using Generator
# These are equivalent
rails g migration AddUserToPosts user:references
rails g migration AddUserToPosts user:belongs_to
Reference Options
add_reference :posts, :user,
foreign_key: true, # Add FK constraint
null: false, # NOT NULL
index: true, # Add index (default)
type: :uuid # Match UUID primary key
Without Foreign Key
# Just add the column and index, no FK constraint
add_reference :posts, :user, foreign_key: false
# Or without index
add_reference :posts, :user, index: false
Custom Column Name
# Creates author_id instead of user_id
add_reference :posts, :author, foreign_key: { to_table: :users }
In create_table
create_table :posts do |t|
t.references :user, foreign_key: true
# or
t.belongs_to :user, foreign_key: true
t.string :title
t.timestamps
end
How do I add a polymorphic reference?
Polymorphic associations allow a model to belong to multiple other models using a single association. This requires two columns: one for the ID and one for the type.
Add Polymorphic Reference
class AddCommentableToComments < ActiveRecord::Migration[7.1]
def change
add_reference :comments, :commentable, polymorphic: true
end
end
# Creates:
# - commentable_id (bigint)
# - commentable_type (string)
# - composite index on [commentable_type, commentable_id]
In create_table
create_table :comments do |t|
t.references :commentable, polymorphic: true, null: false
t.text :body
t.timestamps
end
Model Setup
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
class Post < ApplicationRecord
has_many :comments, as: :commentable
end
class Photo < ApplicationRecord
has_many :comments, as: :commentable
end
Polymorphic with NOT NULL
add_reference :comments, :commentable, polymorphic: true, null: false
# Or in create_table
t.references :commentable, polymorphic: true, null: false
Note on Foreign Keys
PostgreSQL doesn't support foreign keys on polymorphic associations because
the reference can point to multiple tables. The foreign_key: true
option is ignored for polymorphic references.
How do I add a foreign key constraint?
Foreign key constraints ensure referential integrity at the database level, preventing orphaned records.
Add Foreign Key to Existing Column
class AddForeignKeyToPosts < ActiveRecord::Migration[7.1]
def change
add_foreign_key :posts, :users
end
end
With Custom Column Name
# posts.author_id references users.id
add_foreign_key :posts, :users, column: :author_id
With Custom Primary Key
# posts.user_uuid references users.uuid
add_foreign_key :posts, :users, column: :user_uuid, primary_key: :uuid
On Delete/Update Actions
# Delete posts when user is deleted
add_foreign_key :posts, :users, on_delete: :cascade
# Set to NULL when user is deleted
add_foreign_key :posts, :users, on_delete: :nullify
# Prevent deletion if posts exist
add_foreign_key :posts, :users, on_delete: :restrict
# Options: :cascade, :nullify, :restrict, :no_action
With Reference Helper
# Combined column + index + foreign key
add_reference :posts, :user, foreign_key: true
# With on_delete
add_reference :posts, :user, foreign_key: { on_delete: :cascade }
Validate Later (Large Tables)
# Add constraint without validating existing data
add_foreign_key :posts, :users, validate: false
# Later, validate in a separate migration
validate_foreign_key :posts, :users
How do I remove a foreign key constraint?
Use remove_foreign_key to drop a foreign key constraint
while keeping the column.
Remove by Table
class RemoveForeignKeyFromPosts < ActiveRecord::Migration[7.1]
def change
remove_foreign_key :posts, :users
end
end
Remove by Column
# When column name doesn't match convention
remove_foreign_key :posts, column: :author_id
Remove by Name
# When you know the constraint name
remove_foreign_key :posts, name: 'fk_rails_5b5ddfd518'
Making Removal Reversible
class RemoveForeignKey < ActiveRecord::Migration[7.1]
def change
# Provide to_table for reversibility
remove_foreign_key :posts, :users, column: :author_id
end
end
# Or with full options
class RemoveForeignKey < ActiveRecord::Migration[7.1]
def up
remove_foreign_key :posts, :users
end
def down
add_foreign_key :posts, :users, on_delete: :cascade
end
end
Find Foreign Key Name
# In Rails console
ActiveRecord::Base.connection.foreign_keys(:posts)
# Returns array of ForeignKeyDefinition objects
How do I add a self-referential foreign key?
Self-referential associations link a table to itself, common for hierarchies like categories, comments (replies), or employee managers.
Parent-Child Hierarchy
class AddParentToCategories < ActiveRecord::Migration[7.1]
def change
add_reference :categories, :parent,
foreign_key: { to_table: :categories },
null: true # Root categories have no parent
end
end
Manager-Employee Relationship
class AddManagerToEmployees < ActiveRecord::Migration[7.1]
def change
add_reference :employees, :manager,
foreign_key: { to_table: :employees },
null: true
end
end
# Model
class Employee < ApplicationRecord
belongs_to :manager, class_name: 'Employee', optional: true
has_many :subordinates, class_name: 'Employee', foreign_key: :manager_id
end
Reply to Comment
class AddParentToComments < ActiveRecord::Migration[7.1]
def change
add_reference :comments, :parent,
foreign_key: { to_table: :comments, on_delete: :cascade }
end
end
# Model
class Comment < ApplicationRecord
belongs_to :parent, class_name: 'Comment', optional: true
has_many :replies, class_name: 'Comment', foreign_key: :parent_id
end
In create_table
create_table :categories do |t|
t.string :name
t.references :parent, foreign_key: { to_table: :categories }
t.timestamps
end
How do I add a composite foreign key?
Composite foreign keys reference multiple columns in the parent table. Rails 7.1+ has built-in support for composite foreign keys.
Rails 7.1+ Composite Foreign Key
class AddCompositeForeignKey < ActiveRecord::Migration[7.1]
def change
add_foreign_key :bookings, :travel_routes,
column: [:origin, :destination],
primary_key: [:origin, :destination]
end
end
With create_table
create_table :bookings do |t|
t.string :origin, null: false
t.string :destination, null: false
t.datetime :departure_time
t.timestamps
end
add_foreign_key :bookings, :travel_routes,
column: [:origin, :destination],
primary_key: [:origin, :destination]
Pre-Rails 7.1: Use SQL
class AddCompositeForeignKey < ActiveRecord::Migration[7.0]
def up
execute <<-SQL
ALTER TABLE bookings
ADD CONSTRAINT fk_bookings_routes
FOREIGN KEY (origin, destination)
REFERENCES travel_routes (origin, destination);
SQL
end
def down
execute <<-SQL
ALTER TABLE bookings
DROP CONSTRAINT fk_bookings_routes;
SQL
end
end
Model Association
class Booking < ApplicationRecord
belongs_to :travel_route,
foreign_key: [:origin, :destination],
primary_key: [:origin, :destination]
end
Why is my reference column integer instead of bigint?
This is a migration version compatibility issue. Rails 5.1+ uses bigint for primary keys by default, but older migration versions still use integer for references.
The Problem
# In a Rails 6.0 migration
class AddUserToPosts < ActiveRecord::Migration[6.0]
def change
add_reference :posts, :user, foreign_key: true
# Creates user_id as INTEGER, not BIGINT!
end
end
The Error
ActiveRecord::MismatchedForeignKey: Column `user_id` on table `posts`
does not match column `id` on `users`, which has type `bigint`.
Fix: Specify Type Explicitly
class AddUserToPosts < ActiveRecord::Migration[6.0]
def change
add_reference :posts, :user, type: :bigint, foreign_key: true
end
end
Fix Existing Column
class FixUserIdType < ActiveRecord::Migration[7.1]
def up
# Remove FK first
remove_foreign_key :posts, :users if foreign_key_exists?(:posts, :users)
# Change column type
change_column :posts, :user_id, :bigint
# Re-add FK
add_foreign_key :posts, :users
end
def down
remove_foreign_key :posts, :users
change_column :posts, :user_id, :integer
add_foreign_key :posts, :users
end
end
Best Practice
Always use the latest migration version to get sensible defaults, or explicitly
specify type: :bigint for references.
Why does my polymorphic reference have the wrong index name?
This is a known issue with migration version compatibility. Different Rails versions generate different index names for polymorphic associations.
The Issue
# In older migrations
class AddCommentable < ActiveRecord::Migration[6.0]
def change
add_reference :comments, :commentable, polymorphic: true
end
end
# Might create: index_comments_on_commentable_type_and_commentable_id
# Or: index_comments_on_commentable
Index Name Varies by Version
- Rails 6.0:
index_comments_on_commentable_type_and_commentable_id - Rails 7.0+:
index_comments_on_commentable
Fix: Specify Index Name
class AddCommentable < ActiveRecord::Migration[7.1]
def change
add_reference :comments, :commentable, polymorphic: true,
index: { name: 'idx_comments_on_commentable' }
end
end
Fix Existing Index
class RenamePolymorphicIndex < ActiveRecord::Migration[7.1]
def change
rename_index :comments,
'index_comments_on_commentable_type_and_commentable_id',
'index_comments_on_commentable'
end
end
Check Current Index Name
# In Rails console
ActiveRecord::Base.connection.indexes(:comments).map(&:name)
What is the change method and when should I use it?
The change method is the modern way to write reversible migrations.
Rails automatically figures out how to reverse most operations.
Basic change Method
class AddEmailToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :email, :string
add_index :users, :email, unique: true
end
end
# Rails knows to:
# - UP: add_column, add_index
# - DOWN: remove_index, remove_column
Reversible Operations in change
# These are automatically reversible:
add_column / remove_column (with type)
add_index / remove_index
add_reference / remove_reference
add_timestamps / remove_timestamps
add_foreign_key / remove_foreign_key
create_table / drop_table (with block)
create_join_table / drop_join_table
rename_column
rename_table
rename_index
change_column_default (with from/to)
change_column_null
When change Doesn't Work
# These need up/down:
change_column # Can't know previous type
remove_column without type
execute (raw SQL)
Custom reversibility logic
Using reversible Inside change
class UpdateData < ActiveRecord::Migration[7.1]
def change
add_column :users, :status, :string, default: 'active'
reversible do |dir|
dir.up do
User.update_all(status: 'active')
end
dir.down do
# Nothing needed for down
end
end
end
end
When to Use up/down Instead
class ChangeColumnType < ActiveRecord::Migration[7.1]
# change_column is not reversible
def up
change_column :users, :age, :string
end
def down
change_column :users, :age, :integer
end
end
When should I use up and down methods?
Use up and down methods when the migration isn't
automatically reversible, or when you need different logic for each direction.
Operations That Need up/down
# change_column - Rails can't know the previous type
class ChangeColumnType < ActiveRecord::Migration[7.1]
def up
change_column :users, :phone, :string, limit: 20
end
def down
change_column :users, :phone, :string, limit: 255
end
end
Data Transformations
class SplitName < ActiveRecord::Migration[7.1]
def up
add_column :users, :first_name, :string
add_column :users, :last_name, :string
User.find_each do |user|
parts = user.name.to_s.split(' ', 2)
user.update_columns(first_name: parts[0], last_name: parts[1])
end
remove_column :users, :name
end
def down
add_column :users, :name, :string
User.find_each do |user|
user.update_column(:name, "#{user.first_name} #{user.last_name}".strip)
end
remove_column :users, :first_name
remove_column :users, :last_name
end
end
Raw SQL
class CreateView < ActiveRecord::Migration[7.1]
def up
execute <<-SQL
CREATE VIEW active_users AS
SELECT * FROM users WHERE active = true;
SQL
end
def down
execute "DROP VIEW active_users;"
end
end
Different Index Strategies
class ChangeIndex < ActiveRecord::Migration[7.1]
def up
remove_index :orders, :user_id
add_index :orders, [:user_id, :created_at]
end
def down
remove_index :orders, [:user_id, :created_at]
add_index :orders, :user_id
end
end
How do I make a non-reversible migration reversible with reversible?
The reversible helper lets you define custom up/down behavior
within a change method.
Basic reversible Usage
class AddStatusWithData < ActiveRecord::Migration[7.1]
def change
add_column :orders, :status, :string
reversible do |dir|
dir.up do
execute "UPDATE orders SET status = 'pending' WHERE status IS NULL"
end
# dir.down is optional - nothing to undo here
end
change_column_null :orders, :status, false
end
end
Custom Logic Both Directions
class ChangeColumnWithCast < ActiveRecord::Migration[7.1]
def change
reversible do |dir|
dir.up do
change_column :orders, :amount, :decimal, precision: 10, scale: 2
end
dir.down do
change_column :orders, :amount, :integer
end
end
end
end
Raw SQL in Both Directions
class CreateFunction < ActiveRecord::Migration[7.1]
def change
reversible do |dir|
dir.up do
execute <<-SQL
CREATE FUNCTION update_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
SQL
end
dir.down do
execute "DROP FUNCTION update_timestamp();"
end
end
end
end
Combining with Regular change Operations
class ComplexMigration < ActiveRecord::Migration[7.1]
def change
# This is automatically reversible
add_column :users, :settings, :jsonb, default: {}
# This needs custom handling
reversible do |dir|
dir.up do
User.where(legacy_settings: nil).update_all(settings: {})
end
end
# This is also automatically reversible
remove_column :users, :legacy_settings, :text
end
end
How do I prevent a migration from being reverted?
Use raise ActiveRecord::IrreversibleMigration in the down method
to explicitly prevent rollback of destructive or one-way migrations.
Raise IrreversibleMigration
class DropLegacyData < ActiveRecord::Migration[7.1]
def up
drop_table :legacy_users
drop_table :legacy_orders
end
def down
raise ActiveRecord::IrreversibleMigration, "Cannot restore dropped legacy tables"
end
end
In change Method
class DestructiveMigration < ActiveRecord::Migration[7.1]
def change
drop_table :temporary_data
reversible do |dir|
dir.down do
raise ActiveRecord::IrreversibleMigration
end
end
end
end
With Helpful Message
class TruncateAuditLogs < ActiveRecord::Migration[7.1]
def up
execute "TRUNCATE TABLE audit_logs"
end
def down
raise ActiveRecord::IrreversibleMigration, <<~MSG
Cannot reverse truncation of audit_logs.
Data has been permanently deleted.
If you need to rollback, restore from a backup.
MSG
end
end
Common Irreversible Operations
- Dropping tables without schema definition
- Deleting or truncating data
- Lossy type conversions (text → integer)
- Removing columns that had unique data
How do I revert a previous migration?
Use the revert method to undo the changes from a previous migration
class, useful when you need to undo and redo changes differently.
Revert Entire Migration
class RevertAddEmailIndex < ActiveRecord::Migration[7.1]
def change
revert AddEmailToUsers
end
end
Revert Specific Operations
class PartialRevert < ActiveRecord::Migration[7.1]
def change
revert do
# Only revert these specific operations
add_column :users, :temporary_field, :string
add_index :users, :temporary_field
end
end
end
Revert and Replace
class ReplaceIndexStrategy < ActiveRecord::Migration[7.1]
def change
# Undo the old index
revert AddUserIndex
# Add new improved index
add_index :users, [:email, :created_at], name: 'idx_users_email_created'
end
end
Important: Migration Must Be Reversible
The migration you're reverting must use reversible operations. If it uses
up/down, the down method will be executed.
# This works because AddEmailToUsers uses `change`
revert AddEmailToUsers
# For up/down migrations, `revert` runs the `down` method
How do I use down_only?
The down_only helper (proposed for Rails 8) lets you specify
code that only runs during rollback. Until then, use reversible.
Current Approach with reversible
class AddColumnWithBackfill < ActiveRecord::Migration[7.1]
def change
add_column :users, :full_name, :string
reversible do |dir|
dir.up do
User.update_all("full_name = first_name || ' ' || last_name")
end
# Nothing needed for down - column removal handles it
end
end
end
Proposed down_only Syntax (Future Rails)
# This syntax may be available in future Rails versions
class RemoveColumn < ActiveRecord::Migration[8.0]
def change
remove_column :users, :legacy_field, :string
down_only do
# Only runs during rollback
User.update_all(legacy_field: 'default_value')
end
end
end
Current Workaround
class Migration < ActiveRecord::Migration[7.1]
def change
remove_column :users, :old_field, :string
reversible do |dir|
dir.down do
# This runs only during rollback
User.where(old_field: nil).update_all(old_field: '')
end
end
end
end
Helper Method Pattern
# In migration
def down_only(&block)
reversible do |dir|
dir.down(&block)
end
end
How do I update existing records in a migration?
You can use ActiveRecord or raw SQL to update data during migrations. Be cautious with large tables and consider performance implications.
Using ActiveRecord
class BackfillStatus < ActiveRecord::Migration[7.1]
def up
User.where(status: nil).update_all(status: 'active')
end
def down
# Usually nothing needed
end
end
Using Raw SQL (Faster)
class BackfillStatus < ActiveRecord::Migration[7.1]
def up
execute "UPDATE users SET status = 'active' WHERE status IS NULL"
end
def down
# Nothing needed
end
end
Batch Updates (Large Tables)
class BackfillData < ActiveRecord::Migration[7.1]
def up
User.in_batches(of: 10_000) do |batch|
batch.update_all(status: 'active')
end
end
def down
# Nothing needed
end
end
With Progress Output
class BackfillData < ActiveRecord::Migration[7.1]
def up
total = User.where(status: nil).count
say "Updating #{total} users..."
User.where(status: nil).find_each.with_index do |user, index|
user.update_column(:status, 'active')
say_with_time "Progress: #{index + 1}/#{total}" if (index + 1) % 1000 == 0
end
end
def down
# Nothing needed
end
end
Conditional Updates
class SetDefaultRoles < ActiveRecord::Migration[7.1]
def up
execute <<-SQL
UPDATE users
SET role = CASE
WHEN admin = true THEN 'admin'
WHEN moderator = true THEN 'moderator'
ELSE 'member'
END
WHERE role IS NULL
SQL
end
def down
# Nothing needed
end
end
How do I backfill data when adding a NOT NULL column?
Adding a NOT NULL column to a table with existing data requires either a default value or backfilling before adding the constraint.
Option 1: With Default (Simplest)
class AddStatusToOrders < ActiveRecord::Migration[7.1]
def change
# PostgreSQL 11+ applies default to existing rows
add_column :orders, :status, :string, default: 'pending', null: false
end
end
Option 2: Backfill Then Constrain
class AddStatusToOrders < ActiveRecord::Migration[7.1]
def change
# Step 1: Add nullable column
add_column :orders, :status, :string
# Step 2: Backfill existing records
reversible do |dir|
dir.up do
Order.where(status: nil).update_all(status: 'pending')
end
end
# Step 3: Add NOT NULL constraint
change_column_null :orders, :status, false
# Step 4: Add default for new records
change_column_default :orders, :status, 'pending'
end
end
Option 3: For Very Large Tables
class AddStatusToOrders < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def up
# Add column without lock
add_column :orders, :status, :string
# Backfill in batches
Order.in_batches(of: 50_000) do |batch|
batch.where(status: nil).update_all(status: 'pending')
end
# Add constraint with validation
change_column_null :orders, :status, false
change_column_default :orders, :status, 'pending'
end
def down
remove_column :orders, :status
end
end
Using SQL for Speed
class AddStatusToOrders < ActiveRecord::Migration[7.1]
def up
add_column :orders, :status, :string
execute "UPDATE orders SET status = 'pending' WHERE status IS NULL"
change_column_null :orders, :status, false
change_column_default :orders, :status, 'pending'
end
def down
remove_column :orders, :status
end
end
Should I use models in migrations?
Using models in migrations can be risky because the model may change after the migration is written, causing failures when running old migrations.
The Problem
# This migration worked when written...
class BackfillData < ActiveRecord::Migration[7.1]
def up
User.where(status: nil).update_all(status: 'active')
end
end
# But later, if you rename status to state:
# - Old migration still references :status
# - Running migrations on fresh DB fails
Solution 1: Define Migration-Local Model
class BackfillData < ActiveRecord::Migration[7.1]
class User < ApplicationRecord
# Isolated model that won't change
end
def up
User.where(status: nil).update_all(status: 'active')
end
end
Solution 2: Use Raw SQL
class BackfillData < ActiveRecord::Migration[7.1]
def up
execute "UPDATE users SET status = 'active' WHERE status IS NULL"
end
end
Solution 3: Reset Column Information
class AddAndBackfill < ActiveRecord::Migration[7.1]
def up
add_column :users, :full_name, :string
User.reset_column_information # Refresh model's column cache
User.find_each do |user|
user.update_column(:full_name, "#{user.first_name} #{user.last_name}")
end
end
end
Best Practices
- Prefer raw SQL for simple updates
- Define local model classes for complex logic
- Call
reset_column_informationafter schema changes - Avoid callbacks/validations: use
update_column/update_all
How do I safely run data migrations?
Data migrations modify data rather than schema. They require extra care to avoid data loss, long locks, and deployment issues.
Separate Schema and Data Migrations
# Migration 1: Schema change (runs during deploy)
class AddStatusColumn < ActiveRecord::Migration[7.1]
def change
add_column :orders, :status, :string
end
end
# Migration 2: Data backfill (can run separately)
class BackfillStatus < ActiveRecord::Migration[7.1]
def up
Order.in_batches.update_all(status: 'pending')
end
end
# Migration 3: Add constraint (after backfill complete)
class AddStatusConstraint < ActiveRecord::Migration[7.1]
def change
change_column_null :orders, :status, false
end
end
Use Batching for Large Tables
class BackfillSafely < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def up
Order.in_batches(of: 10_000) do |batch|
batch.update_all(status: 'pending')
sleep(0.1) # Give database a breather
end
end
end
Idempotent Migrations
class SafeBackfill < ActiveRecord::Migration[7.1]
def up
# Only update records that need it
Order.where(status: nil).in_batches.update_all(status: 'pending')
end
def down
# Explicitly do nothing or document why
end
end
Consider Using a Data Migration Gem
# Gems like 'data_migrate' separate data migrations
# from schema migrations, allowing independent execution
# Schema migrations: db/migrate/
# Data migrations: db/data/
Production Safety Checklist
- Test on production-like data volume
- Run during low-traffic periods
- Have a rollback plan
- Monitor database load during execution
- Use
disable_ddl_transaction!if needed
How do I execute raw SQL in a migration?
Use execute to run raw SQL statements when Rails' DSL doesn't
support what you need.
Basic execute
class RunRawSql < ActiveRecord::Migration[7.1]
def up
execute "UPDATE users SET status = 'active' WHERE confirmed_at IS NOT NULL"
end
def down
execute "UPDATE users SET status = NULL WHERE status = 'active'"
end
end
Multi-line SQL with Heredoc
class CreateComplexIndex < ActiveRecord::Migration[7.1]
def up
execute <<-SQL
CREATE INDEX CONCURRENTLY idx_users_search
ON users
USING GIN (to_tsvector('english', name || ' ' || email));
SQL
end
def down
execute "DROP INDEX idx_users_search;"
end
end
With reversible
class CreateView < ActiveRecord::Migration[7.1]
def change
reversible do |dir|
dir.up do
execute <<-SQL
CREATE VIEW active_users AS
SELECT id, name, email, created_at
FROM users
WHERE active = true;
SQL
end
dir.down do
execute "DROP VIEW active_users;"
end
end
end
end
Query Results
class DataMigration < ActiveRecord::Migration[7.1]
def up
# execute returns result object
results = execute("SELECT id, name FROM users WHERE status IS NULL")
results.each do |row|
puts "User #{row['id']}: #{row['name']}"
end
end
end
Database-Specific SQL
class PostgresSpecific < ActiveRecord::Migration[7.1]
def up
return unless postgresql?
execute "CREATE EXTENSION IF NOT EXISTS pg_trgm;"
end
private
def postgresql?
ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
end
end
How do I create a database view?
Database views are virtual tables based on a SELECT query. They're useful for complex queries, data abstraction, and security.
Create View with SQL
class CreateActiveUsersView < ActiveRecord::Migration[7.1]
def up
execute <<-SQL
CREATE VIEW active_users AS
SELECT id, name, email, created_at
FROM users
WHERE active = true AND deleted_at IS NULL;
SQL
end
def down
execute "DROP VIEW active_users;"
end
end
Using reversible
class CreateOrderSummaryView < ActiveRecord::Migration[7.1]
def change
reversible do |dir|
dir.up do
execute <<-SQL
CREATE VIEW order_summaries AS
SELECT
users.id AS user_id,
users.name AS user_name,
COUNT(orders.id) AS order_count,
SUM(orders.total) AS total_spent
FROM users
LEFT JOIN orders ON orders.user_id = users.id
GROUP BY users.id, users.name;
SQL
end
dir.down do
execute "DROP VIEW order_summaries;"
end
end
end
end
Materialized View (PostgreSQL)
class CreateMaterializedView < ActiveRecord::Migration[7.1]
def up
execute <<-SQL
CREATE MATERIALIZED VIEW daily_stats AS
SELECT
DATE(created_at) AS date,
COUNT(*) AS order_count,
SUM(total) AS revenue
FROM orders
GROUP BY DATE(created_at);
CREATE UNIQUE INDEX idx_daily_stats_date ON daily_stats (date);
SQL
end
def down
execute "DROP MATERIALIZED VIEW daily_stats;"
end
end
Using View in Rails Model
class ActiveUser < ApplicationRecord
self.table_name = 'active_users'
self.primary_key = 'id'
# Read-only by default
def readonly?
true
end
end
How do I create a stored procedure?
Stored procedures are precompiled SQL code stored in the database. They're database-specific—use functions for PostgreSQL.
PostgreSQL Function
class CreateArchiveFunction < ActiveRecord::Migration[7.1]
def up
execute <<-SQL
CREATE OR REPLACE FUNCTION archive_old_orders(days_old INTEGER)
RETURNS INTEGER AS $$
DECLARE
archived_count INTEGER;
BEGIN
WITH moved AS (
DELETE FROM orders
WHERE created_at < NOW() - (days_old || ' days')::INTERVAL
RETURNING *
)
INSERT INTO archived_orders SELECT * FROM moved;
GET DIAGNOSTICS archived_count = ROW_COUNT;
RETURN archived_count;
END;
$$ LANGUAGE plpgsql;
SQL
end
def down
execute "DROP FUNCTION IF EXISTS archive_old_orders(INTEGER);"
end
end
MySQL Stored Procedure
class CreateArchiveProcedure < ActiveRecord::Migration[7.1]
def up
execute <<-SQL
CREATE PROCEDURE archive_old_orders(IN days_old INT)
BEGIN
INSERT INTO archived_orders
SELECT * FROM orders
WHERE created_at < DATE_SUB(NOW(), INTERVAL days_old DAY);
DELETE FROM orders
WHERE created_at < DATE_SUB(NOW(), INTERVAL days_old DAY);
END
SQL
end
def down
execute "DROP PROCEDURE IF EXISTS archive_old_orders;"
end
end
Calling from Rails
# PostgreSQL function
result = ActiveRecord::Base.connection.execute("SELECT archive_old_orders(90)")
archived_count = result.first['archive_old_orders']
# MySQL procedure
ActiveRecord::Base.connection.execute("CALL archive_old_orders(90)")
How do I create a database trigger?
Triggers automatically execute code in response to database events (INSERT, UPDATE, DELETE). They're useful for audit logs and data integrity.
PostgreSQL Trigger
class CreateAuditTrigger < ActiveRecord::Migration[7.1]
def up
# Create the trigger function
execute <<-SQL
CREATE OR REPLACE FUNCTION audit_user_changes()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO user_audits (user_id, action, old_data, new_data, created_at)
VALUES (
COALESCE(NEW.id, OLD.id),
TG_OP,
row_to_json(OLD),
row_to_json(NEW),
NOW()
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
SQL
# Create the trigger
execute <<-SQL
CREATE TRIGGER users_audit_trigger
AFTER INSERT OR UPDATE OR DELETE ON users
FOR EACH ROW
EXECUTE FUNCTION audit_user_changes();
SQL
end
def down
execute "DROP TRIGGER IF EXISTS users_audit_trigger ON users;"
execute "DROP FUNCTION IF EXISTS audit_user_changes();"
end
end
Auto-Update Timestamp Trigger
class CreateUpdatedAtTrigger < ActiveRecord::Migration[7.1]
def up
execute <<-SQL
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON orders
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();
SQL
end
def down
execute "DROP TRIGGER IF EXISTS set_updated_at ON orders;"
execute "DROP FUNCTION IF EXISTS update_updated_at();"
end
end
MySQL Trigger
class CreateMySQLTrigger < ActiveRecord::Migration[7.1]
def up
execute <<-SQL
CREATE TRIGGER before_user_update
BEFORE UPDATE ON users
FOR EACH ROW
BEGIN
INSERT INTO user_audits (user_id, old_name, new_name, changed_at)
VALUES (OLD.id, OLD.name, NEW.name, NOW());
END
SQL
end
def down
execute "DROP TRIGGER IF EXISTS before_user_update;"
end
end
How do I create a PostgreSQL schema/namespace?
PostgreSQL schemas are namespaces that organize database objects. Useful for multi-tenant applications or separating concerns.
Create Schema (Rails 7.1+)
class CreateTenantSchema < ActiveRecord::Migration[7.1]
def change
create_schema :tenant_acme
end
end
Create Schema with Raw SQL
class CreateTenantSchema < ActiveRecord::Migration[7.1]
def up
execute "CREATE SCHEMA IF NOT EXISTS tenant_acme;"
end
def down
execute "DROP SCHEMA IF EXISTS tenant_acme CASCADE;"
end
end
Create Table in Schema
class CreateTenantUsers < ActiveRecord::Migration[7.1]
def up
execute "CREATE SCHEMA IF NOT EXISTS tenant_acme;"
execute <<-SQL
CREATE TABLE tenant_acme.users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255),
email VARCHAR(255),
created_at TIMESTAMP,
updated_at TIMESTAMP
);
SQL
end
def down
execute "DROP TABLE IF EXISTS tenant_acme.users;"
execute "DROP SCHEMA IF EXISTS tenant_acme;"
end
end
Using Schema in Rails
# In model
class Tenant::User < ApplicationRecord
self.table_name = 'tenant_acme.users'
end
# Or set schema search path
ActiveRecord::Base.connection.schema_search_path = 'tenant_acme,public'
Multi-Tenant Pattern
class CreateTenantSchema < ActiveRecord::Migration[7.1]
def change
reversible do |dir|
dir.up do
execute "CREATE SCHEMA IF NOT EXISTS #{tenant_schema};"
# Copy structure from template
execute "CREATE TABLE #{tenant_schema}.users (LIKE public.users INCLUDING ALL);"
end
dir.down do
execute "DROP SCHEMA IF EXISTS #{tenant_schema} CASCADE;"
end
end
end
private
def tenant_schema
ENV['TENANT_SCHEMA'] || 'tenant_default'
end
end
Why isn't my create_schema reversible?
The create_schema method was not automatically reversible
in earlier Rails versions. Rails 7.1+ added drop_schema support.
The Error (Pre-Rails 7.1)
ActiveRecord::IrreversibleMigration:
create_schema is not reversible.
Fix: Use up/down
class CreateAnalyticsSchema < ActiveRecord::Migration[7.0]
def up
execute "CREATE SCHEMA analytics;"
end
def down
execute "DROP SCHEMA analytics CASCADE;"
end
end
Rails 7.1+ Reversible Version
class CreateAnalyticsSchema < ActiveRecord::Migration[7.1]
def change
create_schema :analytics
# Automatically reversed with drop_schema
end
end
With reversible Helper
class CreateSchema < ActiveRecord::Migration[7.0]
def change
reversible do |dir|
dir.up { execute "CREATE SCHEMA analytics;" }
dir.down { execute "DROP SCHEMA analytics CASCADE;" }
end
end
end
Warning: CASCADE Drops Everything
Using CASCADE in the down method will drop all objects in the
schema. Be careful in production!
How do I run migrations for multiple databases?
Rails 6+ supports multiple databases. Each database has its own migrations directory and migration commands.
Database Configuration
# config/database.yml
production:
primary:
database: myapp_production
adapter: postgresql
analytics:
database: myapp_analytics
adapter: postgresql
migrations_paths: db/analytics_migrate
Run All Migrations
# Run migrations for all databases
rails db:migrate
# This runs migrations in:
# - db/migrate (primary)
# - db/analytics_migrate (analytics)
Run Migrations for Specific Database
# Only primary database
rails db:migrate:primary
# Only analytics database
rails db:migrate:analytics
Check Status
# Status for all databases
rails db:migrate:status
# Status for specific database
rails db:migrate:status:analytics
Rollback
# Rollback primary
rails db:rollback:primary
# Rollback analytics
rails db:rollback:analytics STEP=2
Directory Structure
db/
├── migrate/ # Primary database
│ └── 20260106_create_users.rb
├── analytics_migrate/ # Analytics database
│ └── 20260106_create_events.rb
├── schema.rb # Primary schema
└── analytics_schema.rb # Analytics schema
How do I generate a migration for a specific database?
Use the --database flag when generating migrations to target
a specific database in a multi-database setup.
Generate for Specific Database
# Generate migration for primary (default)
rails g migration CreateUsers name:string
# Generate migration for analytics database
rails g migration CreateEvents --database=analytics
File Location
# Primary: db/migrate/20260106_create_users.rb
# Analytics: db/analytics_migrate/20260106_create_events.rb
In the Migration File
# db/analytics_migrate/20260106_create_events.rb
class CreateEvents < ActiveRecord::Migration[7.1]
def change
create_table :events do |t|
t.string :name
t.jsonb :properties
t.timestamps
end
end
end
Generate Model for Specific Database
rails g model Event name:string --database=analytics
Model Configuration
# app/models/analytics_record.rb
class AnalyticsRecord < ApplicationRecord
self.abstract_class = true
connects_to database: { writing: :analytics, reading: :analytics }
end
# app/models/event.rb
class Event < AnalyticsRecord
# Uses analytics database
end
How do I check pending migrations for all databases?
Rails provides commands to check migration status across all configured databases.
Check All Databases
rails db:migrate:status
Output:
database: myapp_development
Status Migration ID Migration Name
--------------------------------------------------
up 20260101120000 Create users
up 20260102120000 Add email to users
database: myapp_analytics_development
Status Migration ID Migration Name
--------------------------------------------------
up 20260101120000 Create events
down 20260106120000 Add properties to events
Check Specific Database
rails db:migrate:status:primary
rails db:migrate:status:analytics
Programmatic Check
# In Rails console or code
ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |config|
ActiveRecord::Base.establish_connection(config)
context = ActiveRecord::MigrationContext.new(config.migrations_paths)
puts "Database: #{config.name}"
puts "Pending: #{context.open.pending_migrations.map(&:version)}"
end
In Application Code
# Check if any migrations are pending
ActiveRecord::Migration.check_all_pending!
Why are my migrations running on the wrong database?
This usually happens due to incorrect migrations_paths configuration
or migration files in the wrong directory.
Common Causes
- Migration file in wrong directory
- Missing
migrations_pathsin database.yml - Incorrect database name in command
Fix: Check migrations_paths
# config/database.yml
development:
primary:
database: myapp_development
migrations_paths: db/migrate # Explicit path
analytics:
database: myapp_analytics_development
migrations_paths: db/analytics_migrate # Must be different!
Fix: Check File Location
# Verify migration is in correct directory
ls db/migrate/ # Primary migrations
ls db/analytics_migrate/ # Analytics migrations
Fix: Specify Database in Generator
# Always specify database when generating
rails g migration CreateEvents --database=analytics
Debug: Check Which DB Migration Runs On
class CreateEvents < ActiveRecord::Migration[7.1]
def change
puts "Running on: #{ActiveRecord::Base.connection_db_config.name}"
create_table :events do |t|
t.timestamps
end
end
end
Common Error
Table 'events' already exists in primary database
-- when it should be in analytics database
Fix: Move the migration file to the correct directory and ensure
migrations_paths is configured.
Why aren't pending migrations detected for my secondary database?
The pending migration check might not detect secondary database migrations due to configuration issues or middleware behavior.
The Problem
# Server starts without error even though analytics DB has pending migrations
# ActiveRecord::PendingMigrationError not raised
Fix: Check All Databases in Middleware
# config/application.rb
config.active_record.migration_error = :page_load
# This should check all databases, but verify your Rails version
Fix: Manual Check
# config/initializers/check_pending_migrations.rb
if Rails.env.development?
ActiveRecord::Migration.check_all_pending!
end
Rails 7.1+ Behavior
# Rails 7.1+ checks all databases by default
# If still not working, ensure migrations_paths is set
Verify Configuration
# Check that Rails sees both migration paths
rails runner "puts ActiveRecord::Base.configurations.configs_for(env_name: 'development').map(&:migrations_paths)"
Fix: database_tasks Option
# Ensure database_tasks is not false
development:
primary:
database: myapp_dev
database_tasks: true # Default, but be explicit
analytics:
database: myapp_analytics_dev
database_tasks: true # Must be true for migrations to run
Common Gotcha
# This DISABLES migration tasks for analytics!
analytics:
database: analytics_db
database_tasks: false # No migrations will run or be detected
How do I fix migration ordering across databases?
When using multiple databases, migrations run in timestamp order across all databases by default. This can cause issues if migrations have dependencies.
The Problem
# These run in timestamp order:
20260106100000_create_users.rb (primary)
20260106100001_create_events.rb (analytics) # Might need users first!
20260106100002_add_user_to_events.rb (analytics)
Fix: Run Databases Separately
# Run primary first
rails db:migrate:primary
# Then analytics
rails db:migrate:analytics
Fix: Use Later Timestamps
# Ensure dependent migrations have later timestamps
# Primary: 20260106100000_create_users.rb
# Analytics: 20260106110000_create_events.rb (1 hour later)
For db:prepare
Rails 7.1+ fixed ordering for db:prepare:
# This now handles ordering correctly
rails db:prepare
Deployment Strategy
# In deployment script
#!/bin/bash
set -e
# Run primary database migrations first
rails db:migrate:primary
# Then other databases
rails db:migrate:analytics
# Or for all databases in correct order (Rails 7.1+)
rails db:prepare
Why does database_tasks: false break my other migrations?
Setting database_tasks: false on the primary database can prevent
migrations from running on other databases.
The Problem
# config/database.yml
production:
primary:
database: external_db
database_tasks: false # Can't run migrations on external DB
internal:
database: internal_db
migrations_paths: db/internal_migrate
# Migrations don't run here either!
The Error
rails db:migrate
# Nothing happens, or
# ActiveRecord::ConnectionNotEstablished
Fix: Run Specific Database
# Explicitly run internal database migrations
rails db:migrate:internal
Fix: Reorder Databases
# Put the database with database_tasks last
production:
internal:
database: internal_db
migrations_paths: db/internal_migrate
primary: # Still called primary, but listed second
database: external_db
database_tasks: false
Fix: Use Different Primary
production:
primary:
database: internal_db
migrations_paths: db/migrate
external:
database: external_db
database_tasks: false # Now this is secondary
When to Use database_tasks: false
- External databases you don't control
- Read replicas
- Databases managed by other applications
What is schema.rb vs structure.sql?
Rails can dump your database schema in two formats: Ruby DSL (schema.rb)
or raw SQL (structure.sql).
schema.rb (Default)
# db/schema.rb
ActiveRecord::Schema[7.1].define(version: 2026_01_06_120000) do
create_table "users", force: :cascade do |t|
t.string "name"
t.string "email"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["email"], name: "index_users_on_email", unique: true
end
end
structure.sql
-- db/structure.sql
CREATE TABLE users (
id bigint NOT NULL,
name character varying,
email character varying,
created_at timestamp(6) NOT NULL,
updated_at timestamp(6) NOT NULL
);
CREATE UNIQUE INDEX index_users_on_email ON users (email);
Comparison
| Feature | schema.rb | structure.sql |
|---|---|---|
| Database-agnostic | Yes | No |
| Views, triggers, functions | No | Yes |
| Custom types (enums) | Limited | Yes |
| Readable | Yes | Less so |
When to Use structure.sql
- Using database views
- Using stored procedures or functions
- Using database triggers
- Using native enum types
- Using extensions (pgcrypto, postgis, etc.)
How do I switch from schema.rb to structure.sql?
Configure Rails to use SQL format for schema dumps when you need database-specific features that schema.rb can't represent.
Change Configuration
# config/application.rb
config.active_record.schema_format = :sql
Generate structure.sql
# Dump current schema to structure.sql
rails db:schema:dump
# Or specifically
rails db:structure:dump
Update .gitignore
# .gitignore
# Remove or comment out:
# /db/schema.rb
# Add (if you don't want to track SQL):
# /db/structure.sql
Loading Schema
# Load structure.sql into database
rails db:schema:load
# For test database
rails db:test:prepare
Mixed Approach (Not Recommended)
You can keep both files, but it's confusing:
# This generates both files
config.active_record.schema_format = :ruby
# Then manually run: rails db:structure:dump
Per-Database Format (Rails 7+)
# config/database.yml
production:
primary:
schema_format: ruby
analytics:
schema_format: sql
How do I dump schema for only specific databases?
In multi-database setups, you can dump schemas for individual databases or skip certain databases entirely.
Dump Specific Database
# Dump primary only
rails db:schema:dump:primary
# Dump analytics only
rails db:schema:dump:analytics
Skip Database from Schema Dump
# config/database.yml
production:
primary:
database: myapp_db
external:
database: external_db
database_tasks: false # Skips all tasks including dump
Custom Rake Task for Selective Dump
# lib/tasks/db.rake
namespace :db do
namespace :schema do
desc "Dump only internal databases"
task dump_internal: :environment do
%w[primary analytics].each do |db|
Rake::Task["db:schema:dump:#{db}"].invoke
end
end
end
end
Output Files
db/
├── schema.rb # Primary database
├── analytics_schema.rb # Analytics database
└── structure.sql # If using SQL format
CI/CD Consideration
# In CI, you might want to verify schema is up to date
rails db:schema:dump
git diff --exit-code db/schema.rb || (echo "Schema out of date!" && exit 1)
Why isn't my schema.rb updating after migrations?
Several issues can prevent schema.rb from updating after running migrations.
Common Causes
- Running migrations in production mode
- File permission issues
- Schema dump disabled
- Running migrations via console/script
Force Schema Dump
# Manually dump schema
rails db:schema:dump
# Or structure.sql
rails db:structure:dump
Check Configuration
# config/environments/production.rb
# This disables schema dump in production (correct behavior)
config.active_record.dump_schema_after_migration = false
# config/environments/development.rb
# This should be true (default)
config.active_record.dump_schema_after_migration = true
Running in Wrong Environment
# This might not update schema.rb
RAILS_ENV=production rails db:migrate
# This should update schema.rb
rails db:migrate # defaults to development
Browser-Based Migration
Running migrations via the browser (development error page) may not update schema.rb.
Run rails db:migrate from command line instead.
File Permissions
# Check schema.rb is writable
ls -la db/schema.rb
# Fix permissions if needed
chmod 644 db/schema.rb
What is migration versioning (ActiveRecord::Migration[7.1])?
Migration versioning ensures old migrations behave the same way they did when
written, even after Rails upgrades. The version number (e.g., [7.1])
locks in the behavior from that Rails version.
How It Works
# Rails 6.0 behavior
class AddEmailToUsers < ActiveRecord::Migration[6.0]
def change
add_reference :posts, :user # Creates INTEGER column
end
end
# Rails 7.1 behavior
class AddEmailToUsers < ActiveRecord::Migration[7.1]
def change
add_reference :posts, :user # Creates BIGINT column
end
end
What Changes Between Versions
- 5.1: Primary keys became bigint by default
- 6.0: References defaulted to integer (for compatibility)
- 7.0: DateTime precision defaults changed
- 7.1: Index naming for rename_table changed
Why It Matters
# If you wrote this migration in 2020 (Rails 6.0)
class CreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
t.timestamps # Creates datetime without precision
end
end
end
# Running it in 2026 (Rails 8.0) still creates datetime without precision
# because of the [6.0] version lock
Best Practice
# Always use current Rails version for new migrations
rails g migration AddFeature
# Generates: class AddFeature < ActiveRecord::Migration[7.1]
Don't Change Version of Existing Migrations
# BAD: Changing version can break existing databases
class OldMigration < ActiveRecord::Migration[7.1] # Was [6.0]
# GOOD: Keep original version
class OldMigration < ActiveRecord::Migration[6.0]
Why does my old migration behave differently after upgrading Rails?
If a migration version doesn't match its behavior expectations, you might see unexpected results after a Rails upgrade.
Common Scenario
# This migration was written for Rails 6, but missing version
class AddFields < ActiveRecord::Migration # No version!
def change
add_column :users, :last_login, :datetime
end
end
# In Rails 6: datetime without precision
# In Rails 7: datetime WITH precision (different behavior!)
The Error
StandardError: Directly inheriting from ActiveRecord::Migration is not supported.
Please specify the Rails release the migration was written for:
class AddFields < ActiveRecord::Migration[7.1]
Fix: Add Version Number
# Add the version from when migration was written
class AddFields < ActiveRecord::Migration[6.0]
def change
add_column :users, :last_login, :datetime
end
end
Check Compatibility Module
# See what compatibility changes exist
ActiveRecord::Migration::Compatibility::V6_0.instance_methods(false)
# Returns methods with version-specific behavior
Upgrading Strategy
# 1. Keep old migrations at their original version
class OldMigration < ActiveRecord::Migration[6.0]
# 2. Write new migrations with current version
class NewMigration < ActiveRecord::Migration[7.1]
# 3. Never change version of run migrations
How do I maintain compatibility for datetime precision?
Rails 7 changed the default datetime precision from nil to 6 (microseconds). This can cause issues when mixing old and new migrations.
The Change
# Rails 6 (Migration[6.x])
t.datetime :created_at # TIMESTAMP (no precision)
# Rails 7 (Migration[7.x])
t.datetime :created_at # TIMESTAMP(6) with microseconds
Problem: Schema Mismatch
# Fresh db:schema:load creates different columns than
# running migrations sequentially if versions are mixed
Fix: Explicit Precision in Old Migrations
class CreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
t.string :name
# Explicitly set precision to match Rails 7 behavior
t.timestamps precision: 6
end
end
end
Fix: Match Existing Schema
class AddPublishedAt < ActiveRecord::Migration[7.1]
def change
# Match existing timestamps (no precision) if needed
add_column :posts, :published_at, :datetime, precision: nil
end
end
Global Configuration
# config/application.rb
# Force all datetimes to have precision 6
config.active_record.default_precision = 6
Check Current Precision
# In Rails console
col = User.columns_hash['created_at']
puts col.precision # nil or 6
How do I fix index naming issues after a Rails upgrade?
Rails 7.1 changed how index names are generated during rename_table.
This can cause issues with existing migrations.
The Change
# Rails 7.0 and earlier
rename_table :users, :members
# Index stays: index_users_on_email
# Rails 7.1+
rename_table :users, :members
# Index becomes: index_members_on_email
The Error
ArgumentError: Index name 'index_users_on_email' on table 'members' does not exist
-- when rolling back a migration
Fix: Use Migration Version
# Keep old behavior by using old version
class RenameUsersToMembers < ActiveRecord::Migration[7.0]
def change
rename_table :users, :members
# Index keeps old name: index_users_on_email
end
end
Fix: Rename Index Separately
class RenameUsersToMembers < ActiveRecord::Migration[7.1]
def change
rename_table :users, :members
# Explicitly rename index if needed
rename_index :members, 'index_users_on_email', 'index_members_on_email'
end
end
Fix Existing Database
class FixIndexNames < ActiveRecord::Migration[7.1]
def change
# Rename old-style index names to new-style
if index_exists?(:members, :email, name: 'index_users_on_email')
rename_index :members, 'index_users_on_email', 'index_members_on_email'
end
end
end
Check Index Names
# In Rails console
ActiveRecord::Base.connection.indexes(:members).each do |idx|
puts "#{idx.name}: #{idx.columns}"
end
How do I fix "Migrations are pending" error?
This error occurs when there are migrations that haven't been run against your database. It's raised when starting the Rails server in development.
The Error
Migrations are pending. To resolve this issue, run:
bin/rails db:migrate RAILS_ENV=development
ActiveRecord::PendingMigrationError
Fix: Run Migrations
# Run pending migrations
rails db:migrate
# For specific environment
RAILS_ENV=production rails db:migrate
Check What's Pending
rails db:migrate:status
Disable Check Temporarily
# config/environments/development.rb
config.active_record.migration_error = false # Not recommended
Common Causes
- Pulled code with new migrations
- Switched branches with different migrations
- Fresh clone without running setup
- Multiple databases with pending migrations
For Multi-Database
# Check all databases
rails db:migrate:status
# Run for all databases
rails db:migrate
How do I fix duplicate migration names?
Rails requires unique migration names, even with different timestamps. This prevents confusion about which migration does what.
The Error
Another migration is already named add_email_to_users: db/migrate/20260105_add_email_to_users.rb
Fix: Use Unique Name
# Instead of
rails g migration AddEmailToUsers
# Use a more specific name
rails g migration AddEmailFieldToUsers
rails g migration AddEmailColumnToUsersTable
rails g migration AddUserEmailAddress
If Migration Already Exists
# Option 1: Delete old migration (if not run)
rm db/migrate/20260105_add_email_to_users.rb
# Option 2: Rename the file and class
mv db/migrate/20260106_add_email_to_users.rb \
db/migrate/20260106_add_primary_email_to_users.rb
# Then edit the file to rename the class
Check for Duplicates
# Find migrations with similar names
ls db/migrate/ | grep -i email
Multi-Database Consideration
Different databases can have migrations with the same name since they're in different directories:
# These are OK
db/migrate/20260106_add_email.rb
db/analytics_migrate/20260106_add_email.rb
How do I fix "Table already exists" error?
This occurs when running a migration that creates a table that already exists in your database.
The Error
PG::DuplicateTable: ERROR: relation "users" already exists
-- or --
Mysql2::Error: Table 'users' already exists
-- or --
SQLite3::SQLException: table users already exists
Cause: Migration Already Applied
# Check if table exists but migration not recorded
rails db:migrate:status
# Look for "NO FILE" entries or missing entries
Fix: Mark Migration as Run
# If table exists and is correct, mark migration as run
rails runner "ActiveRecord::SchemaMigration.create!(version: '20260106120000')"
Fix: Use if_not_exists
class CreateUsers < ActiveRecord::Migration[7.1]
def change
create_table :users, if_not_exists: true do |t|
t.string :name
t.timestamps
end
end
end
Fix: Drop and Recreate
# WARNING: This deletes data!
rails runner "ActiveRecord::Base.connection.drop_table(:users, if_exists: true)"
rails db:migrate
Fix: Reset Database (Development Only)
# WARNING: Drops all data!
rails db:reset
How do I fix migration deadlocks?
Deadlocks can occur when migrations lock tables while other processes are trying to access them, or when the CheckPending middleware conflicts with autoloading.
The Error
ActiveRecord::StatementInvalid: PG::TRDeadlockDetected: ERROR: deadlock detected
-- or --
Mysql2::Error: Deadlock found when trying to get lock
Fix: Disable DDL Transactions
class AddIndexConcurrently < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def change
add_index :users, :email, algorithm: :concurrently
end
end
Fix: Run During Low Traffic
# Schedule migrations during maintenance window
RAILS_ENV=production rails db:migrate
Fix: Use Timeouts
class SafeMigration < ActiveRecord::Migration[7.1]
def change
# Set statement timeout (PostgreSQL)
execute "SET statement_timeout = '5s'"
add_column :users, :new_field, :string
execute "SET statement_timeout = DEFAULT"
end
end
CheckPending Middleware Deadlock
# config/environments/development.rb
# If you're seeing deadlocks in development with classic autoloader
config.active_record.migration_error = false # Disable auto-check
# Then run migrations manually
Avoid Long Locks
# BAD: Locks table for entire update
class BackfillData < ActiveRecord::Migration[7.1]
def up
User.update_all(status: 'active') # Locks users table
end
end
# GOOD: Process in batches
class BackfillData < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def up
User.in_batches(of: 1000) do |batch|
batch.update_all(status: 'active')
sleep(0.1) # Give other queries a chance
end
end
end
How do I fix "No connection pool" errors during migration?
This error typically occurs with multi-database setups or when connection handling conflicts during migration checks.
The Error
ActiveRecord::ConnectionNotEstablished:
No connection pool for 'ApplicationRecord' found.
-- or --
No connection pool for 'ActiveRecord::Base' found for the 'primary' shard.
Common Causes
- CheckPending middleware issues
- Thread accessing wrong connection
- Multi-database configuration issues
- Connection closed during migration
Fix: Ensure Connection in Migration
class DataMigration < ActiveRecord::Migration[7.1]
def up
# Ensure we have a connection
ActiveRecord::Base.connection_pool.with_connection do
User.update_all(status: 'active')
end
end
end
Fix: Multi-Database Connection
class AnalyticsMigration < ActiveRecord::Migration[7.1]
def up
# Use specific connection
AnalyticsRecord.connection.execute("UPDATE events SET processed = true")
end
end
Fix: Check Configuration
# config/database.yml
development:
primary:
database: myapp_dev
pool: 5 # Ensure pool is set
analytics:
database: analytics_dev
pool: 5
Fix: Advisory Lock Issues
# If you see advisory lock issues
class Migration < ActiveRecord::Migration[7.1]
# Rails 7+ handles this better, but for older versions:
def change
ActiveRecord::Base.connection.reconnect!
# ... migration code
end
end
Why is my migration reporting errors on the wrong column?
Sometimes error messages reference a different column than the one causing the actual problem. This is usually due to constraint or foreign key issues.
The Misleading Error
PG::NotNullViolation: ERROR: column "status" of relation "orders" contains null values
-- But you're adding a column "priority", not "status"!
Common Causes
- Multiple changes in one migration
- Triggers updating other columns
- Generated columns or computed values
- After-save callbacks in models
Fix: Isolate the Problem
# Split into separate migrations
class AddPriority < ActiveRecord::Migration[7.1]
def change
add_column :orders, :priority, :integer
end
end
class AddStatusConstraint < ActiveRecord::Migration[7.1]
def change
change_column_null :orders, :status, false
end
end
Debug: Check What's Actually Happening
class DebugMigration < ActiveRecord::Migration[7.1]
def up
say "Before changes:"
say Order.columns.map(&:name).inspect
add_column :orders, :priority, :integer
say "After add_column:"
Order.reset_column_information
say Order.columns.map(&:name).inspect
end
end
Check for Triggers
# PostgreSQL: check for triggers
execute "SELECT * FROM pg_trigger WHERE tgrelid = 'orders'::regclass"
Disable Triggers Temporarily
class Migration < ActiveRecord::Migration[7.1]
def up
execute "ALTER TABLE orders DISABLE TRIGGER ALL"
add_column :orders, :priority, :integer
execute "ALTER TABLE orders ENABLE TRIGGER ALL"
end
end
How do I recover from a failed migration?
When a migration fails partway through, your database can be in an inconsistent state. Here's how to recover.
PostgreSQL: Transaction Rollback (Automatic)
PostgreSQL runs migrations in a transaction by default, so failures automatically rollback:
== 20260106120000 AddFields: migrating ===
-- add_column(:users, :email, :string)
-> 0.0012s
-- add_index(:users, :email)
rails aborted!
PG::UniqueViolation: duplicate key value
# Transaction rolled back automatically!
MySQL: No Transaction (Manual Recovery)
# MySQL doesn't wrap DDL in transactions
# You may need to manually fix:
# 1. Check what was applied
rails db:migrate:status
# 2. Manually undo partial changes
rails runner "ActiveRecord::Base.connection.remove_column(:users, :email)"
# 3. Fix migration and retry
rails db:migrate
Clear Failed Migration from History
# If migration is marked as run but failed
rails runner "ActiveRecord::SchemaMigration.where(version: '20260106120000').delete_all"
Fix and Re-run
# Make migration idempotent
class AddEmailToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :email, :string unless column_exists?(:users, :email)
unless index_exists?(:users, :email)
add_index :users, :email, unique: true
end
end
end
Disable Transaction for Partial Success
class LargeMigration < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def up
# Changes are committed as they happen
# Be careful - partial state possible on failure!
end
end
Production Recovery Checklist
- Don't panic - assess current state
- Check
db:migrate:status - Review database schema directly
- Fix migration to be idempotent
- Test fix on staging first
- Apply fix to production
How do I fix "Directly inheriting from ActiveRecord::Migration is not supported"?
Rails 5+ requires specifying a version number in migration class definitions. This error occurs when using old-style migrations without a version.
The Error
StandardError: Directly inheriting from ActiveRecord::Migration is not supported.
Please specify the Rails release the migration was written for:
class AddEmailToUsers < ActiveRecord::Migration[7.1]
The Problem
# Old style (Rails 4 and earlier)
class AddEmailToUsers < ActiveRecord::Migration # No version!
def change
add_column :users, :email, :string
end
end
Fix: Add Version Number
# Add appropriate version
class AddEmailToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :email, :string
end
end
Choosing the Right Version
# Use the Rails version when migration was written:
# - Rails 5.0 app: ActiveRecord::Migration[5.0]
# - Rails 6.1 app: ActiveRecord::Migration[6.1]
# - Rails 7.1 app: ActiveRecord::Migration[7.1]
# If unsure about old migration, use oldest supported:
class OldMigration < ActiveRecord::Migration[5.0]
Bulk Fix Old Migrations
# Find all migrations without version
grep -l "< ActiveRecord::Migration$" db/migrate/*.rb
# You can use sed to fix (careful with this!)
# sed -i 's/< ActiveRecord::Migration$/< ActiveRecord::Migration[5.0]/' db/migrate/*.rb
Available Versions
# Check available compatibility versions
ActiveRecord::Migration.descendants
# Or look at Rails source for Compatibility modules
Why did rename_table break after upgrading to Rails 7.1?
Rails 7.1 changed how rename_table handles index names during
rollback, which can break existing migrations.
The Error
ArgumentError: wrong number of arguments (given 3, expected 2)
-- when rolling back a migration with rename_table
The Cause
# This migration worked in Rails 7.0
class RenameUsers < ActiveRecord::Migration[7.0]
def change
rename_table :users, :members
end
end
# But in Rails 7.1, the rollback behavior changed
Fix: Keep Original Migration Version
# DON'T change the version of existing migrations
class RenameUsers < ActiveRecord::Migration[7.0] # Keep as 7.0!
def change
rename_table :users, :members
end
end
Fix: Use up/down for Control
class RenameUsers < ActiveRecord::Migration[7.1]
def up
rename_table :users, :members
end
def down
rename_table :members, :users
end
end
Fix: Handle Index Names Explicitly
class RenameUsers < ActiveRecord::Migration[7.1]
def change
rename_table :users, :members
# Rename indexes to match new table name
rename_index :members, 'index_users_on_email', 'index_members_on_email'
end
end
Check Your Rails Version
rails -v
# Rails 7.1.0+: new behavior
# Rails 7.0.x and below: old behavior
How do I write zero-downtime migrations?
Zero-downtime migrations allow database changes without stopping your application. They require careful planning and often multiple deploy cycles.
Key Principles
- Never lock tables for long periods
- Make changes backward-compatible
- Deploy in stages
- Use concurrent operations where possible
Add Column Safely
class AddFieldSafely < ActiveRecord::Migration[7.1]
def change
# This is safe - no lock in PostgreSQL 11+
add_column :users, :new_field, :string
# DON'T do this - locks table while updating all rows
# add_column :users, :new_field, :string, default: 'value', null: false
end
end
Add Index Without Locking
class AddIndexConcurrently < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def change
add_index :users, :email, algorithm: :concurrently
end
end
Rename Column (Multi-Deploy)
# Deploy 1: Add new column
class AddNewColumn < ActiveRecord::Migration[7.1]
def change
add_column :users, :full_name, :string
end
end
# Deploy 2: Update code to write to both columns
# Deploy 3: Backfill data
class BackfillFullName < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def up
User.in_batches.update_all('full_name = name')
end
end
# Deploy 4: Update code to read from new column only
# Deploy 5: Remove old column
class RemoveOldColumn < ActiveRecord::Migration[7.1]
def change
remove_column :users, :name, :string
end
end
Remove Column Safely
# Deploy 1: Ignore column in model
class User < ApplicationRecord
self.ignored_columns = [:old_field]
end
# Deploy 2: Remove column (after deploy 1 is stable)
class RemoveOldField < ActiveRecord::Migration[7.1]
def change
remove_column :users, :old_field, :string
end
end
Tools for Safe Migrations
# Gemfile
gem 'strong_migrations' # Catches unsafe migrations
How do I safely add an index to a large table?
Adding an index to a large table can lock the table for extended periods, causing downtime. Use concurrent indexing to avoid this.
PostgreSQL: Concurrent Index
class AddIndexToLargeTable < ActiveRecord::Migration[7.1]
disable_ddl_transaction! # Required for CONCURRENTLY
def change
add_index :orders, :user_id, algorithm: :concurrently
end
end
Why disable_ddl_transaction?
CREATE INDEX CONCURRENTLY cannot run inside a transaction.
disable_ddl_transaction! prevents Rails from wrapping the
migration in a transaction.
Unique Concurrent Index
class AddUniqueIndex < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def change
add_index :users, :email, unique: true, algorithm: :concurrently
end
end
Handle Failed Concurrent Index
# If concurrent index creation fails, it leaves an INVALID index
# Check for invalid indexes:
# SELECT * FROM pg_indexes WHERE indexdef LIKE '%INVALID%'
class RetryIndex < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def up
# Remove invalid index if exists
remove_index :orders, :user_id, if_exists: true
# Try again
add_index :orders, :user_id, algorithm: :concurrently
end
end
MySQL: pt-online-schema-change
# MySQL doesn't have CONCURRENTLY
# Use Percona tools for large tables:
pt-online-schema-change \
--alter "ADD INDEX idx_user_id (user_id)" \
D=myapp,t=orders
Estimate Time
# Check table size before adding index
size = ActiveRecord::Base.connection.execute(
"SELECT pg_size_pretty(pg_total_relation_size('orders'))"
).first['pg_size_pretty']
puts "Table size: #{size}"
How do I safely remove a column?
Removing a column while the application is running can cause errors if code still references that column. Use a multi-deploy strategy.
The Danger
# If you remove column while old code is running:
ActiveRecord::StatementInvalid: PG::UndefinedColumn:
ERROR: column "old_field" does not exist
Safe Removal Process
Step 1: Ignore Column in Model
# app/models/user.rb
class User < ApplicationRecord
self.ignored_columns += ['old_field']
end
Step 2: Deploy and Verify
# Deploy the code change
# Wait for all servers to update
# Verify no errors related to the column
Step 3: Remove Column in Migration
class RemoveOldFieldFromUsers < ActiveRecord::Migration[7.1]
def change
remove_column :users, :old_field, :string
end
end
Step 4: Remove ignored_columns (Optional)
# After migration is deployed everywhere
class User < ApplicationRecord
# Can remove this line now
# self.ignored_columns += ['old_field']
end
Using strong_migrations Gem
# Gemfile
gem 'strong_migrations'
# This will warn you about unsafe column removal
class RemoveColumn < ActiveRecord::Migration[7.1]
def change
safety_assured { remove_column :users, :old_field }
end
end
Batch Column Removal
# app/models/user.rb
class User < ApplicationRecord
self.ignored_columns += [
'legacy_field1',
'legacy_field2',
'deprecated_at'
]
end
# Migration (after code deployed)
class RemoveLegacyFields < ActiveRecord::Migration[7.1]
def change
remove_column :users, :legacy_field1, :string
remove_column :users, :legacy_field2, :integer
remove_column :users, :deprecated_at, :datetime
end
end
How do I test migrations?
Testing migrations ensures they work correctly for both fresh databases and existing data. Here are strategies for testing migrations.
Manual Testing Checklist
# 1. Run migration
rails db:migrate
# 2. Verify schema
rails db:migrate:status
cat db/schema.rb | grep -A 20 "create_table :users"
# 3. Test rollback
rails db:rollback
# 4. Run again
rails db:migrate
# 5. Test with production data (on staging)
RAILS_ENV=staging rails db:migrate
Automated Migration Tests
# test/migrations/add_email_to_users_test.rb
require 'test_helper'
class AddEmailToUsersTest < ActiveSupport::TestCase
def setup
# Run migration up
ActiveRecord::Migration.suppress_messages do
AddEmailToUsers.migrate(:up)
end
end
def teardown
ActiveRecord::Migration.suppress_messages do
AddEmailToUsers.migrate(:down)
end
end
test "adds email column" do
User.reset_column_information
assert User.column_names.include?('email')
end
test "email has correct type" do
column = User.columns_hash['email']
assert_equal :string, column.type
end
end
Test Data Migration
# test/migrations/backfill_status_test.rb
require 'test_helper'
class BackfillStatusTest < ActiveSupport::TestCase
test "backfills nil status to pending" do
# Setup: create user with nil status
user = User.create!(name: 'Test', status: nil)
# Run migration
BackfillStatus.migrate(:up)
# Verify
user.reload
assert_equal 'pending', user.status
end
end
CI Pipeline Testing
# .github/workflows/test.yml
- name: Test migrations from scratch
run: |
rails db:create
rails db:migrate
rails db:seed
- name: Test migration rollback
run: |
rails db:rollback STEP=5
rails db:migrate
Test Against Production Schema
# Dump production schema (safely!)
pg_dump -s production_db > production_schema.sql
# Load into test database
psql test_db < production_schema.sql
# Run pending migrations
RAILS_ENV=test rails db:migrate
When should I squash migrations?
Squashing migrations combines many migration files into fewer files, typically based on the current schema. Consider squashing when migrations become unwieldy.
Signs You Need to Squash
- Hundreds of migration files
- Fresh
db:migratetakes too long - Old migrations reference deleted models
- Migration history is confusing
- Starting a major new version
How to Squash
# 1. Ensure all environments are migrated
rails db:migrate:status # All should be "up"
# 2. Delete old migrations (AFTER all DBs migrated!)
rm db/migrate/*.rb
# 3. Generate schema migration
rails db:schema:dump # Ensure schema.rb is current
# 4. Create initial migration from schema
rails g migration InitialSchema
New Initial Migration
# db/migrate/20260106000000_initial_schema.rb
class InitialSchema < ActiveRecord::Migration[7.1]
def change
# Copy content from schema.rb's block
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
add_index :users, :email, unique: true
create_table :posts do |t|
t.references :user, foreign_key: true
t.string :title
t.timestamps
end
# ... rest of schema
end
end
Alternative: Use schema:load
# For fresh databases, use schema load instead of migrations
rails db:schema:load # Much faster than running all migrations
Cautions
- All team members must be on same migration state
- All deployed environments must be migrated
- Can't squash if anyone has pending migrations
- Keep backups before deleting migrations
Keep Some History
# Option: Keep recent migrations, squash old ones
# Keep: Last 6 months of migrations
# Squash: Everything older into InitialSchema
# Move old migrations to archive
mkdir db/migrate_archive
mv db/migrate/2024*.rb db/migrate_archive/
What are the best migration naming conventions?
Good migration names make it easy to understand what each migration does without reading the code. Rails recognizes certain patterns for auto-generation.
Auto-Generated Patterns
# AddXxxToYyy - adds columns to table
rails g migration AddEmailToUsers email:string
# Creates: add_column :users, :email, :string
# RemoveXxxFromYyy - removes columns
rails g migration RemoveAgeFromUsers age:integer
# Creates: remove_column :users, :age, :integer
# CreateXxx - creates table
rails g migration CreateProducts name:string price:decimal
# Creates: create_table :products
# AddXxxRefToYyy - adds reference
rails g migration AddUserRefToPosts user:references
# Creates: add_reference :posts, :user
Descriptive Names
# Good: Clear and specific
AddEmailIndexToUsers
CreateOrdersProductsJoinTable
RemoveDeprecatedFieldsFromUsers
ChangeStatusTypeInOrders
AddUniqueConstraintToUsersEmail
# Bad: Vague or unclear
UpdateUsers
FixDatabase
Changes
V2
Action-First Naming
# Start with verb describing the action
Add...
Remove...
Create...
Drop...
Change...
Rename...
Enable... # For extensions
Backfill... # For data migrations
Data Migration Names
# Clearly indicate data migrations
BackfillUserStatuses
MigrateOrdersToNewFormat
PopulateDefaultSettings
CleanupOrphanedRecords
Multi-Part Migration Names
# For related changes
AddFieldsToUsers # Multiple fields
AddIndexesToOrders # Multiple indexes
RemoveLegacyColumnsFromUsers
Avoid
# Don't use dates in names (timestamp handles this)
Add20260106Email # Bad
# Don't use generic names
UpdateDatabase # Too vague
FixBug # Not descriptive
Temp # What is this?
# Don't use abbreviations
AddUsrEmailIdx # Hard to read
AddUserEmailIndex # Better
File Naming
# Rails generates: timestamp_snake_case_name.rb
20260106120000_add_email_to_users.rb
20260106120001_create_orders.rb
20260106120002_add_user_ref_to_posts.rb