1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Support relation.and for intersection as Set theory

As described at #39328, `relation.merge` behaves inspired as Hash-like
merge for where clause. In other words, currently there is no official
way to intersect the result by both relation conditions (i.e. there is
no official way to maintain both relation conditions).

To resolve that issue, I'd like to support a way to intersect relations
as `relation.and`.

```ruby
david_and_mary = Author.where(id: [david, mary])
mary_and_bob   = Author.where(id: [mary, bob]) # => [bob]

david_and_mary.merge(mary_and_bob) # => [mary, bob]

david_and_mary.and(mary_and_bob) # => [mary]
david_and_mary.or(mary_and_bob)  # => [david, mary, bob]
```

Fixes #39232.
This commit is contained in:
Ryuta Kamizono 2020-06-07 06:49:42 +09:00
parent 159cc3421c
commit 7219eb2cd2
5 changed files with 81 additions and 12 deletions

View file

@ -1,3 +1,17 @@
* Support `relation.and` for intersection as Set theory.
```ruby
david_and_mary = Author.where(id: [david, mary])
mary_and_bob = Author.where(id: [mary, bob]) # => [bob]
david_and_mary.merge(mary_and_bob) # => [mary, bob]
david_and_mary.and(mary_and_bob) # => [mary]
david_and_mary.or(mary_and_bob) # => [david, mary, bob]
```
*Ryuta Kamizono*
* Merging conditions on the same column no longer maintain both conditions,
and will be consistently replaced by the latter condition in Rails 6.2.
To migrate to Rails 6.2's behavior, use `relation.merge(other, rewhere: true)`.

View file

@ -13,9 +13,10 @@ module ActiveRecord
:destroy_all, :delete_all, :update_all, :touch_all, :destroy_by, :delete_by,
:find_each, :find_in_batches, :in_batches,
:select, :reselect, :order, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins,
:where, :rewhere, :preload, :extract_associated, :eager_load, :includes, :from, :lock, :readonly, :extending, :or,
:having, :create_with, :distinct, :references, :none, :unscope, :optimizer_hints, :merge, :except, :only,
:count, :average, :minimum, :maximum, :sum, :calculate, :annotate,
:where, :rewhere, :preload, :extract_associated, :eager_load, :includes, :from, :lock, :readonly,
:and, :or, :annotate, :optimizer_hints, :extending,
:having, :create_with, :distinct, :references, :none, :unscope, :merge, :except, :only,
:count, :average, :minimum, :maximum, :sum, :calculate,
:pluck, :pick, :ids, :strict_loading
].freeze # :nodoc:
delegate(*QUERYING_METHODS, to: :all)

View file

@ -699,6 +699,38 @@ module ActiveRecord
scope
end
# Returns a new relation, which is the logical intersection of this relation and the one passed
# as an argument.
#
# The two relations must be structurally compatible: they must be scoping the same model, and
# they must differ only by #where (if no #group has been defined) or #having (if a #group is
# present).
#
# Post.where(id: [1, 2]).and(Post.where(id: [2, 3]))
# # SELECT `posts`.* FROM `posts` WHERE `posts`.`id` IN (1, 2) AND `posts`.`id` IN (2, 3)
#
def and(other)
if other.is_a?(Relation)
spawn.and!(other)
else
raise ArgumentError, "You have passed #{other.class.name} object to #and. Pass an ActiveRecord::Relation object instead."
end
end
def and!(other) # :nodoc:
incompatible_values = structurally_incompatible_values_for_or(other)
unless incompatible_values.empty?
raise ArgumentError, "Relation passed to #and must be structurally compatible. Incompatible values: #{incompatible_values}"
end
self.where_clause |= other.where_clause
self.having_clause |= other.having_clause
self.references_values |= other.references_values
self
end
# Returns a new relation, which is the logical union of this relation and the one passed as an
# argument.
#
@ -710,11 +742,11 @@ module ActiveRecord
# # SELECT `posts`.* FROM `posts` WHERE ((id = 1) OR (author_id = 3))
#
def or(other)
unless other.is_a? Relation
if other.is_a?(Relation)
spawn.or!(other)
else
raise ArgumentError, "You have passed #{other.class.name} object to #or. Pass an ActiveRecord::Relation object instead."
end
spawn.or!(other)
end
def or!(other) # :nodoc:

View file

@ -19,6 +19,10 @@ module ActiveRecord
WhereClause.new(predicates - other.predicates)
end
def |(other)
WhereClause.new(predicates | other.predicates)
end
def merge(other, rewhere = nil)
predicates = if rewhere
except_predicates(other.extract_attributes)

View file

