Skip to content
This repository

ActiveRecord support for where_not #5950

Closed
wants to merge 1 commit into from
Jacob Burkhart

It's incredibly annoying to negate things in Active Record! I can do:

Person.where(:name => "Jacob")

but to negate it, I'd have to use a SQL fragment like this:

Person.where("name <> ? ", "Jacob")

I want support:

Person.where(:name.not => "Jacob")

(see: #5948)

But barring that, let's support:

Person.where_not(:name => "Jacob")
Jeremy Friesen
jeremyf commented May 08, 2012

Also closes #5160

Jeremy Friesen
jeremyf commented May 08, 2012

@jacobo Unfortunatley the tests do not cleanly merge with master. Could you resolve the merge and let me know.

Jacob Burkhart
jacobo commented May 08, 2012

cool, I'm glad to see interest. Will work on this soon.

How do you feel about a "where_like" method being included also?

Jeremy Friesen
jeremyf commented May 08, 2012

@jacobo Get the rebase passing, and keep the pull request to a single focus; If the rails core team merges the "where_not" then by all means pursue the "where_like"

Jeremy Kemper
Owner
jeremy commented May 08, 2012

Perhaps a Model.where with no args should be chainable: Model.where.not field: nil or Model.where.like name: 'John%'

Celestino Gomes

where should be chainable. I like it!

Akira Matsuda
Collaborator

Perhaps a Model.where with no args should be chainable: Model.where.not field: nil or Model.where.like name: 'John%'

I added this feature to my existing "everywhere" gem as a proof of concept. amatsuda/everywhere@v1.0.1...v2.0.0
You can try this syntax just by adding 'everywhere' to your existing Rails 3 or 4 app's Gemfile.

@jeremy
Does this implementation satisfy your spec?

@tenderlove @jonleighton
And of course I'm willing to push this feature into AR core. Any thoughts? Shall I go ahead and send a PR?

Aaron Patterson
Owner

@amatsuda seems good. I'd rather we have chainable where than monkeypatch symbol! :+1:

Jeremy Kemper
Owner
jeremy commented June 21, 2012

@amatsuda Yes, very nice! Clean implementation, too. :+1:

Carlos Antonio da Silva

@amatsuda really cool, thanks for that :)

Carlos Antonio da Silva

@amatsuda do you have any thoughts whether we're going to be able add support for this? Thanks

Akira Matsuda
Collaborator

@carlosantoniodasilva Yeah, I'm already having a working branch in my local machine. I'll add some tests and documents, then PR within few days.

Tomasz Pajor

+1

Carlos Antonio da Silva

@amatsuda alright, great! That'd be a great addition for sure :)

Jon Leighton

Chainable where also gets a :+1: from me

Prem Sichanugrist
Collaborator

@amatsuda すごい!

Would you mind finish up the test, doc change, so we can get this bad boy in before Rails 4 release? :D

Kostyantyn Stepanyuk

What are you thinking about implementation of #gt, #gte and etc methods which mongoid orm has. As for me, this syntax is better than "where_#{method_name}"

Francesco Rodríguez

@amatsuda Hi, did you open a pull request with your implementation? Can we close this PR? Thanks.

Akira Matsuda amatsuda referenced this pull request from a commit November 26, 2012
Commit has since been removed from the repository and is no longer available.
Akira Matsuda amatsuda referenced this pull request from a commit November 26, 2012
Commit has since been removed from the repository and is no longer available.
Claudio B. claudiob referenced this pull request from a commit in claudiob/rails November 05, 2012
Akira Matsuda 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
Claudio B. claudiob referenced this pull request from a commit November 28, 2012
Commit has since been removed from the repository and is no longer available.
Claudio B. claudiob referenced this pull request from a commit November 28, 2012
Commit has since been removed from the repository and is no longer available.
Claudio B. claudiob referenced this pull request from a commit November 28, 2012
Commit has since been removed from the repository and is no longer available.
Claudio B. claudiob referenced this pull request from a commit in claudiob/rails November 28, 2012
Claudio B. 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
Akira Matsuda amatsuda referenced this pull request from a commit in amatsuda/rails November 05, 2012
Akira Matsuda 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
1d4e319
Jeremy Kemper
Owner

