Rails 7 adds invert_where method to ActiveRecord


We often come across cases in our Rails application where we want to negate our where clause conditions.

Let’s say we have a system that requires users to verify both their email and phone number. We add two columns email_verified and phone_verified to our User model for verifying the account.

A user is verified only if both the email and phone of the user are verified by the system.

Before

Before Rails 7, we would add verified and unverified scopes to our User model to get the details of verified and unverified users in our system.

class User < ApplicationRecord
  scope :verified, -> { where(email_verified: true, phone_verified: true) }
  scope :unverified, -> { where.not(email_verified: true, phone_verified: true) }

  scope :with_verified_email, -> { where(email_verified: true) }
  scope :with_unverified_email, -> { where.not(email_verified: true) }
end

User.verified
# SELECT "users".* FROM "users" WHERE "users"."email_verified" = $1 AND "users"."phone_verified" = $2 /* loading for inspect */ LIMIT $3  [["email_verified", true], ["phone_verified", true], ["LIMIT", 11]]

User.unverified
# SELECT "users".* FROM "users" WHERE NOT ("users"."email_verified" = $1 AND "users"."phone_verified" = $2) /* loading for inspect */ LIMIT $3  [["email_verified", true], ["phone_verified", true], ["LIMIT", 11]]

User.with_verified_email
# SELECT "users".* FROM "users" WHERE "users"."email_verified" = $1 /* loading for inspect */ LIMIT $2  [["email_verified", true], ["LIMIT", 11]]

User.with_unverified_email
# SELECT "users".* FROM "users" WHERE "users"."email_verified" != $1 /* loading for inspect */ LIMIT $2  [["email_verified", true], ["LIMIT", 11]]

As seen above, we have a verified scope that checks if email_verified and phone_verified columns are true. But to fetch unverified users we had to introduce another scope with the where.not clause. This exposes the where.not method which can be kept internal to Rails code.

Similarly, we have added separate scopes for users with verified and unverified emails.

After

Rails 7 adds #invert_where method to ActiveRecord that will invert all scope conditions.

Instead of creating unverified and with_unverified_email scopes with negating conditions, we can just chain invert_where to verified and with_verified_email scopes respectively as shown below.

class User < ApplicationRecord
  scope :verified, -> { where(email_verified: true, phone_verified: true) }

  scope :with_verified_email, -> { where(email_verified: true) }
end

User.verified
# SELECT "users".* FROM "users" WHERE "users"."email_verified" = $1 AND "users"."phone_verified" = $2 /* loading for inspect */ LIMIT $3  [["email_verified", true], ["phone_verified", true], ["LIMIT", 11]]

User.verified.invert_where
# SELECT "users".* FROM "users" WHERE NOT ("users"."email_verified" = $1 AND "users"."phone_verified" = $2) /* loading for inspect */ LIMIT $3  [["email_verified", true], ["phone_verified", true], ["LIMIT", 11]]

User.with_verified_email
# SELECT "users".* FROM "users" WHERE "users"."email_verified" = $1 /* loading for inspect */ LIMIT $2  [["email_verified", true], ["LIMIT", 11]]

User.with_verified_email.invert_where
# SELECT "users".* FROM "users" WHERE "users"."email_verified" != $1 /* loading for inspect */ LIMIT $2  [["email_verified", true], ["LIMIT", 11]]