Skip to content

Commit 74026f4

Browse files
committed
Reducing limit and offset over the life of a datastore query iterator.
In the process, also keeping tracking of the number of skipped results (so that we can update the offset). Fixes #1763.
1 parent b449451 commit 74026f4

3 files changed

Lines changed: 105 additions & 33 deletions

File tree

gcloud/datastore/connection.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,11 @@ def run_query(self, project, query_pb, namespace=None,
263263
:param transaction_id: If passed, make the request in the scope of
264264
the given transaction. Incompatible with
265265
``eventual==True``.
266+
267+
:rtype: tuple
268+
:returns: Four-tuple containing the entities returned,
269+
the end cursor of the query, a ``more_results``
270+
enum and a count of the number of skipped results.
266271
"""
267272
request = _datastore_pb2.RunQueryRequest()
268273
_set_read_options(request, eventual, transaction_id)

gcloud/datastore/query.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -363,8 +363,7 @@ class Iterator(object):
363363
:param limit: (Optional) Limit the number of results returned.
364364
365365
:type offset: integer
366-
:param offset: (Optional) Defaults to 0. Offset used to begin
367-
a query.
366+
:param offset: (Optional) Offset used to begin a query.
368367
369368
:type start_cursor: bytes
370369
:param start_cursor: (Optional) Cursor to begin paging through
@@ -380,9 +379,10 @@ class Iterator(object):
380379
_FINISHED = (
381380
_query_pb2.QueryResultBatch.NO_MORE_RESULTS,
382381
_query_pb2.QueryResultBatch.MORE_RESULTS_AFTER_LIMIT,
382+
_query_pb2.QueryResultBatch.MORE_RESULTS_AFTER_CURSOR,
383383
)
384384

385-
def __init__(self, query, client, limit=None, offset=0,
385+
def __init__(self, query, client, limit=None, offset=None,
386386
start_cursor=None, end_cursor=None):
387387
self._query = query
388388
self._client = client
@@ -391,6 +391,7 @@ def __init__(self, query, client, limit=None, offset=0,
391391
self._start_cursor = start_cursor
392392
self._end_cursor = end_cursor
393393
self._page = self._more_results = None
394+
self._skipped_results = None
394395

395396
def next_page(self):
396397
"""Fetch a single "page" of query results.
@@ -413,7 +414,8 @@ def next_page(self):
413414
if self._limit is not None:
414415
pb.limit.value = self._limit
415416

416-
pb.offset = self._offset
417+
if self._offset is not None:
418+
pb.offset = self._offset
417419

418420
transaction = self._client.current_transaction
419421

@@ -423,16 +425,8 @@ def next_page(self):
423425
namespace=self._query.namespace,
424426
transaction_id=transaction and transaction.id,
425427
)
426-
# NOTE: `query_results` contains an extra value that we don't use,
427-
# namely `skipped_results`.
428-
#
429-
# NOTE: The value of `more_results` is not currently useful because
430-
# the back-end always returns an enum
431-
# value of MORE_RESULTS_AFTER_LIMIT even if there are no more
432-
# results. See
433-
# https://github.com/GoogleCloudPlatform/gcloud-python/issues/280
434-
# for discussion.
435-
entity_pbs, cursor_as_bytes, more_results_enum = query_results[:3]
428+
(entity_pbs, cursor_as_bytes,
429+
more_results_enum, self._skipped_results) = query_results
436430

