Rails 7 is introducing a new method invert_where that will invert all scope conditions. It allows us to invert an entire where clause instead of manually applying conditions.

We can either chain invert_where to a scope or to a where condition.

class Account
  scope :active, -> { where(active: true) }
end
Account.active.invert_where
=> "SELECT \"accounts\".* FROM \"accounts\" WHERE \"accounts\".\"active\" != 1"
Account.where(active: true).invert_where
=> "SELECT \"accounts\".* FROM \"accounts\" WHERE \"accounts\".\"active\" != 1"

What are the various side effects of using invert_where?

1. The invert_where method inverts all the where clauses present in the query

Let's say we create a scope to fetch inactive accounts using invert_where.

class Account
  scope :active, -> { where(active: true) }
  scope :inactive, -> { active.invert_where }
end

Now, somewhere in the controller, we want to fetch all the inactive admin accounts so we may end up with a query as follows:

Account.where(role: 'admin').inactive

We may think that the above query will return all the inactive admin accounts. Let's look at the SQL equivalent of the above code.

=> "SELECT \"accounts\".* FROM \"accounts\" WHERE NOT (\"accounts\".\"role\" = 'admin' AND \"accounts\".\"active\" = 1)"

But instead, we received all the inactive non-admin accounts. The invert_where also inverted the clause where(role: 'admin') internally.

2. The invert_where affects the default scope as well

Continuing the above example, let us add a default scope in the Account model to filter archived accounts (soft-deleted accounts).

class Account
  default_scope { where(archived: false) }
  scope :active, -> { where(active: true) }
end

Now let us write a query to fetch inactive accounts using invert_where.

Account.active.invert_where

The SQL equivalent of the above will be:

=> "SELECT \"accounts\".* FROM \"accounts\" WHERE NOT (\"accounts\".\"archived\" = 0 AND \"accounts\".\"active\" = 1)"

We can see that invert_where has also inverted the default_scope. We wanted to fetch inactive accounts which are not archived but instead received inactive accounts which are archived.

Using default_scope is quite dangerous in the model in the first place, using invert_where will increase the chances of bugs on top of that.

3. Challenges we will face on chaining two or more scopes that contain invert_where

Let's say we have two scopes that contain invert_where as follows:

class Account
  scope :inactive, -> { active.invert_where }
  scope :non_admin, -> { admin.invert_where }
end

Now we want to fetch the inactive non-admin accounts, so we chain the above two scopes together and write the query as follows:

Account.non_admin.inactive

The SQL equivalent of the above is:

=> "SELECT \"accounts\".* FROM \"accounts\" WHERE NOT (\"accounts\".\"role\" != 'admin' AND \"accounts\".\"active\" = 1)"

We can see that the above query will unexpectedly return all the inactive admin accounts.

Substitute for invert_where

Rails 7 is still in the alpha phase, invert_where 's behavior might be changed once Rails 7 is officially released. For now, we can make use of the NOT condition to invert the individual WHERE clause.

Let us take the example from the first point and create a scope to fetch inactive accounts using the NOT clause.

class Account
  scope :active, -> { where(active: true) }
  scope :inactive, -> { where.not(active: true) }
end

Now if we want to fetch all the inactive admin accounts, we can write the query as follows:

Account.where(role: 'admin').inactive

The SQL equivalent of the above will be:

=> "SELECT \"accounts\".* FROM \"accounts\" WHERE \"accounts\".\"role\" = 'admin' AND \"accounts\".\"active\" != 1"

The above query will return all the inactive admin accounts. This is the expected behavior we wanted and we achieved it using the NOT clause.

Conclusion

One should cautiously use this proposed method invert_where otherwise we may end up with bugs that may be difficult to debug. What do you think? Do you agree or have any other suggestions?

Thank you for reading.


References