`where.not` now generates NAND predicates instead of NOR
This commit is contained in:
parent
b6d62491e4
commit
688a1c9d1d
|
@ -1,3 +1,17 @@
|
|||
* `where.not` now generates NAND predicates instead of NOR.
|
||||
|
||||
Before:
|
||||
|
||||
User.where.not(name: "Jon", role: "admin")
|
||||
# SELECT * FROM users WHERE name != 'Jon' AND role != 'admin'
|
||||
|
||||
After:
|
||||
|
||||
User.where.not(name: "Jon", role: "admin")
|
||||
# SELECT * FROM users WHERE NOT (name == 'Jon' AND role == 'admin')
|
||||
|
||||
*Rafael Mendonça França*
|
||||
|
||||
* Remove deprecated `ActiveRecord::Result#to_hash` method.
|
||||
|
||||
*Rafael Mendonça França*
|
||||
|
|
|
@ -39,27 +39,13 @@ module ActiveRecord
|
|||
#
|
||||
# User.where.not(name: %w(Ko1 Nobu))
|
||||
# # SELECT * FROM users WHERE name NOT IN ('Ko1', 'Nobu')
|
||||
#
|
||||
# User.where.not(name: "Jon", role: "admin")
|
||||
# # SELECT * FROM users WHERE NOT (name == 'Jon' AND role == 'admin')
|
||||
def not(opts, *rest)
|
||||
where_clause = @scope.send(:build_where_clause, opts, rest)
|
||||
|
||||
if not_behaves_as_nor?(opts)
|
||||
ActiveSupport::Deprecation.warn(<<~MSG.squish)
|
||||
NOT conditions will no longer behave as NOR in Rails 6.1.
|
||||
To continue using NOR conditions, NOT each condition individually
|
||||
(`#{
|
||||
opts.flat_map { |key, value|
|
||||
if value.is_a?(Hash) && value.size > 1
|
||||
value.map { |k, v| ".where.not(#{key.inspect} => { #{k.inspect} => ... })" }
|
||||
else
|
||||
".where.not(#{key.inspect} => ...)"
|
||||
end
|
||||
}.join
|
||||
}`).
|
||||
MSG
|
||||
@scope.where_clause += where_clause.invert(:nor)
|
||||
else
|
||||
@scope.where_clause += where_clause.invert
|
||||
end
|
||||
@scope.where_clause += where_clause.invert
|
||||
|
||||
@scope
|
||||
end
|
||||
|
@ -92,14 +78,6 @@ module ActiveRecord
|
|||
|
||||
@scope
|
||||
end
|
||||
|
||||
private
|
||||
def not_behaves_as_nor?(opts)
|
||||
return false unless opts.is_a?(Hash)
|
||||
|
||||
opts.any? { |k, v| v.is_a?(Hash) && v.size > 1 } ||
|
||||
opts.size > 1
|
||||
end
|
||||
end
|
||||
|
||||
FROZEN_EMPTY_ARRAY = [].freeze
|
||||
|
|
|
@ -77,11 +77,9 @@ module ActiveRecord
|
|||
predicates == other.predicates
|
||||
end
|
||||
|
||||
def invert(as = :nand)
|
||||
def invert
|
||||
if predicates.size == 1
|
||||
inverted_predicates = [ invert_predicate(predicates.first) ]
|
||||
elsif as == :nor
|
||||
inverted_predicates = predicates.map { |node| invert_predicate(node) }
|
||||
else
|
||||
inverted_predicates = [ Arel::Nodes::Not.new(ast) ]
|
||||
end
|
||||
|
|
|
@ -87,7 +87,7 @@ class ActiveRecord::Relation
|
|||
end
|
||||
end
|
||||
|
||||
test "invert replaces each part of the predicate with its inverse" do
|
||||
test "invert wraps the ast inside a NAND node" do
|
||||
original = WhereClause.new([
|
||||
table["id"].in([1, 2, 3]),
|
||||
table["id"].not_in([1, 2, 3]),
|
||||
|
@ -102,20 +102,24 @@ class ActiveRecord::Relation
|
|||
"sql literal"
|
||||
])
|
||||
expected = WhereClause.new([
|
||||
table["id"].not_in([1, 2, 3]),
|
||||
table["id"].in([1, 2, 3]),
|
||||
table["id"].not_eq(1),
|
||||
table["id"].eq(2),
|
||||
table["id"].lteq(1),
|
||||
table["id"].lt(2),
|
||||
table["id"].gteq(1),
|
||||
table["id"].gt(2),
|
||||
table["id"].is_distinct_from(1),
|
||||
table["id"].is_not_distinct_from(2),
|
||||
Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new("sql literal"))
|
||||
Arel::Nodes::Not.new(
|
||||
Arel::Nodes::And.new([
|
||||
table["id"].in([1, 2, 3]),
|
||||
table["id"].not_in([1, 2, 3]),
|
||||
table["id"].eq(1),
|
||||
table["id"].not_eq(2),
|
||||
table["id"].gt(1),
|
||||
table["id"].gteq(2),
|
||||
table["id"].lt(1),
|
||||
table["id"].lteq(2),
|
||||
table["id"].is_not_distinct_from(1),
|
||||
table["id"].is_distinct_from(2),
|
||||
Arel::Nodes::Grouping.new("sql literal")
|
||||
])
|
||||
)
|
||||
])
|
||||
|
||||
assert_equal expected, original.invert(:nor)
|
||||
assert_equal expected, original.invert
|
||||
end
|
||||
|
||||
test "except removes binary predicates referencing a given column" do
|
||||
|
|
|
@ -158,11 +158,7 @@ module ActiveRecord
|
|||
all = [treasures(:diamond), sapphire, cars(:honda), sapphire]
|
||||
assert_equal all, PriceEstimate.all.sort_by(&:id).map(&:estimate_of)
|
||||
|
||||
actual = PriceEstimate.where.yield_self do |where_chain|
|
||||
where_chain.stub(:not_behaves_as_nor?, false) do
|
||||
where_chain.not(estimate_of_type: sapphire.class.polymorphic_name, estimate_of_id: sapphire.id)
|
||||
end
|
||||
end
|
||||
actual = PriceEstimate.where.not(estimate_of_type: sapphire.class.polymorphic_name, estimate_of_id: sapphire.id)
|
||||
only = PriceEstimate.where(estimate_of_type: sapphire.class.polymorphic_name, estimate_of_id: sapphire.id)
|
||||
|
||||
expected = all - [sapphire]
|
||||
|
@ -170,43 +166,14 @@ module ActiveRecord
|
|||
assert_equal all - expected, only.sort_by(&:id).map(&:estimate_of)
|
||||
end
|
||||
|
||||
def test_where_not_polymorphic_id_and_type_as_nor_is_deprecated
|
||||
def test_where_not_association_as_nand
|
||||
sapphire = treasures(:sapphire)
|
||||
|
||||
all = [treasures(:diamond), sapphire, cars(:honda), sapphire]
|
||||
assert_equal all, PriceEstimate.all.sort_by(&:id).map(&:estimate_of)
|
||||
|
||||
message = <<~MSG.squish
|
||||
NOT conditions will no longer behave as NOR in Rails 6.1.
|
||||
To continue using NOR conditions, NOT each condition individually
|
||||
(`.where.not(:estimate_of_type => ...).where.not(:estimate_of_id => ...)`).
|
||||
MSG
|
||||
actual = assert_deprecated(message) do
|
||||
PriceEstimate.where.not(estimate_of_type: sapphire.class.polymorphic_name, estimate_of_id: sapphire.id)
|
||||
end
|
||||
only = PriceEstimate.where(estimate_of_type: sapphire.class.polymorphic_name, estimate_of_id: sapphire.id)
|
||||
|
||||
expected = all - [sapphire]
|
||||
# NOT (estimate_of_type = 'Treasure' OR estimate_of_id = sapphire.id) matches only `cars(:honda)` unfortunately.
|
||||
assert_not_equal expected, actual.sort_by(&:id).map(&:estimate_of)
|
||||
assert_equal all - expected, only.sort_by(&:id).map(&:estimate_of)
|
||||
end
|
||||
|
||||
def test_where_not_association_as_nor_is_deprecated
|
||||
treasure = Treasure.create!(name: "my_treasure")
|
||||
PriceEstimate.create!(estimate_of: treasure, price: 2, currency: "USD")
|
||||
PriceEstimate.create!(estimate_of: treasure, price: 2, currency: "EUR")
|
||||
|
||||
message = <<~MSG.squish
|
||||
NOT conditions will no longer behave as NOR in Rails 6.1.
|
||||
To continue using NOR conditions, NOT each condition individually
|
||||
(`.where.not(:price_estimates => { :price => ... }).where.not(:price_estimates => { :currency => ... })`).
|
||||
MSG
|
||||
assert_deprecated(message) do
|
||||
result = Treasure.joins(:price_estimates).where.not(price_estimates: { price: 2, currency: "USD" })
|
||||
result = Treasure.joins(:price_estimates).where.not(price_estimates: { price: 2, currency: "USD" })
|
||||
|
||||
assert_predicate result, :empty?
|
||||
end
|
||||
assert_equal [treasures(:diamond), sapphire, sapphire], result
|
||||
end
|
||||
|
||||
def test_polymorphic_nested_array_where
|
||||
|
|
|
@ -159,6 +159,18 @@ Please refer to the [Changelog][active-record] for detailed changes.
|
|||
|
||||
### Notable changes
|
||||
|
||||
* `where.not` now generates NAND predicates instead of NOR.
|
||||
|
||||
Before:
|
||||
|
||||
User.where.not(name: "Jon", role: "admin")
|
||||
# SELECT * FROM users WHERE name != 'Jon' AND role != 'admin'
|
||||
|
||||
After:
|
||||
|
||||
User.where.not(name: "Jon", role: "admin")
|
||||
# SELECT * FROM users WHERE NOT (name == 'Jon' AND role == 'admin')
|
||||
|
||||
Active Storage
|
||||
--------------
|
||||
|
||||
|
|
Loading…
Reference in New Issue