Code Monkey home page Code Monkey logo

strong_migrations's Introduction

Strong Migrations

Catch unsafe migrations at dev time

🍊 Battle-tested at Instacart

Build Status

Installation

Add this line to your application’s Gemfile:

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
  • adding an index non-concurrently (Postgres only)
  • adding a json column to an existing table (Postgres only)

For more info, check out:

Also checks for best practices:

  • keeping indexes to three columns or less

The Zero Downtime Way

Adding a column with a default value

  1. Add the column without a default value
  2. Add the default value
  3. Commit the transaction
  4. Backfill the column
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

    # 4.a (Rails 5+)
    User.in_batches.update_all some_column: "default_value"

    # 4.b (Rails < 5)
    User.find_in_batches do |users|
      User.where(id: users.map(&:id)).update_all some_column: "default_value"
    end
  end

  def down
    remove_column :users, :some_column
  end
end

Renaming or changing the type of a column

If you really have to:

  1. Create a new column
  2. Write to both columns
  3. Backfill data from the old column to the new column
  4. Move reads from the old column to the new column
  5. Stop writing to the old column
  6. Drop the old column

Renaming a table

If you really have to:

  1. Create a new table
  2. Write to both tables
  3. Backfill data from the old table to new table
  4. Move reads from the old table to the new table
  5. Stop writing to the old table
  6. Drop the old table

Removing a column

Tell ActiveRecord to ignore the column from its cache.

# For Rails 5+
class User < ActiveRecord::Base
  self.ignored_columns = %w(some_column)
end

# For Rails < 5
class User < ActiveRecord::Base
  def self.columns
    super.reject { |c| c.name == "some_column" }
  end
end

Once it’s deployed, create a migration to remove the column.

Adding an index (Postgres)

Add indexes concurrently.

class AddSomeIndexToUsers < ActiveRecord::Migration
  def change
    commit_db_transaction
    add_index :users, :some_index, algorithm: :concurrently
  end
end

Adding a json column (Postgres)

There’s no equality operator for the json column type, which causes issues for SELECT DISTINCT queries. Replace all calls to uniq with a custom scope.

scope :uniq_on_id, -> { select("DISTINCT ON (your_table.id) your_table.*") }

Assuring Safety

To mark a step in the migration as safe, despite using method that might otherwise be dangerous, wrap it in a safety_assured block.

class MySafeMigration < ActiveRecord::Migration
  def change
    safety_assured { remove_column :users, :some_column }
  end
end

Existing Migrations

To mark migrations as safe that were created before installing this gem, create an initializer with:

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:

SAFETY_ASSURED=1 rake db:drop

Faster Migrations

Only dump the schema when adding a new migration. If you use Git, create an initializer with:

ActiveRecord::Base.dump_schema_after_migration = Rails.env.development? &&
  `git status db/migrate/ --porcelain`.present?

Schema Sanity

Columns can flip order in db/schema.rb when you have multiple developers. One way to prevent this is to alphabetize them. Add to the end of your Rakefile:

task "db:schema:dump": "strong_migrations:alphabetize_columns"

Analyze Tables (Postgres)

Analyze tables automatically (to update planner statistics) after an index is added. Create an initializer with:

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.

ALTER ROLE myuser SET lock_timeout = '10s';

Credits

Thanks to Bob Remeika and David Waller for the original code.

Contributing

Everyone is encouraged to help improve this project. Here are a few ways you can help:

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.