Skip to content

Commit a34563b

Browse files
authored
Merge pull request rails#55086 from zzak/re-55078
Fix `remove_foreign_key` when multiple columns are found
2 parents 985e75f + 1a6d67b commit a34563b

File tree

5 files changed

+147
-35
lines changed

5 files changed

+147
-35
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: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,27 @@ 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+
t.singleton_class.prepend(TableDefinition)
54+
super
55+
end
3556
end
3657

3758
class V7_2 < V8_0
@@ -157,9 +178,7 @@ def add_foreign_key(from_table, to_table, **options)
157178

158179
private
159180
def compatible_table_definition(t)
160-
class << t
161-
prepend TableDefinition
162-
end
181+
t.singleton_class.prepend(TableDefinition)
163182
super
164183
end
165184
end
@@ -220,9 +239,7 @@ def raise_on_if_exist_options(options)
220239

221240
private
222241
def compatible_table_definition(t)
223-
class << t
224-
prepend TableDefinition
225-
end
242+
t.singleton_class.prepend(TableDefinition)
226243
super
227244
end
228245
end
@@ -263,9 +280,7 @@ def add_reference(table_name, ref_name, **options)
263280

264281
private
265282
def compatible_table_definition(t)
266-
class << t
267-
prepend TableDefinition
268-
end
283+
t.singleton_class.prepend(TableDefinition)
269284
super
270285
end
271286
end
@@ -311,17 +326,13 @@ def add_timestamps(table_name, **options)
311326

312327
private
313328
def compatible_table_definition(t)
314-
class << t
315-
prepend TableDefinition
316-
end
329+
t.singleton_class.prepend(TableDefinition)
317330
super
318331
end
319332

320333
def command_recorder
321334
recorder = super
322-
class << recorder
323-
prepend CommandRecorder
324-
end
335+
recorder.singleton_class.prepend(CommandRecorder)
325336
recorder
326337
end
327338
end
@@ -409,9 +420,7 @@ def add_reference(table_name, ref_name, **options)
409420

410421
private
411422
def compatible_table_definition(t)
412-
class << t
413-
prepend TableDefinition
414-
end
423+
t.singleton_class.prepend(TableDefinition)
415424
super
416425
end
417426
end
@@ -463,9 +472,7 @@ def remove_index(table_name, column_name = nil, **options)
463472

464473
private
465474
def compatible_table_definition(t)
466-
class << t
467-
prepend TableDefinition
468-
end
475+
t.singleton_class.prepend(TableDefinition)
469476
super
470477
end
471478

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)