Order comments from certain users first, then by comment score - sql

In a Rails 4 project I have articles which has_many comments.
I’m trying to show a list of comments under an article ordered by the comment score (an integer column in the comment table), but where comments from a small number of users are shown first (eg. admins & mods). These users would be passed as an array of user_ids to the query (user_id is also a column in the comment table):
class Comment < ActiveRecord::Base
# article_id :integer
# user_id :integer
# score :integer
belongs_to :article
belongs_to :user
scope :by_users, -> (user_ids) { order("user_id IN (?), score DESC", user_ids) }
Thus:
some_article.comments.by_users([1,2,3])
This scope gives a syntax error, and I can’t quite work out a query that orders comments so that ALL an article’s comments are returned, shown first from users with a user_id in the passed array, then secondarily ordered by comment score.
Update:
mahemoff’s great conditional idea doesn’t work for Postgres, but lead to this non-working attempt:
scope :by_users, -> (user_ids) { order("case when (user_id IN (?)) then 0 else 1 end, score DESC", user_ids) }
Which gives me a PostgreSQL syntax error relating to the placeholder ? that I haven’t been able to fix.

With mahemoff & mu is too short’s help and suggestions, this is my working scope:
scope :by_users, -> (user_ids) { order([sanitize_sql("case when (user_id IN (#{user_ids.join(',')})) then 0 else 1 end"), "score DESC" ]) }
It’s somewhat imperfect, as Rails’ order apparently doesn’t support the ? placeholder, so instead I’ve used string interpolation and (even though user_ids isn’t user exposed) sanitized the sql.
...a frustrating problem with this though, is that it’s impossible to use .last to retrieve the final record. some_article.by_users[1,2].last now throws an error as AR can’t work out where to reverse the ordering:
ORDER BY case when (user_id IN (1 DESC, 2)) then 0 else 1 end DESC, score ASC LIMIT 1
sigh...

Try this:
class Comment < ActiveRecord::Base
scope :by_users, -> (user_ids) { order("if(user_id IN (#{ids.join ','}), 0, 1), score DESC") }

Related

Building a trending algorithm based on post count and frequency

