Skip to content

Commit 50fc96c

Browse files
fix(checks): restore column-vs-literal comparisons in comparator family (#227)
Deequ 2.0.x's Check.isLessThan/isLessThanOrEqualTo/isGreaterThan/ isGreaterThanOrEqualTo forward columns = List(columnA, columnB) to satisfies, which makes Deequ require both operands to be existing columns. This regressed the long-supported column-vs-literal usage (e.g. isGreaterThanOrEqualTo("cluster_size", "1")), failing with 'Input data does not include column 1!' (issue #227). Route the comparator family through Deequ's satisfies with an empty columns list (the pre-2.0 behaviour), building the SQL predicate in the wrapper. columnB may now be a column name or a SQL literal/expression. Column-vs-column comparisons are unchanged. Adds regression tests for column-vs-literal comparisons.
1 parent 2e628b2 commit 50fc96c

2 files changed

Lines changed: 67 additions & 36 deletions

File tree

pydeequ/checks.py

Lines changed: 39 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -727,81 +727,84 @@ def isPositive(self, column, assertion=None, hint=None):
727727
self._Check = self._Check.isPositive(column, assertion_func, hint)
728728
return self
729729

730+
def _column_comparison(self, columnA, columnB, operator, description, assertion, hint):
731+
"""Builds a column comparison constraint via Deequ's ``satisfies``.
732+
733+
Deequ's native ``isLessThan``/``isGreaterThan`` family (since Deequ 2.0.x)
734+
forwards ``columns = List(columnA, columnB)`` to ``satisfies``, which makes
735+
Deequ require *both* operands to be existing columns. That breaks the long
736+
supported column-vs-literal usage (e.g. ``isGreaterThanOrEqualTo("col", "1")``),
737+
failing with ``Input data does not include column 1!`` (see issue #227).
738+
739+
We instead build the same Spark SQL predicate ourselves and call ``satisfies``
740+
with an empty ``columns`` list (the pre-2.0 behaviour), so ``columnB`` may be
741+
either a column name or a SQL literal/expression.
742+
"""
743+
assertion_func = (
744+
ScalaFunction1(self._spark_session.sparkContext._gateway, assertion)
745+
if assertion
746+
else getattr(self._Check, "satisfies$default$3")()
747+
)
748+
hint = self._jvm.scala.Option.apply(hint)
749+
column_condition = f"{columnA} {operator} {columnB}"
750+
constraint_name = f"{columnA} is {description} {columnB}"
751+
self._Check = self._Check.satisfies(
752+
column_condition,
753+
constraint_name,
754+
assertion_func,
755+
hint,
756+
self._jvm.scala.collection.Seq.empty(),
757+
self._jvm.scala.Option.apply(None),
758+
)
759+
return self
760+
730761
def isLessThan(self, columnA, columnB, assertion=None, hint=None):
731762
"""
732763
Asserts that, in each row, the value of columnA is less than the value of columnB
733764
734765
:param str columnA: Column in DataFrame to run the assertion on.
735-
:param str columnB: Column in DataFrame to run the assertion on.
766+
:param str columnB: Column in DataFrame to compare against, or a SQL literal/expression.
736767
:param lambda assertion: A function that accepts an int or float parameter.
737768
:param str hint: A hint that states why a constraint could have failed.
738769
:return: isLessThan self : A Check object that checks the assertion on the columns.
739770
"""
740-
assertion_func = (
741-
ScalaFunction1(self._spark_session.sparkContext._gateway, assertion)
742-
if assertion
743-
else getattr(self._Check, "isLessThan$default$3")()
744-
)
745-
hint = self._jvm.scala.Option.apply(hint)
746-
self._Check = self._Check.isLessThan(columnA, columnB, assertion_func, hint)
747-
return self
771+
return self._column_comparison(columnA, columnB, "<", "less than", assertion, hint)
748772

749773
def isLessThanOrEqualTo(self, columnA, columnB, assertion=None, hint=None):
750774
"""
751775
Asserts that, in each row, the value of columnA is less than or equal to the value of columnB.
752776
753777
:param str columnA: Column in DataFrame to run the assertion on.
754-
:param str columnB: Column in DataFrame to run the assertion on.
778+
:param str columnB: Column in DataFrame to compare against, or a SQL literal/expression.
755779
:param lambda assertion: A function that accepts an int or float parameter.
756780
:param str hint: A hint that states why a constraint could have failed.
757781
:return: isLessThanOrEqualTo self (isLessThanOrEqualTo): A Check object that checks the assertion on the columns.
758782
"""
759-
assertion_func = (
760-
ScalaFunction1(self._spark_session.sparkContext._gateway, assertion)
761-
if assertion
762-
else getattr(self._Check, "isLessThanOrEqualTo$default$3")()
763-
)
764-
hint = self._jvm.scala.Option.apply(hint)
765-
self._Check = self._Check.isLessThanOrEqualTo(columnA, columnB, assertion_func, hint)
766-
return self
783+
return self._column_comparison(columnA, columnB, "<=", "less than or equal to", assertion, hint)
767784

768785
def isGreaterThan(self, columnA, columnB, assertion=None, hint=None):
769786
"""
770787
Asserts that, in each row, the value of columnA is greater than the value of columnB
771788
772789
:param str columnA: Column in DataFrame to run the assertion on.
773-
:param str columnB: Column in DataFrame to run the assertion on.
790+
:param str columnB: Column in DataFrame to compare against, or a SQL literal/expression.
774791
:param lambda assertion: A function that accepts an int or float parameter.
775792
:param str hint: A hint that states why a constraint could have failed.
776793
:return: isGreaterThan self: A Check object that runs the assertion on the columns.
777794
"""
778-
assertion_func = (
779-
ScalaFunction1(self._spark_session.sparkContext._gateway, assertion)
780-
if assertion
781-
else getattr(self._Check, "isGreaterThan$default$3")()
782-
)
783-
hint = self._jvm.scala.Option.apply(hint)
784-
self._Check = self._Check.isGreaterThan(columnA, columnB, assertion_func, hint)
785-
return self
795+
return self._column_comparison(columnA, columnB, ">", "greater than", assertion, hint)
786796

787797
def isGreaterThanOrEqualTo(self, columnA, columnB, assertion=None, hint=None):
788798
"""
789799
Asserts that, in each row, the value of columnA is greather than or equal to the value of columnB
790800
791801
:param str columnA: Column in DataFrame to run the assertion on.
792-
:param str columnB: Column in DataFrame to run the assertion on.
802+
:param str columnB: Column in DataFrame to compare against, or a SQL literal/expression.
793803
:param lambda assertion: A function that accepts an int or float parameter.
794804
:param str hint: A hint that states why a constraint could have failed.
795805
:return: isGreaterThanOrEqualTo self: A Check object that runs the assertion on the columns.
796806
"""
797-
assertion_func = (
798-
ScalaFunction1(self._spark_session.sparkContext._gateway, assertion)
799-
if assertion
800-
else getattr(self._Check, "isGreaterThanOrEqualTo$default$3")()
801-
)
802-
hint = self._jvm.scala.Option.apply(hint)
803-
self._Check = self._Check.isGreaterThanOrEqualTo(columnA, columnB, assertion_func, hint)
804-
return self
807+
return self._column_comparison(columnA, columnB, ">=", "greater than or equal to", assertion, hint)
805808

806809
def isContainedIn(self, column, allowed_values, assertion=None, hint=None):
807810
"""

tests/test_checks.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,6 +1020,34 @@ def test_fail_isGreaterThanOrEqualTo(self):
10201020
self.isGreaterThanOrEqualTo("h", "f", lambda x: x == 1), [Row(constraint_status="Failure")]
10211021
)
10221022

1023+
def test_comparator_against_literal(self):
1024+
# Regression test for issue #227: comparing a column to a SQL literal
1025+
# (rather than another column) must not fail with
1026+
# "Input data does not include column <literal>!".
1027+
# Column "b" holds values 1, 2, 3 -> all are >= 1 and all are <= 10.
1028+
self.assertEqual(
1029+
self.isGreaterThanOrEqualTo("b", "1", hint="Cluster should have at least one element"),
1030+
[Row(constraint_status="Success")],
1031+
)
1032+
self.assertEqual(
1033+
self.isLessThanOrEqualTo("b", "10", hint="b never exceeds 10"),
1034+
[Row(constraint_status="Success")],
1035+
)
1036+
self.assertEqual(
1037+
self.isGreaterThan("b", "0"),
1038+
[Row(constraint_status="Success")],
1039+
)
1040+
self.assertEqual(
1041+
self.isLessThan("b", "100"),
1042+
[Row(constraint_status="Success")],
1043+
)
1044+
1045+
def test_fail_comparator_against_literal(self):
1046+
# Column "b" holds values 1, 2, 3 -> not all are >= 3.
1047+
self.assertEqual(
1048+
self.isGreaterThanOrEqualTo("b", "3"), [Row(constraint_status="Failure")]
1049+
)
1050+
10231051
def test_where(self):
10241052
self.assertEqual(
10251053
self.where(lambda x: x == 2.0, "boolean='true'", "column 'boolean' has two values true"),

0 commit comments

Comments
 (0)