How to write this SQL using Rails' read methods? - sql

I have the following setup:
question.rb
class Question < ActiveRecord::Base
has_many :answers
#validations, methods, etc
...
#Returns the questions with the most answers
def Question.top_questions(max = 10)
sql = "SELECT question_id, COUNT('question_id') as aCount FROM answers GROUP BY question_id ORDER BY aCount DESC LIMIT #{max.to_i}" # Probably shouldn't use string interpolation here :D
Question.connection.execute(sql)
end
end
answer.rb
class Answer < ActiveRecord::Base
belongs_to :question
...
end
And if I call Question.top_questions(), then it returns this:
[{"question_id"=>1, "aCount"=>25, 0=>1, 1=>25}, {"question_id"=>38, "aCount"=>3, 0=>38, 1=>3}, {"question_id"=>45, "aCount"=>3, 0=>45, 1=>3}, {"question_id"=>26, "aCount"=>2, 0=>26, 1=>2}, {"question_id"=>46, "aCount"=>2, 0=>46, 1=>2}, {"question_id"=>48, "aCount"=>2, 0=>48, 1=>2}, {"question_id"=>51, "aCount"=>2, 0=>51, 1=>2}, {"question_id"=>5, "aCount"=>1, 0=>5, 1=>1}, {"question_id"=>15, "aCount"=>1, 0=>15, 1=>1}, {"question_id"=>20, "aCount"=>1, 0=>20, 1=>1}]
I'm not sure how I would use the data returned in a view while still keeping the code clean.
So I'm wondering if I could write the Question.top_questions() method using rails' read methods(find(), where(), etc). Or how I could get it to return an array of Question objects.

It returns an array of hashes, you could use it in a view as you like.
But if you don't want to write native sql, you could rewrite it as below.
def self.top_questions(max = 10)
Question.joins('LEFT JOIN answers ON questions.id = answers.question_id')
.select('questions.*, count(answers.id) as answers_count')
.group('questions.id')
.order('answers_count desc')
.limit(max)
end

Related

Rails Brakeman: SQL Injection for Arel

I have the following code in my user_ransaker.rb file:
ransacker :new_donors do
sql = %{(
users.id IN (
#{User.new_donor_sql}
)
)}
Arel.sql(sql)
end
On user.rb model:
def self.new_donor_sql
part_1 = %{(
SELECT distinct(user_id)
FROM donations
}
part_1
end
I get the following Brakeman warning for above statement:
Confidence: High
Category: SQL Injection
Check: SQL
Message: Possible SQL injection
Code: Arel.sql("(\n users.id IN (\n #{User.new_donor_sql}\n)\n)")
File: app/models/concerns/user_ransackers.rb
Is this a valid error? If I used ActiveRecord to write the SQL statement, I could have used ? placeholder if I needed to interpolate values. I am not really sure how to fix this warning. If this is a valid warning, how do I remediate it?
If you gonna Arel then do some relational algebra:
class User < ApplicationRecord
def self.new_donor_sql
arel_table.project(arel_table[:user_id]).distinct
end
end
ransacker :new_donors do
User.arel_table.then do |users|
users.where(users[:id].in(User.new_donor_sql)).where_sql
end
end
You could also just drop the class method:
ransacker :new_donors do
User.arel_table.then do |users|
subquery = users.project(users[:user_id]).distinct
users.where(users[:id].in(subquery)).where_sql
end
end

Relation passed to #or must be structurally compatible. Incompatible values: [:joins]

I have this error message
Relation passed to #or must be structurally compatible. Incompatible values: [:joins]
in my user model: has_many :orders
in my order model: belongs_to :user, optional: true
How I am supposed to write my query to have either the users' names and the order id in the same search input?
def filter_orders
return if params[:query].blank?
#orders = Order.joins(:user).where('lower(users.first_name) LIKE ?', "%#{params[:query][:keyword]}%")
.or(Order.joins(:user).where('lower(users.last_name) LIKE ?', "%#{params[:query][:keyword]}%"))
.or(Order.where(id: "#{params[:query][:keyword]}.to_i"))
end
It sound like this is a know issue with .or. Try using SQL or you can override .or as seen in this answer: https://stackoverflow.com/a/40742512/10987825
It occurs when you try to combine two multi-active records of the same type, but one of them has a reference value or an includes value, or in your case a joins value, that the other does not.
Therefore we need to match the values between them, and I found a general way to do this without knowing the actual values in advance.
def filter_orders
return if params[:query].blank?
orders_1 = Order.joins(:user).where('lower(users.first_name) LIKE ?', "%#{params[:query][:keyword]}%")
orders_2 = Order.joins(:user).where('lower(users.last_name) LIKE ?', "%#{params[:query][:keyword]}%")
orders_3 = Order.where(id: "#{params[:query][:keyword]}.to_i")
joind_orders = orders_1.or(
orders_2
.joins(orders_1.joins_values)
.includes(orders_1.includes_values)
.references(orders_1.references_values)
)
#orders = joind_orders.or(
orders_3
.joins(joind_orders.joins_values)
.includes(joind_orders.includes_values)
.references(joind_orders.references_values)
)
end

