ActiveRecord scope returns all records when result of the scope is nil


ActiveRecord scopes are used very oftenly in Rails applications. It adds a class method for retrieving and querying objects.

There is one unusual but expected behaviour of scope. Let’s check it by example.

Suppose we need to find recently published post then scope will look like as below -

class Post < ApplicationRecord
  scope :recent_published, -> { where(published: true).order('published_at DESC').first }
end

And we have only unpublished posts in table. Let’s check in rails console -

2.6.0 :001 > Post.recent_published
  Post Load (0.3ms)  SELECT  "posts".* FROM "posts" WHERE "posts"."published" = ? ORDER BY published_at DESC LIMIT ?  [["published", 1], ["LIMIT", 1]]
  Post Load (0.1ms)  SELECT  "posts".* FROM "posts" LIMIT ?  [["LIMIT", 11]]
 => #<ActiveRecord::Relation [#<Post id: 1, published: false, published_at: "2019-09-14 07:32:24", created_at: "2019-09-14 07:32:24", updated_at: "2019-09-14 07:32:24">, #<Post id: 2, published: false, published_at: "2019-09-14 07:32:27", created_at: "2019-09-14 07:32:27", updated_at: "2019-09-14 07:32:27">]>

Gotchaaaa!!!

So you must be expecting [] but you got some results. Let’s look at the query -

Post Load (0.3ms)  SELECT  "posts".* FROM "posts" WHERE "posts"."published" = ? ORDER BY published_at DESC LIMIT ?  [["published", 1], ["LIMIT", 1]]
Post Load (0.1ms)  SELECT  "posts".* FROM "posts" LIMIT ?  [["LIMIT", 11]]

The first query returns nil and hence second query is executed which loads all data from posts table.

But why is this happening at all?

To find answer let’s look into activerecord code. The comment about scope in ActiveRecord says -

# If it returns +nil+ or +false+, an
# {all}[rdoc-ref:Scoping::Named::ClassMethods#all] scope is returned instead.

This is very important to understand that scope should not return nil or false otherwise it returns all records.

In our case, scope recent_published i.e. Post.where(published: true).order('published_at DESC'].first returns nil and hence second query SELECT "posts".* FROM "posts" executed which load all data.

Let’s try to understand the implementation of scope method. Let’s see the method _exec_scope#L403 -

instance_exec(*args, &block) || self

Below is output of instance_exec(*args, &block) -

[1] pry(#<Post::ActiveRecord_Relation>)> instance_exec(*args, &block)
  Post Load (0.4ms)  SELECT "posts".* FROM "posts" WHERE "posts"."published" = ? ORDER BY published_at DESC LIMIT ?  [["published", 1], ["LIMIT", 1]]
=> nil

This returns nil and hence self is executed which returns all records -

[2] pry(#<Post::ActiveRecord_Relation>)> self
  Post Load (0.2ms)  SELECT "posts".* FROM "posts"

So what is the solution?

If you notice in recent_published scope, we have used first which is actully a problem. This first method is from module ActiveRecord::FinderMethods.

We can use limit instead of first.

scope :recent_published, -> { where(published: true).order('published_at DESC').limit(1) }

How/why limit works and first didn’t?

Short answer

limit returns ActiveRecord::Relation and first returns the first item or nil.

Long answer

Check how ruby works for || condition with nil vs [] -

nil || anything => anything #case of using `first` in scope
[]  || anything => []       #case of using `limit` in scope

This is same implementations of _exec_scope which is instance_exec(*args, &block) || self.

To check further we should see the implementation of first method in active record.

The first method calls find_nth

def find_nth(index)
  @offsets[offset_index + index] ||= find_nth_with_limit(index, 1).first
end

In above find_nth_with_limit(index, 1) which returns empty Array.

And find_nth_with_limit(index, 1).first is same as [].first which returns nil.

In case of limit method, the return object is ActiveRecord::Relation and it is chainable too.

Summary

This is very important to note that the scope is intended to return an ActiveRecord::Relation object which is composable with other scopes. In short scopes should be chainable.

Use class method instead of scope when return object can be nil, false or non #<ActiveRecord::Relation []> like Array, Hash etc.

Reference

https://github.com/rails/rails/issues/21882