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/queries | |
| parent | 795da6306a048b18c0158496b0d49e8e4f197a32 (diff) | |
Fixed #29865 -- Added logical XOR support for Q() and querysets.
Diffstat (limited to 'tests/queries')
| -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 |
3 files changed, 76 insertions, 0 deletions
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( |
