Skip to content

Commit

Permalink
ActiveRecord support for where_not
Browse files Browse the repository at this point in the history
  • Loading branch information
Jacob Burkhart committed Apr 24, 2012
1 parent 82c3aca commit 5b82762
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class CollectionProxy # :nodoc:

instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ }

delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from,
delegate :group, :order, :limit, :joins, :where, :where_not, :preload, :eager_load, :includes, :from,
:lock, :readonly, :having, :pluck, :to => :scoped

delegate :target, :load_target, :loaded?, :to => :@association
Expand Down
2 changes: 1 addition & 1 deletion activerecord/lib/active_record/querying.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module Querying
delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped
delegate :find_each, :find_in_batches, :to => :scoped
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins,
:where, :preload, :eager_load, :includes, :from, :lock, :readonly,
:where, :where_not, :preload, :eager_load, :includes, :from, :lock, :readonly,
:having, :create_with, :uniq, :references, :none, :to => :scoped
delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :to => :scoped

Expand Down
44 changes: 26 additions & 18 deletions activerecord/lib/active_record/relation/predicate_builder.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
module ActiveRecord
class PredicateBuilder # :nodoc:
def self.build_from_hash(engine, attributes, default_table)
def self.build_from_hash(engine, attributes, default_table, operator = nil)
attributes.map do |column, value|
table = default_table

if value.is_a?(Hash)
table = Arel::Table.new(column, engine)
build_from_hash(engine, value, table)
else
column = column.to_s
if operator
build(table[column.to_sym], value, operator)
else
column = column.to_s

if column.include?('.')
table_name, column = column.split('.', 2)
table = Arel::Table.new(table_name, engine)
end
if column.include?('.')
table_name, column = column.split('.', 2)
table = Arel::Table.new(table_name, engine)
end

build(table[column.to_sym], value)
build(table[column.to_sym], value)
end
end
end.flatten
end
Expand All @@ -32,11 +36,15 @@ def self.references(attributes)
end

private
def self.build(attribute, value)
def self.build(attribute, value, operator = nil)
in_pred, eq_pred = :in, :eq
if operator == :not
in_pred, eq_pred = :not_in, :not_eq
end
case value
when ActiveRecord::Relation
value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
attribute.in(value.arel.ast)
attribute.send(in_pred,value.arel.ast)
when Array, ActiveRecord::Associations::CollectionProxy
values = value.to_a.map {|x| x.is_a?(ActiveRecord::Model) ? x.id : x}
ranges, values = values.partition {|v| v.is_a?(Range)}
Expand All @@ -46,28 +54,28 @@ def self.build(attribute, value)

case values.length
when 0
attribute.eq(nil)
attribute.send(eq_pred,nil)
when 1
attribute.eq(values.first).or(attribute.eq(nil))
attribute.send(eq_pred,values.first).or(attribute.send(eq_pred,nil))
else
attribute.in(values).or(attribute.eq(nil))
attribute.send(in_pred,values).or(attribute.send(eq_pred,nil))
end
else
attribute.in(values)
attribute.send(in_pred,values)
end

array_predicates = ranges.map { |range| attribute.in(range) }
array_predicates = ranges.map { |range| attribute.send(in_pred,range) }
array_predicates << values_predicate
array_predicates.inject { |composite, predicate| composite.or(predicate) }
when Range
attribute.in(value)
attribute.send(in_pred,value)
when ActiveRecord::Model
attribute.eq(value.id)
attribute.send(eq_pred,value.id)
when Class
# FIXME: I think we need to deprecate this behavior
attribute.eq(value.name)
attribute.send(eq_pred,value.name)
else
attribute.eq(value)
attribute.send(eq_pred,value)
end
end
end
Expand Down
15 changes: 13 additions & 2 deletions activerecord/lib/active_record/relation/query_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,17 @@ def bind!(value)
self
end

def where_not(opts, *rest)
opts.blank? ? self : clone.where_not!(opts, *rest)
end

def where_not!(opts, *rest)
references!(PredicateBuilder.references(opts)) if Hash === opts

self.where_values += build_where(opts, rest, :not)
self
end

def where(opts, *rest)
opts.blank? ? self : clone.where!(opts, *rest)
end
Expand Down Expand Up @@ -452,13 +463,13 @@ def collapse_wheres(arel, wheres)
end
end

def build_where(opts, other = [])
def build_where(opts, other = [], operator = nil)
case opts
when String, Array
[@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
when Hash
attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts)
PredicateBuilder.build_from_hash(table.engine, attributes, table)
PredicateBuilder.build_from_hash(table.engine, attributes, table, operator)
else
[opts]
end
Expand Down
5 changes: 5 additions & 0 deletions activerecord/test/cases/finder_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ def test_first_bang_missing
end
end

def test_where_not
assert_equal [authors(:david)], Author.where_not(organization_id: nil).all
assert_equal [authors(:bob)], Author.where_not(name: ["David", "Mary"]).all
end

def test_model_class_responds_to_first_bang
assert Topic.first!
Topic.delete_all
Expand Down

0 comments on commit 5b82762

Please sign in to comment.