Skip to content

Commit a622a15

Browse files
committed
Change operators for expression in lookups.
1 parent 2f86210 commit a622a15

File tree

7 files changed

+37
-61
lines changed

7 files changed

+37
-61
lines changed

django_mongodb/base.py

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from .features import DatabaseFeatures
1313
from .introspection import DatabaseIntrospection
1414
from .operations import DatabaseOperations
15-
from .query_utils import safe_regex
15+
from .query_utils import regex_match_expression
1616
from .schema import DatabaseSchemaEditor
1717
from .utils import CollectionDebugWrapper
1818

@@ -74,30 +74,23 @@ class DatabaseWrapper(BaseDatabaseWrapper):
7474
"iendswith": "LIKE UPPER(%s)",
7575
}
7676
mongo_operators = {
77-
"exact": lambda val: val,
78-
"gt": lambda val: {"$gt": val},
79-
"gte": lambda val: {"$gte": val},
80-
"lt": lambda val: {"$lt": val},
81-
"lte": lambda val: {"$lte": val},
82-
"in": lambda val: {"$in": val},
83-
"range": lambda val: {"$gte": val[0], "$lte": val[1]},
84-
"isnull": lambda val: None if val else {"$ne": None},
85-
"iexact": safe_regex("^%s$", re.IGNORECASE),
86-
"startswith": safe_regex("^%s"),
87-
"istartswith": safe_regex("^%s", re.IGNORECASE),
88-
"endswith": safe_regex("%s$"),
89-
"iendswith": safe_regex("%s$", re.IGNORECASE),
90-
"contains": safe_regex("%s"),
91-
"icontains": safe_regex("%s", re.IGNORECASE),
92-
"regex": lambda val: re.compile(val),
93-
"iregex": lambda val: re.compile(val, re.IGNORECASE),
94-
}
95-
mongo_aggregations = {
9677
"exact": lambda a, b: {"$eq": [a, b]},
9778
"gt": lambda a, b: {"$gt": [a, b]},
9879
"gte": lambda a, b: {"$gte": [a, b]},
9980
"lt": lambda a, b: {"$lt": [a, b]},
10081
"lte": lambda a, b: {"$lte": [a, b]},
82+
"in": lambda a, b: {"$in": [a, b]},
83+
"isnull": lambda a, b: {"$eq" if b else "$ne": [a, None]},
84+
"range": lambda a, b: {"$and": [{"$gte": [a, b[0]]}, {"$lte": [a, b[1]]}]},
85+
"iexact": lambda a, b: regex_match_expression(a, b, "^%s$", re.IGNORECASE),
86+
"startswith": lambda a, b: regex_match_expression(a, b, "^%s"),
87+
"istartswith": lambda a, b: regex_match_expression(a, b, "^%s", re.IGNORECASE),
88+
"endswith": lambda a, b: regex_match_expression(a, b, "%s$"),
89+
"iendswith": lambda a, b: regex_match_expression(a, b, "%s$", re.IGNORECASE),
90+
"contains": lambda a, b: regex_match_expression(a, b, "%s"),
91+
"icontains": lambda a, b: regex_match_expression(a, b, "%s", re.IGNORECASE),
92+
"regex": lambda a, b: regex_match_expression(a, "", f"{b}%s"),
93+
"iregex": lambda a, b: regex_match_expression(a, "", f"{b}%s", re.IGNORECASE),
10194
}
10295

10396
display_name = "MongoDB"

django_mongodb/compiler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def build_query(self, columns=None):
138138
self.setup_query()
139139
query = self.query_class(self, columns)
140140
try:
141-
query.mongo_query = self.query.where.as_mql(self, self.connection)
141+
query.mongo_query = {"$expr": self.query.where.as_mql(self, self.connection)}
142142
except FullResultSet:
143143
query.mongo_query = {}
144144
query.order_by(self._get_ordering())

