Skip to content

Commit 6fe2a5d

Browse files
authored
MONGOID-5844 Fix counting bug in HABTM associations (#5949)
In certain situations, calling #size on a HABTM association will return the wrong count. This will happen if the association is initialized to a single element (forcing the _unloaded Criteria object to be scoped to that specific element) and then assigning an array of multiple (already-persisted) elements to the association, where one of the elements is the same element that already existed there. In this case, `_unloaded.count` will return 1, and then the _added array will have two previously-persisted records. Naive implementations will thus return either 1, or 3, rather than the correct answer of 2. To get the correct answer, it is necessary to add a filter condition to `_unloaded.count` that excludes the records in the `_added` array.
1 parent 902fbc4 commit 6fe2a5d

File tree

2 files changed

+49
-41
lines changed

2 files changed

+49
-41
lines changed

lib/mongoid/association/referenced/has_many/enumerable.rb

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -419,11 +419,28 @@ def respond_to?(name, include_private = false)
419419
#
420420
# @return [ Integer ] The size of the enumerable.
421421
def size
422-
count = (_unloaded ? _unloaded.count : _loaded.count)
423-
if count.zero?
424-
count + _added.count
422+
# If _unloaded is present, then it will match the set of documents
423+
# that belong to this association, which have already been persisted
424+
# to the database. This set of documents must be considered when
425+
# computing the size of the association, along with anything that has
426+
# since been added.
427+
if _unloaded
428+
if _added.any?
429+
# Note that _added may include records that _unloaded already
430+
# matches. This is the case if the association is assigned an array
431+
# of items and some of them were already elements of the association.
432+
#
433+
# we need to thus make sure _unloaded.count excludes any elements
434+
# that already exist in _added.
435+
436+
count = _unloaded.not(:_id.in => _added.values.map(&:id)).count
437+
count + _added.values.count
438+
else
439+
_unloaded.count
440+
end
441+
425442
else
426-
count + _added.values.count { |d| d.new_record? }
443+
_loaded.count + _added.count
427444
end
428445
end
429446

spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb

Lines changed: 28 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1756,43 +1756,6 @@
17561756
end
17571757
end
17581758

1759-
describe "#any?" do
1760-
1761-
let(:person) do
1762-
Person.create!
1763-
end
1764-
1765-
context "when nothing exists on the relation" do
1766-
1767-
context "when no document is added" do
1768-
1769-
let!(:sandwich) do
1770-
Sandwich.create!
1771-
end
1772-
1773-
it "returns false" do
1774-
expect(sandwich.meats.any?).to be false
1775-
end
1776-
end
1777-
1778-
context "when the document is destroyed" do
1779-
1780-
before do
1781-
Meat.create!
1782-
end
1783-
1784-
let!(:sandwich) do
1785-
Sandwich.create!
1786-
end
1787-
1788-
it "returns false" do
1789-
sandwich.destroy
1790-
expect(sandwich.meats.any?).to be false
1791-
end
1792-
end
1793-
end
1794-
end
1795-
17961759
context "when documents have been persisted" do
17971760

17981761
let!(:preference) do
@@ -3041,6 +3004,34 @@
30413004
end
30423005
end
30433006

3007+
# MONGOID-5844
3008+
#
3009+
# Specifically, this tests the case where the association is
3010+
# initialized with a single element (so that Proxy#push does not take
3011+
# the `concat` route), which causes `reset_unloaded` to be called, which
3012+
# sets the `_unloaded` Criteria object to match only the specific element
3013+
# that was given.
3014+
#
3015+
# The issue now is that when the events list is updated to be both events,
3016+
# _unloaded matches one of them already, and the other has previously been
3017+
# persisted so `new_record?` won't match it. We need to make sure the
3018+
# `#size` logic properly accounts for this case.
3019+
context 'when documents have been previously persisted' do
3020+
let(:person1) { Person.create! }
3021+
let(:person2) { Person.create! }
3022+
let(:event1) { Event.create!(administrators: [ person1 ]) }
3023+
let(:event2) { Event.create!(administrators: [ person2 ]) }
3024+
3025+
before do
3026+
person1.administrated_events = [ event1, event2 ]
3027+
end
3028+
3029+
it 'returns the number of associated documents [MONGOID-5844]' do
3030+
expect(person1.administrated_events.to_a.size).to eq(2)
3031+
expect(person1.administrated_events.size).to eq(2)
3032+
end
3033+
end
3034+
30443035
context "when documents have not been persisted" do
30453036

30463037
before do

0 commit comments

Comments
 (0)