Skip to content
Permalink
Browse files
Merge Pull Request #16052 Added #or to ActiveRecord::Relation
  • Loading branch information
sgrif committed Jan 28, 2015
2 parents 96ac14a + ff45b9e commit 9e42cf019f2417473e7dcbfcb885709fa2709f89
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 1 deletion.
@@ -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,9 @@ def calculate(operation, _column_name)
def exists?(_id = false)
false
end

def or(other)
other.spawn
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,37 @@ 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) # :nodoc:
unless structurally_compatible_for_or?(other)
raise ArgumentError, 'Relation passed to #or must be structurally compatible'
end

self.where_clause = self.where_clause.or(other.where_clause)
self.having_clause = self.having_clause.or(other.having_clause)

self
end

private def structurally_compatible_for_or?(other) # :nodoc:

This comment has been minimized.

Copy link
@rafaelfranca

rafaelfranca Jan 29, 2015

Member

Not cool bro 😢

This comment has been minimized.

Copy link
@rafaelfranca

rafaelfranca Jan 29, 2015

Member

Why the nodoc?

Relation::SINGLE_VALUE_METHODS.all? { |m| send("#{m}_value") == other.send("#{m}_value") } &&
(Relation::MULTI_VALUE_METHODS - [:extending]).all? { |m| send("#{m}_values") == other.send("#{m}_values") } &&
(Relation::CLAUSE_METHODS - [:having, :where]).all? { |m| send("#{m}_clause") != other.send("#{m}_clause") }
end

# Allows to specify a HAVING clause. Note that you can't use HAVING
# without also specifying a GROUP clause.
#
@@ -31,6 +31,19 @@ def except(*columns)
)
end

def or(other)
if empty?
other
elsif other.empty?
self
else
WhereClause.new(
[ast.or(other.ast)],
binds + other.binds
)
end
end

def to_h(table_name = nil)
equalities = predicates.grep(Arel::Nodes::Equality)
if table_name
@@ -0,0 +1,84 @@
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_bind_params
assert_equal Post.find([1, 2]), Post.where(id: 1).or(Post.where(id: 2)).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.where('id = 1')
assert_equal expected, Post.or(Post.where('id = 1')).to_a
end

def test_or_without_right_where
expected = Post.where('id = 1')
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
@@ -145,6 +145,26 @@ class WhereClauseTest < ActiveRecord::TestCase
assert_equal where_clause.ast, where_clause_with_empty.ast
end

test "or joins the two clauses using OR" do
where_clause = WhereClause.new([table["id"].eq(bind_param)], [attribute("id", 1)])
other_clause = WhereClause.new([table["name"].eq(bind_param)], [attribute("name", "Sean")])
expected_ast =
Arel::Nodes::Grouping.new(
Arel::Nodes::Or.new(table["id"].eq(bind_param), table["name"].eq(bind_param))
)
expected_binds = where_clause.binds + other_clause.binds

assert_equal expected_ast.to_sql, where_clause.or(other_clause).ast.to_sql
assert_equal expected_binds, where_clause.or(other_clause).binds
end

test "or does nothing with an empty where clause" do
where_clause = WhereClause.new([table["id"].eq(bind_param)], [attribute("id", 1)])

assert_equal where_clause, where_clause.or(WhereClause.empty)
assert_equal where_clause, WhereClause.empty.or(where_clause)
end

private

def table
@@ -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

34 comments on commit 9e42cf0

@gabebw
Copy link

@gabebw gabebw commented on 9e42cf0 Jan 29, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<3

@jonatack
Copy link
Contributor

@jonatack jonatack commented on 9e42cf0 Jan 29, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Gael, Olivier, Matthew and Sean for your work on this 🎉 looking forward to giving it a try with scopes/class methods.

@mcmire
Copy link
Contributor

@mcmire mcmire commented on 9e42cf0 Jan 29, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am surprised at what a small commit this is!

@sgrif
Copy link
Contributor Author

@sgrif sgrif commented on 9e42cf0 Jan 29, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's because I've been refactoring to make changes like this trivial for a few weeks.

@mdespuits
Copy link

@mdespuits mdespuits commented on 9e42cf0 Jan 29, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 😄 Been waiting for this for a few years. Thanks guys!

@nshoes
Copy link

@nshoes nshoes commented on 9e42cf0 Jan 29, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@kelvinst
Copy link

