README.md in strong_migrations-0.6.3 vs README.md in strong_migrations-0.6.4

- old
+ new

@@ -1,9 +1,11 @@ # Strong Migrations Catch unsafe migrations in development +&nbsp;&nbsp;✓&nbsp;&nbsp;Detects potentially dangerous operations<br />&nbsp;&nbsp;✓&nbsp;&nbsp;Prevents them from running by default<br />&nbsp;&nbsp;✓&nbsp;&nbsp;Provides instructions on safer ways to do what you want + Supports for PostgreSQL, MySQL, and MariaDB :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource) [![Build Status](https://travis-ci.org/ankane/strong_migrations.svg?branch=master)](https://travis-ci.org/ankane/strong_migrations) @@ -14,42 +16,40 @@ ```ruby gem 'strong_migrations' ``` -## How It Works +We highly recommend [setting timeouts](#timeouts). You can [mark existing migrations as safe](#existing-migrations) as well. -Strong Migrations detects potentially dangerous operations in migrations, prevents them from running by default, and provides instructions on safer ways to do what you want. +## Checks -![Screenshot](https://ankane.org/images/strong-migrations.png) +Potentially dangerous operations: -## Dangerous Operations +- [removing a column](#removing-a-column) +- [adding a column with a default value](#adding-a-column-with-a-default-value) +- [backfilling data](#backfilling-data) +- [changing the type of a column](#renaming-or-changing-the-type-of-a-column) +- [renaming a column](#renaming-or-changing-the-type-of-a-column) +- [renaming a table](#renaming-a-table) +- [creating a table with the force option](#creating-a-table-with-the-force-option) +- [using change_column_null with a default value](#using-change_column_null-with-a-default-value) +- [executing SQL directly](#executing-SQL-directly) -The following operations can cause downtime or errors: +Postgres-specific checks: -- [[+]](#removing-a-column) removing a column -- [[+]](#adding-a-column-with-a-default-value) adding a column with a default value -- [[+]](#backfilling-data) backfilling data -- [[+]](#adding-an-index) adding an index non-concurrently -- [[+]](#adding-a-reference) adding a reference -- [[+]](#adding-a-foreign-key) adding a foreign key -- [[+]](#renaming-or-changing-the-type-of-a-column) changing the type of a column -- [[+]](#renaming-or-changing-the-type-of-a-column) renaming a column -- [[+]](#renaming-a-table) renaming a table -- [[+]](#creating-a-table-with-the-force-option) creating a table with the `force` option -- [[+]](#setting-not-null-on-an-existing-column) setting `NOT NULL` on an existing column -- [[+]](#adding-a-json-column) adding a `json` column +- [adding an index non-concurrently](#adding-an-index) +- [removing an index non-concurrently](#removing-an-index) +- [adding a reference](#adding-a-reference) +- [adding a foreign key](#adding-a-foreign-key) +- [adding a json column](#adding-a-json-column) +- [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column) -Optional checks: - -- [[+]](#removing-an-index) removing an index non-concurrently - Best practices: -- [[+]](#keeping-non-unique-indexes-to-three-columns-or-less) keeping non-unique indexes to three columns or less +- [keeping non-unique indexes to three columns or less](#keeping-non-unique-indexes-to-three-columns-or-less) -## The Zero Downtime Way +You can also add [custom checks](#custom-checks) or [disable specific checks](#disable-checks). ### Removing a column #### Bad @@ -86,11 +86,11 @@ 4. Deploy and run migration ### Adding a column with a default value -Note: This operation is safe in Postgres 11+, MySQL 8.0.12+, and MariaDB 10.3.2+ +Note: This operation is safe in Postgres 11+, MySQL 8.0.12+, and MariaDB 10.3.2+. #### Bad Adding a column with a default value to an existing table causes the entire table to be rewritten. @@ -153,304 +153,314 @@ end end end ``` -### Adding an index +### Renaming or changing the type of a column #### Bad -In Postgres, adding an index non-concurrently locks the table. - ```ruby -class AddSomeIndexToUsers < ActiveRecord::Migration[6.0] +class RenameSomeColumn < ActiveRecord::Migration[6.0] def change - add_index :users, :some_column + rename_column :users, :some_column, :new_name end end ``` -#### Good +or -Add indexes concurrently. - ```ruby -class AddSomeIndexToUsers < ActiveRecord::Migration[6.0] - disable_ddl_transaction! - +class ChangeSomeColumnType < ActiveRecord::Migration[6.0] def change - add_index :users, :some_column, algorithm: :concurrently + change_column :users, :some_column, :new_type end end ``` -If you forget `disable_ddl_transaction!`, the migration will fail. Also, note that indexes on new tables (those created in the same migration) don’t require this. +A few changes are safe in Postgres: -With [gindex](https://github.com/ankane/gindex), you can generate an index migration instantly with: +- Changing between `varchar` and `text` columns +- Increasing the precision of a `decimal` or `numeric` column +- Making a `decimal` or `numeric` column unconstrained +- Changing between `timestamp` and `timestamptz` columns when session time zone is UTC in Postgres 12+ -```sh -rails g index table column -``` +And a few in MySQL and MariaDB: -### Adding a reference +- Increasing the length of a `varchar` column from under 255 up to 255 +- Increasing the length of a `varchar` column over 255 -#### Bad +#### Good -Rails adds an index non-concurrently to references by default, which is problematic for Postgres. +A safer approach is 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 + +#### Bad + ```ruby -class AddReferenceToUsers < ActiveRecord::Migration[6.0] +class RenameUsersToCustomers < ActiveRecord::Migration[6.0] def change - add_reference :users, :city + rename_table :users, :customers end end ``` #### Good -Make sure the index is added concurrently. +A safer approach is to: -```ruby -class AddReferenceToUsers < ActiveRecord::Migration[6.0] - disable_ddl_transaction! +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 - def change - add_reference :users, :city, index: {algorithm: :concurrently} - end -end -``` +### Creating a table with the force option -### Adding a foreign key - #### Bad -In Postgres, new foreign keys are validated by default, which acquires a `ShareRowExclusiveLock` that can be [expensive on large tables](https://travisofthenorth.com/blog/2017/2/2/postgres-adding-foreign-keys-with-zero-downtime). +The `force` option can drop an existing table. ```ruby -class AddForeignKeyOnUsers < ActiveRecord::Migration[6.0] +class CreateUsers < ActiveRecord::Migration[6.0] def change - add_foreign_key :users, :orders + create_table :users, force: true do |t| + # ... + end end end ``` #### Good -Instead, validate it in a separate migration with a more agreeable `RowShareLock`. This approach is documented by Postgres to have “[the least impact on other work](https://www.postgresql.org/docs/current/sql-altertable.html).” +Create tables without the `force` option. -For Rails 5.2+, use: - ```ruby -class AddForeignKeyOnUsers < ActiveRecord::Migration[6.0] +class CreateUsers < ActiveRecord::Migration[6.0] def change - add_foreign_key :users, :orders, validate: false + create_table :users do |t| + # ... + end end end ``` -Then validate it in a separate migration. +### Using change_column_null with a default value +#### Bad + +This generates a single `UPDATE` statement to set the default value. + ```ruby -class ValidateForeignKeyOnUsers < ActiveRecord::Migration[6.0] +class ChangeSomeColumnNull < ActiveRecord::Migration[6.0] def change - validate_foreign_key :users, :orders + change_column_null :users, :some_column, false, "default_value" end end ``` -For Rails < 5.2, use: +#### Good +Backfill the column [safely](#backfilling-data). Then use: + ```ruby -class AddForeignKeyOnUsers < ActiveRecord::Migration[5.1] +class ChangeSomeColumnNull < ActiveRecord::Migration[6.0] def change - safety_assured do - execute 'ALTER TABLE "users" ADD CONSTRAINT "fk_rails_c1e9b98e31" FOREIGN KEY ("order_id") REFERENCES "orders" ("id") NOT VALID' - end + change_column_null :users, :some_column, false end end ``` -Then validate it in a separate migration. +Note: In Postgres, `change_column_null` is still [not safe](#setting-not-null-on-an-existing-column) with this method. +### Executing SQL directly + +Strong Migrations can’t ensure safety for raw SQL statements. Make really sure that what you’re doing is safe, then use: + ```ruby -class ValidateForeignKeyOnUsers < ActiveRecord::Migration[5.1] +class ExecuteSQL < ActiveRecord::Migration[6.0] def change - safety_assured do - execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "fk_rails_c1e9b98e31"' - end + safety_assured { execute "..." } end end ``` -### Renaming or changing the type of a column +### Adding an index #### Bad +In Postgres, adding an index non-concurrently locks the table. + ```ruby -class RenameSomeColumn < ActiveRecord::Migration[6.0] +class AddSomeIndexToUsers < ActiveRecord::Migration[6.0] def change - rename_column :users, :some_column, :new_name + add_index :users, :some_column end end ``` -or +#### Good +Add indexes concurrently. + ```ruby -class ChangeSomeColumnType < ActiveRecord::Migration[6.0] +class AddSomeIndexToUsers < ActiveRecord::Migration[6.0] + disable_ddl_transaction! + def change - change_column :users, :some_column, :new_type + add_index :users, :some_column, algorithm: :concurrently end end ``` -A few changes are safe in Postgres: +If you forget `disable_ddl_transaction!`, the migration will fail. Also, note that indexes on new tables (those created in the same migration) don’t require this. -- Changing between `varchar` and `text` columns -- Increasing the precision of a `decimal` or `numeric` column -- Making a `decimal` or `numeric` column unconstrained -- Changing between `timestamp` and `timestamptz` columns when session time zone is UTC in Postgres 12+ +With [gindex](https://github.com/ankane/gindex), you can generate an index migration instantly with: -And a few in MySQL and MariaDB: +```sh +rails g index table column +``` -- Increasing the length of a `varchar` column from under 255 up to 255 -- Increasing the length of a `varchar` column over 255 +### Removing an index -#### Good +Note: This check is [opt-in](#opt-in-checks). -A safer approach is 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 - #### Bad +In Postgres, removing an index non-concurrently locks the table for a brief period. + ```ruby -class RenameUsersToCustomers < ActiveRecord::Migration[6.0] +class RemoveSomeIndexFromUsers < ActiveRecord::Migration[6.0] def change - rename_table :users, :customers + remove_index :users, :some_column end end ``` #### Good -A safer approach is to: +Remove indexes concurrently. -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 +```ruby +class RemoveSomeIndexFromUsers < ActiveRecord::Migration[6.0] + disable_ddl_transaction! -### Creating a table with the force option + def change + remove_index :users, column: :some_column, algorithm: :concurrently + end +end +``` +### Adding a reference + #### Bad -The `force` option can drop an existing table. +Rails adds an index non-concurrently to references by default, which is problematic for Postgres. ```ruby -class CreateUsers < ActiveRecord::Migration[6.0] +class AddReferenceToUsers < ActiveRecord::Migration[6.0] def change - create_table :users, force: true do |t| - # ... - end + add_reference :users, :city end end ``` #### Good -Create tables without the `force` option. +Make sure the index is added concurrently. ```ruby -class CreateUsers < ActiveRecord::Migration[6.0] +class AddReferenceToUsers < ActiveRecord::Migration[6.0] + disable_ddl_transaction! + def change - create_table :users do |t| - # ... - end + add_reference :users, :city, index: {algorithm: :concurrently} end end ``` -### Setting `NOT NULL` on an existing column +### Adding a foreign key #### Bad -In Postgres, setting `NOT NULL` on an existing column requires an `AccessExclusiveLock`, which is expensive on large tables. +In Postgres, new foreign keys are validated by default, which acquires a `ShareRowExclusiveLock` that can be [expensive on large tables](https://travisofthenorth.com/blog/2017/2/2/postgres-adding-foreign-keys-with-zero-downtime). ```ruby -class SetSomeColumnNotNull < ActiveRecord::Migration[6.0] +class AddForeignKeyOnUsers < ActiveRecord::Migration[6.0] def change - change_column_null :users, :some_column, false + add_foreign_key :users, :orders end end ``` +or + +```ruby +class AddReferenceToUsers < ActiveRecord::Migration[6.0] + def change + add_reference :users, :order, foreign_key: true + end +end +``` + #### Good -Instead, add a constraint: +Instead, validate it in a separate migration with a more agreeable `RowShareLock`. This approach is documented by Postgres to have “[the least impact on other work](https://www.postgresql.org/docs/current/sql-altertable.html).” +For Rails 5.2+, use: + ```ruby -class SetSomeColumnNotNull < ActiveRecord::Migration[6.0] +class AddForeignKeyOnUsers < ActiveRecord::Migration[6.0] def change - safety_assured do - execute 'ALTER TABLE "users" ADD CONSTRAINT "users_some_column_null" CHECK ("some_column" IS NOT NULL) NOT VALID' - end + add_foreign_key :users, :orders, validate: false end end ``` Then validate it in a separate migration. ```ruby -class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0] +class ValidateForeignKeyOnUsers < ActiveRecord::Migration[6.0] def change - safety_assured do - execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "users_some_column_null"' - end + validate_foreign_key :users, :orders end end ``` -Note: This is not 100% the same as `NOT NULL` column constraint. Here’s a [good explanation](https://medium.com/doctolib/adding-a-not-null-constraint-on-pg-faster-with-minimal-locking-38b2c00c4d1c). +For Rails < 5.2, use: -### Using change_column_null with a default value - -#### Bad - -This generates a single `UPDATE` statement to set the default value. - ```ruby -class ChangeSomeColumnNull < ActiveRecord::Migration[6.0] +class AddForeignKeyOnUsers < ActiveRecord::Migration[5.1] def change - change_column_null :users, :some_column, false, "default_value" + safety_assured do + execute 'ALTER TABLE "users" ADD CONSTRAINT "fk_rails_c1e9b98e31" FOREIGN KEY ("order_id") REFERENCES "orders" ("id") NOT VALID' + end end end ``` -#### Good +Then validate it in a separate migration. -Backfill the column [safely](#backfilling-data). Then use: - ```ruby -class ChangeSomeColumnNull < ActiveRecord::Migration[6.0] +class ValidateForeignKeyOnUsers < ActiveRecord::Migration[5.1] def change - change_column_null :users, :some_column, false + safety_assured do + execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "fk_rails_c1e9b98e31"' + end end end ``` -Note: In Postgres, `change_column_null` is still [not safe](#setting-not-null-on-an-existing-column) with this method. - ### Adding a json column #### Bad In Postgres, there’s no equality operator for the `json` column type, which can cause errors for existing `SELECT DISTINCT` queries. @@ -473,53 +483,51 @@ add_column :users, :properties, :jsonb end end ``` -## Optional Checks +### Setting NOT NULL on an existing column -Some operations rarely cause issues in practice, but can be checked if desired. Enable checks with: +#### Bad -```ruby -StrongMigrations.enable_check(:remove_index) -``` +In Postgres, setting `NOT NULL` on an existing column requires an `AccessExclusiveLock`, which is expensive on large tables. -To start a check only after a specific migration, use: - ```ruby -StrongMigrations.enable_check(:remove_index, start_after: 20170101000000) +class SetSomeColumnNotNull < ActiveRecord::Migration[6.0] + def change + change_column_null :users, :some_column, false + end +end ``` -### Removing an index +#### Good -#### Bad +Instead, add a constraint: -In Postgres, removing an index non-concurrently locks the table for a brief period. - ```ruby -class RemoveSomeIndexFromUsers < ActiveRecord::Migration[6.0] +class SetSomeColumnNotNull < ActiveRecord::Migration[6.0] def change - remove_index :users, :some_column + safety_assured do + execute 'ALTER TABLE "users" ADD CONSTRAINT "users_some_column_null" CHECK ("some_column" IS NOT NULL) NOT VALID' + end end end ``` -#### Good +Then validate it in a separate migration. -Remove indexes concurrently. - ```ruby -class RemoveSomeIndexFromUsers < ActiveRecord::Migration[6.0] - disable_ddl_transaction! - +class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0] def change - remove_index :users, column: :some_column, algorithm: :concurrently + safety_assured do + execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "users_some_column_null"' + end end end ``` -## Best Practices +Note: This is not 100% the same as `NOT NULL` column constraint. Here’s a [good explanation](https://medium.com/doctolib/adding-a-not-null-constraint-on-pg-faster-with-minimal-locking-38b2c00c4d1c). ### Keeping non-unique indexes to three columns or less #### Bad @@ -575,103 +583,117 @@ Use the `stop!` method to stop migrations. Note: Since `remove_column` always requires a `safety_assured` block, it’s not possible to add a custom check for `remove_column` operations. +## Opt-in Checks + +Some operations rarely cause issues in practice, but can be checked if desired. Enable checks with: + +```ruby +StrongMigrations.enable_check(:remove_index) +``` + +To start a check only after a specific migration, use: + +```ruby +StrongMigrations.enable_check(:remove_index, start_after: 20170101000000) +``` + ## Disable Checks Disable specific checks with: ```ruby StrongMigrations.disable_check(:add_index) ``` Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb) for the list of keys. -## Existing Migrations +## Custom Messages -To mark migrations as safe that were created before installing this gem, create an initializer with: +To customize specific messages, create an initializer with: ```ruby -StrongMigrations.start_after = 20170101000000 +StrongMigrations.error_messages[:add_column_default] = "Your custom instructions" ``` -Use the version from your latest migration. +Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb) for the list of keys. -## Dangerous Tasks +## Timeouts -For safety, dangerous database tasks are disabled in production - `db:drop`, `db:reset`, `db:schema:load`, and `db:structure:load`. To get around this, use: +It’s a good idea to set a long statement timeout and a short lock timeout for migrations. This way, migrations can run for a while, and if a migration can’t acquire a lock in a timely manner, other statements won’t be stuck behind it. -```sh -SAFETY_ASSURED=1 rails db:drop +Create `config/initializers/strong_migrations.rb` with: + +```ruby +StrongMigrations.statement_timeout = 1.hour +StrongMigrations.lock_timeout = 10.seconds ``` -## Faster Migrations +Or set the timeouts directly on the database user that runs migrations. For Postgres, use: -Only dump the schema when adding a new migration. If you use Git, create an initializer with: - -```ruby -ActiveRecord::Base.dump_schema_after_migration = Rails.env.development? && - `git status db/migrate/ --porcelain`.present? +```sql +ALTER ROLE myuser SET statement_timeout = '1h'; +ALTER ROLE myuser SET lock_timeout = '10s'; ``` -## Schema Sanity +Note: If you use PgBouncer in transaction mode, you must set timeouts on the database user. -Columns can flip order in `db/schema.rb` when you have multiple developers. One way to prevent this is to [alphabetize them](https://www.pgrs.net/2008/03/12/alphabetize-schema-rb-columns/). Add to the end of your `Rakefile`: +## Existing Migrations +To mark migrations as safe that were created before installing this gem, create an initializer with: + ```ruby -task "db:schema:dump": "strong_migrations:alphabetize_columns" +StrongMigrations.start_after = 20170101000000 ``` -## Custom Messages +Use the version from your latest migration. -To customize specific messages, create an initializer with: +## Target Version +If your development database version is different from production, you can specify the production version so the right checks are run in development. + ```ruby -StrongMigrations.error_messages[:add_column_default] = "Your custom instructions" +StrongMigrations.target_postgresql_version = "10" +StrongMigrations.target_mysql_version = "8.0.12" +StrongMigrations.target_mariadb_version = "10.3.2" ``` -Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb) for the list of keys. +For safety, this option only affects development and test environments. In other environments, the actual server version is always used. ## Analyze Tables Analyze tables automatically (to update planner statistics) after an index is added. Create an initializer with: ```ruby StrongMigrations.auto_analyze = true ``` -## Target Version +## Faster Migrations -If your development database version is different from production, you can specify the production version so the right checks are run in development. +Only dump the schema when adding a new migration. If you use Git, create an initializer with: ```ruby -StrongMigrations.target_postgresql_version = "10" -StrongMigrations.target_mysql_version = "8.0.12" -StrongMigrations.target_mariadb_version = "10.3.2" +ActiveRecord::Base.dump_schema_after_migration = Rails.env.development? && + `git status db/migrate/ --porcelain`.present? ``` -For safety, this option only affects development and test environments. In other environments, the actual server version is always used. +## Schema Sanity -## Timeouts +Columns can flip order in `db/schema.rb` when you have multiple developers. One way to prevent this is to [alphabetize them](https://www.pgrs.net/2008/03/12/alphabetize-schema-rb-columns/). Add to the end of your `Rakefile`: -It’s a good idea to set a long statement timeout and a short lock timeout for migrations. This way, migrations can run for a while, and if a migration can’t acquire a lock in a timely manner, other statements won’t be stuck behind it. - -You can use: - ```ruby -StrongMigrations.statement_timeout = 1.hour -StrongMigrations.lock_timeout = 10.seconds +task "db:schema:dump": "strong_migrations:alphabetize_columns" ``` -Or set the timeouts directly on the database user that runs migrations. For Postgres, use: +## Dangerous Tasks -```sql -ALTER ROLE myuser SET statement_timeout = '1h'; -ALTER ROLE myuser SET lock_timeout = '10s'; -``` +For safety, dangerous database tasks are disabled in production - `db:drop`, `db:reset`, `db:schema:load`, and `db:structure:load`. To get around this, use: -Note: If you use PgBouncer in transaction mode, you must set timeouts on the database user. +```sh +SAFETY_ASSURED=1 rails db:drop +``` ## Permissions We recommend using a [separate database user](https://ankane.org/postgres-users) for migrations when possible so you don’t need to grant your app user permission to alter tables.