Ruby on Rails - ActiveRecord::Relation count method is wrong?

Adam Waite picture Adam Waite · Sep 20, 2013 · Viewed 8.2k times · Source

I'm writing an application that allows users to send one another messages about an 'offer'.

I thought I'd save myself some work and use the Mailboxer gem.

I'm following a test driven development approach with RSpec. I'm writing a test that should ensure that only one Conversation is allowed per offer. An offer belongs_to two different users (the user that made the offer, and the user that received the offer).

Here is my failing test:

describe "after a message is sent to the same user twice" do
  before do 
    2.times { sending_user.message_user_regarding_offer!  offer, receiving_user, random_string }
  end
  specify { sending_user.mailbox.conversations.count.should == 1 }
end

So before the test runs a user sending_user sends a message to the receiving_user twice. The message_user_regarding_offer! looks like this:

def message_user_regarding_offer! offer, receiver, body
    conversation = offer.conversation
    if conversation.nil?
      self.send_message(receiver, body, offer.conversation_subject)
    else  
      self.reply_to_conversation(conversation, body)
      # I put a binding.pry here to examine in console
    end
    offer.create_activity key: PublicActivityKeys.message_received, owner: self, recipient: receiver
end

On the first iteration in the test (when the first message is sent) the conversation variable is nil therefore a message is sent and a conversation is created between the two users.

On the second iteration the conversation created in the first iteration is returned and the user replies to that conversation, but a new conversation isn't created.

This all works, but the test fails and I cannot understand why!

When I place a pry binding in the code in the location specified above I can examine what is going on... now riddle me this:

wtf ruby

self.mailbox.conversations[0] returns a Conversation instance

self.mailbox.conversations[1] returns nil

self.mailbox.conversations clearly shows a collection containing ONE object.

self.mailbox.conversations.count returns 2?!

What is going on there? the count method is incorrect and my test is failing...

What am I missing? Or is this a bug?!

EDIT

offer.conversation looks like this:

  def conversation
    Conversation.where({subject: conversation_subject}).last
  end

and offer.conversation_subject:

  def conversation_subject
    "offer-#{self.id}"
  end

EDIT 2 - Showing the first and second iteration in pry

wtf ruby 2

Also...

Conversation.all.count returns 1!

and:

Conversation.all == self.mailbox.conversations returns true

and

Conversation.all.count == self.mailbox.conversations.count returns false

How can that be if the arrays are equal? I don't know what's going on here, blown hours on this now. Think it's a bug?!

EDIT 3

From the source of the Mailboxer gem...

def conversations(options = {})
  conv = Conversation.participant(@messageable)

  if options[:mailbox_type].present?
    case options[:mailbox_type]
    when 'inbox'
      conv = Conversation.inbox(@messageable)
    when 'sentbox'
      conv = Conversation.sentbox(@messageable)
    when 'trash'
      conv = Conversation.trash(@messageable)
    when  'not_trash'
      conv = Conversation.not_trash(@messageable)
    end
  end

  if (options.has_key?(:read) && options[:read]==false) || (options.has_key?(:unread) && options[:unread]==true)
    conv = conv.unread(@messageable)
  end

  conv
end

The reply_to_convesation code is available here -> http://rubydoc.info/gems/mailboxer/frames.

Just can't see what I'm doing wrong! Might rework my tests to get around this. Or ditch the gem and write my own.

Answer

tihom picture tihom · Sep 23, 2013

see this Rails 3: Difference between Relation.count and Relation.all.count

In short Rails ignores the select columns (if more than one) when you apply count to the query. This is because

SQL's COUNT allows only one or less columns as parameters.

From Mailbox code

 scope :participant, lambda {|participant|
    select('DISTINCT conversations.*').
      where('notifications.type'=> Message.name).
      order("conversations.updated_at DESC").
      joins(:receipts).merge(Receipt.recipient(participant))
  }

self.mailbox.conversations.count ignores the select('DISTINCT conversations.*') and counts the join table with receipts, essentially counting number of receipts with duplicate conversations in it.

On the other hand, self.mailbox.conversations.all.count first gets the records applying the select, which gets unique conversations and then counts it.

self.mailbox.conversations.all == self.mailbox.conversations since both of them query the db with the select.

To solve your problem you can use sending_user.mailbox.conversations.all.count or sending_user.mailbox.conversations.group('conversations.id').length