Skip to content

Commit 715f8d6

Browse files
committed
Removing cursor and more results from query object.
This is a follow up to a discussion in #423 and #425. Also - Adding fetch_page() method to return the needed properties no longer available on the Query object. - Made fetch_page() fail if more_results is `NOT_FINISHED`, which seems to be intended for batched requests. - Added start_cursor and end_cursor properties to Query. - Updated docstrings to reflect change - Updated tests to ensure the returned cursor was always set. - Streamlined fetch()/fetch_page() tests to run via a single tweakable helper. - Updated a datastore.connection test to construct and return an initialized QueryResultBatch to simulate a valid cursor. - Updated regression.clear_datastore and regression.datastore to reflect changed API surface.
1 parent c2ba130 commit 715f8d6

File tree

6 files changed

+171
-159
lines changed

6 files changed

+171
-159
lines changed

gcloud/datastore/connection.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -289,10 +289,13 @@ def run_query(self, dataset_id, query_pb, namespace=None):
289289
290290
Using the `fetch`` method...
291291
292-
>>> query.fetch()
292+
>>> entities, cursor, more_results = query.fetch_page()
293+
>>> entities
293294
[<list of Entity unmarshalled from protobuf>]
294-
>>> query.cursor()
295+
>>> cursor
295296
<string containing cursor where fetch stopped>
297+
>>> more_results
298+
<boolean of more results>
296299
297300
Under the hood this is doing...
298301
@@ -318,7 +321,7 @@ def run_query(self, dataset_id, query_pb, namespace=None):
318321
datastore_pb.RunQueryResponse)
319322
return (
320323
[e.entity for e in response.batch.entity_result],
321-
response.batch.end_cursor,
324+
response.batch.end_cursor, # Assume response always has cursor.
322325
response.batch.more_results,
323326
response.batch.skipped_results,
324327
)

gcloud/datastore/query.py

Lines changed: 83 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ class Query(object):
5656
:param dataset: The namespace to which to restrict results.
5757
"""
5858

59+
_MORE_RESULTS = datastore_pb.QueryResultBatch.MORE_RESULTS_AFTER_LIMIT
60+
_NO_MORE_RESULTS = datastore_pb.QueryResultBatch.NO_MORE_RESULTS
5961
OPERATORS = {
6062
'<=': datastore_pb.PropertyFilter.LESS_THAN_OR_EQUAL,
6163
'>=': datastore_pb.PropertyFilter.GREATER_THAN_OR_EQUAL,
@@ -69,7 +71,6 @@ def __init__(self, kind=None, dataset=None, namespace=None):
6971
self._dataset = dataset
7072
self._namespace = namespace
7173
self._pb = datastore_pb.Query()
72-
self._cursor = self._more_results = None
7374
self._offset = 0
7475

7576
if kind:
@@ -84,8 +85,6 @@ def _clone(self):
8485
clone = self.__class__(dataset=self._dataset,
8586
namespace=self._namespace)
8687
clone._pb.CopyFrom(self._pb)
87-
clone._cursor = self._cursor
88-
clone._more_results = self._more_results
8988
return clone
9089

9190
def namespace(self):
@@ -239,8 +238,8 @@ def kind(self, *kinds):
239238
:type kinds: string
240239
:param kinds: The entity kinds for which to query.
241240
242-
:rtype: string or :class:`Query`
243-
:returns: If no arguments, returns the kind.
241+
:rtype: string, list of strings, or :class:`Query`
242+
:returns: If no arguments, returns the kind or list of kinds.
244243
If a kind is provided, returns a clone of the :class:`Query`
245244
with those kinds set.
246245
"""
@@ -250,7 +249,13 @@ def kind(self, *kinds):
250249
clone._pb.kind.add().name = kind
251250
return clone
252251
else:
253-
return self._pb.kind
252+
# In the proto definition for Query, `kind` is repeated.
253+
kind_names = [kind_expr.name for kind_expr in self._pb.kind]
254+
num_kinds = len(kind_names)
255+
if num_kinds == 1:
256+
return kind_names[0]
257+
elif num_kinds > 1:
258+
return kind_names
254259

255260
def limit(self, limit=None):
256261
"""Get or set the limit of the Query.
@@ -302,8 +307,12 @@ def dataset(self, dataset=None):
302307
else:
303308
return self._dataset
304309

305-
def fetch(self, limit=None):
306-
"""Executes the Query and returns all matching entities.
310+
def fetch_page(self, limit=None):
311+
"""Executes the Query and returns matching entities, and paging info.
312+
313+
In addition to the fetched entities, it also returns a cursor to allow
314+
paging through a results set and a boolean `more_results` indicating
315+
if there are any more.
307316
308317
This makes an API call to the Cloud Datastore, sends the Query
309318
as a protobuf, parses the responses to Entity protobufs, and
@@ -315,10 +324,10 @@ def fetch(self, limit=None):
315324
>>> from gcloud import datastore
316325
>>> dataset = datastore.get_dataset('dataset-id')
317326
>>> query = dataset.query('Person').filter('name', '=', 'Sally')
318-
>>> query.fetch()
319-
[<Entity object>, <Entity object>, ...]
320-
>>> query.fetch(1)
321-
[<Entity object>]
327+
>>> query.fetch_page()
328+
[<Entity object>, <Entity object>, ...], 'cursorbase64', True
329+
>>> query.fetch_page(1)
330+
[<Entity object>], 'cursorbase64', True
322331
>>> query.limit()
323332
None
324333
@@ -328,8 +337,13 @@ def fetch(self, limit=None):
328337
but the limit will be applied to the query
329338
before it is executed.
330339
331-
:rtype: list of :class:`gcloud.datastore.entity.Entity`'s
332-
:returns: The list of entities matching this query's criteria.
340+
:rtype: tuple of mixed types
341+
:returns: The first entry is a :class:`gcloud.datastore.entity.Entity`
342+
list matching this query's criteria. The second is a base64
343+
encoded cursor for paging and the third is a boolean
344+
indicating if there are more results.
345+
:raises: `ValueError` if more_results is not one of the enums
346+
MORE_RESULTS_AFTER_LIMIT or NO_MORE_RESULTS.
333347
"""
334348
clone = self
335349

@@ -350,46 +364,71 @@ def fetch(self, limit=None):
350364
# results. See
351365
# https://github.com/GoogleCloudPlatform/gcloud-python/issues/280
352366
# for discussion.
353-
entity_pbs, self._cursor, self._more_results = query_results[:3]
367+
entity_pbs, cursor_as_bytes, more_results_enum = query_results[:3]
354368

355-
return [helpers.entity_from_protobuf(entity, dataset=self.dataset())
356-
for entity in entity_pbs]
369+
entities = [helpers.entity_from_protobuf(entity,
370+
dataset=self.dataset())
371+
for entity in entity_pbs]
357372

358-
def cursor(self):
359-
"""Returns cursor ID from most recent ``fetch()``.
373+
cursor = base64.b64encode(cursor_as_bytes)
360374

361-
.. warning:: Invoking this method on a query that has not yet
362-
been executed will raise a RuntimeError.
375+
if more_results_enum == self._MORE_RESULTS:
376+
more_results = True
377+
elif more_results_enum == self._NO_MORE_RESULTS:
378+
more_results = False
379+
else:
380+
# Note this covers the value NOT_FINISHED since this fetch does
381+
# not occur within a batch, we don't expect to see NOT_FINISHED.
382+
raise ValueError('Unexpected value returned for `more_results`.')
363383

364-
:rtype: string
365-
:returns: base64-encoded cursor ID string denoting the last position
366-
consumed in the query's result set.
367-
"""
368-
if not self._cursor:
369-
raise RuntimeError('No cursor')
370-
return base64.b64encode(self._cursor)
384+
return entities, cursor, more_results
371385

372-
def more_results(self):
373-
"""Returns ``more_results`` flag from most recent ``fetch()``.
386+
def fetch(self, limit=None):
387+
"""Executes the Query and returns matching entities
374388
375-
.. warning:: Invoking this method on a query that has not yet
376-
been executed will raise a RuntimeError.
389+
This calls `fetch_page()` but does not use the paging information.
377390
378-
.. note::
391+
For example::
392+
393+
>>> from gcloud import datastore
394+
>>> dataset = datastore.get_dataset('dataset-id')
395+
>>> query = dataset.query('Person').filter('name', '=', 'Sally')
396+
>>> query.fetch()
397+
[<Entity object>, <Entity object>, ...]
398+
>>> query.fetch(1)
399+
[<Entity object>]
400+
>>> query.limit()
401+
None
379402
380-
The `more_results` is not currently useful because it is
381-
always returned by the back-end as ``MORE_RESULTS_AFTER_LIMIT``
382-
even if there are no more results. See
383-
https://github.com/GoogleCloudPlatform/gcloud-python/issues/280
384-
for discussion.
403+
:type limit: integer
404+
:param limit: An optional limit to apply temporarily to this query.
405+
That is, the Query itself won't be altered,
406+
but the limit will be applied to the query
407+
before it is executed.
385408
386-
:rtype: :class:`gcloud.datastore.datastore_v1_pb2.
387-
QueryResultBatch.MoreResultsType`
388-
:returns: enumerated value: are there more results available.
409+
:rtype: list of :class:`gcloud.datastore.entity.Entity`'s
410+
:returns: The list of entities matching this query's criteria.
389411
"""
390-
if self._more_results is None:
391-
raise RuntimeError('No results')
392-
return self._more_results
412+
entities, _, _ = self.fetch_page(limit=limit)
413+
return entities
414+
415+
@property
416+
def start_cursor(self):
417+
"""Property to encode start cursor bytes as base64."""
418+
if not self._pb.HasField('start_cursor'):
419+
return None
420+
421+
start_as_bytes = self._pb.start_cursor
422+
return base64.b64encode(start_as_bytes)
423+
424+
@property
425+
def end_cursor(self):
426+
"""Property to encode end cursor bytes as base64."""
427+
if not self._pb.HasField('end_cursor'):
428+
return None
429+
430+
end_as_bytes = self._pb.end_cursor
431+
return base64.b64encode(end_as_bytes)
393432

394433
def with_cursor(self, start_cursor, end_cursor=None):
395434
"""Specifies the starting / ending positions in a query's result set.

gcloud/datastore/test_connection.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,8 +449,13 @@ def test_run_query_wo_namespace_empty_result(self):
449449

450450
DATASET_ID = 'DATASET'
451451
KIND = 'Nonesuch'
452+
CURSOR = b'\x00'
452453
q_pb = Query(KIND, DATASET_ID).to_protobuf()
453454
rsp_pb = datastore_pb.RunQueryResponse()
455+
rsp_pb.batch.end_cursor = CURSOR
456+
no_more = datastore_pb.QueryResultBatch.NO_MORE_RESULTS
457+
rsp_pb.batch.more_results = no_more
458+
rsp_pb.batch.entity_result_type = datastore_pb.EntityResult.FULL
454459
conn = self._makeOne()
455460
URI = '/'.join([
456461
conn.API_BASE_URL,
@@ -463,7 +468,7 @@ def test_run_query_wo_namespace_empty_result(self):
463468
http = conn._http = Http({'status': '200'}, rsp_pb.SerializeToString())
464469
pbs, end, more, skipped = conn.run_query(DATASET_ID, q_pb)
465470
self.assertEqual(pbs, [])
466-
self.assertEqual(end, '')
471+
self.assertEqual(end, CURSOR)
467472
self.assertTrue(more)
468473
self.assertEqual(skipped, 0)
469474
cw = http._called_with

0 commit comments

Comments
 (0)