|
| 1 | +from bson import ObjectId |
| 2 | +from django.db import connection |
1 | 3 | from django.test import TestCase
|
2 | 4 |
|
3 | 5 | from django_mongodb_backend.test import MongoTestCaseMixin
|
4 | 6 |
|
5 |
| -from .models import Book, Number |
| 7 | +from .models import Book, NullableJSONModel, Number |
6 | 8 |
|
7 | 9 |
|
8 | 10 | class NumericLookupTests(TestCase):
|
@@ -66,3 +68,70 @@ def test_eq_and_in(self):
|
66 | 68 | "lookup__book",
|
67 | 69 | [{"$match": {"$and": [{"isbn": {"$in": ("12345", "56789")}}, {"title": "Moby Dick"}]}}],
|
68 | 70 | )
|
| 71 | + |
| 72 | + |
| 73 | +class NullValueLookupTests(MongoTestCaseMixin, TestCase): |
| 74 | + _OPERATOR_PREDICATE_MAP = { |
| 75 | + "eq": lambda field: {field: None}, |
| 76 | + "in": lambda field: {field: {"$in": [None]}}, |
| 77 | + } |
| 78 | + |
| 79 | + @classmethod |
| 80 | + def setUpTestData(cls): |
| 81 | + cls.book_objs = Book.objects.bulk_create( |
| 82 | + Book(title=f"Book {i}", isbn=str(i)) for i in range(5) |
| 83 | + ) |
| 84 | + |
| 85 | + cls.null_objs = NullableJSONModel.objects.bulk_create(NullableJSONModel() for _ in range(5)) |
| 86 | + cls.null_objs.append(NullableJSONModel.objects.create(value={"name": None})) |
| 87 | + cls.unique_id = ObjectId() |
| 88 | + |
| 89 | + def _test_none_filter_nullable_json(self, op, predicate, field): |
| 90 | + with self.assertNumQueries(1) as ctx: |
| 91 | + list(NullableJSONModel.objects.filter(**{f"{field}__{op}": None})) |
| 92 | + self.assertAggregateQuery( |
| 93 | + ctx.captured_queries[0]["sql"], |
| 94 | + "lookup__nullablejsonmodel", |
| 95 | + [{"$match": {"$and": [{"$exists": False}, predicate(field)]}}], |
| 96 | + ) |
| 97 | + self.assertQuerySetEqual( |
| 98 | + NullableJSONModel.objects.filter(**{f"{field}__{op}": None}), |
| 99 | + [], |
| 100 | + ) |
| 101 | + |
| 102 | + def _test_none_filter_binary_operator(self, op, predicate, field): |
| 103 | + with self.assertNumQueries(1) as ctx: |
| 104 | + list(Book.objects.filter(**{f"{field}__{op}": None})) |
| 105 | + self.assertAggregateQuery( |
| 106 | + ctx.captured_queries[0]["sql"], |
| 107 | + "lookup__book", |
| 108 | + [ |
| 109 | + { |
| 110 | + "$match": { |
| 111 | + "$or": [ |
| 112 | + {"$and": [{field: {"$exists": True}}, predicate(field)]}, |
| 113 | + {"$expr": {"$eq": [{"$type": f"${field}"}, "missing"]}}, |
| 114 | + ] |
| 115 | + } |
| 116 | + } |
| 117 | + ], |
| 118 | + ) |
| 119 | + self.assertQuerySetEqual(Book.objects.filter(**{f"{field}__{op}": None}), []) |
| 120 | + |
| 121 | + def _test_with_raw_data(self, model, test_function): |
| 122 | + collection = connection.database.get_collection(model._meta.db_table) |
| 123 | + try: |
| 124 | + collection.insert_one({"_id": self.unique_id}) |
| 125 | + |
| 126 | + for op, predicate in self._OPERATOR_PREDICATE_MAP.items(): |
| 127 | + with self.subTest(op=op): |
| 128 | + test_function(op, predicate) |
| 129 | + |
| 130 | + finally: |
| 131 | + collection.delete_one({"_id": self.unique_id}) |
| 132 | + |
| 133 | + def test_none_filter_nullable_json(self): |
| 134 | + self._test_with_raw_data(NullableJSONModel, self._test_none_filter_nullable_json) |
| 135 | + |
| 136 | + def test_none_filter_binary_operator(self): |
| 137 | + self._test_with_raw_data(Book, self._test_none_filter_binary_operator) |
0 commit comments