Building a trending algorithm based on post count and frequency - sql

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

Related

ActiveRecord .missing - Why doesnt this work?

Working on Rails 6.1
I've got this base class:
class Post < ApplicationRecord
end
And these two subclasses representing a face-to-face lesson and it's live transmission:
class Live < Post
belongs_to :in_person_live,
foreign_key: :live_of_in_person_live_id,
class_name: 'InPersonLive',
required: false,
inverse_of: :online_live,
touch: true
end
class InPersonLive < Post
has_one :online_live,
class_name: 'Live',
foreign_key: 'live_of_in_person_live_id',
inverse_of: :in_person_live,
touch: true
end
I'd like to query for face-to-face lessons without a live transmission.
I came up with this, which seems to work:
InPersonLive.where.not(id: Live.where.not(live_of_in_person_live_id: nil).select(:live_of_in_person_live_id)).count
But was wondering why this doesnt work:
InPersonLive.where.missing(:online_live).count
I'd expect the .missing to return the same result but it always returns 0.
The generated SQL is different in both cases but again I don't understand why the result is different, seems to me like they should return the same set.
InPersonLive.where.not(id: Live.where.not(live_of_in_person_live_id: nil)).count generates:
SELECT COUNT(*) FROM "posts" WHERE "posts"."type" = $1 AND "posts"."id" NOT IN (SELECT "posts"."live_of_in_person_live_id" FROM "posts" WHERE "posts"."type" = $2 AND "posts"."live_of_in_person_live_id" IS NOT NULL) [["type", "InPersonLive"], ["type", "Live"]]
while InPersonLive.where.missing(:online_live).count generates:
SELECT COUNT(*) FROM "posts" LEFT OUTER JOIN "posts" "online_lives_posts" ON "online_lives_posts"."live_of_in_person_live_id" = "posts"."id" AND "online_lives_posts"."type" = $1 WHERE "posts"."type" = $2 AND "posts"."id" IS NULL [["type", "Live"], ["type", "InPersonLive"]]
Hi #dwaynemac.
I wonder if you mapped or queried backward.
Maybe InPersonLive belongs to Live and Live has_one or has_many. Or maybe you should query Live.where.missing(:in_person_live).count
So, similarly Something.where.missing(:thing) should generate the same query.
Because, if you wanted to test prior to missing you would do something along the lines of:
Live.left_joins(:in_person_live).where(in_person_live: { id: nil } )
The other answer already gave you the manual way of doing this, just wanted to add that this actually looks like it might be a bug. The AND "posts"."id" IS NULL should be AND "online_lives_posts"."id" IS NULL, ie. the null check should be for the left outer joined relation.
Obviously your associations are quite the mouthful, so not sure what's tripping it up, my first guess would be the STI.

Ransack / Rails 6 : PG::UndefinedTable for a query through a belongs_to

I can't chain a where and a Ransack query, I get a PG::UndefinedTable error. I'm using Rails 6.1.3 and Ransack 2.4.2.
I have seen this issue : https://github.com/activerecord-hackery/ransack/issues/1119 where everybody agrees that the problem has been fixed with the upgrade to rails 6.1. Sadly, not for my case.
My models looks like this :
class Appointment < ApplicationRecord
belongs_to :slot
end
class Slot < ApplicationRecord
belongs_to :place
end
This query
Appointment.joins(slot: [:place])
.where('slot.date' => Date.today)
.ransack(slot_place_id_eq: 2)
.result
returns this error :
ActiveRecord::StatementInvalid (PG::UndefinedTable: ERROR: invalid reference to FROM-clause entry for table "slots")
LINE 1: ...ointments"."slot_id" WHERE "slot"."date" = $1 AND "slots"."p...
^
HINT: Perhaps you meant to reference the table alias "slot".
The generated query is the following, and indeed, slots.place_id can't work for a belongs_to.
SELECT appointments.*
FROM appointments
INNER JOIN slots slot ON slot.id = appointments.slot_id
INNER JOIN places ON places.id = slot.place_id
WHERE slot.date = '2021-06-30' AND slots.place_id = 2
Any clue ?
I just solved this : it was a different issue, with the same error message than the other one.
This :
.where('slot.date' => Date.today)
is supposed to be this :
.where('slots.date' => Date.today)
Two hour of research, 10 minutes to write a SO question, finding the solution by yourself 30 seconds after clicking publish....

