Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix override existing join types in the query in the where.associated method #51078

Merged

Conversation

saleh-alhaddad
Copy link
Contributor

@saleh-alhaddad saleh-alhaddad commented Feb 14, 2024

Motivation / Background

Currently, the where.associated method in Rails only allows filtering associated records with an inner join, which can potentially override existing join types in the query. The fix ensures that the association is joined using the appropriate join type (either inner join or left outer join) based on the existing joins in the scope. This prevents unintentional overrides of existing join types and ensures consistency in the generated SQL queries.

Example:

 User.left_outer_joins(:orders).where.associated(:orders).count

Old query

SELECT COUNT(*) FROM "users" INNER JOIN "orders" ON "orders"."user_id" = "users"."id" WHERE "orders"."id" IS NOT NULL

after fixing the issue, the query:
New Query

SELECT COUNT(*) FROM "users" LEFT OUTER JOIN "orders" ON "orders"."user_id" = "users"."id" WHERE "orders"."id" IS NOT NULL

This enhancement empowers developers with greater flexibility in querying associated data, facilitating scenarios such as:

  • Retrieving posts with authors, even if some posts lack an assigned author (utilizing left_joins).
  • Obtaining comments associated with specific users, encompassing orphaned comments without a user (employing multiple joins with varied types).

Detail

The pivotal improvement in this Pull Request is the inclusion of a check ensuring that associations are joined using the appropriate join type (either inner join or left outer join) based on existing joins within the scope. This safeguards against unintentional overrides of existing join types and fosters consistency in the generated SQL queries.

Additional Information

This refinement is informed by common requirements articulated by Rails developers in various forums and discussions. Analogous functionality is prevalent in certain popular ORM extensions, underscoring its utility. The implementation is crafted to preserve clarity and coherence within the existing codebase.

Note: This is a draft description and should be adapted to your specific implementation details and the project's context.

FYI: After merging this PR, I will check the missing method and fix this issue in a separate PR alternative same PR.

Performance:

  • Benchmark Before the fix:
Benchmark.ips do |x| 
  x.report('joins') { User.joins(:comments, :orders).where.associated(:orders) }
  x.report('left_joins') { User.left_joins(:comments, :orders).where.associated(:orders) }
  x.report('left_outer_joins') { User.left_outer_joins(:comments, :orders).where.associated(:orders) }
  x.report('with out joins') { User.where.associated(:orders) }
end
Warming up --------------------------------------
  joins                        3.260k i/100ms
  left_joins                3.261k i/100ms
  left_outer_joins     3.234k i/100ms
  with out joins         3.518k i/100ms
Calculating -------------------------------------
  joins                         32.610k (± 0.7%) i/s -    166.260k in   5.098730s
  left_joins                 32.240k (± 1.1%) i/s -    163.050k in   5.058117s
  left_outer_joins      32.395k (± 0.5%) i/s -    164.934k in   5.091447s
  with out joins          35.337k (± 0.6%) i/s -    179.418k in   5.077489s
  • Benchmark After the fix:
Benchmark.ips do |x| 
  x.report('joins') { User.joins(:comments, :orders).where.associated(:orders) }
  x.report('left_joins') { User.left_joins(:comments, :orders).where.associated(:orders) }
  x.report('left_outer_joins') { User.left_outer_joins(:comments, :orders).where.associated(:orders) }
  x.report('with out joins') { User.where.associated(:orders) }
end

Warming up --------------------------------------
  joins                          3.278k i/100ms
  left_joins                  3.264k i/100ms
  left_outer_joins       3.245k i/100ms
  with out joins           3.434k i/100ms
Calculating -------------------------------------
  joins                           31.688k (± 3.8%) i/s -    160.622k in   5.076856s
  left_joins                   32.432k (± 1.1%) i/s -    163.200k in   5.032760s
  left_outer_joins        32.398k (± 1.4%) i/s -    162.250k in   5.009054s
  with out joins            34.698k (± 1.3%) i/s -    175.134k in   5.048274s

Checklist

