提交 f6fb3271 编写于 作者: R Ryuta Kamizono

Support merging option `:rewhere` to allow mergee side condition to be replaced exactly

Related #39236.

`relation.merge` method sometimes replaces mergee side condition, but
sometimes maintain both conditions unless `relation.rewhere` is used.

It is very hard to predict merging result whether mergee side condition
will be replaced or not.

One existing way is to use `relation.rewhere` for merger side relation,
but it is also hard to predict a relation will be used for `merge` in
advance, except one-time relation for `merge`.

To address that issue, I propose to support merging option `:rewhere`,
to allow mergee side condition to be replaced exactly.

That option will allow non-`rewhere` relation behaves as `rewhere`d
relation.

```ruby
david_and_mary = Author.where(id: david.id..mary.id)

# both conflict conditions exists
david_and_mary.merge(Author.where(id: bob)) # => []

# mergee side condition is replaced by rewhere
david_and_mary.merge(Author.rewhere(id: bob)) # => [bob]

# mergee side condition is replaced by rewhere option
david_and_mary.merge(Author.where(id: bob), rewhere: true) # => [bob]
```
上级 a095916d
* Support merging option `:rewhere` to allow mergee side condition to be replaced exactly.
```ruby
david_and_mary = Author.where(id: david.id..mary.id)
# both conflict conditions exists
david_and_mary.merge(Author.where(id: bob)) # => []
# mergee side condition is replaced by rewhere
david_and_mary.merge(Author.rewhere(id: bob)) # => [bob]
# mergee side condition is replaced by rewhere option
david_and_mary.merge(Author.where(id: bob), rewhere: true) # => [bob]
```
*Ryuta Kamizono*
* Add support for finding records based on signed ids, which are tamper-proof, verified ids that can be * Add support for finding records based on signed ids, which are tamper-proof, verified ids that can be
set to expire and scoped with a purpose. This is particularly useful for things like password reset set to expire and scoped with a purpose. This is particularly useful for things like password reset
or email verification, where you want the bearer of the signed id to be able to interact with the or email verification, where you want the bearer of the signed id to be able to interact with the
......
...@@ -7,15 +7,16 @@ class Relation ...@@ -7,15 +7,16 @@ class Relation
class HashMerger # :nodoc: class HashMerger # :nodoc:
attr_reader :relation, :hash attr_reader :relation, :hash
def initialize(relation, hash) def initialize(relation, hash, rewhere = nil)
hash.assert_valid_keys(*Relation::VALUE_METHODS) hash.assert_valid_keys(*Relation::VALUE_METHODS)
@relation = relation @relation = relation
@hash = hash @hash = hash
@rewhere = rewhere
end end
def merge #:nodoc: def merge
Merger.new(relation, other).merge Merger.new(relation, other, @rewhere).merge
end end
# Applying values to a relation has some side effects. E.g. # Applying values to a relation has some side effects. E.g.
...@@ -48,10 +49,11 @@ def other ...@@ -48,10 +49,11 @@ def other
class Merger # :nodoc: class Merger # :nodoc:
attr_reader :relation, :values, :other attr_reader :relation, :values, :other
def initialize(relation, other) def initialize(relation, other, rewhere = nil)
@relation = relation @relation = relation
@values = other.values @values = other.values
@other = other @other = other
@rewhere = rewhere
end end
NORMAL_VALUES = Relation::VALUE_METHODS - NORMAL_VALUES = Relation::VALUE_METHODS -
...@@ -172,7 +174,7 @@ def merge_single_values ...@@ -172,7 +174,7 @@ def merge_single_values
def merge_clauses def merge_clauses
relation.from_clause = other.from_clause if replace_from_clause? relation.from_clause = other.from_clause if replace_from_clause?
where_clause = relation.where_clause.merge(other.where_clause) where_clause = relation.where_clause.merge(other.where_clause, @rewhere)
relation.where_clause = where_clause unless where_clause.empty? relation.where_clause = where_clause unless where_clause.empty?
having_clause = relation.having_clause.merge(other.having_clause) having_clause = relation.having_clause.merge(other.having_clause)
......
...@@ -702,15 +702,10 @@ def where!(opts, *rest) # :nodoc: ...@@ -702,15 +702,10 @@ def where!(opts, *rest) # :nodoc:
# This is short-hand for <tt>unscope(where: conditions.keys).where(conditions)</tt>. # This is short-hand for <tt>unscope(where: conditions.keys).where(conditions)</tt>.
# Note that unlike reorder, we're only unscoping the named conditions -- not the entire where statement. # Note that unlike reorder, we're only unscoping the named conditions -- not the entire where statement.
def rewhere(conditions) def rewhere(conditions)
attrs = []
scope = spawn scope = spawn
where_clause = scope.build_where_clause(conditions) where_clause = scope.build_where_clause(conditions)
where_clause.each_attribute do |attr|
attrs << attr
end
scope.unscope!(where: attrs) scope.unscope!(where: where_clause.extract_attributes)
scope.where_clause += where_clause scope.where_clause += where_clause
scope scope
end end
......
...@@ -28,21 +28,22 @@ def spawn #:nodoc: ...@@ -28,21 +28,22 @@ def spawn #:nodoc:
# # => Post.where(published: true).joins(:comments) # # => Post.where(published: true).joins(:comments)
# #
# This is mainly intended for sharing common conditions between multiple associations. # This is mainly intended for sharing common conditions between multiple associations.
def merge(other) def merge(other, *rest)
if other.is_a?(Array) if other.is_a?(Array)
records & other records & other
elsif other elsif other
spawn.merge!(other) spawn.merge!(other, *rest)
else else
raise ArgumentError, "invalid argument: #{other.inspect}." raise ArgumentError, "invalid argument: #{other.inspect}."
end end
end end
def merge!(other) # :nodoc: def merge!(other, *rest) # :nodoc:
options = rest.extract_options!
if other.is_a?(Hash) if other.is_a?(Hash)
Relation::HashMerger.new(self, other).merge Relation::HashMerger.new(self, other, options[:rewhere]).merge
elsif other.is_a?(Relation) elsif other.is_a?(Relation)
Relation::Merger.new(self, other).merge Relation::Merger.new(self, other, options[:rewhere]).merge
elsif other.respond_to?(:to_proc) elsif other.respond_to?(:to_proc)
instance_exec(&other) instance_exec(&other)
else else
......
...@@ -10,21 +10,21 @@ def initialize(predicates) ...@@ -10,21 +10,21 @@ def initialize(predicates)
end end
def +(other) def +(other)
WhereClause.new( WhereClause.new(predicates + other.predicates)
predicates + other.predicates,
)
end end
def -(other) def -(other)
WhereClause.new( WhereClause.new(predicates - other.predicates)
predicates - other.predicates,
)
end end
def merge(other) def merge(other, rewhere = nil)
WhereClause.new( predicates = if rewhere
predicates_unreferenced_by(other) + other.predicates, except_predicates(other.extract_attributes)
) else
predicates_unreferenced_by(other)
end
WhereClause.new(predicates + other.predicates)
end end
def except(*columns) def except(*columns)
...@@ -98,10 +98,12 @@ def contradiction? ...@@ -98,10 +98,12 @@ def contradiction?
end end
end end
def each_attribute(&block) def extract_attributes
attrs = []
predicates.each do |node| predicates.each do |node|
Arel.fetch_attribute(node, &block) Arel.fetch_attribute(node) { |attr| attrs << attr }
end end
attrs
end end
protected protected
......
...@@ -13,6 +13,81 @@ ...@@ -13,6 +13,81 @@
class RelationMergingTest < ActiveRecord::TestCase class RelationMergingTest < ActiveRecord::TestCase
fixtures :developers, :comments, :authors, :author_addresses, :posts fixtures :developers, :comments, :authors, :author_addresses, :posts
def test_merge_in_clause
david, mary, bob = authors(:david, :mary, :bob)
david_and_mary = Author.where(id: [david, mary]).order(:id)
mary_and_bob = Author.where(id: [mary, bob]).order(:id)
assert_equal [david, mary], david_and_mary
assert_equal [mary, bob], mary_and_bob
assert_equal [mary], david_and_mary.merge(Author.where(id: mary))
assert_equal [bob], david_and_mary.merge(Author.where(id: bob))
assert_equal [mary], david_and_mary.merge(Author.rewhere(id: mary))
assert_equal [bob], david_and_mary.merge(Author.rewhere(id: bob))
assert_equal [mary], david_and_mary.merge(Author.where(id: mary), rewhere: true)
assert_equal [bob], david_and_mary.merge(Author.where(id: bob), rewhere: true)
assert_equal [mary, bob], david_and_mary.merge(mary_and_bob)
assert_equal [david, mary], mary_and_bob.merge(david_and_mary)
assert_equal [mary, bob], david_and_mary.merge(mary_and_bob, rewhere: true)
assert_equal [david, mary], mary_and_bob.merge(david_and_mary, rewhere: true)
end
def test_merge_between_clause
david, mary, bob = 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)
assert_equal [david, mary], david_and_mary
assert_equal [mary, bob], mary_and_bob
assert_equal [mary], david_and_mary.merge(Author.where(id: mary))
assert_equal [], david_and_mary.merge(Author.where(id: bob))
assert_equal [mary], david_and_mary.merge(Author.rewhere(id: mary))
assert_equal [bob], david_and_mary.merge(Author.rewhere(id: bob))
assert_equal [mary], david_and_mary.merge(Author.where(id: mary), rewhere: true)
assert_equal [bob], david_and_mary.merge(Author.where(id: bob), rewhere: true)
assert_equal [mary], david_and_mary.merge(mary_and_bob)
assert_equal [mary], mary_and_bob.merge(david_and_mary)
assert_equal [mary, bob], david_and_mary.merge(mary_and_bob, rewhere: true)
assert_equal [david, mary], mary_and_bob.merge(david_and_mary, rewhere: true)
end
def test_merge_or_clause
david, mary, bob = 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)
assert_equal [david, mary], david_and_mary
assert_equal [mary, bob], mary_and_bob
assert_equal [mary], david_and_mary.merge(Author.where(id: mary))
assert_equal [], david_and_mary.merge(Author.where(id: bob))
assert_equal [mary], david_and_mary.merge(Author.rewhere(id: mary))
assert_equal [bob], david_and_mary.merge(Author.rewhere(id: bob))
assert_equal [mary], david_and_mary.merge(Author.where(id: mary), rewhere: true)
assert_equal [bob], david_and_mary.merge(Author.where(id: bob), rewhere: true)
assert_equal [mary], david_and_mary.merge(mary_and_bob)
assert_equal [mary], mary_and_bob.merge(david_and_mary)
assert_equal [mary, bob], david_and_mary.merge(mary_and_bob, rewhere: true)
assert_equal [david, mary], mary_and_bob.merge(david_and_mary, rewhere: true)
end
def test_relation_merging def test_relation_merging
devs = Developer.where("salary >= 80000").merge(Developer.limit(2)).merge(Developer.order("id ASC").where("id < 3")) devs = Developer.where("salary >= 80000").merge(Developer.limit(2)).merge(Developer.order("id ASC").where("id < 3"))
assert_equal [developers(:david), developers(:jamis)], devs.to_a assert_equal [developers(:david), developers(:jamis)], devs.to_a
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册