Skip to content

Commit deac776

Browse files
committed
Improvements and Testing
1 parent be43963 commit deac776

File tree

8 files changed

+554
-275
lines changed

8 files changed

+554
-275
lines changed

query_conversion/expression_converters.py renamed to django_mongodb_backend/query_conversion/expression_converters.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,24 @@ def optimize(cls, expr):
1414
@classmethod
1515
def is_simple_value(cls, value):
1616
"""
17-
Check if the value is a simple type (not a list or dict).
17+
Check if the value is a simple type (not a dict).
1818
"""
19-
return isinstance(value, (str, int, float, bool)) or value is None
19+
if isinstance(value, str) and value.startswith("$"):
20+
return False
21+
if isinstance(value, list | tuple | set):
22+
return all(cls.is_simple_value(v) for v in value)
23+
# TODO: Expand functionality to support `$getField` conversion
24+
return not isinstance(value, dict) or value is None
2025

2126
@classmethod
2227
def is_convertable_field_name(cls, field_name):
2328
"""Validate a field_name is one that can be represented in $match"""
2429
# This needs work and re-evaluation
25-
if (
30+
return (
2631
isinstance(field_name, str)
2732
and field_name.startswith("$")
2833
and not field_name[:1].isalnum()
29-
):
30-
return True
31-
return False
34+
)
3235

3336

3437
class _EqExpressionConverter(_BaseExpressionConverter):
@@ -62,7 +65,7 @@ def optimize(cls, in_args):
6265
# Check if first argument is a simple field reference
6366
if isinstance(field_expr, str) and field_expr.startswith("$"):
6467
field_name = field_expr[1:] # Remove the $ prefix
65-
if isinstance(values, list) and all(
68+
if isinstance(values, list | tuple | set) and all(
6669
cls.is_simple_value(v) for v in values
6770
):
6871
return {field_name: {"$in": values}}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
from copy import deepcopy
2+
3+
from django_mongodb_backend.query_conversion.expression_converters import convert_expression
4+
5+
6+
class QueryOptimizer:
7+
def convert_expr_to_match(self, expr):
8+
"""
9+
Takes an MQL query with $expr and optimizes it by extracting
10+
optimizable conditions into separate $match stages.
11+
12+
Args:
13+
expr_query: Dictionary containing the $expr query
14+
15+
Returns:
16+
List of optimized match conditions
17+
"""
18+
expr_query = deepcopy(expr)
19+
20+
if "$expr" not in expr_query:
21+
return [expr_query]
22+
23+
if expr_query["$expr"] == {}:
24+
return [{"$match": {}}]
25+
26+
expr_content = expr_query["$expr"]
27+
match_conditions = []
28+
remaining_expr_conditions = []
29+
30+
# Handle the expression content
31+
self._process_expression(expr_content, match_conditions, remaining_expr_conditions)
32+
33+
# If there are remaining conditions that couldn't be optimized,
34+
# keep them in an $expr
35+
if remaining_expr_conditions:
36+
if len(remaining_expr_conditions) == 1:
37+
expr_conditions = {"$expr": remaining_expr_conditions[0]}
38+
else:
39+
expr_conditions = {"$expr": {"$and": remaining_expr_conditions}}
40+
41+
if match_conditions:
42+
# This assumes match_conditions is a list of dicts with $match
43+
match_conditions[0]["$match"].update(expr_conditions)
44+
else:
45+
match_conditions.append({"$match": expr_conditions})
46+
47+
return match_conditions
48+
49+
def _process_expression(self, expr, match_conditions, remaining_conditions):
50+
"""
51+
Process an expression and extract optimizable conditions.
52+
53+
Args:
54+
expr: The expression to process
55+
match_conditions: List to append optimized match conditions
56+
remaining_conditions: List to append non-optimizable conditions
57+
"""
58+
if isinstance(expr, dict):
59+
# Check if this is an $and operation
60+
has_and = "$and" in expr
61+
has_or = "$or" in expr
62+
# Do a top-level check for $and or $or because these should inform
63+
# If they fail, they should failover to a remaining conditions list
64+
# There's probably a better way to do this, but this is a start
65+
if has_and:
66+
self._process_logical_conditions(
67+
"$and", expr["$and"], match_conditions, remaining_conditions
68+
)
69+
if has_or:
70+
self._process_logical_conditions(
71+
"$or", expr["$or"], match_conditions, remaining_conditions
72+
)
73+
if not has_and and not has_or:
74+
# Process single condition
75+
optimized = convert_expression(expr)
76+
print(f"{expr=}")
77+
if optimized:
78+
match_conditions.append({"$match": optimized})
79+
else:
80+
remaining_conditions.append(expr)
81+
print(f"{match_conditions=}")
82+
print(f"{remaining_conditions=}")
83+
else:
84+
# Can't optimize
85+
remaining_conditions.append(expr)
86+
87+
def _process_logical_conditions(
88+
self, logical_op, logical_conditions, match_conditions, remaining_conditions
89+
):
90+
"""
91+
Process conditions within a logical array.
92+
93+
Args:
94+
logical_conditions: List of conditions within logical operator
95+
match_conditions: List to append optimized match conditions
96+
remaining_conditions: List to append non-optimizable conditions
97+
"""
98+
optimized_conditions = []
99+
for condition in logical_conditions:
100+
if isinstance(condition, dict):
101+
if optimized := convert_expression(condition):
102+
optimized_conditions.append(optimized)
103+
else:
104+
remaining_conditions.append(condition)
105+
else:
106+
remaining_conditions.append(condition)
107+
match_conditions.append({"$match": {logical_op: optimized_conditions}})

0 commit comments

Comments
 (0)