Skip to content

Commit c14498f

Browse files
authored
feat: Allow queries using server side IN. (#954)
* feat: Allow queries using server side IN. * Rename force_server to server_op.
1 parent 5e53016 commit c14498f

File tree

6 files changed

+69
-25
lines changed

6 files changed

+69
-25
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
">": query_pb2.PropertyFilter.Operator.GREATER_THAN,
5858
">=": query_pb2.PropertyFilter.Operator.GREATER_THAN_OR_EQUAL,
5959
"!=": query_pb2.PropertyFilter.Operator.NOT_EQUAL,
60-
"IN": query_pb2.PropertyFilter.Operator.IN,
60+
"in": query_pb2.PropertyFilter.Operator.IN,
6161
}
6262

6363
_KEY_NOT_IN_CACHE = object()

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,7 +1258,7 @@ def __ge__(self, value):
12581258
"""FilterNode: Represents the ``>=`` comparison."""
12591259
return self._comparison(">=", value)
12601260

1261-
def _IN(self, value):
1261+
def _IN(self, value, server_op=False):
12621262
"""For the ``in`` comparison operator.
12631263
12641264
The ``in`` operator cannot be overloaded in the way we want
@@ -1315,7 +1315,7 @@ def _IN(self, value):
13151315
sub_value = self._datastore_type(sub_value)
13161316
values.append(sub_value)
13171317

1318-
return query.FilterNode(self._name, "in", values)
1318+
return query.FilterNode(self._name, "in", values, server_op=server_op)
13191319

13201320
IN = _IN
13211321
"""Used to check if a property value is contained in a set of values.

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

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,7 @@ class FilterNode(Node):
619619
opsymbol (str): The comparison operator. One of ``=``, ``!=``, ``<``,
620620
``<=``, ``>``, ``>=`` or ``in``.
621621
value (Any): The value to filter on / relative to.
622+
server_op (bool): Force the operator to use a server side filter.
622623
623624
Raises:
624625
TypeError: If ``opsymbol`` is ``"in"`` but ``value`` is not a
@@ -630,7 +631,7 @@ class FilterNode(Node):
630631
_opsymbol = None
631632
_value = None
632633

633-
def __new__(cls, name, opsymbol, value):
634+
def __new__(cls, name, opsymbol, value, server_op=False):
634635
# Avoid circular import in Python 2.7
635636
from google.cloud.ndb import model
636637

@@ -648,7 +649,8 @@ def __new__(cls, name, opsymbol, value):
648649
return FalseNode()
649650
if len(nodes) == 1:
650651
return nodes[0]
651-
return DisjunctionNode(*nodes)
652+
if not server_op:
653+
return DisjunctionNode(*nodes)
652654

