Skip to content

Commit 89ded75

Browse files
committed
Merge pull request #432 from dhermes/remove-cursor-from-query
Removing cursor and more results from query object.
2 parents 8a6feff + 1c1333e commit 89ded75

File tree

6 files changed

+190
-171
lines changed

6 files changed

+190
-171
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: 94 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ class Query(object):
5656
:param dataset: The namespace to which to restrict results.
5757
"""
5858

59+
_NOT_FINISHED = datastore_pb.QueryResultBatch.NOT_FINISHED
60+
_FINISHED = (
61+
datastore_pb.QueryResultBatch.NO_MORE_RESULTS,
62+
datastore_pb.QueryResultBatch.MORE_RESULTS_AFTER_LIMIT,
63+
)
5964
OPERATORS = {
6065
'<=': datastore_pb.PropertyFilter.LESS_THAN_OR_EQUAL,
6166
'>=': datastore_pb.PropertyFilter.GREATER_THAN_OR_EQUAL,
@@ -69,7 +74,6 @@ def __init__(self, kind=None, dataset=None, namespace=None):
6974
self._dataset = dataset
7075
self._namespace = namespace
7176
self._pb = datastore_pb.Query()
72-
self._cursor = self._more_results = None
7377
self._offset = 0
7478

7579
if kind:
@@ -84,8 +88,6 @@ def _clone(self):
8488
clone = self.__class__(dataset=self._dataset,
8589
namespace=self._namespace)
8690
clone._pb.CopyFrom(self._pb)
87-
clone._cursor = self._cursor
88-
clone._more_results = self._more_results
8991
return clone
9092

9193
def namespace(self):
@@ -228,29 +230,33 @@ def ancestor(self, ancestor):
228230

229231
return clone
230232

231-
def kind(self, *kinds):
233+
def kind(self, kind=None):
232234
"""Get or set the Kind of the Query.
233235
234-
.. note::
235-
This is an **additive** operation. That is, if the Query is
236-
set for kinds A and B, and you call ``.kind('C')``, it will
237-
query for kinds A, B, *and*, C.
238-
239-
:type kinds: string
240-
:param kinds: The entity kinds for which to query.
236+
:type kind: string
237+
:param kind: Optional. The entity kinds for which to query.
241238
242239
:rtype: string or :class:`Query`
243-
:returns: If no arguments, returns the kind.
244-
If a kind is provided, returns a clone of the :class:`Query`
245-
with those kinds set.
240+
:returns: If `kind` is None, returns the kind. If a kind is provided,
241+
returns a clone of the :class:`Query` with that kind set.
242+
:raises: `ValueError` from the getter if multiple kinds are set on
243+
the query.
246244
"""
247-
if kinds:
245+
if kind is not None:
246+
kinds = [kind]
248247
clone = self._clone()
249-
for kind in kinds:
250-
clone._pb.kind.add().name = kind
248+
clone._pb.ClearField('kind')
249+
for new_kind in kinds:
250+
clone._pb.kind.add().name = new_kind
251251
return clone
252252
else:
253-
return self._pb.kind
253+
# In the proto definition for Query, `kind` is repeated.
254+
kind_names = [kind_expr.name for kind_expr in self._pb.kind]
255+
num_kinds = len(kind_names)
256+
if num_kinds == 1:
257+
return kind_names[0]
258+
elif num_kinds > 1:
259+
raise ValueError('Only a single kind can be set.')
254260

255261
def limit(self, limit=None):
256262
"""Get or set the limit of the Query.
@@ -302,8 +308,12 @@ def dataset(self, dataset=None):
302308
else:
303309
return self._dataset
304310