@ -14,7 +14,7 @@ class RelationMergingTest < ActiveRecord::TestCase
fixtures :developers, :comments, :authors, :author_addresses, :posts
def test_merge_in_clause
david, mary, bob = authors(:david, :mary, :bob)
david, mary, bob = authors = authors(:david, :mary, :bob)
david_and_mary = Author.where(id: [david, mary]).order(:id)
mary_and_bob = Author.where(id: [mary, bob]).order(:id)
@ -35,18 +35,24 @@ class RelationMergingTest < ActiveRecord::TestCase
assert_equal [mary, bob], david_and_mary.merge(mary_and_bob)
assert_equal [mary, bob], david_and_mary.merge(mary_and_bob, rewhere: true)
assert_equal [mary], david_and_mary.and(mary_and_bob)
assert_equal authors, david_and_mary.or(mary_and_bob)
assert_equal [david, mary], mary_and_bob.merge(david_and_mary)
assert_equal [david, mary], mary_and_bob.merge(david_and_mary, rewhere: true)
assert_equal [mary], david_and_mary.and(mary_and_bob)
assert_equal authors, david_and_mary.or(mary_and_bob)
david_and_bob = Author.where(id: david).or(Author.where(name: "Bob"))
david_and_bob = Author.where(id: david).or(Author.where(name: "Bob")).order(:id)
assert_equal [david], david_and_mary.merge(david_and_bob)
assert_equal [david], david_and_mary.merge(david_and_bob, rewhere: true)
assert_equal [david], david_and_mary.and(david_and_bob)
assert_equal authors, david_and_mary.or(david_and_bob)
end
def test_merge_between_clause
david, mary, bob = authors(:david, :mary, :bob)
david, mary, bob = authors = authors(:david, :mary, :bob)
david_and_mary = Author.where(id: david.id..mary.id).order(:id)
mary_and_bob = Author.where(id: mary.id..bob.id).order(:id)
@ -80,20 +86,26 @@ class RelationMergingTest < ActiveRecord::TestCase
assert_equal [mary], david_and_mary.merge(mary_and_bob)
end
assert_equal [mary, bob], david_and_mary.merge(mary_and_bob, rewhere: true)
assert_equal [mary], david_and_mary.and(mary_and_bob)
assert_equal authors, david_and_mary.or(mary_and_bob)
assert_deprecated(message) do
assert_equal [mary], mary_and_bob.merge(david_and_mary)
end
assert_equal [david, mary], mary_and_bob.merge(david_and_mary, rewhere: true)
assert_equal [mary], david_and_mary.and(mary_and_bob)
assert_equal authors, david_and_mary.or(mary_and_bob)
david_and_bob = Author.where(id: david).or(Author.where(name: "Bob"))
david_and_bob = Author.where(id: david).or(Author.where(name: "Bob")).order(:id)
assert_equal [david], david_and_mary.merge(david_and_bob)
assert_equal [david], david_and_mary.merge(david_and_bob, rewhere: true)
assert_equal [david], david_and_mary.and(david_and_bob)
assert_equal authors, david_and_mary.or(david_and_bob)
end
def test_merge_or_clause
david, mary, bob = authors(:david, :mary, :bob)
david, mary, bob = authors = authors(:david, :mary, :bob)
david_and_mary = Author.where(id: david).or(Author.where(id: mary)).order(:id)
mary_and_bob = Author.where(id: mary).or(Author.where(id: bob)).order(:id)
@ -127,16 +139,22 @@ class RelationMergingTest < ActiveRecord::TestCase
assert_equal [mary], david_and_mary.merge(mary_and_bob)
end
assert_equal [mary, bob], david_and_mary.merge(mary_and_bob, rewhere: true)
assert_equal [mary], david_and_mary.and(mary_and_bob)
assert_equal authors, david_and_mary.or(mary_and_bob)
assert_deprecated(message) do
assert_equal [mary], mary_and_bob.merge(david_and_mary)
end
assert_equal [david, mary], mary_and_bob.merge(david_and_mary, rewhere: true)
assert_equal [mary], david_and_mary.and(mary_and_bob)
assert_equal authors, david_and_mary.or(mary_and_bob)
david_and_bob = Author.where(id: david).or(Author.where(name: "Bob"))
david_and_bob = Author.where(id: david).or(Author.where(name: "Bob")).order(:id)
assert_equal [david], david_and_mary.merge(david_and_bob)
assert_equal [david], david_and_mary.merge(david_and_bob, rewhere: true)
assert_equal [david], david_and_mary.and(david_and_bob)
assert_equal authors, david_and_mary.or(david_and_bob)
end
def test_merge_not_in_clause