Giter VIP home page Giter VIP logo

bitfields's Introduction

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
[email protected]
License: MIT
Build Status

bitfields's People

Contributors

a-bates avatar dalibor avatar efexen avatar grosser avatar jcwilk avatar justinaiken avatar kmcbride avatar phlipper avatar raelgc avatar shirish-pampoorickal avatar skojin avatar sztheory avatar tjstankus avatar zorbash avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

bitfields's Issues

Volunteer to improve

Hi Grosser!

Good work on the gem and thanks! I have been using flag_shi_tzu for sometime now, but the development has been neglected. I would love to fork your project and add some features and some simplicity, but I don't want to waste my time submitting a pull request that will never get accepted. I need your buy-in and direction on some design changes before I get started in order to increase the chances of getting my pull request pulled. :)

I would like to add sequential keys in the bitfield options. IMO, it would be easier to maintain a list of flags/bits if the number used for the key was NOT the value of the bit but the number. The values get large quick, with the value getting to 4096 on the 12th bit and 4294967296 on the 32nd. I would like to add some bit shifting to make this more simple. Can I add this support? I wanted your permission before I changed the API since existing users of the gem would have to update their code. Thoughts?

Cheers!

Querying with joins

I'd like for something like this to work: Document.joins(:user).where(users: { admin: true }).

This is probably quite difficult to achieve, since it's converted back to an SQL query by Arel under the hood. It's also worth noting that Arel isn't a stable interface, so it'll probably have some maintenance overhead. Not surprised if this doesn't get addressed, but is there a recommended way to phrase queries like this?

Feature request: Inverted bit fields

Hi @grosser,

Your gem looks really nice and the code is really easy to read. I wanted to use this gem for a project I'm working on and came across a situation where the default values for all bits would be true. I'd like to have a bit-field about email notification preferences and by default, all options should be true.

So, I was thinking how do I achieve this? How can I have the individual bits default to true? Should I set the default value of that column to INT_MAX? Then I thought, wouldn't it be nice to have an additional option in your gem called e.g. defaults_to where you can specify true to get the desired behavior. Of course, this defaults_to option would be purely optional and would only make sense when set to true:

bitfield :notification_options,
  1 => :notify_of_new_follower,
  2 => :notify_of_new_post,
  3 => :notify_of_new_comment,
  defaults_to => true

What do you think? Would you say this would be a nice addition or wouldn't that make much sense from your point of view? I would gladly write the code for that and send you a pull request.

Cheers,
Philipe

bitfield column (e.g., flags) missing during tests

I will keep looking of course, but for some reason, while running integration tests, the bitfield is not available:

*** NoMethodError Exception: undefined methodflags' for #WorkItem:0x007ff697151a40`

It works fine in the normal course of events (e.g., rails server, rails console).

Why you say that is not possible to configure defaults?

In readme is stated

[Defaults] afaik it is not possible to have some bits true by default (without monkeypatching AR/see tests) -> choose a good naming like xxx_on / xxx_off to use the default 'false'

But I just had this

bitfield :reminders_bits,
         2 ** 0  => :notify_one,
         2 ** 1  => :notify_two,
         2 ** 2  => :notify_three

And I want :notify_two and :notify_three to be true by default, so I just run this migration and everything is working as expected

DEFAULT_REMINDER_BITS = [1, 2].inject(0) { |v, k| v | 2 ** k }
change_column_default :model, :reminders_bits, DEFAULT_REMINDER_BITS

Is there any drawback in doing this?

Is it possible that something may break in gem functionalities because of this?

Rails 4.2 compatibility

I'm trying combine Bitfields with Rails 4.2 and have a problem.
In 4.2 changed_attributes works different way and there is a problem in line 183:

if defined? changed_attributes
  send(:changed_attributes).merge!(bit_name.to_s => old_value)
end

I tried fix it by using attributes_changed_by_setter alias (check lukasztackowiak/bitfields@1bda52396121cb48f31d4bc36cfaaac2ecc441e5) but this caused more issues deeper in Rails. I can try dig deeper and maybe fix that but I'm not sure if that piece of code is really needed. I don't see any place where Bitfields relay on changed_attributes and for Rails I think it doesn't matter - it still keep information about changes in bitfield column.
Can you take a look and let me know if I should hunt for more complex solution or simply can remove this part from Bitfields code?

Bitfields don't show up when serializing the model.

Right now my current experience with the gem for json serialization behavior is:

class MyModel < ActiveRecord::Base
    include Bitfields
    bitfield :blah, :blah1, :blah2, :blah3
end
m = MyModel.new(:blah1 => true, :blah2 => true, :blah3 => false)
m.to_json

will yield

{"blah":3}