305-
def fetch(self, limit=None):
306-
"""Executes the Query and returns all matching entities.
311+
def fetch_page(self, limit=None):
312+
"""Executes the Query and returns matching entities, and paging info.
313+
314+
In addition to the fetched entities, it also returns a cursor to allow
315+
paging through a results set and a boolean `more_results` indicating
316+
if there are any more.
307317
308318
This makes an API call to the Cloud Datastore, sends the Query
309319
as a protobuf, parses the responses to Entity protobufs, and
@@ -315,10 +325,10 @@ def fetch(self, limit=None):
315325
>>> from gcloud import datastore
316326
>>> dataset = datastore.get_dataset('dataset-id')
317327
>>> query = dataset.query('Person').filter('name', '=', 'Sally')
318-
>>> query.fetch()
319-
[<Entity object>, <Entity object>, ...]
320-
>>> query.fetch(1)
321-
[<Entity object>]
328+
>>> query.fetch_page()
329+
[<Entity object>, <Entity object>, ...], 'cursorbase64', True
330+
>>> query.fetch_page(1)
331+
[<Entity object>], 'cursorbase64', True
322332
>>> query.limit()
323333
None
324334
@@ -328,8 +338,13 @@ def fetch(self, limit=None):
328338
but the limit will be applied to the query
329339
before it is executed.
330340
331-
:rtype: list of :class:`gcloud.datastore.entity.Entity`'s
332-
:returns: The list of entities matching this query's criteria.
341+
:rtype: tuple of mixed types
342+
:returns: The first entry is a :class:`gcloud.datastore.entity.Entity`
343+
list matching this query's criteria. The second is a base64
344+
encoded cursor for paging and the third is a boolean
345+
indicating if there are more results.
346+
:raises: `ValueError` if more_results is not one of the enums
347+
NOT_FINISHED, MORE_RESULTS_AFTER_LIMIT, NO_MORE_RESULTS.
333348
"""
334349
clone = self
335350

@@ -350,46 +365,69 @@ def fetch(self, limit=None):
350365
# results. See
351366
# https://github.com/GoogleCloudPlatform/gcloud-python/issues/280
352367
# for discussion.
353-
entity_pbs, self._cursor, self._more_results = query_results[:3]
368+
entity_pbs, cursor_as_bytes, more_results_enum = query_results[:3]
354369

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

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

361-
.. warning:: Invoking this method on a query that has not yet
362-
been executed will raise a RuntimeError.
376+
if more_results_enum == self._NOT_FINISHED:
377+
more_results = True
378+
elif more_results_enum in self._FINISHED:
379+
more_results = False
380+
else:
381+
raise ValueError('Unexpected value returned for `more_results`.')
363382

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)
383+
return entities, cursor, more_results
371384

372-
def more_results(self):
373-
"""Returns ``more_results`` flag from most recent ``fetch()``.
385+
def fetch(self, limit=None):
386+
"""Executes the Query and returns matching entities
374387
375-
.. warning:: Invoking this method on a query that has not yet
376-
been executed will raise a RuntimeError.
388+
This calls `fetch_page()` but does not use the paging information.
377389
378-
.. note::
390+
For example::
391+
392+
>>> from gcloud import datastore
393+
>>> dataset = datastore.get_dataset('dataset-id')
394+
>>> query = dataset.query('Person').filter('name', '=', 'Sally')
395+
>>> query.fetch()
396+
[<Entity object>, <Entity object>, ...]
397+
>>> query.fetch(1)
398+
[<Entity object>]
399+
>>> query.limit()
400+
None
379401
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.
402+
:type limit: integer
403+
:param limit: An optional limit to apply temporarily to this query.
404+
That is, the Query itself won't be altered,
405+
but the limit will be applied to the query
406+
before it is executed.
385407
386-
:rtype: :class:`gcloud.datastore.datastore_v1_pb2.
387-
QueryResultBatch.MoreResultsType`
388-
:returns: enumerated value: are there more results available.
408+
:rtype: list of :class:`gcloud.datastore.entity.Entity`'s
409+
:returns: The list of entities matching this query's criteria.
389410
"""
390-
if self._more_results is None:
391-
raise RuntimeError('No results')
392-
return self._more_results
411+
entities, _, _ = self.fetch_page(limit=limit)
412+
return entities
413+
414+
@property
415+
def start_cursor(self):
416+
"""Property to encode start cursor bytes as base64."""
417+
if not self._pb.HasField('start_cursor'):
418+
return None
419+
420+
start_as_bytes = self._pb.start_cursor
421+
return base64.b64encode(start_as_bytes)
422+
423+
@property
424+
def end_cursor(self):
425+
"""Property to encode end cursor bytes as base64."""
426+
if not self._pb.HasField('end_cursor'):
427+
return None
428+
429+
end_as_bytes = self._pb.end_cursor
430+
return base64.b64encode(end_as_bytes)
393431

394432
def with_cursor(self, start_cursor, end_cursor=None):
395433
"""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)