Postgres full text search using pg_search - includes(:child_model) breaks the SQL with 'missing FROM-clause entry for table 'child_model'

I'm using postgres full text search with the pg_search gem. The search itself is working well, but I need to further filter the results and here are the details:
class Notebook < ActiveRecord::Base
has_many :invites
def self.text_search(query)
if query.present?
search(query)
else
scoped
end
end
Notebooks Controller:
def index
if params[:query].present?
#notebooks = Notebook.text_search(params[:query]).includes(:invites).where("invites.email = :email OR notebooks.access = :access OR notebooks.access = :caccess OR notebooks.user_id = :uid", email: current_user.email, access: "open", caccess: "closed", uid: current_user.id)
else
#notebooks = Notebook.includes(:invites).where("invites.email = :email OR notebooks.access = :access OR notebooks.access = :caccess OR notebooks.user_id = :uid", email: current_user.email, access: "open", caccess: "closed", uid: current_user.id)
end
The error I get is 'missing FROM-clause entry for table 'invites'. I have tried many different things including:
replacing 'includes' with 'joins'
replacing 'includes(:invites) with joins('LEFT JOIN "invites" ON "invites"."email" = "email" ')
changing the order of the .text_search and the .includes calls.
adding the includes call in the controller, in the model, in a scope, and in the text_search function definition.
I keep getting the same error, and when using the joins call with SQL it does not filter by invite emails, and shows multiple repeats of each search result.
I would just remove the include(:invites) because the text_search itself is working just fine. But I really need this condition to be included.
Any help would be greatly appreciated. Maybe I'm just getting my SQL call wrong, but I also would like to understand why the .includes(:invites) works without the pg text_search but won't work with it.
Edit #1 - more specific question
I think there are 2 slightly different questions here. The first seems to be some issue with combining pg_search gem and an 'includes(:invites)' call. The second question is what is the equivalent SQL statement that I can use in order to avoid making the 'includes(:invites)' call. I think it should be a LEFT JOIN of some sort, but I don't think I'm making it correctly. In my db, a Notebook has_many invites, and invites have an attribute 'email'. I need the the notebooks with invites that have an email equal to the current_user's email.
Help with either of these would be great.
Here is the link that showed me the solution to my problem:
https://github.com/Casecommons/pg_search/issues/109
Here is my specific code:
class Notebook < ActiveRecord::Base
has_many :invites
include PgSearch
pg_search_scope :search, against: [:title],
using: {tsearch: {dictionary: "english"}},
associated_against: {user: :name, notes:[:title, :content]}
scope :with_invites_and_access, lambda{ |c_user_email|
joins('LEFT OUTER JOIN invites ON invites.notebook_id = notebooks.id').where('invites.email = ? OR notebooks.access = ? OR notebooks.access = ?', c_user_email, 'open', 'closed')
}
def self.text_search(query)
if query.present?
search(query).with_invites_and_access(current_user_email)
else
scoped
end
end
end
The key was the joins statement. joins(:invites) doesn't work, includes(:invites) doesn't work. The full SQL statement is required:
joins('LEFT OUTER JOIN invites ON invites.notebook_id = notebooks.id')
I can see a join but I cannot see what makes joined invites fields to appear in the SELECT statement.
I think You may need to add the fields from the invites table into select() like this
select('invites.*').joins('LEFT OUTER JOIN invites ON invites.notebook_id = notebooks.id').where('invites.email = ? OR notebooks.access = ? OR notebooks.access = ?', c_user_email, 'open', 'closed')
}

How to Write Where Clause Condition in Rails using AND & OR Operator

