Skip to content

Commit

Permalink
LEXIO-37887 Add more operator support to binary and unary expressions (
Browse files Browse the repository at this point in the history
…#6)

* binary and unary operations extend scalar

* Version bumped to 0.7.0

Co-authored-by: ns-circle-ci <[email protected]>
  • Loading branch information
jdrake and ns-circle-ci authored Apr 28, 2022
1 parent d4b1cb8 commit 8ee27c3
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 63 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pysaql"
version = "0.6.0"
version = "0.7.0"
description = "Python SAQL query builder"
authors = ["Jonathan Drake <[email protected]>"]
license = "BSD-3-Clause"
Expand Down
2 changes: 1 addition & 1 deletion pysaql/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Python SAQL query builder"""

__version__ = "0.6.0"
__version__ = "0.7.0"
131 changes: 75 additions & 56 deletions pysaql/scalar.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def __or__(self, obj: Any) -> BinaryOperation:
binary operation
"""
return BinaryOperation(operator.or_, self, obj, wrap=True)
return BinaryOperation(operator.or_, self, obj)

def __invert__(self) -> UnaryOperation:
"""Creates a unary operation using the `inv` operator
Expand All @@ -69,58 +69,6 @@ def __invert__(self) -> UnaryOperation:
return UnaryOperation(operator.inv, self)


class BinaryOperation(BooleanOperation):
"""Represents a binary operation"""

def __init__(self, op: Callable, left: Any, right: Any, wrap: bool = False) -> None:
"""Initializer
Args:
op: Operator function that accepts two operands
left: Left operand
right: Right operand
wrap: Flag that indicates whether the stringified operation should be
wrapped in parentheses to denote precedence. Defaults to False.
"""
super().__init__()
if op not in OPERATOR_STRINGS:
operators = ", ".join(f"operator.{fn.__name__}" for fn in OPERATOR_STRINGS)
raise ValueError(f"Operator must be one of: {operators}. Provided: {op}")
self.op = op
self.left = left
self.right = right
self.wrap = wrap

def to_string(self) -> str:
"""Cast the binary operation to a string"""
s = f"{stringify(self.left)} {OPERATOR_STRINGS[self.op]} {stringify(self.right)}"
if self.wrap:
s = f"({s})"

return s


class UnaryOperation(BooleanOperation):
"""Represents a unary operation"""

def __init__(self, op: Callable, value: Any) -> None:
"""Initializer
Args:
op: Operator function that accepts one argument
value: Value to pass to the operator
"""
super().__init__()
self.op = op
self.value = value

def to_string(self) -> str:
"""Cast the unary operation to a string"""
return f"{OPERATOR_STRINGS[self.op]} {stringify(self.value)}"


class Scalar(BooleanOperation, ABC):
"""Represents a scalar expression"""

Expand Down Expand Up @@ -268,13 +216,66 @@ def in_(self, iterable: Union[Sequence, Expression]) -> BinaryOperation:
return BinaryOperation(operator.contains, self, iterable)


class BinaryOperation(Scalar):
"""Represents a binary operation"""

def __init__(self, op: Callable, left: Any, right: Any, wrap: bool = False) -> None:
"""Initializer
Args:
op: Operator function that accepts two operands
left: Left operand
right: Right operand
wrap: Flag that indicates whether the stringified operation should be
wrapped in parentheses to denote precedence. Defaults to False.
"""
super().__init__()
if op not in OPERATOR_STRINGS:
operators = ", ".join(f"operator.{fn.__name__}" for fn in OPERATOR_STRINGS)
raise ValueError(f"Operator must be one of: {operators}. Provided: {op}")
self.op = op
self.left = left
self.right = right
self.wrap = wrap
for operand in (self.left, self.right):
if isinstance(operand, BinaryOperation):
operand.wrap = True

def to_string(self) -> str:
"""Cast the binary operation to a string"""
s = f"{stringify(self.left)} {OPERATOR_STRINGS[self.op]} {stringify(self.right)}"
if self.wrap:
s = f"({s})"

return s


class UnaryOperation(Scalar):
"""Represents a unary operation"""

def __init__(self, op: Callable, value: Any) -> None:
"""Initializer
Args:
op: Operator function that accepts one argument
value: Value to pass to the operator
"""
super().__init__()
self.op = op
self.value = value

def to_string(self) -> str:
"""Cast the unary operation to a string"""
return f"{OPERATOR_STRINGS[self.op]} {stringify(self.value)}"


class field(Scalar):
"""Represents a field (column) in the data stream"""

name: str

def __init__(self, name: str) -> None:
"""Initializer
"""Represents a field (column) in the data stream
Args:
name: Name of the field
Expand All @@ -286,3 +287,21 @@ def __init__(self, name: str) -> None:
def to_string(self) -> str:
"""Cast the field to a string"""
return escape_identifier(self.name)


class literal(Scalar):
"""Represents a literal value"""

def __init__(self, value: Any) -> None:
"""Represents a literal value
Args:
value: Literal value
"""
super().__init__()
self.value = value

def to_string(self) -> str:
"""Cast the literal to a string"""
return stringify(self.value)
2 changes: 1 addition & 1 deletion tests/unit/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def test_complex():
"""q1 = load "opportunities";""",
"""q1 = foreach q1 generate 'name', coalesce('number', 'other number', 0);""",
"""q1 = fill q1 by (dateCols=('Year', 'Month', "Y-M"), partition='Type');""",
"""q1 = filter q1 by 'name' == "abc" && ! 'flag' && ('number' > 0 || 'number' < 0) && 'empty' is null && 'list' in ["ny", "ma"] && 'closed_date' in [date(2022, 1).."2 months ago"];""",
"""q1 = filter q1 by ((((('name' == "abc") && ! 'flag') && (('number' > 0) || ('number' < 0))) && ('empty' is null)) && ('list' in ["ny", "ma"])) && ('closed_date' in [date(2022, 1).."2 months ago"]);""",
"""q1 = foreach q1 generate sum('amount') over ([..2] partition by ('region', 'state') order by sum('amount') desc) as 'total amount', dense_rank() over ([..] partition by 'county' order by 'region' asc) as 'total amount';""",
"""q2 = cogroup q0 by 'Day in Week' full, q1 by 'Day in Week';""",
]
18 changes: 15 additions & 3 deletions tests/unit/test_scalar.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Contains unit tests for the scalar module"""


from pysaql.scalar import field
from pysaql.scalar import field, literal


def test_alias():
Expand Down Expand Up @@ -59,6 +59,16 @@ def test_truediv():
assert str(field("foo") / 10) == """'foo' / 10"""


def test_truediv__literal_right():
"""Should allow a literal value as the right operand when the left operand is a binary operation"""
assert str((field("foo") / 10) * 100) == """('foo' / 10) * 100"""


def test_truediv__literal_left():
"""Should require a literal value as the left operand when the right operand is a binary operation"""
assert str(literal(100) * (field("foo") / 10)) == """100 * ('foo' / 10)"""


def test_neg():
"""Should return string for neg operation"""
assert str(-field("foo")) == """- 'foo'"""
Expand All @@ -72,14 +82,16 @@ def test_in():
def test_and():
"""Should return string for and operation"""
assert (
str((field("foo") > 0) & (field("foo") < 10)) == """'foo' > 0 && 'foo' < 10"""
str((field("foo") > 0) & (field("foo") < 10))
== """('foo' > 0) && ('foo' < 10)"""
)


def test_or():
"""Should return string for or operation"""
assert (
str((field("foo") > 0) | (field("foo") < 10)) == """('foo' > 0 || 'foo' < 10)"""
str((field("foo") > 0) | (field("foo") < 10))
== """('foo' > 0) || ('foo' < 10)"""
)


Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def test_filter__multiple():
"""Should filter by a multiple conditions"""
stream = Stream()
stream.filter(field("name") == "foo", field("bar") == "baz")
assert str(stream) == """q0 = filter q0 by 'name' == "foo" && 'bar' == "baz";"""
assert str(stream) == """q0 = filter q0 by ('name' == "foo") && ('bar' == "baz");"""


def test_limit__invalid():
Expand Down

0 comments on commit 8ee27c3

Please sign in to comment.