Fixed #31396 -- Added binary XOR operator to F expressions.

This commit is contained in:
Hannes Ljungberg 2020-03-20 23:08:32 +01:00 committed by Mariusz Felisiak
parent 39e1c88de6
commit f3da09df0f
8 changed files with 44 additions and 4 deletions

View File

@ -240,7 +240,8 @@ class DatabaseOperations(BaseDatabaseOperations):
return 'POW(%s)' % ','.join(sub_expressions) return 'POW(%s)' % ','.join(sub_expressions)
# Convert the result to a signed integer since MySQL's binary operators # Convert the result to a signed integer since MySQL's binary operators
# return an unsigned integer. # return an unsigned integer.
elif connector in ('&', '|', '<<'): elif connector in ('&', '|', '<<', '#'):
connector = '^' if connector == '#' else connector
return 'CONVERT(%s, SIGNED)' % connector.join(sub_expressions) return 'CONVERT(%s, SIGNED)' % connector.join(sub_expressions)
elif connector == '>>': elif connector == '>>':
lhs, rhs = sub_expressions lhs, rhs = sub_expressions

View File

@ -3,7 +3,7 @@ import uuid
from functools import lru_cache from functools import lru_cache
from django.conf import settings from django.conf import settings
from django.db import DatabaseError from django.db import DatabaseError, NotSupportedError
from django.db.backends.base.operations import BaseDatabaseOperations from django.db.backends.base.operations import BaseDatabaseOperations
from django.db.backends.utils import strip_quotes, truncate_name from django.db.backends.utils import strip_quotes, truncate_name
from django.db.models import AutoField, Exists, ExpressionWrapper from django.db.models import AutoField, Exists, ExpressionWrapper
@ -575,6 +575,8 @@ END;
return 'FLOOR(%(lhs)s / POWER(2, %(rhs)s))' % {'lhs': lhs, 'rhs': rhs} return 'FLOOR(%(lhs)s / POWER(2, %(rhs)s))' % {'lhs': lhs, 'rhs': rhs}
elif connector == '^': elif connector == '^':
return 'POWER(%s)' % ','.join(sub_expressions) return 'POWER(%s)' % ','.join(sub_expressions)
elif connector == '#':
raise NotSupportedError('Bitwise XOR is not supported in Oracle.')
return super().combine_expression(connector, sub_expressions) return super().combine_expression(connector, sub_expressions)
def _get_no_autofield_sequence_name(self, table): def _get_no_autofield_sequence_name(self, table):

View File

@ -218,6 +218,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
conn.create_function('ASIN', 1, none_guard(math.asin)) conn.create_function('ASIN', 1, none_guard(math.asin))
conn.create_function('ATAN', 1, none_guard(math.atan)) conn.create_function('ATAN', 1, none_guard(math.atan))
conn.create_function('ATAN2', 2, none_guard(math.atan2)) conn.create_function('ATAN2', 2, none_guard(math.atan2))
conn.create_function('BITXOR', 2, none_guard(operator.xor))
conn.create_function('CEILING', 1, none_guard(math.ceil)) conn.create_function('CEILING', 1, none_guard(math.ceil))
conn.create_function('COS', 1, none_guard(math.cos)) conn.create_function('COS', 1, none_guard(math.cos))
conn.create_function('COT', 1, none_guard(lambda x: 1 / math.tan(x))) conn.create_function('COT', 1, none_guard(lambda x: 1 / math.tan(x)))

View File

@ -312,6 +312,8 @@ class DatabaseOperations(BaseDatabaseOperations):
# function that's registered in connect(). # function that's registered in connect().
if connector == '^': if connector == '^':
return 'POWER(%s)' % ','.join(sub_expressions) return 'POWER(%s)' % ','.join(sub_expressions)
elif connector == '#':
return 'BITXOR(%s)' % ','.join(sub_expressions)
return super().combine_expression(connector, sub_expressions) return super().combine_expression(connector, sub_expressions)
def combine_duration_expression(self, connector, sub_expressions): def combine_duration_expression(self, connector, sub_expressions):

View File

