Squirrel Updates for Rails 2.1, now with Named Scopes!

Jon Yurek

As a little follow up to the gotchas, I updated Squirrel. Its pagination is compatible with the latest will_paginate now. However, more important is the fact that you can use Squirrel blocks to build scopes:

User.scoped{ name.not =~ "Jon%" }.ordered.find(:all)

It works just like scoped does normally, but it lets you give Squirrel-style blocks. This lets you keep all your nice named_scope methods and it lets you do all the crazy crap that Squirrel makes look so much nicer at the same time.

A Refresher

For those of you who may not have seen Squirrel before, it’s a plugin for ActiveRecord that allows for a more Ruby-ish syntax when specifying queries. It’s especially handy for advanced searches and the like, where lots of joins and conditionals are required. It makes those a lot more easy to read, and makes absolutely sure that it uses the right names to refer to the right columns, which isn’t always as straightforward as you might think on a complex join.

Sadly, we can’t actually use named_scope with a Squirrel block; it’s already using blocks for other things. However, we can do the next best thing, which is creating class methods on our models. Since named_scopes proxy all their methods back to where they were created, everything will work out fine.

Let’s say we had an app that lets users put up postings for a length of time. We can also tag those postings. We want to allow people to search for the postings, so we whip up a Squirrel query like so:

class Post
  named_scope :ordered, :order => "created_on DESC"
  named_scope :limit, lambda{|x| {:limit => x} }
  named_scope :after, lambda{|d| {
    :conditions => ["posts.created_on > ?", d]
  } }

  def self.search(params)
    scoped do
      any do
        title.contains? params[:keyword]
        body.contains?  params[:keyword]

      expired == false unless params[:all] == "1"
      created_on > params[:timeframe].to_i.days.ago

      params[:tag].split(",").each do |tag|
        tags.name == tag.strip

This lets us chain search alongside the limit and ordered scopes (using some params that look like they came in a Rails request):

Post.search({:all => "1",
             :tag => "one, two",
             :keyword => "Something",
             :timeframe => ""}).ordered.limit(5)

This performs the following query:

SELECT DISTINCT "posts".id FROM "posts"
  LEFT OUTER JOIN "tags" ON tags.post_id = posts.id
  WHERE ((posts.created_on > '2008-06-25 15::36'
        AND (posts.title LIKE '%Something%' OR posts.body LIKE '%Something%')
        AND (tags.name = 'one') AND (tags.name = 'two')))
  ORDER BY created_on DESC

Now let’s say those Posts have Comments (and both of those belong_to :user). We can find all Posts that have Comments by their author like so:

def Post.with_author_comments
  scoped { user.id == comments.user.id }

So to find all the Posts in the last week that have a comment by their Author, we can say:


Which gets us this lovely bit of SQL:

... 32 aliases snipped ...
FROM "posts"
  LEFT OUTER JOIN "comments" ON comments.post_id = posts.id
  LEFT OUTER JOIN "users" ON "users".id = "comments".user_id
  LEFT OUTER JOIN "users" users_posts ON "users_posts".id = "posts".user_id
WHERE ((posts.created_at > '2008-06-18 15:35:27')
      AND (((users_posts.id = users.id))))

And we can even find all the Users who have never made a Comment this way:

def User.without_comments
  scoped { comments.id.nil? }

Each of these works exactly like any other namedscope and can be used in any situation where a namedscope could also be used.

Update: After I hit publish on this, I noticed that the Squirrel-style named scopes need to be specified first in the chain or they clobber what came before, but otherwise work fine. Using blocks with scoped not using the named methods also works completely fine. I’ll be looking into why this is and I’ll have an update soon. So, you can use them almost exactly the same as regular named scopes, anyway.

You can get the latest on Squirrel’s github page.

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