Finding the opposite of what you have with rails invert_where

Trésor Bireke

Rails 7 introduced invert_where. This method inverts all scope conditions that it’s applied to, simplifying the process of defining the opposite of a where clause in ActiveRecord queries. It also comes with a few caveats that should be considered before using it.

What is invert_where?

invert_where is a Rails method that can fetch all the data that is excluded by an existing query.

Let’s take this example: finding recent claims that have been raised on completed orders and have an amount of less than £100.

recent_claims = Claim.joins(:order)
                .where(orders: { status: "completed" })
                .where("claims.created_at >= ?", 10.days.ago)
                .where("claims.claim_amount < ?", 100)

If you want to query data opposite to the query condition, you would need to write the opposite query like:

claims = Claim.joins(:order)
old_claims = claims.where.not(orders: { status: "completed" })
                   .or(claims.where("claims.created_at < ?", 10.days.ago))
                   .or(claims.where("claims.claim_amount >= ?", 100))

Using invert_where the opposite condition becomes as simple as

old_claims = recent_claims.invert_where

invert_where Gotcha

Using invert_where in models with default scopes can lead to unintended inversions. For instance:

class Claim < ApplicationRecord
  default_scope { where(archived: false) }
  scope :recent, -> { where("created_at >= ?", 10.days.ago) }
end

# Inverted scope
old_claims = Claim.recent.invert_where

In the above example, invert_where will invert both the recent scope and the default_scope, potentially returning archived claims, which is most likely not the desired result.

Conclusion

Prefer Using Explicit Conditions: Consider explicitly defining the inverse conditions using where.not instead of inverse_where. This approach provides more clarity and control over the query’s behavior.

Isolate Inversions: If you choose to use invert_where, apply it to isolated scopes or conditions to minimize unintended inversions.

Be Cautious with Default Scopes: Avoid using invert_where in models with default scopes, or ensure that the default scope’s inversion aligns with your intended query results.

Unit test Inverted scopes: Since invert_where inverts all conditions in a scope, it’s important to validate that the new scope behaves as expected with tests; that becomes more crucial when you have default scopes or complex queries.

About thoughtbot

We've been helping engineering teams deliver exceptional products for over 20 years. Our designers, developers, and product managers work closely with teams to solve your toughest software challenges through collaborative design and development. Learn more about us.