What is wrong with my scope below - ruby-on-rails

I am writing an active record scope in rails. I have the following sql:
SELECT user.roll_number, first_name, last_name FROM user
INNER JOIN classes ON user.roll_number = classes.roll_number
AND classes.id IN #{id} ORDER BY(roll_number)
I wrote the scope in the user class, basically the tables are in the db2 database, so i wrote the scope as shown below
scope :by_id, lambda { |id|
{ select("user.roll_number, first_name, last_name")
.joins("INNER JOIN classes ON (user.roll_number = classes.roll_number \
and classes.id = #{id})")
}
}
Is there something wrong with the scope? I am getting "unexpected keyword end error"

Like said in the comment, you need to remove those {} braces. I'm guessing the error you are seeing is because this:
{ select("...") }
Is interpreted as an instantiation of a Hash, with a key of select("...") and no value. Ruby isn't happy with you.
Your follow-up:
What if i pass multiple ids? i am getting empty list, even though there are records for id 1 and id 2. i am passing id in the array form as id = [1,2]
You made a SQL lambda, so it's all on you to make that SQL work. A list of [1,2] is going to look like:
INNER JOIN classes ON (user.roll_number = classes.roll_number
and classes.id = [1,2])
Will that work on your SQL DB? Wouldn't you want to use and classes.id IN (1,2)?

Related

Check if ActiveRecord::Relation alread includes JOIN

I'm inside method that adds filter (user.type) to my query/relation.
Sometimes if grouping by the user (which needs INNER join to users table in another module) is selected before filtering I receive an error:
PostgreSQL: PG::DuplicateAlias: ERROR: table name "users" specified more than once
Before error happen JOIN is already in query -
$ pry> relation.to_sql
SELECT \"posts\".* FROM \"posts\"
INNER JOIN users ON users.id = posts.user_id
WHERE \"posts\".\"created_at\" BETWEEN '2019-05-01 00:00:00'
AND '2020-05-01 23:59:59' AND \"users\".\"type\" = 'Guest'"
I wanna fix it, by checking if the table is already joined inside my ActiveRecord::Relation object. I added:
def join_users
return relation if /JOIN users/.match? relation.to_sql
relation.joins('LEFT JOIN users ON users.id = posts.user_id')
end
This solution works, but I wonder - is there any better way to check if JOIN is inside relation?
Perhaps you can use joins_values, which isn't documented, but is an ActiveRecord_Relation public method that returns an array containing the name of the table the current query (object) is constructed with:
Post.joins(:user).joins_values # [:user]
Post.all.joins_values # []
if simple join
Post.joins(:user)
you can find via joins_values
so it will look like Post.joins(:user).joins_values # [:user]
if post has left joins
Post.left_joins(:user)
you can find via left_outer_joins_values
So in this case if you write Post.joins(:user).joins_values # []
so you can fix it by writing Post.joins(:user).left_outer_joins_values # [:user]

Searching by multiple possible attributes for one search field