Say I have a Board model. Boards have many Posts. I simply want to find the boards that have the highest post count within the span of (x) days. Below is my extremely naive approach to this. With the code provided I get the error:
ActiveRecord::StatementInvalid (PG::UndefinedTable: ERROR: missing FROM-clause entry for table "posts")
LINE 1: SELECT "boards".* FROM "boards" WHERE (board.posts.created_...
^
: SELECT "boards".* FROM "boards" WHERE (board.posts.created_at >= '2019-06-05 12:14:30.661233') LIMIT $1
Please let me know if there's a better way to do this in addition the error I'm receiving.
class Board < ApplicationRecord
has_many :posts
scope :trending, -> { includes(:posts).where('board.posts.created_at >= ?', Time.now-7.days).order(posts_count: :desc) }
end
class Post < ApplicationRecord
belongs_to :board, counter_cache: true
end
Update:
So I managed to come up with a working scope but not 100% sure if it's the most optimal. Your thoughts would be appreciated:
scope :trending, -> { includes(:posts).where(posts: { created_at: Time.now - 7.days }).order(posts_count: :desc) }
Update:
Board.joins(:posts)
.select("boards.*, count(posts.id) as latest_posts_count")
.where('posts.created_at >= ?', 7.days.ago)
.order('latest_posts_count desc')
.group('boards.id')
Try this, you will need to join it and group them by board_id
Board.joins(:posts)
.select("boards.*, count(posts.id) as posts_count")
.where('posts.created_at >= ?', 7.days.ago)
.order('posts_count desc')
.group('boards.id')
Explanation:
We joined (inner join) the tables so by default you get only boards which has at least one post associated with it
we ordered them based on posts count
we grouped them based on boards.id

Query for all of a specific record where its related resource all have the same value for a single attribute

class User < ActiveRecord::Base
has_many :memberships
# included columns
# id: integer
---------------------
Membership < ActiveRecord::Base
belongs_to :user
# included columns
# user_id: integer
# active: boolean
I'd like to be able to grab all users where all their memberships have 'active = false' in a single query. So far the best that I've been able to come up with is:
#grab possibles
users = User.joins(:memberships).where('memberships.active = false')
#select ones that satisfy condition
users.select{ |user| user.memberships.pluck(&:active).uniq == [false] }
which is not that great since I have to use ruby to pluck out the valid ones.
This can do the trick:
users_with_active_membership = User.joins(:memberships).where(memberships: { active: true })
users = User.where( 'users.id NOT IN (?)', users_with_active_membership.pluck(:id) )
I am not sure of the result but I expect it to be 2 nested queries, one selecting the User ids having an active membership, the other query to select the User not in this previous ids list.
I can't test it since I don't have an environment with these relationships. Can you try it and post the SQL query generated? (add .to_sql to see it)
Another way, I don't know which could be the most efficient:
User.where( 'users.id NOT IN (?)', Membership.where(active: true).group(:user_id).pluck(:user_id) )

Filtering Parents by Children

I'm doing a simple blog application - There are posts, which have many tags through a posts_tags table (my models are below). What I have implemented is if a user clicks a tag, it will show just the posts with that tag. What I want is for the user to them be able to select another tag, and it will filter to only the posts that have both of those tags, then a third, then a fourth, etc. I'm having difficulty making the active record query - especially dynamically. The closest I've gotten is listed below - however its in pure SQL and I would like to at least have it in ActiveRecord Rubyland syntax even with the complexity it contains.
Also, the "having count 2" does not work, its saying that "count" does not exist and even if I assign it a name. However, it is outputting in my table (the idea behind count is that if it contains a number that is as much as how many tags we are searching for, then theoretically/ideally it has all the tags)
My current test SQL query
select posts_tags.post_id,count(*) from posts_tags where tag_id=1 or tag_id=3 group by post_id ### having count=2
The output from the test SQL (I know it doesnt contain much but just with some simple seed data).
post_id | count
---------+-------
1 | 2
2 | 1
My Models:
/post.rb
class Post < ActiveRecord::Base
has_many :posts_tags
has_many :tags, :through => :posts_tags
end
/tag.rb
class Tag < ActiveRecord::Base
has_many :posts_tags
has_many :posts, :through => :posts_tags
end
/poststag.rb
class PostsTag < ActiveRecord::Base
belongs_to :tag
belongs_to :post
end
Give a try to:
Post.joins(:tags).where(tags: {id: [1, 3]}).select("posts.id, count(*)").group("posts.id").having("count(*) > 2")
I think "count = 2" is not correct. It should be "count(*) = 2". Your query then will be
select post_id,count(post_id)
from posts_tags
where tag_id = 1 or tag_id = 3
group by post_id
having count(post_id) = 2
In general you want to stay away from writing raw sql when using rails. Active Record has great helper methods to make your sql more readable and maintainable.
If you only have a few tags you can create scopes for each of them (http://guides.rubyonrails.org/active_record_querying.html#scopes)
Since people are clicking on tags one at a time you could just query for each tag and then use the & operator on the arrays. Because you have already requested the exact same set of data from the database the query results should be cached meaning you are only hitting the db for the newest query.

Rails 3 Order Records By Grand-child Count

I'm trying to do some fairly complicated record sorting that I was having a bit of trouble with. I have three models:
class User < ActiveRecord::Base
has_many :registers
has_many :results, :through => :registers
#Find all the Users that exist as registrants for a tournament
scope :with_tournament_entrees, :include => :registers, :conditions => "registers.id IS NOT NULL"
end
Register
class Register < ActiveRecord::Base
belongs_to :user
has_many :results
end
Result
class Result < ActiveRecord::Base
belongs_to :register
end
Now on a Tournament result page I list all users by their total wins (wins is calculated through the results table). First thing first I find all users who have entered a tournament with the query:
User.with_tournament_entrees
With this I can simply loop through the returned users and query each individual record with the following to retrieve each users "Total Wins":
user.results.where("win = true").count()
However I would also like to take this a step further and order all of the users by their "Total Wins", and this is the best I could come up with:
User.with_tournament_entrees.select('SELECT *,
(SELECT count(*)
FROM results
INNER JOIN "registers"
ON "results"."register_id" = "registers"."id"
WHERE "registers"."user_id" = "users.id"
AND (win = true)
) AS total_wins
FROM users ORDER BY total_wins DESC')
I think it's close, but it doesn't actually order by the total_wins in descending order as I instruct it to. I'm using a PostgreSQL database.
Edit:
There's actually three selects taking place, the first occurs on User.with_tournament_entries which just performs a quick filter on the User table. If I ignore that and try
SELECT *, (SELECT count(*) FROM results INNER JOIN "registers" ON "results"."register_id" = "registers"."id" WHERE "registers"."user_id" = "users.id" AND (win = true)) AS total_wins FROM users ORDER BY total_wins DESC;
it fails in both PSQL and the ERB console. I get the error message:
PGError: ERROR: column "users.id" does not exist
I think this happens because the inner-select occurs before the outer-select so it doesn't have access to the user id before hand. Not sure how to give it access to all user ids before than inner select occurs but this isn't an issue when I do User.with_tournament_entires followed by the query.
In your SQL, "users.id" is quoted wrong -- it's telling Postgres to look for a column named, literally, "users.id".
It should be "users"."id", or, just users.id (you only need to quote it if you have a table/column name that conflicts with a postgres keyword, or have punctuation or something else unusual).

Need help finding the first created record of each group, in Rails ActiveRecord

I have a table called FeedItems, and basically the user needs to see the first created feed item for each post id.
At the moment I'm using 'group', and SQLite is giving me the last created, for some reason. I've tried to sort the list of feed items before grouping, but it makes no difference.
user_id | post_id | action
----------------------------------------
1 1 'posted'
3 2 'loved' <--- this, being created afterwards
should not appear in the query.
I know this has to do with an INNER JOIN, and I have seen some similar examples of this being done, however the difference is that I'm not sure how to do this AND use the existing query I already have to find out if the users are friends with the current user.
Here's the code for the model:
class FeedItem < ActiveRecord::Base
belongs_to :post
belongs_to :user
default_scope :order => 'created_at desc'
scope :feed_for, lambda { |user| feed_items_for(user).group(:post_id) }
private
def self.feed_items_for(user)
friend_ids = %(SELECT friend_id FROM friendships WHERE (user_id = :user_id OR friend_id = :user_id))
ghosted_ids = %(SELECT pending_friend_id FROM friend_requests WHERE user_id = :user_id)
where("user_id IN (#{friend_ids}) OR user_id IN (#{ghosted_ids}) OR user_id = :user_id", {:user_id => user.id})
end
end
Any help would be greatly appreciated, thanks.
ActiveRecord provides you with dynamic attribute-based finders, which means that you have
Feeds.find_last_by_post_id(117)
If you would prefer to have the fist, you have:
Feeds.where(:post_id => 117).first
And you can always do, which I'm not sure is a "best practice":
Feeds.where(:post_id => 117).order('created_on DESC').first
You can read more about it at:
http://api.rubyonrails.org/classes/ActiveRecord/Base.html