diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 7f9d3766c3..fee15a134e 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,19 @@ +* Expose an `invert_where` method that will invert all scope conditions. + + ```ruby + 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) + ``` + + *Kevin Deisz* + * Restore possibility of passing `false` to :polymorphic option of `belongs_to`. Previously, passing `false` would trigger the option validation logic diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 5e0669e5ec..17e452640b 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -13,7 +13,7 @@ module ActiveRecord :destroy_all, :delete_all, :update_all, :touch_all, :destroy_by, :delete_by, :find_each, :find_in_batches, :in_batches, :select, :reselect, :order, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins, - :where, :rewhere, :preload, :extract_associated, :eager_load, :includes, :from, :lock, :readonly, + :where, :rewhere, :invert_where, :preload, :extract_associated, :eager_load, :includes, :from, :lock, :readonly, :and, :or, :annotate, :optimizer_hints, :extending, :having, :create_with, :distinct, :references, :none, :unscope, :merge, :except, :only, :count, :average, :minimum, :maximum, :sum, :calculate, diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 54fe513304..bf76f4deb6 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -693,6 +693,32 @@ module ActiveRecord scope end + # 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) + def invert_where + spawn.invert_where! + end + + def invert_where! # :nodoc: + self.where_clause = where_clause.invert + self + end + # Returns a new relation, which is the logical intersection of this relation and the one passed # as an argument. # diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index 98a1f68890..c64c4b6c78 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -425,5 +425,12 @@ module ActiveRecord def test_where_with_unsupported_arguments assert_raises(ArgumentError) { Author.where(42) } end + + def test_invert_where + author = authors(:david) + posts = author.posts.where.not(id: 1) + + assert_equal 1, posts.invert_where.first.id + end end end