24

I want to match a url field against a url prefix (which may contain percent signs), e.g. .where("url LIKE ?", "#{some_url}%"). What's the most Rails way?

3
  • I hope you don't have any plans to scale to millions of posts. That query will suck system resources faster than an SUV drains gas out of a tank. Commented Apr 18, 2011 at 23:42
  • @Wes: Depends on your database. AFAIK, recent versions of PostgreSQL can utilize an index for LIKE matches that use a prefix (i.e. those of the form X% for some fixed X). There are some notes on this over here: stackoverflow.com/questions/1566717/… Commented Apr 18, 2011 at 23:50
  • @Wes: You're still hosed with %X% patterns though, that'll almost certainly hand you a table scan. Sorry that I don't have an authoritative reference but you could probably grok a query plan and see what happens. Commented Apr 19, 2011 at 1:48

4 Answers 4

28

From Rails version 4.2.x there is an active record method called sanitize_sql_like. So, you can do in your model a search scope like:

scope :search, -> search { where('"accounts"."name" LIKE ?', "#{sanitize_sql_like(search)}%") }

and call the scope like:

Account.search('Test_%')

The resulting escaped sql string is:

SELECT "accounts".* FROM "accounts" WHERE ("accounts"."name" LIKE 'Test\_\%%')

Read more here: http://edgeapi.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html

Sign up to request clarification or add additional context in comments.

5 Comments

In my opinion, this is the most elegant way to solve this issue
Guess from rails 5.* sanitize_sql_like is not supported Any other alternative?
It is supported on Rails 5.x. See here github.com/rails/rails/blob/v5.2.1.1/activerecord/lib/…
FYI these are protected/private methods on 5.0 and 5.1. They're only public on 5.2.
@Teoulas is right. Rails 5.0 and 5.1, use this to call them: ActiveRecord::Base.send(:sanitize_sql_like, "Test_%")
22

If I understand correctly, you're worried about "%" appearing inside some_url and rightly so; you should also be worried about embedded underscores ("_") too, they're the LIKE version of "." in a regex. I don't think there is any Rails-specific way of doing this so you're left with gsub:

.where('url like ?', some_url.gsub('%', '\\\\\%').gsub('_', '\\\\\_') + '%')

There's no need for string interpolation here either. You need to double the backslashes to escape their meaning from the database's string parser so that the LIKE parser will see simple "\%" and know to ignore the escaped percent sign.

You should check your logs to make sure the two backslashes get through. I'm getting confusing results from checking things in irb, using five (!) gets the right output but I don't see the sense in it; if anyone does see the sense in five of them, an explanatory comment would be appreciated.

UPDATE: Jason King has kindly offered a simplification for the nightmare of escaped escape characters. This lets you specify a temporary escape character so you can do things like this:

.where("url LIKE ? ESCAPE '!'", some_url.gsub(/[!%_]/) { |x| '!' + x })

I've also switched to the block form of gsub to make it a bit less nasty.

This is standard SQL92 syntax, so will work in any DB that supports that, including PostgreSQL, MySQL and SQLite.

Embedding one language inside another is always a bit of a nightmarish kludge and there's not that much you can do about it. There will always be ugly little bits that you just have to grin and bear.

9 Comments

Btw, if (I don't know) you need to get double-backslashes into those strings, the above won't do it. `'\\' => ` in Ruby (and most languages).
@Jason: Five escapes produces the right thing in irb, thanks for catching that. I'm guessing that it goes through as three tokens "\\", "\\", and "\%" which, after escape processing, ends up as "\\%" as desired. I usually just keep adding them until it works then reverse engineer the justification. Does anyone know of a quote operator that really does produce literal results without any processing or big pile of mess?
Something else you could do is to add an ESCAPE clause, like this: .where( "url LIKE ? ESCAPE '!'", some_url.gsub('%', '!%').gsub('_', '!_')
@Jason: That's genius. I wish I could give you points for a comment or just transfer ownership of the answer to you.
You could also use capturing groups instead of the block syntax (shorter and probably faster): .where("url LIKE ? ESCAPE '!'", some_url.gsub(/([!%_])/, '!\1')
|
6

https://gist.github.com/3656283

With this code,

Item.where(Item.arel_table[:name].matches("%sample!%code%"))

correctly escapes % between "sample" and "code", and matches "AAAsample%codeBBB" but does not for "AAAsampleBBBcodeCCC" on MySQL, PostgreSQL and SQLite3 at least.

Comments

-3
Post.where('url like ?', "%#{some_url + '%'}%)

10 Comments

@Sam Very close. Post.where('url like ?', "%#{some_url}%")
You code just makes a like condition match other entries. @Costa-Shaprio is looking for a way to do what your doing plus having % or at least that is how I'm reading it ie 'which may contain percent signs'
@Rein: No. That changes the nature of the match ("begins with" becomes "anywhere in") and does nothing to account for an embedded "%" in some_url let alone underscores.
@mu Did you check to see if ActiveRecord's sql sanitization already escapes %s before you commented?
@Rein: How would AR tell the difference between an intentional %x%x% and an accidental "%#{a}%" when a just happens to be an unescaped 'x%x'? The first (intentional) case wants "anything x anything x anything" whereas the second would want "anything x%x anything" but get "anything x anything x anything" because no one escaped it. By the time AR sees the argument for LIKE, it just has a string that has embedded SQL syntax and it is too late to infer the programmer's intent.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.