-
Type:
Bug
-
Resolution: Unresolved
-
Priority:
Unknown
-
None
-
Affects Version/s: 9.1.0, 9.1.1
-
Component/s: None
-
None
-
None
-
Ruby Drivers
-
None
-
None
-
None
-
None
-
None
-
None
Summary
When a foreign key array of an embedded document is changed, touch is
called, and a preceding sibling is then removed, the next save also applies the
change to the following sibling. No error is raised, so the data is silently
wrong.
It needs only one level of embeds_many with a has_and_belongs_to_many
on the embedded document, and it happens on Mongoid 9.1.0 and 9.1.1.
Environment
- Mongoid 9.1.0, 9.1.1 and master 7780ca294 (Mongoid::VERSION is still 9.1.1)
- mongo driver 2.26.0
- Ruby 4.0.0 and 4.0.7
- MongoDB 8.0 (replica set)
Models
class Grade include Mongoid::Document end class Child include Mongoid::Document field :name, type: String embedded_in :root has_and_belongs_to_many :grades, inverse_of: nil end class Root include Mongoid::Document embeds_many :children accepts_nested_attributes_for :children, allow_destroy: true end
Every child is persisted with grade_ids: [g0].
Steps to reproduce
A self-contained script is attached (stale_fk_array_buffers.rb, using
bundler/inline; it needs nothing but a reachable MongoDB). It runs against
master by default, and against a released version with MONGOID_VERSION:
ruby stale_fk_array_buffers.rb MONGOID_VERSION=9.1.1 ruby stale_fk_array_buffers.rb
# children: [a, b, c]
root = Root.find(id)
a, b, _c = root.children.to_a
b.grade_ids -= [g0.id]
root.touch
root.children.delete(a)
root.save!
- Expected: children == [b, c], b.grade_ids == [],
c.grade_ids == [g0] - Actual: children == [b, c], b.grade_ids == [], c.grade_ids == [].
g0 is also removed from c.
The same happens when a is destroyed through nested attributes
({{root.children_attributes = [
]}}) instead of
delete. On 9.1.1 that form fails with code 40 (ConflictingUpdateOperators)
and nothing is written; since MONGOID-6013
(https://github.com/mongodb/mongoid/pull/6189) it also writes to c.
Analysis
- touch calls atomic_updates (through
Touchable#_extract_touches_from_atomic_sets), and
Fields::ForeignKey#add_atomic_changes stores
$pullAll children.1.grade_ids in b's atomic_array_pulls, keyed by b's
position at that time. This buffer is only cleared after a successful save
(move_changes); touch does not clear it. - delete removes a with its own $pull, and b moves to index 0.
- The save rebuilds the updates and adds $pullAll children.0.grade_ids, but
the stale children.1.grade_ids key is still in the buffer. Index 1 is now
c, so g0 is removed from c as well.
The same happens when a save fails after atomic_updates has been built and
the document is reused without a reload. atomic_array_add_to_sets has the
same problem for additions.
Possible fix
Clear atomic_array_add_to_sets and atomic_array_pulls of each document
at the start of generate_atomic_updates. Only
Fields::ForeignKey#add_atomic_changes fills them, through atomic_sets
(Changeable#setters) in the same method, so they are rebuilt from the
current changes with the current positions and no change is lost.
Related
MONGOID-6013(https://github.com/mongodb/mongoid/pull/6189): makes the nested
attributes form write to the wrong document instead of failing with code 40.- MONGOID-6019: wrong-target writes from the order of conflicting writes
afterMONGOID-6013. Independent of this issue. - MONGOID-5364 (https://github.com/mongodb/mongoid/pull/6182): another case of a
position remembered before a sibling is removed, through
delayed_atomic_sets and remove_attribute.
- related to
-
MONGOID-6019 Conflicting embedded array updates are silently written to the wrong embedded document after MONGOID-6013
-
- Needs Triage
-
-
MONGOID-6013 Conflicting embedded array updates raise MongoDB code 40
-
- Closed
-