Skip to content

Commit 489e08d

Browse files
zzakfatkodima
authored andcommitted
Fix remove_foreign_key when multiple columns are found
Co-authored-by: fatkodima <[email protected]>
1 parent b9a7fa7 commit 489e08d

File tree

5 files changed

+142
-14
lines changed

5 files changed

+142
-14
lines changed

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1815,7 +1815,20 @@ def foreign_key_name(table_name, options)
18151815

18161816
def foreign_key_for(from_table, **options)
18171817
return unless use_foreign_keys?
1818-
foreign_keys(from_table).detect { |fk| fk.defined_for?(**options) }
1818+
1819+
keys = foreign_keys(from_table)
1820+
1821+
if options[:_skip_column_match]
1822+
return keys.find { |fk| fk.defined_for?(**options) }
1823+
end
1824+
1825+
if options[:column].nil?
1826+
default_column = foreign_key_column_for(options[:to_table], "id")
1827+
matches = keys.select { |fk| fk.column == default_column }
1828+
keys = matches if matches.any?
1829+
end
1830+
1831+
keys.find { |fk| fk.defined_for?(**options) }
18191832
end
18201833

18211834
def foreign_key_for!(from_table, to_table: nil, **options)

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

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,23 +63,13 @@ def add_foreign_key(from_table, to_table, **options)
6363
end
6464

6565
def remove_foreign_key(from_table, to_table = nil, **options)
66-
return if options.delete(:if_exists) == true && !foreign_key_exists?(from_table, to_table, **options.slice(:column))
66+
return if options.delete(:if_exists) && !foreign_key_exists?(from_table, to_table, **options.slice(:column))
6767

6868
to_table ||= options[:to_table]
6969
options = options.except(:name, :to_table, :validate)
70-
foreign_keys = foreign_keys(from_table)
71-
72-
fkey = foreign_keys.detect do |fk|
73-
table = to_table || begin
74-
table = options[:column].to_s.delete_suffix("_id")
75-
Base.pluralize_table_names ? table.pluralize : table
76-
end
77-
table = strip_table_name_prefix_and_suffix(table)
78-
options = options.slice(*fk.options.keys)
79-
fk_to_table = strip_table_name_prefix_and_suffix(fk.to_table)
80-
fk_to_table == table && options.all? { |k, v| fk.options[k].to_s == v.to_s }
81-
end || raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{to_table || options}")
70+
fkey = foreign_key_for!(from_table, to_table: to_table, **options)
8271

72+
foreign_keys = foreign_keys(from_table)
8373
foreign_keys.delete(fkey)
8474
alter_table(from_table, foreign_keys)
8575
end

activerecord/lib/active_record/migration/compatibility.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,29 @@ def self.find(version)
3232
V8_1 = Current
3333

3434
class V8_0 < V8_1
35+
module RemoveForeignKeyColumnMatch
36+
def remove_foreign_key(from_table, to_table = nil, **options)
37+
options[:_skip_column_match] = true
38+
super
39+
end
40+
end
41+
42+
module TableDefinition
43+
def remove_foreign_key(to_table = nil, **options)
44+
options[:_skip_column_match] = true
45+
super
46+
end
47+
end
48+
49+
include RemoveForeignKeyColumnMatch
50+
51+
private
52+
def compatible_table_definition(t)
53+
class << t
54+
prepend TableDefinition
55+
end
56+
super
57+
end
3558
end
3659

3760
class V7_2 < V8_0

activerecord/test/cases/migration/compatibility_test.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,60 @@ def up
664664
end
665665
end
666666

