Rails - chain 'where' scopes with OR - ruby-on-rails

I can chain scopes like so:
scoped = User.where(sex: 'F')
scoped = scoped.where(color: 'blue')
ActiveRecord generates this SQL for the 1st line:
SELECT COUNT(*) FROM "users" WHERE "users"."sex" = 'F'
And this SQL for the 2nd line:
SELECT COUNT(*) FROM "users" WHERE "users"."sex" = 'F' AND "users"."color" = 'blue'
While keeping the initial query so it's built over 2 lines how can I change it from AND to OR so the end result looks like:
SELECT COUNT(*) FROM "users" WHERE "users"."sex" = 'F' OR "users"."color" = 'blue'

Without extensions, this is currently impossible with Rails unless you use raw SQL:
User.where('sex = ? OR color = ?', 'F', 'blue')
In Rails 5, these types of ORs will be supported out of the box.

If you use the squeel gem, you can implement this like so:
User.where(
{ sex: 'F' } |
{ color: 'blue' }
)
However, since it sounds like you need to build the query dynamically, I'd recommend doing something like this:
queries = [
"sex = 'F'",
"color = 'blue'"
]
User.where(queries.join(" OR "))
With this method, you can dynamically add as many queries as you need to the queries array. Obviously, you'll have to write the queries in raw SQL but this is the only option I'm aware of for accomplishing this with ActiveRecord.
UPDATE
To answer #Thomas question in the comments:
I'm wondering if there is a way to utilize join data with this. Like
user.sex instead of sex.
queries = [
"users.sex = 'F'",
"color = 'blue'"
]
Car.joins(:user).where(queries.join(" OR "))

Related

Ruby/Rails - Chain unknown number of method calls

I would like to dynamically create (potentially complex) Active Record queries from a 2D array passed into a method as an argument. In other words, I'd like to take this:
arr = [
['join', :comments],
['where', :author => 'Bob']
]
And create the equivalent of this:
Articles.join(:comments).where(:author => 'Bob')
One way to do this is:
Articles.send(*arr[0]).send(*arr[1])
But what if arr contains 3 nested arrays, or 4, or 5? A very unrefined way would be to do this:
case arr.length
when 1
Articles.send(*arr[0])
when 2
Articles.send(*arr[0]).send(*arr[1])
when 3
Articles.send(*arr[0]).send(*arr[1]).send(*arr[2])
# etc.
end
But is there a cleaner, more succinct way (without having to hit the database multiple times)? Perhaps some way to construct a chain of method calls before executing them?
One convenient way would be to use a hash instead of a 2D array.
Something like this
query = {
join: [:comments],
where: {:author => 'Bob'}
}
This approach is not much complex and You don't need to worry if the key is not provided or is empty
Article.joins(query[:join]).where(query[:where])
#=> "SELECT `articles`.* FROM `articles` INNER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` WHERE `articles`.`author` = 'Bob'"
If the keys are empty or not present at all
query = {
join: []
}
Article.joins(query[:join]).where(query[:where])
#=> "SELECT `articles`.* FROM `articles`"
Or nested
query = {
join: [:comments],
where: {:author => 'Bob', comments: {author: 'Joe'}}
}
#=> "SELECT `articles`.* FROM `articles` INNER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` WHERE `articles`.`author` = 'Bob' AND `comments`.`author` = 'Joe'"
I created following query which will work on any model and associated chained query array.
def chain_queries_on(klass, arr)
arr.inject(klass) do |relation, query|
begin
relation.send(query[0], *query[1..-1])
rescue
break;
end
end
end
I tested in local for following test,
arr = [['where', {id: [1,2]}], ['where', {first_name: 'Shobiz'}]]
chain_queries_on(Article, arr)
Query fired is like below to return proper output,
Article Load (0.9ms) SELECT `article`.* FROM `article` WHERE `article`.`id` IN (1, 2) AND `article`.`first_name` = 'Shobiz' ORDER BY created_at desc
Note-1: few noticeable cases
for empty arr, it will return class we passed as first argument in method.
It will return nil in case of error. Error can occur if we use pluck which will return array (output which is not chain-able) or if we do not pass class as first parameter etc.
More modification can be done for improvement in above & avoid edge cases.
Note-2: improvements
You can define this method as a class method for Object class also with one argument (i.e. array) and call directly on class like,
# renamed to make concise
Article.chain_queries(arr)
User.chain_queries(arr)
Inside method, use self instead of klass
arr.inject(Articles){|articles, args| articles.send(*args)}