Is there any way to get it to produce the bitfields in the serialized output?

Thanks!

Bitfield not being set when all bits are 0

Hi! Thanks for the handy gem.

I'm using bitfield in my User class as follows:

class User < ActiveRecord::Base
  ...
  bitfield :email_preferences, 1 => :credits_email, 2 => :weekly_results_email, 4 => :challenges_email, 8 => :jackpot_results_email
  ...
  before_create :set_email_prefs_to_defaults
  ...
  def set_email_prefs_to_defaults
    if !attribute_present?(:email_preferences)
      self.credits_email = true
      self.weekly_results_email = true
      self.challenges_email = true
      self.jackpot_results_email = true
    end
  end
end

Unfortunately, this spec is failing:

  describe "set_email_prefs_to_defaults" do
    it "should not set the email preferences to the defaults on create if they're already set" do
      user = Factory(:user, :credits_email => false, :weekly_results_email => false,
                            :challenges_email => false, :jackpot_results_email => false)
      user.credits_email.should == false
      user.weekly_results_email.should == false
      user.challenges_email.should == false
      user.jackpot_results_email.should == false
    end
  end

I believe this is due to this behavior:

User.new(:credits_email => false, :weekly_results_email => false, :challenges_email => false, :jackpot_results_email => false).attribute_present?(:email_preferences)  #=> false
User.new(:credits_email => false, :weekly_results_email => false, :challenges_email => false, :jackpot_results_email => false).email_preferences  #=> nil

It seems as though the bitfield is not being set when all bits are set to false. Unless I'm mistaken, this does not seem to be desired behavior. I'm using version 0.4.0. Any thoughts/help would be appreciated. Thanks!

Requires AsMut and AsRef with buffers > 32 bytes

I think this is similar to the issue #21:

Assume the following code:

extern crate bitfield;
use bitfield::bitfield;

pub const SIZE: usize = 33;

bitfield! {
    pub struct Foo(MSB0[u8]);
    impl Debug;

    pub u8, foo, _ : 0;
}

fn main() {
    let buffer = [0u8; SIZE];
    Foo(buffer).foo();
}

This will result in:

error[E0599]: no method named `foo` found for struct `Foo<[u8; 33]>` in the current scope
  --> src/main.rs:16:17
   |
7  | / bitfield! {
8  | |     pub struct Foo(MSB0[u8]);
9  | |     impl Debug;
10 | |
11 | |     pub u8, foo, _ : 0;
12 | | }
   | |_- method `foo` not found for this
...
16 |       Foo(buffer).foo();
   |                   ^^^ method not found in `Foo<[u8; 33]>`
   |
   = note: the method `foo` exists but the following trait bounds were not satisfied:
           `[u8; 33]: std::convert::AsRef<[u8]>`
           `[u8; 33]: std::convert::AsMut<[u8]>`

If you lower the size to "32" (or less), it works.

Deprecation Warnings in AR 5.1/Likely breaking in AR 5.2

ActiveRecord 5.1 deprecated many methods in the Dirty API and they will be going away in ActiveRecord 5.2

The bitfields gem still depends on many of the old methods such as send("#{column}_was").

In addition, the API that bitfields are emulating is the old deprecated API. It's probably a good idea to update this match the new Dirty API.

I'll give this a shot and try to pull together a PR.

Never using bit0

I'm calling bitfield without specifying the index of each field:
bitfield :preferences, :prefA, :prefB, :prefC
And when I set them all to true, it shows up in the database with the value "14" => 1110. It seems like in the gem you never use bit0:
args.each_with_index{|field,i| options[2**(i+1)] = field } # add fields given in normal args to options
Since you're doing (i+1), the first index you get in the bitfield is 1 (with a mask of 2**1, you're setting bit1 first). You should be able to retrace this just by using the setup above and setting them all to true, you'll get the value 14 in the preferences column instead of the expected 7.

Impossible to have multiple bitfields columns

It's possible to set up multiple columns and retrieve bits, but it's impossible to set the values on specific columns with same bitfields

  bitfield :exposed_fields, 2**0 => :title,
                            2**1 => :last_name,
                            2**2 => :first_name,
                            2**3 => :address_1,
                            2**4 => :address_2,
                            2**5 => :postal_code,
                            2**6 => :city

  bitfield :required_fields, 2**0 => :title,
                             2**1 => :last_name,
                             2**2 => :first_name,
                             2**3 => :address_1,
                             2**4 => :address_2,
                             2**5 => :postal_code,
                             2**6 => :city

Querying with #includes

I tried to use this with includes similar to how I had in #44, but the query didn't return any results. It might be possible to query from the other direction and map, but I find that sub-optimal. Is there a way to use includes with this?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.