Arel: Need help converting SQL statement into Arel code for Rails - ruby-on-rails

Trying to get the following SQL to work in a Rails query.
The Query:
select addr.*
from addresses addr
join users u
on addr.addressable_type = 'User'
and addr.addressable_id = u.id
join customers c
on c.id = u.actable_id
and u.actable_type = 'Customer'
where c.account_id = 1
and c.site_contact = 't'
This is my Rails code:
# Inside my account.rb model
def site_addresses
a = Address.arel_table #Arel::Table.new(:addresses)
u = User.arel_table #Arel::Table.new(:users)
c = Customer.arel_table #Arel::Table.new(:customers)
# trying to debug/test by rendering the sql. Eventually, I want
# to return a relation array of addresses.
sql = Address.
joins(u).
on(a[:addressable_type].eq("User").and(a[:addressable_id].eq(u[:id]))).
joins(c).
on(c[:id].eq(u[:actable_id]).and(u[:actable_type].eq("Customer"))).
where(c[:account_id].eq(self.id).and(c[:site_contact].eq(true))).to_sql
raise sql.to_yaml #trying to debug, I'll remove this later
end
end
I'm getting errors like "unknown class: Arel::Table". Im not using Arel correctly because the SQL code is valid (I can run it on the database just fine)

Try the following:
a.join(u).on(a[:addressable_type].eq("User")... # Using the arel_table and "join" instead
I based my answer from the docs:
users.join(photos).on(users[:id].eq(photos[:user_id]))

Related

Inner Join in Rails options_from_collection_for_select

I have created below join query to fetch group name details.I am using below query in postgresql.
select DISTINCT e.group_id,e.groupname from user_group_table u, execution_groups e where (u.user_id=e.user_id or u.group_id=e.group_id) and u.user_id=12;
How to write above join query using "options_from_collection_for_select" in rails?
I have tried below code
user_groups = UserGroupTable.where(:user_id => id)
#execution_group_options = ExecutionGroup.where(user_id: user_groups.select(:user_id)).or(ExecutionGroup.where(group_id: user_groups.select(:group_id)))
Getting below error
SELECT "execution_groups".* FROM "execution_groups" WHERE "execution_groups"."user_id" IN (SELECT "user_group_table"."user_id" FROM "user_group_table" WHERE "user_group_table"."user_id" = ?) [["user_id", 6]]
"Threw an exception inside test.new() undefined method `or' for #<ExecutionGroup::ActiveRecord_Relation:0x00007faf9e10ee38>"
Threw an exception inside test.new() undefined method `or' for #<ExecutionGroup::ActiveRecord_Relation:0x00007faf9e10ee38>
Let's start of by writing that query in the Rails query syntax.
I'll assume that user_group_table is available through the UserGroup model and execution_groups is available through the ExecutionGroup model.
# in controller
user_groups = UserGroup.where(user_id: 12)
#execution_group_options =
ExecutionGroup.where(user_id: user_groups.select(:user_id))
.or(ExecutionGroup.where(group_id: user_groups.select(:group_id)))
This produces the following SQL:
SELECT execution_groups.*
FROM execution_groups
WHERE
execution_groups.user_id IN (
SELECT user_group_table.user_id
FROM user_group_table
WHERE user_group_table.user_id = 12
)
OR
execution_groups.group_id IN (
SELECT user_group_table.group_id
FROM user_group_table
WHERE user_group_table.user_id = 12
)
Which should produce the same collection (assuming there are no ExecutionGroup records that have the same group_id and groupname).
With the above in place you can use the following in your view:
options_from_collection_for_select(#execution_group_options, 'group_id', 'groupname')
If there are ExecutionGroup records with both the same group_id and groupname. You can instead use:
options_for_select(#execution_group_options.distinct.pluck(:groupname, :group_id))
If you don't have access to or (introduced in Ruby on Rails 5), things become slightly more complicated. You should be able to use the internal arel interface.
# in controller
user_groups = UserGroup.where(user_id: 12)
exec_groups_table = ExecutionGroup.arel_table
#execution_group_options = ExecutionGroup.where(
exec_groups_table[:user_id].in(user_groups.select(:user_id).arel)
.or(exec_groups_table[:group_id].in(user_groups.select(:group_id).arel))
)
This should produce the exact same query. Other than the controller code nothing changes.

What's the right way to search a Rails model by multiple IDs in a more advanced query?

I'm using Rails 5 and PostGres 9.5. How do I write a Rails query that returns results based on finding multiple IDs? I have this
criteria = "my_objects.id IN ?"
param = "(#{params[:ids]})"
...
#results = MyObject.joins("LEFT OUTER JOIN addresses ON my_objects.address_id = addresses.id")
.where("#{criteria} AND EXISTS (SELECT * FROM my_object_times WHERE my_object_times.my_object_id = my_objects.id)", param)
.order(order_by)
.paginate(:page => params[:page])
The IDs are passed in via a query string parameter, taht looks something like
ids=9e24abc1-1422-4e51-9d0b-72ea444f8110,dcba2558-9bcc-48a2-b5ba-61b230aa796f
but the above results in the error
PG::SyntaxError: ERROR: syntax error at or near "'(9e24abc1-1422-4e51-9d0b-72ea444f8110,dcba2558-9bcc-48a2-b5ba-61b230aa796f)'"
Just pass an array as a parameterized value to the query:
ids = params[:ids].split(',')
#results = MyObject.joins("LEFT OUTER JOIN addresses ON my_objects.address_id = addresses.id")
.where("my_objects.id IN ? AND EXISTS (SELECT * FROM my_object_times WHERE my_object_times.my_object_id = my_objects.id)", ids)
.order(order_by)
.paginate(:page => params[:page])
Additionally you should be able to replace AND EXISTS by doing a INNER LEFT join:
#results.left_outer_joins(:addresses)
.joins(:my_object_times)
.where(id: ids)
left_outer_joins is new in Rails 5.

Why does Rails produce this SQL error, when similar code works elsewhere?

I have this rake task to import a csv file that I'm trying to debug, with a similar pattern to create records in 8 different tables from each row in the file. Records in the independent tables get created, then I need to find those records' id numbers to use as foreign keys to create records in the dependent tables.
The error says:
rake aborted!
ActiveRecord::StatementInvalid: Mysql2::Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'BLANCO) LIMIT 1' at line 1: SELECT `fields`.* FROM `fields` WHERE (IGNACIO BLANCO) LIMIT 1
which I verified in the MySQL console. But in the previous five tables, this similar code works fine to obtain the foreign keys:
require 'csv'
CSV.foreach('public/partial.csv', :headers => true) do |row|
# create the Company object
this_company_name = row['name'].strip!
this_operator_num = row['operator_num']
if !(Companies.exists?(:company_name => this_company_name))
Companies.create(company_name: this_company_name, operator_num: this_operator_num)
end
thecompany = Companies.find_by(company_name: this_company_name)
company_id = thecompany.id
# create the County object
this_county_name = row['county'].strip!
if !(Counties.exists?(county_name: this_county_name))
Counties.create(county_name: this_county_name)
end
thecounty = Counties.find_by(county_name: this_county_name)
county_id = thecounty.id
# create the Gastype object
this_gastype_name = row['gas_type'].strip!
if !(Gastypes.exists?(gas_type: this_gastype_name))
Gastypes.create(gas_type: this_gastype_name)
end
thegastype = Gastypes.find_by(gas_type: this_gastype_name)
gastype_id = thegastype.id
# create the Field object
this_field_name = row['field_name'].strip!
this_field_code = row['field_code'].strip!
if !(Fields.exists?(field_name: this_field_name))
Fields.create(field_name: this_field_name, field_code: this_field_code)
end
thisfield = Fields.find_by(this_field_name)
field_id = thisfield.id
...
The SQL statement that Rails created which produces the error is:
SELECT `fields`.* FROM `fields` WHERE (IGNACIO BLANCO) LIMIT 1;
which has an obviously incorrect WHERE clause. My question is why did Rails not produce the correct statement, like:
SELECT fields.* FROM fields WHERE (field_name ='IGNACIO BLANCO') LIMIT 1;
Should I change how the .find_by statement is written? Or is there a better way of obtaining the requisite foreign key?
Because of this line:
thisfield = Fields.find_by(this_field_name)
You're simply passing a string to find_by, and Rails will consider it to be raw SQL.
You need to use either of these two solutions:
thisfield = Fields.find_by_field_name(this_field_name)
thisfield = Fields.find_by(field_name: this_field_name)

Right way to union in rails 3.1.4 with sqlite3?

There are 3 tables payment_logs, sourcings and purchasings in our rails app. A payment_log belongs to either sourcing or purchasing but not both at the same time. There is a col project_id in both sourcing and purchasing. We want to pick up all payment_logs with its project_id = project_id_search (project_id_search passed from a search page). Also we need a ActiveRecord as resultset returned. Here is the individual query, assuming payment_logs holds the ActiveRecord result set:
pick all payment_logs with its sourcing's project_id = project_id_search
payment_logs = payment_logs.joins(:sourcing).where("sourcings.project_id = ?", project_id_search)
pick all payment_logs with its purchasing's project_id = project_id_search
payment_logs = payment_logs.(:purchasing).where("purchasings.project_id = ?", project_id_search)
We need to union 1 and 2 in order to pick up all the payment_logs whose project_id = project_id_search. What's the right way to accomplish it? We did not find union in rails and find_by_sql returns an array which is not what we want. Thanks.
payment_logs.where(["
payment_logs.sourcing_id IN (
SELECT id FROM sourcings WHERE sourcings.project_id = ?
)
OR payment_logs.purchasing_id IN
(
SELECT id FROM purchasings WHERE purchasings.project_id = ?
)", project_id_search, project_id_search])
Lot of SQL, but it should work
Option 2 (two SQL requests ...) :
payment_logs = []
payment_logs << PaymentLog.joins(:sourcing).where("sourcings.project_id" => project_id_search)
payment_logs << PaymentLog.joins(:purchasing).where("purchasings.project_id" => project_id_search)
payment_logs.uniq! #In case some records have both a sourcing and a purchasing
Option 3, with the squeel gem : https://github.com/ernie/squeel
PaymentLog.where{(source_id.in Sourcing.where(:project_id => project_id_search)) | (purchasing_id.in Purchasing.where(:project_id => project_id_search))}
I like this solution :)
Also, whenever you have a doubt on the generated SQL, from the console or anywhere else, you can add .to_sql at the end of an ActiveRecord query to double check the generated SQL

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