Is there a way to invert an ActiveRecord::Relation query?
Asked Answered
D

6

13

Let's say we have the following:

irb> Post.where(:hidden => true).to_sql
=> "SELECT `posts`.* FROM `posts` WHERE posts.hidden = 1"

Could we somehow get an inverted SQL query out of it?

What I am looking for, should probably look like this:

irb> Post.where(:hidden => true).invert.to_sql
=> "SELECT `posts`.* FROM `posts` WHERE NOT (posts.hidden = 1)"
Dorsey answered 14/3, 2011 at 18:5 Comment(1)
Hmm. Bad upvote on the comment where(:hidden => false) by someone. That code will not generate the type of SQL the OP is looking for.Pau
P
18

With a different syntax, yes. Example:

posts = Post.scoped.table # or Arel::Table.new("posts")
posts.where(posts[:hidden].eq(true).not).to_sql
# => SELECT  FROM `posts` WHERE NOT ((`posts`.`hidden` = 1))
Pau answered 14/3, 2011 at 19:29 Comment(3)
Not precisely what I was looking for but still a very good answer.Dorsey
Did you find another way to do this? Perhaps a better syntax? Did the answer work for you?Pau
Actually I think it's not even possible since what I am actually asking is to invert and ActiveRecord::Relation object which might or might not have multiple joins and includes which would complicate the matter too much (which WHEREs should we invert and which not?). I think I'll leave this open until an answer comes along.Dorsey
N
7

In rails 4 there is not suffix for this purpose:

Post.where.not(hidden: true).to_sql
# => SELECT FROM `posts` WHERE `posts`.`hidden` != 1

In rails 3 you can use squeel gem. It gives many usefull features. And with it you can write:

Post.where{ hidden != true }.to_sql
# => SELECT FROM `posts` WHERE `posts`.`hidden` != 1
Numbers answered 21/2, 2013 at 9:2 Comment(0)
C
5

We can take Zabba's answer further by passing the inverted query back into ActiveRecord:

table = Post.arel_table
query = table[:hidden].eq(true).not # the inverted query, still ARel
Post.where(query) # plug it back into ActiveRecord

This will return ActiveRecord objects, as you would normally expect.

Commonality answered 12/8, 2011 at 18:59 Comment(0)
S
5

invert_where (Rails 7+)

Starting from Rails 7, there is a new invert_where method.

According to the docs, it:

Allows you to invert an entire where clause instead of manually applying conditions.

class User
  scope :active, -> { where(accepted: true, locked: false) }
end

User.where(accepted: true)
# WHERE `accepted` = 1

User.where(accepted: true).invert_where
# WHERE `accepted` != 1

User.active
# WHERE `accepted` = 1 AND `locked` = 0

User.active.invert_where
# WHERE NOT (`accepted` = 1 AND `locked` = 0)

Be careful because this inverts all conditions before invert_where call.

class User
  scope :active, -> { where(accepted: true, locked: false) }
  scope :inactive, -> { active.invert_where } # Do not attempt it
end

# It also inverts `where(role: 'admin')` unexpectedly.
User.where(role: 'admin').inactive
# WHERE NOT (`role` = 'admin' AND `accepted` = 1 AND `locked` = 0)

Sources:

Sixty answered 18/7, 2021 at 23:10 Comment(1)
He, I just googled a solution for a inverse where and you answered this solution yesterday. Another reason to move to rails 7 :)Valdes
B
0

What I do when I'm looking for records with a "not true" condition (eg, false or nil) is:

Post.where(["(hidden IS NULL) OR (hidden = ?)", false])
Bova answered 11/9, 2012 at 6:38 Comment(0)
P
0

Finally we have the method invert_where with Rails 7.

irb> Post.where(:hidden => true).invert_where.to_sql
"SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"hidden\" != 1"

Please check this commit reference for more details.

Poop answered 17/2, 2021 at 17:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.