diff --git a/docs/querysets.md b/docs/querysets.md index 260e66c..c4e84ea 100644 --- a/docs/querysets.md +++ b/docs/querysets.md @@ -17,21 +17,29 @@ The `filter` and `exclude` methods are used for filtering the matching instances >>> qs = Person.objects_in(database) >>> qs = qs.filter(first_name__startswith='V').exclude(birthday__lt='2000-01-01') - >>> qs.conditions_as_sql() + >>> qs.conditions_as_sql(qs._where) u"first_name LIKE 'V%' AND NOT (birthday < '2000-01-01')" It is possible to specify several fields to filter or exclude by: >>> qs = Person.objects_in(database).filter(last_name='Smith', height__gt=1.75) - >>> qs.conditions_as_sql() + >>> qs.conditions_as_sql(qs._where) u"last_name = 'Smith' AND height > 1.75" For filters with compound conditions you can use `Q` objects inside `filter` with overloaded operators `&` (AND), `|` (OR) and `~` (NOT): >>> qs = Person.objects_in(database).filter((Q(first_name='Ciaran', last_name='Carver') | Q(height_lte=1.8)) & ~Q(first_name='David')) - >>> qs.conditions_as_sql() + >>> qs.conditions_as_sql(qs._where) u"((first_name = 'Ciaran' AND last_name = 'Carver') OR height <= 1.8) AND (NOT (first_name = 'David'))" +By default conditions from `filter` and `exclude` methods are add to `WHERE` clause. +For better aggregation performance you can add them to `PREWHERE` section using `prewhere=True` parameter + + >>> qs = Person.objects_in(database) + >>> qs = qs.filter(first_name__startswith='V', prewhere=True) + >>> qs.conditions_as_sql(qs._prewhere) + u"first_name LIKE 'V%'" + There are different operators that can be used, by passing `__=` (two underscores separate the field name from the operator). In case no operator is given, `eq` is used by default. Below are all the supported operators. | Operator | Equivalent SQL | Comments | diff --git a/src/infi/clickhouse_orm/query.py b/src/infi/clickhouse_orm/query.py index b6a8baf..c4839aa 100644 --- a/src/infi/clickhouse_orm/query.py +++ b/src/infi/clickhouse_orm/query.py @@ -189,14 +189,14 @@ class Q(object): Checks if there are any conditions in Q object :return: Boolean """ - return bool(self._fovs or self._l_child or self._r_child) + return not bool(self._fovs or self._l_child or self._r_child) @classmethod def _construct_from(cls, l_child, r_child, mode): q = Q() q._l_child = l_child q._r_child = r_child - q._mode = mode # AND/OR + q._mode = mode # AND/OR return q def _build_fov(self, key, value): @@ -209,14 +209,17 @@ class Q(object): def to_sql(self, model_cls): if self._fovs: sql = ' {} '.format(self._mode).join(fov.to_sql(model_cls) for fov in self._fovs) + elif self._l_child and self._r_child: + sql = '({}) {} ({})'.format(self._l_child.to_sql(model_cls), self._mode, self._r_child.to_sql(model_cls)) + elif self._l_child or self._r_child: + # Return existing condition + sql = (self._l_child or self._r_child).to_sql(model_cls) else: - if self._l_child and self._r_child: - sql = '({}) {} ({})'.format( - self._l_child.to_sql(model_cls), self._mode, self._r_child.to_sql(model_cls)) - else: - return '1' + sql = '1' + if self._negate: sql = 'NOT (%s)' % sql + return sql def __or__(self, other): @@ -303,7 +306,7 @@ class QuerySet(object): distinct = 'DISTINCT ' if self._distinct else '' params = (distinct, self.select_fields_as_sql(), self._model_cls.table_name()) - sql = u'SELECT %s%s\nFROM `%s`\n' % params + sql = u'SELECT %s%s\nFROM `%s`' % params if self._prewhere_q: sql += '\nPREWHERE ' + self.conditions_as_sql(self._prewhere_q) @@ -320,7 +323,7 @@ class QuerySet(object): if self._limits: sql += '\nLIMIT %d, %d' % self._limits - return + return sql def order_by_as_sql(self): """ @@ -335,10 +338,7 @@ class QuerySet(object): """ Returns the contents of the query's `WHERE` or `PREWHERE` clause as a string. """ - if q_object: - return u' AND '.join([q.to_sql(self._model_cls) for q in q_object]) - else: - return u'' + return q_object.to_sql(self._model_cls) def count(self): """ @@ -378,8 +378,8 @@ class QuerySet(object): condition = copy(self._where_q) qs = copy(self) - if q: - condition &= q + for q_obj in q: + condition &= q_obj if kwargs: condition &= Q(**kwargs) @@ -487,7 +487,8 @@ class AggregateQuerySet(QuerySet): self._grouping_fields = grouping_fields self._calculated_fields = calculated_fields self._order_by = list(base_qs._order_by) - self._q = list(base_qs._q) + self._where_q = base_qs._where_q + self._prewhere_q = base_qs._prewhere_q self._limits = base_qs._limits self._distinct = base_qs._distinct diff --git a/tests/test_querysets.py b/tests/test_querysets.py index 9dd1ca4..14db99c 100644 --- a/tests/test_querysets.py +++ b/tests/test_querysets.py @@ -11,7 +11,7 @@ from datetime import date, datetime try: Enum # exists in Python 3.4+ except NameError: - from enum import Enum # use the enum34 library instead + from enum import Enum # use the enum34 library instead class QuerySetTestCase(TestCaseWithData): @@ -29,6 +29,13 @@ class QuerySetTestCase(TestCaseWithData): self.assertEqual(count, expected_count) self.assertEqual(qs.count(), expected_count) + def test_prewhere(self): + # We can't distinguish prewhere and where results, it affects performance only. + # So let's control prewhere acts like where does + qs = Person.objects_in(self.database) + self.assertTrue(qs.filter(first_name='Connor', prewhere=True)) + self.assertFalse(qs.filter(first_name='Willy', prewhere=True)) + def test_no_filtering(self): qs = Person.objects_in(self.database) self._test_qs(qs, len(data))