Active Record Migrations
A migration is one version step of the schema: Ruby DSL forward, reversible when the change allows, recorded so every environment can replay the same history.
<!-- hal:authoritative:yaml -->
A migration is one version step of the schema: Ruby DSL forward, reversible when the change allows, recorded so every environment can replay the same history.
§I — Frame
Duha session 03 for the Rails track. Dual-fire beside Rust. The spine remains the official Rails Guides snapshot on disk: rails-guides/ at v8.1.3.1. Today's file is active_record_migrations.html.
Session 02 mapped models to tables and ran CRUD through Active Record. That work assumed a table already existed. This fire owns how the table got there and how it changes over time without hand-written SQL for every environment.
By the end, generate a migration, run bin/rails db:migrate and a rollback, and say what db/schema.rb is for. Validations, callbacks, and associations stay later syllabus rows.
§II — What a migration is
Migrations evolve the database schema in a reproducible way. Each file is a new version on a timeline. Active Record knows how to move a database from its current version to the latest by running the migrations it has not yet applied.
They use a Ruby DSL so you do not write vendor SQL for ordinary table and column changes. The same migration can target PostgreSQL, SQLite, or MySQL within the limits of the DSL.
A create-table migration looks like this:
# db/migrate/20240502100843_create_products.rb
class CreateProducts < ActiveRecord::Migration[8.1]
def change
create_table :products do |t|
t.string :name
t.text :description
t.timestamps
end
end
end
id is added as the default primary key unless you say otherwise. t.timestamps adds created_at and updated_at, which Active Record maintains when the columns exist.
change describes the forward move. For many DSL methods, Active Record can reverse the step on rollback (drop the table for create_table). Irreversible operations need an explicit up/down or a raised ActiveRecord::IrreversibleMigration.
§III — Generating migration files
Migrations live under db/migrate/. The filename is YYYYMMDDHHMMSS_name.rb: a UTC timestamp, then _, then a snake_case name. The class name is the CamelCase form of that name. Rails orders migrations by the timestamp prefix.
Standalone generator:
bin/rails generate migration CreateProducts name:string description:text
Name patterns teach the generator what to emit:
bin/rails generate migration AddPartNumberToProducts part_number:string
class AddPartNumberToProducts < ActiveRecord::Migration[8.1]
def change
add_column :products, :part_number, :string
end
end
Add an index in the same generator pass with part_number:string:index. Multiple columns work in one name:
bin/rails generate migration AddDetailsToProducts part_number:string price:decimal
Model and scaffold generators also emit migrations. Prefer the dedicated generate migration form when you are changing an existing table without inventing a new model.
Creating a model generates the model file and a matching Create… migration in one step:
bin/rails generate model Product name:string description:text
That pairs with Session 02’s empty Product story: the migration is what created products before CRUD worked.
§IV — The change DSL you will use daily
Create a table with create_table and a block of column helpers (t.string, t.text, t.integer, t.boolean, t.datetime, references, and so on).
Add or remove columns with add_column / remove_column.
Batch edits with change_table:
change_table :products do |t|
t.remove :description, :name
t.string :part_number
t.index :part_number
t.rename :upccode, :upc_code
end
Change a column’s type with change_column. That call is irreversible in change; supply up/down (or reversible) when you need a safe rollback path.
Indexes, foreign keys, and renames have their own helpers. Read the guide’s method list when the change is not a plain column add. Keep data-only transforms out of schema migrations when you can; the guide points at seeds and separate data-migration practice for loading or rewriting rows.
§IV.b — Foreign keys and references (brief)
t.references :user (or add_reference) adds the usual user_id column and can add a foreign key when you ask for it. Database foreign keys and unique indexes complement model validations: they stop writers that never touch your Active Record callbacks. Associations as a teaching topic stay on the Associations Guides page; here you only need to know migrations are where the column and constraint appear.
§V — Running and rolling back
Apply pending migrations:
bin/rails db:migrate
That runs each pending change (or up) in timestamp order and then dumps the schema. Target a version with VERSION=... when you need to move to a specific point on the timeline.
Roll back the latest step:
bin/rails db:rollback
Roll back several with STEP=3. db:migrate:redo rolls back and re-applies for a local fix cycle. On databases that support DDL transactions, a failed migration can abort as a unit; some statements still sit outside transactions, and the guide shows disable_ddl_transaction! when you need that escape.
Environment matters: default is development. Pass RAILS_ENV=... when the target is test or production.
bin/rails db:prepare is the practical bootstrap for an empty machine: create the database if needed, load the schema or run migrations, and load seeds when the guide’s conditions say so. Prefer it in setup scripts; keep db:migrate as the everyday “apply what is pending” command once the database exists.
Rails records applied versions in the schema_migrations table. The timestamp in the filename is the version id.
§VI — schema.rb and the source of truth
After migrate, Active Record updates db/schema.rb to match the current database structure. Migrations are the history of how you got here. The database is the live source of truth for what exists now. The schema dump is the fast way to build a new database without replaying every old migration.
Default dump format is Ruby (schema.rb). Set the format to SQL when you rely on database-specific features the Ruby dumper cannot express. Commit the schema file. When two branches conflict in it, migrate to regenerate a clean dump rather than hand-merging forever.
Old migrations can be removed only with care once they have shipped widely; the guide’s “Old Migrations” section covers the trap of deleting files whose versions still sit in schema_migrations. Prefer keeping engine-installed migrations idempotent and present.
§VII — Referential integrity (short)
Active Record likes intelligence in the model layer. Foreign key constraints and unique indexes at the database remain good complements. Use migration helpers to add them when the team wants the database to enforce what the models already assume. Validations alone do not stop every writer that reaches the database.
§VIII — One complete proof
- Generate
AddPartNumberToProducts part_number:string:index(or an equivalent add-column migration on a table you already have). - Run
bin/rails db:migrateand confirmdb/schema.rbpicked up the column and index. - Run
bin/rails db:rollbackand confirm the reverse. - Say aloud: migrations are the timeline; the database is truth;
schema.rbis the dump for new environments.
When those four hold, this Guides page’s selected depth is done.
§IX — Closing
Migrations version the schema in Ruby so every environment can replay the same steps. Generators write the files; change describes the forward edit; db:migrate / db:rollback move along the timeline; schema.rb dumps the result for quick setup. Session 02’s CRUD sits on tables this machinery creates and evolves.
Done-criteria: generate a migration, run migrate and rollback, and state what schema.rb is for.