rails-migrations
Skill2 starsChanged 8 days agoMITView on GitHub
Use when writing, reviewing or running a database migration: adding, removing, renaming or retyping columns, tables or indexes, or backfilling data, above all on tables with production data that must stay online (zero-downtime, strong_migrations, concurrent indexes).
1 other project uses a version of this.
Adopted by
Most recently taken up by
- marckohlbrugge/unofficial-37signals-coding-style-guidetheir own version, first seen Oct 8, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- marckohlbrugge/unofficial-37signals-coding-style-guide★ 724same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository24 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- rails-activestorageSkill
- rails-apiSkill
- rails-auditSkill
- rails-clean-test-outputSkill
- rails-controllersSkill
- rails-coreSkill
- rails-database-performanceSkill
- rails-i18nSkill
- rails-jobsSkill
- rails-loggingSkill
- rails-mailersSkill
- rails-modelsSkill
- rails-multi-tenancySkill
- rails-performanceSkill
- rails-philosophySkill
- rails-project-setupSkill
- rails-securitySkill
- rails-stimulusSkill
- rails-styleSkill
- rails-testingSkill
- rails-turboSkill
- rails-upgradeSkill
- rails-viewcomponentsSkill
The file
We split this skill into the rules it gives an agent
10 rules. About 0 are common, 4 are distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 10
Start with [[rails-core]] rule 5 (multi-step nullable columns) and rule 6 (multi-DB rollback) — this skill extends those.
- Very specific: line 25
When strong_migrations raises, it tells you the safe alternative.
- Very specific: line 90
Never use rename_column directly on a live table.
- Very specific: line 104
Never call Model.all.each { |r| r.update(...) } inside a schema migration:
- Very specific: line 123
Option C — inline
update_all: only when the table is new or empty (no rows to iterate), or a single update_all without looping is … - Very specific: line 128
Use update_all in batches — never .each { |r| r.update }, which triggers callbacks and N+1 saves:
- Distinctive: line 139
Add a new column of the desired type (nullable).
- Distinctive: line 140
Write to both the old and new columns.
- Distinctive: line 143
Remove the old column (Pattern 4).
- Very specific: line 151
Always define down or use the reversible block so db:rollback works cleanly in development and CI:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Safe Rails Migrations
Rails migrations are easy to write and easy to break production with. Schema changes on populated tables can lock rows or the whole table for minutes — enough to time out requests and trigger an incident. These patterns work on PostgreSQL (the common production default); MySQL equivalents are noted where they differ.
Start with [[rails-core]] rule 5 (multi-step nullable columns) and rule 6 (multi-DB rollback) — this skill extends those.
1. strong_migrations — catch unsafe operations before they ship
Install strong_migrations to auto-detect dangerous migration operations at startup. It raises in development and CI for any migration that would lock a production table.
# Gemfile
gem "strong_migrations"After install, run bin/rails generate strong_migrations:install — it creates config/initializers/strong_migrations.rb where you can configure the production table-size threshold (default 10 MB). Operations it catches: adding a column with a non-null default (on older stacks), adding a non-concurrent index, changing a column type, renaming a column or table, and several others.
When strong_migrations raises, it tells you the safe alternative. Read the message before adding a safety_assured { } bypass — the bypass is for cases you've genuinely reasoned through, not for making the error go away.
2. Adding a column with a default value
On PostgreSQL 11+: adding a NOT NULL column with a constant (non-volatile) default: is safe — PostgreSQL 11 stores the default in the catalog instead of rewriting the table, so the addition is instant. Add it directly. A volatile default (default: -> { "gen_random_uuid()" }, clock_timestamp(), …) is evaluated per row and still rewrites the table.
Older stacks, or a volatile default: the column addition rewrites the table. Use the multi-step pattern from [[rails-core]] rule 5:
# Migration 1: add as nullable, no default
add_column :users, :timezone, :string
# Run a backfill (see Pattern 6), then:
# Migration 2: add the constraint
change_column_null :users, :timezone, false, "UTC"3. Adding an index — always concurrent on PostgreSQL
A standard index creation locks the table for writes. On PostgreSQL, always use algorithm: :concurrently. Concurrent index creation cannot run inside a transaction, so disable it for that migration:
class AddIndexToUsersEmail < ActiveRecord::Migration[8.1]
disable_ddl_transaction!
def change
add_index :users, :email, algorithm: :concurrently, if_not_exists: true
end
endstrong_migrations will raise if you omit algorithm: :concurrently on a non-tiny table. One index per migration file when using disable_ddl_transaction! — concurrent operations need a separate connection state per index.
MySQL uses ALGORITHM=INPLACE, LOCK=NONE for online DDL; check the strong_migrations docs for the MySQL-specific safe alternatives.
4. Removing a column — ignore it first
Active Record caches the column list at boot. Dropping a column the running application still references causes ActiveModel::MissingAttributeError until the app restarts. Safe sequence requires two deploys:
Deploy 1 — tell Active Record to ignore the column:
class User < ApplicationRecord
self.ignored_columns += ["old_column"]
endDeploy 2 — drop it after all running processes have the new code:
remove_column :users, :old_column
# Also remove the ignored_columns entrystrong_migrations will warn you if you try to drop a column that isn't already in ignored_columns.
5. Renaming a column — dual-write sequence
Never use rename_column directly on a live table; the running app will reference the old name and break immediately. Safe pattern (four to five steps across deploys):
- Add the new column (nullable).
- Write to both — update the model and all writes to set both old and new columns.
- Backfill the new column for all existing rows (see Pattern 6).
- Switch reads to the new column; remove dual writes.
- Drop the old column (see Pattern 4 — use
ignored_columnsfirst).
strong_migrations guides you through this with a rename_column_safely suggestion when it raises.
6. Data backfills — never in a schema migration
Never call Model.all.each { |r| r.update(...) } inside a schema migration:
- It holds the migration open (and its transaction) while iterating potentially millions of rows.
- Any application error rolls back the schema change along with the data.
- It loads every record into memory.
Option A — data-migrate gem (separate data migration):
# db/data/20250101000000_backfill_user_timezone.rb
class BackfillUserTimezone < ActiveRecord::DataMigration
def up
User.in_batches(of: 1_000) do |batch|
batch.update_all(timezone: "UTC")
end
end
endOption B — maintenance_tasks gem for long-running or resumable backfills (minutes to hours, needs monitoring or re-queueing on failure).
Option C — inline update_all only when the table is new or empty (no rows to iterate), or a single update_all without looping is genuinely safe:
User.where(timezone: nil).update_all(timezone: "UTC")Use update_all in batches — never .each { |r| r.update }, which triggers callbacks and N+1 saves:
User.in_batches(of: 1_000) { |b| b.update_all(timezone: "UTC") }7. Changing a column type
change_column almost always rewrites the table on PostgreSQL. Safe zero-downtime pattern:
- Add a new column of the desired type (nullable).
- Write to both the old and new columns.
- Backfill the new column (Pattern 6).
- Switch reads to the new column.
- Remove the old column (Pattern 4).
strong_migrations blocks change_column on non-trivially-sized tables and points you here.
8. Reversibility
Always define down or use the reversible block so db:rollback works cleanly in development and CI:
def change
reversible do |dir|
dir.up { add_column :users, :timezone, :string }
dir.down { remove_column :users, :timezone }
end
endFor data migrations, define down to undo the backfill, or explicitly raise ActiveRecord::IrreversibleMigration with a comment explaining why reversal isn't safe.
Quick Reference
| Situation | Pattern |
| Add nullable column | Direct add_column — always safe |
| Add NOT NULL with constant default (PostgreSQL 11+) | Direct — safe |
| Add NOT NULL with default (older stack, or volatile default) | Nullable → backfill → add constraint (two migrations) |
| Add index on large table | disable_ddl_transaction! + algorithm: :concurrently |
| Remove a column | ignored_columns first, drop in next deploy |
| Rename a column | Dual-write sequence (4–5 steps) |
| Bulk backfill | in_batches with update_all, not .each { update } |
| Change column type | New column → dual-write → backfill → switch → drop |
| Auto-catch unsafe ops | strong_migrations gem |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

