Repository: googleapis/google-cloud-python

Issue:
[Firestore] Query.get returns incorrect type
Query.get returns an Array of DocumentSnapshots, not a QuerySnapshot. QuerySnapshot are a requirement for Watch.



Discussion:
@mcdonc what is the status of this?
https://github.com/googleapis/google-cloud-python/blob/master/firestore/google/cloud/firestore_v1beta1/query.py#L698

This is currently returning an iterable of DocumentSnapshot. The request from the firestore team is that, to match node, this would return a QuerySnapshot. QuerySnapshots contain a list of 'docs', list of 'changes', and a 'readTime'.

Here is the corresponding class in node. https://github.com/googleapis/nodejs-firestore/blob/master/types/firestore.d.ts#L995

If were to take this change, I think it would make sense to enumerate the QueryDocumentSnapshots (docs). Query document snapshot is an extended class of DocumentSnapshot, though I am not 100% sure we need this as it doesn't seem to add additional data. We can likely just use DocumentSnapshot.
@crwilcox From my read of the [API docs](https://cloud.google.com/firestore/docs/reference/rpc/google.firestore.v1beta1#google.firestore.v1beta1.RunQueryResponse), the snapshot should be an iterator over the stream of responses of .  *Each* response has:

- a single `Document` message (mapped onto our `DocumentSnapshot`
- a `read_time` (we currently copy this into the snapshot constructor)
- a count of `skipped_results` between the last response and the current one (normally set only on the first response when the user passed in a `start_at`).

There are no `changes` available here:  that notion is relevant only to `listen`, I think.
In Python, `Query.get` is a parallel for Node's `Query.stream`.  Should we rename it to match (breaking existing users?)  Nobody needs the equivalent of Node's `Query.get`, AFAICT, exept the listen / watch implementation, which already works around its absence.
@crwilcox As discussed in chat, I propose resolving this issue by:

- Renaming `Query.get` -> `Query.stream` (for conformance with Node's semantics).
- Leaving a docs-deprecated `Query.get` wrapper for `Query.stream`.

Later, if needed, we can add a `Query.get_snapshot` method with Node's `Query.get` semantics.

Resolving patch:
diff --git a/firestore/google/cloud/firestore_v1beta1/collection.py b/firestore/google/cloud/firestore_v1beta1/collection.py
--- a/firestore/google/cloud/firestore_v1beta1/collection.py
+++ b/firestore/google/cloud/firestore_v1beta1/collection.py
@@ -13,9 +13,8 @@
 # limitations under the License.
 
 """Classes for representing collections for the Google Cloud Firestore API."""
-
-
 import random
+import warnings
 
 import six
 
@@ -384,6 +383,15 @@ def end_at(self, document_fields):
         return query.end_at(document_fields)
 
     def get(self, transaction=None):
+        """Deprecated alias for :meth:`stream`."""
+        warnings.warn(
+            "'Collection.get' is deprecated:  please use 'Collection.stream' instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.stream(transaction=transaction)
+
+    def stream(self, transaction=None):
         """Read the documents in this collection.
 
         This sends a ``RunQuery`` RPC and then returns an iterator which
@@ -411,7 +419,7 @@ def get(self, transaction=None):
             document that fulfills the query.
         """
         query = query_mod.Query(self)
-        return query.get(transaction=transaction)
+        return query.stream(transaction=transaction)
 
     def on_snapshot(self, callback):
         """Monitor the documents in this collection.
diff --git a/firestore/google/cloud/firestore_v1beta1/query.py b/firestore/google/cloud/firestore_v1beta1/query.py
--- a/firestore/google/cloud/firestore_v1beta1/query.py
+++ b/firestore/google/cloud/firestore_v1beta1/query.py
@@ -18,10 +18,9 @@
 a :class:`~.firestore_v1beta1.collection.Collection` and that can be
 a more common way to create a query than direct usage of the constructor.
 """
-
-
 import copy
 import math
+import warnings
 
 from google.protobuf import wrappers_pb2
 import six
@@ -696,6 +695,15 @@ def _to_protobuf(self):
         return query_pb2.StructuredQuery(**query_kwargs)
 
     def get(self, transaction=None):
+        """Deprecated alias for :meth:`stream`."""
+        warnings.warn(
+            "'Query.get' is deprecated:  please use 'Query.stream' instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.stream(transaction=transaction)
+
+    def stream(self, transaction=None):
         """Read the documents in the collection that match this query.
 
         This sends a ``RunQuery`` RPC and then returns an iterator which


Test patch:
diff --git a/firestore/tests/system.py b/firestore/tests/system.py
--- a/firestore/tests/system.py
+++ b/firestore/tests/system.py
@@ -483,7 +483,7 @@ def test_collection_add(client, cleanup):
     assert set(collection3.list_documents()) == {document_ref5}
 
 
-def test_query_get(client, cleanup):
+def test_query_stream(client, cleanup):
     sub_collection = "child" + unique_resource_id("-")
     collection = client.collection("collek", "shun", sub_collection)
 
@@ -504,7 +504,7 @@ def test_query_get(client, cleanup):
 
     # 0. Limit to snapshots where ``a==1``.
     query0 = collection.where("a", "==", 1)
-    values0 = {snapshot.id: snapshot.to_dict() for snapshot in query0.get()}
+    values0 = {snapshot.id: snapshot.to_dict() for snapshot in query0.stream()}
     assert len(values0) == num_vals
     for key, value in six.iteritems(values0):
         assert stored[key] == value
@@ -512,7 +512,7 @@ def test_query_get(client, cleanup):
 
     # 1. Order by ``b``.
     query1 = collection.order_by("b", direction=query0.DESCENDING)
-    values1 = [(snapshot.id, snapshot.to_dict()) for snapshot in query1.get()]
+    values1 = [(snapshot.id, snapshot.to_dict()) for snapshot in query1.stream()]
     assert len(values1) == len(stored)
     b_vals1 = []
     for key, value in values1:
@@ -523,7 +523,7 @@ def test_query_get(client, cleanup):
 
     # 2. Limit to snapshots where ``stats.sum > 1`` (a field path).
     query2 = collection.where("stats.sum", ">", 4)
-    values2 = {snapshot.id: snapshot.to_dict() for snapshot in query2.get()}
+    values2 = {snapshot.id: snapshot.to_dict() for snapshot in query2.stream()}
     assert len(values2) == 10
     ab_pairs2 = set()
     for key, value in six.iteritems(values2):
@@ -546,7 +546,7 @@ def test_query_get(client, cleanup):
         .start_at({"a": num_vals - 2})
         .end_before({"a": num_vals - 1})
     )
-    values3 = [(snapshot.id, snapshot.to_dict()) for snapshot in query3.get()]
+    values3 = [(snapshot.id, snapshot.to_dict()) for snapshot in query3.stream()]
     assert len(values3) == num_vals
     for key, value in values3:
         assert stored[key] == value
@@ -555,13 +555,13 @@ def test_query_get(client, cleanup):
 
     # 4. Send a query with no results.
     query4 = collection.where("b", "==", num_vals + 100)
-    values4 = list(query4.get())
+    values4 = list(query4.stream())
     assert len(values4) == 0
 
     # 5. Select a subset of fields.
     query5 = collection.where("b", "<=", 1)
     query5 = query5.select(["a", "stats.product"])
-    values5 = {snapshot.id: snapshot.to_dict() for snapshot in query5.get()}
+    values5 = {snapshot.id: snapshot.to_dict() for snapshot in query5.stream()}
     assert len(values5) == num_vals * 2  # a ANY, b in (0, 1)
     for key, value in six.iteritems(values5):
         expected = {
@@ -573,7 +573,7 @@ def test_query_get(client, cleanup):
     # 6. Add multiple filters via ``where()``.
     query6 = collection.where("stats.product", ">", 5)
     query6 = query6.where("stats.product", "<", 10)
-    values6 = {snapshot.id: snapshot.to_dict() for snapshot in query6.get()}
+    values6 = {snapshot.id: snapshot.to_dict() for snapshot in query6.stream()}
 
     matching_pairs = [
         (a_val, b_val)
@@ -591,7 +591,7 @@ def test_query_get(client, cleanup):
     query7 = collection.where("b", "==", 2)
     offset = 3
     query7 = query7.offset(offset)
-    values7 = {snapshot.id: snapshot.to_dict() for snapshot in query7.get()}
+    values7 = {snapshot.id: snapshot.to_dict() for snapshot in query7.stream()}
     # NOTE: We don't check the ``a``-values, since that would require
     #       an ``order_by('a')``, which combined with the ``b == 2``
     #       filter would necessitate an index.
@@ -617,7 +617,7 @@ def test_query_unary(client, cleanup):
 
     # 0. Query for null.
     query0 = collection.where(field_name, "==", None)
-    values0 = list(query0.get())
+    values0 = list(query0.stream())
     assert len(values0) == 1
     snapshot0 = values0[0]
     assert snapshot0.reference._path == document0._path
@@ -625,7 +625,7 @@ def test_query_unary(client, cleanup):
 
     # 1. Query for a NAN.
     query1 = collection.where(field_name, "==", nan_val)
-    values1 = list(query1.get())
+    values1 = list(query1.stream())
     assert len(values1) == 1
     snapshot1 = values1[0]
     assert snapshot1.reference._path == document1._path
@@ -813,7 +813,7 @@ def on_snapshot(docs, changes, read_time):
         on_snapshot.called_count += 1
 
         # A snapshot should return the same thing as if a query ran now.
-        query_ran = db.collection(u"users").where("first", "==", u"Ada").get()
+        query_ran = db.collection(u"users").where("first", "==", u"Ada").stream()
         assert len(docs) == len([i for i in query_ran])
 
     on_snapshot.called_count = 0
@@ -856,7 +856,7 @@ def on_snapshot(docs, changes, read_time):
             if len(docs) != 5:
                 return
             # A snapshot should return the same thing as if a query ran now.
-            query_ran = query_ref.get()
+            query_ran = query_ref.stream()
             query_ran_results = [i for i in query_ran]
             assert len(docs) == len(query_ran_results)
 
diff --git a/firestore/tests/unit/test_collection.py b/firestore/tests/unit/test_collection.py
--- a/firestore/tests/unit/test_collection.py
+++ b/firestore/tests/unit/test_collection.py
@@ -497,24 +497,59 @@ def test_list_documents_w_page_size(self):
 
     @mock.patch("google.cloud.firestore_v1beta1.query.Query", autospec=True)
     def test_get(self, query_class):
+        import warnings
+
         collection = self._make_one("collection")
-        get_response = collection.get()
+        with warnings.catch_warnings(record=True) as warned:
+            get_response = collection.get()
 
         query_class.assert_called_once_with(collection)
         query_instance = query_class.return_value
-        self.assertIs(get_response, query_instance.get.return_value)
-        query_instance.get.assert_called_once_with(transaction=None)
+        self.assertIs(get_response, query_instance.stream.return_value)
+        query_instance.stream.assert_called_once_with(transaction=None)
+
+        # Verify the deprecation
+        self.assertEqual(len(warned), 1)
+        self.assertIs(warned[0].category, DeprecationWarning)
 
     @mock.patch("google.cloud.firestore_v1beta1.query.Query", autospec=True)
     def test_get_with_transaction(self, query_class):
+        import warnings
+
+        collection = self._make_one("collection")
+        transaction = mock.sentinel.txn
+        with warnings.catch_warnings(record=True) as warned:
+            get_response = collection.get(transaction=transaction)
+
+        query_class.assert_called_once_with(collection)
+        query_instance = query_class.return_value
+        self.assertIs(get_response, query_instance.stream.return_value)
+        query_instance.stream.assert_called_once_with(transaction=transaction)
+
+        # Verify the deprecation
+        self.assertEqual(len(warned), 1)
+        self.assertIs(warned[0].category, DeprecationWarning)
+
+    @mock.patch("google.cloud.firestore_v1beta1.query.Query", autospec=True)
+    def test_stream(self, query_class):
+        collection = self._make_one("collection")
+        stream_response = collection.stream()
+
+        query_class.assert_called_once_with(collection)
+        query_instance = query_class.return_value
+        self.assertIs(stream_response, query_instance.stream.return_value)
+        query_instance.stream.assert_called_once_with(transaction=None)
+
+    @mock.patch("google.cloud.firestore_v1beta1.query.Query", autospec=True)
+    def test_stream_with_transaction(self, query_class):
         collection = self._make_one("collection")
         transaction = mock.sentinel.txn
-        get_response = collection.get(transaction=transaction)
+        stream_response = collection.stream(transaction=transaction)
 
         query_class.assert_called_once_with(collection)
         query_instance = query_class.return_value
-        self.assertIs(get_response, query_instance.get.return_value)
-        query_instance.get.assert_called_once_with(transaction=transaction)
+        self.assertIs(stream_response, query_instance.stream.return_value)
+        query_instance.stream.assert_called_once_with(transaction=transaction)
 
     @mock.patch("google.cloud.firestore_v1beta1.collection.Watch", autospec=True)
     def test_on_snapshot(self, watch):
diff --git a/firestore/tests/unit/test_query.py b/firestore/tests/unit/test_query.py
--- a/firestore/tests/unit/test_query.py
+++ b/firestore/tests/unit/test_query.py
@@ -1014,6 +1014,52 @@ def test__to_protobuf_limit_only(self):
         self.assertEqual(structured_query_pb, expected_pb)
 
     def test_get_simple(self):
+        import warnings
+
+        # Create a minimal fake GAPIC.
+        firestore_api = mock.Mock(spec=["run_query"])
+
+        # Attach the fake GAPIC to a real client.
+        client = _make_client()
+        client._firestore_api_internal = firestore_api
+
+        # Make a **real** collection reference as parent.
+        parent = client.collection("dee")
+
+        # Add a dummy response to the minimal fake GAPIC.
+        _, expected_prefix = parent._parent_info()
+        name = "{}/sleep".format(expected_prefix)
+        data = {"snooze": 10}
+        response_pb = _make_query_response(name=name, data=data)
+        firestore_api.run_query.return_value = iter([response_pb])
+
+        # Execute the query and check the response.
+        query = self._make_one(parent)
+
+        with warnings.catch_warnings(record=True) as warned:
+            get_response = query.get()
+
+        self.assertIsInstance(get_response, types.GeneratorType)
+        returned = list(get_response)
+        self.assertEqual(len(returned), 1)
+        snapshot = returned[0]
+        self.assertEqual(snapshot.reference._path, ("dee", "sleep"))
+        self.assertEqual(snapshot.to_dict(), data)
+
+        # Verify the mock call.
+        parent_path, _ = parent._parent_info()
+        firestore_api.run_query.assert_called_once_with(
+            parent_path,
+            query._to_protobuf(),
+            transaction=None,
+            metadata=client._rpc_metadata,
+        )
+
+        # Verify the deprecation
+        self.assertEqual(len(warned), 1)
+        self.assertIs(warned[0].category, DeprecationWarning)
+
+    def test_stream_simple(self):
         # Create a minimal fake GAPIC.
         firestore_api = mock.Mock(spec=["run_query"])
 
@@ -1033,7 +1079,7 @@ def test_get_simple(self):
 
         # Execute the query and check the response.
         query = self._make_one(parent)
-        get_response = query.get()
+        get_response = query.stream()
         self.assertIsInstance(get_response, types.GeneratorType)
         returned = list(get_response)
         self.assertEqual(len(returned), 1)
@@ -1050,7 +1096,7 @@ def test_get_simple(self):
             metadata=client._rpc_metadata,
         )
 
-    def test_get_with_transaction(self):
+    def test_stream_with_transaction(self):
         # Create a minimal fake GAPIC.
         firestore_api = mock.Mock(spec=["run_query"])
 
@@ -1075,7 +1121,7 @@ def test_get_with_transaction(self):
 
         # Execute the query and check the response.
         query = self._make_one(parent)
-        get_response = query.get(transaction=transaction)
+        get_response = query.stream(transaction=transaction)
         self.assertIsInstance(get_response, types.GeneratorType)
         returned = list(get_response)
         self.assertEqual(len(returned), 1)
@@ -1091,7 +1137,7 @@ def test_get_with_transaction(self):
             metadata=client._rpc_metadata,
         )
 
-    def test_get_no_results(self):
+    def test_stream_no_results(self):
         # Create a minimal fake GAPIC with a dummy response.
         firestore_api = mock.Mock(spec=["run_query"])
         empty_response = _make_query_response()
@@ -1106,7 +1152,7 @@ def test_get_no_results(self):
         parent = client.collection("dah", "dah", "dum")
         query = self._make_one(parent)
 
-        get_response = query.get()
+        get_response = query.stream()
         self.assertIsInstance(get_response, types.GeneratorType)
         self.assertEqual(list(get_response), [])
 
@@ -1119,7 +1165,7 @@ def test_get_no_results(self):
             metadata=client._rpc_metadata,
         )
 
-    def test_get_second_response_in_empty_stream(self):
+    def test_stream_second_response_in_empty_stream(self):
         # Create a minimal fake GAPIC with a dummy response.
         firestore_api = mock.Mock(spec=["run_query"])
         empty_response1 = _make_query_response()
@@ -1135,7 +1181,7 @@ def test_get_second_response_in_empty_stream(self):
         parent = client.collection("dah", "dah", "dum")
         query = self._make_one(parent)
 
-        get_response = query.get()
+        get_response = query.stream()
         self.assertIsInstance(get_response, types.GeneratorType)
         self.assertEqual(list(get_response), [])
 
@@ -1148,7 +1194,7 @@ def test_get_second_response_in_empty_stream(self):
             metadata=client._rpc_metadata,
         )
 
-    def test_get_with_skipped_results(self):
+    def test_stream_with_skipped_results(self):
         # Create a minimal fake GAPIC.
         firestore_api = mock.Mock(spec=["run_query"])
 
@@ -1169,7 +1215,7 @@ def test_get_with_skipped_results(self):
 
         # Execute the query and check the response.
         query = self._make_one(parent)
-        get_response = query.get()
+        get_response = query.stream()
         self.assertIsInstance(get_response, types.GeneratorType)
         returned = list(get_response)
         self.assertEqual(len(returned), 1)
@@ -1186,7 +1232,7 @@ def test_get_with_skipped_results(self):
             metadata=client._rpc_metadata,
         )
 
-    def test_get_empty_after_first_response(self):
+    def test_stream_empty_after_first_response(self):
         # Create a minimal fake GAPIC.
         firestore_api = mock.Mock(spec=["run_query"])
 
@@ -1207,7 +1253,7 @@ def test_get_empty_after_first_response(self):
 
         # Execute the query and check the response.
         query = self._make_one(parent)
-        get_response = query.get()
+        get_response = query.stream()
         self.assertIsInstance(get_response, types.GeneratorType)
         returned = list(get_response)
         self.assertEqual(len(returned), 1)