Simplified Q objects

Removed QueryTreeTransformerVisitor (#98) (#171)
This commit is contained in:
Ross Lawley 2012-12-21 16:35:09 +00:00
parent 3aff461039
commit 1cdf71b647
3 changed files with 53 additions and 112 deletions

View File

@ -24,6 +24,7 @@ Changes in 0.8.X
- Added support for multiple slices and made slicing chainable. (#170) (#190) (#191) - Added support for multiple slices and made slicing chainable. (#170) (#190) (#191)
- Fixed GridFSProxy __getattr__ behaviour (#196) - Fixed GridFSProxy __getattr__ behaviour (#196)
- Fix Django timezone support (#151) - Fix Django timezone support (#151)
- Simplified Q objects, removed QueryTreeTransformerVisitor (#98) (#171)
Changes in 0.7.9 Changes in 0.7.9
================ ================

View File

@ -55,57 +55,6 @@ class SimplificationVisitor(QNodeVisitor):
return combined_query return combined_query
class QueryTreeTransformerVisitor(QNodeVisitor):
"""Transforms the query tree in to a form that may be used with MongoDB.
"""
def visit_combination(self, combination):
if combination.operation == combination.AND:
# MongoDB doesn't allow us to have too many $or operations in our
# queries, so the aim is to move the ORs up the tree to one
# 'master' $or. Firstly, we must find all the necessary parts (part
# of an AND combination or just standard Q object), and store them
# separately from the OR parts.
or_groups = []
and_parts = []
for node in combination.children:
if isinstance(node, QCombination):
if node.operation == node.OR:
# Any of the children in an $or component may cause
# the query to succeed
or_groups.append(node.children)
elif node.operation == node.AND:
and_parts.append(node)
elif isinstance(node, Q):
and_parts.append(node)
# Now we combine the parts into a usable query. AND together all of
# the necessary parts. Then for each $or part, create a new query
# that ANDs the necessary part with the $or part.
clauses = []
for or_group in product(*or_groups):
q_object = reduce(lambda a, b: a & b, and_parts, Q())
q_object = reduce(lambda a, b: a & b, or_group, q_object)
clauses.append(q_object)
# Finally, $or the generated clauses in to one query. Each of the
# clauses is sufficient for the query to succeed.
return reduce(lambda a, b: a | b, clauses, Q())
if combination.operation == combination.OR:
children = []
# Crush any nested ORs in to this combination as MongoDB doesn't
# support nested $or operations
for node in combination.children:
if (isinstance(node, QCombination) and
node.operation == combination.OR):
children += node.children
else:
children.append(node)
combination.children = children
return combination
class QueryCompilerVisitor(QNodeVisitor): class QueryCompilerVisitor(QNodeVisitor):
"""Compiles the nodes in a query tree to a PyMongo-compatible query """Compiles the nodes in a query tree to a PyMongo-compatible query
dictionary. dictionary.
@ -115,45 +64,14 @@ class QueryCompilerVisitor(QNodeVisitor):
self.document = document self.document = document
def visit_combination(self, combination): def visit_combination(self, combination):
operator = "$and"
if combination.operation == combination.OR: if combination.operation == combination.OR:
return {'$or': combination.children} operator = "$or"
elif combination.operation == combination.AND: return {operator: combination.children}
return self._mongo_query_conjunction(combination.children)
return combination
def visit_query(self, query): def visit_query(self, query):
return transform.query(self.document, **query.query) return transform.query(self.document, **query.query)
def _mongo_query_conjunction(self, queries):
"""Merges Mongo query dicts - effectively &ing them together.
"""
combined_query = {}
for query in queries:
for field, ops in query.items():
if field not in combined_query:
combined_query[field] = ops
else:
# The field is already present in the query the only way
# we can merge is if both the existing value and the new
# value are operation dicts, reject anything else
if (not isinstance(combined_query[field], dict) or
not isinstance(ops, dict)):
message = 'Conflicting values for ' + field
raise InvalidQueryError(message)
current_ops = set(combined_query[field].keys())
new_ops = set(ops.keys())
# Make sure that the same operation isn't applied more than
# once to a single field
intersection = current_ops.intersection(new_ops)
if intersection:
msg = 'Duplicate query conditions: '
raise InvalidQueryError(msg + ', '.join(intersection))
# Right! We've got two non-overlapping dicts of operations!
combined_query[field].update(copy.deepcopy(ops))
return combined_query
class QNode(object): class QNode(object):
"""Base class for nodes in query trees. """Base class for nodes in query trees.
@ -164,7 +82,6 @@ class QNode(object):
def to_query(self, document): def to_query(self, document):
query = self.accept(SimplificationVisitor()) query = self.accept(SimplificationVisitor())
query = query.accept(QueryTreeTransformerVisitor())
query = query.accept(QueryCompilerVisitor(document)) query = query.accept(QueryCompilerVisitor(document))
return query return query
@ -205,7 +122,8 @@ class QCombination(QNode):
# If the child is a combination of the same type, we can merge its # If the child is a combination of the same type, we can merge its
# children directly into this combinations children # children directly into this combinations children
if isinstance(node, QCombination) and node.operation == operation: if isinstance(node, QCombination) and node.operation == operation:
self.children += node.children # self.children += node.children
self.children.append(node)
else: else:
self.children.append(node) self.children.append(node)

View File

@ -115,29 +115,31 @@ class QTest(unittest.TestCase):
x = IntField() x = IntField()
y = BooleanField() y = BooleanField()
TestDoc.drop_collection()
query = (Q(x__gt=0) | Q(x__exists=False)) query = (Q(x__gt=0) | Q(x__exists=False))
query &= Q(x__lt=100) query &= Q(x__lt=100)
self.assertEqual(query.to_query(TestDoc), { self.assertEqual(query.to_query(TestDoc), {'$and': [
'$or': [ {'$or': [{'x': {'$gt': 0}},
{'x': {'$lt': 100, '$gt': 0}}, {'x': {'$exists': False}}]},
{'x': {'$lt': 100, '$exists': False}}, {'x': {'$lt': 100}}]
]
}) })
q1 = (Q(x__gt=0) | Q(x__exists=False)) q1 = (Q(x__gt=0) | Q(x__exists=False))
q2 = (Q(x__lt=100) | Q(y=True)) q2 = (Q(x__lt=100) | Q(y=True))
query = (q1 & q2).to_query(TestDoc) query = (q1 & q2).to_query(TestDoc)
self.assertEqual(['$or'], query.keys()) TestDoc(x=101).save()
conditions = [ TestDoc(x=10).save()
{'x': {'$lt': 100, '$gt': 0}}, TestDoc(y=True).save()
{'x': {'$lt': 100, '$exists': False}},
{'x': {'$gt': 0}, 'y': True}, self.assertEqual(query,
{'x': {'$exists': False}, 'y': True}, {'$and': [
] {'$or': [{'x': {'$gt': 0}}, {'x': {'$exists': False}}]},
self.assertEqual(len(conditions), len(query['$or'])) {'$or': [{'x': {'$lt': 100}}, {'y': True}]}
for condition in conditions: ]})
self.assertTrue(condition in query['$or'])
self.assertEqual(2, TestDoc.objects(q1 & q2).count())
def test_or_and_or_combination(self): def test_or_and_or_combination(self):
"""Ensure that Q-objects handle ORing ANDed ORed components. :) """Ensure that Q-objects handle ORing ANDed ORed components. :)
@ -146,20 +148,40 @@ class QTest(unittest.TestCase):
x = IntField() x = IntField()
y = BooleanField() y = BooleanField()
TestDoc.drop_collection()
TestDoc(x=-1, y=True).save()
TestDoc(x=101, y=True).save()
TestDoc(x=99, y=False).save()
TestDoc(x=101, y=False).save()
q1 = (Q(x__gt=0) & (Q(y=True) | Q(y__exists=False))) q1 = (Q(x__gt=0) & (Q(y=True) | Q(y__exists=False)))
q2 = (Q(x__lt=100) & (Q(y=False) | Q(y__exists=False))) q2 = (Q(x__lt=100) & (Q(y=False) | Q(y__exists=False)))
query = (q1 | q2).to_query(TestDoc) query = (q1 | q2).to_query(TestDoc)
self.assertEqual(['$or'], query.keys()) self.assertEqual(query,
conditions = [ {'$or': [
{'x': {'$gt': 0}, 'y': True}, {'$and': [{'x': {'$gt': 0}},
{'x': {'$gt': 0}, 'y': {'$exists': False}}, {'$or': [{'y': True}, {'y': {'$exists': False}}]}]},
{'x': {'$lt': 100}, 'y':False}, {'$and': [{'x': {'$lt': 100}},
{'x': {'$lt': 100}, 'y': {'$exists': False}}, {'$or': [{'y': False}, {'y': {'$exists': False}}]}]}
] ]}
self.assertEqual(len(conditions), len(query['$or'])) )
for condition in conditions:
self.assertTrue(condition in query['$or']) self.assertEqual(2, TestDoc.objects(q1 | q2).count())
def test_multiple_occurence_in_field(self):
class Test(Document):
name = StringField(max_length=40)
title = StringField(max_length=40)
q1 = Q(name__contains='te') | Q(title__contains='te')
q2 = Q(name__contains='12') | Q(title__contains='12')
q3 = q1 & q2
query = q3.to_query(Test)
self.assertEqual(query["$and"][0], q1.to_query(Test))
self.assertEqual(query["$and"][1], q2.to_query(Test))
def test_q_clone(self): def test_q_clone(self):