summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorRyan Heard <ryanwheard@gmail.com>2021-07-02 15:09:13 -0500
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2022-03-04 12:55:37 +0100
commitc6b4d62fa2c7f73b87f6ae7e8cf1d64ee5312dc5 (patch)
tree238bd8c3045c8d4577e09bd913de9748dcd49968 /tests
parent795da6306a048b18c0158496b0d49e8e4f197a32 (diff)
Fixed #29865 -- Added logical XOR support for Q() and querysets.
Diffstat (limited to 'tests')
-rw-r--r--tests/aggregation_regress/tests.py22
-rw-r--r--tests/expressions/tests.py12
-rw-r--r--tests/queries/test_q.py38
-rw-r--r--tests/queries/test_qs_combinators.py1
-rw-r--r--tests/queries/tests.py37
-rw-r--r--tests/xor_lookups/__init__.py0
-rw-r--r--tests/xor_lookups/models.py8
-rw-r--r--tests/xor_lookups/tests.py67
8 files changed, 184 insertions, 1 deletions
diff --git a/tests/aggregation_regress/tests.py b/tests/aggregation_regress/tests.py
index fb9d4ca92b..92a66298e4 100644
--- a/tests/aggregation_regress/tests.py
+++ b/tests/aggregation_regress/tests.py
@@ -1704,6 +1704,28 @@ class AggregationTests(TestCase):
attrgetter("pk"),
)
+ def test_filter_aggregates_xor_connector(self):
+ q1 = Q(price__gt=50)
+ q2 = Q(authors__count__gt=1)
+ query = Book.objects.annotate(Count("authors")).filter(q1 ^ q2).order_by("pk")
+ self.assertQuerysetEqual(
+ query,
+ [self.b1.pk, self.b4.pk, self.b6.pk],
+ attrgetter("pk"),
+ )
+
+ def test_filter_aggregates_negated_xor_connector(self):
+ q1 = Q(price__gt=50)
+ q2 = Q(authors__count__gt=1)
+ query = (
+ Book.objects.annotate(Count("authors")).filter(~(q1 ^ q2)).order_by("pk")
+ )
+ self.assertQuerysetEqual(
+ query,
+ [self.b2.pk, self.b3.pk, self.b5.pk],
+ attrgetter("pk"),
+ )
+
def test_ticket_11293_q_immutable(self):
"""
Splitting a q object to parts for where/having doesn't alter
diff --git a/tests/expressions/tests.py b/tests/expressions/tests.py
index c7488d7e25..12bf8996d1 100644
--- a/tests/expressions/tests.py
+++ b/tests/expressions/tests.py
@@ -2339,7 +2339,9 @@ class ReprTests(SimpleTestCase):
class CombinableTests(SimpleTestCase):
- bitwise_msg = "Use .bitand() and .bitor() for bitwise logical operations."
+ bitwise_msg = (
+ "Use .bitand(), .bitor(), and .bitxor() for bitwise logical operations."
+ )
def test_negation(self):
c = Combinable()
@@ -2353,6 +2355,10 @@ class CombinableTests(SimpleTestCase):
with self.assertRaisesMessage(NotImplementedError, self.bitwise_msg):
Combinable() | Combinable()
+ def test_xor(self):
+ with self.assertRaisesMessage(NotImplementedError, self.bitwise_msg):
+ Combinable() ^ Combinable()
+
def test_reversed_and(self):
with self.assertRaisesMessage(NotImplementedError, self.bitwise_msg):
object() & Combinable()
@@ -2361,6 +2367,10 @@ class CombinableTests(SimpleTestCase):
with self.assertRaisesMessage(NotImplementedError, self.bitwise_msg):
object() | Combinable()
+ def test_reversed_xor(self):
+ with self.assertRaisesMessage(NotImplementedError, self.bitwise_msg):
+ object() ^ Combinable()
+
class CombinedExpressionTests(SimpleTestCase):
def test_resolve_output_field(self):
diff --git a/tests/queries/test_q.py b/tests/queries/test_q.py
index b1dc45be13..39645a6f31 100644
--- a/tests/queries/test_q.py
+++ b/tests/queries/test_q.py
@@ -27,6 +27,15 @@ class QTests(SimpleTestCase):
self.assertEqual(q | Q(), q)
self.assertEqual(Q() | q, q)
+ def test_combine_xor_empty(self):
+ q = Q(x=1)
+ self.assertEqual(q ^ Q(), q)
+ self.assertEqual(Q() ^ q, q)
+
+ q = Q(x__in={}.keys())
+ self.assertEqual(q ^ Q(), q)
+ self.assertEqual(Q() ^ q, q)
+
def test_combine_empty_copy(self):
base_q = Q(x=1)
tests = [
@@ -34,6 +43,8 @@ class QTests(SimpleTestCase):
Q() | base_q,
base_q & Q(),
Q() & base_q,
+ base_q ^ Q(),
+ Q() ^ base_q,
]
for i, q in enumerate(tests):
with self.subTest(i=i):
@@ -43,6 +54,9 @@ class QTests(SimpleTestCase):
def test_combine_or_both_empty(self):
self.assertEqual(Q() | Q(), Q())
+ def test_combine_xor_both_empty(self):
+ self.assertEqual(Q() ^ Q(), Q())
+
def test_combine_not_q_object(self):
obj = object()
q = Q(x=1)
@@ -50,12 +64,15 @@ class QTests(SimpleTestCase):
q | obj
with self.assertRaisesMessage(TypeError, str(obj)):
q & obj
+ with self.assertRaisesMessage(TypeError, str(obj)):
+ q ^ obj
def test_combine_negated_boolean_expression(self):
tagged = Tag.objects.filter(category=OuterRef("pk"))
tests = [
Q() & ~Exists(tagged),
Q() | ~Exists(tagged),
+ Q() ^ ~Exists(tagged),
]
for q in tests:
with self.subTest(q=q):
@@ -88,6 +105,20 @@ class QTests(SimpleTestCase):
)
self.assertEqual(kwargs, {"_connector": "OR"})
+ def test_deconstruct_xor(self):
+ q1 = Q(price__gt=F("discounted_price"))
+ q2 = Q(price=F("discounted_price"))
+ q = q1 ^ q2
+ path, args, kwargs = q.deconstruct()
+ self.assertEqual(
+ args,
+ (
+ ("price__gt", F("discounted_price")),
+ ("price", F("discounted_price")),
+ ),
+ )
+ self.assertEqual(kwargs, {"_connector": "XOR"})
+
def test_deconstruct_and(self):
q1 = Q(price__gt=F("discounted_price"))
q2 = Q(price=F("discounted_price"))
@@ -144,6 +175,13 @@ class QTests(SimpleTestCase):
path, args, kwargs = q.deconstruct()
self.assertEqual(Q(*args, **kwargs), q)
+ def test_reconstruct_xor(self):
+ q1 = Q(price__gt=F("discounted_price"))
+ q2 = Q(price=F("discounted_price"))
+ q = q1 ^ q2
+ path, args, kwargs = q.deconstruct()
+ self.assertEqual(Q(*args, **kwargs), q)
+
def test_reconstruct_and(self):
q1 = Q(price__gt=F("discounted_price"))
q2 = Q(price=F("discounted_price"))
diff --git a/tests/queries/test_qs_combinators.py b/tests/queries/test_qs_combinators.py
index 5aa5f6c8d1..445e862adc 100644
--- a/tests/queries/test_qs_combinators.py
+++ b/tests/queries/test_qs_combinators.py
@@ -526,6 +526,7 @@ class QuerySetSetOperationTests(TestCase):
operators = [
("|", operator.or_),
("&", operator.and_),
+ ("^", operator.xor),
]
for combinator in combinators:
combined_qs = getattr(qs, combinator)(qs)
diff --git a/tests/queries/tests.py b/tests/queries/tests.py
index 800e71557b..f9d2ebf98f 100644
--- a/tests/queries/tests.py
+++ b/tests/queries/tests.py
@@ -1883,6 +1883,10 @@ class Queries5Tests(TestCase):
Note.objects.exclude(~Q() & ~Q()),
[self.n1, self.n2],
)
+ self.assertSequenceEqual(
+ Note.objects.exclude(~Q() ^ ~Q()),
+ [self.n1, self.n2],
+ )
def test_extra_select_literal_percent_s(self):
# Allow %%s to escape select clauses
@@ -2129,6 +2133,15 @@ class Queries6Tests(TestCase):
sql = captured_queries[0]["sql"]
self.assertIn("AS %s" % connection.ops.quote_name("col1"), sql)
+ def test_xor_subquery(self):
+ self.assertSequenceEqual(
+ Tag.objects.filter(
+ Exists(Tag.objects.filter(id=OuterRef("id"), name="t3"))
+ ^ Exists(Tag.objects.filter(id=OuterRef("id"), parent=self.t1))
+ ),
+ [self.t2],
+ )
+
class RawQueriesTests(TestCase):
@classmethod
@@ -2432,6 +2445,30 @@ class QuerySetBitwiseOperationTests(TestCase):
qs2 = Classroom.objects.filter(has_blackboard=True).order_by("-name")[:1]
self.assertCountEqual(qs1 | qs2, [self.room_3, self.room_4])
+ @skipUnlessDBFeature("allow_sliced_subqueries_with_in")
+ def test_xor_with_rhs_slice(self):
+ qs1 = Classroom.objects.filter(has_blackboard=True)
+ qs2 = Classroom.objects.filter(has_blackboard=False)[:1]
+ self.assertCountEqual(qs1 ^ qs2, [self.room_1, self.room_2, self.room_3])
+
+ @skipUnlessDBFeature("allow_sliced_subqueries_with_in")
+ def test_xor_with_lhs_slice(self):
+ qs1 = Classroom.objects.filter(has_blackboard=True)[:1]
+ qs2 = Classroom.objects.filter(has_blackboard=False)
+ self.assertCountEqual(qs1 ^ qs2, [self.room_1, self.room_2, self.room_4])
+
+ @skipUnlessDBFeature("allow_sliced_subqueries_with_in")
+ def test_xor_with_both_slice(self):
+ qs1 = Classroom.objects.filter(has_blackboard=False)[:1]
+ qs2 = Classroom.objects.filter(has_blackboard=True)[:1]
+ self.assertCountEqual(qs1 ^ qs2, [self.room_1, self.room_2])
+
+ @skipUnlessDBFeature("allow_sliced_subqueries_with_in")
+ def test_xor_with_both_slice_and_ordering(self):
+ qs1 = Classroom.objects.filter(has_blackboard=False).order_by("-pk")[:1]
+ qs2 = Classroom.objects.filter(has_blackboard=True).order_by("-name")[:1]
+ self.assertCountEqual(qs1 ^ qs2, [self.room_3, self.room_4])
+
def test_subquery_aliases(self):
combined = School.objects.filter(pk__isnull=False) & School.objects.filter(
Exists(
diff --git a/tests/xor_lookups/__init__.py b/tests/xor_lookups/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/xor_lookups/__init__.py
diff --git a/tests/xor_lookups/models.py b/tests/xor_lookups/models.py
new file mode 100644
index 0000000000..22e79aa94f
--- /dev/null
+++ b/tests/xor_lookups/models.py
@@ -0,0 +1,8 @@
+from django.db import models
+
+
+class Number(models.Model):
+ num = models.IntegerField()
+
+ def __str__(self):
+ return str(self.num)
diff --git a/tests/xor_lookups/tests.py b/tests/xor_lookups/tests.py
new file mode 100644
index 0000000000..a9cdf9cb31
--- /dev/null
+++ b/tests/xor_lookups/tests.py
@@ -0,0 +1,67 @@
+from django.db.models import Q
+from django.test import TestCase
+
+from .models import Number
+
+
+class XorLookupsTests(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.numbers = [Number.objects.create(num=i) for i in range(10)]
+
+ def test_filter(self):
+ self.assertCountEqual(
+ Number.objects.filter(num__lte=7) ^ Number.objects.filter(num__gte=3),
+ self.numbers[:3] + self.numbers[8:],
+ )
+ self.assertCountEqual(
+ Number.objects.filter(Q(num__lte=7) ^ Q(num__gte=3)),
+ self.numbers[:3] + self.numbers[8:],
+ )
+
+ def test_filter_negated(self):
+ self.assertCountEqual(
+ Number.objects.filter(Q(num__lte=7) ^ ~Q(num__lt=3)),
+ self.numbers[:3] + self.numbers[8:],
+ )
+ self.assertCountEqual(
+ Number.objects.filter(~Q(num__gt=7) ^ ~Q(num__lt=3)),
+ self.numbers[:3] + self.numbers[8:],
+ )
+ self.assertCountEqual(
+ Number.objects.filter(Q(num__lte=7) ^ ~Q(num__lt=3) ^ Q(num__lte=1)),
+ [self.numbers[2]] + self.numbers[8:],
+ )
+ self.assertCountEqual(
+ Number.objects.filter(~(Q(num__lte=7) ^ ~Q(num__lt=3) ^ Q(num__lte=1))),
+ self.numbers[:2] + self.numbers[3:8],
+ )
+
+ def test_exclude(self):
+ self.assertCountEqual(
+ Number.objects.exclude(Q(num__lte=7) ^ Q(num__gte=3)),
+ self.numbers[3:8],
+ )
+
+ def test_stages(self):
+ numbers = Number.objects.all()
+ self.assertSequenceEqual(
+ numbers.filter(num__gte=0) ^ numbers.filter(num__lte=11),
+ [],
+ )
+ self.assertSequenceEqual(
+ numbers.filter(num__gt=0) ^ numbers.filter(num__lt=11),
+ [self.numbers[0]],
+ )
+
+ def test_pk_q(self):
+ self.assertCountEqual(
+ Number.objects.filter(Q(pk=self.numbers[0].pk) ^ Q(pk=self.numbers[1].pk)),
+ self.numbers[:2],
+ )
+
+ def test_empty_in(self):
+ self.assertCountEqual(
+ Number.objects.filter(Q(pk__in=[]) ^ Q(num__gte=5)),
+ self.numbers[5:],
+ )