Repository: MongoEngine/mongoengine

Issue:
db_field with dot-notation works on inserts but not on reads
```
class Metric(Document):
    key = StringField(primary_key=True)
    date = DateTimeField()
    clicks = IntField(default=0, required=True, db_field='fields.clicks')

m = Metric()

m.key = 'foobar'
m.date = datetime.utcnow()
m.clicks = 45
m.save()

m = Metric.objects.first()
# >> mongoengine.errors.FieldDoesNotExist: The field 'fields' does not exist on the document 'Metric'
```

It is saved in the database

```
> db.metric.find().pretty()
{
        "_id" : "foobar",
        "date" : ISODate("2016-09-14T05:18:38.137Z"),
        "fields" : {
                "clicks" : 45
        }
}
```

I have a feeling the issue is that you can use dot notation when you assign values after creating the document instance. Somehow assigning the values after bypasses the validation. For example, this doesn't work:

```
m = Metric(key='foobar',
           date=datetime.utcnow(),
           clicks=45)
m.save()
# mongoengine.errors.OperationError: Could not save document (The dotted field 'fields.clicks' in 'fields.clicks' is not valid for storage.)
```



Discussion:
It's a bug that we allow dots/dollar signs in `db_field`. We should validate db_field values and hence catch the error early because you even try to save/retrieve any documents.

MongoDB doesn't allow dots or dollar signs in the field names: https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names.

Resolving patch:
diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py
--- a/mongoengine/base/fields.py
+++ b/mongoengine/base/fields.py
@@ -41,7 +41,7 @@ def __init__(self, db_field=None, name=None, required=False, default=None,
         """
         :param db_field: The database field to store this field in
             (defaults to the name of the field)
-        :param name: Depreciated - use db_field
+        :param name: Deprecated - use db_field
         :param required: If the field is required. Whether it has to have a
             value or not. Defaults to False.
         :param default: (optional) The default value for this field if no value
@@ -81,6 +81,17 @@ def __init__(self, db_field=None, name=None, required=False, default=None,
         self.sparse = sparse
         self._owner_document = None
 
+        # Validate the db_field
+        if isinstance(self.db_field, six.string_types) and (
+            '.' in self.db_field or
+            '\0' in self.db_field or
+            self.db_field.startswith('$')
+        ):
+            raise ValueError(
+                'field names cannot contain dots (".") or null characters '
+                '("\\0"), and they must not start with a dollar sign ("$").'
+            )
+
         # Detect and report conflicts between metadata and base properties.
         conflicts = set(dir(self)) & set(kwargs)
         if conflicts:


Test patch:
diff --git a/tests/fields/fields.py b/tests/fields/fields.py
--- a/tests/fields/fields.py
+++ b/tests/fields/fields.py
@@ -306,6 +306,24 @@ class Person(Document):
         person.id = '497ce96f395f2f052a494fd4'
         person.validate()
 
+    def test_db_field_validation(self):
+        """Ensure that db_field doesn't accept invalid values."""
+
+        # dot in the name
+        with self.assertRaises(ValueError):
+            class User(Document):
+                name = StringField(db_field='user.name')
+
+        # name starting with $
+        with self.assertRaises(ValueError):
+            class User(Document):
+                name = StringField(db_field='$name')
+
+        # name containing a null character
+        with self.assertRaises(ValueError):
+            class User(Document):
+                name = StringField(db_field='name\0')
+
     def test_string_validation(self):
         """Ensure that invalid values cannot be assigned to string fields.
         """
@@ -3973,30 +3991,25 @@ def test_undefined_field_exception(self):
         """Tests if a `FieldDoesNotExist` exception is raised when trying to
         instanciate a document with a field that's not defined.
         """
-
         class Doc(Document):
-            foo = StringField(db_field='f')
+            foo = StringField()
 
-        def test():
+        with self.assertRaises(FieldDoesNotExist):
             Doc(bar='test')
 
-        self.assertRaises(FieldDoesNotExist, test)
 
     def test_undefined_field_exception_with_strict(self):
         """Tests if a `FieldDoesNotExist` exception is raised when trying to
         instanciate a document with a field that's not defined,
         even when strict is set to False.
         """
-
         class Doc(Document):
-            foo = StringField(db_field='f')
+            foo = StringField()
             meta = {'strict': False}
 
-        def test():
+        with self.assertRaises(FieldDoesNotExist):
             Doc(bar='test')
 
-        self.assertRaises(FieldDoesNotExist, test)
-
     def test_long_field_is_considered_as_int64(self):
         """
         Tests that long fields are stored as long in mongo, even if long value