Rails 7 adds support for `if_exists/if_not_exists` on `remove_foreign_key/add_foreign_key`


Rails 6.1 added support for if_exists/if_not_exists on remove/add column and extended it further to support if_not_exists on add_index and if_exists on remove_index.

To maintain the same behavior across add/remove constraints of databases, Rails 7 added support for if_exists/if_not_exists on remove_foreign_key/add_foreign_key.

Before Rails 7

Add foreign key

Let’s say we have an e-commerce application with Order and User models. An order belongs to a user, and we want to add a user_id foreign key constraint on the orders table. We would add a migration as shown below:

class AddUserReferenceToOrder < ActiveRecord::Migration[6.0]
  def change
    add_foreign_key :orders, :users
  end
end

But if the orders table already contains foreign_key constraint on the user, the above DB migration will raise an error.

rails db:migrate

== 20210714080612 AddUserReferenceToOrder: migrating ================
-- add_foreign_key(:orders, :users)
rake aborted!
StandardError: An error has occurred, this and all later migrations canceled:

PG::DuplicateObject: ERROR:  constraint "fk_rails_11f1189a77" for relation "orders" already exists

Remove foreign key

Similarly, if we try to remove a foreign_key constraint that never existed on orders table the migration would also raise an error.

class RemoveProductReferenceFromOrder < ActiveRecord::Migration[6.0]
  def change
    remove_foreign_key :orders, :products
  end
end

rails db:migrate

== 20210714080712 RemoveProductReferenceFromOrder: migrating ================
-- remove_foreign_key(:orders, :products)
rake aborted!
StandardError: An error has occurred, this and all later migrations canceled:

Table 'orders' has no foreign key for products

In Rails 7

To avoid above issues and to keep things consistent, Rails team added support to pass if_exists/if_not_exists options to remove_foreign_key/add_foreign_key.

With this change, the below migrations run successfully without raising any error.

Add foreign key

class AddUserReferenceToOrder < ActiveRecord::Migration[6.0]
  def change
    add_foreign_key :orders, :users, if_not_exists: true
  end
end

rails db:migrate

== 20210714080612 AddUserReferenceToOrder: migrating ================
-- add_foreign_key(:orders, :users, {:if_not_exists=>true})
   -> 0.0151s
== 20210714080612 AddUserReferenceToOrder: migrated (0.0153s) ==================

Remove foreign key

class RemoveProductReferenceFromOrder < ActiveRecord::Migration[6.0]
  def change
    remove_foreign_key :orders, :products, if_exists: true
  end
end

rails db:migrate

== 20210714080712 RemoveProductReferenceFromOrder: migrating ================
-- remove_foreign_key(:orders, :products, {if_exists: true})
   -> 0.0118s
== 20210714080712 RemoveProductReferenceFromOrder: migrated (0.0118s) ================

Check out this pull request for more details.