Skip to content

Commit

Permalink
add composite condition check and bracket helper for it
Browse files Browse the repository at this point in the history
also added some simple tests with 3 conditions, passing
  • Loading branch information
icyveins7 committed Mar 18, 2024
1 parent 91e2130 commit 007b000
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 21 deletions.
77 changes: 56 additions & 21 deletions sew/condition.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Import this for type hints with classes
from __future__ import annotations
import re

class Condition:
"""
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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))

Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -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)

Expand Down
37 changes: 37 additions & 0 deletions tests/columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
)


0 comments on commit 007b000

Please sign in to comment.