README.md in strong_migrations-0.1.9 vs README.md in strong_migrations-0.2.0

- old
+ new

@@ -2,11 +2,11 @@ Catch unsafe migrations at dev time :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource) -[![Build Status](https://travis-ci.org/ankane/strong_migrations.svg)](https://travis-ci.org/ankane/strong_migrations) +[![Build Status](https://travis-ci.org/ankane/strong_migrations.svg?branch=master)](https://travis-ci.org/ankane/strong_migrations) ## Installation Add this line to your application’s Gemfile: @@ -14,16 +14,17 @@ gem 'strong_migrations' ``` ## Dangerous Operations +The following operations can cause downtime or errors: + - adding a column with a non-null default value to an existing table - changing the type of a column - renaming a table - renaming a column - removing a column -- executing arbitrary SQL - adding an index non-concurrently (Postgres only) - adding a `json` column to an existing table (Postgres only) For more info, check out: @@ -37,33 +38,33 @@ ## The Zero Downtime Way ### Adding a column with a default value 1. Add the column without a default value -2. Commit the transaction -3. Backfill the column -4. Add the default value +2. Add the default value +3. Commit the transaction - **extremely important if you are backfilling in the migration** +4. Backfill the column ```ruby class AddSomeColumnToUsers < ActiveRecord::Migration def up # 1 add_column :users, :some_column, :text # 2 + change_column_default :users, :some_column, "default_value" + + # 3 commit_db_transaction - # 3.a (Rails 5+) + # 4.a (Rails 5+) User.in_batches.update_all some_column: "default_value" - # 3.b (Rails < 5) + # 4.b (Rails < 5) User.find_in_batches do |users| User.where(id: users.map(&:id)).update_all some_column: "default_value" end - - # 4 - change_column_default :users, :some_column, "default_value" end def down remove_column :users, :some_column end @@ -92,28 +93,41 @@ 5. Stop writing to the old table 6. Drop the old table ### Removing a column -Tell ActiveRecord to ignore the column from its cache. +ActiveRecord caches database columns at runtime, so if you drop a column, it can cause exceptions until your app reboots. To prevent this: -```ruby -# For Rails 5+ -class User < ActiveRecord::Base - self.ignored_columns = %w(some_column) -end +1. Tell ActiveRecord to ignore the column from its cache -# For Rails < 5 -class User < ActiveRecord::Base - def self.columns - super.reject { |c| c.name == "some_column" } + ```ruby + # For Rails 5+ + class User < ApplicationRecord + self.ignored_columns = %w(some_column) end -end -``` -Once it’s deployed, create a migration to remove the column. + # For Rails < 5 + class User < ActiveRecord::Base + def self.columns + super.reject { |c| c.name == "some_column" } + end + end + ``` +2. Deploy code +3. Write a migration to remove the column (wrap in `safety_assured` block) + + ```ruby + class RemoveSomeColumnFromUsers < ActiveRecord::Migration + def change + safety_assured { remove_column :users, :some_column } + end + end + ``` + +4. Deploy and run migration + ### Adding an index (Postgres) Add indexes concurrently. ```ruby @@ -151,10 +165,12 @@ ```ruby StrongMigrations.start_after = 20170101000000 ``` +Use the version from your latest migration. + ## Dangerous Tasks For safety, dangerous rake tasks are disabled in production - `db:drop`, `db:reset`, `db:schema:load`, and `db:structure:load`. To get around this, use: ```sh @@ -176,16 +192,36 @@ ```ruby task "db:schema:dump": "strong_migrations:alphabetize_columns" ``` +## Custom Error Messages + +To customize specific error messages, create an initializer with: + +```ruby +StrongMigrations.error_messages[:add_column_default] = "Your custom instructions" +``` + +Check the source code for the list of keys. + ## Analyze Tables (Postgres) Analyze tables automatically (to update planner statistics) after an index is added. Create an initializer with: ```ruby StrongMigrations.auto_analyze = true ``` + +## Lock Timeout (Postgres) + +It’s a good idea to set a lock timeout for the database user that runs migrations. This way, if migrations can’t acquire a lock in a timely manner, other statements won’t be stuck behind it. + +```sql +ALTER ROLE myuser SET lock_timeout = '10s'; +``` + +There’s also [a gem](https://github.com/gocardless/activerecord-safer_migrations) you can use for this. ## Credits Thanks to Bob Remeika and David Waller for the [original code](https://github.com/foobarfighter/safe-migrations).