Repository: googleapis/google-cloud-python

Issue:
Firestore: `Collection.on_snapshot` does not work correctly on nested collections
**EDIT: Problem Found, see my next comment !**

`Collection.on_snapshot` works only on top-level collections.
Calling `on_snapshot` on any sub-collection does nothing. It will be called once, where
first two arguments will be empty arrays, and last one will be correct timestamp, no matter what is actually inside of the collection. It will never be called again.

Someone already found this issue on the Stack Overflow:
https://stackoverflow.com/questions/54407320/firestore-listen-to-documents-in-a-subcollection

But no response given, nor any similiar issue opened.

#### Environment details
Fedora 29, Linux 4.20.14.-200.fc29.x86_64
Python 3.6.8 (virtualenv)
```
google-api-core==1.8.1
google-auth==1.6.3
google-cloud-core==0.29.1
google-cloud-firestore==0.31.0
google-cloud-storage==1.14.0
google-resumable-media==0.3.2
```
#### Steps to reproduce

  1. Create document with subcollection in Firestore
  2. Attach any listener on created collection 

#### Code example

```python
db = firestore.Client()

def on_snapshot(snapshot, changes, ts):
  for doc in snapshot:
    print(doc)

# This would work as expected
db.collection("users").on_snapshot(on_snapshot)

# These will do nothing
db.collection("users/USER/values").on_snapshot(on_snapshot)
db.collection("users", "USER", "values").on_snapshot(on_snapshot)
db.collection("users").document("USER").collection("values").on_snapshot(on_snapshot)
```

As pointed out, I am rewriting Node.js and similar code does indeed work in Node, but not in the Python version



Discussion:
Ok fast update:
Found out the problem:
https://github.com/googleapis/google-cloud-python/blob/f242cc1a72818d4dc25927d962ec309ff5c5f3c2/firestore/google/cloud/firestore_v1beta1/watch.py#L354

`query._client._database_string` will always returns a root of database, instead of parent document of the collection.

Quicklu digging up in the guts of the Query class, found out that it is actually easy to get a real parent path in the `_parent_info()`, and this makes on_snapshot to work as expected:
```python
(parent, _) = query._parent._parent_info()
query_target = firestore_pb2.Target.QueryTarget(
    parent=parent, structured_query=query._to_protobuf()
)
```

Sorry, but I actually doesn't have time to create patch or PR.Please update manually if possible.

