MultiBitField

Description

MultiBitField creates convenience methods for using multiple filum bit-fields with ActiveRecord.

I'm sure the first question might be: "Why?" I assure you there are good reasons, but it will be a pretty specific need. I'll try to show a couple of ways that I use this technique, so you can decide if it's useful for you.

MultiBitField extends an integer field of the database, and allows the user to specify multiple columns on that field.

Other plugins have leveraged this technique to store multiple boolean flags in a single database field. While that's certainly possible with this tool, it really better for storing larger values like settings or counters.

I've found it useful sorting or comparing these types of fields. For instance, you may have daily, weekly and monthly counters, and want to sort on this combination. The order of the fields applys weight to each, so it should be chosen with care.

Example

Say you have daily, weekly and monthly counters:

Value Daily Weekly Monthly All columns together
bits 00011 00101 00001 000110010100001
values 3 5 1 3_233
weighted 3072 160 1 -
maxval 31 31 31 32_767

If this model is now sorted in ascending order, it'll sort first by day, then by week and then by month. You could also compare counters with a paired "limit" field.

These methods only require an integer attribute (any ORM will do.) Here's how we'd set this up:

  class User < ActiveRecord::Base
    has_bit_field :counter, :daily_count => 0..4, :weekly_count => 5..9, :monthly_count => 10..14
    has_bit_field :limit,   :daily_limit => 0..4, :weekly_limit => 5..9, :monthly_limit => 10..14
  end

this provides the following methods:

person = Person.new :daily => 3, :weekly => 5, :monthly => 1
person.daily 
=> 3
person.counter
=> 3233

person = Person.new :counter => 3233
person.monthly
=> 1
person.weekly
=> 5
person.monthly = 4
person.counter
=> 3236

We can inspsect what this bitstring looks like to converting it like so:

person.counter.to_s(2)
-> 000110010100100

We also provide convenient methods for resetting and incrementing fields. These methods require active-record and active-relation since they use the "update_attributes" and "update_all" methods.

When only the columnn name is supplied, it will increment or reset all of the fields.

peron.reset(:counter, :daily)
person.daily
=> 0
person.monthly
=> 4

person.reset(:counter)
person.daily
=> 0
person.monthly
=> 0

person.increment(:counter, :daily)
person.daily
=> 1

person.increment(:counter)
person.daily
=> 2
person.monthly
=> 1

person.reset(:counter, :daily, :monthly)
person.daily
=> 0
person.monthly
=> 0

The same thing works with bulk assignment:

[person1.daily, person2.daily]
=> [15, 23]

Person.reset :counter, :daily
[person1.daily, person2.daily]
=> [0, 0]

Person.increment :counter, :daily
=> [1, 1]

By the way, these methods all work with your chainable active-relation query methods!

Person.where(:daily => 0).increment_bitfield(:counter, :daily)

We also support some cool counting features -- be sure to admire the nice clean SQL:

Person.count_by(:counter, :monthly)
(0.4ms)  SELECT count(id) as daily_count, (counter & 31744)/1024 as daily FROM "people" GROUP BY daily
=> [{"daily_count" => 2, "daily" => 5}, {"daily_count" => 3, "daily" => 1}]

One limitation you should be aware of:

Since this technique pins the counters/limits to specific bits, you will need to plan the size of integer you intend to store in each field. For instance, if you need numbers 0-7, you can store that in 3 bits, if you need 0-31, you'll need 5 bits, etc.

Todo

I intend to add some more methods in the models for the following features:

  • Add comparison methods between bitfields on the same column, or between multiple columns
  • Investigate if there's a use for right/left bit shifting

Author:: Aaron Spiegel Copyright:: Copyright (c) 2012 Aaron Spiegel License:: MIT License (http://www.opensource.org/licenses/mit-license.php)