README.md in strong_migrations-0.4.2 vs README.md in strong_migrations-0.5.0

- old
+ new

@@ -32,15 +32,19 @@ - [[+]](#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 -- [[+]](#using-change_column_null-with-a-default-value) using `change_column_null` with a default value +- [[+]](#using-change_column_null) using `change_column_null` - [[+]](#adding-a-json-column) adding a `json` column -Also checks for best practices: +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 ## The Zero Downtime Way ### Removing a column @@ -80,10 +84,12 @@ 4. Deploy and run migration ### Adding a column with a default value +Note: This operation is safe in Postgres 11+. + #### Bad Adding a column with a default value to an existing table causes the entire table to be rewritten. ```ruby @@ -92,12 +98,10 @@ add_column :users, :some_column, :text, default: "default_value" end end ``` -> This operation is safe in Postgres 11+ - #### Good Instead, add the column without a default value, then change the default. ```ruby @@ -151,11 +155,11 @@ ### Adding an index #### Bad -In Postgres, adding a non-concurrent index locks the table. +In Postgres, adding an index non-concurrently locks the table. ```ruby class AddSomeIndexToUsers < ActiveRecord::Migration[6.0] def change add_index :users, :some_column @@ -181,11 +185,11 @@ ### Adding a reference #### Bad -Rails adds a non-concurrent index to references by default, which is problematic for Postgres. +Rails adds an index non-concurrently to references by default, which is problematic for Postgres. ```ruby class AddReferenceToUsers < ActiveRecord::Migration[6.0] def change add_reference :users, :city @@ -205,17 +209,15 @@ add_reference :users, :city, index: {algorithm: :concurrently} end end ``` -For polymorphic references, add a compound index on type and id. - ### Adding a foreign key #### Bad -In Postgres, new foreign keys are validated by default, which acquires an `AccessExclusiveLock` that can be [expensive on large tables](https://travisofthenorth.com/blog/2017/2/2/postgres-adding-foreign-keys-with-zero-downtime). +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 AddForeignKeyOnUsers < ActiveRecord::Migration[6.0] def change add_foreign_key :users, :orders @@ -357,14 +359,56 @@ end end end ``` -### Using change_column_null with a default value +### Using change_column_null #### Bad +In Postgres, setting `NOT NULL` on an existing column requires an `AccessExclusiveLock`, which is expensive on large tables. + +```ruby +class SetSomeColumnNotNull < ActiveRecord::Migration[6.0] + def change + change_column_null :users, :some_column, false + end +end +``` + +#### Good + +Instead, add a constraint: + +```ruby +class SetSomeColumnNotNull < 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 + end +end +``` + +Then validate it in a separate migration. + +```ruby +class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0] + def change + safety_assured do + execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "users_some_column_null"' + end + 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). + +### Using change_column_null with a default value (non-Postgres) + +#### Bad + This generates a single `UPDATE` statement to set the default value. ```ruby class ChangeSomeColumnNull < ActiveRecord::Migration[6.0] def change @@ -383,15 +427,17 @@ change_column_null :users, :some_column, false end end ``` +Note: In Postgres, `change_column_null` is still [not safe](#using-change_column_null) with this method. + ### Adding a json column #### Bad -In Postgres, there’s no equality operator for the `json` column type, which causes issues for `SELECT DISTINCT` queries. +In Postgres, there’s no equality operator for the `json` column type, which can cause errors for existing `SELECT DISTINCT` queries. ```ruby class AddPropertiesToUsers < ActiveRecord::Migration[6.0] def change add_column :users, :properties, :json @@ -409,10 +455,52 @@ add_column :users, :properties, :jsonb end end ``` +## Optional 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) +``` + +### Removing an index + +#### Bad + +In Postgres, removing an index non-concurrently locks the table for a brief period. + +```ruby +class RemoveSomeIndexFromUsers < ActiveRecord::Migration[6.0] + def change + remove_index :users, :some_column + end +end +``` + +#### Good + +Remove indexes concurrently. + +```ruby +class RemoveSomeIndexFromUsers < ActiveRecord::Migration[6.0] + disable_ddl_transaction! + + def change + remove_index :users, column: :some_column, algorithm: :concurrently + end +end +``` + ## Best Practices ### Keeping non-unique indexes to three columns or less #### Bad @@ -437,11 +525,11 @@ add_index :users, [:b, :d] end end ``` -> For Postgres, be sure to add them concurrently +For Postgres, be sure to add them concurrently. ## Assuring Safety To mark a step in the migration as safe, despite using a method that might otherwise be dangerous, wrap it in a `safety_assured` block. @@ -467,12 +555,22 @@ end ``` Use the `stop!` method to stop migrations. -> Since `remove_column` always requires a `safety_assured` block, it’s not possible to add a custom check for `remove_column` operations +Note: Since `remove_column` always requires a `safety_assured` block, it’s not possible to add a custom check for `remove_column` operations. +## 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 To mark migrations as safe that were created before installing this gem, create an initializer with: ```ruby @@ -498,10 +596,10 @@ `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](https://www.pgrs.net/2008/03/13/alphabetize-schema-rb-columns/). Add to the end of your `Rakefile`: +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`: ```ruby task "db:schema:dump": "strong_migrations:alphabetize_columns" ```