Ruby on Rails SQL Injection - Building a query

I'm resolving all the SQL Injections in a system and I've found something that I don't know how to treat.
Can somebody help me?
Here is my method
def get_structure()
#build query
sql = %(
SELECT pc.id AS "product_id", pc.code AS "code", pc.description AS "description", pc.family AS "family",
p.code AS "father_code", p.description AS "father_description",
p.family AS "father_family"
FROM products pc
LEFT JOIN imported_structures imp ON pc.id = imp.product_id
LEFT JOIN products p ON imp.product_father_id = p.id
WHERE pc.enable = true AND p.enable = true
)
#verify if there is any filter
if !params[:code].blank?
sql = sql + " AND UPPER(pc.code) LIKE '%#{params[:code].upcase}%'"
end
#many other parameters like the one above
#execute query
str = ProductStructure.find_by_sql(sql)
end
Thank you!
You could use Arel which will escape for you, and is the underlying query builder for ActiveRecord/Rails. eg.
products = Arel::Table.new("products")
products2 = Arel::Table.new("products", as: 'p')
imported_structs = Arel::Table.new("imported_structures")
query = products.project(
products[:id].as('product_id'),
products[:code],
products[:description],
products[:family],
products2[:code].as('father_code'),
products2[:description].as('father_description'),
products2[:family].as('father_family')).
join(imported_structs,Arel::Nodes::OuterJoin).
on(imported_structs[:product_id].eq(products[:id])).
join(products2,Arel::Nodes::OuterJoin).
on(products2[:id].eq(imported_structs[:product_father_id])).
where(products[:enable].eq(true).and(products2[:enable].eq(true)))
if !params[:code].blank?
query.where(
Arel::Nodes::NamedFunction.new('UPPER',[products[:code]])
.matches("%#{params[:code].to_s.upcase}%")
)
end
SQL result: (with params[:code] = "' OR 1=1 --test")
SELECT
[products].[id] AS product_id,
[products].[code],
[products].[description],
[products].[family],
[p].[code] AS father_code,
[p].[description] AS father_description,
[p].[family] AS father_family
FROM
[products]
LEFT OUTER JOIN [imported_structures] ON [imported_structures].[product_id] = [products].[id]
LEFT OUTER JOIN [products] [p] ON [p].[id] = [imported_structures].[product_father_id]
WHERE
[products].[enable] = true AND
[p].[enable] = true AND
UPPER([products].[code]) LIKE N'%'' OR 1=1 --test%'
To use
ProductStructure.find_by_sql(query.to_sql)
I prefer Arel, when available, over String queries because:
it supports escaping
it leverages your existing connection adapter for sytnax (so it is portable if you change databases)
it is built in code so statement order does not matter
it is far more dynamic and maintainable
it is natively supported by ActiveRecord
you can build any complex query you can possibly imagine (including complex joins, CTEs, etc.)
it is still very readable
You need to turn that into a placeholder value (?) and add the data as a separate argument. find_by_sql can take an array:
def get_structure
#build query
sql = %(SELECT...)
query = [ sql ]
if !params[:code].blank?
sql << " AND UPPER(pc.code) LIKE ?"
query << "%#{params[:code].upcase}%"
end
str = ProductStructure.find_by_sql(query)
end
Note, use << on String in preference to += when you can as it avoids making a copy.

How to use dynamic array of ids in raw query (rails + postgres)?

