Skip to content

Commit 331f507

Browse files
committed
Add support for gt, gte, lt, lte
1 parent 650a11c commit 331f507

File tree

3 files changed

+228
-45
lines changed

3 files changed

+228
-45
lines changed

django_mongodb_backend/query_conversion/expression_converters.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,118 @@ def convert(cls, eq_args):
5252
return None
5353

5454

55+
class _GtExpressionConverter(_BaseExpressionConverter):
56+
"""Convert $gt operation to a $match compatible query.
57+
58+
For example::
59+
"$expr": {
60+
{"$gt": ["$price", 100]}
61+
}
62+
is converted to::
63+
{"$gt": ["price", 100]}
64+
"""
65+
66+
@classmethod
67+
def convert(cls, gt_args):
68+
if isinstance(gt_args, list) and len(gt_args) == 2:
69+
field_expr, value = gt_args
70+
71+
# Check if first argument is a simple field reference
72+
if (
73+
isinstance(field_expr, str)
74+
and field_expr.startswith("$")
75+
and cls.is_simple_value(value)
76+
):
77+
field_name = field_expr[1:] # Remove the $ prefix
78+
return {field_name: {"$gt": value}}
79+
80+
return None
81+
82+
83+
class _GteExpressionConverter(_BaseExpressionConverter):
84+
"""Convert $gte operation to a $match compatible query.
85+
86+
For example::
87+
"$expr": {
88+
{"$gte": ["$price", 100]}
89+
}
90+
is converted to::
91+
{"price": {"$gte", 100}}
92+
"""
93+
94+
@classmethod
95+
def convert(cls, gte_args):
96+
if isinstance(gte_args, list) and len(gte_args) == 2:
97+
field_expr, value = gte_args
98+
99+
# Check if first argument is a simple field reference
100+
if (
101+
isinstance(field_expr, str)
102+
and field_expr.startswith("$")
103+
and cls.is_simple_value(value)
104+
):
105+
field_name = field_expr[1:] # Remove the $ prefix
106+
return {field_name: {"$gte": value}}
107+
108+
return None
109+
110+
111+
class _LtExpressionConverter(_BaseExpressionConverter):
112+
"""Convert $lt operation to a $match compatible query.
113+
114+
For example::
115+
"$expr": {
116+
{"$lt": ["$price", 100]}
117+
}
118+
is converted to::
119+
{"$lt": ["price", 100]}
120+
"""
121+
122+
@classmethod
123+
def convert(cls, lt_args):
124+
if isinstance(lt_args, list) and len(lt_args) == 2:
125+
field_expr, value = lt_args
126+
127+
# Check if first argument is a simple field reference
128+
if (
129+
isinstance(field_expr, str)
130+
and field_expr.startswith("$")
131+
and cls.is_simple_value(value)
132+
):
133+
field_name = field_expr[1:] # Remove the $ prefix
134+
return {field_name: {"$lt": value}}
135+
136+
return None
137+
138+
139+
class _LteExpressionConverter(_BaseExpressionConverter):
140+
"""Convert $lte operation to a $match compatible query.
141+
142+
For example::
143+
"$expr": {
144+
{"$lte": ["$price", 100]}
145+
}
146+
is converted to::
147+
{"price": {"$lte", 100}}
148+
"""
149+
150+
@classmethod
151+
def convert(cls, lte_args):
152+
if isinstance(lte_args, list) and len(lte_args) == 2:
153+
field_expr, value = lte_args
154+
155+
# Check if first argument is a simple field reference
156+
if (
157+
isinstance(field_expr, str)
158+
and field_expr.startswith("$")
159+
and cls.is_simple_value(value)
160+
):
161+
field_name = field_expr[1:] # Remove the $ prefix
162+
return {field_name: {"$lte": value}}
163+
164+
return None
165+
166+
55167
class _InExpressionConverter(_BaseExpressionConverter):
56168
"""Convert $in operation to a $match compatible query.
57169
@@ -145,6 +257,10 @@ class _AndExpressionConverter(_LogicalExpressionConverter):
145257
"$in": _InExpressionConverter,
146258
"$and": _AndExpressionConverter,
147259
"$or": _OrExpressionConverter,
260+
"$gt": _GtExpressionConverter,
261+
"$gte": _GteExpressionConverter,
262+
"$lt": _LtExpressionConverter,
263+
"$lte": _LteExpressionConverter,
148264
}
149265

150266

tests/expression_converter_/test_match_conversion.py

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def test_multiple_optimizable_conditions(self):
1919
{"$eq": ["$status", "active"]},
2020
{"$in": ["$category", ["electronics", "books"]]},
2121
{"$eq": ["$verified", True]},
22+
{"$gte": ["$price", 50]},
2223
]
2324
}
2425
}
@@ -29,6 +30,7 @@ def test_multiple_optimizable_conditions(self):
2930
{"status": "active"},
3031
{"category": {"$in": ["electronics", "books"]}},
3132
{"verified": True},
33+
{"price": {"$gte": 50}},
3234
]
3335
}
3436
}
@@ -40,7 +42,7 @@ def test_mixed_optimizable_and_non_optimizable_conditions(self):
4042
"$expr": {
4143
"$and": [
4244
{"$eq": ["$status", "active"]},
43-
{"$gt": ["$price", 100]}, # Not optimizable
45+
{"$gt": ["$price", "$min_price"]}, # Not optimizable
4446
{"$in": ["$category", ["electronics"]]},
4547
]
4648
}
@@ -51,19 +53,19 @@ def test_mixed_optimizable_and_non_optimizable_conditions(self):
5153
"$and": [
5254
{"status": "active"},
5355
{"category": {"$in": ["electronics"]}},
54-
{"$expr": {"$gt": ["$price", 100]}},
56+
{"$expr": {"$gt": ["$price", "$min_price"]}},
5557
],
5658
}
5759
}
5860
]
5961
self.assertOptimizerEqual(expr, expected)
6062

6163
def test_non_optimizable_condition(self):
62-
expr = {"$expr": {"$gt": ["$price", 100]}}
64+
expr = {"$expr": {"$gt": ["$price", "$min_price"]}}
6365
expected = [
6466
{
6567
"$match": {
66-
"$expr": {"$gt": ["$price", 100]},
68+
"$expr": {"$gt": ["$price", "$min_price"]},
6769
}
6870
}
6971
]
@@ -75,7 +77,7 @@ def test_nested_logical_conditions(self):
7577
"$or": [
7678
{"$eq": ["$status", "active"]},
7779
{"$in": ["$category", ["electronics", "books"]]},
78-
{"$and": [{"$eq": ["$verified", True]}, {"$gt": ["$price", 50]}]},
80+
{"$and": [{"$eq": ["$verified", True]}, {"$lte": ["$price", 50]}]},
7981
]
8082
}
8183
}
@@ -85,11 +87,7 @@ def test_nested_logical_conditions(self):
8587
"$or": [
8688
{"status": "active"},
8789
{"category": {"$in": ["electronics", "books"]}},
88-
{
89-
"$expr": {
90-
"$and": [{"$eq": ["$verified", True]}, {"$gt": ["$price", 50]}]
91-
}
92-
},
90+
{"$and": [{"verified": True}, {"price": {"$lte": 50}}]},
9391
]
9492
}
9593
}
@@ -101,32 +99,30 @@ def test_complex_nested_with_non_optimizable_parts(self):
10199
"$expr": {
102100
"$and": [
103101
{
104-
"$or": [ # Not optimizable because of $gt
102+
"$or": [
105103
{"$eq": ["$status", "active"]},
106104
{"$gt": ["$views", 1000]},
107105
]
108106
},
109107
{"$in": ["$category", ["electronics", "books"]]},
110108
{"$eq": ["$verified", True]},
111-
{"$gt": ["$price", 50]}, # Not optimizable
109+
{"$gt": ["$price", "$min_price"]}, # Not optimizable
112110
]
113111
}
114112
}
115113
expected = [
116114
{
117115
"$match": {
118116
"$and": [
119-
{"category": {"$in": ["electronics", "books"]}},
120-
{"verified": True},
121117
{
122-
"$expr": {
123-
"$or": [
124-
{"$eq": ["$status", "active"]},
125-
{"$gt": ["$views", 1000]},
126-
]
127-
}
118+
"$or": [
119+
{"status": "active"},
120+
{"views": {"$gt": 1000}},
121+
]
128122
},
129-
{"$expr": {"$gt": ["$price", 50]}},
123+
{"category": {"$in": ["electronics", "books"]}},
124+
{"verified": True},
125+
{"$expr": {"$gt": ["$price", "$min_price"]}},
130126
]
131127
}
132128
}

tests/expression_converter_/test_op_expressions.py

Lines changed: 95 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,6 @@ def test_empty_dict_expression(self):
4343
expr = {}
4444
self.assertNotOptimizable(expr)
4545

46-
def test_non_convertible(self):
47-
expr = {"$gt": ["$price", 100]}
48-
self.assertNotOptimizable(expr)
49-
5046

5147
class TestEqExprConversion(ExpressionConversionTestCase):
5248
def test_eq_conversion(self):
@@ -158,7 +154,7 @@ def test_logical_or_conversion_failure(self):
158154
{
159155
"$and": [
160156
{"verified": True},
161-
{"$gt": ["$price", 50]}, # Not optimizable
157+
{"$gt": ["$price", "$min_price"]}, # Not optimizable
162158
]
163159
},
164160
]
@@ -171,37 +167,112 @@ def test_logical_mixed_conversion(self):
171167
{
172168
"$or": [
173169
{"$eq": ["$status", "active"]},
170+
{"$gt": ["$views", 1000]},
174171
]
175172
},
176173
{"$in": ["$category", ["electronics", "books"]]},
177174
{"$eq": ["$verified", True]},
175+
{"$lte": ["$price", 2000]},
178176
]
179177
}
180178
expected = {
181179
"$and": [
182-
{
183-
"$or": [
184-
{"status": "active"},
185-
]
186-
},
180+
{"$or": [{"status": "active"}, {"views": {"$gt": 1000}}]},
187181
{"category": {"$in": ["electronics", "books"]}},
188182
{"verified": True},
183+
{"price": {"$lte": 2000}},
189184
]
190185
}
191186
self.assertConversionEqual(expr, expected)
192187

193-
def test_logical_mixed_conversion_failure(self):
194-
expr = {
195-
"$and": [
196-
{
197-
"$or": [
198-
{"$eq": ["$status", "active"]},
199-
{"$gt": ["$views", 1000]},
200-
]
201-
},
202-
{"$in": ["$category", ["electronics", "books"]]},
203-
{"$eq": ["$verified", True]},
204-
{"$gt": ["$price", 50]}, # Not optimizable
205-
]
206-
}
188+
189+
class TestGtExpressionConversion(ExpressionConversionTestCase):
190+
def test_gt_conversion(self):
191+
expr = {"$gt": ["$price", 100]}
192+
expected = {"price": {"$gt": 100}}
193+
self.assertConversionEqual(expr, expected)
194+
195+
def test_gt_no_conversion_non_simple_field(self):
196+
expr = {"$gt": ["$price", "$min_price"]}
197+
self.assertNotOptimizable(expr)
198+
199+
def test_gt_no_conversion_dict_value(self):
200+
expr = {"$gt": ["$price", {}]}
207201
self.assertNotOptimizable(expr)
202+
203+
def _test_gt_conversion_valid_type(self, _type):
204+
expr = {"$gt": ["$price", _type]}
205+
expected = {"price": {"$gt": _type}}
206+
self.assertConversionEqual(expr, expected)
207+
208+
def test_gt_conversion_various_types(self):
209+
self._test_conversion_various_types(self._test_gt_conversion_valid_type)
210+
211+
212+
class TestGteExpressionConversion(ExpressionConversionTestCase):
213+
def test_gte_conversion(self):
214+
expr = {"$gte": ["$price", 100]}
215+
expected = {"price": {"$gte": 100}}
216+
self.assertConversionEqual(expr, expected)
217+
218+
def test_gte_no_conversion_non_simple_field(self):
219+
expr = {"$gte": ["$price", "$min_price"]}
220+
self.assertNotOptimizable(expr)
221+
222+
def test_gte_no_conversion_dict_value(self):
223+
expr = {"$gte": ["$price", {}]}
224+
self.assertNotOptimizable(expr)
225+
226+
def _test_gte_conversion_valid_type(self, _type):
227+
expr = {"$gte": ["$price", _type]}
228+
expected = {"price": {"$gte": _type}}
229+
self.assertConversionEqual(expr, expected)
230+
231+
def test_gte_conversion_various_types(self):
232+
self._test_conversion_various_types(self._test_gte_conversion_valid_type)
233+
234+
235+
class TestLtExpressionConversion(ExpressionConversionTestCase):
236+
def test_lt_conversion(self):
237+
expr = {"$lt": ["$price", 100]}
238+
expected = {"price": {"$lt": 100}}
239+
self.assertConversionEqual(expr, expected)
240+
241+
def test_lt_no_conversion_non_simple_field(self):
242+
expr = {"$lt": ["$price", "$min_price"]}
243+
self.assertNotOptimizable(expr)
244+
245+
def test_lt_no_conversion_dict_value(self):
246+
expr = {"$lt": ["$price", {}]}
247+
self.assertNotOptimizable(expr)
248+
249+
def _test_lt_conversion_valid_type(self, _type):
250+
expr = {"$lt": ["$price", _type]}
251+
expected = {"price": {"$lt": _type}}
252+
self.assertConversionEqual(expr, expected)
253+
254+
def test_lt_conversion_various_types(self):
255+
self._test_conversion_various_types(self._test_lt_conversion_valid_type)
256+
257+
258+
class TestLteExpressionConversion(ExpressionConversionTestCase):
259+
def test_lte_conversion(self):
260+
expr = {"$lte": ["$price", 100]}
261+
expected = {"price": {"$lte": 100}}
262+
self.assertConversionEqual(expr, expected)
263+
264+
def test_lte_no_conversion_non_simple_field(self):
265+
expr = {"$lte": ["$price", "$min_price"]}
266+
self.assertNotOptimizable(expr)
267+
268+
def test_lte_no_conversion_dict_value(self):
269+
expr = {"$lte": ["$price", {}]}
270+
self.assertNotOptimizable(expr)
271+
272+
def _test_lte_conversion_valid_type(self, _type):
273+
expr = {"$lte": ["$price", _type]}
274+
expected = {"price": {"$lte": _type}}
275+
self.assertConversionEqual(expr, expected)
276+
277+
def test_lte_conversion_various_types(self):
278+
self._test_conversion_various_types(self._test_lte_conversion_valid_type)

0 commit comments

Comments
 (0)