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 3b2445a25fa Fix SQLColumnCheckOperator crash on non-numeric column
bounds (#70895)
3b2445a25fa is described below
commit 3b2445a25fa7cab54604ede173eeed8de3e7da8a
Author: Shahar Epstein <[email protected]>
AuthorDate: Sat Aug 1 15:16:41 2026 +0300
Fix SQLColumnCheckOperator crash on non-numeric column bounds (#70895)
The tolerance rewrite made every bound go through arithmetic, including
when no tolerance is configured. A min/max check declared on a date or
text column has a bound that cannot be subtracted from, so checks that
compared cleanly before now fail the task with a TypeError.
An equal_to check also stopped reporting a NULL result as a failed check
and started raising instead, because a degenerate range still orders the
record against the bound.
---
.../airflow/providers/common/sql/operators/sql.py | 29 ++++++++++++---------
.../tests/unit/common/sql/operators/test_sql.py | 30 ++++++++++++++++++++++
2 files changed, 47 insertions(+), 12 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 9c2da9d6d7e..01148ae4507 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
@@ -746,25 +746,30 @@ class SQLColumnCheckOperator(BaseSQLOperator):
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
+ # Without a tolerance the bound is compared as-is: a min/max check may
be declared on a
+ # date or text column, where arithmetic on the bound raises TypeError.
+ def _lower(expected):
+ return expected - abs(expected) * tolerance if tolerance is not
None else expected
+
+ def _upper(expected):
+ return expected + abs(expected) * tolerance if tolerance is not
None else expected
if "geq_to" in check_values:
- match_boolean = record >= check_values["geq_to"] -
_margin(check_values["geq_to"])
+ match_boolean = record >= _lower(check_values["geq_to"])
elif "greater_than" in check_values:
- match_boolean = record > check_values["greater_than"] -
_margin(check_values["greater_than"])
+ match_boolean = record > _lower(check_values["greater_than"])
if "leq_to" in check_values:
- match_boolean = (
- record <= check_values["leq_to"] +
_margin(check_values["leq_to"]) and match_boolean
- )
+ match_boolean = record <= _upper(check_values["leq_to"]) and
match_boolean
elif "less_than" in check_values:
- match_boolean = (
- record < check_values["less_than"] +
_margin(check_values["less_than"]) and match_boolean
- )
+ match_boolean = record < _upper(check_values["less_than"]) and
match_boolean
if "equal_to" in check_values:
expected = check_values["equal_to"]
- margin = _margin(expected)
- match_boolean = (expected - margin <= record <= expected + margin)
and match_boolean
+ if tolerance is None:
+ # Equality, not a degenerate range: a NULL result must fail
the check rather
+ # than raise on an ordering comparison against None.
+ match_boolean = record == expected and match_boolean
+ else:
+ match_boolean = (_lower(expected) <= record <=
_upper(expected)) 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 6c6fa9a405a..fe3bc8cb265 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
@@ -1721,6 +1721,36 @@ class TestSQLColumnCheckOperatorBuildCheckResults:
op = self._make_operator({"col": {"min": {"geq_to": 1}}})
assert op._get_match(check_values, record, tolerance) == expected
+ @pytest.mark.parametrize(
+ ("check_values", "record", "expected"),
+ [
+ ({"geq_to": "2020-01-01"}, "2021-06-30", True),
+ ({"geq_to": "2020-01-01"}, "2019-12-31", False),
+ ({"greater_than": "2020-01-01"}, "2020-01-01", False),
+ ({"leq_to": "2020-01-01"}, "2019-12-31", True),
+ ({"less_than": "2020-01-01"}, "2020-01-01", False),
+ ({"equal_to": "abc"}, "abc", True),
+ ({"equal_to": "abc"}, "abcd", False),
+ (
+ {"geq_to": datetime.date(2020, 1, 1), "leq_to":
datetime.date(2020, 12, 31)},
+ datetime.date(2020, 6, 30),
+ True,
+ ),
+ (
+ {"geq_to": datetime.date(2020, 1, 1), "leq_to":
datetime.date(2020, 12, 31)},
+ datetime.date(2021, 1, 1),
+ False,
+ ),
+ ],
+ )
+ def test_get_match_compares_non_numeric_bounds_without_tolerance(self,
check_values, record, expected):
+ op = self._make_operator({"col": {"min": {"geq_to": 1}}})
+ assert op._get_match(check_values, record) == expected
+
+ def test_get_match_equal_to_fails_cleanly_on_none_record(self):
+ op = self._make_operator({"col": {"null_check": {"equal_to": 0}}},
accept_none=False)
+ assert op._get_match({"equal_to": 0}, None) is False
+
def test_multiple_checks_correct_names_and_order(self):
op = self._make_operator(
{