Rails 3 scopes with chaining

Here’s a quick gotcha you should be aware of when using the fantastic new scope facility in Rails 3 (formerly named_scope).

Here’s a snippet that demonstrates how to chain scopes, allowing you to call Post.recent to get posts that have been published, ordered by date.

class Post < ActiveRecord::Base
  scope :published, lambda { where('published_at IS NOT NULL AND published_at <= ?', Time.zone.now) }
  scope :recent, published.order(:published_at)
end

However it contains a subtle bug; when chaining a lambda scope you must also wrap it with a lambda or else you will end up with the wrong result. The correct example is as follows.

class Post < ActiveRecord::Base
  scope :published, lambda { where('published_at IS NOT NULL AND published_at <= ?', Time.zone.now) }
  scope :recent, lambda { published.order(:published_at) }
end

José Valim explains the reason in the Rails ticket #4960.

class Auction < ActiveRecord::Base
  scope :started, lambda { where("starting_at <= ?", Time.now) }
  scope :unfinished, lambda { where("ending_at > ?", Time.now) }
  scope :active, started.unfinished
end

You can chain scopes, but they will be evaluated at the moment you call them.

That said, when you call started, it will execute the lambda, so it will have a frozen Time.now. In other words, chaining lambda scopes will likely give you the wrong result.

You need to wrap the scope :active in a lambda as well.

scope :active, lambda { started.unfinished }


About this entry