Skip to content
Create a gist now

Instantly share code, notes, and snippets.

@j-mcnally /or_scopes.rb secret
Last active May 25, 2016

OR'ing scopes
module ActiveRecord
module Querying
delegate :or, :to => :all
end
end
module ActiveRecord
module QueryMethods
# OrChain objects act as placeholder for queries in which #or does not have any parameter.
# In this case, #or must be chained with any other relation method to return a new relation.
# It is intended to allow .or.where() and .or.named_scope.
class OrChain
def initialize(scope)
@scope = scope
end
def method_missing(method, *args, &block)
right_relation = @scope.klass.unscoped do
@scope.klass.send(method, *args, &block)
end
@scope.or(right_relation)
end
end
# Returns a new relation, which is the result of filtering the current relation
# according to the conditions in the arguments, joining WHERE clauses with OR
# operand, contrary to the default behaviour that uses AND.
#
# #or accepts conditions in one of several formats. In the examples below, the resulting
# SQL is given as an illustration; the actual query generated may be different depending
# on the database adapter.
#
# === without arguments
#
# If #or is used without arguments, it returns an ActiveRecord::OrChain object that can
# be used to chain queries with any other relation method, like where:
#
# Post.where("id = 1").or.where("id = 2")
# # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2'))
#
# It can also be chained with a named scope:
#
# Post.where("id = 1").or.containing_the_letter_a
# # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'body LIKE \\'%a%\\''))
#
# === ActiveRecord::Relation
#
# When #or is used with an ActiveRecord::Relation as an argument, it merges the two
# relations, with the exception of the WHERE clauses, that are joined using the OR
# operand.
#
# Post.where("id = 1").or(Post.where("id = 2"))
# # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2'))
#
# === anything you would pass to #where
#
# #or also accepts anything that could be passed to the #where method, as
# a shortcut:
#
# Post.where("id = 1").or("id = ?", 2)
# # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2'))
#
def or(opts = :chain, *rest)
if opts == :chain
OrChain.new(self)
else
left = with_default_scope
right = (ActiveRecord::Relation === opts) ? opts : klass.unscoped.where(opts, rest)
unless left.where_values.empty? || right.where_values.empty?
left.where_values = [left.where_ast.or(right.where_ast)]
right.where_values = []
end
left = left.merge(right)
end
end
# Returns an Arel AST containing only where_values
def where_ast
arel_wheres = []
where_values.each do |where|
arel_wheres << (String === where ? Arel.sql(where) : where)
end
return Arel::Nodes::And.new(arel_wheres) if arel_wheres.length >= 2
if Arel::Nodes::SqlLiteral === arel_wheres.first
Arel::Nodes::Grouping.new(arel_wheres.first)
else
arel_wheres.first
end
end
end
end
@murtuzakz

I'm not sure If Im doing it wrong, or if the PR had buggy code.
Consider this:
Activity.from_users( current_user.following ).faves.or.follows.or.comments in controller and
Activity.rb looks like :
scope :from_users, ->(user_ids) { where("owner_type = ? and owner_id in (?)",str_to_num['User'],user_ids) }
scope :faves, -> { where("action = 'fave'") }
scope :follows, -> { where("action = 'follow'") }
scope :comments, -> { where("action = 'comment'") }

I was expecting the SQL query to be :
SELECT "activities".* FROM "activities" WHERE (owner_type = 1 and owner_id in (1,2,9,10,15,20) AND (action = 'fave' OR action = 'follow' OR action = 'comment'))

but the resultant query was :
SELECT "activities".* FROM "activities" WHERE (((owner_type = 1 and owner_id in (1,2,9,10,15,20) AND action = 'fave' OR (action = 'follow')) OR (action = 'comment')))

Which is totally different!

Im sorry I'm commenting here, as the conversation on the PR is locked.

@ankurdelight

@murtuzakz; it seems like youre getting the intended behaivor: the or works on the current state of the relation (everything on the left) and whatever condition is to the right of the or.

so your query is basically doing
x = from_users( current_user.following ).faves OR .follows
and then
x OR .comments

to get what you want, i think you could do the following:

Activity.faves.or.follow.or.comments.from_users( current_user.following )

@bcackerman

Such a huge help! Thanks!

@vasilakisfil

