Skip to content

Commit b0d1493

Browse files
authored
Merge pull request rails#41552 from gmcgibbon/has_many_inverse_reflect_self
Fix has_many inversing recursion on models with recursive associations
2 parents 5257da7 + a77f81c commit b0d1493

File tree

6 files changed

+62
-2
lines changed

6 files changed

+62
-2
lines changed

activerecord/CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
* Fix `has_many` inversing recursion on models with recursive associations.
2+
3+
*Gannon McGibbon*
4+
15
* Add nested_attributes_for support for `delegated_type`
26

37
```ruby
@@ -33,7 +37,7 @@
3337
3438
Prior to this change, deletes with GROUP_BY and HAVING were returning an error.
3539
36-
After this change, GROUP_BY and HAVING are valid clauses in DELETE queries, generating the following query:
40+
After this change, GROUP_BY and HAVING are valid clauses in DELETE queries, generating the following query:
3741
3842
```sql
3943
DELETE FROM "posts" WHERE "posts"."id" IN (
@@ -57,7 +61,7 @@
5761
5862
```sql
5963
UPDATE "posts" SET "flagged" = ? WHERE "posts"."id" IN (
60-
SELECT "posts"."id" FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
64+
SELECT "posts"."id" FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
6165
GROUP BY posts.id HAVING (count(comments.id) >= 2)
6266
) [["flagged", "t"]]
6367
```

activerecord/lib/active_record/associations.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@ def corrections
5959
end
6060
end
6161

62+
class InverseOfAssociationRecursiveError < ActiveRecordError # :nodoc:
63+
attr_reader :reflection
64+
def initialize(reflection = nil)
65+
if reflection
66+
@reflection = reflection
67+
super("Inverse association #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{reflection.class_name}) is recursive.")
68+
else
69+
super("Inverse association is recursive.")
70+
end
71+
end
72+
end
73+
6274
class HasManyThroughAssociationNotFoundError < ActiveRecordError # :nodoc:
6375
attr_reader :owner_class, :reflection
6476

activerecord/lib/active_record/reflection.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,9 @@ def check_validity_of_inverse!
235235
if has_inverse? && inverse_of.nil?
236236
raise InverseOfAssociationNotFoundError.new(self)
237237
end
238+
if has_inverse? && inverse_of == self
239+
raise InverseOfAssociationRecursiveError.new(self)
240+
end
238241
end
239242
end
240243

@@ -632,6 +635,7 @@ def automatic_inverse_of
632635
# with the current reflection's klass name.
633636
def valid_inverse_reflection?(reflection)
634637
reflection &&
638+
reflection != self &&
635639
foreign_key == reflection.foreign_key &&
636640
klass <= reflection.active_record &&
637641
can_find_inverse_of_automatically?(reflection, true)

activerecord/test/cases/associations/inverse_associations_test.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
require "models/contract"
2626
require "models/subscription"
2727
require "models/book"
28+
require "models/branch"
2829

2930
class AutomaticInverseFindingTests < ActiveRecord::TestCase
3031
fixtures :ratings, :comments, :cars, :books
@@ -787,6 +788,30 @@ def test_with_hash_many_inversing_does_not_add_duplicate_associated_objects
787788
end
788789
end
789790

791+
def test_recursive_model_has_many_inversing
792+
with_has_many_inversing do
793+
main = Branch.create!
794+
feature = main.branches.create!
795+
topic = feature.branches.build
796+
797+
assert_equal(main, topic.branch.branch)
798+
end
799+
end
800+
801+
def test_recursive_inverse_on_recursive_model_has_many_inversing
802+
with_has_many_inversing do
803+
main = BrokenBranch.create!
804+
feature = main.branches.create!
805+
topic = feature.branches.build
806+
807+
error = assert_raises(ActiveRecord::InverseOfAssociationRecursiveError) do
808+
topic.branch.branch
809+
end
810+
811+
assert_equal("Inverse association branch (:branch in BrokenBranch) is recursive.", error.message)
812+
end
813+
end
814+
790815
def test_unscope_does_not_set_inverse_when_incorrect
791816
interest = interests(:trainspotting)
792817
human = interest.human

activerecord/test/models/branch.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# frozen_string_literal: true
2+
3+
class Branch < ActiveRecord::Base
4+
has_many :branches
5+
belongs_to :branch, optional: true
6+
end
7+
8+
class BrokenBranch < Branch
9+
has_many :branches, class_name: "BrokenBranch", foreign_key: :branch_id
10+
belongs_to :branch, optional: true, inverse_of: :branch, class_name: "BrokenBranch"
11+
end

activerecord/test/schema/schema.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@
145145
t.boolean :has_fun, null: false, default: false
146146
end
147147

148+
create_table :branches, force: true do |t|
149+
t.references :branch
150+
end
151+
148152
create_table :bulbs, primary_key: "ID", force: true do |t|
149153
t.integer :car_id
150154
t.string :name

0 commit comments

Comments
 (0)