Skip to content

Commit 3789b9f

Browse files
WaVEVtimgraham
andcommitted
refactor query generation
fixes #13, #22 Co-authored-by: Tim Graham <[email protected]>
1 parent e3f02fc commit 3789b9f

File tree

10 files changed

+204
-260
lines changed

10 files changed

+204
-260
lines changed

.github/workflows/test-python.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ jobs:
8080
lookup
8181
model_fields
8282
or_lookups
83+
queries.tests.Ticket12807Tests.test_ticket_12807
8384
sessions_tests
8485
timezones
8586
update

django_mongodb/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,13 @@
55
from .utils import check_django_compatability
66

77
check_django_compatability()
8+
9+
from .expressions import register_expressions # noqa: E402
10+
from .functions import register_functions # noqa: E402
11+
from .lookups import register_lookups # noqa: E402
12+
from .query import register_nodes # noqa: E402
13+
14+
register_expressions()
15+
register_functions()
16+
register_lookups()
17+
register_nodes()

django_mongodb/base.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import re
2+
13
from django.core.exceptions import ImproperlyConfigured
24
from django.db.backends.base.base import BaseDatabaseWrapper
35
from django.db.backends.signals import connection_created
@@ -10,6 +12,7 @@
1012
from .features import DatabaseFeatures
1113
from .introspection import DatabaseIntrospection
1214
from .operations import DatabaseOperations
15+
from .query_utils import safe_regex
1316
from .schema import DatabaseSchemaEditor
1417
from .utils import CollectionDebugWrapper
1518

@@ -52,11 +55,23 @@ class DatabaseWrapper(BaseDatabaseWrapper):
5255
"UUIDField": "string",
5356
}
5457
operators = {
55-
"exact": "= %s",
56-
"gt": "> %s",
57-
"gte": ">= %s",
58-
"lt": "< %s",
59-
"lte": "<= %s",
58+
"exact": lambda val: val,
59+
"gt": lambda val: {"$gt": val},
60+
"gte": lambda val: {"$gte": val},
61+
"lt": lambda val: {"$lt": val},
62+
"lte": lambda val: {"$lte": val},
63+
"in": lambda val: {"$in": val},
64+
"range": lambda val: {"$gte": val[0], "$lte": val[1]},
65+
"isnull": lambda val: None if val else {"$ne": None},
66+
"iexact": safe_regex("^%s$", re.IGNORECASE),
67+
"startswith": safe_regex("^%s"),
68+
"istartswith": safe_regex("^%s", re.IGNORECASE),
69+
"endswith": safe_regex("%s$"),
70+
"iendswith": safe_regex("%s$", re.IGNORECASE),
71+
"contains": safe_regex("%s"),
72+
"icontains": safe_regex("%s", re.IGNORECASE),
73+
"regex": lambda val: re.compile(val),
74+
"iregex": lambda val: re.compile(val, re.IGNORECASE),
6075
}
6176

6277
display_name = "MongoDB"

django_mongodb/compiler.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from django.core.exceptions import EmptyResultSet
1+
from django.core.exceptions import EmptyResultSet, FullResultSet
22
from django.db import DatabaseError, IntegrityError, NotSupportedError
33
from django.db.models import NOT_PROVIDED, Count, Expression, Value
44
from django.db.models.aggregates import Aggregate
@@ -136,7 +136,10 @@ def build_query(self, columns=None):
136136
self.check_query()
137137
self.setup_query()
138138
query = self.query_class(self, columns)
139-
query.add_filters(self.query.where)
139+
try:
140+
query.mongo_query = self.query.where.as_mql(self, self.connection)
141+
except FullResultSet:
142+
query.mongo_query = {}
140143
query.order_by(self._get_ordering())
141144
return query
142145

django_mongodb/expressions.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from django.db.models.expressions import Col
2+
3+
4+
def col(self, compiler, connection): # noqa: ARG001
5+
return self.target.column
6+
7+
8+
def register_expressions():
9+
Col.as_mql = col

