Multiple LIKE and AND operators in RAILS/POSTGRESQL - ruby-on-rails

I'm using Rails 5.0.1 and Postgresql as my database. I have a table with column :content which contains words.
The problem: When I'm looking for a specific word, I want to see if the word contains letters (chars) of my choice. Let's say i want to DB to return words containg letters "a", "b" and "c" (all of them, but with no specific order)
What I'm doing: I found that i could use
Word.where("content like ?", "%a%").where("content like ?", "%b%").where("content like ?", "%c%")
OR
Word.where("content like ? content like ? content like ?", "%a%", "%b%", "%c%")
In both cases even if i switch order of given letters/substrings it works fine, ex. both would find word "back", "cab" etc..
The question: Is there any better/more DRY way to do it? What if want to find word with 8 different letters? Do i have to use "content like ?" 8 times? Is it possible to pass arguments as an array? (let's assume i don't know how many letters user will input)

PostgreSQL has a handy expr op all (array) expression so you can say things like:
where content like all (array['%a%', '%b%', '%c'])
as a short form of:
where content like '%a%'
and content like '%b%'
and content like '%c%'
Also, ActiveRecord will conveniently replace a ? placeholder with a comma-delimited list if you hand it a Ruby array. That lets you say things like:
Word.where('content like all (array[?])', %w[a b c].map { |c| "%#{c}%" })
and:
Word.where('content like all (array[?])', some_other_array.map { |c| "%#{c}%" })

I found a solution:
letters = ["%a%", "%b%", "%c%"]
Word.where((['content LIKE ?'] * letters.size).join(' AND '), *letters)
This is easy and much better than I was using.

I think the SIMILAR TO operator might help. It allows you to pass in a regular expression that you could construct on the fly.
letters = ['a', 'b', 'c']
pattern = "%(#{letters.join('|')})%"
Word.where("content SIMILAR TO ?", pattern)

Related

convert my string to comma based elements

