views:

209

answers:

3

what would I put in the named scope :by_unique_users so that I can do Comment.recent.by_unique_users.limit(3), and only get one comment per user?

class User
  has_many :comments
end

class Comment
  belongs_to :user
  named_scope :recent, :order => 'comments.created_at DESC'
  named_scope :limit, lambda { |limit| {:limit => limit}}
  named_scope :by_unique_users
end

on sqlite named_scope :by_unique_user, :group => "user_id" works,

but makes it freak out on postgres, which is deployed on production PGError: ERROR: column "comments.id" must appear in the GROUP BY clause or be used in an aggregate function

A: 

Named scope might not work, but you can force Rails to do a manual query (From Geek Skillz).

Item.find( :all, :order => 'comments.created_at DESC', :select => 'DISTINCT user' )[0, 3]
mcandre
it'd be :select => "DISTINCT user_id" but then that only gives you the user id so something like :select => "DISTINCT user_id, id, title, body" would be better, but clobbers all the other named scope stuff. I could do something like get the 10 or 15 most recent records and then delete all the duplicate user_id records from the array, and get the first 3 out of that... but that'd be really cpu/database intensive, and performance kinda matters, though I guess I this could be cached...
Dennis Collective
A: 

Normally the order clause looks like this:

named_scope :recent, :order => 'created_at DESC'

You may want to try it.

Mark Thomas
+2  A: 

Postgres is different from MySQL and SQLite in how it treats GROUP BY. In essence, it's very strict. Let's say you have.

id name
1  foo
2  bar
2  baz

Then you're doing GROUP BY id. MySQL assumes that you just want to drop all but the first name. So it will produce.

id name
1  foo
2  bar

However, Postgres will not be guessing the grouping method in this case. It needs a specific instruction of how you want to group other columns. It provides what's called aggregate functions for that purpose, and in this case you're looking for a function that takes a first of a bunch. I couldn't find a function that does that, but perhaps min() or max() could serve as one. In that case you need to use :select => 'min(comments.id), min(comments.some_other_column)', and this you should do for every column except user_id. Then you can use :group => 'user_id' without problems.

Btw, min() and max() accept strings, not just numbers, so they should work for any column. If you want to really take the first of a bunch, then google for "postgres aggregate first" to find some implementations, or use postgres arrays. Although these would break compatibility with mysql and sqlite.

Update

On the other hand if fetching recent comments isn't too expensive, let ruby handle the unique users part.

unique_comments = []
Comment.recent.each do |comment|
  unless unique_comments.find{|c| c.user_id == comment.user_id}
    unique_comments << comment
  end
  break if unique_comments.size > 2
end

Now you have at most 3 comments from distinct users.

hakunin
thanks for your step in the right direciton, I'm trying to figure out how to integrate the solution with rails, this should help.
Dennis Collective
Just added an update on the bottom, providing examples how to do this in ruby, without named scopes. I'd say, doing this in ruby is the easiest.
hakunin
oops, didn't see the :select => and thought you were going into deep esoteric postgres land, installing postgres right now on my dev box to see if it works, bye bye sqlite....
Dennis Collective