Squirrel - Natural Looking Queries for Rails

Jon Yurek

I’ve never liked how you query the database in ActiveRecord. Sure it works, but so does writing straight SQL. Neither seem very integrated into the framework. So after thinking about it, I figured I’d do it one better and make something a little more Rubyish looking. So I made Squirrel.

posts = Post.find do
  user.email =~ "%thoughtbot%"
  tags.name === %w( ruby rails )
  created_on <=> [ 2.weeks.ago, 1.week.ago ]

I’ve seen various plugins for making queries nicer, but none looked like Ruby, because even in the best of them, they still used what I consider to be overly-awkward nested hashes to get relationships. Squirrel automatically gets the relationships and builds them as you use them. It supports all the normal associations, and it does it in a much friendlier way.

You reference associations by whatever name you gave to it in the has\_many, belongs\_to, etc. You can then access all the columns and relationships on that model. It is rather specific, though, and will raise errors if you misspell your relationships or don’t pluralize right.

And you simply reference columns by their normal names and use any of ==, ===, &lt;=&rt;, =~, &rt;=, &lt;=, &rt;, and &lt; on them pretty much like you’d expect to be able to (before you ask, yes, I cribbed the syntax from ez-where, since it makes sense). It handles nil values in == with a quick trip to IS NULL and it handles negation of conditions through the unary -.

It doesn’t yet do all the fancy stuff I’d like it to, like adding aggregation columns, limiting, or even OR joins and grouping, but it’s still much better looking than normal ActiveRecord::Base#find queries, I think. It does have some fancy stuff like order_by and placeholders, though. Here’s an example with both:

query = User.find do
  company.name = cname?
  order_by created_on

When you do that, instead of an array of results, it hands you back Squirrel’s Query object, which is the base of all its querying (imagine that). From there, you call find on it and pass a hash of the names of the placeholders you specified. No surprises there. (Also, you can get a query object for inspecting the SQL by passing :query to find, like so: query = User.find(:query) {id == 2})

users = query.find(:cname => "thoughtbot")

Now, what’s tricky is that you could pass any syntax-changing value to that and you’d get the right SQL in the executed query.

query.find :cname => "thoughtbot"
# =>   company.name = "thoughtbot"
query.find :cname => nil
# =>   company.name IS NULL
query.find :cname => ["Google", "37 Signals"]
# =>   company.name IN ("Google", "37 Signals")

So there you have it. I think it makes queries much more readable and easier to maintain.

You can obtain it with:

piston init https://svn.thoughtbot.com/plugins/squirrel/trunk vendor/plugins/squirrel

(Updated to reflect correct URL.)

Visit our Open Source page to learn more about our team’s contributions.