Repository: mdowds/python-mock-firestore

Issue:
Updating fields for nested objects (dot notation) does not work
Updating using the dot notation should update the nested field value

```python
frank_ref = db.collection("users").document("frank")
frank_ref.set(
    {
        "name": "Frank",
        "favorites": {"food": "Pizza", "color": "Blue", "subject": "Recess"},
        "age": 12,
    }
)

# Update age and favorite color
frank_ref.update({"age": 13, "favorites.color": "Red"})

doc = db.collection("users").document("frank").get().to_dict()

assert doc["age"] == 13  # ok
assert doc["favorites.color"] == "Red"  # This shouldnt pass but passes
assert doc["favorites"]["color"] == "Red"  # This fails
```
Reference : 
https://firebase.google.com/docs/firestore/manage-data/add-data#update_fields_in_nested_objects

Discussion:

Patch:
diff --git a/README.md b/README.md
index f4a7906..42dba75 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,7 @@ mock_db.reset()
 mock_db = MockFirestore()
 
 # Collections
+mock_db.collections()
 mock_db.collection('users')
 mock_db.collection('users').get()
 mock_db.collection('users').list_documents()
@@ -52,6 +53,7 @@ mock_db.collection('users').document('alovelace').set({
 })
 mock_db.collection('users').document('alovelace').set({'first': 'Augusta Ada'}, merge=True)
 mock_db.collection('users').document('alovelace').update({'born': 1815})
+mock_db.collection('users').document('alovelace').update({'favourite.color': 'red'})
 mock_db.collection('users').document('alovelace').update({'associates': ['Charles Babbage', 'Michael Faraday']})
 mock_db.collection('users').document('alovelace').collection('friends')
 mock_db.collection('users').document('alovelace').delete()
diff --git a/mockfirestore/_helpers.py b/mockfirestore/_helpers.py
index f1db796..f80fc22 100644
--- a/mockfirestore/_helpers.py
+++ b/mockfirestore/_helpers.py
@@ -12,14 +12,23 @@ Collection = Dict[str, Document]
 Store = Dict[str, Collection]
 
 
-def get_by_path(data: Dict[str, T], path: Sequence[str]) -> T:
+def get_by_path(data: Dict[str, T], path: Sequence[str], create_nested: bool = False) -> T:
     """Access a nested object in root by item sequence."""
-    return reduce(operator.getitem, path, data)
 
+    def get_or_create(a, b):
+        if b not in a:
+            a[b] = {}
+        return a[b]
 
-def set_by_path(data: Dict[str, T], path: Sequence[str], value: T):
+    if create_nested:
+        return reduce(get_or_create, path, data)
+    else:
+        return reduce(operator.getitem, path, data)
+
+
+def set_by_path(data: Dict[str, T], path: Sequence[str], value: T, create_nested: bool = True):
     """Set a value in a nested object in root by item sequence."""
-    get_by_path(data, path[:-1])[path[-1]] = value
+    get_by_path(data, path[:-1], create_nested=True)[path[-1]] = value
 
 
 def delete_by_path(data: Dict[str, T], path: Sequence[str]):
diff --git a/mockfirestore/client.py b/mockfirestore/client.py
index 07fd398..75943bd 100644
--- a/mockfirestore/client.py
+++ b/mockfirestore/client.py
@@ -1,4 +1,4 @@
-from typing import Iterable
+from typing import Iterable, Sequence
 from mockfirestore.collection import CollectionReference
 from mockfirestore.document import DocumentReference, DocumentSnapshot
 from mockfirestore.transaction import Transaction
@@ -44,6 +44,9 @@ class MockFirestore:
                 self._data[name] = {}
             return CollectionReference(self._data, [name])
 
+    def collections(self) -> Sequence[CollectionReference]:
+        return [CollectionReference(self._data, [collection_name]) for collection_name in self._data]
+
     def reset(self):
         self._data = {}
 
diff --git a/mockfirestore/document.py b/mockfirestore/document.py
index 5a39269..5a9c80d 100644
--- a/mockfirestore/document.py
+++ b/mockfirestore/document.py
@@ -130,9 +130,15 @@ def _apply_transformations(document: Dict[str, Any], data: Dict[str, Any]):
             except (TypeError, KeyError):
                 item = default
 
-            set_by_path(data, path, item + value)
-    
+            set_by_path(data, path, item + value, create_nested=True)
+
     _update_data(increments, 0)
     _update_data(arr_unions, [])
 
