Skip to content

Commit

Permalink
Changing Query.filter to ask for operator and prop. name separately.
Browse files Browse the repository at this point in the history
Also adding a test for property names with spaces in them.

Fixes googleapis#424.
  • Loading branch information
dhermes committed Dec 17, 2014
1 parent 1b9f1c2 commit 0850558
Show file tree
Hide file tree
Showing 5 changed files with 52 additions and 45 deletions.
3 changes: 2 additions & 1 deletion gcloud/datastore/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,8 @@ def run_query(self, dataset_id, query_pb, namespace=None):
>>> from gcloud import datastore
>>> connection = datastore.get_connection()
>>> dataset = connection.dataset('dataset-id')
>>> query = dataset.query().kind('MyKind').filter('property =', 'val')
>>> query = dataset.query().kind('MyKind').filter(
... 'property', '=', 'val')
Using the `fetch`` method...
Expand Down
4 changes: 2 additions & 2 deletions gcloud/datastore/demo/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@
print(query.limit(2).fetch())

# Now let's check for Thing entities named 'Computer'
print(query.filter('name =', 'Computer').fetch())
print(query.filter('name', '=', 'Computer').fetch())

# If you want to filter by multiple attributes,
# you can string .filter() calls together.
print(query.filter('name =', 'Computer').filter('age =', 10).fetch())
print(query.filter('name', '=', 'Computer').filter('age', '=', 10).fetch())

# You can also work inside a transaction.
# (Check the official docs for explanations of what's happening here.)
Expand Down
41 changes: 17 additions & 24 deletions gcloud/datastore/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,58 +104,51 @@ def to_protobuf(self):
"""
return self._pb

def filter(self, expression, value):
"""Filter the query based on an expression and a value.
def filter(self, property_name, operator, value):
"""Filter the query based on a property name, operator and a value.
This will return a clone of the current :class:`Query`
filtered by the expression and value provided.
Expressions take the form of::
.filter('<property> <operator>', <value>)
.filter('<property>', '<operator>', <value>)
where property is a property stored on the entity in the datastore
and operator is one of ``OPERATORS``
(ie, ``=``, ``<``, ``<=``, ``>``, ``>=``)::
>>> query = Query('Person')
>>> filtered_query = query.filter('name =', 'James')
>>> filtered_query = query.filter('age >', 50)
>>> filtered_query = query.filter('name', '=', 'James')
>>> filtered_query = query.filter('age', '>', 50)
Because each call to ``.filter()`` returns a cloned ``Query`` object
we are able to string these together::
>>> query = Query('Person').filter(
... 'name =', 'James').filter('age >', 50)
... 'name', '=', 'James').filter('age', '>', 50)
:type expression: string
:param expression: An expression of a property and an
operator (ie, ``=``).
:type property_name: string
:param property_name: A property name.
:type operator: string
:param operator: One of ``=``, ``<``, ``<=``, ``>``, ``>=``.
:type value: integer, string, boolean, float, None, datetime
:param value: The value to filter on.
:rtype: :class:`Query`
:returns: A Query filtered by the expression and value provided.
:raises: `ValueError` if `operation` is not one of the specified
values.
"""
clone = self._clone()

# Take an expression like 'property >=', and parse it into
# useful pieces.
property_name, operator = None, None
expression = expression.strip()

# Use None to split on *any* whitespace.
expr_pieces = expression.rsplit(None, 1)
if len(expr_pieces) == 2:
property_name, operator = expr_pieces
property_name = property_name.strip()

# If no whitespace in `expression`, `operator` will be `None` and
# self.OPERATORS[None] will be `None` as well.
pb_op_enum = self.OPERATORS.get(operator)
if pb_op_enum is None:
raise ValueError('Invalid expression: "%s"' % expression)
error_message = 'Invalid expression: "%s"' % (operator,)
choices_message = 'Please use one of: =, <, <=, >, >=.'
raise ValueError(error_message, choices_message)

# Build a composite filter AND'd together.
composite_filter = clone._pb.filter.composite_filter
Expand Down Expand Up @@ -321,7 +314,7 @@ def fetch(self, limit=None):
>>> from gcloud import datastore
>>> dataset = datastore.get_dataset('dataset-id')
>>> query = dataset.query('Person').filter('name =', 'Sally')
>>> query = dataset.query('Person').filter('name', '=', 'Sally')
>>> query.fetch()
[<Entity object>, <Entity object>, ...]
>>> query.fetch(1)
Expand Down
41 changes: 27 additions & 14 deletions gcloud/datastore/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,15 @@ def test_to_protobuf_w_kind(self):
kq_pb, = list(q_pb.kind)
self.assertEqual(kq_pb.name, _KIND)

def test_filter_w_no_operator(self):
query = self._makeOne()
self.assertRaises(ValueError, query.filter, 'firstname', 'John')

def test_filter_w_unknown_operator(self):
query = self._makeOne()
self.assertRaises(ValueError, query.filter, 'firstname ~~', 'John')
self.assertRaises(ValueError, query.filter, 'firstname', '~~', 'John')

def test_filter_w_known_operator(self):
from gcloud.datastore import datastore_v1_pb2 as datastore_pb

query = self._makeOne()
after = query.filter('firstname =', u'John')
after = query.filter('firstname', '=', u'John')
self.assertFalse(after is query)
self.assertTrue(isinstance(after, self._getTargetClass()))
q_pb = after.to_protobuf()
Expand All @@ -108,11 +104,11 @@ def test_filter_w_all_operators(self):
from gcloud.datastore import datastore_v1_pb2 as datastore_pb

query = self._makeOne()
query = query.filter('leq_prop <=', u'val1')
query = query.filter('geq_prop >=', u'val2')
query = query.filter('lt_prop <', u'val3')
query = query.filter('gt_prop >', u'val4')
query = query.filter('eq_prop =', u'val5')
query = query.filter('leq_prop', '<=', u'val1')
query = query.filter('geq_prop', '>=', u'val2')
query = query.filter('lt_prop', '<', u'val3')
query = query.filter('gt_prop', '>', u'val4')
query = query.filter('eq_prop', '=', u'val5')

query_pb = query.to_protobuf()
pb_values = [
Expand All @@ -139,7 +135,7 @@ def test_filter_w_known_operator_and_entity(self):
other = Entity()
other['firstname'] = u'John'
other['lastname'] = u'Smith'
after = query.filter('other =', other)
after = query.filter('other', '=', other)
self.assertFalse(after is query)
self.assertTrue(isinstance(after, self._getTargetClass()))
q_pb = after.to_protobuf()
Expand All @@ -155,6 +151,23 @@ def test_filter_w_known_operator_and_entity(self):
self.assertEqual(props[1].name, 'lastname')
self.assertEqual(props[1].value.string_value, u'Smith')

def test_filter_w_whitespace_property_name(self):
from gcloud.datastore import datastore_v1_pb2 as datastore_pb

query = self._makeOne()
PROPERTY_NAME = ' property with lots of space '
after = query.filter(PROPERTY_NAME, '=', u'John')
self.assertFalse(after is query)
self.assertTrue(isinstance(after, self._getTargetClass()))
q_pb = after.to_protobuf()
self.assertEqual(q_pb.filter.composite_filter.operator,
datastore_pb.CompositeFilter.AND)
f_pb, = list(q_pb.filter.composite_filter.filter)
p_pb = f_pb.property_filter
self.assertEqual(p_pb.property.name, PROPERTY_NAME)
self.assertEqual(p_pb.value.string_value, u'John')
self.assertEqual(p_pb.operator, datastore_pb.PropertyFilter.EQUAL)

def test_ancestor_w_non_key_non_list(self):
query = self._makeOne()
self.assertRaises(TypeError, query.ancestor, object())
Expand All @@ -165,7 +178,7 @@ def test_ancestor_wo_existing_ancestor_query_w_key_and_propfilter(self):
_ID = 123
_NAME = u'NAME'
key = Key(path=[{'kind': _KIND, 'id': _ID}])
query = self._makeOne().filter('name =', _NAME)
query = self._makeOne().filter('name', '=', _NAME)
after = query.ancestor(key)
self.assertFalse(after is query)
self.assertTrue(isinstance(after, self._getTargetClass()))
Expand Down Expand Up @@ -226,7 +239,7 @@ def test_ancestor_clears_existing_ancestor_query_w_others(self):
_KIND = 'KIND'
_ID = 123
_NAME = u'NAME'
query = self._makeOne().filter('name =', _NAME)
query = self._makeOne().filter('name', '=', _NAME)
between = query.ancestor([_KIND, _ID])
after = between.ancestor(None)
self.assertFalse(after is query)
Expand Down
8 changes: 4 additions & 4 deletions regression/datastore.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def test_save_key_self_reference(self):
self.case_entities_to_delete.append(entity)

query = self.dataset.query('Person').filter(
'linkedTo =', key).limit(2)
'linkedTo', '=', key).limit(2)

stored_persons = query.fetch()
self.assertEqual(len(stored_persons), 1)
Expand Down Expand Up @@ -199,15 +199,15 @@ def test_limit_queries(self):
self.assertEqual(len(new_character_entities), characters_remaining)

def test_query_simple_filter(self):
query = self._base_query().filter('appearances >=', 20)
query = self._base_query().filter('appearances', '>=', 20)
expected_matches = 6
# We expect 6, but allow the query to get 1 extra.
entities = query.fetch(limit=expected_matches + 1)
self.assertEqual(len(entities), expected_matches)

def test_query_multiple_filters(self):
query = self._base_query().filter(
'appearances >=', 26).filter('family =', 'Stark')
'appearances', '>=', 26).filter('family', '=', 'Stark')
expected_matches = 4
# We expect 4, but allow the query to get 1 extra.
entities = query.fetch(limit=expected_matches + 1)
Expand All @@ -225,7 +225,7 @@ def test_query___key___filter(self):
rickard_key = datastore.key.Key(
path=[populate_datastore.ANCESTOR, populate_datastore.RICKARD])

query = self._base_query().filter('__key__ =', rickard_key)
query = self._base_query().filter('__key__', '=', rickard_key)
expected_matches = 1
# We expect 1, but allow the query to get 1 extra.
entities = query.fetch(limit=expected_matches + 1)
Expand Down

0 comments on commit 0850558

Please sign in to comment.