Join on a subquery with grouped by in Arel? - ruby-on-rails-3

I've searched a lot here but couldn't find a similar topic: i need to write a Query in Arel because i use will_paginate to browse through the results, so i'd loose much comfort in implementation with raw SQL.
Here's what i need to spell in Arel:
SELECT m.*
FROM messages m
JOIN (SELECT tmp.original_id as original_id,
max(tmp.id) as id
FROM messages tmp
WHERE tmp.recipient_id = ?
GROUP BY tmp.original_id) g ON (m.id = g.id)
ORDER BY m.updated_at DESC;
Explained in short words: the subquery retrieves all messages for a user. If a message has newer replies (in replies i save the refering message id as original_id) the older ones will be ignored. For the result of all these messages i want Rails to deliver me the correpsonding objects.
I'm quite skilled in SQL but unfortunately not with Arel. Any help would be kindly appreciated.

How about something like this?
class Message < ActiveRecord::Base
scope :newer_messages lambda { |num_days=10| where("updated_at > #{Time.now} - #{num_days}.days") }
end
You can then find newer messages for a given user like this:
Message.find_by_user_id(user_id).newer_messages

Related

Why does Postgres not accept my count column?

I am building a Rails app with the following models:
# vote.rb
class Vote < ApplicationRecord
belongs_to :person
belongs_to :show
scope :fulfilled, -> { where(fulfilled: true) }
scope :unfulfilled, -> { where(fulfilled: false) }
end
# person.rb
class Person < ApplicationRecord
has_many :votes, dependent: :destroy
def self.order_by_votes(show = nil)
count = 'nullif(votes.fulfilled, true)'
count = "case when votes.show_id = #{show.id} AND NOT votes.fulfilled then 1 else null end" if show
people = left_joins(:votes).group(:id).uniq!(:group)
people = people.select("people.*, COUNT(#{count}) AS people.vote_count")
people.order('people.vote_count DESC')
end
end
The idea behind order_by_votes is to sort People by the number of unfulfilled votes, either counting all votes, or counting only votes associated with a given Show.
This seem to work fine when I test against SQLite. But when I switch to Postgres I get this error:
Error:
PeopleControllerIndexTest#test_should_get_previously_on_show:
ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column people.vote_count does not exist
LINE 1: ...s"."show_id" = $1 GROUP BY "people"."id" ORDER BY people.vot...
^
If I dump the SQL using #people.to_sql, this is what I get:
SELECT people.*, COUNT(nullif(votes.fulfilled, true)) AS people.vote_count FROM "people" LEFT OUTER JOIN "votes" ON "votes"."person_id" = "people"."id" GROUP BY "people"."id" ORDER BY people.vote_count DESC
Why is this failing on Postgres but working on SQLite? And what should I be doing instead to make it work on Postgres?
(PS: I named the field people.vote_count, with a dot, so I can access it in my view without having to do another SQL query to actually view the vote count for each person in the view (not sure if this works) but I get the same error even if I name the field simply vote_count.)
(PS2: I recently added the .uniq!(:group) because of some deprecation warning for Rails 6.2, but I couldn't find any documentation for it so I am not sure I am doing it right, still the error is there without that part.)
Are you sure you're not getting a syntax error from PostgreSQL somewhere? If you do something like this:
select count(*) as t.vote_count from t ... order by t.vote_count
I get a syntax error before PostgreSQL gets to complain about there being no t.vote_count column.
No matter, the solution is to not try to put your vote_count in the people table:
people = people.select("people.*, COUNT(#{count}) AS vote_count")
...
people.order(vote_count: :desc)
You don't need it there, you'll still be able to reference the vote_count just like any "normal" column in people. Anything in the select list will appear as an accessor in the resultant model instances whether they're columns or not, they won't show up in the #inspect output (since that's generated based on the table's columns) but you call the accessor methods nonetheless.
Historically there have been quite a few AR problems (and bugs) in getting the right count by just using count on a scope, and I am not sure they are actually all gone.
That depends on the scope (AR version, relations, group, sort, uniq, etc). A defaut count call that a gem has to generically use on a scope is not a one-fit-all solution. For that known reason Pagy allows you to pass the right count to its pagy method as explained in the Pagy documentation.
Your scope might become complex and the default pagy collection.count(:all) may not get the actual count. In that case you can get the right count with some custom statement, and pass it to pagy.
#pagy, #records = pagy(collection, count: your_count)
Notice: pagy will efficiently skip its internal count query and will just use the passed :count variable.
So... just get your own calculated count and pass it to pagy, and it will not even try to use the default.
EDIT: I forgot to mention: you may want to try the pagy arel extra that:
adds specialized pagination for collections from sql databases with GROUP BY clauses, by computing the total number of results with COUNT(*) OVER ().
Thanks to all the comments and answers I have finally found a solution which I think is the best way to solve this.
First of, the issue occurred when I called pagy which tried to count my scope by appending .count(:all). This is what caused the errors. The solution was to not create a "field" in select() and use it in .order().
So here is the proper code:
def self.order_by_votes(show = nil)
count = if show
"case when votes.show_id = #{show.id} AND NOT votes.fulfilled then 1 else null end"
else
'nullif(votes.fulfilled, true)'
end
left_joins(:votes).group(:id)
.uniq!(:group)
.select("people.*, COUNT(#{count}) as vote_count")
.order(Arel.sql("COUNT(#{count}) DESC"))
end
This sorts the number of people on the number of unfulfilled votes for them, with the ability to count only votes for a given show, and it works with pagy(), and pagy_arel() which in my case is a much better fit, so the results can be properly paginated.

Rails - get distinct events, sorted by the start date of associated event instances

I've spent several hours going through StackOverflow and playing around with this query, but still can't get it to work! Hopefully an expert here on SO can make the pain go away...
I have two models, Event and EventInstance. An Event has_many EventInstances.
What I want to do is easily get a list of Events (not EventInstances), where:
Events are distinct and not repeated
Events are sorted by the start_date of the nearest EventInstance
Event instances have the attribute :active => true
Only event instances that have a start date in the future are returned
I currently have the query
Event.joins(:event_instances).select('distinct events.*').where('event_instances.start_date >= ?', Time.now).where('event_instances.active = true')
This returns a list of events, but not sorted by date. Excellent - so I am almost there!
If I change the query to add this on the end:
.order('event_instances.start_date')
I get the error:
PG::InvalidColumnReference: ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select list
So I moved it to the select statement:
select('distinct event_instances.start_date, events.*')
Now I get
PG::UndefinedFunction: ERROR: function count(date, events) does not exist
I've tried moving methods around, using includes, everything but I still can't get it to work. Any help would be really appreciated! Thank you.
try changing
.order('event_instances.start_date')
to
.order(:event_instances.start_date)
or if you need descending order add the .reverse_order method to the end of the query
This is the exact query which worked for my models Post and PostComments on both MySQL and PostgreSQL:
Post.joins(:post_comments).select('distinct post_comments.body, post_comments.created_at').order('post_comments.created_at desc')
So for you, it's equivalent should work too. If it still doesn't then please update your post with the fields of your model.

Trying to refactor Rails 4.2 scope

I have updated this question
I have the following SQL scope in a RAILS 4 app, it works, but has a couple of issues.
1) Its really RAW SQL and not the rails way
2) The string interpolation opens up risks with SQL injection
here is what I have:
scope :not_complete -> (user_id) { joins("WHERE id NOT IN
(SELECT modyule_id FROM completions WHERE user_id = #{user_id})")}
The relationship is many to many, using a join table called completions for matching id(s) on relationships between users and modyules.
any help with making this Rails(y) and how to set this up to take the arg of user_id with out the risk, so I can call it like:
Modyule.not_complete("1")
Thanks!
You should have added few info about the models and their assocciation, anyways here's my trial, might have some errors because I don't know if the assocciation is one to many or many to many.
scope :not_complete, lambda do |user_id|
joins(:completion).where.not( # or :completions ?
id: Completion.where(user_id: user_id).pluck(modyule_id)
)
end
PS: I turned it into multi line just for readability, you can change it back to a oneline if you like.

Converting SQL into Arel or Squeel

tl;dr
How to convert below SQL to Arel(or whatever is considered standard in Rails)
#toplist = ActiveRecord::Base.connection.execute(
'select ci.crash_info_id,
count(distinct(user_guid))[Occurences],
c.md5
from crashes ci
join crash_infos c on c.id=crash_info_id
group by ci.crash_info_id
order by [Occurences] desc')
--- end of tl;dr ----
I'm working on a small web project, it's goal is to take our customers crash reports(when our desktop app crashes, we send diagnostics to our servers), analyze them and then display what is the most common bug that causes our application to crash.. So that we concentrate on fixing bugs that affect biggest chunk of our users..
I have 2 tables:
crash_infos - id, md5(md5 of a stacktrace. in .net stacktrace and exception messages are sometimes translated by .net to user's native language! so I basically sanitize exception, by removing language specific parts and produce md5 out of it)
crashes - id, user_guid(user id), crash_info_id(id to crash_infos table)
Now the question, I wanted to make a query that shows most common crashes for unique user(avoid counting many times same crash for same user) and sort it by number of crashes. Unfortunately I didn't know enough Arel(or whatever is the ruby on rails way), so I ended up with raw SQL :(
#toplist = ActiveRecord::Base.connection.execute(
'select ci.crash_info_id,
count(distinct(user_guid))[Occurences],
c.md5
from crashes ci
join crash_infos c on c.id=crash_info_id
group by ci.crash_info_id
order by [Occurences] desc')
How can I convert this, to something more "railsy" ?
Thank you in advance
Not sure if this is actually any better but at least you should have a database independent query...
crashes = Arel::Table.new(:crashes)
crash_infos = Arel::Table.new(:crash_infos)
crashes.
project(crashes[:crash_info_id], crash_infos[:md5], Arel::Nodes::Count.new([crashes[:user_guid]], true, "occurences")).
join(crash_infos).on(crashes[:crash_info_id].eq(crash_infos[:id])).
group(crashes[:crash_info_id]).
order("occurences DESC").
to_sql
Gives:
SELECT "crashes"."crash_info_id", "crash_infos"."md5", COUNT(DISTINCT "crashes"."user_guid") AS occurences FROM "crashes" INNER JOIN "crash_infos" ON "crashes"."crash_info_id" = "crash_infos"."id" GROUP BY "crashes"."crash_info_id" ORDER BY occurences DESC

what is the same equivalent query in `rails console` with this sql ?

SQL:
select * from user where room_id not in (select id from rooms);
what is the same equivalent query in rails console with this sql?
ex:
User.all.each { |u| user.room }
(sorry, but this example is not correct.)
You can translate it almost literally:
User.where('room_id not in (select id from rooms)').all
The where clause is quite flexible in what it accepts.
User.where("room_id not in (select id from rooms)")
but you want this since it would be rather faster:
User.where("not exist (select id from rooms where id=users.room_id)")
that's the closest you can get. There appears to be no way to create an Active Record query that translates to SQL NOT(). A search on the subject returns a bunch of SO questions with much the same answer.
You could do something like
User.all.select { |u| !Room.find_by_id(u.room_id) }
But that could be less efficient again.
I don't know if you are familiar with the squeel gem. It allows allows you to build very complex SQL-queries in a pure Ruby code. In your case it should be as simple as the following code (afer adding the gem 'squeel' line in your Gemfile and running bundle install):
room_ids = Room.select{id}; User.where{room_id.not_in room_ids}
Worth trying, isn't it?
Here's the squeel's page.