Repository: great-expectations/great_expectations
Pull request #7053 (2023-02-05 11:55:07)

[CONTRIB] Row condition parser sqlalchemy: adding support for != operator & adding support all operators for string
Hi dear reviewers,

Changes proposed in this pull request:
- Add support for !=
- Add support for all operators (and not only ==) for comparing column to a str value
- Cast num to int if it's only digits (else cast to float) instead of try except
- Using operator, refactored the code and created generate_condition_by_operator function


### Definition of Done
Please delete options that are not relevant.

- [ V] My code follows the Great Expectations [style guide](https://docs.greatexpectations.io/docs/contributing/style_guides/code_style)
- [ V] I have performed a [self-review](https://docs.greatexpectations.io/docs/contributing/contributing_checklist) of my own code
- [ V] I have added [unit tests](https://docs.greatexpectations.io/docs/contributing/contributing_test#writing-unit-and-integration-tests) where applicable and made sure that new and existing tests are passing.


Thank you for reviewing!


netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy request for *niobium-lead-7998* pending review.
[Visit the deploys page to approve it](https://app.netlify.com/sites/niobium-lead-7998/deploys)

|  Name | Link |
|---------------------------------|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 67c15a53e8441bbe3165985450c0f898c75aedd6 |

Patch:
diff --git a/great_expectations/expectations/row_conditions.py b/great_expectations/expectations/row_conditions.py
index 91a2dcdd7..573db965b 100644
--- a/great_expectations/expectations/row_conditions.py
+++ b/great_expectations/expectations/row_conditions.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import enum
+import operator
 from dataclasses import dataclass
 from string import punctuation
 from typing import TYPE_CHECKING
@@ -50,7 +51,8 @@ lt = Literal("<")
 ge = Literal(">=")
 le = Literal("<=")
 eq = Literal("==")
-ops = (gt ^ lt ^ ge ^ le ^ eq).setResultsName("op")
+ne = Literal("!=")
+ops = (gt ^ lt ^ ge ^ le ^ eq ^ ne).setResultsName("op")
 fnumber = Regex(r"[+-]?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?").setResultsName("fnumber")
 punctuation_without_apostrophe = punctuation.replace('"', "").replace("'", "")
 condition_value_chars = alphanums + punctuation_without_apostrophe
@@ -158,34 +160,30 @@ def parse_condition_to_spark(
         raise ConditionParserError(f"unrecognized column condition: {row_condition}")
 
 
-def parse_condition_to_sqlalchemy(
-    row_condition: str,
-) -> ColumnElement:
+def generate_condition_by_operator(column, op, value):
+    operators = {
+        "==": operator.eq,
+        "<": operator.lt,
+        ">": operator.gt,
+        ">=": operator.ge,
+        "<=": operator.le,
+        "!=": operator.ne,
+    }
+    return operators[op](column, value)
+
+
+def parse_condition_to_sqlalchemy(row_condition: str) -> ColumnElement:
     parsed = _parse_great_expectations_condition(row_condition)
     column = parsed["column"]
     if "condition_value" in parsed:
-        if parsed["op"] == "==":
-            return sa.column(column) == parsed["condition_value"]
-        else:
-            raise ConditionParserError(
-                f"Invalid operator: {parsed['op']} for string literal spark condition."
-            )
+        return generate_condition_by_operator(
+            sa.column(column), parsed["op"], parsed["condition_value"]
+        )
     elif "fnumber" in parsed:
-        try:
-            num = int(parsed["fnumber"])
-        except ValueError:
-            num = float(parsed["fnumber"])
-        op = parsed["op"]
-        if op == ">":
-            return sa.column(column) > num
-        elif op == "<":
-            return sa.column(column) < num
-        elif op == ">=":
-            return sa.column(column) >= num
-        elif op == "<=":
-            return sa.column(column) <= num
-        elif op == "==":
-            return sa.column(column) == num
+        number_value = parsed["fnumber"]
+        num = int(number_value) if number_value.isdigit() else float(number_value)
+        return generate_condition_by_operator(sa.column(column), parsed["op"], num)
+
     elif "notnull" in parsed and parsed["notnull"] is True:
         return sa.not_(sa.column(column).is_(None))
     else:


Test patch:
diff --git a/tests/expectations/test_row_conditions.py b/tests/expectations/test_row_conditions.py
index d1d926ceb..02547a41b 100644
--- a/tests/expectations/test_row_conditions.py
+++ b/tests/expectations/test_row_conditions.py
@@ -44,3 +44,9 @@ def test_parse_condition_to_sqlalchemy(sa):
 
     res = parse_condition_to_sqlalchemy('col("foo") == "a-b"')
     assert str(res) == "foo = :foo_1"
+
+    res = parse_condition_to_sqlalchemy('col("foo") != "a-b"')
+    assert str(res) == "foo != :foo_1"
+
+    res = parse_condition_to_sqlalchemy('col("foo") <= 1.34')
+    assert str(res) == "foo <= :foo_1"