Skip to content

Commit 61db831

Browse files
committed
Add mql check in EMF and EMFA unit test and handle empty set or full set in range queries.
1 parent 849f25f commit 61db831

File tree

6 files changed

+111
-36
lines changed

6 files changed

+111
-36
lines changed

django_mongodb_backend/base.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
import logging
33
import os
44

5-
from django.core.exceptions import ImproperlyConfigured
5+
from bson import Decimal128
6+
from django.core.exceptions import EmptyResultSet, FullResultSet, ImproperlyConfigured
67
from django.db import DEFAULT_DB_ALIAS
78
from django.db.backends.base.base import BaseDatabaseWrapper
89
from django.db.backends.utils import debug_transaction
@@ -143,14 +144,21 @@ def _isnull_operator_match(a, b):
143144
}
144145

145146
def range_match(a, b):
146-
## TODO: MAKE A TEST TO TEST WHEN BOTH ENDS ARE NONE. WHAT SHALL I RETURN?
147147
conditions = []
148-
if b[0] is not None:
148+
start, end = b
149+
if start is not None:
149150
conditions.append({a: {"$gte": b[0]}})
150-
if b[1] is not None:
151+
if end is not None:
151152
conditions.append({a: {"$lte": b[1]}})
152153
if not conditions:
153-
return {"$literal": True}
154+
raise FullResultSet
155+
if start is not None and end is not None:
156+
if isinstance(start, Decimal128):
157+
start = start.to_decimal()
158+
if isinstance(end, Decimal128):
159+
end = end.to_decimal()
160+
if start > end:
161+
raise EmptyResultSet
154162
return {"$and": conditions}
155163

156164
# match, path, find? don't know which name use.

django_mongodb_backend/fields/json.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,7 @@ def key_transform_is_null_expr(self, compiler, connection):
175175

176176
def key_transform_is_null_path(self, compiler, connection):
177177
"""
178-
Return MQL to check the nullability of a key.
179-
180-
If `isnull=True`, the query matches objects where the key is missing or the
181-
root column is null. If `isnull=False`, the query negates the result to
182-
match objects where the key exists.
183-
184-
Reference: https://code.djangoproject.com/ticket/32252
178+
Return MQL to check the nullability of a key using the operator $exists.
185179
"""
186180
lhs_mql = process_lhs(self, compiler, connection, as_path=True)
187181
rhs_mql = process_rhs(self, compiler, connection, as_path=True)

