Hedronite Lesson · Polyglot-Dev / Rails · Fri 2026-09-11

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.

Lesson Class: Duha (Rails Guides track)
Focus: generators · change DSL · db:migrate · rollback · schema.rb
Code Blocks: clean blocks, explanation in prose
Done-criteria: generate a migration, migrate/rollback, state schema.rb role
Grounding: rails-guides/active_record_migrations.html · VERSION.txt v8.1.3.1 · Flanagan not cited
The timeline
Each migration is a version step Active Record can replay.
The DSL
create_table and add_column replace hand SQL for ordinary edits.
The dump
schema.rb mirrors the live DB for fast new environments.
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

  1. Generate AddPartNumberToProducts part_number:string:index (or an equivalent add-column migration on a table you already have).
  2. Run bin/rails db:migrate and confirm db/schema.rb picked up the column and index.
  3. Run bin/rails db:rollback and confirm the reverse.
  4. Say aloud: migrations are the timeline; the database is truth; schema.rb is 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.

Related