Rails scope searching with wild cards - ruby-on-rails

I would like to add the ability to use wildcards to my search. I'm using rails 3 with SQLite3. My search is built into the index action of my controller using a scope defined in the model.
scope :by_drawing_number, lambda { |drawing_number| where('drawing_number LIKE ?', "#{drawing_number}") unless drawing_number.nil? }
I would like to modify this so if the user enters a '?' it is replaced by a '_' for a single character wildcards, and a '*' is replaced by a '%' for multiple character wildcards.
Is there a way to incorporate these substitutions in my scope or will I need to rewrite the scope as a method? If so, what should the method look like?

You can put any logic you want inside a scope. scope is basically just syntactic sugar for defining a class method. Something like this ought to work:
scope :by_drawing_number, lambda {|drawing_number|
break if drawing_number.nil?
match = drawing_number.tr('?*', '_%')
where('drawing_number LIKE ?', match)
}

Related

activerecord not like query

I could not find an activerecord equivalent of "Not Like". I was able to find a where.not, but that will check if a string does not match a value, as so:
User.where.not(name: 'Gabe')
is the same as:
User.where('name != ?', 'Gabe')
I was looking for a NOT LIKE, where the value is not contained in the string. The equivalent sql query would look like as follows:
SELECT * FROM users WHERE name NOT LIKE '%Gabe%'
In ActiveRecord I can currently get away with the following:
User.where("name NOT LIKE ?", "%Gabe%")
But that leaves a lot to be desired. Any new additions to Rails 4 to facilitate this?
Well, you can do something like:
User.where.not("name LIKE ?", "%Gabe%")
Note: This is only available in Rails 4.
As others have pointed out ActiveRecord does not have a nice syntax for building like statements. I would suggest using Arel as it makes the query less database platform specific (will use ilike for sqlite & like for other platforms).
User.where(User.arel_table[:name].does_not_match('%Gabe%'))
You could also implement this as a scope to contain the implementation to the model:
class User < ActiveRecord::Base
scope :not_matching,
-> (str) { where(arel_table[:name].does_not_match("%#{str}%")) }
end
Unfortunately ActiveRecord does not have a like query builder. I agree that the raw 'NOT LIKE' leaves a lot to be desired; you could make it a scope (scope :not_like, (column, val) -> { ... }), but AR itself does not do this.
Just addition to the answer of "where.not" of active record. "where.not" will exclude null values also. i.e. Query User.where.not(name: 'Gabe') will leave record with name 'Gabe' but it also exclude name column with NULL values. So in this scenario the solution would be
User.where.not(name: 'Gabe')
.or(User.where(name: nil))

Make search NOT case sensitive on my rails app

Using this gem: http://filterrific.clearcove.ca/ I have successfully setup search on my app. Here is the scope that is used.
scope :search_by_name, ->(name){ where(name: name) }
Lets say there is a name of 'Jonathon' and you search for 'jon' I would like for it to bring that result up. Also, as it stands now if you search for 'jonathon' (change J to j) it doesn't show the result. You have to have it exactly as the entry.
To stay portable between databases, consider changing your scope to a named method and use AREL matches.
def search_by_name(name)
where(arel_table[:name].matches(name))
end
Though maybe a scope could work:
scope :search_by_name, ->(name){ where(arel_table[:name].matches(name)) }
Since a named scope is the same as a method, especially when you pass it a lambda variable, there is no disadvantage of it being a method instead of a scope.
You may need to prepend or append a % (or both) to the name argument for it to look for anything containing the argument (instead of only exact case-insensitive matches. So perhaps...
def search_by_name(name)
where(arel_table[:name].matches('%' + name + '%'))
end

How to avoid SQL Injection with Rails #order method

I haven't been able to find any resources online about prevent SQL injections when using #order. There's no trouble using ?-placeholders for the where-clause, but it doesn't seem to work for the order-clause.
Here's an example:
query = Foo.where("ST_DISTANCE(coords, ?) < ?", point, distance)
# The line below works:
.order("ST_DISTANCE(coords, ST_GeomFromText('#{point}'))")
# This line doesn't work:
.order("ST_DISTANCE(coords, ST_GeomFromText(?))", point)
Just to be clear: the line that doesn't work returns a PGError which logs ST_DISTANCE(coords, ST_GeomFromText(?)) literally.
Is this a known issue?
Are you trying to pass something like POINT(-71.064544 42.28787) in GET/POST params? I saw example here http://www.postgis.org/docs/ST_GeomFromText.html
I think better to
order("ST_DISTANCE(coords, ST_GeomFromText('POINT(%f %f))" % [lat, lon])
% is shorthand for Kernel::sprintf
The QueryMethod order calls preprocess_order_args which expects it will just be given a list of fields and optionally directions.
One option would be to call sanitize_sql which can be done from within an ActiveRecord class method:
# Inside Foo class
def self.order_by_distance(point)
order(sanitize_sql(["ST_DISTANCE(coords, ST_GeomFromText(?))", point]))
end

Why is Rails 3 not excluding 'or'-ed condition when overriding scopes?

Consider these scopes on an ActiveRecord model:
scope :onsale, where(:sales_state => "onsale")
scope :outdated, where(:sales_state => "outdated")
scope :onsale_or_outdated, where(" sales_state = 'onsale' OR sales_state = 'outdated' ")
Why is it that Rails knows to override :onsale with :outdated, but not :onsale with :onsale_or_outdated?
My use case:
I have a relation object with many scopes (let's say it's a saved search), and one of those is :onsale. I want to make another relation starting from that, but this time I want the sales_state to be either onsale or outdated.
If I use relation.onsale_or_outdated, the sales_state is not overridden, it just adds a new condition.
[...] WHERE "cars"."sales_state" = 'onsale' [...] AND (("cars"."sales_state" = 'onsale' OR "cars"."sales_state" = 'outdated'))
How can I use my 'or'-ed condition in this context?
If I use relation.onsale_or_outdated, the sales_state is not overridden, it just adds a new condition.
That's how scopes work. They append, not replace. If you have two mutually exclusive scopes, you need to use one or the other, not both. The special case is when you have a scope involves a single field in :symbol = <value> syntax. Rails is smart enough to allow one scope to cancel the other out. In your case, the onsale_or_updated scope is simply a string, Rails has no means to tell which fields are involved and so the scopes are chained.
You should rewrite your scope to use fields/values instead of a blob of SQL, so Rails knows which fields are involved.
scope :onsale_or_outdated, where(:sales_state => %w(onsale outdated))
Alternatively, if you want to use only your onsale_or_outdated scope, you can unscope the relationship and reapply a scope:
relation.unscoped.onsale_or_outdated
Note that this will remove any previously applied scopes.

How to have a non query based scope in Rails

How can I turn a filter on an array of ActiveRecord objects into a scope?
For example turn this:
Users.all.collect { |u| u.has_super_powers? }
Into:
scope :supers, #something here
The point is that it can be used as a scope so Users.all is not always "all" like:
Users.undeleted.supers.find_by_this('that')
I thought maybe using lambda expressions in scopes is the way, but don't think that would work since I don't have access to records as the expression is added to a DB query and not run as a post step over the results.
It depends wether or not you can transform u.has_super_powers? into a database query. Sometimes it is possible, sometimes its not.
For example: If you have a database field has_super_powers (boolean column) in the users table you can create a scope on that:
scope :has_super_powers, where(:has_super_powers => true)
Now you can chain it together with other scopes:
User.undeleted.has_super_powers.find_by_this('that')

Resources