Moved to #8332

Jeremy Kemper jeremy closed this November 29, 2012
Akira Matsuda amatsuda referenced this pull request from a commit November 29, 2012
Commit has since been removed from the repository and is no longer available.
Akira Matsuda amatsuda referenced this pull request from a commit in amatsuda/rails November 05, 2012
Akira Matsuda 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
de75af7
Adrien Lamothe

What about when mixing WHERE and NOT clauses? Tried chaining them, which doesn't work. Don't see any example of combining/chaining where and where.not anywhere in the documentation.

Robin Dupret
Collaborator

@Alamoz : What about this example?

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

Showing 1 unique commit by 1 author.

Apr 23, 2012
Jacob Burkhart ActiveRecord support for where_not 5b82762
This page is out of date. Refresh to see the latest.
2  activerecord/lib/active_record/associations/collection_proxy.rb
@@ -38,7 +38,7 @@ class CollectionProxy # :nodoc:
38 38
 
39 39
       instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ }
40 40
 
41  
-      delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from,
  41
+      delegate :group, :order, :limit, :joins, :where, :where_not, :preload, :eager_load, :includes, :from,
42 42
                :lock, :readonly, :having, :pluck, :to => :scoped
43 43
 
44 44
       delegate :target, :load_target, :loaded?, :to => :@association
2  activerecord/lib/active_record/querying.rb
@@ -9,7 +9,7 @@ module Querying
9 9
     delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped
10 10
     delegate :find_each, :find_in_batches, :to => :scoped
11 11
     delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins,
12  
-             :where, :preload, :eager_load, :includes, :from, :lock, :readonly,
  12
+             :where, :where_not, :preload, :eager_load, :includes, :from, :lock, :readonly,
13 13
              :having, :create_with, :uniq, :references, :none, :to => :scoped
14 14
     delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :to => :scoped
15 15
 
44  activerecord/lib/active_record/relation/predicate_builder.rb
... ...
@@ -1,6 +1,6 @@
1 1
 module ActiveRecord
2 2
   class PredicateBuilder # :nodoc:
3  
-    def self.build_from_hash(engine, attributes, default_table)
  3
+    def self.build_from_hash(engine, attributes, default_table, operator = nil)
4 4
       attributes.map do |column, value|
5 5
         table = default_table
6 6
 
@@ -8,14 +8,18 @@ def self.build_from_hash(engine, attributes, default_table)
8 8
           table = Arel::Table.new(column, engine)
9 9
           build_from_hash(engine, value, table)
10 10
         else
11  
-          column = column.to_s
  11
+          if operator
  12
+            build(table[column.to_sym], value, operator)
  13
+          else
  14
+            column = column.to_s
12 15
 
13  
-          if column.include?('.')
14  
-            table_name, column = column.split('.', 2)
15  
-            table = Arel::Table.new(table_name, engine)
16  
-          end
  16
+            if column.include?('.')
  17
+              table_name, column = column.split('.', 2)
  18
+              table = Arel::Table.new(table_name, engine)
  19
+            end
17 20
 
18  
-          build(table[column.to_sym], value)
  21
+            build(table[column.to_sym], value)
  22
+          end
19 23
         end
20 24
       end.flatten
21 25
     end
@@ -32,11 +36,15 @@ def self.references(attributes)
32 36
     end
33 37
 
34 38
     private
35  
-      def self.build(attribute, value)
  39
+      def self.build(attribute, value, operator = nil)
  40
+        in_pred, eq_pred = :in, :eq
  41
+        if operator == :not
  42
+          in_pred, eq_pred = :not_in, :not_eq
  43
+        end
36 44
         case value
