Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 34 additions & 8 deletions lib/mongoid/association/accessors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,31 @@ def without_autobuild
Threaded.exit_execution("without_autobuild")
end

# Is the current code executing within an association setter?
#
# @example Is setter active?
# document.in_setter?
#
# @return [ true | false ] If within a setter.
def in_setter?
Threaded.executing?(:in_setter)
end

# Yield to the block with setter flag enabled.
#
# @example Execute within setter context.
# in_setter do
# # setter logic
# end
#
# @return [ Object ] The result of the yield.
def in_setter
Threaded.begin_execution("in_setter")
yield
ensure
Threaded.exit_execution("in_setter")
end

# Parse out the attributes and the options from the args passed to a
# build_ or create_ methods.
#
Expand Down Expand Up @@ -345,15 +370,16 @@ def self.define_setter!(association)
name = association.name
association.inverse_class.tap do |klass|
klass.re_define_method("#{name}=") do |object|
without_autobuild do
if value = get_relation(name, association, object)
if !value.respond_to?(:substitute)
value = __build__(name, value, association)
in_setter do
without_autobuild do
if value = get_relation(name, association, object)
if !value.respond_to?(:substitute)
value = __build__(name, value, association)
end
set_relation(name, value.substitute(object.substitutable))
else
__build__(name, object.substitutable, association)
end

set_relation(name, value.substitute(object.substitutable))
else
__build__(name, object.substitutable, association)
end
end
end
Expand Down
22 changes: 21 additions & 1 deletion lib/mongoid/association/embedded/embeds_many/proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -601,11 +601,31 @@ def as_attributes
# @api private
def update_attributes_hash
if _target.empty?
_base.attributes.delete(_association.store_as)
handle_empty_target
else
_base.attributes.merge!(_association.store_as => _target.map(&:attributes))
end
end

# Check if we're currently in a setter
def in_setter?
_base.send(:in_setter?)
end

# Handle the case when the target is empty.
#
# @api private
def handle_empty_target
# Only persist empty array if:
# We're explicitly assigning (setter was called)
if _base.attributes.key?(_association.store_as) || in_setter?
_base.attributes.merge!(_association.store_as => _target.map(&:attributes))
else
# During initialization with no prior data, delete the key to maintain
# the original behavior of not persisting unassigned empty associations
_base.attributes.delete(_association.store_as)
end
end
end
end
end
Expand Down
55 changes: 47 additions & 8 deletions spec/mongoid/association/embedded/embeds_many/proxy_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4271,6 +4271,25 @@ class TrackingIdValidationHistory
end
end

context "when setting an embedded relation to an empty array" do
let(:document) do
Person.create!
end

let(:person) do
Person.find(document.id)
end

before do
person.update_attributes!(addresses: [])
end

it "sets the embedded relation to an empty array" do
expect(person.addresses).to be_empty
expect(person.attributes.keys).to include("addresses")
end
end

context "when pushing with a before_add callback" do

let(:artist) do
Expand Down Expand Up @@ -4771,18 +4790,38 @@ class DNS::Record

context "in an embeds_many relation" do

let(:band) { Band.create! }
context "using .create! first" do

before do
band.labels = []
band.save!
let(:band) { Band.create! }

before do
band.labels = []
band.save!
end

let(:reloaded_band) { Band.collection.find(_id: band._id).first }

it "persists the empty list" do
expect(reloaded_band).to have_key(:labels)
expect(reloaded_band[:labels]).to eq []
end
end

let(:reloaded_band) { Band.collection.find(_id: band._id).first }
context "using .new first" do

it "persists the empty list" do
expect(reloaded_band).to have_key(:labels)
expect(reloaded_band[:labels]).to eq []
let(:band) { Band.new }

before do
band.labels = []
band.save!
end

let(:reloaded_band) { Band.collection.find(_id: band._id).first }

it "persists the empty list" do
expect(reloaded_band).to have_key(:labels)
expect(reloaded_band[:labels]).to eq []
end
end
end

Expand Down
8 changes: 5 additions & 3 deletions spec/mongoid/attributes_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2368,7 +2368,7 @@
end

it "updates the attributes" do
expect(doc.attributes).to_not have_key("pages")
expect(doc.pages).to be_empty
end

it "has the same attributes after reloading" do
Expand Down Expand Up @@ -2475,10 +2475,12 @@
end

it "updates the attributes" do
expect(doc.attributes).to_not have_key("pages")
expect(doc.pages).to be_empty
end

it "has the same attributes after reloading" do
puts doc.attributes
puts doc.reload.attributes
expect(doc.attributes).to eq(doc.reload.attributes)
end
end
Expand All @@ -2495,7 +2497,7 @@
end

it "updates the attributes" do
expect(doc.attributes).to_not have_key("pages")
expect(doc.pages).to be_empty
end

it "has the same attributes after reloading" do
Expand Down
Loading