Repository: googleapis/google-cloud-python

Issue:
Firestore: collection_group query with start_after passing a snapshot
Problem: start_after() does not work for collection group queries when passing a document snapshot.

#### Environment details

python 3.7
google-cloud-firestore version 1.2

#### Steps to reproduce

  1. Setup a Google Cloud project with Firestore in Native Mode.

#### Code example

```
parent_ref = db.collection('parents').document('parent')
parent_ref.set({})

child_data = {'name': 'Tom'}
child_ref = parent_ref.collection('children').document('child')
child_ref.set(child_data)

# works
db.collection_group('children').where('name', '==', 'Tom').stream()

# works
db.collection_group('children').where('name', '==', 'Tom').start_after(child_data).stream()

# does not work
child_snapshot = child_ref.get()
db.collection_group('children').where('name', '==', 'Tom').end_before(child_snapshot).stream()
```

#### Stack trace
```
Traceback (most recent call last):
  File "C:/Users/tombr/Documents/GitHub/trader-python/scripts/gcloud/firestore/test.py", line 22, in <module>
    db.collection_group('children').where('name', '==', 'Tom').end_before(child_snapshot).stream()
  File "C:\Users\tombr\Documents\GitHub\trader-python\venv\lib\site-packages\google\cloud\firestore_v1\query.py", line 529, in end_before
    return self._cursor_helper(document_fields, before=True, start=False)
  File "C:\Users\tombr\Documents\GitHub\trader-python\venv\lib\site-packages\google\cloud\firestore_v1\query.py", line 420, in _cursor_helper
    "Cannot use snapshot from another collection as a cursor."
ValueError: Cannot use snapshot from another collection as a cursor.
```



Discussion:
@BenWhitehead Should `start_after` work in this case?
After a bit of testing, in the java client both `start_after` and `end_before` appear to be handled by the backend just fine with a port of this particular example. I'll check with someone from firestore to ensure that it is supposed to work.
@BenWhitehead Maybe we need to add a conformance test that asserts it is OK.
I got confirmation that the python client should not be erroring in this scenario. Collection group queries are designed to span collections and thus shouldn't restrict the DocumentSnapshot based on what collection it comes from.

I've added an internal bug to create a new conformance-test for this scenario [b/138785241](http://b/138785241)

@tseaver Can you relax the client validation so that this can work?
/cc @crwilcox 

Resolving patch:
diff --git a/firestore/google/cloud/firestore_v1/query.py b/firestore/google/cloud/firestore_v1/query.py
--- a/firestore/google/cloud/firestore_v1/query.py
+++ b/firestore/google/cloud/firestore_v1/query.py
@@ -390,6 +390,19 @@ def offset(self, num_to_skip):
             all_descendants=self._all_descendants,
         )
 
+    def _check_snapshot(self, document_fields):
+        """Validate local snapshots for non-collection-group queries.
+
+        Raises:
+            ValueError: for non-collection-group queries, if the snapshot
+                is from a different collection.
+        """
+        if self._all_descendants:
+            return
+
+        if document_fields.reference._path[:-1] != self._parent._path:
+            raise ValueError("Cannot use snapshot from another collection as a cursor.")
+
     def _cursor_helper(self, document_fields, before, start):
         """Set values to be used for a ``start_at`` or ``end_at`` cursor.
 
@@ -419,10 +432,7 @@ def _cursor_helper(self, document_fields, before, start):
         if isinstance(document_fields, tuple):
             document_fields = list(document_fields)
         elif isinstance(document_fields, document.DocumentSnapshot):
-            if document_fields.reference._path[:-1] != self._parent._path:
-                raise ValueError(
-                    "Cannot use snapshot from another collection as a cursor."
-                )
+            self._check_snapshot(document_fields)
         else:
             # NOTE: We copy so that the caller can't modify after calling.
             document_fields = copy.deepcopy(document_fields)


Test patch:
diff --git a/firestore/tests/unit/v1/test_query.py b/firestore/tests/unit/v1/test_query.py
--- a/firestore/tests/unit/v1/test_query.py
+++ b/firestore/tests/unit/v1/test_query.py
@@ -459,6 +459,28 @@ def test__cursor_helper_w_snapshot_wrong_collection(self):
         with self.assertRaises(ValueError):
             query._cursor_helper(snapshot, False, False)
 
+    def test__cursor_helper_w_snapshot_other_collection_all_descendants(self):
+        values = {"a": 7, "b": "foo"}
+        docref = self._make_docref("there", "doc_id")
+        snapshot = self._make_snapshot(docref, values)
+        collection = self._make_collection("here")
+        query1 = self._make_one(collection, all_descendants=True)
+
+        query2 = query1._cursor_helper(snapshot, False, False)
+
+        self.assertIs(query2._parent, collection)
+        self.assertIsNone(query2._projection)
+        self.assertEqual(query2._field_filters, ())
+        self.assertEqual(query2._orders, ())
+        self.assertIsNone(query2._limit)
+        self.assertIsNone(query2._offset)
+        self.assertIsNone(query2._start_at)
+
+        cursor, before = query2._end_at
+
+        self.assertIs(cursor, snapshot)
+        self.assertFalse(before)
+
     def test__cursor_helper_w_snapshot(self):
         values = {"a": 7, "b": "foo"}
         docref = self._make_docref("here", "doc_id")