Rails active record order by sum of a column

So I have 2 Models Posts and Topics
Posts has number of "viewed".
Posts has a topic.
Here I want to get the most view topics and order it DESC (the highest total views of
all posts tagged with that topic) using rails active record. Here is my current code of what I am trying to do but it is not correct :-
class Topic < ApplicationRecord
has_many :posts
scope :ordered, -> {
joins(:posts).order("sum(posts.viewed) DESC").limit(2).uniq
}
end
You need to group your topics_id
class Topic < ApplicationRecord
has_many :posts
scope :ordered, -> {
joins(:posts).group("topics.id").order('SUM(posts.viewed) desc').limit(2).uniq
}
end
This would work
It is a bad pattern to sum childer and than order by the sum.
I would advise you to add a total_views:integer column to your Topic.rb and update it whenever the sum of post.views changes.
When a child post's viewed value increases, you can call a callback to automatically update the total_views column.
Post.rb can have something like:
after_create do
topic.update_total_views
end
after_update do
topic.update_total_views
end
after_destroy do
topic.update_total_views
end
Topic.rb:
def update_total_views
update_column :total_views, (posts.map(&:viewed).sum)
end
Than in your controller you can call Topic.all.order(total_views: :desc)

Rails Many-to-many relationship with extension generating incorrect SQL

