Skip to content
Browse files

Transforms WhereChain from a mixin to a builder

This feature was originally suggested by José Valim rails#8332 (comment)

The original commit fddf9c2 by Akira Matsuda enabled the methods #not, #like,
and #not_like to be chained to a Relation.where with no args.
This commit maintains the same behavior, but requires using #where before
chaining any of the previous methods, which makes chaining clearer.

Closes #5950
  • Loading branch information...
1 parent bbc4526 commit be71e1a7a1048f1dfa68bf73a487228f251b7be5 @claudiob committed Nov 28, 2012
View
9 activerecord/CHANGELOG.md
@@ -1,5 +1,14 @@
## Rails 4.0.0 (unreleased) ##
+* Allow Relation.where with no arguments to be chained with new query methods
+ `not`, `like`, and `not_like`.
+
+ Example:
+
+ User.where.not(name: "Akira").where.not_like(name: "claudio%")
+
+ *Akira Matsuda* and *claudiob*
+
* Add STI support to init and building associations.
Allows you to do BaseClass.new(:type => "SubClass") as well as
parent.children.build(:type => "SubClass") or parent.build_child
View
127 activerecord/lib/active_record/relation/query_methods.rb
@@ -4,70 +4,72 @@ module ActiveRecord
module QueryMethods
extend ActiveSupport::Concern
- module WhereChain
- module NotBuilder
- private
- def build_where(opts, other = [])
- super.map do |rel|
- case rel
- when Arel::Nodes::Equality
- Arel::Nodes::NotEqual.new rel.left, rel.right
- when Arel::Nodes::In
- Arel::Nodes::NotIn.new rel.left, rel.right
- when String
- Arel::Nodes::Not.new Arel::Nodes::SqlLiteral.new(rel)
- else
- Arel::Nodes::Not.new rel
- end
- end
- end
+ # WhereChain objects act as placeholder for queries in which #where does not have any parameter.
+ # In this case, #where must be chained with either #not, #like, or #not_like to return a new relation.
+ class WhereChain
+ def initialize(scope)
+ @scope = scope
end
- module LikeBuilder
- private
- def build_where(opts, other = [])
- super.map {|r| Arel::Nodes::Matches.new r.left, r.right}
- end
- end
-
- module NotLikeBuilder
- private
- def build_where(opts, other = [])
- super.map {|r| Arel::Nodes::DoesNotMatch.new r.left, r.right}
- end
- end
-
- # Returns a new relation expressing WHERE + NOT condition
- # according to the conditions in the arguments.
+ # Returns a new relation that includes only elements which do not match the conditions
#
- # User.where.not(name: "Jon")
+ # where_chain = WhereChain.new(User)
+ # where_chain.not(name: "Jon")
# # SELECT * FROM users WHERE name <> 'Jon'
#
- # User.where.not(name: nil)
+ # where_chain.not(name: nil)
# # SELECT * FROM users WHERE name IS NOT NULL
#
- # User.where.not(name: %(Ko1 Nobu))
+ # where_chain.not(name: %(Ko1 Nobu))
# # SELECT * FROM users WHERE name NOT IN ('Ko1', 'Nobu')
- def not(opts, *rest)
- extend(NotBuilder).where(opts, *rest).dup
+ def not(conditions)
+ @scope.where_values += build_where_not(conditions)
+ @scope
end
- # Returns a new relation expressing WHERE + LIKE condition
- # according to the conditions in the arguments.
+ # Returns a new relation that includes only elements which match the pattern specified in the conditions
#
- # Book.where.like(title: "Rails%")
+ # where_chain = WhereChain.new(Book)
+ # where_chain.like(title: "Rails%")
# # SELECT * FROM books WHERE title LIKE 'Rails%'
- def like(opts, *rest)
- extend(LikeBuilder).where(opts, *rest).dup
+ def like(conditions)
+ @scope.where_values += build_where_like(conditions)
+ @scope
end
- # Returns a new relation expressing WHERE + NOT LIKE condition
- # according to the conditions in the arguments.
+ # Returns a new relation that includes only elements which do not match the pattern specified in the conditions
#
- # Conference.where.not_like(name: "%Kaigi")
+ # where_chain = WhereChain.new(Conference)
+ # where_chain.not_like(name: "%Kaigi")
# # SELECT * FROM conferences WHERE name NOT LIKE '%Kaigi'
- def not_like(opts, *rest)
- extend(NotLikeBuilder).where(opts, *rest).dup
+ def not_like(conditions)
+ @scope.where_values += build_where_not_like(conditions)
+ @scope
+ end
+
+ private
+
+ def build_where_not(conditions)
+ @scope.send(:build_where, conditions).map do |rel|
+ case rel
+ when Arel::Nodes::Equality
+ Arel::Nodes::NotEqual.new rel.left, rel.right
+ when Arel::Nodes::In
+ Arel::Nodes::NotIn.new rel.left, rel.right
+ when String
+ Arel::Nodes::Not.new Arel::Nodes::SqlLiteral.new(rel)
+ else
+ Arel::Nodes::Not.new rel
+ end
+ end
+ end
+
+ def build_where_like(conditions)
+ @scope.send(:build_where, conditions).map {|rel| Arel::Nodes::Matches.new rel.left, rel.right}
+ end
+
+ def build_where_not_like(conditions)
+ @scope.send(:build_where, conditions).map {|rel| Arel::Nodes::DoesNotMatch.new rel.left, rel.right}
end
end
@@ -448,28 +450,33 @@ def bind!(value)
#
# === no argument or nil
#
- # If <tt>where</tt> was called with no argument, it returns a special relation that can be
- # chained with <tt>not</tt>, <tt>like</tt>, and <tt>not_like</tt> query methods.
+ # If no argument or nil is passed, #where returns a new instance of WhereChain which, when chained with either
+ # #not, #like, or #not_like, returns a new relation.
+ #
+ # User.where.not(name: "Jon")
+ # # SELECT * FROM users WHERE name <> 'Jon'
+ #
+ # Book.where.like(title: "Rails%")
+ # # SELECT * FROM books WHERE title LIKE 'Rails%'
+ #
+ # Conference.where.not_like(name: "%Kaigi")
+ # # SELECT * FROM conferences WHERE name NOT LIKE '%Kaigi'
+ #
+ # See WhereChain for more details on #not, #like, and #not_like.
+ #
#
# === empty condition
#
- # If the condition is any other blank-ish object than nil, then where is a # no-op and returns
- # the current relation.
+ # If any blank object other than nil is passed, then #where is a no-op and returns the current relation.
def where(opts = nil, *rest)
- if opts.nil?
- spawn.extend(WhereChain)
- elsif opts.blank?
- self
- else
- spawn.where!(opts, *rest)
- end
+ opts.blank? && !opts.nil? ? self : spawn.where!(opts, *rest)
end
# #where! is identical to #where, except that instead of returning a new relation, it adds
# the condition to the existing relation.
def where!(opts = nil, *rest)
if opts.nil?
- self.extend(WhereChain)
+ WhereChain.new(spawn)
else
references!(PredicateBuilder.references(opts)) if Hash === opts
View
2 activerecord/test/cases/associations/has_many_associations_test.rb
@@ -299,7 +299,7 @@ def test_finding_array_compatibility
end
def test_find_with_blank_conditions
- [[], {}, nil, ""].each do |blank|
+ [[], {}, ""].each do |blank|
assert_equal 2, Firm.all.merge!(:order => "id").first.clients.where(blank).to_a.size
end
end
View
6 activerecord/test/cases/relation/where_chain_test.rb
@@ -31,7 +31,7 @@ def test_association_not_eq
end
def test_not_eq_with_preceding_where
- relation = Post.where(:title => 'hello').where.not(:title => 'world')
+ relation = Post.where(title: 'hello').where.not(title: 'world')
expected = Arel::Nodes::Equality.new(Post.arel_table[:title], 'hello')
assert_equal(expected, relation.where_values.first)
@@ -41,7 +41,7 @@ def test_not_eq_with_preceding_where
end
def test_not_eq_with_succeeding_where
- relation = Post.where.not(:title => 'hello').where(:title => 'world')
+ relation = Post.where.not(title: 'hello').where(title: 'world')
expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], 'hello')
assert_equal(expected, relation.where_values.first)
@@ -63,7 +63,7 @@ def test_not_like
end
def test_chaining_multiple
- relation = Post.where.like(:title => 'ruby on %').where.not(:title => 'ruby on rails').where.not_like(:title => '% ales')
+ relation = Post.where.like(title: 'ruby on %').where.not(title: 'ruby on rails').where.not_like(title: '% ales')
expected = Arel::Nodes::Matches.new(Post.arel_table[:title], 'ruby on %')
assert_equal(expected, relation.where_values[0])
View
7 activerecord/test/cases/relation/where_test.rb
@@ -85,5 +85,12 @@ def test_where_with_table_name_and_empty_hash
def test_where_with_empty_hash_and_no_foreign_key
assert_equal 0, Edge.where(:sink => {}).count
end
+
+ def test_where_with_blank_condition
+ expected = Post.all
+ actual = Post.where('')
+
+ assert_equal expected.to_sql, actual.to_sql
+ end
end
end

0 comments on commit be71e1a

Please sign in to comment.
Something went wrong with that request. Please try again.