1) Unified QuerySet filter and exclude methods,

so both can load *q and **kwargs at the same time
 and accept prewhere flag
2) Added ability to add prewhere clause
 in QuerySet.filter() and QuerySet.exclude() methods
3) Added ability to check, if Q() object is empty (including bool check)
4) Refactored QuerySet.as_sql() method:
 + don't add GROUP BY and WHERE if it's not needed
 + ability to add PREWHERE condition
 + Common style of adding optional query parts
This commit is contained in:
M1ha 2018-12-08 11:40:05 +05:00
parent 41cf4c3a79
commit 3bc5f27cda

View File

@ -183,6 +183,14 @@ class Q(object):
self._negate = False self._negate = False
self._mode = self.AND_MODE self._mode = self.AND_MODE
@property
def is_empty(self):
"""
Checks if there are any conditions in Q object
:return: Boolean
"""
return bool(self._fovs or self._l_child or self._r_child)
@classmethod @classmethod
def _construct_from(cls, l_child, r_child, mode): def _construct_from(cls, l_child, r_child, mode):
q = Q() q = Q()
@ -203,7 +211,7 @@ class Q(object):
sql = ' {} '.format(self._mode).join(fov.to_sql(model_cls) for fov in self._fovs) sql = ' {} '.format(self._mode).join(fov.to_sql(model_cls) for fov in self._fovs)
else: else:
if self._l_child and self._r_child: if self._l_child and self._r_child:
sql = '({} {} {})'.format( sql = '({}) {} ({})'.format(
self._l_child.to_sql(model_cls), self._mode, self._r_child.to_sql(model_cls)) self._l_child.to_sql(model_cls), self._mode, self._r_child.to_sql(model_cls))
else: else:
return '1' return '1'
@ -222,6 +230,9 @@ class Q(object):
q._negate = True q._negate = True
return q return q
def __bool__(self):
return not self.is_empty
@six.python_2_unicode_compatible @six.python_2_unicode_compatible
class QuerySet(object): class QuerySet(object):
@ -239,7 +250,8 @@ class QuerySet(object):
self._model_cls = model_cls self._model_cls = model_cls
self._database = database self._database = database
self._order_by = [] self._order_by = []
self._q = [] self._where_q = Q()
self._prewhere_q = Q()
self._fields = model_cls.fields().keys() self._fields = model_cls.fields().keys()
self._limits = None self._limits = None
self._distinct = False self._distinct = False
@ -303,14 +315,14 @@ class QuerySet(object):
for field in self._order_by for field in self._order_by
]) ])
def conditions_as_sql(self): def conditions_as_sql(self, q_object):
""" """
Returns the contents of the query's `WHERE` clause as a string. Returns the contents of the query's `WHERE` or `PREWHERE` clause as a string.
""" """
if self._q: if q_object:
return u' AND '.join([q.to_sql(self._model_cls) for q in self._q]) return u' AND '.join([q.to_sql(self._model_cls) for q in q_object])
else: else:
return u'1' return u''
def count(self): def count(self):
""" """
@ -342,25 +354,40 @@ class QuerySet(object):
qs._fields = field_names qs._fields = field_names
return qs return qs
def filter(self, *q, **filter_fields): def _filter_or_exclude(self, *q, **kwargs):
reverse = kwargs.pop('reverse', False)
prewhere = kwargs.pop('prewhere', False)
condition = copy(self._where_q)
qs = copy(self)
if q:
condition &= q
if kwargs:
condition &= Q(**kwargs)
if reverse:
condition = ~condition
if prewhere:
qs._prewhere_q = condition
else:
qs._where_q = condition
return qs
def filter(self, *q, **kwargs):
""" """
Returns a copy of this queryset that includes only rows matching the conditions. Returns a copy of this queryset that includes only rows matching the conditions.
Add q object to query if it specified. Add q object to query if it specified.
""" """
qs = copy(self) return self._filter_or_exclude(*q, **kwargs)
if q:
qs._q = list(self._q) + list(q)
else:
qs._q = list(self._q) + [Q(**filter_fields)]
return qs
def exclude(self, **filter_fields): def exclude(self, *q, **kwargs):
""" """
Returns a copy of this queryset that excludes all rows matching the conditions. Returns a copy of this queryset that excludes all rows matching the conditions.
""" """
qs = copy(self) return self._filter_or_exclude(*q, reverse=True, **kwargs)
qs._q = list(self._q) + [~Q(**filter_fields)]
return qs
def paginate(self, page_num=1, page_size=100): def paginate(self, page_num=1, page_size=100):
""" """
@ -476,20 +503,30 @@ class AggregateQuerySet(QuerySet):
Returns the whole query as a SQL string. Returns the whole query as a SQL string.
""" """
distinct = 'DISTINCT ' if self._distinct else '' distinct = 'DISTINCT ' if self._distinct else ''
grouping = comma_join('`%s`' % field for field in self._grouping_fields)
fields = comma_join(list(self._fields) + ['%s AS %s' % (v, k) for k, v in self._calculated_fields.items()]) fields = comma_join(list(self._fields) + ['%s AS %s' % (v, k) for k, v in self._calculated_fields.items()])
params = dict( params = dict(
distinct=distinct, distinct=distinct,
grouping=grouping or "''",
fields=fields, fields=fields,
table=self._model_cls.table_name(), table=self._model_cls.table_name(),
conds=self.conditions_as_sql()
) )
sql = u'SELECT %(distinct)s%(fields)s\nFROM `%(table)s`\nWHERE %(conds)s\nGROUP BY %(grouping)s' % params sql = u'SELECT %(distinct)s%(fields)s\nFROM `%(table)s`' % params
if self._prewhere_q:
sql += '\nPREWHERE ' + self.conditions_as_sql(self._prewhere_q)
if self._where_q:
sql += '\nWHERE ' + self.conditions_as_sql(self._where_q)
if self._grouping_fields:
sql += '\nGROUP BY %s' % comma_join('`%s`' % field for field in self._grouping_fields)
if self._order_by: if self._order_by:
sql += '\nORDER BY ' + self.order_by_as_sql() sql += '\nORDER BY ' + self.order_by_as_sql()
if self._limits: if self._limits:
sql += '\nLIMIT %d, %d' % self._limits sql += '\nLIMIT %d, %d' % self._limits
return sql return sql
def __iter__(self): def __iter__(self):