ExportXMLWordPrintableJSON

    • 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 = [

      { id: a.id, _destroy: true }

      ]}}) 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

      1. 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.
      2. delete removes a with its own $pull, and b moves to index 0.
      3. 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

            Assignee:
            Unassigned
            Reporter:
            Masato Ikeda (EXT)
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated: