diff --git a/pyiceberg/expressions/visitors.py b/pyiceberg/expressions/visitors.py index 9329b9940d..26698921b5 100644 --- a/pyiceberg/expressions/visitors.py +++ b/pyiceberg/expressions/visitors.py @@ -1433,6 +1433,30 @@ def visit_not_starts_with(self, term: BoundTerm[L], literal: Literal[L]) -> bool return ROWS_MIGHT_MATCH +def strict_projection( + schema: Schema, spec: PartitionSpec, case_sensitive: bool = True +) -> Callable[[BooleanExpression], BooleanExpression]: + return StrictProjection(schema, spec, case_sensitive).project + + +class StrictProjection(ProjectionEvaluator): + def visit_bound_predicate(self, predicate: BoundPredicate[Any]) -> BooleanExpression: + parts = self.spec.fields_by_source_id(predicate.term.ref().field.field_id) + + result: BooleanExpression = AlwaysFalse() + for part in parts: + # consider (ts > 2019-01-01T01:00:00) with day(ts) and hour(ts) + # projections: d >= 2019-01-02 and h >= 2019-01-01-02 (note the inclusive bounds). + # any timestamp where either projection predicate is true must match the original + # predicate. For example, ts = 2019-01-01T03:00:00 matches the hour projection but not + # the day, but does match the original predicate. + strict_projection = part.transform.strict_project(name=part.name, pred=predicate) + if strict_projection is not None: + result = Or(result, strict_projection) + + return result + + class _StrictMetricsEvaluator(_MetricsEvaluator): struct: StructType expr: BooleanExpression diff --git a/pyiceberg/transforms.py b/pyiceberg/transforms.py index e678a77e69..6dcae59e49 100644 --- a/pyiceberg/transforms.py +++ b/pyiceberg/transforms.py @@ -35,6 +35,7 @@ BoundLessThan, BoundLessThanOrEqual, BoundLiteralPredicate, + BoundNotEqualTo, BoundNotIn, BoundNotStartsWith, BoundPredicate, @@ -43,8 +44,11 @@ BoundTerm, BoundUnaryPredicate, EqualTo, + GreaterThan, GreaterThanOrEqual, + LessThan, LessThanOrEqual, + NotEqualTo, NotStartsWith, Reference, StartsWith, @@ -144,6 +148,9 @@ def result_type(self, source: IcebergType) -> IcebergType: ... @abstractmethod def project(self, name: str, pred: BoundPredicate[L]) -> Optional[UnboundPredicate[Any]]: ... + @abstractmethod + def strict_project(self, name: str, pred: BoundPredicate[Any]) -> Optional[UnboundPredicate[Any]]: ... + @property def preserves_order(self) -> bool: return False @@ -216,6 +223,21 @@ def project(self, name: str, pred: BoundPredicate[L]) -> Optional[UnboundPredica # For example, (x > 0) and (x < 3) can be turned into in({1, 2}) and projected. return None + def strict_project(self, name: str, pred: BoundPredicate[Any]) -> Optional[UnboundPredicate[Any]]: + transformer = self.transform(pred.term.ref().field.field_type) + + if isinstance(pred.term, BoundTransform): + return _project_transform_predicate(self, name, pred) + elif isinstance(pred, BoundUnaryPredicate): + return pred.as_unbound(Reference(name)) + elif isinstance(pred, BoundNotEqualTo): + return pred.as_unbound(Reference(name), _transform_literal(transformer, pred.literal)) + elif isinstance(pred, BoundNotIn): + return pred.as_unbound(Reference(name), {_transform_literal(transformer, literal) for literal in pred.literals}) + else: + # no strict projection for comparison or equality + return None + def can_transform(self, source: IcebergType) -> bool: return isinstance( source, @@ -306,6 +328,19 @@ def project(self, name: str, pred: BoundPredicate[L]) -> Optional[UnboundPredica else: return None + def strict_project(self, name: str, pred: BoundPredicate[Any]) -> Optional[UnboundPredicate[Any]]: + transformer = self.transform(pred.term.ref().field.field_type) + if isinstance(pred.term, BoundTransform): + return _project_transform_predicate(self, name, pred) + elif isinstance(pred, BoundUnaryPredicate): + return pred.as_unbound(Reference(name)) + elif isinstance(pred, BoundLiteralPredicate): + return _truncate_number_strict(name, pred, transformer) + elif isinstance(pred, BoundNotIn): + return _set_apply_transform(name, pred, transformer) + else: + return None + @property def dedup_name(self) -> str: return "time" @@ -516,10 +551,20 @@ def project(self, name: str, pred: BoundPredicate[L]) -> Optional[UnboundPredica return pred.as_unbound(Reference(name)) elif isinstance(pred, BoundLiteralPredicate): return pred.as_unbound(Reference(name), pred.literal) - elif isinstance(pred, (BoundIn, BoundNotIn)): + elif isinstance(pred, BoundSetPredicate): + return pred.as_unbound(Reference(name), pred.literals) + else: + return None + + def strict_project(self, name: str, pred: BoundPredicate[Any]) -> Optional[UnboundPredicate[Any]]: + if isinstance(pred, BoundUnaryPredicate): + return pred.as_unbound(Reference(name)) + elif isinstance(pred, BoundLiteralPredicate): + return pred.as_unbound(Reference(name), pred.literal) + elif isinstance(pred, BoundSetPredicate): return pred.as_unbound(Reference(name), pred.literals) else: - raise ValueError(f"Could not project: {pred}") + return None @property def preserves_order(self) -> bool: @@ -590,6 +635,47 @@ def project(self, name: str, pred: BoundPredicate[L]) -> Optional[UnboundPredica return _truncate_array(name, pred, self.transform(field_type)) return None + def strict_project(self, name: str, pred: BoundPredicate[Any]) -> Optional[UnboundPredicate[Any]]: + field_type = pred.term.ref().field.field_type + + if isinstance(pred.term, BoundTransform): + return _project_transform_predicate(self, name, pred) + + if isinstance(field_type, (IntegerType, LongType, DecimalType)): + if isinstance(pred, BoundUnaryPredicate): + return pred.as_unbound(Reference(name)) + elif isinstance(pred, BoundLiteralPredicate): + return _truncate_number_strict(name, pred, self.transform(field_type)) + elif isinstance(pred, BoundNotIn): + return _set_apply_transform(name, pred, self.transform(field_type)) + else: + return None + + if isinstance(pred, BoundLiteralPredicate): + if isinstance(pred, BoundStartsWith): + literal_width = len(pred.literal.value) + if literal_width < self.width: + return pred.as_unbound(name, pred.literal.value) + elif literal_width == self.width: + return EqualTo(name, pred.literal.value) + else: + return None + elif isinstance(pred, BoundNotStartsWith): + literal_width = len(pred.literal.value) + if literal_width < self.width: + return pred.as_unbound(name, pred.literal.value) + elif literal_width == self.width: + return NotEqualTo(name, pred.literal.value) + else: + return pred.as_unbound(name, self.transform(field_type)(pred.literal.value)) + else: + # ProjectionUtil.truncateArrayStrict(name, pred, this); + return _truncate_array_strict(name, pred, self.transform(field_type)) + elif isinstance(pred, BoundNotIn): + return _set_apply_transform(name, pred, self.transform(field_type)) + else: + return None + @property def width(self) -> int: return self._width @@ -714,6 +800,9 @@ def result_type(self, source: IcebergType) -> StringType: def project(self, name: str, pred: BoundPredicate[L]) -> Optional[UnboundPredicate[Any]]: return None + def strict_project(self, name: str, pred: BoundPredicate[Any]) -> Optional[UnboundPredicate[Any]]: + return None + def __repr__(self) -> str: """Return the string representation of the UnknownTransform class.""" return f"UnknownTransform(transform={repr(self._transform)})" @@ -736,6 +825,9 @@ def result_type(self, source: IcebergType) -> IcebergType: def project(self, name: str, pred: BoundPredicate[L]) -> Optional[UnboundPredicate[Any]]: return None + def strict_project(self, name: str, pred: BoundPredicate[L]) -> Optional[UnboundPredicate[Any]]: + return None + def to_human_string(self, _: IcebergType, value: Optional[S]) -> str: return "null" @@ -766,6 +858,47 @@ def _truncate_number( return None +def _truncate_number_strict( + name: str, pred: BoundLiteralPredicate[L], transform: Callable[[Optional[L]], Optional[L]] +) -> Optional[UnboundPredicate[Any]]: + boundary = pred.literal + + if not isinstance(boundary, (LongLiteral, DecimalLiteral, DateLiteral, TimestampLiteral)): + raise ValueError(f"Expected a numeric literal, got: {type(boundary)}") + + if isinstance(pred, BoundLessThan): + return LessThan(Reference(name), _transform_literal(transform, boundary)) + elif isinstance(pred, BoundLessThanOrEqual): + return LessThan(Reference(name), _transform_literal(transform, boundary.increment())) # type: ignore + elif isinstance(pred, BoundGreaterThan): + return GreaterThan(Reference(name), _transform_literal(transform, boundary)) + elif isinstance(pred, BoundGreaterThanOrEqual): + return GreaterThan(Reference(name), _transform_literal(transform, boundary.decrement())) # type: ignore + elif isinstance(pred, BoundNotEqualTo): + return EqualTo(Reference(name), _transform_literal(transform, boundary)) + elif isinstance(pred, BoundEqualTo): + # there is no predicate that guarantees equality because adjacent longs transform to the + # same value + return None + else: + return None + + +def _truncate_array_strict( + name: str, pred: BoundLiteralPredicate[L], transform: Callable[[Optional[L]], Optional[L]] +) -> Optional[UnboundPredicate[Any]]: + boundary = pred.literal + + if isinstance(pred, (BoundLessThan, BoundLessThanOrEqual)): + return LessThan(Reference(name), _transform_literal(transform, boundary)) + elif isinstance(pred, (BoundGreaterThan, BoundGreaterThanOrEqual)): + return GreaterThan(Reference(name), _transform_literal(transform, boundary)) + if isinstance(pred, BoundNotEqualTo): + return NotEqualTo(Reference(name), _transform_literal(transform, boundary)) + else: + return None + + def _truncate_array( name: str, pred: BoundLiteralPredicate[L], transform: Callable[[Optional[L]], Optional[L]] ) -> Optional[UnboundPredicate[Any]]: @@ -808,7 +941,8 @@ def _remove_transform(partition_name: str, pred: BoundPredicate[L]) -> UnboundPr def _set_apply_transform(name: str, pred: BoundSetPredicate[L], transform: Callable[[L], L]) -> UnboundPredicate[Any]: literals = pred.literals if isinstance(pred, BoundSetPredicate): - return pred.as_unbound(Reference(name), {_transform_literal(transform, literal) for literal in literals}) + transformed_literals = {_transform_literal(transform, literal) for literal in literals} + return pred.as_unbound(Reference(name=name), literals=transformed_literals) else: raise ValueError(f"Unknown BoundSetPredicate: {pred}") diff --git a/tests/conftest.py b/tests/conftest.py index e090e7c020..3e97dc450c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -79,6 +79,7 @@ NestedField, StringType, StructType, + UUIDType, ) from pyiceberg.utils.datetime import datetime_to_millis @@ -1928,6 +1929,16 @@ def bound_reference_str() -> BoundReference[str]: return BoundReference(field=NestedField(1, "field", StringType(), required=False), accessor=Accessor(position=0, inner=None)) +@pytest.fixture +def bound_reference_binary() -> BoundReference[str]: + return BoundReference(field=NestedField(1, "field", BinaryType(), required=False), accessor=Accessor(position=0, inner=None)) + + +@pytest.fixture +def bound_reference_uuid() -> BoundReference[str]: + return BoundReference(field=NestedField(1, "field", UUIDType(), required=False), accessor=Accessor(position=0, inner=None)) + + @pytest.fixture(scope="session") def session_catalog() -> Catalog: return load_catalog( diff --git a/tests/test_transforms.py b/tests/test_transforms.py index 4fea7739d1..4dc3d9819f 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -17,7 +17,7 @@ # pylint: disable=eval-used,protected-access,redefined-outer-name from datetime import date from decimal import Decimal -from typing import Any, Callable +from typing import Any, Callable, Optional from uuid import UUID import mmh3 as mmh3 @@ -30,32 +30,42 @@ ) from typing_extensions import Annotated -from pyiceberg import transforms from pyiceberg.expressions import ( BoundEqualTo, BoundGreaterThan, BoundGreaterThanOrEqual, BoundIn, + BoundIsNull, BoundLessThan, BoundLessThanOrEqual, + BoundLiteralPredicate, + BoundNotEqualTo, BoundNotIn, BoundNotNull, BoundNotStartsWith, BoundReference, BoundStartsWith, EqualTo, + GreaterThan, GreaterThanOrEqual, In, + LessThan, LessThanOrEqual, + LiteralPredicate, + NotEqualTo, NotIn, NotNull, NotStartsWith, Reference, + SetPredicate, StartsWith, + UnaryPredicate, + UnboundPredicate, ) from pyiceberg.expressions.literals import ( DateLiteral, DecimalLiteral, + LongLiteral, TimestampLiteral, literal, ) @@ -74,7 +84,7 @@ YearTransform, parse_transform, ) -from pyiceberg.typedef import UTF8 +from pyiceberg.typedef import UTF8, L from pyiceberg.types import ( BinaryType, BooleanType, @@ -438,7 +448,7 @@ def test_truncate_method(type_var: PrimitiveType, value: Any, expected_human_str def test_unknown_transform() -> None: - unknown_transform = transforms.UnknownTransform("unknown") # type: ignore + unknown_transform = UnknownTransform("unknown") # type: ignore assert str(unknown_transform) == str(eval(repr(unknown_transform))) with pytest.raises(AttributeError): unknown_transform.transform(StringType())("test") @@ -603,9 +613,7 @@ def bound_reference_decimal() -> BoundReference[Decimal]: @pytest.fixture def bound_reference_long() -> BoundReference[int]: - return BoundReference( - field=NestedField(1, "field", DecimalType(8, 2), required=False), accessor=Accessor(position=0, inner=None) - ) + return BoundReference(field=NestedField(1, "field", LongType(), required=False), accessor=Accessor(position=0, inner=None)) def test_projection_bucket_unary(bound_reference_str: BoundReference[str]) -> None: @@ -958,3 +966,845 @@ def test_projection_truncate_string_not_starts_with(bound_reference_str: BoundRe assert TruncateTransform(2).project( "name", BoundNotStartsWith(term=bound_reference_str, literal=literal("hello")) ) == NotStartsWith(term="name", literal=literal("he")) + + +def _test_projection(lhs: Optional[UnboundPredicate[L]], rhs: Optional[UnboundPredicate[L]]) -> None: + assert type(lhs) == type(lhs), f"Different classes: {type(lhs)} != {type(rhs)}" + if lhs is None and rhs is None: + # Both null + pass + elif isinstance(lhs, UnaryPredicate) and isinstance(rhs, UnaryPredicate): + # Nothing more to check + pass + elif isinstance(lhs, LiteralPredicate) and isinstance(rhs, LiteralPredicate): + assert lhs.literal == rhs.literal, f"Different literal: {lhs.literal} != {rhs.literal}" + elif isinstance(lhs, SetPredicate) and isinstance(rhs, SetPredicate): + assert lhs.literals == rhs.literals, f"Different literals: {lhs.literals} != {rhs.literals}" + else: + raise ValueError(f"Comparing unrelated: {lhs} <> {rhs}") + + +def test_month_projection_strict_epoch(bound_reference_date: BoundReference[int]) -> None: + date = literal("1970-01-01").to(DateType()) + transform: Transform[Any, int] = MonthTransform() + _test_projection( + transform.strict_project(name="name", pred=BoundLessThan(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=DateLiteral(0)), + ) + _test_projection( + transform.strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=DateLiteral(0)), + ) + _test_projection( + transform.strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=DateLiteral(0)), + ) + _test_projection( + transform.strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=LongLiteral(-1)), # In Java this is human string 1970-01 + ) + _test_projection( + transform.strict_project(name="name", pred=BoundNotEqualTo(term=bound_reference_date, literal=date)), + NotEqualTo(term="name", literal=DateLiteral(0)), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundEqualTo(term=bound_reference_date, literal=date)), rhs=None + ) + + another_date = literal("1969-12-31").to(DateType()) + _test_projection( + transform.strict_project(name="name", pred=BoundNotIn(term=bound_reference_date, literals={date, another_date})), + NotIn(term="name", literals={DateLiteral(-1), DateLiteral(0)}), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundIn(term=bound_reference_date, literals={date, another_date})), + rhs=None, + ) + + +def test_month_projection_strict_lower_bound(bound_reference_date: BoundReference[int]) -> None: + date = literal("2017-01-01").to(DateType()) # == 564 months since epoch + transform: Transform[Any, int] = MonthTransform() + _test_projection( + transform.strict_project(name="name", pred=BoundLessThan(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=DateLiteral(564)), + ) + _test_projection( + transform.strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=DateLiteral(564)), + ) + _test_projection( + transform.strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=DateLiteral(564)), + ) + _test_projection( + transform.strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=LongLiteral(563)), + ) + _test_projection( + transform.strict_project(name="name", pred=BoundNotEqualTo(term=bound_reference_date, literal=date)), + NotEqualTo(term="name", literal=DateLiteral(564)), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundEqualTo(term=bound_reference_date, literal=date)), rhs=None + ) + + another_date = literal("2017-12-02").to(DateType()) + _test_projection( + transform.strict_project(name="name", pred=BoundNotIn(term=bound_reference_date, literals={date, another_date})), + NotIn(term="name", literals={LongLiteral(575), LongLiteral(564)}), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundIn(term=bound_reference_date, literals={date, another_date})), + rhs=None, + ) + + +def test_negative_month_projection_strict_lower_bound(bound_reference_date: BoundReference[int]) -> None: + date = literal("1969-01-01").to(DateType()) # == 564 months since epoch + transform: Transform[Any, int] = MonthTransform() + _test_projection( + transform.strict_project(name="name", pred=BoundLessThan(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=DateLiteral(-12)), + ) + _test_projection( + transform.strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=DateLiteral(-12)), + ) + _test_projection( + transform.strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=LongLiteral(-12)), + ) + _test_projection( + transform.strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=LongLiteral(-13)), + ) + _test_projection( + transform.strict_project(name="name", pred=BoundNotEqualTo(term=bound_reference_date, literal=date)), + NotEqualTo(term="name", literal=DateLiteral(-12)), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundEqualTo(term=bound_reference_date, literal=date)), rhs=None + ) + + another_date = literal("1969-12-31").to(DateType()) + _test_projection( + transform.strict_project(name="name", pred=BoundNotIn(term=bound_reference_date, literals={date, another_date})), + NotIn(term="name", literals={LongLiteral(-1), LongLiteral(-12)}), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundIn(term=bound_reference_date, literals={date, another_date})), + rhs=None, + ) + + +def test_month_projection_strict_upper_bound(bound_reference_date: BoundReference[int]) -> None: + date = literal("2017-12-31").to(DateType()) # == 575 months since epoch + transform: Transform[Any, int] = MonthTransform() + _test_projection( + transform.strict_project(name="name", pred=BoundLessThan(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=DateLiteral(575)), + ) + _test_projection( + transform.strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=LongLiteral(576)), + ) + _test_projection( + transform.strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=DateLiteral(575)), + ) + _test_projection( + transform.strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=LongLiteral(575)), + ) + _test_projection( + transform.strict_project(name="name", pred=BoundNotEqualTo(term=bound_reference_date, literal=date)), + NotEqualTo(term="name", literal=DateLiteral(575)), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundEqualTo(term=bound_reference_date, literal=date)), rhs=None + ) + + another_date = literal("2017-01-01").to(DateType()) + _test_projection( + transform.strict_project(name="name", pred=BoundNotIn(term=bound_reference_date, literals={date, another_date})), + NotIn(term="name", literals={LongLiteral(575), LongLiteral(564)}), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundIn(term=bound_reference_date, literals={date, another_date})), + rhs=None, + ) + + +def test_negative_month_projection_strict_upper_bound(bound_reference_date: BoundReference[int]) -> None: + date = literal("1969-12-31").to(DateType()) # == -1 month since epoch + transform: Transform[Any, int] = MonthTransform() + _test_projection( + transform.strict_project(name="name", pred=BoundLessThan(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=DateLiteral(-1)), + ) + _test_projection( + transform.strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=LongLiteral(0)), + ) + _test_projection( + transform.strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=DateLiteral(-1)), + ) + _test_projection( + transform.strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=LongLiteral(-1)), + ) + _test_projection( + transform.strict_project(name="name", pred=BoundNotEqualTo(term=bound_reference_date, literal=date)), + NotEqualTo(term="name", literal=DateLiteral(-1)), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundEqualTo(term=bound_reference_date, literal=date)), rhs=None + ) + + another_date = literal("1969-11-01").to(DateType()) + _test_projection( + transform.strict_project(name="name", pred=BoundNotIn(term=bound_reference_date, literals={date, another_date})), + NotIn(term="name", literals={LongLiteral(-1), LongLiteral(-2)}), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundIn(term=bound_reference_date, literals={date, another_date})), + rhs=None, + ) + + +def test_day_strict(bound_reference_date: BoundReference[int]) -> None: + date = literal("2017-01-01").to(DateType()) + _test_projection( + DayTransform().strict_project(name="name", pred=BoundLessThan(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=DateLiteral(17167)), + ) + _test_projection( + DayTransform().strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=LongLiteral(17168)), + ) + _test_projection( + DayTransform().strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=DateLiteral(17167)), + ) + _test_projection( + DayTransform().strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=LongLiteral(17166)), + ) + _test_projection( + DayTransform().strict_project(name="name", pred=BoundNotEqualTo(term=bound_reference_date, literal=date)), + NotEqualTo(term="name", literal=DateLiteral(17167)), + ) + _test_projection( + lhs=DayTransform().strict_project(name="name", pred=BoundEqualTo(term=bound_reference_date, literal=date)), rhs=None + ) + + another_date = literal("2017-12-31").to(DateType()) + _test_projection( + DayTransform().strict_project(name="name", pred=BoundNotIn(term=bound_reference_date, literals={date, another_date})), + NotIn(term="name", literals={LongLiteral(17531), LongLiteral(17167)}), + ) + _test_projection( + lhs=DayTransform().strict_project(name="name", pred=BoundIn(term=bound_reference_date, literals={date, another_date})), + rhs=None, + ) + + +def test_day_negative_strict(bound_reference_date: BoundReference[int]) -> None: + date = literal("1969-12-30").to(DateType()) + _test_projection( + DayTransform().strict_project(name="name", pred=BoundLessThan(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=DateLiteral(-2)), + ) + _test_projection( + DayTransform().strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=LongLiteral(-1)), + ) + _test_projection( + DayTransform().strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=DateLiteral(-2)), + ) + _test_projection( + DayTransform().strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=LongLiteral(-3)), + ) + _test_projection( + DayTransform().strict_project(name="name", pred=BoundNotEqualTo(term=bound_reference_date, literal=date)), + NotEqualTo(term="name", literal=DateLiteral(-2)), + ) + _test_projection( + lhs=DayTransform().strict_project(name="name", pred=BoundEqualTo(term=bound_reference_date, literal=date)), rhs=None + ) + + another_date = literal("1969-12-28").to(DateType()) + _test_projection( + DayTransform().strict_project(name="name", pred=BoundNotIn(term=bound_reference_date, literals={date, another_date})), + NotIn(term="name", literals={LongLiteral(-2), LongLiteral(-4)}), + ) + _test_projection( + lhs=DayTransform().strict_project(name="name", pred=BoundIn(term=bound_reference_date, literals={date, another_date})), + rhs=None, + ) + + +def test_year_strict_lower_bound(bound_reference_date: BoundReference[int]) -> None: + date = literal("2017-01-01").to(DateType()) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundLessThan(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=DateLiteral(47)), + ) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=LongLiteral(47)), + ) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=DateLiteral(47)), + ) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=LongLiteral(46)), + ) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundNotEqualTo(term=bound_reference_date, literal=date)), + NotEqualTo(term="name", literal=DateLiteral(47)), + ) + _test_projection( + lhs=YearTransform().strict_project(name="name", pred=BoundEqualTo(term=bound_reference_date, literal=date)), rhs=None + ) + + another_date = literal("2016-12-31").to(DateType()) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundNotIn(term=bound_reference_date, literals={date, another_date})), + NotIn(term="name", literals={LongLiteral(46), LongLiteral(47)}), + ) + _test_projection( + lhs=YearTransform().strict_project(name="name", pred=BoundIn(term=bound_reference_date, literals={date, another_date})), + rhs=None, + ) + + +def test_negative_year_strict_lower_bound(bound_reference_date: BoundReference[int]) -> None: + date = literal("1970-01-01").to(DateType()) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundLessThan(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=DateLiteral(0)), + ) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=LongLiteral(0)), + ) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=DateLiteral(0)), + ) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=LongLiteral(-1)), + ) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundNotEqualTo(term=bound_reference_date, literal=date)), + NotEqualTo(term="name", literal=DateLiteral(0)), + ) + _test_projection( + lhs=YearTransform().strict_project(name="name", pred=BoundEqualTo(term=bound_reference_date, literal=date)), rhs=None + ) + + another_date = literal("1969-12-31").to(DateType()) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundNotIn(term=bound_reference_date, literals={date, another_date})), + NotIn(term="name", literals={LongLiteral(-1), LongLiteral(0)}), + ) + _test_projection( + lhs=YearTransform().strict_project(name="name", pred=BoundIn(term=bound_reference_date, literals={date, another_date})), + rhs=None, + ) + + +def test_year_strict_upper_bound(bound_reference_date: BoundReference[int]) -> None: + date = literal("2017-12-31").to(DateType()) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundLessThan(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=DateLiteral(47)), + ) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=LongLiteral(48)), + ) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=DateLiteral(47)), + ) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=LongLiteral(47)), + ) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundNotEqualTo(term=bound_reference_date, literal=date)), + NotEqualTo(term="name", literal=DateLiteral(47)), + ) + _test_projection( + lhs=YearTransform().strict_project(name="name", pred=BoundEqualTo(term=bound_reference_date, literal=date)), rhs=None + ) + + another_date = literal("2016-01-01").to(DateType()) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundNotIn(term=bound_reference_date, literals={date, another_date})), + NotIn(term="name", literals={LongLiteral(46), LongLiteral(47)}), + ) + _test_projection( + lhs=YearTransform().strict_project(name="name", pred=BoundIn(term=bound_reference_date, literals={date, another_date})), + rhs=None, + ) + + +def test_negative_year_strict_upper_bound(bound_reference_date: BoundReference[int]) -> None: + date = literal("1969-12-31").to(DateType()) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundLessThan(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=DateLiteral(-1)), + ) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_date, literal=date)), + LessThan(term="name", literal=LongLiteral(0)), + ) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=DateLiteral(-1)), + ) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_date, literal=date)), + GreaterThan(term="name", literal=LongLiteral(-1)), + ) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundNotEqualTo(term=bound_reference_date, literal=date)), + NotEqualTo(term="name", literal=DateLiteral(-1)), + ) + _test_projection( + lhs=YearTransform().strict_project(name="name", pred=BoundEqualTo(term=bound_reference_date, literal=date)), rhs=None + ) + + another_date = literal("1970-01-01").to(DateType()) + _test_projection( + YearTransform().strict_project(name="name", pred=BoundNotIn(term=bound_reference_date, literals={date, another_date})), + NotIn(term="name", literals={LongLiteral(-1), LongLiteral(0)}), + ) + _test_projection( + lhs=YearTransform().strict_project(name="name", pred=BoundIn(term=bound_reference_date, literals={date, another_date})), + rhs=None, + ) + + +def test_strict_bucket_integer(bound_reference_long: BoundReference[int]) -> None: + value = literal(100) + transform: Transform[Any, int] = BucketTransform(num_buckets=10) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundNotEqualTo(term=bound_reference_long, literal=value)), + rhs=LessThan(term="name", literal=literal(6)), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundEqualTo(term=bound_reference_long, literal=value)), rhs=None + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThan(term=bound_reference_long, literal=value)), rhs=None + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_long, literal=value)), rhs=None + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_long, literal=value)), rhs=None + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_long, literal=value)), + rhs=None, + ) + _test_projection( + lhs=transform.strict_project( + name="name", pred=BoundNotIn(term=bound_reference_long, literals={literal(100 - 1), value, literal(100 + 1)}) + ), + rhs=NotIn(term=Reference("name"), literals={6, 7, 8}), + ) + + _test_projection( + lhs=transform.strict_project( + name="name", pred=BoundIn(term=bound_reference_long, literals={literal(100 - 1), value, literal(100 + 1)}) + ), + rhs=None, + ) + + +def test_strict_bucket_decimal(bound_reference_decimal: BoundReference[int]) -> None: + value = literal(Decimal("100.00")) + transform: Transform[Any, int] = BucketTransform(num_buckets=10) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundNotEqualTo(term=bound_reference_decimal, literal=value)), + rhs=LessThan(term="name", literal=literal(2)), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundEqualTo(term=bound_reference_decimal, literal=value)), rhs=None + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThan(term=bound_reference_decimal, literal=value)), rhs=None + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_decimal, literal=value)), + rhs=None, + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_decimal, literal=value)), rhs=None + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_decimal, literal=value)), + rhs=None, + ) + _test_projection( + lhs=transform.strict_project( + name="name", + pred=BoundNotIn( + term=bound_reference_decimal, literals={literal(Decimal("99.00")), value, literal(Decimal("101.00"))} + ), + ), + rhs=NotIn(term=Reference("name"), literals={2, 6}), + ) + _test_projection( + lhs=transform.strict_project( + name="name", + pred=BoundIn(term=bound_reference_decimal, literals={literal(Decimal("99.00")), value, literal(Decimal("101.00"))}), + ), + rhs=None, + ) + + +def test_strict_bucket_string(bound_reference_str: BoundReference[int]) -> None: + value = literal("abcdefg") + transform: Transform[Any, int] = BucketTransform(num_buckets=10) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundNotEqualTo(term=bound_reference_str, literal=value)), + rhs=LessThan(term="name", literal=literal(4)), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundEqualTo(term=bound_reference_str, literal=value)), rhs=None + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThan(term=bound_reference_str, literal=value)), rhs=None + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_str, literal=value)), rhs=None + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_str, literal=value)), rhs=None + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_str, literal=value)), rhs=None + ) + _test_projection( + lhs=transform.strict_project( + name="name", pred=BoundNotIn(term=bound_reference_str, literals={literal("abcdefg"), literal("abcdefgabc")}) + ), + rhs=NotIn(term=Reference("name"), literals={4, 9}), + ) + _test_projection( + lhs=transform.strict_project( + name="name", pred=BoundIn(term=bound_reference_str, literals={literal("abcdefg"), literal("abcdefgabc")}) + ), + rhs=None, + ) + + +def test_strict_bucket_bytes(bound_reference_binary: BoundReference[int]) -> None: + value = literal(str.encode("abcdefg")) + transform: Transform[Any, int] = BucketTransform(num_buckets=10) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundNotEqualTo(term=bound_reference_binary, literal=value)), + rhs=LessThan(term="name", literal=literal(4)), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundEqualTo(term=bound_reference_binary, literal=value)), rhs=None + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThan(term=bound_reference_binary, literal=value)), rhs=None + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_binary, literal=value)), rhs=None + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_binary, literal=value)), rhs=None + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_binary, literal=value)), + rhs=None, + ) + _test_projection( + lhs=transform.strict_project( + name="name", pred=BoundNotIn(term=bound_reference_binary, literals={value, literal(str.encode("abcdehij"))}) + ), + rhs=NotIn(term=Reference("name"), literals={4, 6}), + ) + _test_projection( + lhs=transform.strict_project( + name="name", pred=BoundIn(term=bound_reference_binary, literals={value, literal(str.encode("abcdehij"))}) + ), + rhs=None, + ) + + +def test_strict_bucket_uuid(bound_reference_uuid: BoundReference[int]) -> None: + value = literal(UUID('12345678123456781234567812345678')) + transform: Transform[Any, int] = BucketTransform(num_buckets=10) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundNotEqualTo(term=bound_reference_uuid, literal=value)), + rhs=LessThan(term="name", literal=literal(1)), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundEqualTo(term=bound_reference_uuid, literal=value)), rhs=None + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThan(term=bound_reference_uuid, literal=value)), rhs=None + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_uuid, literal=value)), rhs=None + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_uuid, literal=value)), rhs=None + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_uuid, literal=value)), + rhs=None, + ) + _test_projection( + lhs=transform.strict_project( + name="name", + pred=BoundNotIn(term=bound_reference_uuid, literals={value, literal(UUID('12345678123456781234567812345679'))}), + ), + rhs=NotIn(term=Reference("name"), literals={1, 4}), + ) + _test_projection( + lhs=transform.strict_project( + name="name", + pred=BoundIn(term=bound_reference_uuid, literals={value, literal(UUID('12345678123456781234567812345679'))}), + ), + rhs=None, + ) + + +def test_strict_identity_projection(bound_reference_long: BoundReference[int]) -> None: + transform: Transform[Any, Any] = IdentityTransform() + predicates = [ + BoundNotNull(term=bound_reference_long), + BoundIsNull(term=bound_reference_long), + BoundLessThan(term=bound_reference_long, literal=literal(100)), + BoundLessThanOrEqual(term=bound_reference_long, literal=literal(101)), + BoundGreaterThan(term=bound_reference_long, literal=literal(102)), + BoundGreaterThanOrEqual(term=bound_reference_long, literal=literal(103)), + BoundEqualTo(term=bound_reference_long, literal=literal(104)), + BoundNotEqualTo(term=bound_reference_long, literal=literal(105)), + ] + for predicate in predicates: + if isinstance(predicate, BoundLiteralPredicate): + _test_projection( + lhs=transform.strict_project( + name="name", + pred=predicate, + ), + rhs=predicate.as_unbound(term=Reference("name"), literal=predicate.literal), + ) + else: + _test_projection( + lhs=transform.strict_project( + name="name", + pred=predicate, + ), + rhs=predicate.as_unbound(term=Reference("name")), + ) + + +def test_truncate_strict_integer_lower_bound(bound_reference_long: BoundReference[int]) -> None: + value = literal(100) + transform: Transform[Any, Any] = TruncateTransform(width=10) + + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThan(term=bound_reference_long, literal=value)), + rhs=LessThan(term=Reference("name"), literal=LongLiteral(100)), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_long, literal=value)), + rhs=LessThanOrEqual(term=Reference("name"), literal=LongLiteral(100)), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_long, literal=value)), + rhs=GreaterThan(term=Reference("name"), literal=LongLiteral(100)), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_long, literal=value)), + rhs=GreaterThan(term=Reference("name"), literal=LongLiteral(90)), + ) + _test_projection( + lhs=transform.strict_project( + name="name", pred=BoundNotIn(term=bound_reference_long, literals={literal(99), literal(100), literal(101)}) + ), + rhs=NotIn(term=Reference("name"), literals={literal(90), literal(100)}), + ) + _test_projection( + lhs=transform.strict_project( + name="name", pred=BoundIn(term=bound_reference_long, literals={literal(99), literal(100), literal(101)}) + ), + rhs=None, + ) + + +def test_truncate_strict_integer_upper_bound(bound_reference_long: BoundReference[int]) -> None: + value = literal(99) + transform: Transform[Any, Any] = TruncateTransform(width=10) + + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThan(term=bound_reference_long, literal=value)), + rhs=LessThan(term=Reference("name"), literal=LongLiteral(90)), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_long, literal=value)), + rhs=LessThan(term=Reference("name"), literal=LongLiteral(100)), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_long, literal=value)), + rhs=GreaterThan(term=Reference("name"), literal=LongLiteral(90)), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_long, literal=value)), + rhs=GreaterThan(term=Reference("name"), literal=LongLiteral(90)), + ) + _test_projection( + lhs=transform.strict_project( + name="name", pred=BoundNotIn(term=bound_reference_long, literals={literal(99), literal(100), literal(101)}) + ), + rhs=NotIn(term=Reference("name"), literals={literal(90), literal(100)}), + ) + _test_projection( + lhs=transform.strict_project( + name="name", pred=BoundIn(term=bound_reference_long, literals={literal(99), literal(100), literal(101)}) + ), + rhs=None, + ) + + +def test_truncate_strict_decimal_lower_bound(bound_reference_decimal: BoundReference[Decimal]) -> None: + value = literal(Decimal("100.00")) + transform: Transform[Any, Any] = TruncateTransform(width=10) + + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThan(term=bound_reference_decimal, literal=value)), + rhs=LessThan(term=Reference("name"), literal=Decimal("100.00")), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_decimal, literal=value)), + rhs=LessThanOrEqual(term=Reference("name"), literal=Decimal("100.00")), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_decimal, literal=value)), + rhs=GreaterThan(term=Reference("name"), literal=Decimal("100.00")), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_decimal, literal=value)), + rhs=GreaterThan(term=Reference("name"), literal=Decimal("99.90")), + ) + set_of_literals = {literal(Decimal("99.00")), literal(Decimal("100.00")), literal(Decimal("101.00"))} + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundNotIn(term=bound_reference_decimal, literals=set_of_literals)), + rhs=NotIn(term=Reference("name"), literals=set_of_literals), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundIn(term=bound_reference_decimal, literals=set_of_literals)), rhs=None + ) + + +def test_truncate_strict_decimal_upper_bound(bound_reference_decimal: BoundReference[Decimal]) -> None: + value = literal(Decimal("99.99")) + transform: Transform[Any, Any] = TruncateTransform(width=10) + + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThan(term=bound_reference_decimal, literal=value)), + rhs=LessThan(term=Reference("name"), literal=Decimal("99.90")), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_decimal, literal=value)), + rhs=LessThan(term=Reference("name"), literal=Decimal("100.00")), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_decimal, literal=value)), + rhs=GreaterThan(term=Reference("name"), literal=Decimal("99.90")), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_decimal, literal=value)), + rhs=GreaterThan(term=Reference("name"), literal=Decimal("99.90")), + ) + set_of_literals = {literal(Decimal("98.99")), literal(Decimal("99.99")), literal(Decimal("100.99"))} + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundNotIn(term=bound_reference_decimal, literals=set_of_literals)), + rhs=NotIn( + term=Reference("name"), literals={literal(Decimal("98.90")), literal(Decimal("99.90")), literal(Decimal("100.90"))} + ), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundIn(term=bound_reference_decimal, literals=set_of_literals)), rhs=None + ) + + +def test_string_strict(bound_reference_str: BoundReference[str]) -> None: + value = literal("abcdefg") + transform: Transform[Any, Any] = TruncateTransform(width=5) + + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThan(term=bound_reference_str, literal=value)), + rhs=LessThan(term=Reference("name"), literal=literal("abcde")), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_str, literal=value)), + rhs=LessThan(term=Reference("name"), literal=literal("abcde")), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_str, literal=value)), + rhs=GreaterThan(term=Reference("name"), literal=literal("abcde")), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_str, literal=value)), + rhs=GreaterThan(term=Reference("name"), literal=literal("abcde")), + ) + set_of_literals = {literal("abcde"), literal("abcdefg")} + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundNotIn(term=bound_reference_str, literals=set_of_literals)), + rhs=NotEqualTo(term=Reference("name"), literal=literal("abcde")), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundIn(term=bound_reference_str, literals=set_of_literals)), rhs=None + ) + + +def test_strict_binary(bound_reference_binary: BoundReference[str]) -> None: + value = literal(b"abcdefg") + transform: Transform[Any, Any] = TruncateTransform(width=5) + abcde = literal(b"abcde") + + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThan(term=bound_reference_binary, literal=value)), + rhs=LessThan(term=Reference("name"), literal=abcde), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundLessThanOrEqual(term=bound_reference_binary, literal=value)), + rhs=LessThan(term=Reference("name"), literal=abcde), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThan(term=bound_reference_binary, literal=value)), + rhs=GreaterThan(term=Reference("name"), literal=abcde), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundGreaterThanOrEqual(term=bound_reference_binary, literal=value)), + rhs=GreaterThan(term=Reference("name"), literal=abcde), + ) + set_of_literals = {literal(b"abcde"), literal(b"abcdefg")} + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundNotIn(term=bound_reference_binary, literals=set_of_literals)), + rhs=NotEqualTo(term=Reference("name"), literal=abcde), + ) + _test_projection( + lhs=transform.strict_project(name="name", pred=BoundIn(term=bound_reference_binary, literals=set_of_literals)), rhs=None + )