mirror of https://github.com/django/django.git
Fixed #27143 -- Allowed combining SearchQuery with more than one & or | operators.
This commit is contained in:
parent
40d5011471
commit
978a00e39f
|
@ -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()
|
||||||
|
|
|
@ -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`).
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue