From 50f3fa9a7be8407c786c540d3c0d734873ce7b26 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 29 Apr 2026 15:05:19 -0600 Subject: [PATCH] MONGOID-5684 Ensure field_was returns the same type as field for hash-typed fields --- lib/mongoid/extensions.rb | 1 + lib/mongoid/extensions/bson_document.rb | 28 +++++++++++++++++++++++++ spec/mongoid/changeable_spec.rb | 18 ++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 lib/mongoid/extensions/bson_document.rb diff --git a/lib/mongoid/extensions.rb b/lib/mongoid/extensions.rb index 8e18df3a10..1ad9416f63 100644 --- a/lib/mongoid/extensions.rb +++ b/lib/mongoid/extensions.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'mongoid/extensions/array' +require 'mongoid/extensions/bson_document' require 'mongoid/extensions/big_decimal' require 'mongoid/extensions/binary' require 'mongoid/extensions/boolean' diff --git a/lib/mongoid/extensions/bson_document.rb b/lib/mongoid/extensions/bson_document.rb new file mode 100644 index 0000000000..eb2add1649 --- /dev/null +++ b/lib/mongoid/extensions/bson_document.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Mongoid + module Extensions + # Adds behavior to BSON::Document. + module BsonDocument + # Make a deep copy of this document, preserving the BSON::Document type. + # + # Hash#__deep_copy__ returns a plain Hash, which causes field_was to + # return a different type than the field getter when the stored attribute + # is a BSON::Document. + # + # @example Make a deep copy of the document. + # doc.__deep_copy__ + # + # @return [ BSON::Document ] The copied document. + def __deep_copy__ + self.class.new.tap do |copy| + each_pair do |key, value| + copy.store(key, value.__deep_copy__) + end + end + end + end + end +end + +BSON::Document.include Mongoid::Extensions::BsonDocument diff --git a/spec/mongoid/changeable_spec.rb b/spec/mongoid/changeable_spec.rb index 9c012b0423..c33a0eabed 100644 --- a/spec/mongoid/changeable_spec.rb +++ b/spec/mongoid/changeable_spec.rb @@ -674,6 +674,24 @@ expect(person.send(:attribute_was, 'title')).to eq('Grand Poobah') end end + + context 'when the attribute is a Hash field stored as a BSON::Document' do + let(:person) do + Person.new(map: BSON::Document.new('foo' => true)).tap(&:move_changes) + end + + before do + person.map['bar'] = true + end + + it 'returns a BSON::Document, consistent with the field getter' do + expect(person.map_was).to be_a(BSON::Document) + end + + it 'returns the correct previous value' do + expect(person.map_was).to eq('foo' => true) + end + end end describe '#attribute_previously_was' do