django_mongodb/expressions.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,9 @@ def col(self, compiler, connection): # noqa: ARG001
66

77

88
def value(self, compiler, connection): # noqa: ARG001
9-
return self.value
10-
11-
12-
def value_agg(self, compiler, connection): # noqa: ARG001
139
return {"$literal": self.value}
1410

1511

1612
def register_expressions():
1713
Col.as_mql = col
1814
Value.as_mql = value
19-
Value.as_mql_agg = value_agg

django_mongodb/features.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
2121
# Database defaults not supported: bson.errors.InvalidDocument:
2222
# cannot encode object: <django.db.models.expressions.DatabaseDefault
2323
"basic.tests.ModelInstanceCreationTests.test_save_primary_with_db_default",
24-
# Query for chained lookups not generated correctly.
25-
"lookup.tests.LookupTests.test_chain_date_time_lookups",
2624
# 'NulledTransform' object has no attribute 'as_mql'.
2725
"lookup.tests.LookupTests.test_exact_none_transform",
2826
# "Save with update_fields did not affect any rows."
@@ -58,8 +56,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
5856
"db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_trunc_timezone_applied_before_truncation",
5957
# Coalesce() with expressions doesn't generate correct query.
6058
"db_functions.comparison.test_coalesce.CoalesceTests.test_mixed_values",
61-
# $and must be an array
62-
"db_functions.tests.FunctionTests.test_function_as_filter",
6359
# pk__in=queryset doesn't work because subqueries aren't a thing in
6460
# MongoDB.
6561
"annotations.tests.NonAggregateAnnotationTestCase.test_annotation_and_alias_filter_in_subquery",
@@ -165,20 +161,16 @@ class DatabaseFeatures(BaseDatabaseFeatures):
165161
"annotations.tests.NonAggregateAnnotationTestCase.test_annotation_exists_none_query",
166162
"lookup.tests.LookupTests.test_exact_exists",
167163
"lookup.tests.LookupTests.test_nested_outerref_lhs",
168-
"lookup.tests.LookupQueryingTests.test_filter_exists_lhs",
169-
# QuerySet.alias(greater=GreaterThan(F("year"), 1910)).filter(greater=True)
170-
# generates incorrect an incorrect query:
171-
# {'$expr': {'$eq': [{'year': {'$gt': 1910}}, True]}}}
172-
"lookup.tests.LookupQueryingTests.test_alias",
164+
# QuerySet.alias() doesn't work.
165+
"annotations.tests.AliasTests.test_basic_alias_f_transform_annotation",
166+
"annotations.tests.NonAggregateAnnotationTestCase.test_annotation_and_alias_filter_in_subquery",
173167
# annotate() with combined expressions doesn't work:
174168
# 'WhereNode' object has no attribute 'field'
175169
"lookup.tests.LookupQueryingTests.test_combined_annotated_lookups_in_filter",
176170
"lookup.tests.LookupQueryingTests.test_combined_annotated_lookups_in_filter_false",
177171
"lookup.tests.LookupQueryingTests.test_combined_lookups",
178172
# Case not supported.
179173
"lookup.tests.LookupQueryingTests.test_conditional_expression",
180-
# Using expression in filter() doesn't work.
181-
"lookup.tests.LookupQueryingTests.test_filter_lookup_lhs",
182174
# Subquery not supported.
183175
"annotations.tests.NonAggregateAnnotationTestCase.test_empty_queryset_annotation",
184176
"db_functions.comparison.test_coalesce.CoalesceTests.test_empty_queryset",

django_mongodb/lookups.py

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,14 @@
11
from django.db import NotSupportedError
22
from django.db.models.fields.related_lookups import In, MultiColSource, RelatedIn
3-
from django.db.models.lookups import BuiltinLookup, Exact, IsNull, UUIDTextMixin
3+
from django.db.models.lookups import BuiltinLookup, IsNull, UUIDTextMixin
44

