Skip to content

Commit 6ce79ac

Browse files
WaVEVtimgraham
authored andcommitted
make all lookups use $expr
1 parent 138f5ff commit 6ce79ac

File tree

7 files changed

+29
-97
lines changed

7 files changed

+29
-97
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
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(a, b, "^%s$", re.IGNORECASE),
86+
"startswith": lambda a, b: regex_match(a, b, "^%s"),
87+
"istartswith": lambda a, b: regex_match(a, b, "^%s", re.IGNORECASE),
88+
"endswith": lambda a, b: regex_match(a, b, "%s$"),
89+
"iendswith": lambda a, b: regex_match(a, b, "%s$", re.IGNORECASE),
90+
"contains": lambda a, b: regex_match(a, b, "%s"),
91+
"icontains": lambda a, b: regex_match(a, b, "%s", re.IGNORECASE),
92+
"regex": lambda a, b: regex_match(a, "", f"{b}%s"),
93+
"iregex": lambda a, b: regex_match(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 & 35 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."
@@ -56,11 +54,11 @@ class DatabaseFeatures(BaseDatabaseFeatures):
5654
# the result back to UTC.
5755
"db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_trunc_func_with_timezone",
5856
"db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_trunc_timezone_applied_before_truncation",
59-
# $and must be an array
60-
"db_functions.tests.FunctionTests.test_function_as_filter",
6157
# pk__in=queryset doesn't work because subqueries aren't a thing in
6258
# MongoDB.
6359
"annotations.tests.NonAggregateAnnotationTestCase.test_annotation_and_alias_filter_in_subquery",
60+
# Length of null considered zero rather than null.
61+
"db_functions.text.test_length.LengthTests.test_basic",
6462
}
6563

