Skip to content

Commit 50bbff3

Browse files
committed
Fix .left_outer_joins when multiple associations have the same child.
Signed-off-by: Garrett Blehm <[email protected]>
1 parent e09dd95 commit 50bbff3

File tree

7 files changed

+47
-7
lines changed

7 files changed

+47
-7
lines changed

activerecord/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
* Fix an issue where `.left_outer_joins` used with multiple associations that have
2+
the same child association but different parents does not join all parents.
3+
4+
Previously, using `.left_outer_joins` with the same child association would only join one of the parents.
5+
6+
Now it will correctly join both parents.
7+
8+
Fixes #41498.
9+
10+
*Garrett Blehm*
11+
112
* Deprecate `unsigned_float` and `unsigned_decimal` short-hand column methods.
213

314
As of MySQL 8.0.17, the UNSIGNED attribute is deprecated for columns of type FLOAT, DOUBLE,

activerecord/lib/active_record/associations/join_dependency.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,12 @@ def make_join_constraints(join_root, join_type)
190190
def make_constraints(parent, child, join_type)
191191
foreign_table = parent.table
192192
foreign_klass = parent.base_klass
193-
child.join_constraints(foreign_table, foreign_klass, join_type, alias_tracker) do |reflection|
194-
table, terminated = @joined_tables[reflection]
193+
child.join_constraints(foreign_table, foreign_klass, join_type, alias_tracker) do |reflection, remaining_reflection_chain|
194+
table, terminated = @joined_tables[remaining_reflection_chain]
195195
root = reflection == child.reflection
196196

197197
if table && (!root || !terminated)
198-
@joined_tables[reflection] = [table, root] if root
198+
@joined_tables[remaining_reflection_chain] = [table, root] if root
199199
next table, true
200200
end
201201

@@ -206,7 +206,7 @@ def make_constraints(parent, child, join_type)
206206
root ? name : "#{name}_join"
207207
end
208208

209-
@joined_tables[reflection] ||= [table, root] if join_type == Arel::Nodes::OuterJoin
209+
@joined_tables[remaining_reflection_chain] ||= [table, root] if join_type == Arel::Nodes::OuterJoin
210210
table
211211
end.concat child.children.flat_map { |c| make_constraints(child, c, join_type) }
212212
end

activerecord/lib/active_record/associations/join_dependency/join_association.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ def join_constraints(foreign_table, foreign_klass, join_type, alias_tracker)
2525
joins = []
2626
chain = []
2727

28-
reflection.chain.each do |reflection|
29-
table, terminated = yield reflection
28+
reflection_chain = reflection.chain
29+
reflection_chain.each_with_index do |reflection, index|
30+
table, terminated = yield reflection, reflection_chain[index..]
3031
@table ||= table
3132

3233
if terminated

activerecord/test/cases/associations/inner_join_association_test.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
require "models/tag"
1313
require "models/sharded/blog_post"
1414
require "models/sharded/comment"
15+
require "models/friendship"
16+
require "models/reader"
17+
require "models/reference"
18+
require "models/job"
1519

1620
class InnerJoinAssociationTest < ActiveRecord::TestCase
1721
fixtures :authors, :author_addresses, :essays, :posts, :comments, :categories, :categories_posts, :categorizations,
@@ -231,4 +235,12 @@ def test_find_with_conditions_on_through_reflection
231235
assert_not_empty blog_posts
232236
assert_equal(expected_comment.blog_post, blog_posts.first)
233237
end
238+
239+
def test_inner_joins_includes_all_nested_associations
240+
queries = capture_sql { Friendship.joins(:friend_favorite_reference_job, :follower_favorite_reference_job).to_a }
241+
# Match mysql and postgresql/sqlite quoting
242+
quote = Regexp.union(%w[" `])
243+
assert queries.any? { |sql| /#{quote}friendships#{quote}.#{quote}friend_id#{quote}/i.match?(sql) }
244+
assert queries.any? { |sql| /#{quote}friendships#{quote}.#{quote}follower_id#{quote}/i.match?(sql) }
245+
end
234246
end

activerecord/test/cases/associations/left_outer_join_association_test.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
require "models/category"
1010
require "models/categorization"
1111
require "models/person"
12+
require "models/friendship"
13+
require "models/reader"
14+
require "models/reference"
15+
require "models/job"
1216

1317
class LeftOuterJoinAssociationTest < ActiveRecord::TestCase
1418
fixtures :authors, :author_addresses, :essays, :posts, :comments, :ratings, :categorizations, :people
@@ -120,4 +124,12 @@ def test_does_not_override_select
120124

121125
assert_equal [author], Author.where(id: author).left_outer_joins(:special_categorizations)
122126
end
127+
128+
def test_left_outer_joins_includes_all_nested_associations
129+
queries = capture_sql { Friendship.left_outer_joins(:friend_favorite_reference_job, :follower_favorite_reference_job).to_a }
130+
# Match mysql and postgresql/sqlite quoting
131+
quote = Regexp.union(%w[" `])
132+
assert queries.any? { |sql| /#{quote}friendships#{quote}.#{quote}friend_id#{quote}/i.match?(sql) }
133+
assert queries.any? { |sql| /#{quote}friendships#{quote}.#{quote}follower_id#{quote}/i.match?(sql) }
134+
end
123135
end

activerecord/test/models/friendship.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@ class Friendship < ActiveRecord::Base
55
# friend_too exists to test a bug, and probably shouldn't be used elsewhere
66
belongs_to :friend_too, foreign_key: "friend_id", class_name: "Person", counter_cache: :friends_too_count
77
belongs_to :follower, class_name: "Person"
8+
9+
has_one :friend_favorite_reference_job, through: :friend, source: :favorite_reference_job
10+
has_one :follower_favorite_reference_job, through: :follower, source: :favorite_reference_job
811
end

activerecord/test/models/person.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ class Person < ActiveRecord::Base
1818
has_many :references
1919
has_many :bad_references
2020
has_many :fixed_bad_references, -> { where favorite: true }, class_name: "BadReference"
21-
has_one :favorite_reference, -> { where "favorite=?", true }, class_name: "Reference"
21+
has_one :favorite_reference, -> { where favorite: true }, class_name: "Reference"
22+
has_one :favorite_reference_job, through: :favorite_reference, source: :job
2223
has_many :posts_with_comments_sorted_by_comment_id, -> { includes(:comments).order("comments.id") }, through: :readers, source: :post
2324
has_many :first_posts, -> { where(id: [1, 2]) }, through: :readers
2425

0 commit comments

Comments
 (0)