Repository: googleapis/google-cloud-python

Issue:
Query should expose the value of `more_result` after query.fetch is called
A user that runs query.fetch(5) and gets 4 results will not know if they should call it again or if that is all the results.

Query should expose the value of `more_result` after query.fetch is called
A user that runs query.fetch(5) and gets 4 results will not know if they should call it again or if that is all the results.



Discussion:
We're getting mixed signals on this.

We were told in #280 that `more_results` is broken in the API and it is [noted](https://github.com/GoogleCloudPlatform/gcloud-python/blob/cf2b30658d45bce002f8198626ef8925d6b12654/gcloud/datastore/query.py#L355) in our code. This note was added in #287 and there is an accompanying discussion.

I don't believe it should be returned to the user:  that kind of state belongs on the query instance (like the cursor value).

We could choose to save the `more_results` value on the query (along with the `end_cursor`), and expose it via a method or property, even though we know (for now) that the back-end borks it:  after all, they might fix it in the future.

Yeah, exposing sounds better. Fixing the title of the issue.

I don't think it belongs on the `Query` object. It is a result of a query, not a query itself (response, not request).

Two possible solutions that come to mind
- Create a separate method `fetch_page` which returns `more_results`. (I prefer this)
- Add a flag to `fetch` which allows a tuple of things to be returned instead of just the fetched entities.

We're getting mixed signals on this.

We were told in #280 that `more_results` is broken in the API and it is [noted](https://github.com/GoogleCloudPlatform/gcloud-python/blob/cf2b30658d45bce002f8198626ef8925d6b12654/gcloud/datastore/query.py#L355) in our code. This note was added in #287 and there is an accompanying discussion.

I don't believe it should be returned to the user:  that kind of state belongs on the query instance (like the cursor value).

We could choose to save the `more_results` value on the query (along with the `end_cursor`), and expose it via a method or property, even though we know (for now) that the back-end borks it:  after all, they might fix it in the future.

Yeah, exposing sounds better. Fixing the title of the issue.

I don't think it belongs on the `Query` object. It is a result of a query, not a query itself (response, not request).

Two possible solutions that come to mind
- Create a separate method `fetch_page` which returns `more_results`. (I prefer this)
- Add a flag to `fetch` which allows a tuple of things to be returned instead of just the fetched entities.


Resolving patch:
diff --git a/gcloud/datastore/query.py b/gcloud/datastore/query.py
--- a/gcloud/datastore/query.py
+++ b/gcloud/datastore/query.py
@@ -69,7 +69,7 @@ def __init__(self, kind=None, dataset=None, namespace=None):
         self._dataset = dataset
         self._namespace = namespace
         self._pb = datastore_pb.Query()
-        self._cursor = None
+        self._cursor = self._more_results = None
         self._offset = 0
 
         if kind:
@@ -85,6 +85,7 @@ def _clone(self):
                                namespace=self._namespace)
         clone._pb.CopyFrom(self._pb)
         clone._cursor = self._cursor
+        clone._more_results = self._more_results
         return clone
 
     def namespace(self):
@@ -347,21 +348,22 @@ def fetch(self, limit=None):
             dataset_id=self.dataset().id(),
             namespace=self._namespace,
             )
-        # NOTE: `query_results` contains two extra values that we don't use,
-        #       namely `more_results` and `skipped_results`. The value of
-        #       `more_results` is unusable because it always returns an enum
+        # NOTE: `query_results` contains an extra value that we don't use,
+        #       namely `skipped_results`.
+        #
+        # NOTE: The value of `more_results` is not currently useful because
+        #       the back-end always returns an enum
         #       value of MORE_RESULTS_AFTER_LIMIT even if there are no more
         #       results. See
         #       https://github.com/GoogleCloudPlatform/gcloud-python/issues/280
         #       for discussion.
-        entity_pbs, end_cursor = query_results[:2]
+        entity_pbs, self._cursor, self._more_results = query_results[:3]
 
-        self._cursor = end_cursor
         return [helpers.entity_from_protobuf(entity, dataset=self.dataset())
                 for entity in entity_pbs]
 
     def cursor(self):
-        """Returns cursor ID
+        """Returns cursor ID from most recent ``fetch()``.
 
         .. warning:: Invoking this method on a query that has not yet
            been executed will raise a RuntimeError.
