Finding the opposite of what you have with rails invert_where

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:

old_claims = Claim.joins(:order)
                    .where.not(orders: { status: "completed" })
                    .where("claims.created_at < ?", 10.days.ago)
                    .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.