Skip to content

Commit

Permalink
MONGOID-5844 Fix counting bug in HABTM associations
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jamis committed Feb 5, 2025
1 parent 9772be1 commit 42aa417
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 41 deletions.
25 changes: 21 additions & 4 deletions lib/mongoid/association/referenced/has_many/enumerable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -422,11 +422,28 @@ def respond_to?(name, include_private = false)
#
# @return [ Integer ] The size of the enumerable.
def size
count = (_unloaded ? _unloaded.count : _loaded.count)
if count.zero?
count + _added.count
# If _unloaded is present, then it will match the set of documents
# that belong to this association, which have already been persisted
# to the database. This set of documents must be considered when
# computing the size of the association, along with anything that has
# since been added.
if _unloaded
if _added.any?
# Note that _added may include records that _unloaded already
# matches. This is the case if the association is assigned an array
# of items and some of them were already elements of the association.
#
# we need to thus make sure _unloaded.count excludes any elements
# that already exist in _added.

count = _unloaded.not(:_id.in => _added.values.map(&:id)).count
count + _added.values.count
else
_unloaded.count
end

else
count + _added.values.count { |d| d.new_record? }
_loaded.count + _added.count
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1755,43 +1755,6 @@
end
end

describe "#any?" do

let(:person) do
Person.create!
end

context "when nothing exists on the relation" do

context "when no document is added" do

let!(:sandwich) do
Sandwich.create!
end

it "returns false" do
expect(sandwich.meats.any?).to be false
end
end

context "when the document is destroyed" do

before do
Meat.create!
end

let!(:sandwich) do
Sandwich.create!
end

it "returns false" do
sandwich.destroy
expect(sandwich.meats.any?).to be false
end
end
end
end

context "when documents have been persisted" do

let!(:preference) do
Expand Down Expand Up @@ -3041,6 +3004,34 @@
end
end

# MONGOID-5844
#
# Specifically, this tests the case where the association is
# initialized with a single element (so that Proxy#push does not take
# the `concat` route), which causes `reset_unloaded` to be called, which
# sets the `_unloaded` Criteria object to match only the specific element
# that was given.
#
# The issue now is that when the events list is updated to be both events,
# _unloaded matches one of them already, and the other has previously been
# persisted so `new_record?` won't match it. We need to make sure the
# `#size` logic properly accounts for this case.
context 'when documents have been previously persisted' do
let(:person1) { Person.create! }
let(:person2) { Person.create! }
let(:event1) { Event.create!(administrators: [ person1 ]) }
let(:event2) { Event.create!(administrators: [ person2 ]) }

before do
person1.administrated_events = [ event1, event2 ]
end

it 'returns the number of associated documents [MONGOID-5844]' do
expect(person1.administrated_events.to_a.size).to eq(2)
expect(person1.administrated_events.size).to eq(2)
end
end

context "when documents have not been persisted" do

before do
Expand Down

0 comments on commit 42aa417

Please sign in to comment.