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

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

Related

how to match all multiple conditions with rails where method

Using active record, I want to perform a lookup that returns a collection of items that have ALL matching id's.
Given that the below example matches on ANY id in the array, I am trying to figure out the syntax so that it will match when ALL of the id's match. (given that in this example there is a many to many relationship).
The array length of the id's is also variable which prohibits chaining .where()
x.where(id: [1,2])
Note: this question got removed before and there are a lot of answers for performing a sql "where in" but this question is about performing a sql "where and"
You can use exec_query and execute your own bound query:
values = [1, 2]
where_condition = values.map.with_index(1) { |_, index| "id = $#{index}" }.join(" AND ")
sql = "SELECT * FROM table WHERE #{ where_condition }"
binds = values.map { |i| ActiveRecord::Relation::QueryAttribute.new(nil, i, ActiveRecord::Type::Integer.new) }
ActiveRecord::Base.connection.exec_query(sql, nil, binds)
I completely agree with #muistooshort's comment
where(id: [1,2]) doesn't make sense unless you're joining to an association table and in that case,..."where in" combined with HAVING [solves your problem].
But for the sake of answering the question and the assumption that id was just and example.
While #SebastianPalma's answer will work it will return an ActiveRecord::Result whereas most of the time the desire is an ActiveRecord::Relation.
We can achieve this by using Arel to build the where clause like so:
(I modified the example to use description rather than id so that it makes more logical sense)
table = MyObject.arel_table
values = ['Jamesla','Example']
where_clause = values.map {|v| table[:description].matches("%{v}%")}.reduce(&:and)
# OR
where_clause = table[:description].matches_all(values.map {|v| "%#{v}%"})
MyObject.where(where_clause)
This will result in the following SQL query:
SELECT
my_objects.*
FROM
my_objects
WHERE
my_objects.description LIKE '%Jamesla%'
AND my_objects.description LIKE '%Example%'

Create a WHERE (columns) IN (values) clause with Arel?

Is there a way to programatically create a where clause in Arel where the columns and values are specified separately?
SELECT users.*
WHERE (country, occupation) IN (('dk', 'nurse'), ('ch', 'doctor'), ...
Say the input is a really long list of pairs that we want to match.
I'm am NOT asking how to generate a WHERE AND OR clause which is really simple to do with ActiveRecord.
So far I just have basic string manipulation:
columns = [:country, :occupation]
pairs = [['dk', 'nurse'], ['ch', 'doctor']]
User.where(
"(#{columns.join(', ')}) IN (#{ pairs.map { '(?, ?)' }.join(', ')})",
*pairs
)
Its not just about the length of the query WHERE (columns) IN (values) will also perform much better on Postgres (and others as well) as it can use an index only scan where OR will cause a bitmap scan.
I'm only looking for answers that can demonstrate generating a WHERE (columns) IN (values) query with Arel. Not anything else.
All the articles I have read about Arel start building of a single column:
arel_table[:foo].eq...
And I have not been able to find any documentation or articles that cover this case.
The trick to this is to build the groupings correctly and then pass them through to the Arel In Node, for example:
columns = [:country, :occupation]
pairs = [['dk', 'nurse'], ['ch', 'doctor']]
User.where(
Arel::Nodes::In.new(
Arel::Nodes::Grouping.new( columns.map { |column| User.arel_table[column] } ),
pairs.map { |pair| Arel::Nodes::Grouping.new(
pair.map { |value| Arel::Nodes.build_quoted(value) }
)}
)
)
The above will generate the following SQL statement (for MySQL):
"SELECT users.* FROM users WHERE (users.country,
users.occupation) IN (('dk', 'nurse'), ('ch', 'doctor'))"
This will still generate long query with 'OR' in between. But I felt this is lil elegant/different approach to achieve what you want.
ut = User.arel_table
columns = [:country, :occupation]
pairs = [['dk', 'nurse'], ['ch', 'doctor']]
where_condition = pairs.map do |pair|
"(#{ut[columns[0]].eq(pair[0]).and(ut[columns[1]].eq(pair[1])).to_sql})"
end.join(' OR ')
User.where(where_condition)
I have tried this different approach at my end. Hope it will work for you.
class User < ActiveRecord::Base
COLUMNS = %i(
country
occupation
)
PAIRS = [['dk', 'nurse'], ['ch', 'doctor']]
scope :with_country_occupation, -> (pairs = PAIRS, columns = COLUMNS) { where(filter_country_occupation(pairs, columns)) }
def self.filter_country_occupation(pairs, columns)
pairs.each_with_index.reduce(nil) do |query, (pair, index)|
column_check = arel_table[columns[0]].eq(pair[0]).and(arel_table[columns[1]].eq(pair[1]))
if query.nil?
column_check
else
query.or(column_check)
end
end.to_sql
end
end
Call this scope User.with_country_occupation let me know if it works for you.
Thanks!
I think we can do this with Array Conditions as mentioned here
# notice the lack of an array as the last argument
Model.where("attribute = ? OR attribute2 = ?", value, value)
Also, as mentioned here we can use an SQL in statement:
Model.where('id IN (?)', [array of values])
Or simply, as kdeisz pointed out (Using Arel to create the SQL query):
Model.where(id: [array of values])
I have not tried myself, but you can try exploring with these examples.
Always happy to help!

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)}

