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