This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 204ba78cc1f Fix SQL check tolerance for negative expected values 
(#69736)
204ba78cc1f is described below

commit 204ba78cc1f9c3cf66fd95365e1f626e170ec597
Author: Steve Ahn <[email protected]>
AuthorDate: Fri Jul 31 11:28:05 2026 -0700

    Fix SQL check tolerance for negative expected values (#69736)
---
 .../airflow/providers/common/sql/operators/sql.py  | 40 +++++++++-------------
 .../tests/unit/common/sql/operators/test_sql.py    | 30 ++++++++++++++++
 2 files changed, 46 insertions(+), 24 deletions(-)

diff --git 
a/providers/common/sql/src/airflow/providers/common/sql/operators/sql.py 
b/providers/common/sql/src/airflow/providers/common/sql/operators/sql.py
index 0bdd7ea981e..9c2da9d6d7e 100644
--- a/providers/common/sql/src/airflow/providers/common/sql/operators/sql.py
+++ b/providers/common/sql/src/airflow/providers/common/sql/operators/sql.py
@@ -744,35 +744,27 @@ class SQLColumnCheckOperator(BaseSQLOperator):
         if record is None and self.accept_none:
             record = 0
         match_boolean = True
+
+        # abs() so the tolerance margin widens the bound outward for negative 
expected values too.
+        def _margin(expected):
+            return abs(expected) * tolerance if tolerance is not None else 0
+
         if "geq_to" in check_values:
-            if tolerance is not None:
-                match_boolean = record >= check_values["geq_to"] * (1 - 
tolerance)
-            else:
-                match_boolean = record >= check_values["geq_to"]
+            match_boolean = record >= check_values["geq_to"] - 
_margin(check_values["geq_to"])
         elif "greater_than" in check_values:
-            if tolerance is not None:
-                match_boolean = record > check_values["greater_than"] * (1 - 
tolerance)
-            else:
-                match_boolean = record > check_values["greater_than"]
+            match_boolean = record > check_values["greater_than"] - 
_margin(check_values["greater_than"])
         if "leq_to" in check_values:
-            if tolerance is not None:
-                match_boolean = record <= check_values["leq_to"] * (1 + 
tolerance) and match_boolean
-            else:
-                match_boolean = record <= check_values["leq_to"] and 
match_boolean
+            match_boolean = (
+                record <= check_values["leq_to"] + 
_margin(check_values["leq_to"]) and match_boolean
+            )
         elif "less_than" in check_values:
-            if tolerance is not None:
-                match_boolean = record < check_values["less_than"] * (1 + 
tolerance) and match_boolean
-            else:
-                match_boolean = record < check_values["less_than"] and 
match_boolean
+            match_boolean = (
+                record < check_values["less_than"] + 
_margin(check_values["less_than"]) and match_boolean
+            )
         if "equal_to" in check_values:
-            if tolerance is not None:
-                match_boolean = (
-                    check_values["equal_to"] * (1 - tolerance)
-                    <= record
-                    <= check_values["equal_to"] * (1 + tolerance)
-                ) and match_boolean
-            else:
-                match_boolean = record == check_values["equal_to"] and 
match_boolean
+            expected = check_values["equal_to"]
+            margin = _margin(expected)
+            match_boolean = (expected - margin <= record <= expected + margin) 
and match_boolean
         return match_boolean
 
     def _column_mapping_validation(self, check, check_values):
diff --git a/providers/common/sql/tests/unit/common/sql/operators/test_sql.py 
b/providers/common/sql/tests/unit/common/sql/operators/test_sql.py
index 573dba93a6b..6c6fa9a405a 100644
--- a/providers/common/sql/tests/unit/common/sql/operators/test_sql.py
+++ b/providers/common/sql/tests/unit/common/sql/operators/test_sql.py
@@ -1691,6 +1691,36 @@ class TestSQLColumnCheckOperatorBuildCheckResults:
         assert r.expected == ">=1, <=100"
         assert r.params == {"geq_to": 1, "leq_to": 100, "accept_none": True}
 
+    @pytest.mark.parametrize(
+        ("check_values", "record", "tolerance", "expected"),
+        [
+            # Negative thresholds: tolerance widens the bound outward, so a 
record equal to
+            # the threshold passes.
+            ({"geq_to": -1000}, -1000, 0.1, True),
+            ({"geq_to": -1000}, -1100, 0.1, True),
+            ({"geq_to": -1000}, -1101, 0.1, False),
+            ({"greater_than": -1000}, -1050, 0.1, True),
+            ({"greater_than": -1000}, -1100, 0.1, False),  # strict: equal to 
the widened bound fails
+            ({"leq_to": -10}, -10, 0.1, True),
+            ({"less_than": -10}, -11, 0.1, True),
+            ({"equal_to": -100}, -100, 0.1, True),
+            ({"equal_to": -100}, -110, 0.1, True),
+            ({"equal_to": -100}, -111, 0.1, False),
+            ({"geq_to": -100, "leq_to": -50}, -75, 0.1, True),
+            # Positive thresholds keep their existing bounds.
+            ({"geq_to": 1000}, 900, 0.1, True),
+            ({"geq_to": 1000}, 899, 0.1, False),
+            ({"equal_to": 100}, 110, 0.1, True),
+            ({"equal_to": 100}, 111, 0.1, False),
+            # No tolerance: exact bounds.
+            ({"geq_to": 10}, 10, None, True),
+            ({"equal_to": 5}, 6, None, False),
+        ],
+    )
+    def test_get_match_tolerance_handles_negative_thresholds(self, 
check_values, record, tolerance, expected):
+        op = self._make_operator({"col": {"min": {"geq_to": 1}}})
+        assert op._get_match(check_values, record, tolerance) == expected
+
     def test_multiple_checks_correct_names_and_order(self):
         op = self._make_operator(
             {

Reply via email to