Permalink
Browse files

Added #or to ActiveRecord::Relation

    Post.where('id = 1').or(Post.where('id = 2'))
    # => SELECT * FROM posts WHERE (id = 1) OR (id = 2)

[Matthew Draper & Gael Muller]
  • Loading branch information...
matthewd authored and sgrif committed Jun 23, 2014
1 parent 56a3d5e commit b0b37942d729b6bdcd2e3178eda7fa1de203b3d0
View
@@ -1,3 +1,13 @@
* Added the `#or` method on ActiveRecord::Relation, allowing use of the OR
operator to combine WHERE or HAVING clauses.
Example:
Post.where('id = 1').or(Post.where('id = 2'))
# => SELECT * FROM posts WHERE (id = 1) OR (id = 2)
*Sean Griffin*, *Matthew Draper*, *Gael Muller*, *Olivier El Mekki*
* Don't define autosave association callbacks twice from
`accepts_nested_attributes_for`.
@@ -75,5 +75,13 @@ def calculate(operation, _column_name)
def exists?(_id = false)
false
end
def or(other)
if other.is_a?(NullRelation)
super
else
other.or(self)
end
end
end
end
@@ -7,7 +7,7 @@ module Querying
delegate :find_by, :find_by!, to: :all
delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all
delegate :find_each, :find_in_batches, to: :all
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins,
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :or,
:where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly,
:having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all
delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all
@@ -582,6 +582,65 @@ def rewhere(conditions)
unscope(where: conditions.keys).where(conditions)
end
# Returns a new relation, which is the logical union of this relation and the one passed as an
# argument.
#
# The two relations must be structurally compatible: they must be scoping the same model, and
# they must differ only by +where+ (if no +group+ has been defined) or +having+ (if a +group+ is
# present). Neither relation may have a +limit+, +offset+, or +uniq+ set.
#
# Post.where("id = 1").or(Post.where("id = 2"))
# # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2'))
#
def or(other)
spawn.or!(other)
end
def or!(other)
combining = group_values.any? ? :having : :where
unless structurally_compatible?(other, combining)
raise ArgumentError, 'Relation passed to #or must be structurally compatible'
end
unless other.is_a?(NullRelation)
left_values = send("#{combining}_values")
right_values = other.send("#{combining}_values")
common = left_values & right_values
mine = left_values - common
theirs = right_values - common
if mine.any? && theirs.any?
mine = mine.map { |x| String === x ? Arel.sql(x) : x }
theirs = theirs.map { |x| String === x ? Arel.sql(x) : x }
mine = [Arel::Nodes::And.new(mine)] if mine.size > 1
theirs = [Arel::Nodes::And.new(theirs)] if theirs.size > 1
common << Arel::Nodes::Or.new(mine.first, theirs.first)
end
send("#{combining}_values=", common)
end
self
end
def structurally_compatible?(other, allowed_to_vary)
Relation::SINGLE_VALUE_METHODS.all? do |name|
send("#{name}_value") == other.send("#{name}_value")
end &&
(Relation::MULTI_VALUE_METHODS - [allowed_to_vary, :extending]).all? do |name|
send("#{name}_values") == other.send("#{name}_values")
end &&
(extending_values - [NullRelation]) == (other.extending_values - [NullRelation]) &&
!limit_value &&
!offset_value &&
!uniq_value
end
private :structurally_compatible?
# Allows to specify a HAVING clause. Note that you can't use HAVING
# without also specifying a GROUP clause.
#
@@ -0,0 +1,81 @@
require "cases/helper"
require 'models/post'
module ActiveRecord
class OrTest < ActiveRecord::TestCase
fixtures :posts
def test_or_with_relation
expected = Post.where('id = 1 or id = 2').to_a
assert_equal expected, Post.where('id = 1').or(Post.where('id = 2')).to_a
end
def test_or_identity
expected = Post.where('id = 1').to_a
assert_equal expected, Post.where('id = 1').or(Post.where('id = 1')).to_a
end
def test_or_with_null_left
expected = Post.where('id = 1').to_a
assert_equal expected, Post.none.or(Post.where('id = 1')).to_a
end
def test_or_with_null_right
expected = Post.where('id = 1').to_a
assert_equal expected, Post.where('id = 1').or(Post.none).to_a
end
def test_or_with_null_both
expected = Post.none.to_a
assert_equal expected, Post.none.or(Post.none).to_a
end
def test_or_without_left_where
expected = Post.all.to_a
assert_equal expected, Post.or(Post.where('id = 1')).to_a
end
def test_or_without_right_where
expected = Post.all.to_a
assert_equal expected, Post.where('id = 1').or(Post.all).to_a
end
def test_or_preserves_other_querying_methods
expected = Post.where('id = 1 or id = 2 or id = 3').order('body asc').to_a
partial = Post.order('body asc')
assert_equal expected, partial.where('id = 1').or(partial.where(:id => [2, 3])).to_a
assert_equal expected, Post.order('body asc').where('id = 1').or(Post.order('body asc').where(:id => [2, 3])).to_a
end
def test_or_with_incompatible_relations
assert_raises ArgumentError do
Post.order('body asc').where('id = 1').or(Post.order('id desc').where(:id => [2, 3])).to_a
end
end
def test_or_when_grouping
groups = Post.where('id < 10').group('body').select('body, COUNT(*) AS c')
expected = groups.having("COUNT(*) > 1 OR body like 'Such%'").to_a.map {|o| [o.body, o.c] }
assert_equal expected, groups.having('COUNT(*) > 1').or(groups.having("body like 'Such%'")).to_a.map {|o| [o.body, o.c] }
end
def test_or_with_named_scope
expected = Post.where("id = 1 or body LIKE '\%a\%'").to_a
assert_equal expected, Post.where('id = 1').or(Post.containing_the_letter_a)
end
def test_or_inside_named_scope
expected = Post.where("body LIKE '\%a\%' OR title LIKE ?", "%'%").order('id DESC').to_a
assert_equal expected, Post.order(id: :desc).typographically_interesting
end
def test_or_on_loaded_relation
expected = Post.where('id = 1 or id = 2').to_a
p = Post.where('id = 1')
p.load
assert_equal p.loaded?, true
assert_equal expected, p.or(Post.where('id = 2')).to_a
end
end
end
@@ -18,6 +18,7 @@ def greeting
end
scope :containing_the_letter_a, -> { where("body LIKE '%a%'") }
scope :titled_with_an_apostrophe, -> { where("title LIKE '%''%'") }
scope :ranked_by_comments, -> { order("comments_count DESC") }
scope :limit_by, lambda {|l| limit(l) }
@@ -43,6 +44,8 @@ def first_comment
scope :tagged_with, ->(id) { joins(:taggings).where(taggings: { tag_id: id }) }
scope :tagged_with_comment, ->(comment) { joins(:taggings).where(taggings: { comment: comment }) }
scope :typographically_interesting, -> { containing_the_letter_a.or(titled_with_an_apostrophe) }
has_many :comments do
def find_most_recent
order("id DESC").first

0 comments on commit b0b3794

Please sign in to comment.