-
Type:
Bug
-
Resolution: Unresolved
-
Priority:
Major - P3
-
None
-
Affects Version/s: None
-
Component/s: None
-
None
Mongoid::Fields#dot_dollar_field? (lib/mongoid/fields.rb:262) tests fields.key?
before testing the name shape:
def dot_dollar_field?(name)
n = aliased_fields[name] || name
fields.key?
&& (n.include?('.') || n.start_with?('$'))
end
The effect is that validate_writable_field_name! rejects dotted and dollared names only when the field has been declared, and never checks undeclared names. The condition is inverted relative to its intent: the declared case is the one Mongoid already supports, while undeclared names are not validated at all.
On a model including Mongoid::Attributes::Dynamic this means a dotted attribute name passes through write_attribute and is stored verbatim. Dirty tracking then emits the raw dotted key into the update modifier, and the server interprets it as a nested path.
Repro:
class Profile
include Mongoid::Document
embedded_in :account
field :score, type: Integer, default: 0
end
class Account
include Mongoid::Document
include Mongoid::Attributes::Dynamic
embeds_one :profile
end
acct = Account.create!(profile: Profile.new)
acct.assign_attributes('profile.score' => 42)
acct.atomic_updates
- => {"$set"=>{"profile.score"=>42}}
acct.save!
Account.collection.find(_id: acct.id).first['profile'] - => {"_id"=>..., "score"=>42}
Other observed effects of the same path, all reproduced on master:
- The declared type of the target field is not applied. assign_attributes('profile.score' => 'not-an-integer') stores the String "not-an-integer" in a field declared as Integer.
- Keys that the embedded model would otherwise reject are written. assign_attributes('profile' =>
{'undeclared' => 1}
) raises Mongoid::Errors::UnknownAttribute, while assign_attributes('profile.undeclared' => 1) stores undeclared => 1.
- The in-memory object and the stored document diverge. After the first repro above, acct.profile.score is still 0 while the stored document has 42, because the write is recorded against the dotted key on the parent and the embedded object is never touched.
- remove_attribute produces the $unset equivalent. remove_attribute('profile.score') yields {"$unset"=>{"profile.score"=>true}} and removes the key from the embedded document.
The condition is unchanged on 8.0-stable:263, 8.1-stable:263, 9.0-stable:265 and master:262, dating to MONGOID-5351.
Suggested fix: test the name shape independently of fields.key?. No existing spec asserts that dotted writes to undeclared names succeed; spec/integration/dots_and_dollars_spec.rb only asserts that the guard raises for declared dotted fields. Tightening the check does make previously accepted writes raise, so it should target a minor or major release.