Browse Source

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

Hannes Ljungberg 5 years ago
parent
commit
f3da09df0f

+ 2 - 1
django/db/backends/mysql/operations.py

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

+ 3 - 1
django/db/backends/oracle/operations.py

@@ -3,7 +3,7 @@ import uuid
 from functools import lru_cache
 
 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.utils import strip_quotes, truncate_name
 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}
         elif connector == '^':
             return 'POWER(%s)' % ','.join(sub_expressions)
+        elif connector == '#':
+            raise NotSupportedError('Bitwise XOR is not supported in Oracle.')
         return super().combine_expression(connector, sub_expressions)
 
     def _get_no_autofield_sequence_name(self, table):

+ 1 - 0
django/db/backends/sqlite3/base.py

@@ -218,6 +218,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
         conn.create_function('ASIN', 1, none_guard(math.asin))
         conn.create_function('ATAN', 1, none_guard(math.atan))
         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('COS', 1, none_guard(math.cos))
         conn.create_function('COT', 1, none_guard(lambda x: 1 / math.tan(x)))

+ 2 - 0
django/db/backends/sqlite3/operations.py

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

+ 4 - 0
django/db/models/expressions.py

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

+ 3 - 0
docs/releases/3.1.txt

@@ -338,6 +338,9 @@ Models
 * The new ``is_dst``  parameter of the :meth:`.QuerySet.datetimes` determines
   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
 ~~~~~~~~~~
 

+ 9 - 1
docs/topics/db/queries.txt

@@ -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))
 
 The ``F()`` objects support bitwise operations by ``.bitand()``, ``.bitor()``,
-``.bitrightshift()``, and ``.bitleftshift()``. For example::
+``.bitxor()``, ``.bitrightshift()``, and ``.bitleftshift()``. For example::
 
     >>> 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
 --------------------------
 

+ 20 - 1
tests/expressions/tests.py

@@ -6,7 +6,7 @@ from copy import deepcopy
 from unittest import mock
 
 from django.core.exceptions import FieldError
-from django.db import DatabaseError, connection
+from django.db import DatabaseError, NotSupportedError, connection
 from django.db.models import (
     Avg, BooleanField, Case, CharField, Count, DateField, DateTimeField,
     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).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):
         # Right hand operators
         Number.objects.filter(pk=self.n.pk).update(integer=15 + F('integer'), float=42.7 + F('float'))