6664
django_test_skips = {
@@ -164,19 +162,13 @@ class DatabaseFeatures(BaseDatabaseFeatures):
164162
"lookup.tests.LookupTests.test_exact_exists",
165163
"lookup.tests.LookupTests.test_nested_outerref_lhs",
166164
"lookup.tests.LookupQueryingTests.test_filter_exists_lhs",
167-
# QuerySet.alias(greater=GreaterThan(F("year"), 1910)).filter(greater=True)
168-
# generates incorrect an incorrect query:
169-
# {'$expr': {'$eq': [{'year': {'$gt': 1910}}, True]}}}
170-
"lookup.tests.LookupQueryingTests.test_alias",
171165
# annotate() with combined expressions doesn't work:
172166
# 'WhereNode' object has no attribute 'field'
173167
"lookup.tests.LookupQueryingTests.test_combined_annotated_lookups_in_filter",
174168
"lookup.tests.LookupQueryingTests.test_combined_annotated_lookups_in_filter_false",
175169
"lookup.tests.LookupQueryingTests.test_combined_lookups",
176170
# Case not supported.
177171
"lookup.tests.LookupQueryingTests.test_conditional_expression",
178-
# Using expression in filter() doesn't work.
179-
"lookup.tests.LookupQueryingTests.test_filter_lookup_lhs",
180172
# Subquery not supported.
181173
"annotations.tests.NonAggregateAnnotationTestCase.test_empty_queryset_annotation",
182174
"db_functions.comparison.test_coalesce.CoalesceTests.test_empty_queryset",
@@ -202,8 +194,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
202194
# Func not implemented.
203195
"annotations.tests.NonAggregateAnnotationTestCase.test_custom_functions",
204196
"annotations.tests.NonAggregateAnnotationTestCase.test_custom_functions_can_ref_other_functions",
205-
# Floor not implemented.
206-
"annotations.tests.NonAggregateAnnotationTestCase.test_custom_transform_annotation",
207197
# BaseDatabaseOperations may require a format_for_duration_arithmetic().
208198
"annotations.tests.NonAggregateAnnotationTestCase.test_mixed_type_annotation_date_interval",
209199
# FieldDoesNotExist with ordering.
@@ -212,9 +202,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
212202
"annotations.tests.NonAggregateAnnotationTestCase.test_order_by_annotation",
213203
# annotate().filter().count() gives incorrect results.
214204
"db_functions.datetime.test_extract_trunc.DateFunctionTests.test_extract_year_exact_lookup",
215-
# Year lookup + lt/gt crashes: 'dict' object has no attribute 'startswith'
216-
"db_functions.datetime.test_extract_trunc.DateFunctionTests.test_extract_year_greaterthan_lookup",
217-
"db_functions.datetime.test_extract_trunc.DateFunctionTests.test_extract_year_lessthan_lookup",
218205
},
219206
"Count doesn't work in QuerySet.annotate()": {
220207
"annotations.tests.AliasTests.test_alias_annotate_with_aggregation",
@@ -307,28 +294,9 @@ class DatabaseFeatures(BaseDatabaseFeatures):
307294
"timezones.tests.NewDatabaseTests.test_cursor_explicit_time_zone",
308295
"timezones.tests.NewDatabaseTests.test_raw_sql",
309296
},
310-
"Transform not supported.": {
311-
"db_functions.math.test_abs.AbsTests.test_transform",
312-
"db_functions.math.test_acos.ACosTests.test_transform",
313-
"db_functions.math.test_asin.ASinTests.test_transform",
314-
"db_functions.math.test_atan.ATanTests.test_transform",
315-
"db_functions.math.test_ceil.CeilTests.test_transform",
316-
"db_functions.math.test_cos.CosTests.test_transform",
317-
"db_functions.math.test_cot.CotTests.test_transform",
318-
"db_functions.math.test_degrees.DegreesTests.test_transform",
319-
"db_functions.math.test_exp.ExpTests.test_transform",
320-
"db_functions.math.test_floor.FloorTests.test_transform",
321-
"db_functions.math.test_ln.LnTests.test_transform",
322-
"db_functions.math.test_radians.RadiansTests.test_transform",
323-
"db_functions.math.test_round.RoundTests.test_transform",
324-
"db_functions.math.test_sin.SinTests.test_transform",
325-
"db_functions.math.test_sqrt.SqrtTests.test_transform",
326-
"db_functions.math.test_tan.TanTests.test_transform",
297+
"Bilateral transform not implemented.": {
327298
"db_functions.tests.FunctionTests.test_func_transform_bilateral",
328299
"db_functions.tests.FunctionTests.test_func_transform_bilateral_multivalue",
329-
"db_functions.text.test_strindex.StrIndexTests.test_filtering",
330-
"db_functions.text.test_length.LengthTests.test_basic",
331-
"db_functions.text.test_length.LengthTests.test_transform",
332300
},
333301
"MongoDB does not support this database function.": {
334302
"db_functions.datetime.test_now.NowTests",

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: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def is_direct_value(node):
88
return not hasattr(node, "as_sql")
99

1010

11-
def process_lhs(node, compiler, connection, bare_column_ref=False):
11+
def process_lhs(node, compiler, connection):
1212
if not hasattr(node, "lhs"):
1313
# node is a Func or Expression, possibly with multiple source expressions.
1414
result = []
@@ -21,11 +21,7 @@ def process_lhs(node, compiler, connection, bare_column_ref=False):
2121
# node is a Transform with just one source expression, aliased as "lhs".
2222
if is_direct_value(node.lhs):
2323
return node
24-
mql = node.lhs.as_mql(compiler, connection)
25-
# Remove the unneeded $ from column references.
26-
if bare_column_ref and mql.startswith("$"):
27-
mql = mql[1:]
28-
return mql
24+
return node.lhs.as_mql(compiler, connection)
2925

3026

3127
def process_rhs(node, compiler, connection):
@@ -49,9 +45,7 @@ def process_rhs(node, compiler, connection):
4945
return connection.ops.prep_lookup_value(value, node.lhs.output_field, node.lookup_name)
5046

5147

52-
def safe_regex(regex, *re_args, **re_kwargs):
53-
def wrapper(value):
54-
return re.compile(regex % re.escape(value), *re_args, **re_kwargs)
55-
56-
wrapper.__name__ = "safe_regex (%r)" % regex
57-
return wrapper
48+
def regex_match(field, value, regex, *re_args, **re_kwargs):
49+
regex = re.compile(regex % re.escape(value), *re_args, **re_kwargs)
50+
options = "i" if regex.flags & re.I else ""
51+
return {"$regexMatch": {"input": field, "regex": regex.pattern, "options": options}}

0 commit comments

Comments
 (0)