I am working on a legacy Rails project that relies on Ruby version 1.8
I have a string looks like this:
my_str = "a,b,c"
I would like to convert it to
value_list = "('a','b','c')"
so that I can directly use it in my SQL statement like:
"SELECT * from my_table WHERE value IN #{value_list}"
I tried:
my_str.split(",")
but it returns "abc" :(
How to convert it to what I need?
To split the string you can just do
my_str.split(",")
=> ["a", "b", "c"]
The easiest way to use that in a query, is using where as follows:
Post.where(value: my_str.split(","))
This will just work as expected. But, I understand you want to be able to build the SQL-string yourself, so then you need to do something like
quoted_values_str = my_str.split(",").map{|x| "'#{x}'"}.join(",")
=> "'a','b','c'"
sql = ""SELECT * from my_table WHERE value IN (#{quoted_values_str})"
Note that this is a naive approach: normally you should also escape quotes if they should be contained inside your strings, and makes you vulnerable for sql injection. Using where will handle all those edge cases correctly for you.
Under no circumstances should you reinvent the wheel for this. Rails has built-in methods for constructing SQL strings, and you should use them. In this case, you want sanitize_sql_for_assignment (aliased to sanitize_sql):
my_str = "a,b,c"
conditions = sanitize_sql(["value IN (?)", my_str.split(",")])
# => value IN ('a','b','c')
query = "SELECT * from my_table WHERE #{conditions}"
This will give you the result you want while also protecting you from SQL injection attacks (and other errors related to badly formed SQL).
The correct usage may depend what version of Rails you're using, but this method exists as far back as Rails 2.0 so it will definitely work even with a legacy app; just consult the docs for the version of Rails you're using.
value_list = "('#{my_str.split(",").join("','")}')"
But this is a very bad way to query. You better use:
Model.where(value: my_str.split(","))
The string can be manipulated directly; there is no need to convert it to an array, modify the array then join the elements.
str = "a,b,c"
"(%s)" % str.gsub(/([^,]+)/, "'\\1'")
#=> "('a','b','c')"
The regular expression reads, "match one or more characters other than commas and save to capture group 1. \\1 retrieves the contents of capture group 1 in the formation of gsub's replacement string.
couple of use cases:
def full_name
[last_name, first_name].join(' ')
end
or
def address_line
[address[:country], address[:city], address[:street], address[:zip]].join(', ')
end

Can I find all values LIKE ["_bc","a_c","ab_"]

Just usuing SQLlite3 can I find the values that are LIKE what I am looking for and pass in an array?
e.g. I'd like something of the sort
Url.where("token LIKE ?" ["_bc","a_b","ab_"])
No. You'll have to repeat the LIKE clauses for each of the condition, something like this:
like_conditions = %w(_bc a_b ab_)
Url.where((["token LIKE ?"] * like_conditions).join(" OR "), *like_conditions)
# => SELECT `urls`.* FROM `urls` WHERE (token like '_bc' OR token like 'a_b' OR token like 'ab_')
The first part of the where clause constructs an array of token LIKE ? strings with the same length as the number of like conditions and joins them using OR (change this to ANDs if you need an ANDed query instead).
The last part just passes the conditions but not as an array but as the individual elements instead (that's what the * does) because the query now has three parameters instead of one.
You could try somehting like this (untested for SQLite):
class Url < ActiveRecord::Base
scope :with_tokens, lambda{|*tokens|
query = tokens.length.times.map{"token LIKE ?"}.join(" OR ")
where(query, *tokens)
}
end
Url.with_tokens("_bc", "a_b", "ab_")

Using a ternary statement in Rails 4 in a query using group_by

I ran into something interesting that I think would be related to operator precedence, but not sure if I'm just leaving something out. I would like to use a ternary statement in my .group_by sort on a DB query in Rails. So I have something like this that works:
#tools = Tool.all.group_by {|tool| tool.name}
#=> #tools {'anvil' => [<#tool....
which returns a hash tool objects, grouped into keys where the name is the same. It was then brought up that to just sort them into alphabetical groups by first letter of the name would be the desired output so:
#tools = Tool.all.group_by {|tool| tool.name.downcase[0] }
#=> #tools {'a' => [<#tool.....
So great, now I have a hash of the tools grouped by the first letter of their name. But what if a name starts with a number of something else? Not a problem, it really just pulls the first character and uses that for the group, so tool names starting with "1" get sorted into the hash member whose key is "1". Same for any non-number characters that aren't letters.
Here's the question: I can use a conditional statement to choose to sort all of my alphabetical names into letter groups, but put everything else into a single group with some key like "#". But I can't do it with a ternary statement:
#tools = Tool.all.group_by {|tool| if ('a'..'z').include? tool.name.downcase[0] then tool.name.downcase[0] else '#' end }
works great! I get all of my non-letter names sorted into the #tools['#'] part of the hash.
But this does not work:
#tools = Tool.all.group_by {|tool| ('a'..'z').include tool.name.downcase[0] ? tool.name.downcase[0] : '#' }
It returns a hash with only two members: #tools[true] and #tools[false]. I can kind of see why, as a ternary operator is returning true or false, but shouldn't it act like the if-then-else statement? It has to be something with the group_by that is jumping the gun?
Is there some way to tweak the syntax of the group_by statement to make the ternary operator work like I want it to? I have tried enclosing the two return statements in parens () but that didn't seem to work. I tried the entire ternary statement in parens hoping it would eval the whole thing before returning to the group_by function... any ideas?
This is being parsed by ruby as
('a'..'z').include?(tool.name.downcase[0] ? tool.name.downcase[0] : tool.name = '#')
which is the same as ('a'..'z').include?(tool.name.downcase[0]), assuming none of the names are empty?. For it to be equivalent to your previous version you'd need
('a'..'z').include?(tool.name.downcase[0]) ? tool.name.downcase[0] : tool.name = '#'
As an aside, actually changing the name with tool.name='#' sounds like a really bad idea to me. It might not matter here but could easily bite you later on.

Help with rails active record querying (like clause)

I want my code to do two things that is currently not doing
#students = Student.where(["first_name = ? OR middle_name = ? OR last_name = ?", params[:query].split])
Work. (it says im supposed to pass 4 parameters but I want the user to be able to type words and find by those words on each of those fields and return whatever matches)
Actually use Like clause instead of rigid equal clause.
Please Help.
This looks like a problem that would be better suited to using search rather than SQL. Have you considered something like thinking sphinx or act as ferret (solr would probably be overkill).
...if you must do this in sql, you could build a query something like this:
cols = ['first_name', 'last_name', 'middle_name']
query = 'John Smith'
sql_query = cols.map{|c| query.split.map{|q| "#{c} like '?'"}}.join(' OR ')
sql_query_array = query.split * cols.count
Student.where(sql_query, sql_query_array)
I agree with the previous advice that if you need to do search you should look at something like Solr or Sphinx.
Anyhow, this should help you out.
def search
query = params[:query].split.map {|term| "%#{term}%" }
sql = "first_name LIKE ? OR middle_name LIKE ? OR last_name LIKE ?"
#students = Student.where([sql, *query])
end
The answer to step 1 is using Ruby's awesome little feature called the "splat operator" which allows you to take an array and evaluate it as a list of arguments.
The answer to step 2 is to just massage the query string you get back from the params and turn it into something you can use with the LIKE operator. I basically stole this from Railscasts Simple Search Form episode.
I'm not sure if you want all fields to search the same term if that is the case then you can do this:
where("first_name LIKE :term OR middle_name LIKE :term OR last_name LIKE :term", { term: "%#{params[:term]}%"})
No need for any crazy split or map or anything else, this is just straight ActiveRecord

CONCAT_WS for Rails?

No matter what language I'm using I always need to display a list of strings separated by some delimiter.
Let's say, I have a collection of products and need to display its names separated by ', '.
So I have a collection of Products, where each one has a 'name' attribute. I'm looking for some Rails method/helper (if it doesn't exist, maybe you can give me ideas to build it in a rails way) that will receive a collection, an attribute/method that will be called on each collection item and a string for the separator.
But I want something that does not include the separator at the end, because I will end with "Notebook, Computer, Keyboard, Mouse, " that 2 last characters should not be there.
Ex:
concat_ws(#products, :title, ", ")
#displays: Notebook, Computer, Keyboard, Mouse
Supposing #products has 4 products with that names of course.
Thanks!
you should try the helper to_sentence.
If you have an array, you can do something like
array.to_sentence. If your array has the data banana, apple, chocolate it will become:
banana, apple and chocolate.
So now if you have your AR Model with a field named, you could do something like
MyModel.all.map { |r| r.name }.to_sentence
#products.map(&:title).join(', ')
As #VP mentioned, Array#to_sentence does this job well in rails. The code for it is here:
https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/array/conversions.rb
Saying that, its use of the Oxford Comma is questionable :-)

Resources