55
from .query_utils import process_lhs, process_rhs
66

77

88
def builtin_lookup(self, compiler, connection):
9-
lhs_mql = process_lhs(self, compiler, connection, bare_column_ref=True)
10-
value = process_rhs(self, compiler, connection)
11-
rhs_mql = connection.mongo_operators[self.lookup_name](value)
12-
return {lhs_mql: rhs_mql}
13-
14-
15-
def builtin_lookup_agg(self, compiler, connection):
16-
lhs_mql = process_lhs(self, compiler, connection)
17-
value = process_rhs(self, compiler, connection)
18-
return connection.mongo_aggregations[self.lookup_name](lhs_mql, value)
19-
20-
21-
def exact(self, compiler, connection):
229
lhs_mql = process_lhs(self, compiler, connection)
2310
value = process_rhs(self, compiler, connection)
24-
return {"$expr": {"$eq": [lhs_mql, value]}}
11+
return connection.mongo_operators[self.lookup_name](lhs_mql, value)
2512

2613

2714
def in_(self, compiler, connection):
@@ -33,9 +20,8 @@ def in_(self, compiler, connection):
3320
def is_null(self, compiler, connection):
3421
if not isinstance(self.rhs, bool):
3522
raise ValueError("The QuerySet value for an isnull lookup must be True or False.")
36-
lhs_mql = process_lhs(self, compiler, connection, bare_column_ref=True)
37-
rhs_mql = connection.mongo_operators["isnull"](self.rhs)
38-
return {lhs_mql: rhs_mql}
23+
lhs_mql = process_lhs(self, compiler, connection)
24+
return connection.mongo_operators["isnull"](lhs_mql, self.rhs)
3925

4026

4127
def uuid_text_mixin(self, compiler, connection): # noqa: ARG001
@@ -44,8 +30,6 @@ def uuid_text_mixin(self, compiler, connection): # noqa: ARG001
4430

4531
def register_lookups():
4632
BuiltinLookup.as_mql = builtin_lookup
47-
BuiltinLookup.as_mql_agg = builtin_lookup_agg
48-
Exact.as_mql = exact
4933
In.as_mql = RelatedIn.as_mql = in_
5034
IsNull.as_mql = is_null
5135
UUIDTextMixin.as_mql = uuid_text_mixin

django_mongodb/query.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,7 @@ def get_cursor(self):
9191
column = expr.target.column
9292
except AttributeError:
9393
# Generate the MQL for an annotation.
94-
method = "as_mql_agg" if hasattr(expr, "as_mql_agg") else "as_mql"
95-
fields[name] = getattr(expr, method)(self.compiler, self.connection)
94+
fields[name] = expr.as_mql(self.compiler, self.connection)
9695
else:
9796
# If name != column, then this is an annotatation referencing
9897
# another column.
@@ -155,8 +154,7 @@ def where_node(self, compiler, connection):
155154
raise FullResultSet
156155

157156
if self.negated and mql:
158-
lhs, rhs = next(iter(mql.items()))
159-
mql = {lhs: {"$not": rhs}}
157+
mql = {"$eq": [mql, {"$literal": False}]}
160158

161159
return mql
162160

django_mongodb/query_utils.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,17 @@ def wrapper(value):
5555

5656
wrapper.__name__ = "safe_regex (%r)" % regex
5757
return wrapper
58+
59+
60+
def regex_match_expression(field, value, regex, *re_args, **re_kwargs):
61+
regex = safe_regex(regex, *re_args, **re_kwargs)(value)
62+
options = ""
63+
if regex.flags & re.I:
64+
options += "i"
65+
if regex.flags & re.M:
66+
options += "m"
67+
if regex.flags & re.S:
68+
options += "s"
69+
# if regex.flags & re.U:
70+
# options += "u"
71+
return {"$regexMatch": {"input": field, "regex": regex.pattern, "options": options}}

0 commit comments

Comments
 (0)