667+
def test_remove_foreign_key_on_8_1
668+
connection.create_table(:sub_testings) do |t|
669+
t.references :testing, foreign_key: true, type: :bigint
670+
t.references :experiment, foreign_key: { to_table: :testings }, type: :bigint
671+
end
672+
673+
migration = Class.new(ActiveRecord::Migration[8.1]) do
674+
def up
675+
change_table(:sub_testings) do |t|
676+
t.remove_foreign_key :testings
677+
t.remove_foreign_key :testings, column: :experiment_id
678+
end
679+
end
680+
end
681+
682+
ActiveRecord::Migrator.new(:up, [migration], @schema_migration, @internal_metadata).migrate
683+
684+
foreign_keys = @connection.foreign_keys("sub_testings")
685+
assert_equal 0, foreign_keys.size
686+
ensure
687+
connection.drop_table(:sub_testings, if_exists: true)
688+
ActiveRecord::Base.clear_cache!
689+
end
690+
691+
def test_remove_foreign_key_on_8_0
692+
connection.create_table(:sub_testings) do |t|
693+
t.references :testing, foreign_key: true, type: :bigint
694+
t.references :experiment, foreign_key: { to_table: :testings }, type: :bigint
695+
end
696+
697+
migration = Class.new(ActiveRecord::Migration[8.0]) do
698+
def up
699+
change_table(:sub_testings) do |t|
700+
t.remove_foreign_key :testings
701+
t.remove_foreign_key :testings, column: :experiment_id
702+
end
703+
end
704+
end
705+
706+
assert_raise(StandardError, match: /Table 'sub_testings' has no foreign key for testings$/) {
707+
ActiveRecord::Migrator.new(:up, [migration], @schema_migration, @internal_metadata).migrate
708+
}
709+
710+
foreign_keys = @connection.foreign_keys("sub_testings")
711+
if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter)
712+
assert_equal 2, foreign_keys.size
713+
else
714+
assert_equal 1, foreign_keys.size
715+
end
716+
ensure
717+
connection.drop_table(:sub_testings, if_exists: true)
718+
ActiveRecord::Base.clear_cache!
719+
end
720+
667721
private
668722
def precision_implicit_default
669723
if current_adapter?(:Mysql2Adapter, :TrilogyAdapter)

activerecord/test/cases/migration/foreign_key_test.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,22 @@ def test_add_foreign_key_with_if_not_exists_considers_primary_key_option
260260
assert_equal ["id", "id_for_type_change"], foreign_keys.map(&:primary_key).sort
261261
end
262262

263+
def test_remove_foreign_key_with_multiple_keys_between_tables
264+
@connection.add_column :astronauts, :backup_rocket_id, :bigint
265+
266+
@connection.add_foreign_key :astronauts, :rockets, column: :rocket_id
267+
@connection.add_foreign_key :astronauts, :rockets, column: :backup_rocket_id
268+
269+
@connection.remove_foreign_key :astronauts, :rockets
270+
271+
foreign_keys = @connection.foreign_keys(:astronauts)
272+
assert_equal 1, foreign_keys.size
273+
assert_equal "backup_rocket_id", foreign_keys.first.column
274+
275+
@connection.remove_foreign_key :astronauts, :rockets, column: :backup_rocket_id
276+
assert_empty @connection.foreign_keys(:astronauts)
277+
end
278+
263279
def test_add_foreign_key_with_non_standard_primary_key
264280
@connection.create_table :space_shuttles, id: false, force: true do |t|
265281
t.bigint :pk, primary_key: true
@@ -771,6 +787,38 @@ def test_add_foreign_key_with_suffix
771787
ActiveRecord::Base.table_name_suffix = nil
772788
end
773789

790+
def test_remove_foreign_key_with_prefix
791+
ActiveRecord::Base.table_name_prefix = "p_"
792+
793+
migration = CreateSchoolsAndClassesMigration.new
794+
silence_stream($stdout) { migration.migrate(:up) }
795+
assert_equal 1, @connection.foreign_keys("p_classes").size
796+
797+
@connection.remove_foreign_key :p_classes
798+
799+
assert_empty @connection.foreign_keys("p_classes")
800+
ensure
801+
@connection.drop_table :p_classes
802+
@connection.drop_table :p_schools
803+
ActiveRecord::Base.table_name_prefix = nil
804+
end
805+
806+
def test_remove_foreign_key_with_suffix
807+
ActiveRecord::Base.table_name_suffix = "_s"
808+
809+
migration = CreateSchoolsAndClassesMigration.new
810+
silence_stream($stdout) { migration.migrate(:up) }
811+
assert_equal 1, @connection.foreign_keys("classes_s").size
812+
813+
@connection.remove_foreign_key :classes_s
814+
815+
assert_empty @connection.foreign_keys("classes_s")
816+
ensure
817+
@connection.drop_table :classes_s
818+
@connection.drop_table :schools_s
819+
ActiveRecord::Base.table_name_suffix = nil
820+
end
821+
774822
def test_remove_foreign_key_with_if_exists_not_set
775823
@connection.add_foreign_key :astronauts, :rockets
776824
assert_equal 1, @connection.foreign_keys("astronauts").size

0 commit comments

Comments
 (0)