OR operator in WHERE clause with Arel in Rails 4.2
Asked Answered
M

3

6

The following code constructed a valid where clause with an OR operator in Rails 4.1

MyModel.where(
  MyModel.where(attribute1: 1, attribute2: 2).where_values.reduce(:or)
)

Which is roughly equivalent to the SQL

select * from my_models where (attribute1 = 1 OR attribute2 = 2)

In Rails 4.2, the same code generates an SQL query with missing values for it's bind parameters

select * from my_models where attribute1 =  OR attribute2 =  

... and generates an error due to the missing values for the bound values.

What is the equivalent code in Rails 4.2 to generate a valid query with an OR operator?

Edit:

The solution requires an Arel::Nodes::Node derived object to be used so that it can itself be combined with other conditions via AND and OR groupings.

rel = MyModel.where(attribute1: 1, attribute2: 2)
conditions = [rel.where_values.reduce(:or).to_sql, *rel.bind_values.map(&:last)]

MyModel.where(conditions)

The conditions var must be a derivative of Arel::Nodes::Node. The above solution works for simple queries, but for more complicated queries, conditions must be an Arel Node to be passed to a final query method.

Malignant answered 23/12, 2014 at 20:15 Comment(6)
where_values was removed from Rails 4.2 ... at least when I search the APIs I can find it in 3.x but not in 4.2. That may well be the source of your issue.Xuanxunit
where_values is part of the private API in Rails 4.2... it still exists. The bind values are in bind_values... also private. The recent changes to Arel via AdequateRecord are the cause for the issue.Malignant
Ok, good deal. Was not 100% certain since it was 'removed' from the public API (hence the comment v. answer).Xuanxunit
What does Model.where(conditions).where_values.reduce(:or).to_sql return?Faraday
"(attribute1 = ? OR attribute2 = ?)"Malignant
May be of helpWeighting
E
5

I'm using the below until rails 5 is out (in rails 5 AR supports .or):

ActiveRecord::QueryMethods::WhereChain.class_eval do
  def or(*scopes)
    scopes_where_values = []
    scopes_bind_values  = []
    scopes.each do |scope|
      case scope
      when ActiveRecord::Relation
        scopes_where_values += scope.where_values
        scopes_bind_values += scope.bind_values
      when Hash
        temp_scope = @scope.model.where(scope)
        scopes_where_values += temp_scope.where_values
        scopes_bind_values  += temp_scope.bind_values
      end
    end
    scopes_where_values = scopes_where_values.inject(:or)
    @scope.where_values += [scopes_where_values]
    @scope.bind_values  += scopes_bind_values
    @scope
  end
end

With the above you can do:

MyModel.where.or(attribute1: 1, attribute2: 2)
# or
MyModel.where.or(MyModel.where(some conditions), MyModel.where(some other conditions))
Economic answered 28/4, 2015 at 10:56 Comment(3)
Where do i define the where chain method?Vulturine
where do I put this?Americaamerican
For anyone reading this and still at rails 4+. This goes as an initializer, it's a monkey patchCruz
C
3

Using raw arel might be a better option:

t = MyModel.arel_table
MyModel.where(
  t[:attribute1].eq(1).or(
    t[:attribute2].eq(2)
  )
)
Consociate answered 4/5, 2015 at 16:11 Comment(0)
A
3

More correctly solution based on @bsd answer, but allow arbitrary scopes on input

 ActiveRecord::QueryMethods::WhereChain.class_eval do
   def or(*scopes)
     scopes_where_values = []
     scopes_bind_values  = []
     scopes.each do |scope|
       case scope
       when ActiveRecord::Relation
         scopes_where_values << scope.where_values.reduce(:and)
         scopes_bind_values += scope.bind_values
       when Hash
         temp_scope = @scope.model.where(scope)
         scopes_where_values << temp_scope.where_values.reduce(:and)
         scopes_bind_values  += temp_scope.bind_values
       end
     end
     scopes_where_values = scopes_where_values.inject(:or)
     @scope.where_values += [scopes_where_values]
     @scope.bind_values  += scopes_bind_values
     @scope
   end
 end

P.S. Previous code by @bsd can't correctly work in little difficult case: User.where.or(User.where(rating: 3), User.where(startups: { progress: 100, rating: nil })

Result of old code is wrong:

SELECT "users".* FROM "users" WHERE (("startups"."rating" = 3 OR "startups"."progress" = 100) OR "startups"."rating" IS NULL)

Changed code generate correct:

SELECT "users".* FROM "users" WHERE ("startups"."rating" = 3 OR "startups"."progress" = 100 AND "startups"."rating" IS NULL)

Activist answered 19/1, 2016 at 11:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.