Skip to content
master
Go to file
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
 
 
 
 
lib
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Readme.md

Save migrations and columns by storing multiple booleans in a single integer.
e.g. true-false-false = 1, false-true-false = 2, true-false-true = 5 (1,2,4,8,..)

class User < ActiveRecord::Base
  include Bitfields
  bitfield :my_bits, 1 => :seller, 2 => :insane, 4 => :sensible
end

user = User.new(seller: true, insane: true)
user.seller # => true
user.sensible? # => false
user.my_bits # => 3
  • records bitfield_changes user.bitfield_changes # => {"seller" => [false, true], "insane" => [false, true]} (also seller_was / seller_change / seller_changed? / seller_became_true? / seller_became_false?)
    • Individual added methods (i.e, seller_was, seller_changed?, etc..) can be deactivated with bitfield ..., added_instance_methods: false
    • Note: ActiveRecord 5.2 changes the behavior of _was and _changed? methods when used in the context of an after_save callback.
      • ActiveRecord 5.1 will use the use the values that were just changed.
      • ActiveRecord 5.2, however, will return the current value for _was and false for _changed? since the previous changes have been persisted.
  • adds scopes User.seller.sensible.first (deactivate with bitfield ..., scopes: false)
  • builds sql User.bitfield_sql(insane: true, sensible: false) # => '(users.my_bits & 6) = 1'
  • builds sql with OR condition User.bitfield_sql({ insane: true, sensible: true }, query_mode: :bit_operator_or) # => '(users.my_bits & 2) = 2 OR (users.bits & 4) = 4'
  • builds index-using sql with bitfield ... , query_mode: :in_list and User.bitfield_sql(insane: true, sensible: false) # => 'users.my_bits IN (2, 3)' (2 and 1+2) often slower than :bit_operator sql especially for high number of bits
  • builds update sql User.set_bitfield_sql(insane: true, sensible: false) == 'my_bits = (my_bits | 6) - 4'
  • faster sql than any other bitfield lib through combination of multiple bits into a single sql statement
  • gives access to bits User.bitfields[:my_bits][:sensible] # => 4
  • converts hash to bits User.bitfield_bits(seller: true) # => 1

Install

gem install bitfields

Migration

ALWAYS set a default, bitfield queries will not work for NULL

t.integer :my_bits, default: 0, null: false
# OR
add_column :users, :my_bits, :integer, default: 0, null: false

Instance Methods

Global Bitfield Methods

Method Name Example (user = User.new(seller: true, insane: true) Result
bitfield_values user.bitfield_values {"seller" => true, "insane" => true, "sensible" => false}
bitfield_changes user.bitfield_changes {"seller" => [false, true], "insane" => [false, true]}

Individual Bit Methods

Model Getters / Setters

Method Name Example (user = User.new) Result
#{bit_name} user.seller false
#{bit_name}= user.seller = true true
#{bit_name}? user.seller? true

Dirty Methods:

Some, not all, ActiveRecord::AttributeMethods::Dirty and ActiveModel::Dirty methods can be used on each bitfield:

Before Model Persistence
Method Name Example (user = User.new) Result
#{bit_name}_was user.seller_was false
#{bit_name}_in_database user.seller_in_database false
#{bit_name}_change user.seller_change [false, true]
#{bit_name}_change_to_be_saved user.seller_change_to_be_saved [false, true]
#{bit_name}_changed? user.seller_changed? true
will_save_change_to_#{bit_name}? user.will_save_change_to_seller? true
#{bit_name}_became_true? user.seller_became_true? true
#{bit_name}_became_false? user.seller_became_false? false
After Model Persistence
Method Name Example (user = User.create(seller: true)) Result
#{bit_name}_before_last_save user.seller_before_last_save false
saved_change_to_#{bit_name} user.saved_change_to_seller [false, true]
saved_change_to_#{bit_name}? user.saved_change_to_seller? true
  • Note: These methods are dynamically defined for each bitfield, and function separately from the real ActiveRecord::AttributeMethods::Dirty/ActiveModel::Dirty methods. As such, generic methods (e.g. attribute_before_last_save(:attribute)) will not work.

Examples

Update all users

User.seller.not_sensible.update_all(User.set_bitfield_sql(seller: true, insane: true))

Delete the shop when a user is no longer a seller

before_save :delete_shop, if: -> { |u| u.seller_change == [true, false] }

List fields and their respective values

user = User.new(insane: true)
user.bitfield_values(:my_bits) # => { seller: false, insane: true, sensible: false }

TIPS

  • [Upgrading] in version 0.2.2 the first field(when not given as hash) used bit 2 -> add a bogus field in first position
  • [Defaults for new records] set via db migration or name the bit foo_off to avoid confusion, setting via after_initialize does not work
  • It is slow to do: #{bitfield_sql(...)} AND #{bitfield_sql(...)}, merge both into one hash
  • bit_operator is faster in most cases, use query_mode: :in_list sparingly
  • Standard mysql integer is 4 byte -> 32 bitfields
  • If you are lazy or bad at math you can also do bitfields :bits, :foo, :bar, :baz
  • If you are want more readability and reduce clutter you can do bitfields 2**0 => :foo, 2**1 => :bar, 2**32 => :baz

Query-mode Benchmark

The query_mode: :in_list is slower for most queries and scales miserably with the number of bits.
Stay with the default query-mode. Only use :in_list if your edge-case shows better performance.

performance

Testing With RSpec

To assert that a specific flag is a bitfield flag and has the active?, active, and active= methods and behavior use the following matcher:

require 'bitfields/rspec'

describe User do
  it { should have_a_bitfield :active }
end

TODO

  • convenient named scope User.with_bitfields(xxx: true, yyy: false)

Authors

Contributors

Michael Grosser
michael@grosser.it
License: MIT
Build Status

About

n Booleans = 1 Integer, saves columns and migrations.

Resources

Languages

You can’t perform that action at this time.