summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNicola Jordan <nic@hixi.ch>2016-08-25 10:36:49 +0200
committerTim Graham <timograham@gmail.com>2016-09-07 15:26:52 -0400
commit978a00e39fee25cfa99065285b0de88366710fad (patch)
tree80f358c35803e5f64ae893c1149a42c6099456ee
parent40d501147176352b694ec8b67b09127b20193ad2 (diff)
Fixed #27143 -- Allowed combining SearchQuery with more than one & or | operators.
-rw-r--r--django/contrib/postgres/search.py58
-rw-r--r--docs/releases/1.10.2.txt3
-rw-r--r--tests/postgres_tests/test_search.py32
3 files changed, 74 insertions, 19 deletions
diff --git a/django/contrib/postgres/search.py b/django/contrib/postgres/search.py
index 4628f4cf19..fb2f41b524 100644
--- a/django/contrib/postgres/search.py
+++ b/django/contrib/postgres/search.py
@@ -92,14 +92,43 @@ class CombinedSearchVector(SearchVectorCombinable, CombinedExpression):
super(CombinedSearchVector, self).__init__(lhs, connector, rhs, output_field)
-class SearchQuery(Value):
+class SearchQueryCombinable(object):
+ BITAND = '&&'
+ BITOR = '||'
+
+ def _combine(self, other, connector, reversed, node=None):
+ if not isinstance(other, SearchQueryCombinable):
+ raise TypeError(
+ 'SearchQuery can only be combined with other SearchQuerys, '
+ 'got {}.'.format(type(other))
+ )
+ if not self.config == other.config:
+ raise TypeError("SearchQuery configs don't match.")
+ if reversed:
+ return CombinedSearchQuery(other, connector, self, self.config)
+ return CombinedSearchQuery(self, connector, other, self.config)
+
+ # On Combinable, these are not implemented to reduce confusion with Q. In
+ # this case we are actually (ab)using them to do logical combination so
+ # it's consistent with other usage in Django.
+ def __or__(self, other):
+ return self._combine(other, self.BITOR, False)
+
+ def __ror__(self, other):
+ return self._combine(other, self.BITOR, True)
+
+ def __and__(self, other):
+ return self._combine(other, self.BITAND, False)
+
+ def __rand__(self, other):
+ return self._combine(other, self.BITAND, True)
+
+
+class SearchQuery(SearchQueryCombinable, Value):
invert = False
_output_field = SearchQueryField()
config = None
- BITAND = '&&'
- BITOR = '||'
-
def __init__(self, value, output_field=None, **extra):
self.config = extra.pop('config', self.config)
self.invert = extra.pop('invert', self.invert)
@@ -131,21 +160,6 @@ class SearchQuery(Value):
combined.output_field = SearchQueryField()
return combined
- # On Combinable, these are not implemented to reduce confusion with Q. In
- # this case we are actually (ab)using them to do logical combination so
- # it's consistent with other usage in Django.
- def __or__(self, other):
- return self._combine(other, self.BITOR, False)
-
- def __ror__(self, other):
- return self._combine(other, self.BITOR, True)
-
- def __and__(self, other):
- return self._combine(other, self.BITAND, False)
-
- def __rand__(self, other):
- return self._combine(other, self.BITAND, True)
-
def __invert__(self):
extra = {
'invert': not self.invert,
@@ -154,6 +168,12 @@ class SearchQuery(Value):
return type(self)(self.value, **extra)
+class CombinedSearchQuery(SearchQueryCombinable, CombinedExpression):
+ def __init__(self, lhs, connector, rhs, config, output_field=None):
+ self.config = config
+ super(CombinedSearchQuery, self).__init__(lhs, connector, rhs, output_field)
+
+
class SearchRank(Func):
function = 'ts_rank'
_output_field = FloatField()
diff --git a/docs/releases/1.10.2.txt b/docs/releases/1.10.2.txt
index f3c574d7d1..bc11980053 100644
--- a/docs/releases/1.10.2.txt
+++ b/docs/releases/1.10.2.txt
@@ -11,3 +11,6 @@ Bugfixes
* Fixed a crash in MySQL database validation where ``SELECT @@sql_mode``
doesn't return a result (:ticket:`27180`).
+
+* Allowed combining ``contrib.postgres.search.SearchQuery`` with more than one
+ ``&`` or ``|`` operators (:ticket:`27143`).
diff --git a/tests/postgres_tests/test_search.py b/tests/postgres_tests/test_search.py
index 93d86bf275..0bf2df50f1 100644
--- a/tests/postgres_tests/test_search.py
+++ b/tests/postgres_tests/test_search.py
@@ -205,14 +205,46 @@ class TestCombinations(GrailTestData, PostgreSQLTestCase):
).filter(search=SearchQuery('bedemir') & SearchQuery('scales'))
self.assertSequenceEqual(searched, [self.bedemir0])
+ def test_query_multiple_and(self):
+ searched = Line.objects.annotate(
+ search=SearchVector('scene__setting', 'dialogue'),
+ ).filter(search=SearchQuery('bedemir') & SearchQuery('scales') & SearchQuery('nostrils'))
+ self.assertSequenceEqual(searched, [])
+
+ searched = Line.objects.annotate(
+ search=SearchVector('scene__setting', 'dialogue'),
+ ).filter(search=SearchQuery('shall') & SearchQuery('use') & SearchQuery('larger'))
+ self.assertSequenceEqual(searched, [self.bedemir0])
+
def test_query_or(self):
searched = Line.objects.filter(dialogue__search=SearchQuery('kneecaps') | SearchQuery('nostrils'))
self.assertSequenceEqual(set(searched), {self.verse1, self.verse2})
+ def test_query_multiple_or(self):
+ searched = Line.objects.filter(
+ dialogue__search=SearchQuery('kneecaps') | SearchQuery('nostrils') | SearchQuery('Sir Robin')
+ )
+ self.assertSequenceEqual(set(searched), {self.verse1, self.verse2, self.verse0})
+
def test_query_invert(self):
searched = Line.objects.filter(character=self.minstrel, dialogue__search=~SearchQuery('kneecaps'))
self.assertEqual(set(searched), {self.verse0, self.verse2})
+ def test_query_config_mismatch(self):
+ with self.assertRaisesMessage(TypeError, "SearchQuery configs don't match."):
+ Line.objects.filter(
+ dialogue__search=SearchQuery('kneecaps', config='german') |
+ SearchQuery('nostrils', config='english')
+ )
+
+ def test_query_combined_mismatch(self):
+ msg = "SearchQuery can only be combined with other SearchQuerys, got"
+ with self.assertRaisesMessage(TypeError, msg):
+ Line.objects.filter(dialogue__search=None | SearchQuery('kneecaps'))
+
+ with self.assertRaisesMessage(TypeError, msg):
+ Line.objects.filter(dialogue__search=None & SearchQuery('kneecaps'))
+
@modify_settings(INSTALLED_APPS={'append': 'django.contrib.postgres'})
class TestRankingAndWeights(GrailTestData, PostgreSQLTestCase):