I have this code
Language.all.map{|l|[l.language, l.accounts.joins(:children).where("accounts.id IN (?)", #accounts.ids).uniq.count]}
I am trying to get this as output
[["eng", 5] ["span", 3] ["ital", 4] ... ]
I want to write it as a raw query.
I tried this,
ActiveRecord::Base.connection.execute("select languages.language, (SELECT COUNT(*) FROM accounts where accounts.language_id = languages.id) from languages").values
but i need to pass accounts.ids dynamic.. like this
select languages.language, (SELECT COUNT(*) FROM accounts where accounts.language_id = languages.id AND (accounts.id IN (#{#accounts.ids})) from languages"
when i tried to pass accounts.id IN #{#accounts.ids} i am getting error
(accounts.id IN ([2839, 284.. .. this should have been IN (2839, 284..) instead, it is taking array.
How to pass it dynamically ?
You can try:
"... accounts.id IN (#{#accounts.ids.join(',')})"
Hope it helps.
The above 2 answers won't work if you are attempting to use an array of strings in a raw sql query. For a raw sql statement, you can use ActiveRecord's sanitize_sql_array method.
languages = ['eng', 'span', 'chin', 'ital']
base_query = "SELECT * FROM languages WHERE id IN (?)"
sanitized_query = ActiveRecord::Base.send :sanitize_sql_array, [base_query, languages]
ActiveRecord::Base.connection.execute(sanitized_query)
You can use where(id: #accounts.ids) or where("account.ids": #accounts.ids) or where("account.ids IN ?, #accounts.ids). I believe ActiveRecord should understand all of them.
http://guides.rubyonrails.org/active_record_querying.html#array-conditions
http://guides.rubyonrails.org/active_record_querying.html#subset-conditions

ActiveRecord Rails 4.2 - Filter includes query

I'm trying to improve some queries in my Rails 4.2 project. One query in particular, is done in an API Controller. The query is as follows (just as example):
#books = Book.includes(:pages).where( category_id: 1 )
This is producing the following SQL:
SELECT "books".* FROM "books" WHERE "books".category_id = 1
SELECT "pages".* FROM "pages" WHERE "pages"."book_id" IN (1, 2, 3)
This is nice, it's behaving OK in the database side.
Then let's suppose that I need to filter the books based on a page attribute. The query would be something like this:
#books = Book.includes(:pages).where( category_id: 1, pages: { attr1: 'bar' } )
This is producing the following SQL:
SELECT "books".attr1, "books".attr2, "pages".attr1, "pages".attr2
FROM "books"
LEFT OUTER JOIN "pages" ON "books".id = "pages".book_id
WHERE "books".category_id = 1 AND "pages".attr1 = 'bar';
This is not necessarily bad. However I would like to have the following SQL instead:
SELECT "books".* FROM "books" WHERE "books".category_id = 1
SELECT "pages".* FROM "pages" WHERE "pages"."book_id" IN (1, 2, 3) AND "pages".attr1 = 'bar'
You see, it's easier to create indexes for each separate query in the database.
Is there a way that I can achieve what I want?
Thanks.
PS: Please excuse any typo in the written code.

Rails 4 string interpolation in raw SQL request

What would be the best way of rewriting this query without interpolation?
def case_joins(type)
subquery = <<-SQL.squish
SELECT id FROM cases c2
WHERE c2.title_id = titles.id AND c2.value = 0 AND c2.type = '#{type}'
ORDER BY c2.created_at DESC LIMIT 1
SQL
"LEFT OUTER JOIN cases ON cases.title_id = titles.id AND cases.value = 0 AND cases.type = '#{type}' AND cases.id = (#{subquery})"
end
I'm assuming that you want to avoid interpolation of variables, which is dangerous since its open to SQL injection. I would simply join onto the cases selected from the subquery instead of putting the subquery into the WHERE conditions. This does involve interpolation, but only of AR-generated SQL. I would also implement it as a scope to leverage AR scope chaining:
class Title < ActiveRecord::Base
def self.case_joins(type)
case_query = Case.from("cases c").where(c: {title_id: title_id, value: 0, type: type}).order('c.created_at DESC').limit(1)
joins("LEFT OUTER JOIN (#{case_query.to_sql}) cases ON cases.title_id = titles.id")
end
end
This way, you can chain the scope to others like so:
Title.where(attribute1: value1).case_joins("typeA")
(Note that removed the superfluous WHERE conditions in the outer SELECT.)
It's difficult to infer what the rest of your code looks like, but I presume titles is being used in a query further up your call stack.
If you were to use ActiveRecord instead of native SQL, you could do something like this:
def case_joins(scope, title_id, type)
ids = Case.where(title_id: title_id, value: 0, type: type)
.order('created_at desc').limit(1).pluck(:id)
scope.joins('left outer join cases on cases.title_id = titles.id')
.where(value: 0, type: type, id: ids)
end
scope here is the current AR query you are modifying.
This is off the top of my head, so I'm not sure if the AR syntax above is correct, but it does avoid the need to interpolate SQL and also uses scoping.
To be honest, though, it's not all that much more readable than native SQL, and so YMMV. It does at least mean that (apart from the join) you're not encoding SQL in your code.
Here is modification of #eirikir's answer, that works the same way as method in question.
def case_joins(type)
case_query = Case.from("cases c").where('c.title_id = titles.id AND c.value = 0 AND c.type = ?', type).order('c.created_at DESC').select(:id).limit(1)
"LEFT OUTER JOIN cases ON cases.title_id = titles.id AND cases.id = (#{case_query.to_sql})"
end

Resources