226 lines
7.7 KiB
Python
226 lines
7.7 KiB
Python
"""
|
|
Code to manage the creation and SQL rendering of 'where' constraints.
|
|
"""
|
|
from django.db.models.sql.datastructures import EmptyResultSet
|
|
from django.utils.functional import cached_property
|
|
from django.utils import tree
|
|
|
|
|
|
# Connection types
|
|
AND = 'AND'
|
|
OR = 'OR'
|
|
|
|
|
|
class EmptyShortCircuit(Exception):
|
|
"""
|
|
Internal exception used to indicate that a "matches nothing" node should be
|
|
added to the where-clause.
|
|
"""
|
|
pass
|
|
|
|
|
|
class WhereNode(tree.Node):
|
|
"""
|
|
Used to represent the SQL where-clause.
|
|
|
|
The class is tied to the Query class that created it (in order to create
|
|
the correct SQL).
|
|
|
|
A child is usually an expression producing boolean values. Most likely the
|
|
expression is a Lookup instance, but other types of objects fulfilling the
|
|
required API could be used too (for example, sql.where.EverythingNode).
|
|
|
|
However, a child could also be any class with as_sql() and either
|
|
relabeled_clone() method or relabel_aliases() and clone() methods. The
|
|
second alternative should be used if the alias is not the only mutable
|
|
variable.
|
|
"""
|
|
default = AND
|
|
|
|
def as_sql(self, compiler, connection):
|
|
"""
|
|
Returns the SQL version of the where clause and the value to be
|
|
substituted in. Returns '', [] if this node matches everything,
|
|
None, [] if this node is empty, and raises EmptyResultSet if this
|
|
node can't match anything.
|
|
"""
|
|
# Note that the logic here is made slightly more complex than
|
|
# necessary because there are two kind of empty nodes: Nodes
|
|
# containing 0 children, and nodes that are known to match everything.
|
|
# A match-everything node is different than empty node (which also
|
|
# technically matches everything) for backwards compatibility reasons.
|
|
# Refs #5261.
|
|
result = []
|
|
result_params = []
|
|
everything_childs, nothing_childs = 0, 0
|
|
non_empty_childs = len(self.children)
|
|
|
|
for child in self.children:
|
|
try:
|
|
sql, params = compiler.compile(child)
|
|
except EmptyResultSet:
|
|
nothing_childs += 1
|
|
else:
|
|
if sql:
|
|
result.append(sql)
|
|
result_params.extend(params)
|
|
else:
|
|
if sql is None:
|
|
# Skip empty childs totally.
|
|
non_empty_childs -= 1
|
|
continue
|
|
everything_childs += 1
|
|
# Check if this node matches nothing or everything.
|
|
# First check the amount of full nodes and empty nodes
|
|
# to make this node empty/full.
|
|
if self.connector == AND:
|
|
full_needed, empty_needed = non_empty_childs, 1
|
|
else:
|
|
full_needed, empty_needed = 1, non_empty_childs
|
|
# Now, check if this node is full/empty using the
|
|
# counts.
|
|
if empty_needed - nothing_childs <= 0:
|
|
if self.negated:
|
|
return '', []
|
|
else:
|
|
raise EmptyResultSet
|
|
if full_needed - everything_childs <= 0:
|
|
if self.negated:
|
|
raise EmptyResultSet
|
|
else:
|
|
return '', []
|
|
|
|
if non_empty_childs == 0:
|
|
# All the child nodes were empty, so this one is empty, too.
|
|
return None, []
|
|
conn = ' %s ' % self.connector
|
|
sql_string = conn.join(result)
|
|
if sql_string:
|
|
if self.negated:
|
|
# Some backends (Oracle at least) need parentheses
|
|
# around the inner SQL in the negated case, even if the
|
|
# inner SQL contains just a single expression.
|
|
sql_string = 'NOT (%s)' % sql_string
|
|
elif len(result) > 1:
|
|
sql_string = '(%s)' % sql_string
|
|
return sql_string, result_params
|
|
|
|
def get_group_by_cols(self):
|
|
cols = []
|
|
for child in self.children:
|
|
cols.extend(child.get_group_by_cols())
|
|
return cols
|
|
|
|
def relabel_aliases(self, change_map):
|
|
"""
|
|
Relabels the alias values of any children. 'change_map' is a dictionary
|
|
mapping old (current) alias values to the new values.
|
|
"""
|
|
for pos, child in enumerate(self.children):
|
|
if hasattr(child, 'relabel_aliases'):
|
|
# For example another WhereNode
|
|
child.relabel_aliases(change_map)
|
|
elif hasattr(child, 'relabeled_clone'):
|
|
self.children[pos] = child.relabeled_clone(change_map)
|
|
|
|
def clone(self):
|
|
"""
|
|
Creates a clone of the tree. Must only be called on root nodes (nodes
|
|
with empty subtree_parents). Childs must be either (Contraint, lookup,
|
|
value) tuples, or objects supporting .clone().
|
|
"""
|
|
clone = self.__class__._new_instance(
|
|
children=[], connector=self.connector, negated=self.negated)
|
|
for child in self.children:
|
|
if hasattr(child, 'clone'):
|
|
clone.children.append(child.clone())
|
|
else:
|
|
clone.children.append(child)
|
|
return clone
|
|
|
|
def relabeled_clone(self, change_map):
|
|
clone = self.clone()
|
|
clone.relabel_aliases(change_map)
|
|
return clone
|
|
|
|
@classmethod
|
|
def _contains_aggregate(cls, obj):
|
|
if not isinstance(obj, tree.Node):
|
|
return getattr(obj.lhs, 'contains_aggregate', False) or getattr(obj.rhs, 'contains_aggregate', False)
|
|
return any(cls._contains_aggregate(c) for c in obj.children)
|
|
|
|
@cached_property
|
|
def contains_aggregate(self):
|
|
return self._contains_aggregate(self)
|
|
|
|
|
|
class EmptyWhere(WhereNode):
|
|
def add(self, data, connector):
|
|
return
|
|
|
|
def as_sql(self, compiler=None, connection=None):
|
|
raise EmptyResultSet
|
|
|
|
|
|
class EverythingNode(object):
|
|
"""
|
|
A node that matches everything.
|
|
"""
|
|
|
|
def as_sql(self, compiler=None, connection=None):
|
|
return '', []
|
|
|
|
|
|
class NothingNode(object):
|
|
"""
|
|
A node that matches nothing.
|
|
"""
|
|
def as_sql(self, compiler=None, connection=None):
|
|
raise EmptyResultSet
|
|
|
|
|
|
class ExtraWhere(object):
|
|
def __init__(self, sqls, params):
|
|
self.sqls = sqls
|
|
self.params = params
|
|
|
|
def as_sql(self, compiler=None, connection=None):
|
|
sqls = ["(%s)" % sql for sql in self.sqls]
|
|
return " AND ".join(sqls), list(self.params or ())
|
|
|
|
|
|
class SubqueryConstraint(object):
|
|
def __init__(self, alias, columns, targets, query_object):
|
|
self.alias = alias
|
|
self.columns = columns
|
|
self.targets = targets
|
|
self.query_object = query_object
|
|
|
|
def as_sql(self, compiler, connection):
|
|
query = self.query_object
|
|
|
|
# QuerySet was sent
|
|
if hasattr(query, 'values'):
|
|
if query._db and connection.alias != query._db:
|
|
raise ValueError("Can't do subqueries with queries on different DBs.")
|
|
# Do not override already existing values.
|
|
if query._fields is None:
|
|
query = query.values(*self.targets)
|
|
else:
|
|
query = query._clone()
|
|
query = query.query
|
|
if query.can_filter():
|
|
# If there is no slicing in use, then we can safely drop all ordering
|
|
query.clear_ordering(True)
|
|
|
|
query_compiler = query.get_compiler(connection=connection)
|
|
return query_compiler.as_subquery_condition(self.alias, self.columns, compiler)
|
|
|
|
def relabel_aliases(self, change_map):
|
|
self.alias = change_map.get(self.alias, self.alias)
|
|
|
|
def clone(self):
|
|
return self.__class__(
|
|
self.alias, self.columns, self.targets,
|
|
self.query_object)
|