-
Type:
Bug
-
Resolution: Unresolved
-
Priority:
Minor - P4
-
None
-
Affects Version/s: None
-
Component/s: None
-
None
When a document's primary key is nil, its has_many criteria is built as foreign_key: nil, which MongoDB matches against every document whose
foreign key is null or missing. Reads, count, update_all and delete_all
through that association then hit unrelated records instead of the
document's own children.
A nil id is reachable from ordinary code: Model.new(id: "") mongoizes the
blank string to nil and discards the generated ObjectId, and models that
redefine _id without a default start out nil. The document cannot be saved
(the server rejects a nil _id), so this only affects unsaved parents.
Repro:
alice = Owner.create! Item.create!(label: 'alice-item', owner: alice) Item.create!(label: 'loose') # no owner, legitimate state Owner.new(id: '').items.delete_all # => 1, deletes 'loose'
Mongoid already half-holds the opposite position: create/create! on an
unsaved parent raises UnsavedDocument, and the proxy's to_a returns [].
Only the query-issuing methods fall through.
Fix: when the scoping value is nil, make the association criteria match
nothing, the way has_and_belongs_to_many already does for an empty id list
(has_and_belongs_to_many.rb:276). The guard belongs at the association
layer, not at _evolve_object_id_ or in the selector - Model.where(fk: nil)
is a legitimate query and must keep working.
Two related gaps to fix at the same time:
- HasMany::Proxy#remove_all extracts criteria.selector as a plain hash and
rebuilds a class-level query, so it drops the criteria's "matches nothing"
marker. Guarding the criteria alone does not cover delete_all/destroy_all. - Contextual::None does not implement delete_all, destroy_all or update_all
and raises NoMethodError, despite documenting interface parity with
Contextual::Mongo.
has_one, the *_through associations and the polymorphic paths have the same
shape and need the same guard.