diff options
| author | Ryan Heard <ryanwheard@gmail.com> | 2021-07-02 15:09:13 -0500 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2022-03-04 12:55:37 +0100 |
| commit | c6b4d62fa2c7f73b87f6ae7e8cf1d64ee5312dc5 (patch) | |
| tree | 238bd8c3045c8d4577e09bd913de9748dcd49968 /tests | |
| parent | 795da6306a048b18c0158496b0d49e8e4f197a32 (diff) | |
Fixed #29865 -- Added logical XOR support for Q() and querysets.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/aggregation_regress/tests.py | 22 | ||||
| -rw-r--r-- | tests/expressions/tests.py | 12 | ||||
| -rw-r--r-- | tests/queries/test_q.py | 38 | ||||
| -rw-r--r-- | tests/queries/test_qs_combinators.py | 1 | ||||
| -rw-r--r-- | tests/queries/tests.py | 37 | ||||
| -rw-r--r-- | tests/xor_lookups/__init__.py | 0 | ||||
| -rw-r--r-- | tests/xor_lookups/models.py | 8 | ||||
| -rw-r--r-- | tests/xor_lookups/tests.py | 67 |
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:], + ) |