-    document.update(data)
+    _apply_updates(document, data)
+
+
+def _apply_updates(document: Dict[str, Any], data: Dict[str, Any]):
+    for key, value in data.items():
+        path = key.split(".")
+        set_by_path(document, path, value, create_nested=True)
diff --git a/mockfirestore/query.py b/mockfirestore/query.py
index 4761a92..7a4618d 100644
--- a/mockfirestore/query.py
+++ b/mockfirestore/query.py
@@ -121,6 +121,8 @@ class Query:
     def _compare_func(self, op: str) -> Callable[[T, T], bool]:
         if op == '==':
             return lambda x, y: x == y
+        elif op == '!=':
+            return lambda x, y: x != y
         elif op == '<':
             return lambda x, y: x < y
         elif op == '<=':


Test patch:
diff --git a/tests/test_collection_reference.py b/tests/test_collection_reference.py
index 59397be..ed34c8b 100644
--- a/tests/test_collection_reference.py
+++ b/tests/test_collection_reference.py
@@ -81,6 +81,16 @@ class TestCollectionReference(TestCase):
         docs = list(fs.collection('foo').where('valid', '==', True).stream())
         self.assertEqual({'valid': True}, docs[0].to_dict())
 
+    def test_collection_whereNotEquals(self):
+        fs = MockFirestore()
+        fs._data = {'foo': {
+            'first': {'count': 1},
+            'second': {'count': 5}
+        }}
+
+        docs = list(fs.collection('foo').where('count', '!=', 1).stream())
+        self.assertEqual({'count': 5}, docs[0].to_dict())
+
     def test_collection_whereLessThan(self):
         fs = MockFirestore()
         fs._data = {'foo': {
diff --git a/tests/test_document_reference.py b/tests/test_document_reference.py
index f91e144..828bc10 100644
--- a/tests/test_document_reference.py
+++ b/tests/test_document_reference.py
@@ -258,4 +258,42 @@ class TestDocumentReference(TestCase):
         fs.collection('foo').document('first').update({'arr': firestore.ArrayUnion([1])})
 
         doc = fs.collection('foo').document('first').get().to_dict()
-        self.assertEqual(doc, {'arr': [1], 'spicy': 'tuna'})
\ No newline at end of file
+        self.assertEqual(doc, {'arr': [1], 'spicy': 'tuna'})
+
+    def test_document_update_nestedFieldDotNotation(self):
+        fs = MockFirestore()
+        fs._data = {"foo": {"first": {"nested": {"value": 1, "unchanged": "foo"}}}}
+
+        fs.collection("foo").document("first").update({"nested.value": 2})
+
+        doc = fs.collection("foo").document("first").get().to_dict()
+        self.assertEqual(doc, {"nested": {"value": 2, "unchanged": "foo"}})
+
+    def test_document_update_nestedFieldDotNotationNestedFieldCreation(self):
+        fs = MockFirestore()
+        fs._data = {"foo": {"first": {"other": None}}}  # non-existent nested field is created
+
+        fs.collection("foo").document("first").update({"nested.value": 2})
+
+        doc = fs.collection("foo").document("first").get().to_dict()
+        self.assertEqual(doc, {"nested": {"value": 2}, "other": None})
+
+    def test_document_update_nestedFieldDotNotationMultipleNested(self):
+        fs = MockFirestore()
+        fs._data = {"foo": {"first": {"other": None}}}
+
+        fs.collection("foo").document("first").update({"nested.subnested.value": 42})
+
+        doc = fs.collection("foo").document("first").get().to_dict()
+        self.assertEqual(doc, {"nested": {"subnested": {"value": 42}}, "other": None})
+
+    def test_document_update_nestedFieldDotNotationMultipleNestedWithTransformer(self):
+        fs = MockFirestore()
+        fs._data = {"foo": {"first": {"other": None}}}
+
+        fs.collection("foo").document("first").update(
+            {"nested.subnested.value": firestore.ArrayUnion([1, 3])}
+        )
+
+        doc = fs.collection("foo").document("first").get().to_dict()
+        self.assertEqual(doc, {"nested": {"subnested": {"value": [1, 3]}}, "other": None})
diff --git a/tests/test_mock_client.py b/tests/test_mock_client.py
index 227c5c7..7f99414 100644
--- a/tests/test_mock_client.py
+++ b/tests/test_mock_client.py
@@ -16,4 +16,18 @@ class TestMockFirestore(TestCase):
         expected_doc_snapshot = doc.get().to_dict()
         self.assertEqual(returned_doc_snapshot, expected_doc_snapshot)
 
+    def test_client_collections(self):
+        fs = MockFirestore()
+        fs._data = {
+            'foo': {
+                'first': {'id': 1},
+                'second': {'id': 2}
+            },
+            'bar': {}
+        }
+        collections = fs.collections()
+        expected_collections = fs._data
 
+        self.assertEqual(len(collections), len(expected_collections))
+        for collection in collections:
+            self.assertTrue(collection._path[0] in expected_collections)