123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 |
- import copy
- from mongoengine.errors import InvalidQueryError
- from mongoengine.queryset import transform
- __all__ = ('Q', 'QNode')
- class QNodeVisitor(object):
- """Base visitor class for visiting Q-object nodes in a query tree.
- """
- def visit_combination(self, combination):
- """Called by QCombination objects.
- """
- return combination
- def visit_query(self, query):
- """Called by (New)Q objects.
- """
- return query
- class DuplicateQueryConditionsError(InvalidQueryError):
- pass
- class SimplificationVisitor(QNodeVisitor):
- """Simplifies query trees by combining unnecessary 'and' connection nodes
- into a single Q-object.
- """
- def visit_combination(self, combination):
- if combination.operation == combination.AND:
- # The simplification only applies to 'simple' queries
- if all(isinstance(node, Q) for node in combination.children):
- queries = [n.query for n in combination.children]
- try:
- return Q(**self._query_conjunction(queries))
- except DuplicateQueryConditionsError:
- # Cannot be simplified
- pass
- return combination
- def _query_conjunction(self, queries):
- """Merges query dicts - effectively &ing them together.
- """
- query_ops = set()
- combined_query = {}
- for query in queries:
- ops = set(query.keys())
- # Make sure that the same operation isn't applied more than once
- # to a single field
- intersection = ops.intersection(query_ops)
- if intersection:
- raise DuplicateQueryConditionsError()
- query_ops.update(ops)
- combined_query.update(copy.deepcopy(query))
- return combined_query
- class QueryCompilerVisitor(QNodeVisitor):
- """Compiles the nodes in a query tree to a PyMongo-compatible query
- dictionary.
- """
- def __init__(self, document):
- self.document = document
- def visit_combination(self, combination):
- operator = '$and'
- if combination.operation == combination.OR:
- operator = '$or'
- return {operator: combination.children}
- def visit_query(self, query):
- return transform.query(self.document, **query.query)
- class QNode(object):
- """Base class for nodes in query trees."""
- AND = 0
- OR = 1
- def to_query(self, document):
- query = self.accept(SimplificationVisitor())
- query = query.accept(QueryCompilerVisitor(document))
- return query
- def accept(self, visitor):
- raise NotImplementedError
- def _combine(self, other, operation):
- """Combine this node with another node into a QCombination
- object.
- """
- if getattr(other, 'empty', True):
- return self
- if self.empty:
- return other
- return QCombination(operation, [self, other])
- @property
- def empty(self):
- return False
- def __or__(self, other):
- return self._combine(other, self.OR)
- def __and__(self, other):
- return self._combine(other, self.AND)
- class QCombination(QNode):
- """Represents the combination of several conditions by a given
- logical operator.
- """
- def __init__(self, operation, children):
- self.operation = operation
- self.children = []
- for node in children:
- # If the child is a combination of the same type, we can merge its
- # children directly into this combinations children
- if isinstance(node, QCombination) and node.operation == operation:
- self.children += node.children
- else:
- self.children.append(node)
- def __repr__(self):
- op = ' & ' if self.operation is self.AND else ' | '
- return '(%s)' % op.join([repr(node) for node in self.children])
- def accept(self, visitor):
- for i in range(len(self.children)):
- if isinstance(self.children[i], QNode):
- self.children[i] = self.children[i].accept(visitor)
- return visitor.visit_combination(self)
- @property
- def empty(self):
- return not bool(self.children)
- class Q(QNode):
- """A simple query object, used in a query tree to build up more complex
- query structures.
- """
- def __init__(self, **query):
- self.query = query
- def __repr__(self):
- return 'Q(**%s)' % repr(self.query)
- def accept(self, visitor):
- return visitor.visit_query(self)
- @property
- def empty(self):
- return not bool(self.query)
|