django_mongodb/features.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,10 @@ class DatabaseFeatures(BaseDatabaseFeatures):
1616
# cannot encode object: <django.db.models.expressions.DatabaseDefault
1717
"basic.tests.ModelInstanceCreationTests.test_save_primary_with_db_default",
1818
# Date lookups aren't implemented: https://github.com/mongodb-labs/django-mongodb/issues/9
19-
# (e.g. 'ExtractMonth' object has no attribute 'alias')
19+
# (e.g. ExtractWeekDay is not supported.)
2020
"basic.tests.ModelLookupTest.test_does_not_exist",
2121
"basic.tests.ModelLookupTest.test_equal_lookup",
2222
"basic.tests.ModelLookupTest.test_rich_lookup",
23-
"basic.tests.ModelLookupTest.test_too_many",
2423
"basic.tests.ModelTest.test_year_lookup_edge_case",
2524
"lookup.tests.LookupTests.test_chain_date_time_lookups",
2625
"lookup.test_timefield.TimeFieldLookupTests.test_hour_lookups",
@@ -30,29 +29,25 @@ class DatabaseFeatures(BaseDatabaseFeatures):
3029
"timezones.tests.NewDatabaseTests.test_query_convert_timezones",
3130
"timezones.tests.NewDatabaseTests.test_query_datetime_lookups",
3231
"timezones.tests.NewDatabaseTests.test_query_datetime_lookups_in_other_timezone",
33-
# 'NulledTransform' object has no attribute 'alias'
32+
# 'NulledTransform' object has no attribute 'as_mql'.
3433
"lookup.tests.LookupTests.test_exact_none_transform",
3534
# "Save with update_fields did not affect any rows."
3635
"basic.tests.SelectOnSaveTests.test_select_on_save_lying_update",
3736
# filtering on large decimalfield, see https://code.djangoproject.com/ticket/34590
3837
# for some background.
3938
"model_fields.test_decimalfield.DecimalFieldTests.test_lookup_decimal_larger_than_max_digits",
4039
"model_fields.test_decimalfield.DecimalFieldTests.test_lookup_really_big_value",
41-
# 'TruncDate' object has no attribute 'alias'
40+
# 'TruncDate' object has no attribute 'as_mql'.
4241
"model_fields.test_datetimefield.DateTimeFieldTests.test_lookup_date_with_use_tz",
4342
"model_fields.test_datetimefield.DateTimeFieldTests.test_lookup_date_without_use_tz",
44-
# Incorrect empty QuerySet handling: https://github.com/mongodb-labs/django-mongodb/issues/22
45-
"lookup.tests.LookupTests.test_in",
46-
"or_lookups.tests.OrLookupsTests.test_empty_in",
4743
# Slicing with QuerySet.count() doesn't work.
4844
"lookup.tests.LookupTests.test_count",
49-
# Custom lookups not supported.
50-
"lookup.tests.LookupTests.test_custom_lookup_none_rhs",
51-
# Lookup in order_by() not supported: argument of type 'LessThan' is not iterable
45+
# Lookup in order_by() not supported:
46+
# unsupported operand type(s) for %: 'function' and 'str'
5247
"lookup.tests.LookupQueryingTests.test_lookup_in_order_by",
5348
# annotate() after values() doesn't raise NotSupportedError.
5449
"lookup.tests.LookupTests.test_exact_query_rhs_with_selected_columns",
55-
# tuple index out of range in _normalize_lookup_value()
50+
# tuple index out of range in process_rhs()
5651
"lookup.tests.LookupTests.test_exact_sliced_queryset_limit_one",
5752
"lookup.tests.LookupTests.test_exact_sliced_queryset_limit_one_offset",
5853
# Regex lookup doesn't work on non-string fields.

django_mongodb/functions.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from django.db import NotSupportedError
2+
from django.db.models.expressions import Col
3+
from django.db.models.functions.datetime import Extract
4+
5+
from .query_utils import process_lhs
6+
7+
8+
def extract(self, compiler, connection):
9+
lhs_mql = process_lhs(self, compiler, connection)
10+
if self.lookup_name == "week":
11+
operator = "$week"
12+
elif self.lookup_name == "month":
13+
operator = "$month"
14+
elif self.lookup_name == "year":
15+
operator = "$year"
16+
else:
17+
raise NotSupportedError("%s is not supported." % self.__class__.__name__)
18+
if isinstance(self.lhs, Col):
19+
lhs_mql = f"${lhs_mql}"
20+
return {operator: lhs_mql}
21+
22+
23+
def register_functions():
24+
Extract.as_mql = extract

django_mongodb/lookups.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from django.db import NotSupportedError
2+
from django.db.models.expressions import Col
3+
from django.db.models.fields.related_lookups import In, MultiColSource, RelatedIn
4+
from django.db.models.lookups import BuiltinLookup, Exact, IsNull, UUIDTextMixin
5+
6+
from .query_utils import process_lhs, process_rhs
7+
8+
9+
def builtin_lookup(self, compiler, connection):
10+
lhs_mql = process_lhs(self, compiler, connection)
11+
value = process_rhs(self, compiler, connection)
12+
rhs_mql = connection.operators[self.lookup_name](value)
13+
return {lhs_mql: rhs_mql}
14+
15+
16+
def exact(self, compiler, connection):
17+
lhs_mql = process_lhs(self, compiler, connection)
18+
value = process_rhs(self, compiler, connection)
19+
if isinstance(self.lhs, Col):
20+
lhs_mql = f"${lhs_mql}"
21+
return {"$expr": {"$eq": [lhs_mql, value]}}
22+
23+
24+
def in_(self, compiler, connection):
25+
if isinstance(self.lhs, MultiColSource):
26+
raise NotImplementedError("MultiColSource is not supported.")
27+
return builtin_lookup(self, compiler, connection)
28+
29+
30+
def is_null(self, compiler, connection):
31+
if not isinstance(self.rhs, bool):
32+
raise ValueError("The QuerySet value for an isnull lookup must be True or False.")
33+
lhs_mql = process_lhs(self, compiler, connection)
34+
rhs_mql = connection.operators["isnull"](self.rhs)
35+
return {lhs_mql: rhs_mql}
36+
37+
38+
def uuid_text_mixin(self, compiler, connection): # noqa: ARG001
39+
raise NotSupportedError("Pattern lookups on UUIDField are not supported.")
40+
41+
42+
def register_lookups():
43+
BuiltinLookup.as_mql = builtin_lookup
44+
Exact.as_mql = exact
45+
In.as_mql = RelatedIn.as_mql = in_
46+
IsNull.as_mql = is_null
47+
UUIDTextMixin.as_mql = uuid_text_mixin

0 commit comments

Comments
 (0)