I'm having an issue where a many-to-many relationship with an "extension" is generating incorrect SQL.
class OrderItem < ApplicationRecord
belongs_to :buyer, class_name: :User
belongs_to :order
belongs_to :item, polymorphic: true
end
class User < ApplicationRecord
has_many :order_items_bought,
-> { joins(:order).where.not(orders: { state: :expired }).order(created_at: :desc) },
foreign_key: :buyer_id,
class_name: :OrderItem
has_many :videos_bought,
-> { joins(:orders).select('DISTINCT ON (videos.id) videos.*').reorder('videos.id DESC') },
through: :order_items_bought,
source: :item,
source_type: :Video do
def confirmed
where(orders: { state: :confirmed })
end
end
end
user.videos_bought.confirmed generates this SQL:
Video Load (47.0ms) SELECT DISTINCT ON (videos.id) videos.* FROM
"videos" INNER JOIN "order_items" "order_items_videos_join" ON
"order_items_videos_join"."item_id" = "videos"."id" AND
"order_items_videos_join"."item_type" = $1 INNER JOIN
"orders" ON "orders"."id" = "order_items_videos_join"."order_id" INNER JOIN
"order_items" ON "videos"."id" = "order_items"."item_id" WHERE
"order_items"."buyer_id" = $2 AND ("orders"."state" != $3) AND "order_items"."item_type" = $4 AND
"orders"."state" = $5 ORDER BY videos.id DESC, "order_items"."created_at" DESC LIMIT $6
Which returns some Video records which are joined with orders that do NOT have state confirmed. I would expect all orders to have state confirmed.
If I use raw SQL everything works fine:
has_many :videos_bought,
-> {
joins('INNER JOIN orders ON orders.id = order_items.order_id')
.select('DISTINCT ON (videos.id) videos.*')
.reorder('videos.id DESC')
},
through: :order_items_bought,
source: :item,
source_type: :Video do
def confirmed
where(orders: { state: :confirmed })
end
end
Now user.videos_bought.confirmed generates this SQL:
Video Load (5.4ms) SELECT DISTINCT ON (videos.id) videos.* FROM
"videos" INNER JOIN "order_items" ON
"videos"."id" = "order_items"."item_id" INNER JOIN orders ON
orders.id = order_items.order_id WHERE
"order_items"."buyer_id" = $1 AND ("orders"."state" != $2) AND
"order_items"."item_type" = $3 AND "orders"."state" = $4 ORDER BY
videos.id DESC, "order_items"."created_at" DESC LIMIT $5
Which seems more succinct because it avoids the auto generated order_items_videos_join name. It also only returns orders that have state confirmed.
Any idea what is going on? Does ActiveRecord just generate faulty SQL sometimes?
Using rails 5.1.5. Upgrading to latest made no difference.
I'm hoping to get an explanation on why Rails generates the order_items_videos_join string in the first case but not in the second case. Also, why the second SQL query produces incorrect results. I can edit the question with more code and data samples if needed.
ActiveRecord does not just generate faulty SQL sometimes, but there's a little nuance to it such that starting simple is best when it comes to defining relationships. For example, let's rework your queries to get that DISTINCT ON out of there. I've never seen a need to use that SQL clause.
Before chaining highly customized association logic, let's just see if there's simpler way to query first, and then check to see whether there's a strong case for turning your queries into associations.
Looks like you've got a schema like this:
User
id
Order
id
state
OrderItem
id
order_id
buyer_id (any reason this is on OrderItem and not on Order?)
item_type (Video)
item_id (videos.id)
Video
id
A couple of tidbits
No need to create association extensions for query conditions that would make perfectly good scopes on the model. See below.
A perfectly good query might look like
Video.joins(order_item: :order).
where(order_items: {
buyer_id: 123,
order: {
state: 'confirmed'
}).
# The following was part of the initial logic but
# doesn't alter the query results.
# where.not(order_items: {
# order: {state: 'expired'}
# }).
order('id desc')
Here's another way:
class User < ActiveRecord::Base
has_many :order_items, foreign_key: 'buyer_id'
def videos_purchased
Video.where(id: order_items.videos.confirmed.pluck(:id))
end
end
class OrderItem < ActiveRecord::Base
belongs_to :order, class_name: 'User', foreign_key: 'buyer_id'
belongs_to :item, polymorphic: true
scope :bought, -> {where.not(orders: {state: 'cancelled'})}
scope :videos, -> {where(item_type: 'Video')}
end
class Video < ActiveRecord::Base
has_many :order_items, ...
scope :confirmed, -> {where(orders: {state: 'confirmed'})}
end
...
user = User.first()
user.videos_purchased
I might have the syntax a little screwy when it comes to table and attribute names, but this should be a start.
Notice that I changed it from one to two queries. I suggest running with that until you really notice that you have a performance problem, and even then you might have an easier time just caching queries, than trying to optimize complex join logic.

Create a scope in rails based on HABTM association count

I'm trying to create a rails scope based on the count of a model's HABTM assocation, but I'm struggling with the SQL.
I want Match.open to return matches with less than two users. I also have Match.upcoming, which returns matches with a 'future_date' in the future, which is working well.
My code:
class Match < ActiveRecord::Base
has_and_belongs_to_many :users
scope :open, joins('matches_users').
select('*').
group('matches.id').
having('count(matches_users.user_id) < 2')
scope :upcoming, lambda {
where("proposed_date between ? and ?", Date.today, Date.today.next_month.beginning_of_month)
}
I'm currently getting the error:
SQLite3::SQLException: no such column: matches_users.user_id: SELECT * FROM "matches" matches_users GROUP BY matches.id HAVING count(matches_users.user_id) < 2
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: matches_users.user_id: SELECT * FROM "matches" matches_users GROUP BY matches.id HAVING count(matches_users.user_id) < 2
I'm currently achieving this with a class method:
def self.open
self.select{|match| match.users.length < 2}
end
Which works, but I'd really like to move this into a scope for speed, and so that I can chain the scopes like Match.open.upcoming.
What am I doing wrong here? What's the correct way to do this? Any help would be appreciated.
Give this a shot - I've used something similar before and it seems to work for me:
class Match < ActiveRecord::Base
has_and_belongs_to_many :users
scope :open, joins(:matches_users)
.select('matches.*')
.group('matches.id')
.having('count(matches_users.id) < 2')
...
end