How to properly parameterize my postgresql query - ruby-on-rails

I'm trying to parameterize my postgresql query in order to prevent SQL injection in my ruby on rails application. The SQL query will sum a different value in my table depending on the input.
Here is a simplified version of my function:
def self.calculate_value(value)
calculated_value = ""
if value == "quantity"
calculated_value = "COALESCE(sum(amount), 0)"
elsif value == "retail"
calculated_value = "COALESCE(sum(amount * price), 0)"
elsif value == "wholesale"
calculated_value = "COALESCE(sum(amount * cost), 0)"
end
query = <<-SQL
select CAST(? AS DOUBLE PRECISION) as ? from table1
SQL
return Table1.find_by_sql([query, calculated_value, value])
end
If I call calculate_value("retail"), it will execute the query like this:
select location, CAST('COALESCE(sum(amount * price), 0)' AS DOUBLE PRECISION) as 'retail' from table1 group by location
This results in an error. I want it to execute without the quotes like this:
select location, CAST(COALESCE(sum(amount * price), 0) AS DOUBLE PRECISION) as retail from table1 group by location
I understand that the addition of quotations is what prevents the sql injection but how would I prevent it in this case? What is the best way to handle this scenario?
EDIT: I added an extra column to be fetched from the table to highlight that I can't use pick to get one value.

find_by_sql is used when you want to populate objects with a single line of literal SQL. But we can use ActiveRecord for most of this, we just need one single column. To make objects, use select. If you just want results use pluck.
As you're picking from a fixed set of strings there's no risk of SQL injection in this code. Use Arel.sql to pass along a SQL literal you know is safe.
def self.calculate_value(result_name)
sum_sql = case result_name
when "quantity"
"sum(amount)"
when "retail"
"sum(amount * price)"
when "wholesale"
"sum(amount * cost)"
end
sum_sql = Arel.sql(
"coalesce(cast(#{sum_sql} as double precision), 0) as #{result_name}"
)
return Table1
.group(:location)
# replace pluck with select to get Table1 objects
.pluck(:location, sum_sql)
end

Related

How to access the result of an SQL SUM function in ActiveRecord

