Skip to content

@j-mcnally /or_scopes.rb secret
Last active

Embed URL

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
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

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

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

User.where(id: [1, 10])
@8vius

@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

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")
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.