Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Allow Relation.where with no arguments to be chained with new query methods `not`, `like`, and `not_like` #8365

Closed
wants to merge 2 commits into from

3 participants

@claudiob
Collaborator

Similarly to #8332 this commit closes #5950 by making available three new methods not, like, and not_like that can be chained with a Relation.where with no (or nil) arguments.

The difference is that this commit follows the suggestion by José Valim #8332 (comment) to use a class and a builder, rather than a mixin, to implement WhereChain.

amatsuda and others added some commits
@amatsuda amatsuda Relation.where with no args can be chained with not, like, and not_like
examples:

  Model.where.not field: nil
  #=> "SELECT * FROM models WHERE field IS NOT NULL

  Model.where.like name: 'Jeremy%'
  #=> "SELECT * FROM models WHERE name LIKE 'Jeremy%'

this feature was originally suggested by Jeremy Kemper rails#5950 (comment)

Closes #5950
bbc4526
@claudiob claudiob 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
be71e1a
@jeremy
Owner

Thank you for implementing, @claudiob. However, let's give the original author the honors :heart:

Please do open a PR with improvements after his work is merged!

@jeremy jeremy closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Nov 29, 2012
  1. @amatsuda @claudiob

    Relation.where with no args can be chained with not, like, and not_like

    amatsuda authored claudiob committed
    examples:
    
      Model.where.not field: nil
      #=> "SELECT * FROM models WHERE field IS NOT NULL
    
      Model.where.like name: 'Jeremy%'
      #=> "SELECT * FROM models WHERE name LIKE 'Jeremy%'
    
    this feature was originally suggested by Jeremy Kemper rails#5950 (comment)
    
    Closes #5950
  2. @claudiob

    Transforms WhereChain from a mixin to a builder

    claudiob authored
    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
This page is out of date. Refresh to see the latest.
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
104 activerecord/lib/active_record/relation/query_methods.rb
@@ -4,6 +4,75 @@ module ActiveRecord
module QueryMethods
extend ActiveSupport::Concern
+ # 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
+
+ # Returns a new relation that includes only elements which do not match the conditions
+ #
+ # where_chain = WhereChain.new(User)
+ # where_chain.not(name: "Jon")
+ # # SELECT * FROM users WHERE name <> 'Jon'
+ #
+ # where_chain.not(name: nil)
+ # # SELECT * FROM users WHERE name IS NOT NULL
+ #
+ # where_chain.not(name: %(Ko1 Nobu))
+ # # SELECT * FROM users WHERE name NOT IN ('Ko1', 'Nobu')
+ def not(conditions)
+ @scope.where_values += build_where_not(conditions)
+ @scope
+ end
+
+ # Returns a new relation that includes only elements which match the pattern specified in the conditions
+ #
+ # where_chain = WhereChain.new(Book)
+ # where_chain.like(title: "Rails%")
+ # # SELECT * FROM books WHERE title LIKE 'Rails%'
+ def like(conditions)
+ @scope.where_values += build_where_like(conditions)
+ @scope
+ end
+
+ # Returns a new relation that includes only elements which do not match the pattern specified in the conditions
+ #
+ # where_chain = WhereChain.new(Conference)
+ # where_chain.not_like(name: "%Kaigi")
+ # # SELECT * FROM conferences WHERE name NOT LIKE '%Kaigi'
+ 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
+
Relation::MULTI_VALUE_METHODS.each do |name|
class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}_values # def select_values
@@ -379,20 +448,41 @@ def bind!(value)
# User.joins(:posts).where({ "posts.published" => true })
# User.joins(:posts).where({ posts: { published: true } })
#
+ # === no argument or nil
+ #
+ # 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 returns true for blank?, then where is a no-op and returns the current relation.
- def where(opts, *rest)
- opts.blank? ? self : spawn.where!(opts, *rest)
+ # 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)
+ 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, *rest)
- references!(PredicateBuilder.references(opts)) if Hash === opts
+ def where!(opts = nil, *rest)
+ if opts.nil?
+ WhereChain.new(spawn)
+ else
+ references!(PredicateBuilder.references(opts)) if Hash === opts
- self.where_values += build_where(opts, rest)
- self
+ self.where_values += build_where(opts, rest)
+ self
+ end
end
# Allows to specify a HAVING clause. Note that you can't use HAVING
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
78 activerecord/test/cases/relation/where_chain_test.rb
@@ -0,0 +1,78 @@
+require 'cases/helper'
+require 'models/post'
+require 'models/comment'
+
+module ActiveRecord
+ class WhereChainTest < ActiveRecord::TestCase
+ fixtures :posts
+
+ def test_not_eq
+ expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], 'hello')
+ relation = Post.where.not(title: 'hello')
+ assert_equal([expected], relation.where_values)
+ end
+
+ def test_not_null
+ expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], nil)
+ relation = Post.where.not(title: nil)
+ assert_equal([expected], relation.where_values)
+ end
+
+ def test_not_in
+ expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], %w[hello goodbye])
+ relation = Post.where.not(title: %w[hello goodbye])
+ assert_equal([expected], relation.where_values)
+ end
+
+ def test_association_not_eq
+ expected = Arel::Nodes::NotEqual.new(Comment.arel_table[:title], 'hello')
+ relation = Post.joins(:comments).where.not(comments: {title: 'hello'})
+ assert_equal(expected.to_sql, relation.where_values.first.to_sql)
+ end
+
+ def test_not_eq_with_preceding_where
+ 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)
+
+ expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], 'world')
+ assert_equal(expected, relation.where_values.last)
+ end
+
+ def test_not_eq_with_succeeding_where
+ 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)
+
+ expected = Arel::Nodes::Equality.new(Post.arel_table[:title], 'world')
+ assert_equal(expected, relation.where_values.last)
+ end
+
+ def test_like
+ expected = Arel::Nodes::Matches.new(Post.arel_table[:title], 'a%')
+ relation = Post.where.like(title: 'a%')
+ assert_equal([expected], relation.where_values)
+ end
+
+ def test_not_like
+ expected = Arel::Nodes::DoesNotMatch.new(Post.arel_table[:title], 'a%')
+ relation = Post.where.not_like(title: 'a%')
+ assert_equal([expected], relation.where_values)
+ end
+
+ def test_chaining_multiple
+ 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])
+
+ expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], 'ruby on rails')
+ assert_equal(expected, relation.where_values[1])
+
+ expected = Arel::Nodes::DoesNotMatch.new(Post.arel_table[:title], '% ales')
+ assert_equal(expected, relation.where_values[2])
+ end
+ end
+end
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
Something went wrong with that request. Please try again.