From 007b0006fd5d806485806bf834cda35fbc18a081 Mon Sep 17 00:00:00 2001 From: seomac Date: Mon, 18 Mar 2024 11:56:49 +0800 Subject: [PATCH] add composite condition check and bracket helper for it also added some simple tests with 3 conditions, passing --- sew/condition.py | 77 +++++++++++++++++++++++++++++++++++------------- tests/columns.py | 37 +++++++++++++++++++++++ 2 files changed, 93 insertions(+), 21 deletions(-) diff --git a/sew/condition.py b/sew/condition.py index fa0f09e..7e3bec4 100644 --- a/sew/condition.py +++ b/sew/condition.py @@ -1,5 +1,6 @@ # Import this for type hints with classes from __future__ import annotations +import re class Condition: """ @@ -50,10 +51,17 @@ class Condition: c = Condition("col1 = 5") & Condition("col2 = 10") & Condition("col3 = 6") # No parentheses needed if objects completely wrap each substring, but again very verbose. c = (Condition("col1 = 5") & "col2 = 10") & "col3 = 6" # Some parentheses needed, but much less verbose. """ - def __init__(self, first: str): - if not isinstance(first, str): + def __init__(self, cond: str): + if not isinstance(cond, str): raise TypeError("Condition must be a string.") - self._cond = first + self._cond = cond + # A composite condition is one that is made up of several different conditions, + # stitched via AND or OR keywords, so we search for these + if re.search("(and|or)", self._cond, re.IGNORECASE) is not None: + self._isComposite = True + else: + self._isComposite = False + def __str__(self) -> str: return self._cond @@ -67,7 +75,7 @@ def LIKE(self, other: Condition) -> Condition: if isinstance(other, str): condstr = "%s LIKE %s" % (self._cond, other) - # Otherwise mutate the current instance + # elif isinstance(other, Condition): condstr = "%s LIKE %s" % (self._cond, other._cond) @@ -81,7 +89,7 @@ def IN(self, other: Condition) -> Condition: if isinstance(other, list) or isinstance(other, tuple): condstr = "%s IN (%s)" % (self._cond, ",".join(other)) - # Otherwise mutate the current instance + # elif isinstance(other, Condition): condstr = "%s IN (%s)" % (self._cond, ",".join(other._cond)) @@ -92,41 +100,68 @@ def IN(self, other: Condition) -> Condition: # TODO: ALL, ANY, BETWEEN, EXISTS - # Operator overloads + def _compositeBracket(self): + """ + Helper method to surround the condition with brackets if it is a composite condition + e.g. is conditionA AND conditionB. + + You need this in non-commutative conditions e.g. + A AND B OR C is not the same as A AND (B OR C). + """ + if self._isComposite: + return "(" + self._cond + ")" + else: + return self._cond + + + ##### Composite Operator overloads def __and__(self, other: Condition) -> Condition: - # May be a string, in which case just attach it to the current condition + # If it's a string, convert to a Condition and then use the convenience operator if isinstance(other, str): - condstr = "%s AND %s" % (self._cond, other) + condstr = "%s AND %s" % ( + self._compositeBracket(), + other._compositeBracket() + ) - # Otherwise mutate the current instance elif isinstance(other, Condition): - condstr = "%s AND %s" % (self._cond, other._cond) + condstr = "%s AND %s" % ( + self._compositeBracket(), + other._compositeBracket() + ) else: raise TypeError("Condition must be a string or Condition.") return Condition(condstr) + + def __or__(self, other: Condition) -> Condition: - # May be a string, in which case just attach it to the current condition + # If it's a string, convert to a Condition and then use the convenience operator if isinstance(other, str): - condstr = "%s OR %s" % (self._cond, other) + condstr = "%s OR %s" % ( + self._compositeBracket(), + Condition(other)._compositeBracket() + ) - # Otherwise mutate the current instance elif isinstance(other, Condition): - condstr = "%s OR %s" % (self._cond, other._cond) + condstr = "%s OR %s" % ( + self._compositeBracket(), + other._compositeBracket() + ) else: raise TypeError("Condition must be a string or Condition.") return Condition(condstr) + + ##### Simple comparison operator overloads def __eq__(self, other: Condition) -> Condition: - # May be a string, in which case just attach it to the current condition + # If it's a string, convert to a Condition and then use the convenience operator if isinstance(other, str): condstr = "%s = %s" % (self._cond, other) - # Otherwise mutate the current instance elif isinstance(other, Condition): condstr = "%s = %s" % (self._cond, other._cond) @@ -140,7 +175,7 @@ def __ne__(self, other: Condition) -> Condition: if isinstance(other, str): condstr = "%s != %s" % (self._cond, other) - # Otherwise mutate the current instance + # elif isinstance(other, Condition): condstr = "%s != %s" % (self._cond, other._cond) @@ -154,7 +189,7 @@ def __gt__(self, other: Condition) -> Condition: if isinstance(other, str): condstr = "%s > %s" % (self._cond, other) - # Otherwise mutate the current instance + # elif isinstance(other, Condition): condstr = "%s > %s" % (self._cond, other._cond) @@ -168,7 +203,7 @@ def __ge__(self, other: Condition) -> Condition: if isinstance(other, str): condstr = "%s >= %s" % (self._cond, other) - # Otherwise mutate the current instance + # elif isinstance(other, Condition): condstr = "%s >= %s" % (self._cond, other._cond) @@ -182,7 +217,7 @@ def __lt__(self, other: Condition) -> Condition: if isinstance(other, str): condstr = "%s < %s" % (self._cond, other) - # Otherwise mutate the current instance + # elif isinstance(other, Condition): condstr = "%s < %s" % (self._cond, other._cond) @@ -196,7 +231,7 @@ def __le__(self, other: Condition) -> Condition: if isinstance(other, str): condstr = "%s <= %s" % (self._cond, other) - # Otherwise mutate the current instance + # elif isinstance(other, Condition): condstr = "%s <= %s" % (self._cond, other._cond) diff --git a/tests/columns.py b/tests/columns.py index 4c213e2..85f0931 100644 --- a/tests/columns.py +++ b/tests/columns.py @@ -86,5 +86,42 @@ def test_column_composite_comparisons(self): "col1 < 10 OR col2 > 20" ) + def test_column_composite_comparisons_noncommutative(self): + # Here we test 3 or more comparisons in a chain + # First, 3 ORs + cond1 = sew.Condition("col1 < 10") + cond2 = sew.Condition("col2 < 10") + cond3 = sew.Condition("col3 < 10") + + comp = cond1 | cond2 | cond3 + self.assertEqual( + str(comp), + "(col1 < 10 OR col2 < 10) OR col3 < 10" + ) + + # Now, 3 ANDs + comp = cond1 & cond2 & cond3 + self.assertEqual( + str(comp), + "(col1 < 10 AND col2 < 10) AND col3 < 10" + ) + + # Now, if we mix them up, they should be correctly bracketed + comp = (cond1 | cond2) & cond3 + self.assertEqual( + str(comp), + "(col1 < 10 OR col2 < 10) AND col3 < 10" + ) + comp = cond1 | (cond2 & cond3) + self.assertEqual( + str(comp), + "col1 < 10 OR (col2 < 10 AND col3 < 10)" + ) + # Note that the previous one is functionally equivalent to having no brackets, since python evaluates the & first, same as SQLite! + comp = cond1 | cond2 & cond3 + self.assertEqual( + str(comp), + "col1 < 10 OR (col2 < 10 AND col3 < 10)" + )