653655
instance = super(FilterNode, cls).__new__(cls)
654656
instance._name = name
@@ -695,24 +697,12 @@ def _to_filter(self, post=False):
695697
Optional[query_pb2.PropertyFilter]: Returns :data:`None`, if
696698
this is a post-filter, otherwise returns the protocol buffer
697699
representation of the filter.
698-
699-
Raises:
700-
NotImplementedError: If the ``opsymbol`` is ``in``, since
701-
they should correspond to a composite filter. This should
702-
never occur since the constructor will create ``OR`` nodes for
703-
``in``
704700
"""
705701
# Avoid circular import in Python 2.7
706702
from google.cloud.ndb import _datastore_query
707703

708704
if post:
709705
return None
710-
if self._opsymbol in (_IN_OP):
711-
raise NotImplementedError(
712-
"Inequality filters are not single filter "
713-
"expressions and therefore cannot be converted "
714-
"to a single filter ({!r})".format(self._opsymbol)
715-
)
716706

717707
return _datastore_query.make_filter(self._name, self._opsymbol, self._value)
718708

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,40 @@ def make_entities():
865865
assert not more
866866

867867

868+
@pytest.mark.usefixtures("client_context")
869+
def test_fetch_page_in_query(dispose_of):
870+
page_size = 5
871+
n_entities = page_size * 2
872+
873+
class SomeKind(ndb.Model):
874+
foo = ndb.IntegerProperty()
875+
876+
@ndb.toplevel
877+
def make_entities():
878+
entities = [SomeKind(foo=n_entities) for i in range(n_entities)]
879+
keys = yield [entity.put_async() for entity in entities]
880+
raise ndb.Return(keys)
881+
882+
for key in make_entities():
883+
dispose_of(key._key)
884+
885+
query = SomeKind.query().filter(SomeKind.foo.IN([1, 2, n_entities], server_op=True))
886+
eventually(query.fetch, length_equals(n_entities))
887+
888+
results, cursor, more = query.fetch_page(page_size)
889+
assert len(results) == page_size
890+
assert more
891+
892+
safe_cursor = cursor.urlsafe()
893+
next_cursor = ndb.Cursor(urlsafe=safe_cursor)
894+
results, cursor, more = query.fetch_page(page_size, start_cursor=next_cursor)
895+
assert len(results) == page_size
896+
897+
results, cursor, more = query.fetch_page(page_size, start_cursor=cursor)
898+
assert not results
899+
assert not more
900+
901+
868902
@pytest.mark.usefixtures("client_context")
869903
def test_polymodel_query(ds_entity):
870904
class Animal(ndb.PolyModel):

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ def test__IN_wrong_container():
549549
assert model.Property._FIND_METHODS_CACHE == {}
550550

551551
@staticmethod
552-
def test__IN():
552+
def test__IN_default():
553553
prop = model.Property("name", indexed=True)
554554
or_node = prop._IN(["a", None, "xy"])
555555
expected = query_module.DisjunctionNode(
@@ -561,6 +561,33 @@ def test__IN():
561561
# Also verify the alias
562562
assert or_node == prop.IN(["a", None, "xy"])
563563

564+
@staticmethod
565+
def test__IN_client():
566+
prop = model.Property("name", indexed=True)
567+
or_node = prop._IN(["a", None, "xy"], server_op=False)
568+
expected = query_module.DisjunctionNode(
569+
query_module.FilterNode("name", "=", "a"),
570+
query_module.FilterNode("name", "=", None),
571+
query_module.FilterNode("name", "=", "xy"),
572+
)
573+
assert or_node == expected
574+
# Also verify the alias
575+
assert or_node == prop.IN(["a", None, "xy"])
576+
577+
@staticmethod
578+
def test_server__IN():
579+
prop = model.Property("name", indexed=True)
580+
in_node = prop._IN(["a", None, "xy"], server_op=True)
581+
assert in_node == prop.IN(["a", None, "xy"], server_op=True)
582+
assert in_node != query_module.DisjunctionNode(
583+
query_module.FilterNode("name", "=", "a"),
584+
query_module.FilterNode("name", "=", None),
585+
query_module.FilterNode("name", "=", "xy"),
586+
)
587+
assert in_node == query_module.FilterNode(
588+
"name", "in", ["a", None, "xy"], server_op=True
589+
)
590+
564591
@staticmethod
565592
def test___neg__():
566593
prop = model.Property("name")

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

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -701,13 +701,6 @@ def test__to_ne_filter_op():
701701
filter_node = query_module.FilterNode("speed", "!=", 88)
702702
assert filter_node._to_filter(post=True) is None
703703

704-
@staticmethod
705-
def test__to_filter_bad_op():
706-
filter_node = query_module.FilterNode("speed", ">=", 88)
707-
filter_node._opsymbol = "in"
708-
with pytest.raises(NotImplementedError):
709-
filter_node._to_filter()
710-
711704
@staticmethod
712705
@mock.patch("google.cloud.ndb._datastore_query")
713706
def test__to_filter(_datastore_query):

0 commit comments

Comments
 (0)