Destroyed children lingering in memory on a has_many still get validated

XMLWordPrintableJSON

    • Type: Bug
    • Resolution: Fixed
    • Priority: Unknown
    • 9.1.1
    • Affects Version/s: 9.1.0
    • Component/s: Validations
    • None
    • None
    • Fully Compatible
    • Ruby Drivers
    • Not Needed
    • None
    • None
    • None
    • None
    • None
    • None

      When a document referenced via a has_many validate: true is destroyed, then the parent is later saved. before_validation callbacks end up being fired on the destroyed child. If they try to mutate the document, they get an error about modifying a frozen hash.

      I would expect that destroyed children should not be validated, even if they're hanging around in memory.

      Reproduction:

      require 'mongoid'
      
      Mongoid.configure do |c|
        c.clients.default = { hosts: ['localhost:27017'], database: 'mongoid_assoc_validator_repro' }
      end
      
      puts "Mongoid version:  #{Mongoid::VERSION}"
      
      class Parent
        include Mongoid::Document
        has_many :children, validate: true
      end
      
      class Child
        include Mongoid::Document
        field :note, type: String
        belongs_to :parent
      
        before_validation :touch_note
      
        def touch_note
          self.note = "validated at #{Time.now.to_f}"  # writes into the attributes hash
        end
      end
      
      Parent.delete_all
      Child.delete_all
      
      parent = Parent.create!
      child  = parent.children.create!   # created through the association, so it's held in the loaded proxy
      
      # A child is destroyed. In V9, `Persistable::Deletable#prepare_delete` `freeze`s
      # the document's attributes hash before flipping `destroyed = true` - and the
      # destroyed child is still sitting inside the parent's loaded children proxy.
      child.destroy
      
      puts ""
      puts "ruby-side:    child.destroyed?          = #{child.destroyed?}"
      puts "ruby-side:    child.attributes.frozen?  = #{child.attributes.frozen?}"
      puts "ruby-side:    still in parent's proxy?   = #{parent.children.any? { |c| c.equal?(child) }}"
      
      # Validating (or saving) the parent runs `validates_associated :children`, which
      # (V9, lazily) walks the in-memory proxy and calls `.valid?` on each element -
      # including the destroyed, frozen child. Its before_validation then tries to
      # write `note`, mutating the frozen attributes hash.
      begin
        parent.valid?
        puts "result:       parent.valid? raised      = (nothing)"
      rescue => e
        puts "result:       parent.valid? raised      = #{e.class}: #{e.message}"
      end
      
      # Expected:
      #   Validating a destroyed document is never useful (it can't be persisted
      #   again), so AssociatedValidator should skip it; parent.valid? returns true.
      # Actual on mongoid master / 9.1.0:
      #   FrozenError: can't modify frozen Hash / BSON::Document
      #   because AssociatedValidator calls `.valid?` on the frozen destroyed child,
      #   and its mutating before_validation callback writes to the frozen attributes.
      #
      # Fix: AssociatedValidator#validate_association should skip destroyed documents.
      # Its per-value guard is currently
      #   value && !value.flagged_for_destroy? && (!value.persisted? || value.changed?)
      # Adding `!value.destroyed?` resolves it (a destroyed doc reports
      # persisted? == false and flagged_for_destroy? == false, so it slips through).
      

      Output

      Mongoid version:  9.1.0
      
      ruby-side:    child.destroyed?          = true
      ruby-side:    child.attributes.frozen?  = true
      ruby-side:    still in parent's proxy?   = true
      result:       parent.valid? raised      = FrozenError: can't modify frozen Hash: {"_id" => BSON::ObjectId('6a1685d9ff90ba5ca8698b14'), "parent_id" => BSON::ObjectId('6a1685d9ff90ba5ca8698b13'), "note" => "validated at 1779860953.9466271"}
      

            Assignee:
            Jamis Buck
            Reporter:
            Tapio Saarinen
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

              Created:
              Updated:
              Resolved: