Question

I'm implementing a scope for a model based on this railscast. The condition in the scope uses the binary AND operator &, like this:

scope :with_role, lambda { |role| 
  {:conditions => "roles_mask & #{2**ROLES.index(role.to_s)} > 0"} 
}

Since the database I'm currently targetting is Oracle, which uses the BITAND function instead of the & operator, I've rewritten the condition like this:

{:conditions => "BITAND(roles_mask, #{2**ROLES.index(role.to_s)}) > 0"}

My problem is, I would like to keep my code as database agnostic as possible, since in the future we are planning on targetting other databases. My current solution has been to check whether I'm using Oracle and define the scope accordingly, like this (using_oracle is a boolean that I calculate elsewhere):

if using_oracle 
  scope :with_role, lambda { |role| 
    {:conditions => "BITAND(roles_mask, #{2**ROLES.index(role.to_s)}) > 0"}
  }
else
  scope :with_role, lambda { |role| 
    {:conditions => "roles_mask & #{2**ROLES.index(role.to_s)} > 0"}
  }
end

This works, but doesn't strike me as particularly elegant or ruby/rails-like. Could anyone kindly suggest better alternatives?

Was it helpful?

Solution

I think there should be a better way to extend Arel: I struggled to reach this result.

Anyway; this solution uses Model#extending:

module BitOperations
  def bitwise_and_sql
    @bitwise_and_sql ||=
      case connection.adapter_name
      when 'Oracle' # probably wrong!
        "BITAND(%s, %s)"
      else
        "%s & %s"
      end
  end
  def bitwise_and(i, j)
    where(bitwise_and_sql % [i, j])
  end
  def bitmask(i, j)
    where('%s > 0' % scoped.bitwise_and(i, j).wheres.to_a.last.to_sql)
  end
end

p User.scoped.extending(BitOperations).bitwise_and(1, 2).bitmask(3, 4).to_sql
#=> "SELECT \"users\".* FROM \"users\"  WHERE (1 & 2) AND ((3 & 4) > 0)"

.wheres contains the Arel relations; it includes Enumerable, so we can retrieve the last relation converting it to array and taking the last element. I used it in order to get the sql of bitwise_and(i, j) in order to use it in bitmask(i, j). I wonder if there is a better way to get the sql from a where...

.wheres raises a warning about wheres deprecation, which can be ignored at the moment (It works on Rails 4 beta too).

You can define class methods for User:

class User
  def self.scope_with_bit_operations
    @scope_with_bit_operations ||= scoped.extending(BitOperations)    
  end
  def self.bitwise_and(i, j)
    scope_with_bit_operations.bitwise_and(i, j)
  end
  def self.bitmask(i, j)
    scope_with_bit_operations.bitmask(i, j)
  end
end

p User.bitwise_and(1, 2).bitmask(3, 4).to_sql
#=> "SELECT \"users\".* FROM \"users\"  WHERE (1 & 2) AND ((3 & 4) > 0)"

or for all your models:

class ActiveRecord::Base
  def self.scope_with_bit_operations
    @scope_with_bit_operations ||= scoped.extending(BitOperations)    
  end
  def self.bitwise_and(i, j)
    scope_with_bit_operations.bitwise_and(i, j)
  end
  def self.bitmask(i, j)
    scope_with_bit_operations.bitmask(i, j)
  end
end

p Post.bitwise_and(1, 2).bitmask(3, 4).to_sql
#=> "SELECT \"posts\".* FROM \"posts\"  WHERE (1 & 2) AND ((3 & 4) > 0)"

And finally you can implement a slightly more elegant with_role scope:

class User < ActiveRecord::Base
  ROLES = %w[admin moderator author]

  scope :with_role, ->(role) do
    # I'm a fan of quoting everything :-P
    bitmask connection.quote_column_name(:roles_mask),
            connection.quote(2**ROLES.index(role.to_s))
  end
end

p User.with_role('admin').to_sql
#=> "SELECT \"users\".* FROM \"users\"  WHERE ((\"roles_mask\" & 1) > 0)"

I must say that IMO this is more a proof-of-concept: if you don't plan to reuse bitwise_and and bitmask in other models you don't need to abstract them, so probably you're good to go with something similar to your scope, f.e. something like this:

class User < ActiveRecord::Base
  ROLES = %w[admin moderator author]

  BITMASK_SQL =
    case connection.adapter_name
    when 'Oracle' # probably wrong!
      "BITAND(%s, %s) > 0"
    else
      "%s & %s > 0"
    end

  scope :with_role, ->(role) do
    where BITMASK_SQL % [ connection.quote_column_name(:roles_mask), 
                          connection.quote(2**ROLES.index(role.to_s)) ]
  end
end

p User.with_role('admin').to_sql
#=> "SELECT \"users\".* FROM \"users\"  WHERE (\"roles_mask\" & 1 > 0)"

I think the rule is add abstractions when you need of them, do not when you don't need of (I don't know if the english of this phrase is correct :-) )

I want to say another thing: since you are new to Ruby/Rails, I suggest you to read a lot of Rails & c. code; IMO it is the best way to learn how Rails works (this is the reason I spent my time to answer to your question: because I was curious about Rails management of Arel relations :-) ).

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top