Skip to content

Commit b950639

Browse files
author
Chris Rossi
authored
fix: replicate legacy behavior for using cache with queries (#613)
Corrects the issue pointed out in #586, that we weren't doing the right thing for deleted entities. Also corrects an issue noticed while fixing that, where the cache wasn't being updated with entities from queries. Behavior should now match legacy in both these regards. Fixes #586
1 parent 028ebc4 commit b950639

File tree

3 files changed

+192
-37
lines changed

3 files changed

+192
-37
lines changed

packages/google-cloud-ndb/google/cloud/ndb/_datastore_query.py

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,18 @@ def _next_batch(self):
379379
for result_pb in response.batch.entity_results
380380
]
381381

382+
if result_type == RESULT_TYPE_FULL:
383+
# If we cached a delete, remove it from the result set. This may come cause
384+
# some queries to return less than their limit even if there are more
385+
# results. As far as I can tell, that was also a possibility with the legacy
386+
# version.
387+
context = context_module.get_context()
388+
self._batch = [
389+
result
390+
for result in self._batch
391+
if result.check_cache(context) is not None
392+
]
393+
382394
self._has_next_batch = more_results = batch.more_results == NOT_FINISHED
383395

384396
self._more_results_after_limit = batch.more_results == MORE_RESULTS_AFTER_LIMIT
@@ -745,6 +757,8 @@ class _Result(object):
745757
order.
746758
"""
747759

760+
_key = None
761+
748762
def __init__(self, result_type, result_pb, order_by=None):
749763
self.result_type = result_type
750764
self.result_pb = result_pb
@@ -802,8 +816,38 @@ def _compare(self, other):
802816

803817
return 0
804818

819+
def key(self):
820+
"""Construct the key for this result.
821+
822+
Returns:
823+
key.Key: The key.
824+
"""
825+
if self._key is None:
826+
key_pb = self.result_pb.entity.key
827+
ds_key = helpers.key_from_protobuf(key_pb)
828+
self._key = key_module.Key._from_ds_key(ds_key)
829+
830+
return self._key
831+
832+
def check_cache(self, context):
833+
"""Check local context cache for entity.
834+
835+
Returns:
836+
Any: The NDB entity for this result, if it is cached, otherwise
837+
`_KEY_NOT_IN_CACHE`. May also return `None` if entity was deleted which
838+
will cause `None` to be recorded in the cache.
839+
"""
840+
key = self.key()
841+
if context._use_cache(key):
842+
try:
843+
return context.cache.get_and_validate(key)
844+
except KeyError:
845+
pass
846+
847+
return _KEY_NOT_IN_CACHE
848+
805849
def entity(self):
806-
"""Get an entity for an entity result. Use the cache if available.
850+
"""Get an entity for an entity result. Use or update the cache if available.
807851
808852
Args:
809853
projection (Optional[Sequence[str]]): Sequence of property names to
@@ -816,19 +860,12 @@ def entity(self):
816860
if self.result_type == RESULT_TYPE_FULL:
817861
# First check the cache.
818862
context = context_module.get_context()
819-
key_pb = self.result_pb.entity.key
820-
ds_key = helpers.key_from_protobuf(key_pb)
821-
key = key_module.Key._from_ds_key(ds_key)
822-
entity = _KEY_NOT_IN_CACHE
823-
use_cache = context._use_cache(key)
824-
if use_cache:
825-
try:
826-
entity = context.cache.get_and_validate(key)
827-
except KeyError:
828-
pass
829-
if entity is None or entity is _KEY_NOT_IN_CACHE:
830-
# entity not in cache, create one.
863+
entity = self.check_cache(context)
864+
if entity is _KEY_NOT_IN_CACHE:
865+
# entity not in cache, create one, and then add it to cache
831866
entity = model._entity_from_protobuf(self.result_pb.entity)
867+
if context._use_cache(entity.key):
868+
context.cache[entity.key] = entity
832869
return entity
833870

834871
elif self.result_type == RESULT_TYPE_PROJECTION:
@@ -838,10 +875,7 @@ def entity(self):
838875
return entity
839876

840877
elif self.result_type == RESULT_TYPE_KEY_ONLY:
841-
key_pb = self.result_pb.entity.key
842-
ds_key = helpers.key_from_protobuf(key_pb)
843-
key = key_module.Key._from_ds_key(ds_key)
844-
return key
878+
return self.key()
845879

846880
raise NotImplementedError("Got unexpected entity result type for query.")
847881

packages/google-cloud-ndb/tests/system/test_query.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1859,7 +1859,7 @@ class SomeKind(ndb.Model):
18591859
assert result.foo == index
18601860

18611861

1862-
def test_uncomitted_deletes(dispose_of, client_context):
1862+
def test_uncommitted_deletes(dispose_of, client_context):
18631863
"""Regression test for Issue #586
18641864
18651865
https://github.com/googleapis/python-ndb/issues/586
@@ -1878,7 +1878,24 @@ class SomeKind(ndb.Model):
18781878
@ndb.transactional()
18791879
def do_the_thing(key):
18801880
key.delete() # Will be cached but not committed when query runs
1881-
return SomeKind.query(SomeKind.foo == 42, ancestor=parent_key).get()
1881+
return SomeKind.query(SomeKind.foo == 42, ancestor=parent_key).fetch()
18821882

