Skip to content

Commit

Permalink
Add foreign key validation
Browse files Browse the repository at this point in the history
  • Loading branch information
nikolai-b committed Feb 6, 2019
1 parent a395886 commit 292ed4e
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 0 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,35 @@ class BackportPublishedDefaultOnPosts < ActiveRecord::Migration
end
```

### Adding Foreign Keys

#### Bad

Adding a foreign key causes an `ShareRowExclusiveLock` (or `AccessExclusiveLock` on PostgreSQL before 9.5) on both tables.
It is possible to add a foreign key in one step and validate later (which only causes RowShareLocks)

```ruby
class AddUsersForeignKeyToOrder < ActiveRecord::Migration
add_foreign_key :users, :order
end
```

#### Good

Add the foreign key and validate in another migration:

```ruby
class AddUsersForeignKeyToOrder < ActiveRecord::Migration
add_foreign_key :users, :order, validate: false
end

class ValidateUsersForeignKeyToOrder < ActiveRecord::Migration
validate_foreign_key :users, :order
end
```

Note, both `add_foreign_key` and `validate_foreign_key` accept `name` and `column` options.

### TODO

* Changing a column type
Expand Down
1 change: 1 addition & 0 deletions lib/zero_downtime_migrations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require_relative "zero_downtime_migrations/validation"
require_relative "zero_downtime_migrations/validation/add_column"
require_relative "zero_downtime_migrations/validation/add_index"
require_relative "zero_downtime_migrations/validation/add_foreign_key"
require_relative "zero_downtime_migrations/validation/ddl_migration"
require_relative "zero_downtime_migrations/validation/find_each"
require_relative "zero_downtime_migrations/validation/mixed_migration"
Expand Down
54 changes: 54 additions & 0 deletions lib/zero_downtime_migrations/validation/add_foreign_key.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
module ZeroDowntimeMigrations
class Validation
class AddForeignKey < Validation
def validate!
return unless requires_validate_constraints?
error!(message) unless foreign_key_not_validated?
end

private

def message
<<-MESSAGE.strip_heredoc
Adding a foreign key causes an `ShareRowExclusiveLock` (or `AccessExclusiveLock` on PostgreSQL before 9.5) on both tables.
It is possible to add a foreign key in one step and validate later (which only causes RowShareLocks)
class Add#{foreign_table}ForeignKeyTo#{table} < ActiveRecord::Migration
def change
add_foreign_key :#{table}, #{foreign_table}, validate: false
end
end
class Validate#{foreign_table}ForeignKeyOn#{table} < ActiveRecord::Migration
def change
validate_foreign_key :#{table}, :#{foreign_table}
end
end
Note, both `add_foreign_key` and `validate_foreign_key` accept `name` and `column` options.
MESSAGE
end

def foreign_key_not_validated?
options[:validate] == false
end

def foreign_table
args[1]
end

def table
args[0]
end

def requires_validate_constraints?
supports_validate_constraints?
end

def supports_validate_constraints?
ActiveRecord::Base.connection.respond_to?(:supports_validate_constraints?) &&
ActiveRecord::Base.connection.supports_validate_constraints?
end
end
end
end
77 changes: 77 additions & 0 deletions spec/zero_downtime_migrations/validation/add_foreign_key_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
RSpec.describe ZeroDowntimeMigrations::Validation::AddForeignKey do
let(:error) { ZeroDowntimeMigrations::UnsafeMigrationError }

context "when the database supports_validate_constraints" do
context "adding a foreign key and not specifying validate" do
let(:migration) do
Class.new(ActiveRecord::Migration[5.0]) do
def change
create_table :order
add_column :users, :order_id, :integer
add_foreign_key :users, :order
end
end
end

it "raises an unsafe migration error" do
expect { migration.migrate(:up) }.to raise_error(error)
end
end

context "adding a foreign key and specifying validate true" do
let(:migration) do
Class.new(ActiveRecord::Migration[5.0]) do
def change
create_table :order
add_column :users, :order_id, :integer
add_foreign_key :users, :order, validate: true
end
end
end

it "raises an unsafe migration error" do
expect { migration.migrate(:up) }.to raise_error(error)
end
end

context "adding a foreign key and specifying validate false" do
let(:migration) do
Class.new(ActiveRecord::Migration[5.0]) do
def change
create_table :order
add_column :users, :order_id, :integer
add_foreign_key :users, :order, validate: false
end
end
end

it "raises nothing" do
expect { migration.migrate(:up) }.to_not raise_error
end
end
end

context "when the database does not supports_validate_constraints" do
before do
allow_any_instance_of(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter).to receive(
:supports_validate_constraints?
).and_return(false)
end

context "adding a foreign key and not specifying validate" do
let(:migration) do
Class.new(ActiveRecord::Migration[5.0]) do
def change
create_table :order
add_column :users, :order_id, :integer
add_foreign_key :users, :order
end
end
end

it "raises nothing" do
expect { migration.migrate(:up) }.to_not raise_error
end
end
end
end

0 comments on commit 292ed4e

Please sign in to comment.