Logo

Articles / ActiveRecord - where.not with boolean is somewhat misleading

posted: 12/23/2016

Ruby

Just a random discovery that may or may not be a bug - depending on how you look at it.

I recently came across an issue in an application where I was expecting my code to act a certain way and no, so I thought I would share.

I have an ActiveRecord model with a boolean attribute named 'failed'.

I created a class method named 'self.failed' that uses the 'where' clause to check for the 'failed' attribute being true:

 

def self.failed
   where(failed: true)
end

 

And then made an inverse class method named 'self.successful' that uses the 'where.not' clause to check for the failed attribute being not true:

 

def self.successful
   where.not(failed: true)
end

 

I expected the self.successful class method to return any record where failed != true. After all,  it reads 'where failed is not equal to true'. I found that this is slightly misleading when I had 3 total records: the 1st record failed == true, the 2nd failed == false, the 3rd failed == nil.

However, the following is a thing:

 

Message.successful.count
> 1
Message.failed.count
> 1

 

Obviously, 1 + 1 does != 3. So the Message where failed == nil was being ignored by the class methods shown above.

The self.successful class method only returns records where failed == false, despite my thinking that it would return anything that was != true.

This makes sense if you rewrite the self.successful method as:

 

def self.successful
   where(failed: false)
end

 

This makes sense that if failed == nil, it would not == false. So.... you have been warned. 

If you want to remedy this the proper way, you should set the default to false on the migration of the table (or add a migration to set the default to false).

 

class CreateSomeModel < ActiveRecord::Migration
  def change
    create_table :some_model do |t|
      t.boolean :failed, default: false
      # other stuff here...
    end
  end
end

 

Once  you establish that the boolean field will either be true or false, and nil is not an option - you can begin to rely on the where.not clause returning the results you expect.

Alternatively, you could write the self.successful class method to include any records that are either failed=false or failed=nil, like so:

def self.successful
  where(failed: [false, nil])
end

 

Hopefully, this helps someone.