Skip to content
Prev Previous commit
Next Next commit
migrated tests to lookup_ and added data
  • Loading branch information
Jibola committed Sep 12, 2025
commit fafe4795557b14d9b144539be1e55c918a7ab294
15 changes: 0 additions & 15 deletions tests/expression_converter_/models.py

This file was deleted.

34 changes: 0 additions & 34 deletions tests/expression_converter_/test_filter_conversion.py

This file was deleted.

7 changes: 7 additions & 0 deletions tests/lookup_/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,10 @@ class Meta:

def __str__(self):
return str(self.num)


class NullableJSONModel(models.Model):
value = models.JSONField(blank=True, null=True)

class Meta:
required_db_features = {"supports_json_field"}
71 changes: 70 additions & 1 deletion tests/lookup_/tests.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from bson import ObjectId
from django.db import connection
from django.test import TestCase

from django_mongodb_backend.test import MongoTestCaseMixin

from .models import Book, Number
from .models import Book, NullableJSONModel, Number


class NumericLookupTests(TestCase):
Expand Down Expand Up @@ -66,3 +68,70 @@ def test_eq_and_in(self):
"lookup__book",
[{"$match": {"$and": [{"isbn": {"$in": ("12345", "56789")}}, {"title": "Moby Dick"}]}}],
)


class NullValueLookupTests(MongoTestCaseMixin, TestCase):
_OPERATOR_PREDICATE_MAP = {
"eq": lambda field: {field: None},
"in": lambda field: {field: {"$in": [None]}},
}

@classmethod
def setUpTestData(cls):
cls.book_objs = Book.objects.bulk_create(
Book(title=f"Book {i}", isbn=str(i)) for i in range(5)
)

cls.null_objs = NullableJSONModel.objects.bulk_create(NullableJSONModel() for _ in range(5))
cls.null_objs.append(NullableJSONModel.objects.create(value={"name": None}))
cls.unique_id = ObjectId()

def _test_none_filter_nullable_json(self, op, predicate, field):
with self.assertNumQueries(1) as ctx:
list(NullableJSONModel.objects.filter(**{f"{field}__{op}": None}))
self.assertAggregateQuery(
ctx.captured_queries[0]["sql"],
"lookup__nullablejsonmodel",
[{"$match": {"$and": [{"$exists": False}, predicate(field)]}}],
)
self.assertQuerySetEqual(
NullableJSONModel.objects.filter(**{f"{field}__{op}": None}),
[],
)

def _test_none_filter_binary_operator(self, op, predicate, field):
with self.assertNumQueries(1) as ctx:
list(Book.objects.filter(**{f"{field}__{op}": None}))
self.assertAggregateQuery(
ctx.captured_queries[0]["sql"],
"lookup__book",
[
{
"$match": {
"$or": [
{"$and": [{field: {"$exists": True}}, predicate(field)]},
{"$expr": {"$eq": [{"$type": f"${field}"}, "missing"]}},
]
}
}
],
)
self.assertQuerySetEqual(Book.objects.filter(**{f"{field}__{op}": None}), [])

def _test_with_raw_data(self, model, test_function):
collection = connection.database.get_collection(model._meta.db_table)
try:
collection.insert_one({"_id": self.unique_id})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to decide to what extent we'll support "sparse" data with missing columns (another example: #275). We have only a few tests with this sort of data, so the current status is basically "ad-hoc, best effort, react to bug reports." I feel we should decide on an official policy and document it so that users have clear expectations. If we move forward with supporting this, we should at least have these tests omit optional fields, so that we don't have the test assertions fail to generate the usual error message (in this case, Book.__str__() fails because title is None.

====================================================================== ERROR: test_none_filter_binary_operator (lookup_.tests.NullValueLookupTests.test_none_filter_binary_operator) (op='exact') ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/tim/code/django-mongodb/tests/lookup_/tests.py", line 105, in _test_with_raw_data test_function(op, predicate, field) File "/home/tim/code/django-mongodb/tests/lookup_/tests.py", line 80, in _test_none_filter_binary_operator self.assertQuerySetEqual( File "/home/tim/code/django/django/test/testcases.py", line 1290, in assertQuerySetEqual return self.assertEqual(list(items), values, msg=msg) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/unittest/case.py", line 885, in assertEqual assertion_func(first, second, msg=msg) File "/usr/lib/python3.12/unittest/case.py", line 1091, in assertListEqual self.assertSequenceEqual(list1, list2, msg, seq_type=list) File "/usr/lib/python3.12/unittest/case.py", line 1068, in assertSequenceEqual difflib.ndiff(pprint.pformat(seq1).splitlines(), ^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/pprint.py", line 62, in pformat underscore_numbers=underscore_numbers).pformat(object) ^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/pprint.py", line 161, in pformat self._format(object, sio, 0, 0, {}, 0) File "/usr/lib/python3.12/pprint.py", line 178, in _format rep = self._repr(object, context, level) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/pprint.py", line 458, in _repr repr, readable, recursive = self.format(object, context.copy(), ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/pprint.py", line 471, in format return self._safe_repr(object, context, maxlevels, level) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/pprint.py", line 622, in _safe_repr orepr, oreadable, orecur = self.format( ^^^^^^^^^^^^ File "/usr/lib/python3.12/pprint.py", line 471, in format return self._safe_repr(object, context, maxlevels, level) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/pprint.py", line 632, in _safe_repr rep = repr(object) ^^^^^^^^^^^^ File "/home/tim/code/django/django/db/models/base.py", line 590, in __repr__ return "<%s: %s>" % (self.__class__.__name__, self) ^^^^ TypeError: __str__ returned non-string (type NoneType) 
Copy link
Contributor Author

@Jibola Jibola Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm actually of the mind -- for this case specifically -- that we do not include this test. In order to reach this case, you have to do ORM-breaking behavior that we don't have a firm policy on. The real "bugs" for this case stem from optimizing embedded documents in a flow that only leverages the Django ORM.

Like you've also just stated we don't have a clear "limit" on what is a valid experience. Let's loop in product and get their take.


for op, predicate in self._OPERATOR_PREDICATE_MAP.items():
with self.subTest(op=op):
test_function(op, predicate)

finally:
collection.delete_one({"_id": self.unique_id})

def test_none_filter_nullable_json(self):
self._test_with_raw_data(NullableJSONModel, self._test_none_filter_nullable_json)

def test_none_filter_binary_operator(self):
self._test_with_raw_data(Book, self._test_none_filter_binary_operator)
Loading