@@ -374,6 +376,28 @@ def cursor(self):
             raise RuntimeError('No cursor')
         return base64.b64encode(self._cursor)
 
+    def more_results(self):
+        """Returns ``more_results`` flag from most recent ``fetch()``.
+
+        .. warning:: Invoking this method on a query that has not yet
+           been executed will raise a RuntimeError.
+
+        .. note::
+
+           The `more_results` is not currently useful because it is
+           always returned by the back-end as ``MORE_RESULTS_AFTER_LIMIT``
+           even if there are no more results. See
+           https://github.com/GoogleCloudPlatform/gcloud-python/issues/280
+           for discussion.
+
+        :rtype: :class:`gcloud.datastore.datastore_v1_pb2.
+                                QueryResultBatch.MoreResultsType`
+        :returns: enumerated value:  are there more results available.
+        """
+        if self._more_results is None:
+            raise RuntimeError('No results')
+        return self._more_results
+
     def with_cursor(self, start_cursor, end_cursor=None):
         """Specifies the starting / ending positions in a query's result set.
 


Test patch:
diff --git a/gcloud/datastore/test_query.py b/gcloud/datastore/test_query.py
--- a/gcloud/datastore/test_query.py
+++ b/gcloud/datastore/test_query.py
@@ -51,10 +51,12 @@ def test__clone(self):
         _DATASET = 'DATASET'
         _KIND = 'KIND'
         _CURSOR = 'DEADBEEF'
+        _MORE_RESULTS = 2
         _NAMESPACE = 'NAMESPACE'
         dataset = Dataset(_DATASET)
         query = self._makeOne(_KIND, dataset, _NAMESPACE)
         query._cursor = _CURSOR
+        query._more_results = _MORE_RESULTS
         clone = query._clone()
         self.assertFalse(clone is query)
         self.assertTrue(isinstance(clone, self._getTargetClass()))
@@ -63,6 +65,7 @@ def test__clone(self):
         kq_pb, = list(clone.kind())
         self.assertEqual(kq_pb.name, _KIND)
         self.assertEqual(clone._cursor, _CURSOR)
+        self.assertEqual(clone._more_results, _MORE_RESULTS)
 
     def test_to_protobuf_empty(self):
         query = self._makeOne()
@@ -317,6 +320,7 @@ def test_fetch_default_limit(self):
         self.assertEqual(connection._called_with, expected_called_with)
 
     def test_fetch_explicit_limit(self):
+        import base64
         from gcloud.datastore.datastore_v1_pb2 import Entity
         _CURSOR = 'CURSOR'
         _DATASET = 'DATASET'
@@ -336,7 +340,8 @@ def test_fetch_explicit_limit(self):
         query = self._makeOne(_KIND, dataset, _NAMESPACE)
         limited = query.limit(13)
         entities = query.fetch(13)
-        self.assertEqual(query._cursor, _CURSOR)
+        self.assertEqual(query.cursor(), base64.b64encode(_CURSOR))
+        self.assertEqual(query.more_results(), connection._more)
         self.assertEqual(len(entities), 1)
         self.assertEqual(entities[0].key().path(),
                          [{'kind': _KIND, 'id': _ID}])
@@ -347,6 +352,24 @@ def test_fetch_explicit_limit(self):
         }
         self.assertEqual(connection._called_with, expected_called_with)
 
+    def test_more_results_not_fetched(self):
+        _DATASET = 'DATASET'
+        _KIND = 'KIND'
+        connection = _Connection()
+        dataset = _Dataset(_DATASET, connection)
+        query = self._makeOne(_KIND, dataset)
+        self.assertRaises(RuntimeError, query.more_results)
+
+    def test_more_results_fetched(self):
+        _MORE_RESULTS = 2
+        _DATASET = 'DATASET'
+        _KIND = 'KIND'
+        connection = _Connection()
+        dataset = _Dataset(_DATASET, connection)
+        query = self._makeOne(_KIND, dataset)
+        query._more_results = _MORE_RESULTS
+        self.assertEqual(query.more_results(), _MORE_RESULTS)
+
     def test_cursor_not_fetched(self):
         _DATASET = 'DATASET'
         _KIND = 'KIND'
@@ -564,7 +587,7 @@ def connection(self):
 class _Connection(object):
     _called_with = None
     _cursor = ''
-    _more = True
+    _more = 2
     _skipped = 0
 
     def __init__(self, *result):