Active Record provides unscoped to remove all the scopes added to a model previously.

class Article
  default_scope { where(published: true) }
end

Article.all # SELECT * FROM articles WHERE published = true

Article.unscoped # SELECT * FROM articles

Let's say I want to fetch all articles of an author, published or otherwise.

author = Author.find_by(email: "[email protected]")
author.articles.unscoped

I am expecting all articles to be returned for given author. But there is a surprise, instead of getting articles of a specific author, I get back all articles in the database!

author.articles.unscoped 
# SELECT  "articles".* FROM "articles"

How did this happen?

When unscoped is called on a model, it calls unscoped and returns a scope of that model without any previous scopes.

But we called unscoped on an association. Let's see where the unscoped method is defined on an association.

[5] pry(main)> author.articles.method(:unscoped)
=> #<Method: Article::ActiveRecord_Associations_CollectionProxy#unscoped>
[6] pry(main)> author.articles.method(:unscoped).source_location
=> ["/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/relation/delegation.rb",
 96]

Turns out, we don't have a unscoped method defined on the association object. Instead it just delegates to the unscoped on the model of the association through various intermediate objects.

The full stack trace of these intermediate calls is as follows:

 "/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/scoping/default.rb:34:in `unscoped'",
 "/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/associations/association_scope.rb:24:in `scope'",
 "/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/associations/association_scope.rb:7:in `scope'",
 "/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/associations/association.rb:90:in `association_scope'",
 "/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/associations/association.rb:79:in `scope'",
 "/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/associations/collection_association.rb:287:in `scope'",
 "/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/associations/collection_proxy.rb:932:in `scope'",
 "/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/associations/collection_proxy.rb:1104:in `scoping'",
 "/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/relation/delegation.rb:114:in `method_missing'",
 
 "/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/scoping/default.rb:34:in `unscoped'",
 "/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/relation/delegation.rb:114:in `public_send'",
 "/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/relation/delegation.rb:114:in `block in method_missing'",
 "/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/relation.rb:281:in `scoping'",
 "/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/associations/collection_proxy.rb:1104:in `scoping'",
 "/Users/prathamesh/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-5.2.3/lib/active_record/relation/delegation.rb:114:in `method_missing'",

When we are calling unscoped on an association, it is actually getting translated to calling unscoped on the model itself. So author.articles.unscoped gets translated to Article.unscoped. That's why we get all the articles without the author constraint back.

This behavior stumped me as I was expecting it to "just work" on associations as well.

If we want to unscoped articles of an author, we need to fetch them via articles, not via author.

Article.unscoped.where(author: author)
# SELECT  "articles".* FROM "articles" WHERE "articles"."author_id" = ?

So next time you are using unscoped in your code, make sure you are not calling accidentally on an association.

Pro Tip

Ruby has a secret weapon to debug any program. It is a method named method. You can call it on any object and pass the method name to get the method object. Once the method object is caught, we can ask its source location. I used it to debug the code in this blog post.