Chaining complex queries using ActiveRecord - ruby-on-rails

Let's suppose I have a rails model, Table, with methods location,a1,a2,a3
I have a postgresql query of the form
SELECT avg(val) FROM (
SELECT unnest(array[a1,a2,a3]) as val FROM table WHERE location = 'USA') alias;
Which I would like to undertake using activerecord, rather than as raw SQL, mainly because I would like to be able to split the query so it's chainable, along the lines of:
Table.where(location: 'USA').select(...)
(The reason for this is that I'd like to move the postgresql query above into the model as a method.)
Is there any way this can be done?

Table.from(
Table.where(location: 'USA').select('unnest(array[a1,a2,a3]) as val'),
'subquery_name'
).average('subquery_name.val')
Now, for you to understand what the above query is doing, please study the following ActiveRecord methods:
from(value, subquery_name = nil)
average(column_name, options = {})

Related

parameter based joins not susceptible for sql injection

I want to do non-standard query with join based on the parameters.
For example I have 2 tables: a_examples and b_examples both with fields: field_x and field_y.
I want to join rows when both tables have the same values on field_x(or field_y).
Example query can looks like this:
AExample.joins('INNER JOIN b_examples ON b_examples.field_x = a_examples.field_x')
The problem occurs when I have field name based on parameter.
For example I have variable field_name and want to use it for query. I can do it like this:
AExample.joins("INNER JOIN b_examples ON b_examples.#{field_name} = a_examples.#{field_name}")
This query works, but is susceptible for sql injection.
For where clause we have special syntax with ? to avoid sql injection but there isnt any such thing for joins. How can I make this query safe?
Do not attempt this:
(explanation below)
You can use the ActiveRecord::Sanitization module and write something like the following, inside your ActiveRecord model:
AExample.joins("INNER JOIN b_examples ON b_examples.#{sanitize_sql(field_name)} = a_examples.#{sanitize_sql(field_name)}")
Or you can include the module somewhere else and use it there (e.g. your controller).
Do use this, instead: (included from another answer)
AExample.joins("INNER JOIN b_examples ON b_examples.#{ActiveRecord::Base.connection.quote_column_name(field_name)} = a_examples.#{ActiveRecord::Base.connection.quote_column_name(field_name)}")
It will raise an error if the column is not found, preventing malicious code from entering your query.
However I wouldn't do this in my app as it looks suspicious, other programers may not understand what is happening, it may be implemented wrong, solid testing should be included, it may have bugs and such. In your problem, you only need to construct two different queries, with that information I would write something like:
case dynamic_field
when 'field_x'
AExample.joins('INNER JOIN b_examples ON b_examples.field_x = a_examples.field_x')
when 'field_y'
AExample.joins('INNER JOIN b_examples ON b_examples.field_y = a_examples.field_y')
else
raise "Some suspicious parameter was sent!: #{dynamic_field}"
end
Or even write scopes on your model and avoid this code to be flying around.
With problems of this nature, as with encryption, try to find a workaround and avoid implementing your own solutions as much as possible.
EDIT:
The method sanitize_sql is intended to sanitize conditions for a WHERE clause (ActiveRecord::Sanitization):
Accepts an array or string of SQL conditions and sanitizes them into a valid SQL fragment for a WHERE clause.
It is not an option as you try to sanitize for an INNER JOIN, or an ON clause.
Note that the ActiveRecord::Sanitization module only has options for WHERE, SET, ORDER and LIKE clauses. I was unable to find a sanitization method for a column name, an INNER JOIN or an ON clause. Perhaps is a useful funcionality that should be added on Rails on further version.
Using sanitize_sql with a string passes it almost unfiltered, so if the field_name variable has some malicious code as:
"field_x = a_examples.field_x; DROP TABLE a_examples; --"
It will be included in your query, without any error being raised.
This solution is not safe, and is for reasons like these that we should avoid writing code of this nature. Perhaps you find something helpful with Arel or other gems, but I would strongly advice not to.
EDIT 2:
Added the working solution to escape a column name. It raises an error if malicious code is being entered, as the column with that name will not be found.
You can sanitize parameters using ActiveRecord::Base.connection.quote(string) or even .quote_column

How to add attribute/property to each record/object in an array? Rails

I'm not sure if this is just a lacking of the Rails language, or if I am searching all the wrong things here on Stack Overflow, but I cannot find out how to add an attribute to each record in an array.
Here is an example of what I'm trying to do:
#news_stories.each do |individual_news_story|
#user_for_record = User.where(:id => individual_news_story[:user_id]).pluck('name', 'profile_image_url');
individual_news_story.attributes(:author_name) = #user_for_record[0][0]
individual_news_story.attributes(:author_avatar) = #user_for_record[0][1]
end
Any ideas?
If the NewsStory model (or whatever its name is) has a belongs_to relationship to User, then you don't have to do any of this. You can access the attributes of the associated User directly:
#news_stories.each do |news_story|
news_story.user.name # gives you the name of the associated user
news_story.user.profile_image_url # same for the avatar
end
To avoid an N+1 query, you can preload the associated user record for every news story at once by using includes in the NewsStory query:
NewsStory.includes(:user)... # rest of the query
If you do this, you won't need the #user_for_record query — Rails will do the heavy lifting for you, and you could even see a performance improvement, thanks to not issuing a separate pluck query for every single news story in the collection.
If you need to have those extra attributes there regardless:
You can select them as extra attributes in your NewsStory query:
NewsStory.
includes(:user).
joins(:user).
select([
NewsStory.arel_table[Arel.star],
User.arel_table[:name].as("author_name"),
User.arel_table[:profile_image_url].as("author_avatar"),
]).
where(...) # rest of the query
It looks like you're trying to cache the name and avatar of the user on the NewsStory model, in which case, what you want is this:
#news_stories.each do |individual_news_story|
user_for_record = User.find(individual_news_story.user_id)
individual_news_story.author_name = user_for_record.name
individual_news_story.author_avatar = user_for_record.profile_image_url
end
A couple of notes.
I've used find instead of where. find returns a single record identified by it's primary key (id); where returns an array of records. There are definitely more efficient ways to do this -- eager-loading, for one -- but since you're just starting out, I think it's more important to learn the basics before you dig into the advanced stuff to make things more performant.
I've gotten rid of the pluck call, because here again, you're just learning and pluck is a performance optimization useful when you're working with large amounts of data, and if that's what you're doing then activerecord has a batch api you should look into.
I've changed #user_for_record to user_for_record. The # denote instance variables in ruby. Instance variables are shared and accessible from any instance method in an instance of a class. In this case, all you need is a local variable.

ActiveRecord query alias field name output

Let's say I have a table World.
I have a field called foo within the table. I want to query the World table and select foo, but I would like to alias it as bar in the subsequent conversion to JSON output.
Is there any way to alias the field name for just this one ActiveRecord query? Not looking to alias the field through the entire application.
Just use the SQL alias feature in a select method call:
w = World.select("foo as bar").first
w.bar # returns value for foo
w.foo # ActiveModel::MissingAttributeError
I avoid writing SQL as much as I can, so I would rather use ARel to build the query. Something like
World.select(World.arel_table['foo'].as('bar'))
Using some syntactic sugar, it's just:
at = World.arel_table
World.select(at['foo'].as('bar'))
You can override to_json method in yours World model. Check details on how to do that here How to override to_json in Rails?

Finding values in Ruby Hashes that contain the ouput of a SQL select resultset

I am Learning ruby. I would like to use active record to use a simple find command like this: MyModel.find_by_emp_id(i.emp_id) on the model but I cant with the Vertica database gem I am using. The resultset of running a straight SQL query on the model like this:
vemployees = conn.query("select * from employees") returns a hash like data structure.
The data structure vemployees is a Vertica::result type, and the structure looks to be like bellow:
[
{:emp_id=>"3321", :emp_last_name=>"Man", :emp_first_name=>"super", :emp_mid_name=>nil},
{:emp_id=>"3325", :emp_last_name=>"Man", :emp_first_name=>"Bat", :emp_mid_name=>nil},
]
How can I execute something like, vemployees.find_by_emp_id(i.emp_id) without going through a list of results?
N.B. untested code
something like:
in MyModel:
def self.find_by_emp_id(id)
conn.query("select * from employees where emp_id = #{id}")[0]
end
Since the gem is not ActiveRecord you'll have to write your own accessors, you cannot rely on the automagically created ActiveRecord find_by_ methods. I think you can hack that gem into ActiveRecord and have all of ActiveRecord's goodness but that's probably more than you'd want to take on as a Ruby beginner.
Be careful to sanitize any user input ...

Ensure my SQL is not injected receiving array of values

I need to receive an array of values of like:
['restaurant']
['restaurant', 'pharmacy']
I would like which approach to take to ensure that when I use this:
SELECT * FROM places WHERE type IN (array_joined_with_commas_and_quotes)
I don't get injection attacks.
I am writing the sentence without any library and I am working in Rails.
I don't have Active Record, I am doing a postgis query to an external server.
How about using the ActiveRecord functions for query building?
If you use rails, the gem should still be there.
Place.where(:type => ['foo', 'bar']).to_sql
You have two basic approaches (using ? for parameterization in these examples).
If this is an array, then your ['restaurant', 'pharmacy'] becomes '{"restaurant","pharmacy"}' and then you can:
SELECT * FROM places WHERE type = ANY (?);
You could also
SELECT * FROM places WHERE type IN (? ....);
Dynamically generating the number of parameters as you go. As scones mentioned ActiveRecord may be able to automate this for you. The point though is that these methods send data separately from the query, which means that the SQL cannot be injected into the query.

Resources