Matthew Lindfield Seager

Matthew Lindfield Seager

ActiveRecord - solving for value is NULL or is not in list

Today I encountered an issue where 2 + 2 was only equalling 3 when fetching data using ActiveRecord in Rails.

def permanent_staff
  People.staff_like.where(
    contract_type: Settings.permanent_staff_contract_types
  )
end

def non_permanent_staff
  People.staff_like.where.not(
    contract_type: Settings.permanent_staff_contract_types
  )
end

People.staff_like.count == permanent_staff.count + non_permanent_staff.count
=> false # (huh!?)

After a little bit of digging I realised some People were being excluded because they didn’t have a contract type:

People.staff_like.count == permanent_staff.count + non_permanent_staff.count +
  People.staff_like.where(contract_type: nil).count
=> true

I assumed those with a NULL contract type would be included in non_permanent_staff but it turns out that’s not how databases work (at least not how PostrgreSQL or Sequel Server work, but I’m pretty sure this is “standard” behaviour). As soon as the query optimiser sees contract_type NOT IN (... it filters out any results that don’t have a contract_type.

My solution doesn’t feel like idiomatic Ruby or Rails but I solved it with the following method:

def non_permanent_staff
  People.staff_like.where.not(
    contract_type: Settings.permanent_staff_contract_types
  ).or(People.staff_like.where(contract_type: nil))
end

The generated SQL is a bit ugly (the staff_like scope conditions gets included twice) but I’m sure the database can optimise that… better the DB deal with the data collection than Ruby.

If this method proves confusing to future-me I may just put a NOT NULL constraint on contract_type in the database and be done with it. It’s a string we’re importing from a legacy system so the default value would just be “.