Skip to content

Commit f58078c

Browse files
authored
Fix case sensetive bugs (#26)
1 parent 9537cfe commit f58078c

File tree

3 files changed

+128
-16
lines changed

3 files changed

+128
-16
lines changed

pymongosql/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
if TYPE_CHECKING:
77
from .connection import Connection
88

9-
__version__: str = "0.4.6"
9+
__version__: str = "0.4.7"
1010

1111
# Globals https://www.python.org/dev/peps/pep-0249/#globals
1212
apilevel: str = "2.0"

pymongosql/sql/handler.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ def _is_comparison_context(self, ctx: Any) -> bool:
301301
def _has_comparison_pattern(self, ctx: Any) -> bool:
302302
"""Check if the expression text contains comparison patterns"""
303303
try:
304-
text = self.get_context_text(ctx)
304+
text = self.get_context_text(ctx).upper()
305305
# Extended pattern matching for SQL constructs
306306
patterns = COMPARISON_OPERATORS + ["LIKE", "IN", "BETWEEN", "ISNULL", "ISNOTNULL"]
307307
return any(op in text for op in patterns)
@@ -327,12 +327,14 @@ def _extract_field_name(self, ctx: Any) -> str:
327327
"""Extract field name from comparison expression"""
328328
try:
329329
text = self.get_context_text(ctx)
330+
text_upper = text.upper()
330331

331332
# Handle SQL constructs with keywords
332333
sql_keywords = ["IN(", "LIKE", "BETWEEN", "ISNULL", "ISNOTNULL"]
333334
for keyword in sql_keywords:
334-
if keyword in text:
335-
candidate = text.split(keyword, 1)[0].strip()
335+
if keyword in text_upper:
336+
idx = text_upper.index(keyword)
337+
candidate = text[:idx].strip()
336338
return self.normalize_field_path(candidate)
337339

338340
# Try operator-based splitting
@@ -359,6 +361,7 @@ def _extract_operator(self, ctx: Any) -> str:
359361
"""Extract comparison operator"""
360362
try:
361363
text = self.get_context_text(ctx)
364+
text_upper = text.upper()
362365

363366
# Check SQL constructs first (order matters for ISNOTNULL vs ISNULL)
364367
sql_constructs = {
@@ -370,7 +373,7 @@ def _extract_operator(self, ctx: Any) -> str:
370373
}
371374

372375
for construct, operator in sql_constructs.items():
373-
if construct in text:
376+
if construct in text_upper:
374377
return operator
375378

376379
# Look for comparison operators
@@ -394,15 +397,16 @@ def _extract_value(self, ctx: Any) -> Any:
394397
"""Extract value from comparison expression"""
395398
try:
396399
text = self.get_context_text(ctx)
400+
text_upper = text.upper()
397401

398402
# Handle SQL constructs with specific parsing needs
399-
if "IN(" in text:
403+
if "IN(" in text_upper:
400404
return self._extract_in_values(text)
401-
elif "LIKE" in text:
405+
elif "LIKE" in text_upper:
402406
return self._extract_like_pattern(text)
403-
elif "BETWEEN" in text:
407+
elif "BETWEEN" in text_upper:
404408
return self._extract_between_range(text)
405-
elif "ISNULL" in text or "ISNOTNULL" in text:
409+
elif "ISNULL" in text_upper or "ISNOTNULL" in text_upper:
406410
return None
407411

408412
# Standard operator-based extraction
@@ -526,16 +530,24 @@ def _extract_in_values(self, text: str) -> List[Any]:
526530

527531
def _extract_like_pattern(self, text: str) -> str:
528532
"""Extract pattern from LIKE clause"""
529-
parts = text.split("LIKE", 1)
530-
return parts[1].strip().strip("'\"") if len(parts) == 2 else ""
533+
idx = text.upper().find("LIKE")
534+
if idx == -1:
535+
return ""
536+
return text[idx + 4 :].strip().strip("'\"")
531537

532538
def _extract_between_range(self, text: str) -> Optional[Tuple[Any, Any]]:
533539
"""Extract range values from BETWEEN clause"""
534-
parts = text.split("BETWEEN", 1)
535-
if len(parts) == 2 and "AND" in parts[1]:
536-
range_values = parts[1].split("AND", 1)
537-
if len(range_values) == 2:
538-
return (self._parse_value(range_values[0].strip()), self._parse_value(range_values[1].strip()))
540+
text_upper = text.upper()
541+
between_idx = text_upper.find("BETWEEN")
542+
if between_idx == -1:
543+
return None
544+
after = text[between_idx + 7 :]
545+
after_upper = after.upper()
546+
and_idx = after_upper.find("AND")
547+
if and_idx != -1:
548+
low = after[:and_idx].strip()
549+
high = after[and_idx + 3 :].strip()
550+
return (self._parse_value(low), self._parse_value(high))
539551
return None
540552

541553

tests/test_sql_parser_comprehensive.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,103 @@ def test_6_conditions_with_brackets(self):
215215
for key in ["active", "deleted", "age", "name"]:
216216
assert key in flat, f"Missing expected key '{key}' in filter: {f}"
217217
assert "$or" in flat
218+
219+
220+
class TestCaseInsensitiveOperators:
221+
"""Test that SQL operators in WHERE clauses work regardless of case."""
222+
223+
# --- LIKE case variants ---
224+
225+
def test_like_lowercase(self):
226+
sql = "SELECT * FROM col WHERE name like '%john%'"
227+
plan = SQLParser(sql).get_execution_plan()
228+
f = plan.filter_stage
229+
assert "name" in f
230+
assert "$regex" in f["name"]
231+
232+
def test_like_mixed_case(self):
233+
sql = "SELECT * FROM col WHERE name Like '%john%'"
234+
plan = SQLParser(sql).get_execution_plan()
235+
f = plan.filter_stage
236+
assert "name" in f
237+
assert "$regex" in f["name"]
238+
239+
# --- IN case variants ---
240+
241+
def test_in_lowercase(self):
242+
sql = "SELECT * FROM col WHERE status in ('a','b','c')"
243+
plan = SQLParser(sql).get_execution_plan()
244+
f = plan.filter_stage
245+
assert "status" in f
246+
assert "$in" in f["status"]
247+
248+
def test_in_mixed_case(self):
249+
sql = "SELECT * FROM col WHERE status In ('a','b','c')"
250+
plan = SQLParser(sql).get_execution_plan()
251+
f = plan.filter_stage
252+
assert "status" in f
253+
assert "$in" in f["status"]
254+
255+
# --- BETWEEN case variants ---
256+
257+
def test_between_lowercase(self):
258+
sql = "SELECT * FROM col WHERE age between 10 and 50"
259+
plan = SQLParser(sql).get_execution_plan()
260+
f = plan.filter_stage
261+
assert "$and" in f
262+
assert any("age" in cond and "$gte" in cond.get("age", {}) for cond in f["$and"])
263+
assert any("age" in cond and "$lte" in cond.get("age", {}) for cond in f["$and"])
264+
265+
def test_between_mixed_case(self):
266+
sql = "SELECT * FROM col WHERE age Between 10 And 50"
267+
plan = SQLParser(sql).get_execution_plan()
268+
f = plan.filter_stage
269+
assert "$and" in f
270+
assert any("age" in cond and "$gte" in cond.get("age", {}) for cond in f["$and"])
271+
assert any("age" in cond and "$lte" in cond.get("age", {}) for cond in f["$and"])
272+
273+
# --- IS NULL / IS NOT NULL case variants ---
274+
275+
def test_is_null_lowercase(self):
276+
sql = "SELECT * FROM col WHERE name is null"
277+
plan = SQLParser(sql).get_execution_plan()
278+
f = plan.filter_stage
279+
assert "name" in f
280+
assert f["name"] == {"$eq": None}
281+
282+
def test_is_not_null_lowercase(self):
283+
sql = "SELECT * FROM col WHERE name is not null"
284+
plan = SQLParser(sql).get_execution_plan()
285+
f = plan.filter_stage
286+
assert "name" in f
287+
assert f["name"] == {"$ne": None}
288+
289+
# --- AND / OR case variants ---
290+
291+
def test_and_lowercase(self):
292+
sql = "SELECT * FROM col WHERE age=30 and name='John'"
293+
plan = SQLParser(sql).get_execution_plan()
294+
assert plan.filter_stage == {"$and": [{"age": 30}, {"name": "John"}]}
295+
296+
def test_or_lowercase(self):
297+
sql = "SELECT * FROM col WHERE age=30 or name='John'"
298+
plan = SQLParser(sql).get_execution_plan()
299+
assert plan.filter_stage == {"$or": [{"age": 30}, {"name": "John"}]}
300+
301+
# --- Mixed case operators in compound expressions ---
302+
303+
def test_like_and_bool_lowercase_operators(self):
304+
sql = "SELECT * FROM col WHERE name like '%john%' and active=true"
305+
plan = SQLParser(sql).get_execution_plan()
306+
f = plan.filter_stage
307+
assert "$and" in f
308+
assert {"active": True} in f["$and"]
309+
assert any("name" in cond and "$regex" in cond.get("name", {}) for cond in f["$and"])
310+
311+
def test_in_and_comparison_lowercase(self):
312+
sql = "SELECT * FROM col WHERE status in ('a','b') and age>25"
313+
plan = SQLParser(sql).get_execution_plan()
314+
f = plan.filter_stage
315+
assert "$and" in f
316+
assert any("status" in cond and "$in" in cond.get("status", {}) for cond in f["$and"])
317+
assert any("age" in cond and "$gt" in cond.get("age", {}) for cond in f["$and"])

0 commit comments

Comments
 (0)