I get NameError: undefined local variable or method `with_default_scope' :/

@mecampbellsoup

I'm getting NameError ... with_default_scope as well...

My code:

  included do
    scope :jfk,       -> { where(name: "JFK Airport") }
    scope :teterboro, -> { where(name: "Teterboro Airport") }
    scope :bounce,    -> { jfk.or.teterboro }
  end
@j-mcnally
Owner

If you are using rails 4 your gonna have problems, they changed the way it works a bit. I havent updated the gist to reflect this.

@j-mcnally
Owner

try this

module ActiveRecord
  module Querying
    delegate :or, :to => :all
  end
end

module ActiveRecord
  module QueryMethods
     # OrChain objects act as placeholder for queries in which #or does not have any parameter.
    # In this case, #or must be chained with any other relation method to return a new relation.
    # It is intended to allow .or.where() and .or.named_scope.
    class OrChain
      def initialize(scope)
        @scope = scope
      end

      def method_missing(method, *args, &block)
        right_relation = @scope.klass.unscoped do
          @scope.klass.send(method, *args, &block)
        end
        @scope.or(right_relation)
      end
    end

    # Returns a new relation, which is the result of filtering the current relation
    # according to the conditions in the arguments, joining WHERE clauses with OR
    # operand, contrary to the default behaviour that uses AND.
    #
    # #or accepts conditions in one of several formats. In the examples below, the resulting
    # SQL is given as an illustration; the actual query generated may be different depending
    # on the database adapter.
    #
    # === without arguments
    #
    # If #or is used without arguments, it returns an ActiveRecord::OrChain object that can
    # be used to chain queries with any other relation method, like where:
    #
    #    Post.where("id = 1").or.where("id = 2")
    #    # SELECT `posts`.* FROM `posts`  WHERE (('id = 1' OR 'id = 2'))
    #
    # It can also be chained with a named scope:
    #
    #    Post.where("id = 1").or.containing_the_letter_a
    #    # SELECT `posts`.* FROM `posts`  WHERE (('id = 1' OR 'body LIKE \\'%a%\\''))
    #
    # === ActiveRecord::Relation
    #
    # When #or is used with an ActiveRecord::Relation as an argument, it merges the two
    # relations, with the exception of the WHERE clauses, that are joined using the OR
    # operand.
    #
    #    Post.where("id = 1").or(Post.where("id = 2"))
    #    # SELECT `posts`.* FROM `posts`  WHERE (('id = 1' OR 'id = 2'))
    #
    # === anything you would pass to #where
    #
    # #or also accepts anything that could be passed to the #where method, as
    # a shortcut:
    #
    #    Post.where("id = 1").or("id = ?", 2)
    #    # SELECT `posts`.* FROM `posts`  WHERE (('id = 1' OR 'id = 2'))
    #
    def or(opts = :chain, *rest)
      if opts == :chain
        OrChain.new(self)
      else
        left = respond_to?(:with_default_scope) ? with_default_scope : klass.where("1=0")
        right = (ActiveRecord::Relation === opts) ? opts : klass.unscoped.where(opts, rest)

        unless left.where_values.empty? || right.where_values.empty?
          left.where_values = [left.where_ast.or(right.where_ast)]
          right.where_values = []
        end

        left = left.merge(right)
      end
    end


  # Returns an Arel AST containing only where_values
    def where_ast
      arel_wheres = []

      where_values.each do |where|
        arel_wheres << (String === where ? Arel.sql(where) : where)
      end

      return Arel::Nodes::And.new(arel_wheres) if arel_wheres.length >= 2

      if Arel::Nodes::SqlLiteral === arel_wheres.first
        Arel::Nodes::Grouping.new(arel_wheres.first)
      else
        arel_wheres.first
      end
    end


  end
end

Known to work with 4.1.4, YMMV

@8vius
8vius commented Jan 23, 2015

I'm using the updated version you posted and when I run this:

User.where(id: 1).or.where(id: 10)

The executed query is the following:

SELECT "users".* FROM "users" WHERE (((1=0) OR "users"."id" = 10))

Any idea how I can fix this?

@shuber
shuber commented Jan 26, 2015

@8vius I believe this is another way that you can write your specific query:

User.where(id: [1, 10])
@8vius
8vius commented Jan 26, 2015

@shuber of course, but it's just a test I'm doing and that issue came up. The same happens with queries that are actually complex.

@mglenn
mglenn commented Jan 28, 2015

Getting the same thing here.

contacts = Contact.where("first_name='Michael'").or.where("first_name='Steve'")

executes

SELECT "contacts".* FROM "contacts"  WHERE (((1=0) OR (first_name='Steve')))

What is the following attempting to accomplish?

left = respond_to?(:with_default_scope) ? with_default_scope : klass.where("1=0")
@craigweston

I was able to get the issues noted above by @mglenn and @8vius working by removing the following:

 left = respond_to?(:with_default_scope) ? with_default_scope : klass.where("1=0")

and replacing with self.

 left = self

The default scope still gets applied, so I'm not sure why the above line was necessary (please comment if there is a reason I am missing).

However, I did notice that this caused the default_scope to be incorrectly grouped together with the OR conditions.

For instance, given a class with the following scopes:

class Publication < ActiveRecord::Base
     default_scope { where(published: true) }
     scope :books, -> { where(pub_type: 'book') }
     scope :journals, -> { where(pub_type: 'journal') }

And a query such as:

Publication.books.or(Publication.journals)

This would produce a WHERE condition such as:

 (`publication`.`published` = 1 AND `publication`.`pub_type` = 'journal' OR `publication`.`published` = 1 AND `publication`.`pub_type` = 'book')

To fix this, I ensure that each scope is returned in a group by wrapping:

 return Arel::Nodes::And.new(arel_wheres) if arel_wheres.length >= 2

with:

return Arel::Nodes::Grouping.new(Arel::Nodes::And.new(arel_wheres)) if arel_wheres.length >= 2

This produces:

((`publication`.`published` = 1 AND `publication`.`pub_type` = 'journal') OR (`publication`.`published` = 1 AND `publication`.`pub_type` = 'book'))

BTW, this was only tested against Rails 4.2.

You can see the fork here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.