-
Notifications
You must be signed in to change notification settings - Fork 29
INTPYTHON-749: Fix bug in null matching on queryoptimizer #401
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 6 commits
6f2052a
139d0b4
fafe479
50177bc
e8011b9
0ca2a5e
4af823a
0161dfb
0b29263
7802896
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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): | ||
|
@@ -66,3 +68,72 @@ def test_eq_and_in(self): | |
"lookup__book", | ||
[{"$match": {"$and": [{"isbn": {"$in": ("12345", "56789")}}, {"title": "Moby Dick"}]}}], | ||
) | ||
|
||
|
||
class NullValueLookupTests(MongoTestCaseMixin, TestCase): | ||
_OPERATOR_PREDICATE_MAP = { | ||
"exact": 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: | ||
self.assertQuerySetEqual( | ||
NullableJSONModel.objects.filter( | ||
**{f"{field}__{op}": [None] if op == "in" else None} | ||
), | ||
[], | ||
) | ||
self.assertAggregateQuery( | ||
ctx.captured_queries[0]["sql"], | ||
"lookup__nullablejsonmodel", | ||
[{"$match": {"$and": [{"$exists": False}, predicate(field)]}}], | ||
) | ||
|
||
def _test_none_filter_binary_operator(self, op, predicate, field): | ||
with self.assertNumQueries(1) as ctx: | ||
self.assertQuerySetEqual( | ||
Book.objects.filter(**{f"{field}__{op}": [None] if op == "in" else None}), [] | ||
) | ||
self.assertAggregateQuery( | ||
ctx.captured_queries[0]["sql"], | ||
"lookup__book", | ||
[ | ||
{ | ||
"$match": { | ||
"$or": [ | ||
{"$and": [{field: {"$exists": True}}, predicate(field)]}, | ||
{"$expr": {"$eq": [{"$type": f"${field}"}, "missing"]}}, | ||
] | ||
} | ||
} | ||
], | ||
) | ||
|
||
def _test_with_raw_data(self, model, test_function, field): | ||
collection = connection.database.get_collection(model._meta.db_table) | ||
try: | ||
collection.insert_one({"_id": self.unique_id}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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,
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Jibola marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
for op, predicate in self._OPERATOR_PREDICATE_MAP.items(): | ||
with self.subTest(op=op): | ||
test_function(op, predicate, field) | ||
|
||
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, "value") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This style of writing tests with generic methods calling other generic methods doesn't make my heart sing. It seems extremely tedious to understand and confirm that the test actually does what it's designed to do. I saw some of this in the converter patch and let it slide there, but I think it's best to avoid going forward. There can be a role for helper functions and assertions, but when there's special casing like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can agree I don't need to shoe-horn "in" into the test, so I don't mind separating them. I will say, though, it is a matter of what makes the most sense. If readability is the issue then that's more of a lack of explicit documentation on authorship. For context on this; I originally had more operators, but now that it went back down to 2, it's less valuable. In the case where the same rote operation became needlessly repetitious, I do think it still holds value. |
||
|
||
def test_none_filter_binary_operator(self): | ||
self._test_with_raw_data(Book, self._test_none_filter_binary_operator, "title") |
Uh oh!
There was an error while loading. Please reload this page.