18831883
with client_context.new(cache_policy=None).use(): # Use default cache policy
1884-
assert do_the_thing(key).foo == 42
1884+
assert len(do_the_thing(key)) == 0
1885+
1886+
1887+
def test_query_updates_cache(dispose_of, client_context):
1888+
class SomeKind(ndb.Model):
1889+
foo = ndb.IntegerProperty()
1890+
1891+
entity = SomeKind(foo=42)
1892+
key = entity.put()
1893+
dispose_of(key._key)
1894+
eventually(SomeKind.query().fetch, length_equals(1))
1895+
1896+
with client_context.new(cache_policy=None).use(): # Use default cache policy
1897+
retrieved = SomeKind.query().get()
1898+
assert retrieved.foo == 42
1899+
1900+
# If there is a cache hit, we'll get back the same object, not just a copy
1901+
assert key.get() is retrieved

packages/google-cloud-ndb/tests/unit/test__datastore_query.py

Lines changed: 121 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -590,10 +590,28 @@ def test_probably_has_next_finished():
590590
@pytest.mark.usefixtures("in_context")
591591
@mock.patch("google.cloud.ndb._datastore_query._datastore_run_query")
592592
def test__next_batch(_datastore_run_query):
593+
entity1 = mock.Mock(
594+
key=entity_pb2.Key(
595+
partition_id=entity_pb2.PartitionId(project_id="testing"),
596+
path=[entity_pb2.Key.PathElement(kind="ThisKind", id=42)],
597+
)
598+
)
599+
entity2 = mock.Mock(
600+
key=entity_pb2.Key(
601+
partition_id=entity_pb2.PartitionId(project_id="testing"),
602+
path=[entity_pb2.Key.PathElement(kind="ThisKind", id=43)],
603+
)
604+
)
605+
entity3 = mock.Mock(
606+
key=entity_pb2.Key(
607+
partition_id=entity_pb2.PartitionId(project_id="testing"),
608+
path=[entity_pb2.Key.PathElement(kind="ThisKind", id=44)],
609+
)
610+
)
593611
entity_results = [
594-
mock.Mock(entity="entity1", cursor=b"a"),
595-
mock.Mock(entity="entity2", cursor=b"b"),
596-
mock.Mock(entity="entity3", cursor=b"c"),
612+
mock.Mock(entity=entity1, cursor=b"a"),
613+
mock.Mock(entity=entity2, cursor=b"b"),
614+
mock.Mock(entity=entity3, cursor=b"c"),
597615
]
598616
_datastore_run_query.return_value = utils.future_result(
599617
mock.Mock(
@@ -611,24 +629,91 @@ def test__next_batch(_datastore_run_query):
611629
assert iterator._next_batch().result() is None
612630
assert iterator._index == 0
613631
assert len(iterator._batch) == 3
614-
assert iterator._batch[0].result_pb.entity == "entity1"
632+
assert iterator._batch[0].result_pb.entity == entity1
615633
assert iterator._batch[0].result_type == query_pb2.EntityResult.FULL
616634
assert iterator._batch[0].order_by is None
617635
assert not iterator._has_next_batch
618636

637+
@staticmethod
638+
@mock.patch("google.cloud.ndb._datastore_query._datastore_run_query")
639+
def test__next_batch_cached_delete(_datastore_run_query, in_context):
640+
entity1 = mock.Mock(
641+
key=entity_pb2.Key(
642+
partition_id=entity_pb2.PartitionId(project_id="testing"),
643+
path=[entity_pb2.Key.PathElement(kind="ThisKind", id=42)],
644+
)
645+
)
646+
entity2 = mock.Mock(
647+
key=entity_pb2.Key(
648+
partition_id=entity_pb2.PartitionId(project_id="testing"),
649+
path=[entity_pb2.Key.PathElement(kind="ThisKind", id=43)],
650+
)
651+
)
652+
entity3 = mock.Mock(
653+
key=entity_pb2.Key(
654+
partition_id=entity_pb2.PartitionId(project_id="testing"),
655+
path=[entity_pb2.Key.PathElement(kind="ThisKind", id=44)],
656+
)
657+
)
658+
entity_results = [
659+
mock.Mock(entity=entity1, cursor=b"a"),
660+
mock.Mock(entity=entity2, cursor=b"b"),
661+
mock.Mock(entity=entity3, cursor=b"c"),
662+
]
663+
in_context.cache[key_module.Key("ThisKind", 43)] = None
664+
_datastore_run_query.return_value = utils.future_result(
665+
mock.Mock(
666+
batch=mock.Mock(
667+
entity_result_type=query_pb2.EntityResult.FULL,
668+
entity_results=entity_results,
669+
end_cursor=b"abc",
670+
more_results=query_pb2.QueryResultBatch.NO_MORE_RESULTS,
671+
)
672+
)
673+
)
674+
675+
query = query_module.QueryOptions()
676+
iterator = _datastore_query._QueryIteratorImpl(query)
677+
assert iterator._next_batch().result() is None
678+
assert iterator._index == 0
679+
assert len(iterator._batch) == 2
680+
assert iterator._batch[0].result_pb.entity == entity1
681+
assert iterator._batch[0].result_type == query_pb2.EntityResult.FULL
682+
assert iterator._batch[0].order_by is None
683+
assert iterator._batch[1].result_pb.entity == entity3
684+
assert not iterator._has_next_batch
685+
619686
@staticmethod
620687
@pytest.mark.usefixtures("in_context")
621688
@mock.patch("google.cloud.ndb._datastore_query._datastore_run_query")
622689
def test__next_batch_has_more(_datastore_run_query):
690+
entity1 = mock.Mock(
691+
key=entity_pb2.Key(
692+
partition_id=entity_pb2.PartitionId(project_id="testing"),
693+
path=[entity_pb2.Key.PathElement(kind="ThisKind", id=42)],
694+
)
695+
)
696+
entity2 = mock.Mock(
697+
key=entity_pb2.Key(
698+
partition_id=entity_pb2.PartitionId(project_id="testing"),
699+
path=[entity_pb2.Key.PathElement(kind="ThisKind", id=43)],
700+
)
701+
)
702+
entity3 = mock.Mock(
703+
key=entity_pb2.Key(
704+
partition_id=entity_pb2.PartitionId(project_id="testing"),
705+
path=[entity_pb2.Key.PathElement(kind="ThisKind", id=44)],
706+
)
707+
)
623708
entity_results = [
624-
mock.Mock(entity="entity1", cursor=b"a"),
625-
mock.Mock(entity="entity2", cursor=b"b"),
626-
mock.Mock(entity="entity3", cursor=b"c"),
709+
mock.Mock(entity=entity1, cursor=b"a"),
710+
mock.Mock(entity=entity2, cursor=b"b"),
711+
mock.Mock(entity=entity3, cursor=b"c"),
627712
]
628713
_datastore_run_query.return_value = utils.future_result(
629714
mock.Mock(
630715
batch=mock.Mock(
631-
entity_result_type=query_pb2.EntityResult.FULL,
716+
entity_result_type=query_pb2.EntityResult.PROJECTION,
632717
entity_results=entity_results,
633718
end_cursor=b"abc",
634719
more_results=query_pb2.QueryResultBatch.NOT_FINISHED,
@@ -641,8 +726,8 @@ def test__next_batch_has_more(_datastore_run_query):
641726
assert iterator._next_batch().result() is None
642727
assert iterator._index == 0
643728
assert len(iterator._batch) == 3
644-
assert iterator._batch[0].result_pb.entity == "entity1"
645-
assert iterator._batch[0].result_type == query_pb2.EntityResult.FULL
729+
assert iterator._batch[0].result_pb.entity == entity1
730+
assert iterator._batch[0].result_type == query_pb2.EntityResult.PROJECTION
646731
assert iterator._batch[0].order_by is None
647732
assert iterator._has_next_batch
648733
assert iterator._query.start_cursor.cursor == b"abc"
@@ -655,10 +740,28 @@ def test__next_batch_has_more_w_offset_and_limit(_datastore_run_query):
655740
656741
https://github.com/googleapis/python-ndb/issues/236
657742
"""
743+
entity1 = mock.Mock(
744+
key=entity_pb2.Key(
745+
partition_id=entity_pb2.PartitionId(project_id="testing"),
746+
path=[entity_pb2.Key.PathElement(kind="ThisKind", id=42)],
747+
)
748+
)
749+
entity2 = mock.Mock(
750+
key=entity_pb2.Key(
751+
partition_id=entity_pb2.PartitionId(project_id="testing"),
752+
path=[entity_pb2.Key.PathElement(kind="ThisKind", id=43)],
753+
)
754+
)
755+
entity3 = mock.Mock(
756+
key=entity_pb2.Key(
757+
partition_id=entity_pb2.PartitionId(project_id="testing"),
758+
path=[entity_pb2.Key.PathElement(kind="ThisKind", id=44)],
759+
)
760+
)
658761
entity_results = [
659-
mock.Mock(entity="entity1", cursor=b"a"),
660-
mock.Mock(entity="entity2", cursor=b"b"),
661-
mock.Mock(entity="entity3", cursor=b"c"),
762+
mock.Mock(entity=entity1, cursor=b"a"),
763+
mock.Mock(entity=entity2, cursor=b"b"),
764+
mock.Mock(entity=entity3, cursor=b"c"),
662765
]
663766
_datastore_run_query.return_value = utils.future_result(
664767
mock.Mock(
@@ -677,7 +780,7 @@ def test__next_batch_has_more_w_offset_and_limit(_datastore_run_query):
677780
assert iterator._next_batch().result() is None
678781
assert iterator._index == 0
679782
assert len(iterator._batch) == 3
680-
assert iterator._batch[0].result_pb.entity == "entity1"
783+
assert iterator._batch[0].result_pb.entity == entity1
681784
assert iterator._batch[0].result_type == query_pb2.EntityResult.FULL
682785
assert iterator._batch[0].order_by is None
683786
assert iterator._has_next_batch
@@ -1466,15 +1569,16 @@ def test_entity_full_entity(model):
14661569
partition_id=entity_pb2.PartitionId(project_id="testing"),
14671570
path=[entity_pb2.Key.PathElement(kind="ThisKind", id=42)],
14681571
)
1469-
entity = mock.Mock(key=key_pb)
1572+
entity_pb = mock.Mock(key=key_pb)
1573+
entity = mock.Mock(key=key_module.Key("ThisKind", 42))
14701574
model._entity_from_protobuf.return_value = entity
14711575
result = _datastore_query._Result(
14721576
_datastore_query.RESULT_TYPE_FULL,
1473-
mock.Mock(entity=entity, cursor=b"123", spec=("entity", "cursor")),
1577+
mock.Mock(entity=entity_pb, cursor=b"123", spec=("entity", "cursor")),
14741578
)
14751579

14761580
assert result.entity() is entity
1477-
model._entity_from_protobuf.assert_called_once_with(entity)
1581+
model._entity_from_protobuf.assert_called_once_with(entity_pb)
14781582

14791583
@staticmethod
14801584
@pytest.mark.usefixtures("in_context")

0 commit comments

Comments
 (0)