Resolving patch:
diff --git a/firestore/google/cloud/firestore_v1/watch.py b/firestore/google/cloud/firestore_v1/watch.py
--- a/firestore/google/cloud/firestore_v1/watch.py
+++ b/firestore/google/cloud/firestore_v1/watch.py
@@ -351,9 +351,9 @@ def for_document(
     def for_query(
         cls, query, snapshot_callback, snapshot_class_instance, reference_class_instance
     ):
+        parent_path, _ = query._parent._parent_info()
         query_target = firestore_pb2.Target.QueryTarget(
-            parent=query._client._database_string + "/documents",
-            structured_query=query._to_protobuf(),
+            parent=parent_path, structured_query=query._to_protobuf()
         )
 
         return cls(


Test patch:
diff --git a/firestore/tests/unit/v1/test_cross_language.py b/firestore/tests/unit/v1/test_cross_language.py
--- a/firestore/tests/unit/v1/test_cross_language.py
+++ b/firestore/tests/unit/v1/test_cross_language.py
@@ -236,7 +236,8 @@ def test_listen_testprotos(test_proto):  # pragma: NO COVER
                 def callback(keys, applied_changes, read_time):
                     snapshots.append((keys, applied_changes, read_time))
 
-                query = DummyQuery(client=client)
+                collection = DummyCollection(client=client)
+                query = DummyQuery(parent=collection)
                 watch = Watch.for_query(
                     query, callback, DocumentSnapshot, DocumentReference
                 )
@@ -374,11 +375,24 @@ def stop(self):
         self.is_active = False
 
 
+class DummyCollection(object):
+    def __init__(self, client, parent=None):
+        self._client = client
+        self._parent = parent
+
+    def _parent_info(self):
+        return "{}/documents".format(self._client._database_string), None
+
+
 class DummyQuery(object):  # pragma: NO COVER
-    def __init__(self, **kw):
-        self._client = kw["client"]
+    def __init__(self, parent):
+        self._parent = parent
         self._comparator = lambda x, y: 1
 
+    @property
+    def _client(self):
+        return self._parent._client
+
     def _to_protobuf(self):
         from google.cloud.firestore_v1.proto import query_pb2
 
diff --git a/firestore/tests/unit/v1/test_watch.py b/firestore/tests/unit/v1/test_watch.py
--- a/firestore/tests/unit/v1/test_watch.py
+++ b/firestore/tests/unit/v1/test_watch.py
@@ -227,6 +227,8 @@ def test_for_query(self):
         snapshot_callback = self._snapshot_callback
         snapshot_class_instance = DummyDocumentSnapshot
         document_reference_class_instance = DummyDocumentReference
+        client = DummyFirestore()
+        parent = DummyCollection(client)
         modulename = "google.cloud.firestore_v1.watch"
         pb2 = DummyPb2()
         with mock.patch("%s.firestore_pb2" % modulename, pb2):
@@ -234,7 +236,35 @@ def test_for_query(self):
                 with mock.patch(
                     "%s.Watch.BackgroundConsumer" % modulename, DummyBackgroundConsumer
                 ):
-                    query = DummyQuery()
+                    query = DummyQuery(parent=parent)
+                    inst = Watch.for_query(
+                        query,
+                        snapshot_callback,
+                        snapshot_class_instance,
+                        document_reference_class_instance,
+                    )
+        self.assertTrue(inst._consumer.started)
+        self.assertTrue(inst._rpc.callbacks, [inst._on_rpc_done])
+        self.assertEqual(inst._targets["query"], "dummy query target")
+
+    def test_for_query_nested(self):
+        from google.cloud.firestore_v1.watch import Watch
+
+        snapshot_callback = self._snapshot_callback
+        snapshot_class_instance = DummyDocumentSnapshot
+        document_reference_class_instance = DummyDocumentReference
+        client = DummyFirestore()
+        root = DummyCollection(client)
+        grandparent = DummyDocument("document", parent=root)
+        parent = DummyCollection(client, parent=grandparent)
+        modulename = "google.cloud.firestore_v1.watch"
+        pb2 = DummyPb2()
+        with mock.patch("%s.firestore_pb2" % modulename, pb2):
+            with mock.patch("%s.Watch.ResumableBidiRpc" % modulename, DummyRpc):
+                with mock.patch(
+                    "%s.Watch.BackgroundConsumer" % modulename, DummyBackgroundConsumer
+                ):
+                    query = DummyQuery(parent=parent)
                     inst = Watch.for_query(
                         query,
                         snapshot_callback,
@@ -693,18 +723,41 @@ def __init__(self, *document_path, **kw):
         self.__dict__.update(kw)
 
 
-class DummyQuery(object):  # pragma: NO COVER
-    def __init__(self, **kw):
-        if "client" not in kw:
-            self._client = DummyFirestore()
-        else:
-            self._client = kw["client"]
+class DummyDocument(object):
+    def __init__(self, name, parent):
+        self._name = name
+        self._parent = parent
 
-        if "comparator" not in kw:
-            # don't really do the comparison, just return 0 (equal) for all
-            self._comparator = lambda x, y: 1
-        else:
-            self._comparator = kw["comparator"]
+    @property
+    def _document_path(self):
+        return "{}/documents/{}".format(
+            self._parent._client._database_string, self._name
+        )
+
+
+class DummyCollection(object):
+    def __init__(self, client, parent=None):
+        self._client = client
+        self._parent = parent
+
+    def _parent_info(self):
+        if self._parent is None:
+            return "{}/documents".format(self._client._database_string), None
+        return self._parent._document_path, None
+
+
+def _compare(x, y):  # pragma: NO COVER
+    return 1
+
+
+class DummyQuery(object):
+    def __init__(self, parent):
+        self._comparator = _compare
+        self._parent = parent
+
+    @property
+    def _client(self):
+        return self._parent._client
 
     def _to_protobuf(self):
         return ""