@kelvinst kelvinst commented on 9e42cf0 Jan 29, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 😍

@acutemedical
Copy link

@acutemedical acutemedical commented on 9e42cf0 Jan 29, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great!

@mechanicles
Copy link
Contributor

@mechanicles mechanicles commented on 9e42cf0 Jan 30, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome 😍 One of the most required features in active_record :)

@mentero
Copy link

@mentero mentero commented on 9e42cf0 Jan 30, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4f301c3ecbc8e0bf9fd6539c83087d36fd2c6f200a4b8209a9b3bab0e9bbefd7

@JanStevens
Copy link

@JanStevens JanStevens commented on 9e42cf0 Jan 30, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about combining and and or ? How should one do that? Can we nest or's and and's togheter?

@emaiax
Copy link

@emaiax emaiax commented on 9e42cf0 Jan 30, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

completely awesome! 😍 😎

@sgrif
Copy link
Contributor Author

@sgrif sgrif commented on 9e42cf0 Jan 30, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JanStevens Yes, you can combine as many operations as you'd like, just like all the other methods on Relation

@seuros
Copy link
Member

@seuros seuros commented on 9e42cf0 Jan 30, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💚 💚 💚 💚 💚 💚

@jordpo
Copy link

@jordpo jordpo commented on 9e42cf0 Feb 1, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

awesome - excited to use this!

@classyPimp
Copy link

@classyPimp classyPimp commented on 9e42cf0 Feb 1, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm newb, please someboy tell me how can i use this feature? shall i update smthing (im newb to Rails). thanks! p.s. i was just astonished that Rails didnt have OR operator for ORM/ and that's a really great commit!

@sgrif
Copy link
Contributor Author

@sgrif sgrif commented on 9e42cf0 Feb 1, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@classyPimp This will be part of Rails 5.0, which will ship in late 2015.

@korny
Copy link
Contributor

@korny korny commented on 9e42cf0 Feb 2, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The definition of scope :typographically_interesting using two other scopes is really interesting. Is there a way to combine two scopes using AND? If not, an .and method would be useful, too…

@sgrif
Copy link
Contributor Author

@sgrif sgrif commented on 9e42cf0 Feb 2, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@korny Post.typographcially_interesting.containing_the_letter_a would combine them with AND. Or to be more specific, where adds conditions using AND.

@korny
Copy link
Contributor

@korny korny commented on 9e42cf0 Feb 2, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I was missing the obvious 😊

@dkonayuki
Copy link

@dkonayuki dkonayuki commented on 9e42cf0 Mar 7, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 definitely gonna use this.

@keepcosmos
Copy link
Contributor

@keepcosmos keepcosmos commented on 9e42cf0 Mar 7, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@anandhak
Copy link

@anandhak anandhak commented on 9e42cf0 Apr 21, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@rmrgh
Copy link

@rmrgh rmrgh commented on 9e42cf0 Jun 23, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@andre1810
Copy link

@andre1810 andre1810 commented on 9e42cf0 Sep 1, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@kyuden
Copy link
Contributor

@kyuden kyuden commented on 9e42cf0 Sep 1, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@bf4
Copy link
Contributor

@bf4 bf4 commented on 9e42cf0 Sep 1, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, I've backported this to Rails 4.2.3 and am sharing since it was non-trivial https://gist.github.com/bf4/84cff9cc6ac8489d769e

@ClaudioFloreani
Copy link

@ClaudioFloreani ClaudioFloreani commented on 9e42cf0 Sep 24, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@mcmire
Copy link
Contributor

@mcmire mcmire commented on 9e42cf0 Sep 24, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to 👍 this commit, folks -- everyone who has commented or who has starred this repo gets a notification.

@sgrif
Copy link
Contributor Author

@sgrif sgrif commented on 9e42cf0 Sep 24, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 :trollface:

@dogyearm
Copy link

@dogyearm dogyearm commented on 9e42cf0 Oct 5, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@unused
Copy link

@unused unused commented on 9e42cf0 Apr 19, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@sampatbadhe
Copy link
Contributor

@sampatbadhe sampatbadhe commented on 9e42cf0 Jul 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@vitalyp
Copy link

@vitalyp vitalyp commented on 9e42cf0 Jul 3, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:trollface:

Please sign in to comment.