Rails SQL query on unknown (dynamic) number of queries using LIKE - mysql

I have a Rails search form that performs a dynamic query. That is, the user enters a search like:
(hobby="skiing") OR (gender="male" AND hobby="jogging")
So, I don't know how many queries I will be searching by until runtime.
I parse the search query by converting it into a valid SQL query, so the above search would be converted to the following format:
query = "hobby LIKE ? OR (gender LIKE ? AND hobby LIKE ?)"
queries = ["skiing", "male", "jogging"]
For the following query:
where(query, queries)
However, the general syntax of the Rails search query is very limiting:
where(query, query_1, query_2, query_3)
I cannot replace the 'query_n' arguments with an array like I want to, without Rails throwing an error.
NoMethodError (undefined method `where' for ["a", "b", "c"]:Array)
Attempting to splat the array yields the same error:
where(query, *queries)
Again:
NoMethodError (undefined method `where' for ["a", "b", "c"]:Array)
So what can I do?
EDIT:
The full search function looks like this:
def self.search(search)
query = "%#{search}%"
if search
includes(:games, :jobs)
strngs = ["hobby = ? OR name = ? OR gender = ?", "dsfgdsfg", "dsgsdfgsd", "sdfsfsdf"]
.where(strngs)

What you'll want to do is pass an array as a single argument to where which contains both the query AND the dynamic values. For example:
where(["att_1 LIKE ? OR att_2 LIKE ?", "value1", "value2"])
If an array is passed as the first and only argument, then the first element of the array is treated as a template. The following array values are treated as the dynamic values for the query template.
For your example, instead of having two separate variables queries and query, combine them into one query variable:
# A single array with the query AND values
query = ["hobby LIKE ? OR (gender LIKE ? AND hobby LIKE ?)", "skiing", "male", "jogging"]
# Run the `where` with a single array as the argument
YourModel.where(query)
This will allow you to query the DB with an unknown number of values using LIKE.
For reference: Rails where() docs

Related

Creating GORM dynamic query with optional paramters

I've been stuck on a GORM issue for about a full day now. I need to be able to filter a messages table on any of 4 things: sender, recipient, keyword, and date range. It also has to paginate. Filtering by sender and recipient is working, and so is pagination. So far this is the query that I have come up with, but it does not seem to work for date ranges or keywords.
Here is how I am selecting from MySQL
db.Preload("Thread").Where(query).Scopes(Paginate(r)).Find(&threadMessages)
I am creating the query like this:
var query map[string]interface{}
Then based on which parameters I am passed, I update the query like this by adding new key values to the map:
query = map[string]interface{}{"user_id": sender, "recipient_id": recipient}
For dates it does not seem to work if I try something like this:
query = map[string]interface{}{"created_at > ?": fromDate}
And for a LIKE condition is also does not seem to work:
query = map[string]interface{}{"contents LIKE ?": keyword}
The reason I chose this approach is that I could not seem to get optional inputs to work in .Where since it takes a string with positional parameters and null positional parameters seem to cause MySQL to return an empty array. Has anyone else dealt with a complicated GORM issue like this? Any help is appreciated at this point.
Passing the map[string]interface{} into Where() only appears to work for Equals operations, or IN operations (if a slice is provided as the value instead).
One way to achieve what you want, is to construct a slice of clause.Expression, and append clauses to the slice when you need to. Then, you can simply pass in all of the clauses (using the ... operator to pass in the whole slice) into db.Clauses().
clauses := make([]clause.Expression, 0)
if mustFilterCreatedAt {
clauses = append(clauses, clause.Gt{Column: "created_at", fromDate})
}
if mustFilterContents {
clauses = append(clauses, clause.Like{Column: "contents", Value: keyword})
}
db.Preload("Thread").Clauses(clauses...).Scopes(Paginate(r)).Find(&threadMessages)
Note: If you're trying to search for content that contains keyword, then you should concatenate the wildcard % onto the ends of keyword, otherwise LIKE behaves essentially the same as =:
clause.Like{Column: "contents", Value: "%" + keyword + "%"}
My final solution to this was to create dynamic Where clauses based on which query params were sent from the client like this:
fields := []string{""}
values := []interface{}{}
If, for example, there is a keyword param:
fields = []string{"thread_messages.contents LIKE ?"}
values = []interface{}{"%" + keyword + "%"}
And to use the dynamic clauses in the below query:
db.Preload("Thread", "agency_id = ?", agencyID).Preload("Thread.ThreadUsers", "agency_id = ?", agencyID).Joins("JOIN threads on thread_messages.thread_id = threads.id").Where("threads.agency_id = ?", agencyID).Where(strings.Join(fields, " AND "), values...).Scopes(PaginateMessages(r)).Find(&threadMessages)

Get SQL array from ActiveRecord::Relation

I'm building up a query like this:
scope = User.select(:name).where("name = ?", 'test')
In another part of my code, I'm trying to convert scope, which is an ActiveRecord::Relation object into a SQL array like ["SELECT name FROM users WHERE name = ?", 'test']. Is there any way to accomplish this? Thanks in advance.
It doesn't appear to be possible to get the array you want back from an ActiveRecord::Relation object.
Say that we're just using where. When we call User.where("name = ?", "test") we enter the where method in ActiveRecord::QueryMethods. This calls where! which calls build_where. Since we passed in the query as a String we end up here:
# ActiveRecord::QueryMethods#build_where
when String, Array
[#klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
sanitize_sql combines the SQL query with the values and the result is stored in where_values:
> User.where("name = ?", "test").where_values
=> ["name = 'test'"]
ActiveRecord::Relation only holds onto this combined version, not the query and values separately.

EF6 Contains() works with only interned Strings

I want to filter my entity by name property. I use Contains() method. When I pass parameter as "E" it works but if I pass as variable it doesn't work.
Code is below :
if (!String.IsNullOrWhiteSpace(searchingModel.LanguageNameContains))
query = query.Where(n => n.Name.Contains(searchingModel.LanguageNameContains));
if (!String.IsNullOrWhiteSpace(searchingModel.LanguageCodeContains))
query = query.Where(n => n.Code.Contains(searchingModel.LanguageCodeContains));
return query;
Above example doesn't work. But if I write like this
if (!String.IsNullOrWhiteSpace(searchingModel.LanguageNameContains))
query = query.Where(n => n.Name.Contains("E"));
if (!String.IsNullOrWhiteSpace(searchingModel.LanguageCodeContains))
query = query.Where(n => n.Code.Contains("En"));
return query;
It works. I have debugged lots of times. I passed same parameters as variable and as constants. If I pass as constant it works but If I pass as variable it doesn't work. Curiously enough, The same code works MySql.Data.Entity 6.9.3 but now I am using 6.9.5 and it doesn't work. Is it a bug or my mistake ?
Generated SQL :
-when I pass parameter as variable:
SELECT
Extent1.Id,
Extent1.Code,
Extent1.Name
FROM Languages AS Extent1
WHERE Extent1.Name LIKE '%p__linq__0%'
-- p__linq__0: 'E' (Type = String, Size = 1)
-- Executing at 30.12.2014 22:30:40 +02:00
-- Completed in 0 ms with result: EFMySqlDataReader
when I pass as interned string :
SELECT
Extent1.Id,
Extent1.Code,
Extent1.Name
FROM Languages AS Extent1
WHERE Extent1.Name LIKE '%E%'
Second one returns rows but first one doesn't return any rows.

How to get Ruby MySQL returning PHP like DB SELECT result

So I use the PDO for a DB connection like this:
$this->dsn[$key] = array('mysql:host=' . $creds['SRVR'] . ';dbname=' . $db, $creds['USER'], $creds['PWD']);
$this->db[$key] = new PDO($this->dsn[$key]);
Using PDO I can then execute a MySQL SELECT using something like this:
$sql = "SELECT * FROM table WHERE id = ?";
$st = $db->prepare($sql);
$st->execute($id);
$result = $st->fetchAll();
The $result variable will then return an array of arrays where each row is given a incremental key - the first row having the array key 0. And then that data will have an array the DB data like this:
$result (array(2)
[0]=>[0=>1, "id"=>1, 1=>"stuff", "field1"=>"stuff", 2=>"more stuff", "field2"=>"more stuff" ...],
[1]=>[0=>2, "id"=>2, 1=>"yet more stuff", "field1"=>"yet more stuff", 2=>"even more stuff", "field2"=>"even more stuff"]);
In this example the DB table's field names would be id, field1 and field2. And the result allows you to spin through the array of data rows and then access the data using either a index (0, 1, 2) or the field name ("id", "field1", "field2"). Most of the time I prefer to access the data via the field names but access via both means is useful.
So I'm learning the ruby-mysql gem right now and I can retrieve the data from the DB. However, I cannot get the field names. I could probably extract it from the SQL statement given but that requires a fair bit of coding for error trapping and only works so long as I'm not using SELECT * FROM ... as my SELECT statement.
So I'm using a table full of State names and their abbreviations for my testing. When I use "SELECT State, Abbr FROM states" with the following code
st = #db.prepare(sql)
if empty(where)
st.execute()
else
st.execute(where)
end
rows = []
while row = st.fetch do
rows << row
end
st.close
return rows
I get a result like this:
[["Alabama", "AL"], ["Alaska", "AK"], ...]
And I'm wanting a result like this:
[[0=>"Alabama", "State"=>"Alabama", 1=>"AL", "Abbr"=>"AL"], ...]
I'm guessing I don't have the way inspect would display it quite right but I'm hoping you get the idea by now.
Anyway to do this? I've seen some reference to doing this type of thing but it appears to require the DBI module. I guess that isn't the end of the world but is that the only way? Or can I do it with ruby-mysql alone?
I've been digging into all the methods I can find without success. Hopefully you guys can help.
Thanks
Gabe
You can do this yourself without too much effort:
expanded_rows = rows.map do |r|
{ 0 => r[0], 'State' => r[0], 1 => r[1], 'Abbr' => r[1] }
end
Or a more general approach that you could wrap up in a method:
columns = ['State', 'Abbr']
expanded_rows = rows.map do |r|
0.upto(names.length - 1).each_with_object({}) do |i, h|
h[names[i]] = h[i] = r[i]
end
end
So you could collect up the rows as you are now and then pump that array of arrays through something like what's above and you should get the sort of data structure you're looking for out the other side.
There are other methods on the row you get from st.fetch as well:
http://rubydoc.info/gems/mysql/2.8.1/Mysql/Result
But you'll have to experiment a little to see what exactly they return as the documentation is, um, a little thin.
You should be able to get the column names out of row or st:
http://rubydoc.info/gems/mysql/2.8.1/Mysql/Stmt
but again, you'll have to experiment to figure out the API. Sorry, I don't have anything set up to play around with the MySQL API that you're using so I can't be more specific.
I realize that php programmers are all cowboys who think using a db layer is cheating, but you should really consider activerecord.

Different type of data when comparing SQLite vs MySQL query result in Rails

I'm looking for an explanation of the result of the following query with Active Record:
date_range = (Date.today - 7)..(Time.now.to_datetime)
r = Report.find(:all, :conditions => {:created_at => date_range},
:group => 'date(created_at)',
:select => 'date(created_at) as day, count(id) as counter')
Basically I'm just counting the results in a table "reports," but I got different types of values for the named field "counter", array of double quote strings vs array of only numbers.
MySQL
If using MySQL,
r.map(&:counter)
returns:
=> ["3", "3", "5", "4", "4"]
SQLite
If using SQLite,
r.map(&:counter)
returns:
=> [3, 3, 5, 4, 4]
Is it correct that MySQL returns the numbers with quotes (Strings) and SQLite numbers?
I expected that both return just integers. Or am I missing some configuration with on MySQL side?
Edit:
Just in case, both DBs where created using normal migrations, so all fields are equivalent types.
This is a bug in the SQLite ActiveRecord adapter.
When you are synthesizing new column values using "SELECT expr AS col_name" col_name is cast to a String instead of the proper data type (int in this case).
See related Rails bug ticket: https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/4544-rails3-activerecord-sqlite3-lost-column-type-when-using-views#ticket-4544-2
Apparently SQLite can't return proper column type values for views, which is the mechanism used when you're synthesizing attributes like this.
I've been having the same issue using the underlying connection to execute my SQL statements.
I have noticed, however, that if you are using binds in exec_sql statement, the data types come back as expected.
E.g.
ActiveRecord::Base.connection.exec_sql("select * from users where id = 35")
results in
ActiveRecord::Result:0x7fa2c6ede200 #columns=["id", "name"], #hash_rows=nil, #rows=[["35", "FOO"]]
However,
ActiveRecord::Base.connection.exec_sql("select * from users where id = ?", nil, [[nil,35]])
results in
ActiveRecord::Result:0x7fa2c6ede200 #columns=["id", "name"],
#hash_rows=nil, #rows=[[35, "FOO"]]
Note that datatype of 35 is integer!
Hope that helps.