How can I have a search field that can search for multiple different attributess. For example, be able to search for an associated models attribute and the self attribute in one search field?
I want to allow the user to search by what they want to search by, as most websites have this feature...
I have:
Model:
def self.user_search(search)
joins(:shipments).where(shipments: { id: Shipment.where(shipping_label: ShippingLabel.where(tracking_number: "#{search.downcase}")) })
joins(:shipping_address).where(shipping_addresses: { id: ShippingAddress.where(shipping_address_final: ShippingAddressFinal.where(address1: "#{search.downcase}")) })
end
This doesn't work. each joins works on it's own, but together only the last one will ever work.
How can I combine these to work?
Overall, I will be implementing many more possible search attributes but starting with just these 2 will lead me in the correct way.
I have tried using ||
This only allows the first join to work.
Using:
def self.user_search(search)
tracking_number = joins(:shipments).where(shipments: { id: Shipment.where(shipping_label: ShippingLabel.where(tracking_number: "#{search.downcase}")) })
address1 = joins(:shipping_address).where(shipping_addresses: { id: ShippingAddress.where(shipping_address_final: ShippingAddressFinal.where(address1: "#{search.downcase}")) })
tracking_number.or(address1)
end
gives an error upon search:
ArgumentError (Relation passed to #or must be structurally compatible. Incompatible values: [:joins]):
Your or approach is a step in the right direction. The reason you get the error is because the structures are incompatible. Meaning that they should generate the same SQL FROM statement. Currently one query is joining shipments while the other is joining shipping_address.
# statement #1 (tracking_number)
FROM model INNER JOIN shipments ON ...
# statement #2 (address1)
FROM model INNER JOIN shipping_addresses ON ...
You should do the joins first. Both statements should have both joins to make them compatible.
scope = joins(:shipments).joins(:shipping_address)
tracking_number = scope.where(shipments: { id: Shipment.where(shipping_label: ShippingLabel.where(tracking_number: "#{search.downcase}")) })
address1 = scope.where(shipping_addresses: { id: ShippingAddress.where(shipping_address_final: ShippingAddressFinal.where(address1: "#{search.downcase}")) })
tracking_number.or(address1)
The only thing changed in the above statement is that both your queries now join both tables, making them compatible since they generate the same SQL FROM statement.
Resulting in:
# statement #1 (tracking_number)
FROM model
INNER JOIN shipments ON ...
INNER JOIN shipping_addresses ON ...
# statement #2 (address1)
FROM model
INNER JOIN shipments ON ...
INNER JOIN shipping_addresses ON ...

How to get a most recent value group by year by using SQL

I have a Company model that has_many Statement.
class Company < ActiveRecord::Base
has_many :statements
end
I want to get statements that have most latest date field grouped by fiscal_year_end field.
I implemented the function like this:
c = Company.first
c.statements.to_a.group_by{|s| s.fiscal_year_end }.map{|k,v| v.max_by(&:date) }
It works ok, but if possible I want to use ActiveRecord query(SQL), so that I don't need to load unnecessary instance to memory.
How can I write it by using SQL?
select t.username, t.date, t.value
from MyTable t
inner join (
select username, max(date) as MaxDate
from MyTable
group by username
) tm on t.username = tm.username and t.date = tm.MaxDate
For these kinds of things, I find it helpful to get the raw SQL working first, and then translate it into ActiveRecord afterwards. It sounds like a textbook case of GROUP BY:
SELECT fiscal_year_end, MAX(date) AS max_date
FROM statements
WHERE company_id = 1
GROUP BY fiscal_year_end
Now you can express that in ActiveRecord like so:
c = Company.first
c.statements.
group(:fiscal_year_end).
order(nil). # might not be necessary, depending on your association and Rails version
select("fiscal_year_end, MAX(date) AS max_date")
The reason for order(nil) is to prevent ActiveRecord from adding ORDER BY id to the query. Rails 4+ does this automatically. Since you aren't grouping by id, it will cause the error you're seeing. You could also order(:fiscal_year_end) if that is what you want.
That will give you a bunch of Statement objects. They will be read-only, and every attribute will be nil except for fiscal_year_end and the magically-present new field max_date. These instances don't represent specific statements, but statement "groups" from your query. So you can do something like this:
- #statements_by_fiscal_year_end.each do |s|
%tr
%td= s.fiscal_year_end
%td= s.max_date
Note there is no n+1 query problem here, because you fetched everything you need in one query.
If you decide that you need more than just the max date, e.g. you want the whole statement with the latest date, then you should look at your options for the greatest n per group problem. For raw SQL I like LATERAL JOIN, but the easiest approach to use with ActiveRecord is DISTINCT ON.
Oh one more tip: For debugging weird errors, I find it helpful to confirm what SQL ActiveRecord is trying to use. You can use to_sql to get that:
c = Company.first
puts c.statements.
group(:fiscal_year_end).
select("fiscal_year_end, MAX(date) AS max_date").
to_sql
In that example, I'm leaving off order(nil) so you can see that ActiveRecord is adding an ORDER BY clause you don't want.
for example you want to get all statements by start of the months you should use this
#companey = Company.first
#statements = #companey.statements.find(:all, :order => 'due_at, id', :limit => 50)
then group them as you want
#monthly_statements = #statements.group_by { |statement| t.due_at.beginning_of_month }
Building upon Bharat's answer you can do this type of query in Rails using find_by_sql in this way:
Statement.find_by_sql ["Select t.* from statements t INNER JOIN (
SELECT fiscal_year_end, max(date) as MaxDate GROUP BY fiscal_year_end
) tm on t.fiscal_year_end = tm.fiscal_year_end AND
t.created_at = tm.MaxDate WHERE t.company_id = ?", company.id]
Note the last where part to make sure the statements belong to a specific company instance, and that this is called from the class. I haven't tested this with the array form, but I believe you can turn this into a scope and use it like this:
# In Statement model
scope :latest_from_fiscal_year, lambda |enterprise_id| {
find_by_sql[..., enterprise_id] # Query above
}
# Wherever you need these statements for a particular company
company = Company.find(params[:id])
latest_statements = Statement.latest_from_fiscal_year(company.id)
Note that if you somehow need all the latest statements for all companies then this most likely leave you with a N+1 queries problem. But that is a beast for another day.
Note: If anyone else has a way to have this query work on the association without using the last where part (company.statements.latest_from_year and such) let me know and I'll edit this, in my case in rails 3 it just pulled em from the whole table without filtering.

How to found in Rails using a method that is not an attribute

How do you make the following work...
Beagle.beagles.where(:snount => "short")
...in situations where a snout method id defined for Beagle, but there is no snout column in the beagles table?
If you need a scope (i.e. an ActiveRecord::Relation object) then I think you're out of luck, because where deals with DB query logic, not model instance methods. But if you just need to get an array of results that satisfy the method requirement, then this will work:
Beagle.select { |b| b.snout == "short" }
Note that this will grab all records from the DB, then select the ones you want from them. Here's the generated SQL:
Beagle Load (0.1ms) SELECT "beagles".* FROM "beagles"
See also: instance method in scope
maybe try this
beagles = Beagle.find(:all)
beagles.each do |beagle|
if beagle.snount == "short"
beagles_selected << beagle
end
end
you will then have a beagles_selected array containing all the beagles you need

Rails, how to sanitize SQL in find_by_sql

Is there a way to sanitize sql in rails method find_by_sql?
I've tried this solution:
Ruby on Rails: How to sanitize a string for SQL when not using find?
But it fails at
Model.execute_sql("Update users set active = 0 where id = 2")
It throws an error, but sql code is executed and the user with ID 2 now has a disabled account.
Simple find_by_sql also does not work:
Model.find_by_sql("UPDATE user set active = 0 where id = 1")
# => code executed, user with id 1 have now ban
Edit:
Well my client requested to make that function (select by sql) in admin panel to make some complex query(joins, special conditions etc). So I really want to find_by_sql that.
Second Edit:
I want to achieve that 'evil' SQL code won't be executed.
In admin panel you can type query -> Update users set admin = true where id = 232 and I want to block any UPDATE / DROP / ALTER SQL command.
Just want to know, that here you can ONLY execute SELECT.
After some attempts I conclude sanitize_sql_array unfortunatelly don't do that.
Is there a way to do that in Rails??
Sorry for the confusion..
Try this:
connect = ActiveRecord::Base.connection();
connect.execute(ActiveRecord::Base.send(:sanitize_sql_array, "your string"))
You can save it in variable and use for your purposes.
I made a little snippet for this that you can put in initializers.
class ActiveRecord::Base
def self.escape_sql(array)
self.send(:sanitize_sql_array, array)
end
end
Right now you can escape your query with this:
query = User.escape_sql(["Update users set active = ? where id = ?", true, params[:id]])
And you can call the query any way you like:
users = User.find_by_sql(query)
Slightly more general-purpose:
class ActiveRecord::Base
def self.escape_sql(clause, *rest)
self.send(:sanitize_sql_array, rest.empty? ? clause : ([clause] + rest))
end
end
This one lets you call it just like you'd type in a where clause, without extra brackets, and using either array-style ? or hash-style interpolations.
User.find_by_sql(["SELECT * FROM users WHERE (name = ?)", params])
Source: http://blog.endpoint.com/2012/10/dont-sleep-on-rails-3-sql-injection.html
Though this example is for INSERT query, one can use similar approach for UPDATE queries. Raw SQL bulk insert:
users_places = []
users_values = []
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
params[:users].each do |user|
users_places << "(?,?,?,?)" # Append to array
users_values << user[:name] << user[:punch_line] << timestamp << timestamp
end
bulk_insert_users_sql_arr = ["INSERT INTO users (name, punch_line, created_at, updated_at) VALUES #{users_places.join(", ")}"] + users_values
begin
sql = ActiveRecord::Base.send(:sanitize_sql_array, bulk_insert_users_sql_arr)
ActiveRecord::Base.connection.execute(sql)
rescue
"something went wrong with the bulk insert sql query"
end
Here is the reference to sanitize_sql_array method in ActiveRecord::Base, it generates the proper query string by escaping the single quotes in the strings. For example the punch_line "Don't let them get you down" will become "Don\'t let them get you down".
I prefer to do it with key parameters. In your case it may looks like this:
Model.find_by_sql(["UPDATE user set active = :active where id = :id", active: 0, id: 1])
Pay attention, that you pass ONLY ONE parameter to :find_by_sql method - its an array, which contains two elements: string query and hash with params (since its our favourite Ruby, you can omit the curly brackets).

Resources