What is the difference between using ? and % when sanitizing fields in ActiveRecord?

I've been puzzled for a while over the difference between using a question mark, e.g.
Foo.find(:all, :conditions => ['bar IN (?)', #dangerous])
and using sprintf style field types, e.g.
Bar.find(:all, :conditions => ['qux IN (%s)', #dangerous])
in sanitizing inputs. Is there any security advantage whatsoever, if you know you're looking for a number - like an ID - and not a string, in using %d over ?, or are you just asking for a Big Nasty Error when a string comes along instead?
Does this change at all with the newer .where syntax in Rails 3 and 4?
%s is intended for strings. The main difference is that %s doesn't add quotes. From ActiveRecord::QueryMethods.where:
Lastly, you can use sprintf-style % escapes in the template. This
works slightly differently than the previous methods; you are
responsible for ensuring that the values in the template are properly
quoted. The values are passed to the connector for quoting, but the
caller is responsible for ensuring they are enclosed in quotes in the
resulting SQL. After quoting, the values are inserted using the same
escapes as the Ruby core method Kernel::sprintf.
Examples:
User.where(["name = ? and email = ?", "Joe", "joe#example.com"])
# SELECT * FROM users WHERE name = 'Joe' AND email = 'joe#example.com';
User.where(["name = '%s' and email = '%s'", "Joe", "joe#example.com"])
# SELECT * FROM users WHERE name = 'Joe' AND email = 'joe#example.com';
Update:
You are passing an array. %s seems to calls .to_s on the argument so this might not works as expected:
User.where("name IN (%s)", ["foo", "bar"])
# SELECT * FROM users WHERE (name IN ([\"foo\", \"bar\"]))
User.where("name IN (?)", ["foo", "bar"])
# SELECT * FROM users WHERE (name IN ('foo','bar'))
For simple queries you can use the hash notation:
User.where(name: ["foo", "bar"])
# SELECT * FROM users WHERE name IN ('foo', 'bar')
As far as I can tell, %s simply inserts into your query whatever #dangerous.to_s happens to be, and you are responsible for it.
For example, if #dangerous is an array of integers, then you will get an SQL error:
#dangerous = [1,2,3]
User.where("id IN (%s)", #dangerous)
will result in the following incorrect syntax:
SELECT `users`.* FROM `users` WHERE (id IN ([1, 2, 3]))
whereas:
User.where("id IN (?)", #dangerous)
produces the correct query:
SELECT `users`.* FROM `users` WHERE (id IN (1,2,3))
Thus, is seems to me that unless you know very, very well what you are doing, you should let the ? operator do its job, ESPECIALLY if you do not trust the content of #dangerous as safe.

ActiveRecord OR query

How do you do an OR query in Rails 3 ActiveRecord. All the examples I find just have AND queries.
Edit: OR method is available since Rails 5. See ActiveRecord::QueryMethods
If you want to use an OR operator on one column's value, you can pass an array to .where and ActiveRecord will use IN(value,other_value):
Model.where(:column => ["value", "other_value"]
outputs:
SELECT `table_name`.* FROM `table_name` WHERE `table_name`.`column` IN ('value', 'other_value')
This should achieve the equivalent of an OR on a single column
in Rails 3, it should be
Model.where("column = ? or other_column = ?", value, other_value)
This also includes raw sql but I dont think there is a way in ActiveRecord to do OR operation. Your question is not a noob question.
Rails 5 added or, so this is easier now in an app with Rails version greater than 5:
Model.where(column: value).or(Model.where(other_column: other_value)
this handles nil values as well
Use ARel
t = Post.arel_table
results = Post.where(
t[:author].eq("Someone").
or(t[:title].matches("%something%"))
)
The resulting SQL:
ree-1.8.7-2010.02 > puts Post.where(t[:author].eq("Someone").or(t[:title].matches("%something%"))).to_sql
SELECT "posts".* FROM "posts" WHERE (("posts"."author" = 'Someone' OR "posts"."title" LIKE '%something%'))
An updated version of Rails/ActiveRecord may support this syntax natively. It would look similar to:
Foo.where(foo: 'bar').or.where(bar: 'bar')
As noted in this pull request https://github.com/rails/rails/pull/9052
For now, simply sticking with the following works great:
Foo.where('foo= ? OR bar= ?', 'bar', 'bar')
Update: According to https://github.com/rails/rails/pull/16052 the or feature will be available in Rails 5
Update: Feature has been merged to Rails 5 branch
Rails has recently added this into ActiveRecord. It looks to be released in Rails 5. Committed to master already:
https://github.com/rails/rails/commit/9e42cf019f2417473e7dcbfcb885709fa2709f89
Post.where(column: 'something').or(Post.where(other: 'else'))
# => SELECT * FROM posts WHERE (column = 'something') OR (other = 'else)
Rails 5 comes with an or method. (link to documentation)
This method accepts an ActiveRecord::Relation object. eg:
User.where(first_name: 'James').or(User.where(last_name: 'Scott'))
If you want to use arrays as arguments, the following code works in Rails 4:
query = Order.where(uuid: uuids, id: ids)
Order.where(query.where_values.map(&:to_sql).join(" OR "))
#=> Order Load (0.7ms) SELECT "orders".* FROM "orders" WHERE ("orders"."uuid" IN ('5459eed8350e1b472bfee48375034103', '21313213jkads', '43ujrefdk2384us') OR "orders"."id" IN (2, 3, 4))
More information: OR queries with arrays as arguments in Rails 4.
The MetaWhere plugin is completely amazing.
Easily mix OR's and AND's, join conditions on any association, and even specify OUTER JOIN's!
Post.where({sharing_level: Post::Sharing[:everyone]} | ({sharing_level: Post::Sharing[:friends]} & {user: {followers: current_user} }).joins(:user.outer => :followers.outer}
Just add an OR in the conditions
Model.find(:all, :conditions => ["column = ? OR other_column = ?",value, other_value])
With rails + arel, a more clear way:
# Table name: messages
#
# sender_id: integer
# recipient_id: integer
# content: text
class Message < ActiveRecord::Base
scope :by_participant, ->(user_id) do
left = arel_table[:sender_id].eq(user_id)
right = arel_table[:recipient_id].eq(user_id)
where(Arel::Nodes::Or.new(left, right))
end
end
Produces:
$ Message.by_participant(User.first.id).to_sql
=> SELECT `messages`.*
FROM `messages`
WHERE `messages`.`sender_id` = 1
OR `messages`.`recipient_id` = 1
You could do it like:
Person.where("name = ? OR age = ?", 'Pearl', 24)
or more elegant, install rails_or gem and do it like:
Person.where(:name => 'Pearl').or(:age => 24)
I just extracted this plugin from client work that lets you combine scopes with .or., ex. Post.published.or.authored_by(current_user). Squeel (newer implementation of MetaSearch) is also great, but doesn't let you OR scopes, so query logic can get a bit redundant.
I'd like to add this is a solution to search multiple attributes of an ActiveRecord. Since
.where(A: param[:A], B: param[:B])
will search for A and B.
Using the activerecord_any_of gem, you can write
Book.where.any_of(Book.where(:author => 'Poe'), Book.where(:author => 'Hemingway')

Resources