django_mongodb_backend/test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Not a public API."""
22

3-
from bson import SON, ObjectId
3+
from bson import SON, Decimal128, ObjectId
44

55

66
class MongoTestCaseMixin:
@@ -16,6 +16,6 @@ def assertAggregateQuery(self, query, expected_collection, expected_pipeline):
1616
self.assertEqual(operator, "aggregate")
1717
self.assertEqual(collection, expected_collection)
1818
self.assertEqual(
19-
eval(pipeline[:-1], {"SON": SON, "ObjectId": ObjectId}, {}), # noqa: S307
19+
eval(pipeline[:-1], {"SON": SON, "ObjectId": ObjectId, "Decimal128": Decimal128}, {}), # noqa: S307
2020
expected_pipeline,
2121
)

tests/lookup_/tests.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,38 @@
1+
from bson import SON
12
from django.test import TestCase
23

34
from django_mongodb_backend.test import MongoTestCaseMixin
45

56
from .models import Book, Number
67

78

8-
class NumericLookupTests(TestCase):
9+
class NumericLookupTests(MongoTestCaseMixin, TestCase):
910
@classmethod
1011
def setUpTestData(cls):
1112
cls.objs = Number.objects.bulk_create(Number(num=x) for x in range(5))
1213
# Null values should be excluded in less than queries.
13-
Number.objects.create()
14+
cls.null_number = Number.objects.create()
1415

1516
def test_lt(self):
1617
self.assertQuerySetEqual(Number.objects.filter(num__lt=3), self.objs[:3])
1718

1819
def test_lte(self):
1920
self.assertQuerySetEqual(Number.objects.filter(num__lte=3), self.objs[:4])
2021

22+
def test_empty_range(self):
23+
with self.assertNumQueries(0):
24+
self.assertQuerySetEqual(Number.objects.filter(num__range=[3, 1]), [])
25+
26+
def test_full_range(self):
27+
with self.assertNumQueries(1) as ctx:
28+
self.assertQuerySetEqual(
29+
Number.objects.filter(num__range=[None, None]), [self.null_number, *self.objs]
30+
)
31+
query = ctx.captured_queries[0]["sql"]
32+
self.assertAggregateQuery(
33+
query, "lookup__number", [{"$addFields": {"num": "$num"}}, {"$sort": SON([("num", 1)])}]
34+
)
35+
2136

2237
class RegexTests(MongoTestCaseMixin, TestCase):
2338
def test_mql(self):

tests/model_fields_/test_embedded_model.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010
Max,
1111
OuterRef,
1212
Sum,
13+
Value,
1314
)
14-
from django.db.models.expressions import Value
1515
from django.test import SimpleTestCase, TestCase
1616
from django.test.utils import isolate_apps
1717

1818
from django_mongodb_backend.fields import EmbeddedModelField
1919
from django_mongodb_backend.models import EmbeddedModel
20+
from django_mongodb_backend.test import MongoTestCaseMixin
2021

2122
from .models import (
2223
Address,
@@ -131,7 +132,7 @@ def test_embedded_model_field_respects_db_column(self):
131132
self.assertEqual(query[0]["data"]["integer_"], 5)
132133

133134

134-
class QueryingTests(TestCase):
135+
class QueryingTests(MongoTestCaseMixin, TestCase):
135136
@classmethod
136137
def setUpTestData(cls):
137138
cls.objs = [
@@ -585,14 +586,16 @@ def test_nested(self):
585586
)
586587
self.assertCountEqual(Book.objects.filter(author__address__city="NYC"), [obj])
587588

588-
def test_annotate(self):
589+
def test_filter_by_simple_annotate(self):
589590
obj = Book.objects.create(
590591
author=Author(name="Shakespeare", age=55, address=Address(city="NYC", state="NY"))
591592
)
592-
book_from_ny = (
593-
Book.objects.annotate(city=F("author__address__city")).filter(city="NYC").first()
594-
)
595-
self.assertCountEqual(book_from_ny.city, obj.author.address.city)
593+
with self.assertNumQueries(1) as ctx:
594+
book_from_ny = (
595+
Book.objects.annotate(city=F("author__address__city")).filter(city="NYC").first()
596+
)
597+
self.assertCountEqual(book_from_ny.city, obj.author.address.city)
598+
self.assertIn("{'$match': {'author.address.city': 'NYC'}}", ctx.captured_queries[0]["sql"])
596599

597600

598601
class ArrayFieldTests(TestCase):

tests/model_fields_/test_embedded_model_array.py

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from django_mongodb_backend.fields import ArrayField, EmbeddedModelArrayField
1313
from django_mongodb_backend.models import EmbeddedModel
14+
from django_mongodb_backend.test import MongoTestCaseMixin
1415

1516
from .models import Artifact, Audit, Exhibit, Movie, Restoration, Review, Section, Tour
1617

@@ -85,7 +86,7 @@ def test_embedded_model_field_respects_db_column(self):
8586
self.assertEqual(query[0]["reviews"][0]["title_"], "Awesome")
8687

8788

88-
class QueryingTests(TestCase):
89+
class QueryingTests(MongoTestCaseMixin, TestCase):
8990
@classmethod
9091
def setUpTestData(cls):
9192
cls.egypt = Exhibit.objects.create(
@@ -178,15 +179,55 @@ def setUpTestData(cls):
178179
cls.audit_2 = Audit.objects.create(section_number=2, reviewed=True)
179180
cls.audit_3 = Audit.objects.create(section_number=5, reviewed=False)
180181

181-
def test_exact(self):
182-
self.assertCountEqual(
183-
Exhibit.objects.filter(sections__number=1), [self.egypt, self.wonders]
182+
def test_exact_expr(self):
183+
with self.assertNumQueries(1) as ctx:
184+
self.assertCountEqual(
185+
Exhibit.objects.filter(sections__number=Value(2) - 1), [self.egypt, self.wonders]
186+
)
187+
query = ctx.captured_queries[0]["sql"]
188+
self.assertAggregateQuery(
189+
query,
190+
"model_fields__exhibit",
191+
[
192+
{
193+
"$match": {
194+
"$expr": {
195+
"$anyElementTrue": {
196+
"$ifNull": [
197+
{
198+
"$map": {
199+
"input": "$sections",
200+
"as": "item",
201+
"in": {
202+
"$eq": [
203+
"$$item.number",
204+
{
205+
"$subtract": [
206+
{"$literal": 2},
207+
{"$literal": 1},
208+
]
209+
},
210+
]
211+
},
212+
}
213+
},
214+
[],
215+
]
216+
}
217+
}
218+
}
219+
}
220+
],
184221
)
185222

186-
def test_array_index(self):
187-
self.assertCountEqual(
188-
Exhibit.objects.filter(sections__0__number=1),
189-
[self.egypt, self.wonders],
223+
def test_exact_path(self):
224+
with self.assertNumQueries(1) as ctx:
225+
self.assertCountEqual(
226+
Exhibit.objects.filter(sections__number=1), [self.egypt, self.wonders]
227+
)
228+
query = ctx.captured_queries[0]["sql"]
229+
self.assertAggregateQuery(
230+
query, "model_fields__exhibit", [{"$match": {"sections.number": 1}}]
190231
)
191232

192233
def test_array_index_expr(self):
@@ -316,8 +357,20 @@ def test_filter_unsupported_lookups_in_json(self):
316357
kwargs = {f"main_section__artifacts__metadata__origin__{lookup}": ["Pergamon", "Egypt"]}
317358
with CaptureQueriesContext(connection) as captured_queries:
318359
self.assertCountEqual(Exhibit.objects.filter(**kwargs), [])
319-
self.assertIn(
320-
f"'main_section.artifacts.metadata.origin.{lookup}':", captured_queries[0]["sql"]
360+
query = captured_queries[0]["sql"]
361+
self.assertAggregateQuery(
362+
query,
363+
"model_fields__exhibit",
364+
[
365+
{
366+
"$match": {
367+
f"main_section.artifacts.metadata.origin.{lookup}": [
368+
"Pergamon",
369+
"Egypt",
370+
]
371+
}
372+
}
373+
],
321374
)
322375

323376
def test_len(self):
@@ -421,10 +474,12 @@ def test_nested_lookup(self):
421474
with self.assertRaisesMessage(ValueError, msg):
422475
Exhibit.objects.filter(sections__artifacts__name="")
423476

424-
def test_foreign_field_exact(self):
477+
def test_foreign_field_exact_path(self):
425478
"""Querying from a foreign key to an EmbeddedModelArrayField."""
426-
qs = Tour.objects.filter(exhibit__sections__number=1)
427-
self.assertCountEqual(qs, [self.egypt_tour, self.wonders_tour])
479+
with self.assertNumQueries(1) as ctx:
480+
qs = Tour.objects.filter(exhibit__sections__number=1)
481+
self.assertCountEqual(qs, [self.egypt_tour, self.wonders_tour])
482+
self.assertNotIn("anyElementTrue", ctx.captured_queries[0]["sql"])
428483

429484
def test_foreign_field_exact_expr(self):
430485
"""Querying from a foreign key to an EmbeddedModelArrayField."""

0 commit comments

Comments
 (0)