Before submitting the PR make sure the following are checked:

  • This Pull Request is related to one change. Changes that are unrelated should be opened in separate PRs.
  • Commit message has a detailed description of what changed and why. If this PR fixes a related issue include it in the commit message. Ex: [Fix #issue-number]
  • Tests are added or updated if you fix a bug or add a feature.
  • CHANGELOG files are updated for the changed libraries if there is a behavior change or additional feature. Minor bug fixes and documentation changes should not be included.

@saleh-alhaddad saleh-alhaddad force-pushed the support_join_types_in_where_associated branch 3 times, most recently from 0c0e16c to 1c57bd5 Compare February 14, 2024 06:23
@p8
Copy link
Member

p8 commented Feb 15, 2024

Thanks for the PR @saleh-alhaddad.
Could you explain the use-case this is trying to solve?
Don't #associated and #missing already cover all cases?

class Post < ActiveRecord::Base
  belongs_to :author
end

class Author < ActiveRecord::Base
  has_many :posts
end

author1 = Author.create! name: 'Nora'
post1 = Post.create! title: "First post", author: author1
post2 = Post.create! title: "Second post"
assert_equal Post.where.associated(:author).to_a, [post1]
assert_equal Post.where.missing(:author).to_a, [post2]

@saleh-alhaddad saleh-alhaddad force-pushed the support_join_types_in_where_associated branch from 1c57bd5 to 94e1304 Compare February 16, 2024 06:58
@p8
Copy link
Member

p8 commented Feb 16, 2024

@saleh-alhaddad It's clear you want to allow changing the join. It's not clear what would be the benefit.

For example, your added test has the same results as not passing the join_type.

def test_associated_with_child_association
Comment.where.associated(:children).tap do |relation|
assert_includes relation, comments(:greetings)
assert_not_includes relation, comments(:more_greetings)
end
end

If this case is already supported, why do you need to specify the join_type?
The following would all return the same results:

Post.associated(:author)
Post.joins(:author)
Post.left_joins(:author).where.not(author_id: nil)

@saleh-alhaddad saleh-alhaddad force-pushed the support_join_types_in_where_associated branch from 94e1304 to 7b32782 Compare February 17, 2024 08:31
@saleh-alhaddad saleh-alhaddad changed the title Add support join types in where.associated method Fix override existing join types in the query in the where.associated method Feb 17, 2024
@saleh-alhaddad saleh-alhaddad force-pushed the support_join_types_in_where_associated branch 3 times, most recently from 57b2dbc to ec8bfa3 Compare February 17, 2024 10:43
@saleh-alhaddad
Copy link
Contributor Author

@p8 I updated the PR to fix an issue in the method, please read again the description and reply if something is wrong, thanks for you

@saleh-alhaddad saleh-alhaddad force-pushed the support_join_types_in_where_associated branch from ec8bfa3 to 77360db Compare February 17, 2024 11:45
@p8
Copy link
Member

p8 commented Feb 20, 2024

@saleh-alhaddad please don’t ping commiters. Most are volunteers and contribute in their spare time. Hopefully someone will find the time to look at this PR.

@@ -3,6 +3,18 @@

*Jason Nochlin*

* Fix an issue in the `where.associated` method only allows filtering associated records with an `inner join`,
which can potentially override existing join types in the query.
The fix ensures that the association is joined using the appropriate join type (either inner join or left outer join) based on the existing joins in the scope.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The fix ensures that the association is joined using the appropriate join type (either inner join or left outer join) based on the existing joins in the scope.
The fix ensures that the association is joined using the appropriate join type (either inner join or left outer join) based on the existing joins in the scope.

* Fix an issue in the `where.associated` method only allows filtering associated records with an `inner join`,
which can potentially override existing join types in the query.
The fix ensures that the association is joined using the appropriate join type (either inner join or left outer join) based on the existing joins in the scope.
This prevents unintentional overrides of existing join types and ensures consistency in the generated SQL queries.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This prevents unintentional overrides of existing join types and ensures consistency in the generated SQL queries.
This prevents unintentional overrides of existing join types and ensures consistency in the generated SQL queries.

which can potentially override existing join types in the query.
The fix ensures that the association is joined using the appropriate join type (either inner join or left outer join) based on the existing joins in the scope.
This prevents unintentional overrides of existing join types and ensures consistency in the generated SQL queries.
Example:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Example:
Example:

Comment on lines 11 to 18
asoiciated will use left_joins alternative use deafult joins
```ruby
Post.left_joins(:author).where.associated(:author)
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
asoiciated will use left_joins alternative use deafult joins
```ruby
Post.left_joins(:author).where.associated(:author)
```
```ruby
# associated will use LEFT JOIN for this query instead of using JOIN
Post.left_joins(:author).where.associated(:author)
```

@@ -72,10 +72,17 @@ def not(opts, *rest)
# # INNER JOIN "authors" ON "authors"."id" = "posts"."author_id"
# # INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
# # WHERE "authors"."id" IS NOT NULL AND "comments"."id" IS NOT NULL
#
# Additionally, you can define joine type in the scope and associated will not use default that is joins
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Additionally, you can define joine type in the scope and associated will not use default that is joins
# Additionally, you can define join type in the scope and +associated+ will not use default that it is `JOIN`.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need to change the Additionally in the topic above, probably removing it.

@@ -72,10 +72,17 @@ def not(opts, *rest)
# # INNER JOIN "authors" ON "authors"."id" = "posts"."author_id"
# # INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
# # WHERE "authors"."id" IS NOT NULL AND "comments"."id" IS NOT NULL
#
# Additionally, you can define joine type in the scope and associated will not use default that is joins
# Example: Post.left_joins(:author).where.associated(:author)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Example: Post.left_joins(:author).where.associated(:author)
# Post.left_joins(:author).where.associated(:author)
# # Put the SQL here

@saleh-alhaddad saleh-alhaddad force-pushed the support_join_types_in_where_associated branch from 77360db to d5545fd Compare February 21, 2024 16:02
@saleh-alhaddad
Copy link
Contributor Author

@rafaelfranca thanks updated

@rafaelfranca rafaelfranca force-pushed the support_join_types_in_where_associated branch from d5545fd to 3400aac Compare February 21, 2024 18:12
@rafaelfranca rafaelfranca merged commit 3e42d79 into rails:main Feb 21, 2024
4 checks passed
@majedbojan
Copy link

👏 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants