From f3da09df0f4147223ab76a00a841586ccf11005d Mon Sep 17 00:00:00 2001 From: Hannes Ljungberg Date: Fri, 20 Mar 2020 23:08:32 +0100 Subject: [PATCH] Fixed #31396 -- Added binary XOR operator to F expressions. --- django/db/backends/mysql/operations.py | 3 ++- django/db/backends/oracle/operations.py | 4 +++- django/db/backends/sqlite3/base.py | 1 + django/db/backends/sqlite3/operations.py | 2 ++ django/db/models/expressions.py | 4 ++++ docs/releases/3.1.txt | 3 +++ docs/topics/db/queries.txt | 10 +++++++++- tests/expressions/tests.py | 21 ++++++++++++++++++++- 8 files changed, 44 insertions(+), 4 deletions(-) diff --git a/django/db/backends/mysql/operations.py b/django/db/backends/mysql/operations.py index 6aaf5c6295..e9306f03fc 100644 --- a/django/db/backends/mysql/operations.py +++ b/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 diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py index 1e59323655..b0b50f7868 100644 --- a/django/db/backends/oracle/operations.py +++ b/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): diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 7b3f90a2fd..fcc9d31b37 100644 --- a/django/db/backends/sqlite3/base.py +++ b/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))) diff --git a/django/db/backends/sqlite3/operations.py b/django/db/backends/sqlite3/operations.py index 80c32f6fcd..fcc2a06d7a 100644 --- a/django/db/backends/sqlite3/operations.py +++ b/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): diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index 84960d77e1..4753a258f9 100644 --- a/django/db/models/expressions.py +++ b/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) diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt index 630dd4bc35..6cbdac4a1e 100644 --- a/docs/releases/3.1.txt +++ b/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 `. + Pagination ~~~~~~~~~~ diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index 79f38084fa..415d229b02 100644 --- a/docs/topics/db/queries.txt +++ b/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 -------------------------- diff --git a/tests/expressions/tests.py b/tests/expressions/tests.py index da6e04e8fd..46fe6fa89d 100644 --- a/tests/expressions/tests.py +++ b/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'))