The following SQL query in Postgres yields an accurate calculation:
select sum(invoice_items.unit_price * discounts.percentage/100) as discount from invoice_items join items on items.id = invoice_items.item_id join merchants on merchants.id = items.merchant_id join discounts on discounts.merchant_id = merchants.id where invoice_items.quantity >= discounts.threshold and invoice_items.invoice_id = 513;
The ActiveRecord equivalent is producing successful SQL statement:
invoice_items.select('sum(invoice_items.unit_price * discounts.percentage/100)')
.joins(:discounts)
.where('invoice_items.quantity >= discounts.threshold')
And the AR is returning an ActiveRecord_AssociationRelation...yet I cannot get the AR to output the actual SUM.
The question:
How can the AR expression be updated to return the actual value of the SUM function?
What I've tried:
Aliasing the SUM function and then calling it on the resulting AssocationRelation
Iterating through the AssociationRelation to call the alias (method doesn't exist)
Removing the SUM function, adding the alias an then iterating over the results, but I need to return the sum....
Maybe asked this question prematurely.... I've solved the issue.
I update the ActiveRecord query to be this:
invoice_items.select('invoice_items.unit_price * discounts.percentage/100 as calculated_discount')
.joins(:discounts)
.where('invoice_items.quantity >= discounts.threshold')
then in the view I simply called
#invoice.discount.sum(&:calculated_discount)

Multiplying Columns in Active Record?

Have the following sql query, how would I translate the multiplication of two columns in Active Record?
SELECT plan_name, plan_price, count(plan_id), plan_price*count(plan_id) AS totalrevenue
FROM leads
INNER JOIN plans p ON leads.plan_id = p.id
WHERE lead_status_id = 5
GROUP BY plan_name, plan_price;
I'd try something like this.
leads = Lead.joins(:plans).
where(lead_status_id: 5).
group(:plan_name, :plan_price).
select('plan_name, plan_price, count(plan_id), plan_price * count(plan_id) AS totalrevenue')
leads.each { |lead| puts lead.totalrevenue }
Note:
I don't know if it's joins(:plan) or joins(:plans), it depends on your associations
If you need to call even the plan_id count I'd give a name even to that field
A field like totalrevenue is not displaied if you print the entire object, because it's not part of it, it's a virtual attribute

How do I search for '%stars%' within a Postgresql array?

I am trying to find a record in a Postgresql Query, where I have a column 'alternate_names'
t.text "alternate_names", default: [], array: true
And, I would like to find the following record:
altenate_names = ['All Stars']
where my criteria is 'Stars'
In other words, how can I write the following
select * from group where '%Stars%' = ANY(alternate_names);
But this won't return my record above because it has the array ['All Stars']...so, how can I perform an ILIKE '%stars%' with an array?
if you want LIKE operator:
select * from group where alternate_names::text like '%Stars%';
Comparing text representation of array against pattern is not effective, but wildcard before pattern won't be effective in any query on text[]
You can use unnest to check for partial match of all elements in an array.
Ex:
select * from group where exists (select 1 from unnest(alternate_names) as a_names where a_names like '%Stars%');

Rails 4 bulk updating array of models

I have an array of ActiveRecord model and I want to renumber one column and bulk update them.
Code looks like this:
rules = subject.email_rules.order(:number)
rules.each_with_index do |rule, index|
rule.number = index + 1
end
EmailRule.update(rules.map(&:id), rules.map { |r| { number: r.number } })
But this creates N SQL statements and I would like 1, is there a way to do it?
Assuming you are using postgres you can use row_number and the somewhat strange looking UPDATE/FROM construct. This is the basic version:
UPDATE email_rules target
SET number = src.idx
FROM (
SELECT
email_rules.id,
row_number() OVER () as idx
FROM email_rules
) src
WHERE src.id = target.id
You might need to scope this on a subject and of course include the order by number which could look like this:
UPDATE email_rules target
SET number = src.idx
FROM (
SELECT
email_rules.id,
row_number() OVER (partition by subject_id) as idx
FROM email_rules
ORDER BY number ASC
) src
WHERE src.id = target.id
(assuming subject_id is the foreign key that associates subjects/email_rules)
One alternative to you is to put all interaction in a transaction and it will at least make one single commit at the end, making it way faster.
ActiveRecord::Base.transaction do
...
end

Convert PSQL query to work using Rails 4 Objects

I have the following query:
select * from email_contacts
where extract(month from created_at) = 4
and call_type = 'Membership NPS'
and email NOT IN
(
select invite_email from nps_responses
where extract(month from fs_created_at) = 4
and invite_email is not null
)
Then I have a rails 4 app that has two corresponding models, EmailContact and NpsResponse.
How can I convert this query to run within my controller method in Rails? I need to select all email_contacts with the above criteria.
This might not be the perfect "Rails way" solution, but you could to do it by using find_by_sql. Using find_by_sql will still retrieve instantiated objects for your model object:
EmailContact.find_by_sql(your_sql_statement)
Use Bind Variables
To prevent SQL injection, you should use bind variables with the find_by_sql method (bind variables are placeholder parameters represented by the ? character. The find_by_sql method will bind the parameter values before executing the statement.) This is highly recommended from a security perspective.
Here's an example of using your SQL statement with bind variables in the find_by_sql method:
EmailContact.find_by_sql("select * from email_contacts
where extract(month from created_at) = ?
and call_type = ?
and email NOT IN
(
select invite_email from nps_responses
where extract(month from fs_created_at) = ?
and invite_email is not null
)", 4, 'Membership NPS', 4)
Things to notice:
The first parameter to the find_by_sql method is the SQL statement as a string value. In this particular SQL string, there are three ? characters as the placeholder variables.
The next three parameters in find_by_sql are the values to be bound to the ? character in the SQL statement.
The order of the parameters are important - the first bind variable parameter will bind to the first ? character, etc.
You can have a variable amount of bind variable parameters, as long as the number of ? matches the number of bind variable parameters.
For some better code organization, you can have some sort of helper method to construct the SQL statement with the bind variables. Kind of like this:
def email_contacts_SQL
sql = "select * from email_contacts
where extract(month from created_at) = ?
and call_type = ?
and email NOT IN
(
select invite_email from nps_responses
where extract(month from fs_created_at) = ?
and invite_email is not null
)"
end
And then use find_by_sql with your helper method:
EmailContact.find_by_sql(email_contacts_SQL, 4, 'Membership NPS', 4)
The official Rails API doc has more examples of how to use find_by_sql.
Warning: If you use find_by_sql, you lose database agnostic conversions provided by ActiveRecord. The find_by_sql method executes the SQL statement as is.
Something close to what you would write in ActiveRecord, this uses bind variables and protects against sql injection
EmailContact.where("month from created_at ?", some_value).
where(:call_type => 'Membership NPS').
where("email NOT IN (
select invite_email from nps_responses
where extract(month from fs_created_at) = ?
and invite_email is not null
)", some_value).all

Resources