NOTE: This article based on today's latest commit of the main branch.
TL;DR
-
invert_where
inverts allwhere
conditions. - It may invert unexpected conditions, so it's dangerous.
What is invert_where
?
ActiveRecord::QueryMethods::WhereChain#invert_where
will be introduced since Rails 7. It inverts where
conditions.
For example: (From the CHANGELOG
class User scope :active, -> { where(accepted: true, locked: false) } end User.active # ... WHERE `accepted` = 1 AND `locked` = 0 User.active.invert_where # ... WHERE NOT (`accepted` = 1 AND `locked` = 0)
It is implemented in #40249.
Why invert_where
is dangerous
The risk becomes clear with several where
. For example:
puts Post.where(a: 'a').where(b: 'b').invert_where.to_sql
In this case, invert_where
inverts both of the where
conditions. So it prints the following results.
SELECT "posts".* FROM "posts" WHERE NOT ("posts"."a" = 'a' AND "posts"."b" = 'b')
As you can see, the whole of the conditions is inverted with NOT
.
This behavior will be bugs with the following three examples.
The examples work with the following set-up code.
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem "activerecord", github: 'rails/rails', ref: '11a3348c2d58f448b8f1dea4f4dbb8cd5bb95a0e'
gem "activemodel", github: 'rails/rails', ref: '11a3348c2d58f448b8f1dea4f4dbb8cd5bb95a0e'
gem "activesupport", github: 'rails/rails', ref: '11a3348c2d58f448b8f1dea4f4dbb8cd5bb95a0e'
gem "sqlite3"
end
require "active_record"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Schema.define do
create_table :users, force: true do |t|
t.string :role
t.datetime :disabled_at
end
end
1. Using invert_where
in a scope definition
If a scope definition includes invert_where
, the invert_where
affects outside of the scope.
For example:
class User < ActiveRecord::Base
scope :alive, -> { where(disabled_at: nil) }
scope :disabled, -> { alive.invert_where }
scope :admin, -> { where(role: 'admin') }
end
puts User.admin.disabled.to_sql
# => SELECT "users".* FROM "users" WHERE NOT ("users"."role" = 'admin' AND "users"."disabled_at" IS NULL)
In this case, User.admin.disabled.to_sql
expects returning disabled admin users. But actually, it returns not-admin users and disabled users. It means .admin
scope is inverted unexpectedly.
2. When the relation starts in a different place with invert_where
call
It is similar to the first problem. In this case, invert_whre
is not hidden in a scope definition, but the relation and invert_where
are in different places.
This problem is well described in a comment in the pull request, which implements invert_where
, so I quote the comment.
Imagine, somewhere within the request flow, perhaps automatically scoped by Pundit,
current_user = User.first posts = Post.where(author: current_user)
And then later on in a controller…
published_posts = posts.where(published: true) draft_posts = published_posts.invert_where
The draft_posts variable is now all draft posts NOT by the current user. Putting the above logic in published and unpublished scopes, respectively, would be make this issue even more confusing to debug if it did occur.
3. With default_scope
Combination of invert_where
and default_scope
is surprising.
class User < ActiveRecord::Base
default_scope -> { where(disabled_at: nil) }
scope :admin, -> { where(role: 'admin') }
end
puts User.admin.invert_where.to_sql
# => SELECT "users".* FROM "users" WHERE NOT ("users"."disabled_at" IS NULL AND "users"."role" = 'admin')
In this case, default_scope
is used to ignore disabled users always. And User.admin.invert_where
expects returning "available non-admin users".
But actually it displays an unexpected SQL. The query means "disabled, or not-admin users". Because invert_where
also inverts default_scope
.
I think default_scope
is a bad practice. The combination with invert_where
makes it worse.
Conclusion
invert_where
is dangerous, so you need to be careful if you want to use it.
If you pay close attention to the method, you can avoid the problems. But human makes mistakes, and it is not easy for beginners.
So I proposed a RuboCop rule for invert_where
to rubocop-rails project.
https://github.com/rubocop/rubocop-rails/issues/470
But I think it still a dangerous method even if the cop is implemented. So I'm wondering if it should be reverted until releasing Rails 7.
This article is self-translated from a Japanese article
Top comments (0)