Convert PSQL query to work using Rails 4 Objects - ruby-on-rails

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

Related

Scope Passing arguments in Rails

In the Rails Active Record model we use scopes, for scopes, we pass the arguments. But my doubt is when we pass the default value to the argument it shows the result from that value, when we pass value when executing it shows the newly passed values result, how the values are over written
This is the scope in the model
scope :ftr_post,->(start_date = Date.yesterday,end_date = Date.tomorrow) {where("created_at between ? and ?",start_date,end_date)}
When I run the following in console
Post.ftr_post
Post Load (0.1ms) SELECT "posts".* FROM "posts" WHERE (created_at between '2023-01-24' and '2023-01-26')
It return default arguments
When I run the following
Post.ftr_post("2023-01-11","2023-01-20").count
Post Count (0.1ms) SELECT COUNT(*) FROM "posts" WHERE (created_at between '2023-01-11' and '2023-01-20')
it show the new passed arguments values
Please explain me in detail
You have written code for that and it is working fine.
scope :ftr_post,->(start_date = Date.yesterday,end_date = Date.tomorrow) {where("created_at between ? and ?",start_date,end_date)}
In the ruby, we use PARAMETER = ANY_VALUE in the method for defining default values.
Post.ftr_post
In the above first queries, you are not passing any argument hence it is taking default values start_date and end_date which are Date.yesterday and Date.tomorrow.
Post.ftr_post("2023-01-11","2023-01-20").count
In the second query, you are explicitly passing the arguments hence method will take that values instead of default values. Therefore, it is taking start_date as 2023-01-11 and end_date as 2023-01-20.
Please read here if you want to know more about parameter with default values in ruby

Executing a SQL query with an `IN` clause from Rails code

I know precious nothing abour Rails, so please excuse my naivete about this question.
I'm trying to modify a piece of code that I got from somewhere to make it execute it for a randomly selected bunch of users. Here it goes:
users = RedshiftRecord.connection.execute(<<~SQL
select distinct user_id
from tablename
order by random()
limit 1000
SQL
).to_a
sql = 'select user_id, count(*) from tablename where user_id in (?) group by user_id'
<Library>.on_replica(:something) do
Something::SomethingElse.
connection.
exec_query(sql, users.join(',')).to_h
end
This gives me the following error:
ActiveRecord::StatementInvalid: PG::SyntaxError: ERROR: syntax error at or near ")"
LINE 1: ...ount(*) from tablename where user_id in (?) group by...
^
Users is an array, I know this coz I executed the following and it resulted in true:
p users.instance_of? Array
Would someone please help me execute this code? I want to execute a simple SQL query that would look like this:
select user_id, count(*) from tablename where user_id in (user1,user2,...,user1000) group by user_id
The problem here is that IN takes a list of parameters. Using a single bind IN (?) and a comma separated string will not magically turn it into a list of arguments. Thats just not how SQL works.
What you want is:
where user_id in (?, ?, ?, ...)
Where the number of binds matches the length of the array you want to pass.
The simple but hacky way to do this would be just interpolate in n number of question marks into the SQL string:
binds = Array.new(users.length, '?').join(',')
sql = <<~SQL
select user_id, count(*)
from tablename
where user_id in (#{binds)})
group by user_id'
SQL
<Library>.on_replica(:something) do
Something::SomethingElse.
connection.
exec_query(sql, users).to_h
end
But you would typically do this in a Rails app by creating a model and using the ActiveRecord query interface or using Arel to programatically create the SQL query.

Rails query conditions for hour of creation

In an attempt to summarise traffic data base on a time span, one cannot search invoking a component of a datetime object as such:
txat0 = Transaction.where(['shop_id = ? AND created_at.hour = ?', shop, 0]).count
One could go via the SQL route (i.e. postgresql)
select extract(shop_id, hour from created_at) from transactions
and filter from there.
But what is a succinct way of achieving this with ruby or rails (performance is not a concern for this query) ?
I believe you could do a mix and run the SQL part inside an ActiveRecord query.
What about:
Transaction.where("DATE_PART('hour', created_at) = ?", 0)
PS: I've ignored the shop_id clause in the above example, but you can just add it afterwards.

How to properly parameterize my postgresql query

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

Parameterize an ActiveRecord #joins method

I am refactoring a fairly complex query that involves chaining multiple .joins methods together. In one of these joins I am using a raw SQL query which uses string interpolation i.e joining WHERE foo.id = #{id}. I am aware that I can parameterize ActiveRecord #where by using the ? variable and passing in the arguments as parameters, but the joins method does not support multiple arguments in this fashion. For example:
Using:
Post.my_scope_name.joins("LEFT JOIN posts ON posts.id = images.post_id and posts.id = ?", "1") in order to pass in an id of 1 produces an ActiveRecord::StatementInvalid
because the generated SQL looks like this:
"LEFT JOIN posts ON posts.id = images.post_id and posts.id = ? 1"
What is the standard approach to parameterizing queries when using the joins method?
arel "A Relational Algebra" is the underlying query assembler for Rails and can be used to construct queries, conditions, joins, CTEs, etc. that are not high level supported in Rails. Since this library is an integral part of Rails most Rails query methods will support direct injection of Arel objects without issue (to be honest most methods convert your arguments into one of these objects anyway).
In your case you can construct the join you want as follows:
posts_table = Post.arel_table
images_table = Image.arel_table
some_id = 1
post_join = Arel::Nodes::OuterJoin.new(
posts_table,
Arel::Nodes::On.new(
posts_table[:id].eq(images_table[:post_id])
.and(posts_table[:id].eq(some_id))
)
)
SQL produced:
post_join.to_sql
#=> "LEFT OUTER JOIN [posts] ON [posts].[id] = [images].[post_id] AND [posts].[id] = 1"
Then you just add this join to your current query
Image.joins(post_join)
#=> SELECT images.* FROM images LEFT OUTER JOIN [posts] ON [posts].[id] = [images].[post_id] AND [posts].[id] = 1

Resources