437431
if cursor_as_bytes == b'':
438432
self._start_cursor = None
@@ -457,13 +451,19 @@ def __iter__(self):
457451
458452
:rtype: sequence of :class:`gcloud.datastore.entity.Entity`
459453
"""
460-
self.next_page()
461454
while True:
455+
self.next_page()
462456
for entity in self._page:
463457
yield entity
464458
if not self._more_results:
465459
break
466-
self.next_page()
460+
num_results = len(self._page)
461+
if self._limit is not None:
462+
self._limit -= num_results
463+
if self._offset is not None and self._skipped_results is not None:
464+
# NOTE: The offset goes down relative to the location
465+
# because we are updating the cursor each time.
466+
self._offset -= self._skipped_results
467467

468468

469469
def _pb_from_query(query):

gcloud/datastore/test_query.py

Lines changed: 84 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -345,23 +345,31 @@ def _getTargetClass(self):
345345
def _makeOne(self, *args, **kw):
346346
return self._getTargetClass()(*args, **kw)
347347

348-
def _addQueryResults(self, connection, cursor=_END, more=False):
348+
def _addQueryResults(self, connection, cursor=_END, more=False,
349+
skipped_results=None, no_entity=False):
349350
from gcloud.datastore._generated import entity_pb2
350351
from gcloud.datastore._generated import query_pb2
351352
from gcloud.datastore.helpers import _new_value_pb
352353

353-
MORE = query_pb2.QueryResultBatch.NOT_FINISHED
354-
NO_MORE = query_pb2.QueryResultBatch.MORE_RESULTS_AFTER_LIMIT
354+
if more:
355+
more_enum = query_pb2.QueryResultBatch.NOT_FINISHED
356+
else:
357+
more_enum = query_pb2.QueryResultBatch.MORE_RESULTS_AFTER_LIMIT
355358
_ID = 123
356-
entity_pb = entity_pb2.Entity()
357-
entity_pb.key.partition_id.project_id = self._PROJECT
358-
path_element = entity_pb.key.path.add()
359-
path_element.kind = self._KIND
360-
path_element.id = _ID
361-
value_pb = _new_value_pb(entity_pb, 'foo')
362-
value_pb.string_value = u'Foo'
359+
if no_entity:
360+
entities = []
361+
else:
362+
entity_pb = entity_pb2.Entity()
363+
entity_pb.key.partition_id.project_id = self._PROJECT
364+
path_element = entity_pb.key.path.add()
365+
path_element.kind = self._KIND
366+
path_element.id = _ID
367+
value_pb = _new_value_pb(entity_pb, 'foo')
368+
value_pb.string_value = u'Foo'
369+
entities = [entity_pb]
370+
363371
connection._results.append(
364-
([entity_pb], cursor, MORE if more else NO_MORE))
372+
(entities, cursor, more_enum, skipped_results))
365373

366374
def _makeClient(self, connection=None):
367375
if connection is None:
@@ -374,7 +382,8 @@ def test_ctor_defaults(self):
374382
iterator = self._makeOne(query, connection)
375383
self.assertTrue(iterator._query is query)
376384
self.assertEqual(iterator._limit, None)
377-
self.assertEqual(iterator._offset, 0)
385+
self.assertEqual(iterator._offset, None)
386+
self.assertEqual(iterator._skipped_results, None)
378387

379388
def test_ctor_explicit(self):
380389
client = self._makeClient()
@@ -392,6 +401,7 @@ def test_next_page_no_cursors_no_more(self):
392401
self._addQueryResults(connection, cursor=b'')
393402
iterator = self._makeOne(query, client)
394403
entities, more_results, cursor = iterator.next_page()
404+
self.assertEqual(iterator._skipped_results, None)
395405

396406
self.assertEqual(cursor, None)
397407
self.assertFalse(more_results)
@@ -415,13 +425,16 @@ def test_next_page_no_cursors_no_more_w_offset_and_limit(self):
415425
connection = _Connection()
416426
client = self._makeClient(connection)
417427
query = _Query(client, self._KIND, self._PROJECT, self._NAMESPACE)
418-
self._addQueryResults(connection, cursor=b'')
428+
skipped_results = object()
429+
self._addQueryResults(connection, cursor=b'',
430+
skipped_results=skipped_results)
419431
iterator = self._makeOne(query, client, 13, 29)
420432
entities, more_results, cursor = iterator.next_page()
421433

422434
self.assertEqual(cursor, None)
423435
self.assertFalse(more_results)
424436
self.assertFalse(iterator._more_results)
437+
self.assertEqual(iterator._skipped_results, skipped_results)
425438
self.assertEqual(len(entities), 1)
426439
self.assertEqual(entities[0].key.path,
427440
[{'kind': self._KIND, 'id': self._ID}])
@@ -453,6 +466,7 @@ def test_next_page_w_cursors_w_more(self):
453466
self.assertEqual(cursor, urlsafe_b64encode(self._END))
454467
self.assertTrue(more_results)
455468
self.assertTrue(iterator._more_results)
469+
self.assertEqual(iterator._skipped_results, None)
456470
self.assertEqual(iterator._end_cursor, None)
457471
self.assertEqual(urlsafe_b64decode(iterator._start_cursor), self._END)
458472
self.assertEqual(len(entities), 1)
@@ -476,8 +490,8 @@ def test_next_page_w_cursors_w_bogus_more(self):
476490
client = self._makeClient(connection)
477491
query = _Query(client, self._KIND, self._PROJECT, self._NAMESPACE)
478492
self._addQueryResults(connection, cursor=self._END, more=True)
479-
epb, cursor, _ = connection._results.pop()
480-
connection._results.append((epb, cursor, 4)) # invalid enum
493+
epb, cursor, _, _ = connection._results.pop()
494+
connection._results.append((epb, cursor, 5, None)) # invalid enum
481495
iterator = self._makeOne(query, client)
482496
self.assertRaises(ValueError, iterator.next_page)
483497

@@ -523,9 +537,7 @@ def test___iter___w_more(self):
523537
[{'kind': self._KIND, 'id': self._ID}])
524538
self.assertEqual(entities[1]['foo'], u'Foo')
525539
qpb1 = _pb_from_query(query)
526-
qpb1.offset = 0
527540
qpb2 = _pb_from_query(query)
528-
qpb2.offset = 0
529541
qpb2.start_cursor = self._END
530542
EXPECTED1 = {
531543
'project': self._PROJECT,
@@ -543,6 +555,61 @@ def test___iter___w_more(self):
543555
self.assertEqual(connection._called_with[0], EXPECTED1)
544556
self.assertEqual(connection._called_with[1], EXPECTED2)
545557

558+
def test___iter___w_limit(self):
559+
from gcloud.datastore.query import _pb_from_query
560+
561+
connection = _Connection()
562+
client = self._makeClient(connection)
563+
query = _Query(client, self._KIND, self._PROJECT, self._NAMESPACE)
564+
skip1 = 4
565+
skip2 = 9
566+
self._addQueryResults(connection, more=True, skipped_results=skip1,
567+
no_entity=True)
568+
self._addQueryResults(connection, more=True, skipped_results=skip2)
569+
self._addQueryResults(connection)
570+
offset = skip1 + skip2
571+
iterator = self._makeOne(query, client, limit=2, offset=offset)
572+
entities = list(iterator)
573+
574+
self.assertFalse(iterator._more_results)
575+
self.assertEqual(len(entities), 2)
576+
for entity in entities:
577+
self.assertEqual(
578+
entity.key.path,
579+
[{'kind': self._KIND, 'id': self._ID}])
580+
qpb1 = _pb_from_query(query)
581+
qpb1.limit.value = 2
582+
qpb1.offset = offset
583+
qpb2 = _pb_from_query(query)
584+
qpb2.start_cursor = self._END
585+
qpb2.limit.value = 2
586+
qpb2.offset = offset - skip1
587+
qpb3 = _pb_from_query(query)
588+
qpb3.start_cursor = self._END
589+
qpb3.limit.value = 1
590+
EXPECTED1 = {
591+
'project': self._PROJECT,
592+
'query_pb': qpb1,
593+
'namespace': self._NAMESPACE,
594+
'transaction_id': None,
595+
}
596+
EXPECTED2 = {
597+
'project': self._PROJECT,
598+
'query_pb': qpb2,
599+
'namespace': self._NAMESPACE,
600+
'transaction_id': None,
601+
}
602+
EXPECTED3 = {
603+
'project': self._PROJECT,
604+
'query_pb': qpb3,
605+
'namespace': self._NAMESPACE,
606+
'transaction_id': None,
607+
}
608+
self.assertEqual(len(connection._called_with), 3)
609+
self.assertEqual(connection._called_with[0], EXPECTED1)
610+
self.assertEqual(connection._called_with[1], EXPECTED2)
611+
self.assertEqual(connection._called_with[2], EXPECTED3)
612+
546613

547614
class Test__pb_from_query(unittest2.TestCase):
548615

0 commit comments

Comments
 (0)