Fixed #27143 -- Allowed combining SearchQuery with more than one & or | operators.

This commit is contained in:
Nicola Jordan 2016-08-25 10:36:49 +02:00 committed by Tim Graham
parent 40d5011471
commit 978a00e39f
3 changed files with 74 additions and 19 deletions

View File

@ -92,14 +92,43 @@ class CombinedSearchVector(SearchVectorCombinable, CombinedExpression):
super(CombinedSearchVector, self).__init__(lhs, connector, rhs, output_field) 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 invert = False
_output_field = SearchQueryField() _output_field = SearchQueryField()
config = None config = None
BITAND = '&&'
BITOR = '||'
def __init__(self, value, output_field=None, **extra): def __init__(self, value, output_field=None, **extra):
self.config = extra.pop('config', self.config) self.config = extra.pop('config', self.config)
self.invert = extra.pop('invert', self.invert) self.invert = extra.pop('invert', self.invert)
@ -131,21 +160,6 @@ class SearchQuery(Value):
combined.output_field = SearchQueryField() combined.output_field = SearchQueryField()
return combined 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): def __invert__(self):
extra = { extra = {
'invert': not self.invert, 'invert': not self.invert,
@ -154,6 +168,12 @@ class SearchQuery(Value):
return type(self)(self.value, **extra) 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): class SearchRank(Func):
function = 'ts_rank' function = 'ts_rank'
_output_field = FloatField() _output_field = FloatField()

View File

@ -11,3 +11,6 @@ Bugfixes
* Fixed a crash in MySQL database validation where ``SELECT @@sql_mode`` * Fixed a crash in MySQL database validation where ``SELECT @@sql_mode``
doesn't return a result (:ticket:`27180`). doesn't return a result (:ticket:`27180`).
* Allowed combining ``contrib.postgres.search.SearchQuery`` with more than one
``&`` or ``|`` operators (:ticket:`27143`).

View File

@ -205,14 +205,46 @@ class TestCombinations(GrailTestData, PostgreSQLTestCase):
).filter(search=SearchQuery('bedemir') & SearchQuery('scales')) ).filter(search=SearchQuery('bedemir') & SearchQuery('scales'))
self.assertSequenceEqual(searched, [self.bedemir0]) 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): def test_query_or(self):
searched = Line.objects.filter(dialogue__search=SearchQuery('kneecaps') | SearchQuery('nostrils')) searched = Line.objects.filter(dialogue__search=SearchQuery('kneecaps') | SearchQuery('nostrils'))
self.assertSequenceEqual(set(searched), {self.verse1, self.verse2}) 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): def test_query_invert(self):
searched = Line.objects.filter(character=self.minstrel, dialogue__search=~SearchQuery('kneecaps')) searched = Line.objects.filter(character=self.minstrel, dialogue__search=~SearchQuery('kneecaps'))
self.assertEqual(set(searched), {self.verse0, self.verse2}) 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'}) @modify_settings(INSTALLED_APPS={'append': 'django.contrib.postgres'})
class TestRankingAndWeights(GrailTestData, PostgreSQLTestCase): class TestRankingAndWeights(GrailTestData, PostgreSQLTestCase):