Skip to content

Commit 42f7e67

Browse files
authored
MONGOID-5844 Fix counting bug in HABTM associations (#5952)
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 9772be1 commit 42f7e67

File tree

2 files changed

+49
-41
lines changed

2 files changed

+49
-41
lines changed

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

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

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

+28-37
Original file line numberDiff line numberDiff line change
@@ -1755,43 +1755,6 @@
17551755
end
17561756
end
17571757

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

17971760
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)