Skip to content

Commit 63c2efa

Browse files
Add support for if_exists/if_not_exists on remove_foreign_key/add_foreign_key
Applications can set their migrations to ignore exceptions raised when adding a foreign key that already exists or when removing a foreign key that does not exist. Add test cases 💇‍♀️
1 parent 36aee3f commit 63c2efa

File tree

4 files changed

+102
-0
lines changed

4 files changed

+102
-0
lines changed

activerecord/CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
1+
* Adds support for `if_not_exists` to `add_foreign_key` and `if_exists` to `remove_foreign_key`.
2+
3+
Applications can set their migrations to ignore exceptions raised when adding a foreign key
4+
that already exists or when removing a foreign key that does not exist.
5+
6+
Example Usage:
7+
8+
```ruby
9+
class AddAuthorsForeignKeyToArticles < ActiveRecord::Migration[7.0]
10+
def change
11+
add_foreign_key :articles, :authors, if_not_exists: true
12+
end
13+
end
14+
```
15+
16+
```ruby
17+
class RemoveAuthorsForeignKeyFromArticles < ActiveRecord::Migration[7.0]
18+
def change
19+
remove_foreign_key :articles, :authors, if_exists: true
20+
end
21+
end
22+
```
23+
24+
*Roberto Miranda*
25+
126
* Prevent polluting ENV during postgresql structure dump/load
227

328
Some configuration parameters were provided to pg_dump / psql via

activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,6 +1039,10 @@ def foreign_keys(table_name)
10391039
#
10401040
# ALTER TABLE "articles" ADD CONSTRAINT fk_rails_e74ce85cbc FOREIGN KEY ("author_id") REFERENCES "authors" ("id")
10411041
#
1042+
# ====== Creating a foreign key, ignoring method call if the foreign key exists
1043+
#
1044+
# add_foreign_key(:articles, :authors, if_not_exists: true)
1045+
#
10421046
# ====== Creating a foreign key on a specific column
10431047
#
10441048
# add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id"
@@ -1066,10 +1070,14 @@ def foreign_keys(table_name)
10661070
# Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+
10671071
# [<tt>:on_update</tt>]
10681072
# Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+
1073+
# [<tt>:if_not_exists</tt>]
1074+
# Specifies if the foreign key already exists to not try to re-add it. This will avoid
1075+
# duplicate column errors.
10691076
# [<tt>:validate</tt>]
10701077
# (PostgreSQL only) Specify whether or not the constraint should be validated. Defaults to +true+.
10711078
def add_foreign_key(from_table, to_table, **options)
10721079
return unless supports_foreign_keys?
1080+
return if options[:if_not_exists] == true && foreign_key_exists?(from_table, to_table)
10731081

10741082
options = foreign_key_options(from_table, to_table, options)
10751083
at = create_alter_table from_table
@@ -1099,12 +1107,18 @@ def add_foreign_key(from_table, to_table, **options)
10991107
#
11001108
# remove_foreign_key :accounts, name: :special_fk_name
11011109
#
1110+
# Checks if the foreign key exists before trying to remove it. Will silently ignore indexes that
1111+
# don't exist.
1112+
#
1113+
# remove_foreign_key :accounts, :branches, if_exists: true
1114+
#
11021115
# The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key
11031116
# with an addition of
11041117
# [<tt>:to_table</tt>]
11051118
# The name of the table that contains the referenced primary key.
11061119
def remove_foreign_key(from_table, to_table = nil, **options)
11071120
return unless supports_foreign_keys?
1121+
return if options[:if_exists] == true && !foreign_key_exists?(from_table, to_table)
11081122

11091123
fk_name_to_delete = foreign_key_for!(from_table, to_table: to_table, **options).name
11101124

activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ def add_foreign_key(from_table, to_table, **options)
6060
end
6161

6262
def remove_foreign_key(from_table, to_table = nil, **options)
63+
return if options[:if_exists] == true && !foreign_key_exists?(from_table, to_table)
64+
6365
to_table ||= options[:to_table]
6466
options = options.except(:name, :to_table, :validate)
6567
foreign_keys = foreign_keys(from_table)

activerecord/test/cases/migration/foreign_key_test.rb

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,67 @@ def test_add_foreign_key_with_suffix
575575
silence_stream($stdout) { migration.migrate(:down) }
576576
ActiveRecord::Base.table_name_suffix = nil
577577
end
578+
579+
def test_remove_foreign_key_with_if_exists_not_set
580+
@connection.add_foreign_key :astronauts, :rockets
581+
assert_equal 1, @connection.foreign_keys("astronauts").size
582+
583+
@connection.remove_foreign_key :astronauts, :rockets
584+
assert_equal [], @connection.foreign_keys("astronauts")
585+
586+
error = assert_raises do
587+
@connection.remove_foreign_key :astronauts, :rockets
588+
end
589+
590+
assert_equal("Table 'astronauts' has no foreign key for rockets", error.message)
591+
end
592+
593+
def test_remove_foreign_key_with_if_exists_set
594+
@connection.add_foreign_key :astronauts, :rockets
595+
assert_equal 1, @connection.foreign_keys("astronauts").size
596+
597+
@connection.remove_foreign_key :astronauts, :rockets
598+
assert_equal [], @connection.foreign_keys("astronauts")
599+
600+
assert_nothing_raised do
601+
@connection.remove_foreign_key :astronauts, :rockets, if_exists: true
602+
end
603+
end
604+
605+
def test_add_foreign_key_with_if_not_exists_not_set
606+
@connection.add_foreign_key :astronauts, :rockets
607+
assert_equal 1, @connection.foreign_keys("astronauts").size
608+
609+
if current_adapter?(:SQLite3Adapter)
610+
assert_nothing_raised do
611+
@connection.add_foreign_key :astronauts, :rockets
612+
end
613+
else
614+
error = assert_raises do
615+
@connection.add_foreign_key :astronauts, :rockets
616+
end
617+
618+
if current_adapter?(:Mysql2Adapter)
619+
if ActiveRecord::Base.connection.mariadb?
620+
assert_match(/Duplicate key on write or update/, error.message)
621+
else
622+
assert_match(/Duplicate foreign key constraint name/, error.message)
623+
end
624+
else
625+
assert_match(/PG::DuplicateObject: ERROR:.*for relation "astronauts" already exists/, error.message)
626+
end
627+
628+
end
629+
end
630+
631+
def test_add_foreign_key_with_if_not_exists_set
632+
@connection.add_foreign_key :astronauts, :rockets
633+
assert_equal 1, @connection.foreign_keys("astronauts").size
634+
635+
assert_nothing_raised do
636+
@connection.add_foreign_key :astronauts, :rockets, if_not_exists: true
637+
end
638+
end
578639
end
579640
end
580641
end

0 commit comments

Comments
 (0)