Skip to content

grosser/bitfields

Repository files navigation

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

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

About

n Booleans = 1 Integer, saves columns and migrations.

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages