Query multiple key values with Rails + Postgres hstore - ruby-on-rails

I am trying to make a query to search for values in my hstore column properties. I am filtering issues by user input by attribute. It is possible to search Issues where email is X, or Issues where email is X and the sender is "someone". Soon I need to change to search using LIKE for similar results. So if you know how to do it with LIKE also, show both options please.
If I do this:
Issue.where("properties #> ('email => pugozufil#yahoo.com') AND properties #> ('email => pugozufil#yahoo.com')")
it returns a issue.
If I do this:
Issue.where("properties #> ('email => pugozufil#yahoo.com') AND properties #> ('sender => someone')")
Here I got an error, telling me:
ERROR: Syntax error near 'd' at position 11
I change the "#>" to "->" and now this error is displayed:
PG::DatatypeMismatch: ERROR: argument of AND must be type boolean, not type text
I need to know how to query the properties with more than one key/value pair, with "OR" or "AND", doesn't matter.
I wish to get one or more results that include those values I am looking for.

I end up doing like this. Using the array option of the method where. Also using the suggestion from #anusha in the comments. IDK why the downvote though, I couldn't find anything on how to do something simple like this. I had doubt in formatting my query and mostly with hstore. So I hope it helps someone in the future as sure it did for me now.
if params[:filter].present?
filters = params[:filter]
conditions = ["properties -> "]
query_values = []
filter_query = ""
filters.each do |k, v|
if filters[k].present?
filter_query += "'#{k}' LIKE ?"
filter_query += " OR "
query_values << "%#{v}%"
end
end
filter_query = filter_query[0...-(" OR ".size)] # remove the last ' OR '
conditions[0] += filter_query
conditions = conditions + query_values
#issues = #issues.where(conditions)
end

Related

rails dynamic where sql query

I have an object with a bunch of attributes that represent searchable model attributes, and I would like to dynamically create an sql query using only the attributes that are set. I created the method below, but I believe it is susceptible to sql injection attacks. I did some research and read over the rails active record query interface guide, but it seems like the where condition always needs a statically defined string as the first parameter. I also tried to find a way to sanitize the sql string produced by my method, but it doesn't seem like there is a good way to do that either.
How can I do this better? Should I use a where condition or just somehow sanitize this sql string? Thanks.
def query_string
to_return = ""
self.instance_values.symbolize_keys.each do |attr_name, attr_value|
if defined?(attr_value) and !attr_value.blank?
to_return << "#{attr_name} LIKE '%#{attr_value}%' and "
end
end
to_return.chomp(" and ")
end
Your approach is a little off as you're trying to solve the wrong problem. You're trying to build a string to hand to ActiveRecord so that it can build a query when you should simply be trying to build a query.
When you say something like:
Model.where('a and b')
that's the same as saying:
Model.where('a').where('b')
and you can say:
Model.where('c like ?', pattern)
instead of:
Model.where("c like '#{pattern}'")
Combining those two ideas with your self.instance_values you could get something like:
def query
self.instance_values.select { |_, v| v.present? }.inject(YourModel) do |q, (name, value)|
q.where("#{name} like ?", "%#{value}%")
end
end
or even:
def query
empties = ->(_, v) { v.blank? }
add_to_query = ->(q, (n, v)) { q.where("#{n} like ?", "%#{v}%") }
instance_values.reject(&empties)
.inject(YourModel, &add_to_query)
end
Those assume that you've properly whitelisted all your instance variables. If you haven't then you should.

Sending array of values to a sql query in ruby?

I'm struggling on what seems to be a ruby semantics issue. I'm writing a method that takes a variable number of params from a form and creates a Postgresql query.
def self.search(params)
counter = 0
query = ""
params.each do |key,value|
if key =~ /^field[0-9]+$/
query << "name LIKE ? OR "
counter += 1
end
end
query = query[0..-4] #remove extra OR and spacing from last
params_list = []
(1..counter).each do |i|
field = ""
field << '"%#{params[:field'
field << i.to_s
field << ']}%", '
params_list << field
end
last_item = params_list[-1]
last_item = last_item[0..-3] #remove trailing comma and spacing
params_list[-1] = last_item
if params
joins(:ingredients).where(query, params_list)
else
all
end
end
Even though params_list is an array of values that match in number to the "name LIKE ?" parts in query, I'm getting an error: wrong number of bind variables (1 for 2) in: name LIKE ? OR name LIKE ? I tried with params_list as a string and that didn't work any better either.
I'm pretty new to ruby.
I had this working for 2 params with the following code, but want to allow the user to submit up to 5 ( :field1, :field2, :field3 ...)
def self.search(params)
if params
joins(:ingredients).where(['name LIKE ? OR name LIKE ?',
"%#{params[:field1]}%", "%#{params[:field2]}%"]).group(:id)
else
all
end
end
Could someone shed some light on how I should really be programming this?
PostgreSQL supports standard SQL arrays and the standard any op (...) syntax:
9.23.3. ANY/SOME (array)
expression operator ANY (array expression)
expression operator SOME (array expression)
The right-hand side is a parenthesized expression, which must yield an array value. The left-hand expression is evaluated and compared to each element of the array using the given operator, which must yield a Boolean result. The result of ANY is "true" if any true result is obtained. The result is "false" if no true result is found (including the case where the array has zero elements).
That means that you can build SQL like this:
where name ilike any (array['%Richard%', '%Feynman%'])
That's nice and succinct so how do we get Rails to build this? That's actually pretty easy:
Model.where('name ilike any (array[?])', names.map { |s| "%#{s}%" })
No manual quoting needed, ActiveRecord will convert the array to a properly quoted/escaped list when it fills the ? placeholder in.
Now you just have to build the names array. Something simple like this should do:
fields = params.keys.select { |k| k.to_s =~ /\Afield\d+\z/ }
names = params.values_at(*fields).select(&:present)
You could also convert single 'a b' inputs into 'a', 'b' by tossing a split and flatten into the mix:
names = params.values_at(*fields)
.select(&:present)
.map(&:split)
.flatten
You can achieve this easily:
def self.search(string)
terms = string.split(' ') # split the string on each space
conditions = terms.map{ |term| "name ILIKE #{sanitize("'%#{term}%'")}" }.join(' OR ')
return self.where(conditions)
end
This should be flexible: whatever the number of terms in your string, it should returns object matching at least 1 of the terms.
Explanation:
The condition is using "ILIKE", not "LIKE":
"ILIKE" is case-insensitive
"LIKE" is case-sensitive.
The purpose of the sanitize("'%#{term}%'") part is the following:
sanitize() will prevent from SQL injections, such as putting '; DROP TABLE users;' as the input to search.
Usage:
User.search('Michael Mich Mickey')
# can return
<User: Michael>
<User: Juan-Michael>
<User: Jean michel>
<User: MickeyMouse>

