README.md in strong_migrations-0.7.4 vs README.md in strong_migrations-0.7.5

- old
+ new

@@ -41,11 +41,11 @@ self.ignored_columns = ["name"] end Deploy the code, then wrap this step in a safety_assured { ... } block. -class RemoveColumn < ActiveRecord::Migration[6.0] +class RemoveColumn < ActiveRecord::Migration[6.1] def change safety_assured { remove_column :users, :name } end end ``` @@ -64,10 +64,11 @@ - [backfilling data](#backfilling-data) - [changing the type of a column](#changing-the-type-of-a-column) - [renaming 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) +- [adding a check constraint](#adding-a-check-constraint) - [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column) - [executing SQL directly](#executing-SQL-directly) Postgres-specific checks: @@ -87,11 +88,11 @@ #### Bad Active Record caches database columns at runtime, so if you drop a column, it can cause exceptions until your app reboots. ```ruby -class RemoveSomeColumnFromUsers < ActiveRecord::Migration[6.0] +class RemoveSomeColumnFromUsers < ActiveRecord::Migration[6.1] def change remove_column :users, :some_column end end ``` @@ -108,11 +109,11 @@ 2. Deploy code 3. Write a migration to remove the column (wrap in `safety_assured` block) ```ruby - class RemoveSomeColumnFromUsers < ActiveRecord::Migration[6.0] + class RemoveSomeColumnFromUsers < ActiveRecord::Migration[6.1] def change safety_assured { remove_column :users, :some_column } end end ``` @@ -124,11 +125,11 @@ #### Bad In earlier versions of Postgres, MySQL, and MariaDB, adding a column with a default value to an existing table causes the entire table to be rewritten. During this time, reads and writes are blocked in Postgres, and writes are blocked in MySQL and MariaDB. ```ruby -class AddSomeColumnToUsers < ActiveRecord::Migration[6.0] +class AddSomeColumnToUsers < ActiveRecord::Migration[6.1] def change add_column :users, :some_column, :text, default: "default_value" end end ``` @@ -138,11 +139,11 @@ #### Good Instead, add the column without a default value, then change the default. ```ruby -class AddSomeColumnToUsers < ActiveRecord::Migration[6.0] +class AddSomeColumnToUsers < ActiveRecord::Migration[6.1] def up add_column :users, :some_column, :text change_column_default :users, :some_column, "default_value" end @@ -159,11 +160,11 @@ #### Bad Active Record creates a transaction around each migration, and backfilling in the same transaction that alters a table keeps the table locked for the [duration of the backfill](https://wework.github.io/data/2015/11/05/add-columns-with-default-values-to-large-tables-in-rails-postgres/). ```ruby -class AddSomeColumnToUsers < ActiveRecord::Migration[6.0] +class AddSomeColumnToUsers < ActiveRecord::Migration[6.1] def change add_column :users, :some_column, :text User.update_all some_column: "default_value" end end @@ -174,11 +175,11 @@ #### Good There are three keys to backfilling safely: batching, throttling, and running it outside a transaction. Use the Rails console or a separate migration with `disable_ddl_transaction!`. ```ruby -class BackfillSomeColumn < ActiveRecord::Migration[6.0] +class BackfillSomeColumn < ActiveRecord::Migration[6.1] disable_ddl_transaction! def up User.unscoped.in_batches do |relation| relation.update_all some_column: "default_value" @@ -193,11 +194,11 @@ #### Bad Changing the type of a column causes the entire table to be rewritten. During this time, reads and writes are blocked in Postgres, and writes are blocked in MySQL and MariaDB. ```ruby -class ChangeSomeColumnType < ActiveRecord::Migration[6.0] +class ChangeSomeColumnType < ActiveRecord::Migration[6.1] def change change_column :users, :some_column, :new_type end end ``` @@ -232,11 +233,11 @@ #### Bad Renaming a column that’s in use will cause errors in your application. ```ruby -class RenameSomeColumn < ActiveRecord::Migration[6.0] +class RenameSomeColumn < ActiveRecord::Migration[6.1] def change rename_column :users, :some_column, :new_name end end ``` @@ -257,11 +258,11 @@ #### Bad Renaming a table that’s in use will cause errors in your application. ```ruby -class RenameUsersToCustomers < ActiveRecord::Migration[6.0] +class RenameUsersToCustomers < ActiveRecord::Migration[6.1] def change rename_table :users, :customers end end ``` @@ -282,11 +283,11 @@ #### Bad The `force` option can drop an existing table. ```ruby -class CreateUsers < ActiveRecord::Migration[6.0] +class CreateUsers < ActiveRecord::Migration[6.1] def change create_table :users, force: true do |t| # ... end end @@ -296,54 +297,124 @@ #### Good Create tables without the `force` option. ```ruby -class CreateUsers < ActiveRecord::Migration[6.0] +class CreateUsers < ActiveRecord::Migration[6.1] def change create_table :users do |t| # ... end end end ``` If you intend to drop an existing table, run `drop_table` first. +### Adding a check constraint + +:turtle: Safe by default available + +#### Bad + +Adding a check constraint blocks reads and writes in Postgres and blocks writes in MySQL and MariaDB while every row is checked. + +```ruby +class AddCheckConstraint < ActiveRecord::Migration[6.1] + def change + add_check_constraint :users, "price > 0", name: "price_check" + end +end +``` + +#### Good - Postgres + +Add the check constraint without validating existing rows: + +```ruby +class AddCheckConstraint < ActiveRecord::Migration[6.1] + def change + add_check_constraint :users, "price > 0", name: "price_check", validate: false + end +end +``` + +Then validate them in a separate migration. + +```ruby +class ValidateCheckConstraint < ActiveRecord::Migration[6.1] + def change + validate_check_constraint :users, name: "price_check" + end +end +``` + +#### Good - MySQL and MariaDB + +[Let us know](https://github.com/ankane/strong_migrations/issues/new) if you have a safe way to do this (check constraints can be added with `NOT ENFORCED`, but enforcing blocks writes). + ### Setting NOT NULL on an existing column :turtle: Safe by default available #### Bad Setting `NOT NULL` on an existing column blocks reads and writes while every row is checked. ```ruby -class SetSomeColumnNotNull < ActiveRecord::Migration[6.0] +class SetSomeColumnNotNull < ActiveRecord::Migration[6.1] def change change_column_null :users, :some_column, false end end ``` #### Good - Postgres -Instead, add a check constraint: +Instead, add a check constraint. +For Rails 6.1, use: + ```ruby +class SetSomeColumnNotNull < ActiveRecord::Migration[6.1] + def change + add_check_constraint :users, "some_column IS NOT NULL", name: "users_some_column_null", validate: false + end +end +``` + +For Rails < 6.1, use: + +```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. A `NOT NULL` check constraint is [functionally equivalent](https://medium.com/doctolib/adding-a-not-null-constraint-on-pg-faster-with-minimal-locking-38b2c00c4d1c) to setting `NOT NULL` on the column, but it won’t show up in `schema.rb`. In Postgres 12+, once the check constraint is validated, you can safely set `NOT NULL` on the column and drop the check constraint. +Then validate it in a separate migration. A `NOT NULL` check constraint is [functionally equivalent](https://medium.com/doctolib/adding-a-not-null-constraint-on-pg-faster-with-minimal-locking-38b2c00c4d1c) to setting `NOT NULL` on the column (but it won’t show up in `schema.rb` in Rails < 6.1). In Postgres 12+, once the check constraint is validated, you can safely set `NOT NULL` on the column and drop the check constraint. +For Rails 6.1, use: + ```ruby +class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.1] + def change + validate_check_constraint :users, name: "users_some_column_null" + + # in Postgres 12+, you can then safely set NOT NULL on the column + change_column_null :users, :some_column, false + remove_check_constraint :users, name: "users_some_column_null" + end +end +``` + +For Rails < 6.1, use: + +```ruby class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0] def change safety_assured do execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "users_some_column_null"' end @@ -364,11 +435,11 @@ ### 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 ExecuteSQL < ActiveRecord::Migration[6.0] +class ExecuteSQL < ActiveRecord::Migration[6.1] def change safety_assured { execute "..." } end end ``` @@ -380,11 +451,11 @@ #### Bad In Postgres, adding an index non-concurrently blocks writes. ```ruby -class AddSomeIndexToUsers < ActiveRecord::Migration[6.0] +class AddSomeIndexToUsers < ActiveRecord::Migration[6.1] def change add_index :users, :some_column end end ``` @@ -392,11 +463,11 @@ #### Good Add indexes concurrently. ```ruby -class AddSomeIndexToUsers < ActiveRecord::Migration[6.0] +class AddSomeIndexToUsers < ActiveRecord::Migration[6.1] disable_ddl_transaction! def change add_index :users, :some_column, algorithm: :concurrently end @@ -418,11 +489,11 @@ #### Bad Rails adds an index non-concurrently to references by default, which blocks writes in Postgres. ```ruby -class AddReferenceToUsers < ActiveRecord::Migration[6.0] +class AddReferenceToUsers < ActiveRecord::Migration[6.1] def change add_reference :users, :city end end ``` @@ -430,11 +501,11 @@ #### Good Make sure the index is added concurrently. ```ruby -class AddReferenceToUsers < ActiveRecord::Migration[6.0] +class AddReferenceToUsers < ActiveRecord::Migration[6.1] disable_ddl_transaction! def change add_reference :users, :city, index: {algorithm: :concurrently} end @@ -448,21 +519,21 @@ #### Bad In Postgres, adding a foreign key blocks writes on both tables. ```ruby -class AddForeignKeyOnUsers < ActiveRecord::Migration[6.0] +class AddForeignKeyOnUsers < ActiveRecord::Migration[6.1] def change add_foreign_key :users, :orders end end ``` or ```ruby -class AddReferenceToUsers < ActiveRecord::Migration[6.0] +class AddReferenceToUsers < ActiveRecord::Migration[6.1] def change add_reference :users, :order, foreign_key: true end end ``` @@ -472,21 +543,21 @@ Add the foreign key without validating existing rows, then validate them in a separate migration. For Rails 5.2+, use: ```ruby -class AddForeignKeyOnUsers < ActiveRecord::Migration[6.0] +class AddForeignKeyOnUsers < ActiveRecord::Migration[6.1] def change add_foreign_key :users, :orders, validate: false end end ``` Then: ```ruby -class ValidateForeignKeyOnUsers < ActiveRecord::Migration[6.0] +class ValidateForeignKeyOnUsers < ActiveRecord::Migration[6.1] def change validate_foreign_key :users, :orders end end ``` @@ -520,11 +591,11 @@ #### Bad In Postgres, there’s no equality operator for the `json` column type, which can cause errors for existing `SELECT DISTINCT` queries in your application. ```ruby -class AddPropertiesToUsers < ActiveRecord::Migration[6.0] +class AddPropertiesToUsers < ActiveRecord::Migration[6.1] def change add_column :users, :properties, :json end end ``` @@ -532,11 +603,11 @@ #### Good Use `jsonb` instead. ```ruby -class AddPropertiesToUsers < ActiveRecord::Migration[6.0] +class AddPropertiesToUsers < ActiveRecord::Migration[6.1] def change add_column :users, :properties, :jsonb end end ``` @@ -546,11 +617,11 @@ #### Bad Adding a non-unique index with more than three columns rarely improves performance. ```ruby -class AddSomeIndexToUsers < ActiveRecord::Migration[6.0] +class AddSomeIndexToUsers < ActiveRecord::Migration[6.1] def change add_index :users, [:a, :b, :c, :d] end end ``` @@ -558,11 +629,11 @@ #### Good Instead, start an index with columns that narrow down the results the most. ```ruby -class AddSomeIndexToUsers < ActiveRecord::Migration[6.0] +class AddSomeIndexToUsers < ActiveRecord::Migration[6.1] def change add_index :users, [:b, :d] end end ``` @@ -572,11 +643,11 @@ ## 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. ```ruby -class MySafeMigration < ActiveRecord::Migration[6.0] +class MySafeMigration < ActiveRecord::Migration[6.1] def change safety_assured { remove_column :users, :some_column } end end ``` @@ -589,9 +660,10 @@ Make operations safe by default. - adding and removing an index - adding a foreign key +- adding a check constraint - setting NOT NULL on an existing column Add to `config/initializers/strong_migrations.rb`: ```ruby