Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
5a20b46
Add Mongoid::Changeset::Entry value object
jamis May 12, 2026
5b4f0db
Add Mongoid::Changeset core class with accumulation, nesting, and dis…
jamis May 12, 2026
ff92e84
MONGOID-6083 Add thread-local changeset storage accessors to Mongoid:…
jamis May 12, 2026
7b48ceb
Add before_flush and after_flush callbacks to Mongoid::Interceptable
jamis May 12, 2026
351904d
Changeset module-level lifecycle method (Task 4)
jamis May 12, 2026
29b0d3d
Replace NotImplementedError stub with no-op in _flush_entries
jamis May 12, 2026
e78579f
Implement Changeset#flush with batch grouping and callback dispatch
jamis May 12, 2026
07f3a16
Fix collection equality and in_transaction? check in Changeset#flush
jamis May 12, 2026
9ac409b
Rewire Creatable to stage :insert entries in the changeset
jamis May 12, 2026
16b2a5b
Fix RootInsertable override and stale comment after Creatable rewire
jamis May 12, 2026
11ac37c
Add :embedded_insert entry type to disambiguate from regular :update …
jamis May 12, 2026
02351d1
Rewire Updatable to stage :update entries in the changeset
jamis May 12, 2026
952c957
Fix double move_changes and restore MONGOID-4982 comment in Updatable
jamis May 12, 2026
434f4bd
Rewire Destroyable and Deletable to stage :delete entries in the chan…
jamis May 12, 2026
9a1150e
Add :embedded_delete entry type and deferred embedded delete spec
jamis May 12, 2026
dba567a
Add staged? and staged predicates to Mongoid::Stateful
jamis May 12, 2026
1bfdea2
Rewire atomically to use Mongoid.changeset{} internally
jamis May 12, 2026
59cc84c
Rewire atomic field operations to stage entries in the changeset
jamis May 12, 2026
3d94013
Stage criteria-level update_all and delete_all entries in the changeset
jamis May 12, 2026
52a4a0a
Referenced association setters call save! to gate writes with validation
jamis May 12, 2026
1cde79f
HABTM setter stages both sides through the changeset
jamis May 12, 2026
c658963
HABTM changeset staging: add error-path test and fix let! usage
jamis May 12, 2026
6392d45
Changeset persistence layer: regression fixes and cleanup
jamis May 12, 2026
9c7a3f7
ensure that a changeset aborted by an exception is marked terminated
jamis May 14, 2026
f8a68f2
present a more convenient interface for adding entries to a changeset
jamis May 14, 2026
175f537
rename Errors::InvalidOperation => Errors::InvalidChangesetOperation
jamis May 14, 2026
22f8924
ensure upsert operation also goes through changeset
jamis May 14, 2026
25b5479
fix direct database writes to go through changeset
jamis May 14, 2026
7392363
make Mongoid.changeset yield the current changeset to the block
jamis May 14, 2026
e57372b
fix callback definitions for flush
jamis May 14, 2026
09d21e1
allow delete to return a count, if not run in an outer changeset
jamis May 14, 2026
7cd9171
ensure atomic ops do not trigger commit/flush callbacks (unless inclu…
jamis May 14, 2026
f5642a4
fix documentation
jamis May 14, 2026
36d1fd0
recover original `#atomically` block semantics
jamis May 15, 2026
b25a3e3
clean up some duplication
jamis May 15, 2026
9fee183
disallow transactions inside a changeset
jamis May 15, 2026
6745061
fix flush callback specs to use the correct internal API
jamis May 15, 2026
2cab4e9
Account for changed return type in `#delete`
jamis May 18, 2026
57de214
Ensure multiple adjacent entries for the same document each trigger n…
jamis May 18, 2026
bcbf258
split batches by session instance, as well
jamis May 18, 2026
f406374
Forgot to commit this file
jamis May 18, 2026
4c30323
rubocop appeasement (rubocop 1.86.2)
jamis May 18, 2026
3b5cc85
after insert, clear cached descendants for subsequent operations
jamis May 18, 2026
27629dd
wrap all callbacks in changeset for insert and upsert operations
jamis May 18, 2026
b1dee63
apply stage-time state transitions in _stage_insert_as_embedded
jamis May 19, 2026
494eb88
fix test that broke due to changed callback contracts
jamis May 19, 2026
246f3ad
accommodate ruby 2.7 + rspec expections
jamis May 19, 2026
2db691b
might as well fix MONGOID-5911 while we're breaking things
jamis May 19, 2026
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
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ Mongoid is a Ruby Object-Document Mapper (ODM) framework for MongoDB. Mongoid al

# Development Workflow

## Planning and Design

When producing a design document or specification for a new feature or significant change, leave the document unstaged. Do not add it to the repository.

## Running tests

Tests require a running MongoDB instance. Set the URI via the `MONGODB_URI` environment variable:
Expand Down
4 changes: 4 additions & 0 deletions lib/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,10 @@ en:
message: "A transaction was started while another transaction was being used on this client."
summary: "Transactions cannot be nested. Only one transaction can be used in a thread per client."
resolution: "Only use one transaction per client at a time; transactions cannot be nested."
transaction_in_changeset:
message: "Cannot start a transaction inside a changeset."
summary: "A changeset defers all writes and flushes them as a batch when the outermost block exits. Starting a transaction inside a changeset would have no effect, because the operations inside the transaction block are queued rather than executed immediately."
resolution: "Do not nest a transaction inside a changeset."
inverse_not_found:
message: "When adding a(n) %{klass} to %{base}#%{name}, Mongoid could
not determine the inverse foreign key to set. The attempted key was
Expand Down
18 changes: 18 additions & 0 deletions lib/mongoid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
require 'mongoid/tasks/encryption'
require 'mongoid/warnings'
require 'mongoid/utils'
require 'mongoid/changeset'

# If we are using Rails then we will include the Mongoid railtie.
# This configures initializers required to integrate Mongoid with Rails.
Expand Down Expand Up @@ -107,6 +108,23 @@ def reconnect_clients
Clients.reconnect
end

# Creates or reuses the current changeset, yields it to the block, and
# flushes when the outermost scope exits. When nested inside an existing
# changeset block, the inner call accumulates without flushing.
#
# @example Explicit outer scope — flush happens once at block exit.
# Mongoid.changeset do |cs|
# parent.save
# child.save
# end
def changeset
outermost = Threaded.current_changeset.nil?
cs = Threaded.current_changeset || (Threaded.current_changeset = Changeset.new)
cs.run { yield cs }
ensure
Threaded.current_changeset = nil if outermost
end

# Convenience method for getting a named client.
#
# @example Get a named client.
Expand Down
84 changes: 59 additions & 25 deletions lib/mongoid/association/embedded/batchable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,16 @@ def batch_insert(docs)
def batch_clear(docs)
pre_process_batch_remove(docs, :delete)
unless docs.empty?
collection.find(selector).update_one(
positionally(selector, '$unset' => { path => true }),
session: _session
)
Mongoid.changeset do |cs|
cs.add(
type: :update,
collection: collection,
selector: selector,
payload: positionally(selector, '$unset' => { path => true }),
document: nil,
session: _session
)
end
# This solves the case in which a user sets, clears and resets an
# embedded document. Previously, since the embedded document was
# already marked not a "new_record", it wouldn't be persisted to
Expand Down Expand Up @@ -67,22 +73,38 @@ def batch_remove(docs, method = :delete)
end

if docs.empty?
collection.find(selector).update_one(
positionally(selector, '$set' => { path => [] }),
session: _session
)
else
unless pulls.empty?
collection.find(selector).update_one(
positionally(selector, '$pull' => { path => { '_id' => { '$in' => pulls.pluck('_id') } } }),
Mongoid.changeset do |cs|
cs.add(
type: :update,
collection: collection,
selector: selector,
payload: positionally(selector, '$set' => { path => [] }),
document: nil,
session: _session
)
end
unless pull_alls.empty?
collection.find(selector).update_one(
positionally(selector, '$pullAll' => { path => pull_alls }),
session: _session
)
else
Mongoid.changeset do |cs|
unless pulls.empty?
cs.add(
type: :update,
collection: collection,
selector: selector,
payload: positionally(selector, '$pull' => { path => { '_id' => { '$in' => pulls.pluck('_id') } } }),
document: nil,
session: _session
)
end
unless pull_alls.empty?
cs.add(
type: :update,
collection: collection,
selector: selector,
payload: positionally(selector, '$pullAll' => { path => pull_alls }),
document: nil,
session: _session
)
end
end
post_process_batch_remove(docs, method)
end
Expand Down Expand Up @@ -154,10 +176,16 @@ def execute_batch_set(docs)
self.inserts_valid = true
inserts = pre_process_batch_insert(docs)
if insertable?
collection.find(selector).update_one(
positionally(selector, '$set' => { path => inserts }),
session: _session
)
Mongoid.changeset do |cs|
cs.add(
type: :update,
collection: collection,
selector: selector,
payload: positionally(selector, '$set' => { path => inserts }),
document: nil,
session: _session
)
end
post_process_batch_insert(docs)
end
inserts
Expand All @@ -177,10 +205,16 @@ def execute_batch_push(docs)
self.inserts_valid = true
pushes = pre_process_batch_insert(docs)
if insertable?
collection.find(selector).update_one(
positionally(selector, '$push' => { path => { '$each' => pushes } }),
session: _session
)
Mongoid.changeset do |cs|
cs.add(
type: :update,
collection: collection,
selector: selector,
payload: positionally(selector, '$push' => { path => { '$each' => pushes } }),
document: nil,
session: _session
)
end
post_process_batch_insert(docs)
end
pushes
Expand Down
6 changes: 1 addition & 5 deletions lib/mongoid/association/nested/many.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,7 @@ def destroy_document(relation, doc)
# @param [ Hash ] attrs The attributes.
def update_document(doc, attrs)
delete_id(attrs)
if association.embedded?
doc.assign_attributes(attrs)
else
doc.update_attributes(attrs)
end
doc.assign_attributes(attrs)
end

# Update nested association.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,10 @@ def <<(*args)
# the changed_attributes hash.
# See MONGOID-4843 for a longer discussion about this.
reset_foreign_key_changes do
_base.add_to_set(foreign_key => doc.public_send(_association.primary_key))
doc.save if child_persistable?(doc)
Mongoid.changeset do
_base.add_to_set(foreign_key => doc.public_send(_association.primary_key))
doc.save! if child_persistable?(doc)
end
reset_unloaded
end
end
Expand Down
4 changes: 2 additions & 2 deletions lib/mongoid/association/referenced/has_many/proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def <<(*args)

if (doc = docs.first)
append(doc)
doc.save if persistable? && !_assigning? && !doc.validated?
doc.save! if persistable? && !_assigning? && !doc.validated?
end
self
end
Expand Down Expand Up @@ -597,7 +597,7 @@ def save_or_delay(doc, docs, inserts)
docs.push(doc)
inserts.push(doc.send(:as_attributes))
else
doc.save
doc.save!
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/mongoid/association/referenced/has_one/proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def initialize(base, target, association)
raise_mixed if klass.embedded? && !klass.cyclic?
characterize_one(_target)
bind_one
_target.save if persistable?
_target.save! if persistable?
end
end

Expand Down
Loading
Loading