dynamic query for different values rails 4 activerecord

Here is the query I am trying in my controller
query = []
if id
query = "category_id: #{id}"
end
#posts = Post.where(query)
But throwing error as ERROR: syntax error at or near ":"
Why this is not working any other way to do it
if id
query << {sub_category_id: id}
end
if test
query << {test_id: test}
end
#posts = Post.where(query)
Is there any way of doing like this
Change query to a hash instead of string:
if id
query = { category_id: id }
end
#posts = Post.where(query)
The reason query = "category_id: #{id}" did not work is because the supplied string is literally used in the query generated by ActiveRecord, i.e. your select query will have category_id: 1 (assuming id is 1) in the where clause. And this is not a valid SQL syntax.
Please read on how you can use strings in conditions following this link. Thanks to #RustyToms for suggesting the link.
Update: ( Add extra conditions to the query hash )
if id
query[:sub_category_id] = id
end
if test
query[:test_id] = test
end
#posts = Post.where(query)
Another way to do this:
#posts = Post.scoped
#posts = #posts.where(category_id: id) if id
(in case you're playing codegolf)
Edit: (this is definitely a side note that isn't at all relevant)
Your original solution relies on one of my least favorite features of Ruby. Consider the following code:
if false
a = 4
end
puts a
I would expect the puts a to fail with a NameError (undefined local variable "a"), but no! The Ruby parser hits a = and then initalizes its value to nil. So, despite the fact that there is no way for the innards of that if statement to run, it still impacts the other code.

Rails syntax error assigning a variable with each loop

I'm trying to create a list of recipients to send in an external request by assigning it to a variable by doing the following:
recipients = #items.each do |item|
{"email"=>"#{Recipient.find_by_id(item.recip_id).email}", "amount"=>"#{item.price}"},
end
but I'm getting this error:
syntax error, unexpected ',', expecting '}'
I know that what I've done is not the right syntax. I'm kind of a Ruby newbie, so can someone help me figure out the correct syntax here?
EDIT: Thanks for the input. But what if I need to do two hashes for each item?
recipients = #items.map do |item|
{"email"=>"#{Recipient.find_by_id(item.recip_id).email}", "amount"=>"#{item.price}"},
{"email"=>"#{Store.find_by_id(item.recip_id).email}", "amount"=>"#{item.price}"}
end
The problem is with the comma at the end of the hash. Also if you want to store the email and amount in recipients, you should use map. This will return an array of hash with email and amount:
recipients = #items.map do |item|
{"email"=> Recipient.find_by_id(item.recip_id).email, "amount"=> item.price}
end
Also, as you might note, I don't need to pass the values of email and prices as a string.
If you want to return multiple hashes from your map block then you'd be better off switching to each_with_object:
Iterates the given block for each element with an arbitrary object given, and returns the initially given object.
So something like this:
recipients = #items.each_with_object([]) do |item, a|
a << {"email"=>"#{Recipient.find_by_id(item.recip_id).email}", "amount"=>"#{item.price}"}
a << {"email"=>"#{Store.find_by_id(item.recip_id).email}", "amount"=>"#{item.price}"}
end

Is it possible to have variable find conditions for both the key and value?

I'm trying to pass in both the field and the value in a find call:
#employee = Employee.find(:all,
:conditions => [ '? = ?', params[:key], params[:value].to_i)
The output is
SELECT * FROM `employees` WHERE ('is_manager' = 1)
Which returns no results, however when I try this directly in mysqsl using the same call without the '' around is_manager, it works fine. How do I convert my params[:key] value to a symbol so that the resulting SQL call looks like:
SELECT * FROM `employees` WHERE (is_manager = 1)
Thanks,
D
If you want to convert a string to symbol(which is what params[:key] produces, all you need to do is
params[:key].to_s.to_sym
2 points:
A word of caution : symbols are
not garbage collected.
Make sure your key is not a
number, if you convert to_s first
then to_sym, your code will work but
you may get a wierd symbol like
this:
:"5"
You could use variable substitution for column name instead of using bind values:
# make sure the key passed is a valid column
if Employee.columns_hash[params[:key]]
Employee.all :conditions => [ "#{params[:key]} = ?", params[:value]]
end
You can further secure the solution by ensuring column name passed belongs to a pre selected set.:
if ["first_name", "last_name"].include? [params[:key]]
Employee.all :conditions => [ "#{params[:key]} = ?", params[:value]]
end
"string".to_sym

Resources