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"
```