def self.get_previous_feedback current_feedback
Feedback.where("feedbacks.id < ?", current_feedback.id).order('created_at asc').last
end
def self.get_next_feedback current_feedback
Feedback.where("feedbacks.id > ?", current_feedback.id).order('created_at asc').first
end
#current_feeedback is the show page of any feedback.( feedback/show/id=2)
I have got 3 tables in my DB. Feedback, User, Department are connected in one-many relation.
By running above codes I am able to navigate to next/previous Feedback.
My User (current_user) is logged in, and Now on clicking prev/next, I want to retrieve the next feedback from DB(where condition written above) + whose feedback.department_id = current_user.deparment_id.
For including department_id in need to write an AND statement. How to do that ?
Try this...
def self.get_previous_feedback(current_feedback,current_user)
Feedback.where("id < ? & department_id = ?", current_feedback.id, current_user.department_id).order('created_at asc').last
end
def self.get_next_feedback(current_feedback,current_user)
Feedback.where("id > ? & department_id = ?", current_feedback.id, current_user.department_id).order('created_at asc').first
end
Thanks for pointing me out the correct logic.
Above query needs bit modification to work correctly.
simply id does not link to the feedbacks table, hence had to use feedbacks.id
& needs to be replaced by AND.
Corrected Code :
Feedback.where("feedbacks.id < ? AND feedbacks.department_id = ?",
current_feedback.id, current_user.department_id).order('created_at
asc').last
Thanks Man ! :)
You can also chain the two conditions, that way you don't need use the AND and you can also re-use the department_id clause.
def self.get_department_feedback current_user
Feedback.where(department_id: current_user.deparment_id)
end
def self.get_previous_feedback(current_feedback,current_user)
get_deparment_feedback(current_user).where("feedbacks.id < ?", current_feedback.id).order('created_at asc').last
end
def self.get_next_feedback(current_feedback,current_user)
get_deparment_feedback(current_user).where("feedbacks.id > ?", current_feedback.id).order('created_at asc').first
end

How to properly add brackets to SQL queries with 'or' and 'and' clauses by using Arel?

I am using Ruby on Rails 3.2.2 and I would like to generate the following SQL query:
SELECT `articles`.* FROM `articles` WHERE (`articles`.`user_id` = 1 OR `articles`.`status` = 'published' OR (`articles`.`status` = 'temp' AND `articles`.`user_id` IN (10, 11, 12, <...>)))
By using Arel this way
Article
.where(
arel_table[:user_id].eq(1)
.or(arel_table[:status].eq("published"))
.or(
arel_table[:status].eq("temp")
.and(
arel_table[:user_id].in(10, 11, 12, <...>)
)
)
)
it generates the following (note: brackets are not the same as the first SQL query):
SELECT `articles`.* FROM `articles` WHERE (((`articles`.`user_id` = 1 OR `articles`.`status` = 'published') OR `articles`.`status` = 'temp' AND `articles`.`user_id` IN (10, 11, 12, <...>)))
Since I think the latter SQL query doesn't "work" as the first one, how could I use Arel (or, maybe, something else) so to generate the SQL query as the first one?
Update (after comments)
Given SQL queries above "work" the same but I still would like to generate the exact SQL query as the first one in the question (the main reason to make this is that the first SQL query is more readable than the second since in the first one are used less and "explicit" brackets), how could I make that by using Arel?
I had the same problem. I was searching the web for some hours and finally found a method named grouping in Arel::FactoryMethods which simply adds brackets around an expression.
You should wrap your groups with a arel_table.grouping(...) call.
Example of how to use arel_table.grouping(...) as part of scope
# app/model/candy.rb
class Candy < ActiveRecord::Base
has_many :candy_ownerships
has_many :clients, through: :candy_ownerships, source: :owner, source_type: 'Client'
has_many :users, through: :candy_ownerships, source: :owner, source_type: 'User'
# ....
scope :for_user_or_global, ->(user) do
# ->() is new lambda syntax, lamdba{|user| ....}
worldwide_candies = where(type: 'WorldwideCandies').where_values.reduce(:and)
client_candies = where(type: 'ClientCandies', candy_ownerships: { owner_id: user.client.id, owner_type: 'Client'}).where_values.reduce(:and)
user_candies = where(type: 'UserCandies', candy_ownerships: { owner_id: user.id, owner_type: 'User' }).where_values.reduce(:and)
joins(:candy_ownerships).where( worldwide_candies.or( arel_table.grouping(client_candies) ).or( arel_table.grouping(user_candies) ) )
end
# ....
end
call
Candy.for_user_or_global(User.last)
#=> SELECT `candies`.* FROM `candies` INNER JOIN `candy_ownerships` ON `candy_ownerships`.`candy_id` = `candies`.`id` WHERE (`candies`.`deleted_at` IS NULL) AND (((`candies`.`type` = 'WorldwideCandies' OR (`candies`.`type` = 'ClientCandies' AND `candy_ownerships`.`owner_id` = 19 AND `candy_ownerships`.`owner_type` = 'Client')) OR (`candies`.`type` = 'UserCandies' AND `candy_ownerships`.`owner_id` = 121 AND `candy_ownerships`.`owner_type` = 'User')))
thx micha for the tip
I've successfully used this gem: squeel which comes on top of Arel so you don't have to mess with it. So in order to generate your query you would do something like this in Squeel:
#articles = Article.
where{
( user_id.eq(1) | status.eq('published') ) |
( user_id.in([10, 11, 12, '<...>']) & status.eq('temp') )
}
# since this is an ActiveRecord::Relation we can play around with it
#articles = #articles.select{ [ user_id, status ] }
# and you can also inspect your SQL to see what is going to come out
puts #articles.to_sql
The more complicated your queries get the more you're going to like this gem.