@ -51,6 +51,7 @@ class Combinable:
BITOR = '|' BITOR = '|'
BITLEFTSHIFT = '<<' BITLEFTSHIFT = '<<'
BITRIGHTSHIFT = '>>' BITRIGHTSHIFT = '>>'
BITXOR = '#'
def _combine(self, other, connector, reversed): def _combine(self, other, connector, reversed):
if not hasattr(other, 'resolve_expression'): if not hasattr(other, 'resolve_expression'):
@ -105,6 +106,9 @@ class Combinable:
def bitrightshift(self, other): def bitrightshift(self, other):
return self._combine(other, self.BITRIGHTSHIFT, False) return self._combine(other, self.BITRIGHTSHIFT, False)
def bitxor(self, other):
return self._combine(other, self.BITXOR, False)
def __or__(self, other): def __or__(self, other):
if getattr(self, 'conditional', False) and getattr(other, 'conditional', False): if getattr(self, 'conditional', False) and getattr(other, 'conditional', False):
return Q(self) | Q(other) return Q(self) | Q(other)

View File

@ -338,6 +338,9 @@ Models
* The new ``is_dst`` parameter of the :meth:`.QuerySet.datetimes` determines * The new ``is_dst`` parameter of the :meth:`.QuerySet.datetimes` determines
the treatment of nonexistent and ambiguous datetimes. the treatment of nonexistent and ambiguous datetimes.
* The new :class:`~django.db.models.F` expression ``bitxor()`` method allows
:ref:`bitwise XOR operation <using-f-expressions-in-filters>`.
Pagination Pagination
~~~~~~~~~~ ~~~~~~~~~~

View File

@ -656,10 +656,18 @@ that were modified more than 3 days after they were published::
>>> Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3)) >>> Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3))
The ``F()`` objects support bitwise operations by ``.bitand()``, ``.bitor()``, The ``F()`` objects support bitwise operations by ``.bitand()``, ``.bitor()``,
``.bitrightshift()``, and ``.bitleftshift()``. For example:: ``.bitxor()``, ``.bitrightshift()``, and ``.bitleftshift()``. For example::
>>> F('somefield').bitand(16) >>> F('somefield').bitand(16)
.. admonition:: Oracle
Oracle doesn't support bitwise XOR operation.
.. versionchanged:: 3.1
Support for ``.bitxor()`` was added.
The ``pk`` lookup shortcut The ``pk`` lookup shortcut
-------------------------- --------------------------

View File

@ -6,7 +6,7 @@ from copy import deepcopy
from unittest import mock from unittest import mock
from django.core.exceptions import FieldError from django.core.exceptions import FieldError
from django.db import DatabaseError, connection from django.db import DatabaseError, NotSupportedError, connection
from django.db.models import ( from django.db.models import (
Avg, BooleanField, Case, CharField, Count, DateField, DateTimeField, Avg, BooleanField, Case, CharField, Count, DateField, DateTimeField,
DurationField, Exists, Expression, ExpressionList, ExpressionWrapper, F, DurationField, Exists, Expression, ExpressionList, ExpressionWrapper, F,
@ -1163,6 +1163,25 @@ class ExpressionOperatorTests(TestCase):
self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 1764) self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 1764)
self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(61.02, places=2)) self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(61.02, places=2))
@unittest.skipIf(connection.vendor == 'oracle', "Oracle doesn't support bitwise XOR.")
def test_lefthand_bitwise_xor(self):
Number.objects.update(integer=F('integer').bitxor(48))
self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 26)
self.assertEqual(Number.objects.get(pk=self.n1.pk).integer, -26)
@unittest.skipIf(connection.vendor == 'oracle', "Oracle doesn't support bitwise XOR.")
def test_lefthand_bitwise_xor_null(self):
employee = Employee.objects.create(firstname='John', lastname='Doe')
Employee.objects.update(salary=F('salary').bitxor(48))
employee.refresh_from_db()
self.assertIsNone(employee.salary)
@unittest.skipUnless(connection.vendor == 'oracle', "Oracle doesn't support bitwise XOR.")
def test_lefthand_bitwise_xor_not_supported(self):
msg = 'Bitwise XOR is not supported in Oracle.'
with self.assertRaisesMessage(NotSupportedError, msg):
Number.objects.update(integer=F('integer').bitxor(48))
def test_right_hand_addition(self): def test_right_hand_addition(self):
# Right hand operators # Right hand operators
Number.objects.filter(pk=self.n.pk).update(integer=15 + F('integer'), float=42.7 + F('float')) Number.objects.filter(pk=self.n.pk).update(integer=15 + F('integer'), float=42.7 + F('float'))