37 45
         when ActiveRecord::Relation
38 46
           value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
39  
-          attribute.in(value.arel.ast)
  47
+          attribute.send(in_pred,value.arel.ast)
40 48
         when Array, ActiveRecord::Associations::CollectionProxy
41 49
           values = value.to_a.map {|x| x.is_a?(ActiveRecord::Model) ? x.id : x}
42 50
           ranges, values = values.partition {|v| v.is_a?(Range)}
@@ -46,28 +54,28 @@ def self.build(attribute, value)
46 54
 
47 55
             case values.length
48 56
             when 0
49  
-              attribute.eq(nil)
  57
+              attribute.send(eq_pred,nil)
50 58
             when 1
51  
-              attribute.eq(values.first).or(attribute.eq(nil))
  59
+              attribute.send(eq_pred,values.first).or(attribute.send(eq_pred,nil))
52 60
             else
53  
-              attribute.in(values).or(attribute.eq(nil))
  61
+              attribute.send(in_pred,values).or(attribute.send(eq_pred,nil))
54 62
             end
55 63
           else
56  
-            attribute.in(values)
  64
+            attribute.send(in_pred,values)
57 65
           end
58 66
 
59  
-          array_predicates = ranges.map { |range| attribute.in(range) }
  67
+          array_predicates = ranges.map { |range| attribute.send(in_pred,range) }
60 68
           array_predicates << values_predicate
61 69
           array_predicates.inject { |composite, predicate| composite.or(predicate) }
62 70
         when Range
63  
-          attribute.in(value)
  71
+          attribute.send(in_pred,value)
64 72
         when ActiveRecord::Model
65  
-          attribute.eq(value.id)
  73
+          attribute.send(eq_pred,value.id)
66 74
         when Class
67 75
           # FIXME: I think we need to deprecate this behavior
68  
-          attribute.eq(value.name)
  76
+          attribute.send(eq_pred,value.name)
69 77
         else
70  
-          attribute.eq(value)
  78
+          attribute.send(eq_pred,value)
71 79
         end
72 80
       end
73 81
   end
15  activerecord/lib/active_record/relation/query_methods.rb
@@ -194,6 +194,17 @@ def bind!(value)
194 194
       self
195 195
     end
196 196
 
  197
+    def where_not(opts, *rest)
  198
+      opts.blank? ? self : clone.where_not!(opts, *rest)
  199
+    end
  200
+
  201
+    def where_not!(opts, *rest)
  202
+      references!(PredicateBuilder.references(opts)) if Hash === opts
  203
+
  204
+      self.where_values += build_where(opts, rest, :not)
  205
+      self
  206
+    end
  207
+
197 208
     def where(opts, *rest)
198 209
       opts.blank? ? self : clone.where!(opts, *rest)
199 210
     end
@@ -452,13 +463,13 @@ def collapse_wheres(arel, wheres)
452 463
       end
453 464
     end
454 465
 
455  
-    def build_where(opts, other = [])
  466
+    def build_where(opts, other = [], operator = nil)
456 467
       case opts
457 468
       when String, Array
458 469
         [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
459 470
       when Hash
460 471
         attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts)
461  
-        PredicateBuilder.build_from_hash(table.engine, attributes, table)
  472
+        PredicateBuilder.build_from_hash(table.engine, attributes, table, operator)
462 473
       else
463 474
         [opts]
464 475
       end
5  activerecord/test/cases/finder_test.rb
@@ -229,6 +229,11 @@ def test_first_bang_missing
229 229
     end
230 230
   end
231 231
 
  232
+  def test_where_not
  233
+    assert_equal [authors(:david)], Author.where_not(organization_id: nil).all
  234
+    assert_equal [authors(:bob)], Author.where_not(name: ["David", "Mary"]).all
  235
+  end
  236
+
232 237
   def test_model_class_responds_to_first_bang
233 238
     assert Topic.first!
234 239
     Topic.delete_all
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.