diff --git a/src/iceberg/test/predicate_test.cc b/src/iceberg/test/predicate_test.cc index 532e908b4..fab0b5617 100644 --- a/src/iceberg/test/predicate_test.cc +++ b/src/iceberg/test/predicate_test.cc @@ -26,7 +26,6 @@ #include "iceberg/schema.h" #include "iceberg/test/matchers.h" #include "iceberg/type.h" -#include "iceberg/util/macros.h" namespace iceberg { @@ -607,24 +606,24 @@ std::shared_ptr AssertAndCastToBoundPredicate( } // namespace TEST_F(PredicateTest, BoundUnaryPredicateTestIsNull) { - ICEBERG_ASSIGN_OR_THROW(auto is_null_pred, Expressions::IsNull("name")->Bind( - *schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto is_null_pred, Expressions::IsNull("name")->Bind( + *schema_, /*case_sensitive=*/true)); auto bound_pred = AssertAndCastToBoundPredicate(is_null_pred); EXPECT_THAT(bound_pred->Test(Literal::Null(string())), HasValue(testing::Eq(true))); EXPECT_THAT(bound_pred->Test(Literal::String("test")), HasValue(testing::Eq(false))); } TEST_F(PredicateTest, BoundUnaryPredicateTestNotNull) { - ICEBERG_ASSIGN_OR_THROW(auto not_null_pred, Expressions::NotNull("name")->Bind( - *schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto not_null_pred, Expressions::NotNull("name")->Bind( + *schema_, /*case_sensitive=*/true)); auto bound_pred = AssertAndCastToBoundPredicate(not_null_pred); EXPECT_THAT(bound_pred->Test(Literal::String("test")), HasValue(testing::Eq(true))); EXPECT_THAT(bound_pred->Test(Literal::Null(string())), HasValue(testing::Eq(false))); } TEST_F(PredicateTest, BoundUnaryPredicateTestIsNaN) { - ICEBERG_ASSIGN_OR_THROW(auto is_nan_pred, Expressions::IsNaN("salary")->Bind( - *schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto is_nan_pred, Expressions::IsNaN("salary")->Bind( + *schema_, /*case_sensitive=*/true)); auto bound_pred = AssertAndCastToBoundPredicate(is_nan_pred); // Test with NaN values @@ -643,8 +642,8 @@ TEST_F(PredicateTest, BoundUnaryPredicateTestIsNaN) { } TEST_F(PredicateTest, BoundUnaryPredicateTestNotNaN) { - ICEBERG_ASSIGN_OR_THROW(auto not_nan_pred, Expressions::NotNaN("salary")->Bind( - *schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto not_nan_pred, Expressions::NotNaN("salary")->Bind( + *schema_, /*case_sensitive=*/true)); auto bound_pred = AssertAndCastToBoundPredicate(not_nan_pred); // Test with regular values @@ -661,34 +660,34 @@ TEST_F(PredicateTest, BoundUnaryPredicateTestNotNaN) { TEST_F(PredicateTest, BoundLiteralPredicateTestComparison) { // Test less than - ICEBERG_ASSIGN_OR_THROW(auto lt_pred, Expressions::LessThan("age", Literal::Int(30)) - ->Bind(*schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto lt_pred, Expressions::LessThan("age", Literal::Int(30)) + ->Bind(*schema_, /*case_sensitive=*/true)); auto bound_lt = AssertAndCastToBoundPredicate(lt_pred); EXPECT_THAT(bound_lt->Test(Literal::Int(20)), HasValue(testing::Eq(true))); EXPECT_THAT(bound_lt->Test(Literal::Int(30)), HasValue(testing::Eq(false))); EXPECT_THAT(bound_lt->Test(Literal::Int(40)), HasValue(testing::Eq(false))); // Test less than or equal - ICEBERG_ASSIGN_OR_THROW(auto lte_pred, - Expressions::LessThanOrEqual("age", Literal::Int(30)) - ->Bind(*schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto lte_pred, + Expressions::LessThanOrEqual("age", Literal::Int(30)) + ->Bind(*schema_, /*case_sensitive=*/true)); auto bound_lte = AssertAndCastToBoundPredicate(lte_pred); EXPECT_THAT(bound_lte->Test(Literal::Int(20)), HasValue(testing::Eq(true))); EXPECT_THAT(bound_lte->Test(Literal::Int(30)), HasValue(testing::Eq(true))); EXPECT_THAT(bound_lte->Test(Literal::Int(40)), HasValue(testing::Eq(false))); // Test greater than - ICEBERG_ASSIGN_OR_THROW(auto gt_pred, Expressions::GreaterThan("age", Literal::Int(30)) - ->Bind(*schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto gt_pred, Expressions::GreaterThan("age", Literal::Int(30)) + ->Bind(*schema_, /*case_sensitive=*/true)); auto bound_gt = AssertAndCastToBoundPredicate(gt_pred); EXPECT_THAT(bound_gt->Test(Literal::Int(20)), HasValue(testing::Eq(false))); EXPECT_THAT(bound_gt->Test(Literal::Int(30)), HasValue(testing::Eq(false))); EXPECT_THAT(bound_gt->Test(Literal::Int(40)), HasValue(testing::Eq(true))); // Test greater than or equal - ICEBERG_ASSIGN_OR_THROW(auto gte_pred, - Expressions::GreaterThanOrEqual("age", Literal::Int(30)) - ->Bind(*schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto gte_pred, + Expressions::GreaterThanOrEqual("age", Literal::Int(30)) + ->Bind(*schema_, /*case_sensitive=*/true)); auto bound_gte = AssertAndCastToBoundPredicate(gte_pred); EXPECT_THAT(bound_gte->Test(Literal::Int(20)), HasValue(testing::Eq(false))); EXPECT_THAT(bound_gte->Test(Literal::Int(30)), HasValue(testing::Eq(true))); @@ -697,16 +696,16 @@ TEST_F(PredicateTest, BoundLiteralPredicateTestComparison) { TEST_F(PredicateTest, BoundLiteralPredicateTestEquality) { // Test equal - ICEBERG_ASSIGN_OR_THROW(auto eq_pred, Expressions::Equal("age", Literal::Int(25)) - ->Bind(*schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto eq_pred, Expressions::Equal("age", Literal::Int(25)) + ->Bind(*schema_, /*case_sensitive=*/true)); auto bound_eq = AssertAndCastToBoundPredicate(eq_pred); EXPECT_THAT(bound_eq->Test(Literal::Int(25)), HasValue(testing::Eq(true))); EXPECT_THAT(bound_eq->Test(Literal::Int(26)), HasValue(testing::Eq(false))); EXPECT_THAT(bound_eq->Test(Literal::Int(24)), HasValue(testing::Eq(false))); // Test not equal - ICEBERG_ASSIGN_OR_THROW(auto neq_pred, Expressions::NotEqual("age", Literal::Int(25)) - ->Bind(*schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto neq_pred, Expressions::NotEqual("age", Literal::Int(25)) + ->Bind(*schema_, /*case_sensitive=*/true)); auto bound_neq = AssertAndCastToBoundPredicate(neq_pred); EXPECT_THAT(bound_neq->Test(Literal::Int(25)), HasValue(testing::Eq(false))); EXPECT_THAT(bound_neq->Test(Literal::Int(26)), HasValue(testing::Eq(true))); @@ -715,18 +714,18 @@ TEST_F(PredicateTest, BoundLiteralPredicateTestEquality) { TEST_F(PredicateTest, BoundLiteralPredicateTestWithDifferentTypes) { // Test with double - ICEBERG_ASSIGN_OR_THROW(auto gt_pred, - Expressions::GreaterThan("salary", Literal::Double(50000.0)) - ->Bind(*schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto gt_pred, + Expressions::GreaterThan("salary", Literal::Double(50000.0)) + ->Bind(*schema_, /*case_sensitive=*/true)); auto bound_double = AssertAndCastToBoundPredicate(gt_pred); EXPECT_THAT(bound_double->Test(Literal::Double(60000.0)), HasValue(testing::Eq(true))); EXPECT_THAT(bound_double->Test(Literal::Double(40000.0)), HasValue(testing::Eq(false))); EXPECT_THAT(bound_double->Test(Literal::Double(50000.0)), HasValue(testing::Eq(false))); // Test with string - ICEBERG_ASSIGN_OR_THROW(auto str_eq_pred, - Expressions::Equal("name", Literal::String("Alice")) - ->Bind(*schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto str_eq_pred, + Expressions::Equal("name", Literal::String("Alice")) + ->Bind(*schema_, /*case_sensitive=*/true)); auto bound_string = AssertAndCastToBoundPredicate(str_eq_pred); EXPECT_THAT(bound_string->Test(Literal::String("Alice")), HasValue(testing::Eq(true))); EXPECT_THAT(bound_string->Test(Literal::String("Bob")), HasValue(testing::Eq(false))); @@ -734,16 +733,16 @@ TEST_F(PredicateTest, BoundLiteralPredicateTestWithDifferentTypes) { HasValue(testing::Eq(false))); // Case sensitive // Test with boolean - ICEBERG_ASSIGN_OR_THROW(auto bool_eq_pred, - Expressions::Equal("active", Literal::Boolean(true)) - ->Bind(*schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bool_eq_pred, + Expressions::Equal("active", Literal::Boolean(true)) + ->Bind(*schema_, /*case_sensitive=*/true)); auto bound_bool = AssertAndCastToBoundPredicate(bool_eq_pred); EXPECT_THAT(bound_bool->Test(Literal::Boolean(true)), HasValue(testing::Eq(true))); EXPECT_THAT(bound_bool->Test(Literal::Boolean(false)), HasValue(testing::Eq(false))); } TEST_F(PredicateTest, BoundLiteralPredicateTestStartsWith) { - ICEBERG_ASSIGN_OR_THROW( + ICEBERG_UNWRAP_OR_FAIL( auto starts_with_pred, Expressions::StartsWith("name", "Jo")->Bind(*schema_, /*case_sensitive=*/true)); auto bound_pred = AssertAndCastToBoundPredicate(starts_with_pred); @@ -759,7 +758,7 @@ TEST_F(PredicateTest, BoundLiteralPredicateTestStartsWith) { EXPECT_THAT(bound_pred->Test(Literal::String("")), HasValue(testing::Eq(false))); // Test empty prefix - ICEBERG_ASSIGN_OR_THROW( + ICEBERG_UNWRAP_OR_FAIL( auto empty_prefix_pred, Expressions::StartsWith("name", "")->Bind(*schema_, /*case_sensitive=*/true)); auto bound_empty = AssertAndCastToBoundPredicate(empty_prefix_pred); @@ -770,7 +769,7 @@ TEST_F(PredicateTest, BoundLiteralPredicateTestStartsWith) { } TEST_F(PredicateTest, BoundLiteralPredicateTestNotStartsWith) { - ICEBERG_ASSIGN_OR_THROW( + ICEBERG_UNWRAP_OR_FAIL( auto not_starts_with_pred, Expressions::NotStartsWith("name", "Jo")->Bind(*schema_, /*case_sensitive=*/true)); auto bound_pred = AssertAndCastToBoundPredicate(not_starts_with_pred); @@ -787,7 +786,7 @@ TEST_F(PredicateTest, BoundLiteralPredicateTestNotStartsWith) { } TEST_F(PredicateTest, BoundSetPredicateTestIn) { - ICEBERG_ASSIGN_OR_THROW( + ICEBERG_UNWRAP_OR_FAIL( auto in_pred, Expressions::In("age", {Literal::Int(10), Literal::Int(20), Literal::Int(30)}) ->Bind(*schema_, /*case_sensitive=*/true)); @@ -805,7 +804,7 @@ TEST_F(PredicateTest, BoundSetPredicateTestIn) { } TEST_F(PredicateTest, BoundSetPredicateTestNotIn) { - ICEBERG_ASSIGN_OR_THROW( + ICEBERG_UNWRAP_OR_FAIL( auto not_in_pred, Expressions::NotIn("age", {Literal::Int(10), Literal::Int(20), Literal::Int(30)}) ->Bind(*schema_, /*case_sensitive=*/true)); @@ -823,7 +822,7 @@ TEST_F(PredicateTest, BoundSetPredicateTestNotIn) { } TEST_F(PredicateTest, BoundSetPredicateTestWithStrings) { - ICEBERG_ASSIGN_OR_THROW( + ICEBERG_UNWRAP_OR_FAIL( auto in_pred, Expressions::In("name", {Literal::String("Alice"), Literal::String("Bob"), Literal::String("Charlie")}) @@ -843,10 +842,10 @@ TEST_F(PredicateTest, BoundSetPredicateTestWithStrings) { } TEST_F(PredicateTest, BoundSetPredicateTestWithLongs) { - ICEBERG_ASSIGN_OR_THROW(auto in_pred, - Expressions::In("id", {Literal::Long(100L), Literal::Long(200L), - Literal::Long(300L)}) - ->Bind(*schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto in_pred, + Expressions::In("id", {Literal::Long(100L), Literal::Long(200L), + Literal::Long(300L)}) + ->Bind(*schema_, /*case_sensitive=*/true)); auto bound_pred = AssertAndCastToBoundPredicate(in_pred); // Test longs in the set @@ -860,8 +859,8 @@ TEST_F(PredicateTest, BoundSetPredicateTestWithLongs) { } TEST_F(PredicateTest, BoundSetPredicateTestSingleLiteral) { - ICEBERG_ASSIGN_OR_THROW(auto in_pred, Expressions::In("age", {Literal::Int(42)}) - ->Bind(*schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto in_pred, Expressions::In("age", {Literal::Int(42)}) + ->Bind(*schema_, /*case_sensitive=*/true)); // Single element IN becomes Equal EXPECT_EQ(in_pred->op(), Expression::Operation::kEq); diff --git a/src/iceberg/test/transform_test.cc b/src/iceberg/test/transform_test.cc index 821edac55..7f0514df4 100644 --- a/src/iceberg/test/transform_test.cc +++ b/src/iceberg/test/transform_test.cc @@ -36,7 +36,6 @@ #include "iceberg/type.h" #include "iceberg/util/checked_cast.h" #include "iceberg/util/formatter.h" // IWYU pragma: keep -#include "iceberg/util/macros.h" namespace iceberg { @@ -954,12 +953,12 @@ TEST_F(TransformProjectTest, IdentityProjectEquality) { // Test equality predicate auto unbound = Expressions::Equal("value", Literal::Int(100)); - ICEBERG_ASSIGN_OR_THROW(auto bound, - unbound->Bind(*int_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*int_schema_, /*case_sensitive=*/true)); auto bound_pred = std::dynamic_pointer_cast(bound); ASSERT_NE(bound_pred, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected, transform->Project("part", bound_pred)); + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); ASSERT_NE(projected, nullptr); EXPECT_EQ(projected->op(), Expression::Operation::kEq); @@ -977,23 +976,23 @@ TEST_F(TransformProjectTest, IdentityProjectComparison) { // Test less than predicate auto unbound_lt = Expressions::LessThan("value", Literal::Int(50)); - ICEBERG_ASSIGN_OR_THROW(auto bound_lt, - unbound_lt->Bind(*int_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound_lt, + unbound_lt->Bind(*int_schema_, /*case_sensitive=*/true)); auto bound_pred_lt = std::dynamic_pointer_cast(bound_lt); ASSERT_NE(bound_pred_lt, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected_lt, transform->Project("part", bound_pred_lt)); + ICEBERG_UNWRAP_OR_FAIL(auto projected_lt, transform->Project("part", bound_pred_lt)); ASSERT_NE(projected_lt, nullptr); EXPECT_EQ(projected_lt->op(), Expression::Operation::kLt); // Test greater than or equal predicate auto unbound_gte = Expressions::GreaterThanOrEqual("value", Literal::Int(100)); - ICEBERG_ASSIGN_OR_THROW(auto bound_gte, - unbound_gte->Bind(*int_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound_gte, + unbound_gte->Bind(*int_schema_, /*case_sensitive=*/true)); auto bound_pred_gte = std::dynamic_pointer_cast(bound_gte); ASSERT_NE(bound_pred_gte, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected_gte, transform->Project("part", bound_pred_gte)); + ICEBERG_UNWRAP_OR_FAIL(auto projected_gte, transform->Project("part", bound_pred_gte)); ASSERT_NE(projected_gte, nullptr); EXPECT_EQ(projected_gte->op(), Expression::Operation::kGtEq); } @@ -1003,13 +1002,13 @@ TEST_F(TransformProjectTest, IdentityProjectUnary) { // Test IsNull predicate auto unbound_null = Expressions::IsNull("value"); - ICEBERG_ASSIGN_OR_THROW(auto bound_null, - unbound_null->Bind(*int_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound_null, + unbound_null->Bind(*int_schema_, /*case_sensitive=*/true)); auto bound_pred_null = std::dynamic_pointer_cast(bound_null); ASSERT_NE(bound_pred_null, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected_null, - transform->Project("part", bound_pred_null)); + ICEBERG_UNWRAP_OR_FAIL(auto projected_null, + transform->Project("part", bound_pred_null)); ASSERT_NE(projected_null, nullptr); EXPECT_EQ(projected_null->op(), Expression::Operation::kIsNull); } @@ -1020,12 +1019,12 @@ TEST_F(TransformProjectTest, IdentityProjectSet) { // Test IN predicate auto unbound_in = Expressions::In("value", {Literal::Int(1), Literal::Int(2), Literal::Int(3)}); - ICEBERG_ASSIGN_OR_THROW(auto bound_in, - unbound_in->Bind(*int_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound_in, + unbound_in->Bind(*int_schema_, /*case_sensitive=*/true)); auto bound_pred_in = std::dynamic_pointer_cast(bound_in); ASSERT_NE(bound_pred_in, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected_in, transform->Project("part", bound_pred_in)); + ICEBERG_UNWRAP_OR_FAIL(auto projected_in, transform->Project("part", bound_pred_in)); ASSERT_NE(projected_in, nullptr); EXPECT_EQ(projected_in->op(), Expression::Operation::kIn); auto unbound_projected = @@ -1046,12 +1045,12 @@ TEST_F(TransformProjectTest, BucketProjectEquality) { // Bucket can project equality predicates auto unbound = Expressions::Equal("value", Literal::Int(34)); - ICEBERG_ASSIGN_OR_THROW(auto bound, - unbound->Bind(*int_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*int_schema_, /*case_sensitive=*/true)); auto bound_pred = std::dynamic_pointer_cast(bound); ASSERT_NE(bound_pred, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected, transform->Project("part", bound_pred)); + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); ASSERT_NE(projected, nullptr); EXPECT_EQ(projected->op(), Expression::Operation::kEq); @@ -1070,8 +1069,8 @@ TEST_F(TransformProjectTest, BucketProjectWithMatchingTransformedChild) { // Create a predicate like: bucket(value, 16) = 5 auto bucket_term = Expressions::Bucket("value", 16); auto unbound = Expressions::Equal(bucket_term, Literal::Int(5)); - ICEBERG_ASSIGN_OR_THROW(auto bound, - unbound->Bind(*int_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*int_schema_, /*case_sensitive=*/true)); auto bound_pred = std::dynamic_pointer_cast(bound); ASSERT_NE(bound_pred, nullptr); @@ -1080,8 +1079,8 @@ TEST_F(TransformProjectTest, BucketProjectWithMatchingTransformedChild) { // When the transform matches, Project should use RemoveTransform and return the // predicate - ICEBERG_ASSIGN_OR_THROW(auto projected, - partition_transform->Project("part", bound_pred)); + ICEBERG_UNWRAP_OR_FAIL(auto projected, + partition_transform->Project("part", bound_pred)); ASSERT_NE(projected, nullptr); EXPECT_EQ(projected->op(), Expression::Operation::kEq); auto unbound_projected = @@ -1098,12 +1097,12 @@ TEST_F(TransformProjectTest, BucketProjectComparisonReturnsNull) { // Bucket cannot project comparison predicates (they return null) auto unbound_lt = Expressions::LessThan("value", Literal::Int(50)); - ICEBERG_ASSIGN_OR_THROW(auto bound_lt, - unbound_lt->Bind(*int_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound_lt, + unbound_lt->Bind(*int_schema_, /*case_sensitive=*/true)); auto bound_pred_lt = std::dynamic_pointer_cast(bound_lt); ASSERT_NE(bound_pred_lt, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected_lt, transform->Project("part", bound_pred_lt)); + ICEBERG_UNWRAP_OR_FAIL(auto projected_lt, transform->Project("part", bound_pred_lt)); EXPECT_EQ(projected_lt, nullptr); } @@ -1113,12 +1112,12 @@ TEST_F(TransformProjectTest, BucketProjectInSet) { // Bucket can project IN predicates auto unbound_in = Expressions::In("value", {Literal::Int(1), Literal::Int(2), Literal::Int(3)}); - ICEBERG_ASSIGN_OR_THROW(auto bound_in, - unbound_in->Bind(*int_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound_in, + unbound_in->Bind(*int_schema_, /*case_sensitive=*/true)); auto bound_pred_in = std::dynamic_pointer_cast(bound_in); ASSERT_NE(bound_pred_in, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected_in, transform->Project("part", bound_pred_in)); + ICEBERG_UNWRAP_OR_FAIL(auto projected_in, transform->Project("part", bound_pred_in)); ASSERT_NE(projected_in, nullptr); EXPECT_EQ(projected_in->op(), Expression::Operation::kIn); } @@ -1129,13 +1128,13 @@ TEST_F(TransformProjectTest, BucketProjectNotInReturnsNull) { // Bucket cannot project NOT IN predicates auto unbound_not_in = Expressions::NotIn("value", {Literal::Int(1), Literal::Int(2), Literal::Int(3)}); - ICEBERG_ASSIGN_OR_THROW(auto bound_not_in, - unbound_not_in->Bind(*int_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound_not_in, + unbound_not_in->Bind(*int_schema_, /*case_sensitive=*/true)); auto bound_pred_not_in = std::dynamic_pointer_cast(bound_not_in); ASSERT_NE(bound_pred_not_in, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected_not_in, - transform->Project("part", bound_pred_not_in)); + ICEBERG_UNWRAP_OR_FAIL(auto projected_not_in, + transform->Project("part", bound_pred_not_in)); EXPECT_EQ(projected_not_in, nullptr); } @@ -1144,12 +1143,12 @@ TEST_F(TransformProjectTest, TruncateProjectIntEquality) { // Truncate can project equality predicates auto unbound = Expressions::Equal("value", Literal::Int(123)); - ICEBERG_ASSIGN_OR_THROW(auto bound, - unbound->Bind(*int_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*int_schema_, /*case_sensitive=*/true)); auto bound_pred = std::dynamic_pointer_cast(bound); ASSERT_NE(bound_pred, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected, transform->Project("part", bound_pred)); + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); ASSERT_NE(projected, nullptr); EXPECT_EQ(projected->op(), Expression::Operation::kEq); @@ -1167,12 +1166,12 @@ TEST_F(TransformProjectTest, TruncateProjectIntLessThan) { // Truncate projects LT as LTE auto unbound = Expressions::LessThan("value", Literal::Int(25)); - ICEBERG_ASSIGN_OR_THROW(auto bound, - unbound->Bind(*int_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*int_schema_, /*case_sensitive=*/true)); auto bound_pred = std::dynamic_pointer_cast(bound); ASSERT_NE(bound_pred, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected, transform->Project("part", bound_pred)); + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); ASSERT_NE(projected, nullptr); EXPECT_EQ(projected->op(), Expression::Operation::kLtEq); } @@ -1182,12 +1181,12 @@ TEST_F(TransformProjectTest, TruncateProjectIntGreaterThan) { // Truncate projects GT as GTE auto unbound = Expressions::GreaterThan("value", Literal::Int(25)); - ICEBERG_ASSIGN_OR_THROW(auto bound, - unbound->Bind(*int_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*int_schema_, /*case_sensitive=*/true)); auto bound_pred = std::dynamic_pointer_cast(bound); ASSERT_NE(bound_pred, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected, transform->Project("part", bound_pred)); + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); ASSERT_NE(projected, nullptr); EXPECT_EQ(projected->op(), Expression::Operation::kGtEq); @@ -1204,12 +1203,12 @@ TEST_F(TransformProjectTest, TruncateProjectStringEquality) { auto transform = Transform::Truncate(5); auto unbound = Expressions::Equal("value", Literal::String("Hello, World!")); - ICEBERG_ASSIGN_OR_THROW(auto bound, - unbound->Bind(*string_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*string_schema_, /*case_sensitive=*/true)); auto bound_pred = std::dynamic_pointer_cast(bound); ASSERT_NE(bound_pred, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected, transform->Project("part", bound_pred)); + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); ASSERT_NE(projected, nullptr); EXPECT_EQ(projected->op(), Expression::Operation::kEq); @@ -1228,13 +1227,13 @@ TEST_F(TransformProjectTest, TruncateProjectStringStartsWith) { // StartsWith with shorter string than width auto unbound_short = Expressions::StartsWith("value", "Hi"); - ICEBERG_ASSIGN_OR_THROW(auto bound_short, - unbound_short->Bind(*string_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound_short, + unbound_short->Bind(*string_schema_, /*case_sensitive=*/true)); auto bound_pred_short = std::dynamic_pointer_cast(bound_short); ASSERT_NE(bound_pred_short, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected_short, - transform->Project("part", bound_pred_short)); + ICEBERG_UNWRAP_OR_FAIL(auto projected_short, + transform->Project("part", bound_pred_short)); ASSERT_NE(projected_short, nullptr); EXPECT_EQ(projected_short->op(), Expression::Operation::kStartsWith); @@ -1249,13 +1248,13 @@ TEST_F(TransformProjectTest, TruncateProjectStringStartsWith) { // StartsWith with string equal to width auto unbound_equal = Expressions::StartsWith("value", "Hello"); - ICEBERG_ASSIGN_OR_THROW(auto bound_equal, - unbound_equal->Bind(*string_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound_equal, + unbound_equal->Bind(*string_schema_, /*case_sensitive=*/true)); auto bound_pred_equal = std::dynamic_pointer_cast(bound_equal); ASSERT_NE(bound_pred_equal, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected_equal, - transform->Project("part", bound_pred_equal)); + ICEBERG_UNWRAP_OR_FAIL(auto projected_equal, + transform->Project("part", bound_pred_equal)); ASSERT_NE(projected_equal, nullptr); EXPECT_EQ(projected_equal->op(), Expression::Operation::kEq); @@ -1275,15 +1274,15 @@ TEST_F(TransformProjectTest, TruncateProjectStringStartsWithCodePointCountLessTh // Code point count < width (multi-byte UTF-8 characters) // "😜🧐" has 2 code points, width is 5 auto unbound_emoji_short = Expressions::StartsWith("value", "😜🧐"); - ICEBERG_ASSIGN_OR_THROW( + ICEBERG_UNWRAP_OR_FAIL( auto bound_emoji_short, unbound_emoji_short->Bind(*string_schema_, /*case_sensitive=*/true)); auto bound_pred_emoji_short = std::dynamic_pointer_cast(bound_emoji_short); ASSERT_NE(bound_pred_emoji_short, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected_emoji_short, - transform->Project("part", bound_pred_emoji_short)); + ICEBERG_UNWRAP_OR_FAIL(auto projected_emoji_short, + transform->Project("part", bound_pred_emoji_short)); ASSERT_NE(projected_emoji_short, nullptr); EXPECT_EQ(projected_emoji_short->op(), Expression::Operation::kStartsWith); @@ -1304,15 +1303,15 @@ TEST_F(TransformProjectTest, TruncateProjectStringStartsWithCodePointCountEqualT // Code point count == width (exactly 5 code points) // "πŸ˜œπŸ§πŸ€”πŸ€ͺπŸ₯³" has exactly 5 code points auto unbound_emoji_equal = Expressions::StartsWith("value", "πŸ˜œπŸ§πŸ€”πŸ€ͺπŸ₯³"); - ICEBERG_ASSIGN_OR_THROW( + ICEBERG_UNWRAP_OR_FAIL( auto bound_emoji_equal, unbound_emoji_equal->Bind(*string_schema_, /*case_sensitive=*/true)); auto bound_pred_emoji_equal = std::dynamic_pointer_cast(bound_emoji_equal); ASSERT_NE(bound_pred_emoji_equal, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected_emoji_equal, - transform->Project("part", bound_pred_emoji_equal)); + ICEBERG_UNWRAP_OR_FAIL(auto projected_emoji_equal, + transform->Project("part", bound_pred_emoji_equal)); ASSERT_NE(projected_emoji_equal, nullptr); EXPECT_EQ(projected_emoji_equal->op(), Expression::Operation::kEq); @@ -1335,15 +1334,15 @@ TEST_F(TransformProjectTest, // "πŸ˜œπŸ§πŸ€”πŸ€ͺπŸ₯³πŸ˜΅β€πŸ’«πŸ˜‚" has 7 code points, should truncate to 5 auto unbound_emoji_long = Expressions::StartsWith("value", "πŸ˜œπŸ§πŸ€”πŸ€ͺπŸ₯³πŸ˜΅β€πŸ’«πŸ˜‚"); - ICEBERG_ASSIGN_OR_THROW( + ICEBERG_UNWRAP_OR_FAIL( auto bound_emoji_long, unbound_emoji_long->Bind(*string_schema_, /*case_sensitive=*/true)); auto bound_pred_emoji_long = std::dynamic_pointer_cast(bound_emoji_long); ASSERT_NE(bound_pred_emoji_long, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected_emoji_long, - transform->Project("part", bound_pred_emoji_long)); + ICEBERG_UNWRAP_OR_FAIL(auto projected_emoji_long, + transform->Project("part", bound_pred_emoji_long)); ASSERT_NE(projected_emoji_long, nullptr); EXPECT_EQ(projected_emoji_long->op(), Expression::Operation::kStartsWith); @@ -1364,15 +1363,15 @@ TEST_F(TransformProjectTest, TruncateProjectStringStartsWithMixedAsciiAndMultiBy // Mixed ASCII and multi-byte UTF-8 characters // "a😜b🧐c" has 5 code points (3 ASCII + 2 emojis) auto unbound_mixed_equal = Expressions::StartsWith("value", "a😜b🧐c"); - ICEBERG_ASSIGN_OR_THROW( + ICEBERG_UNWRAP_OR_FAIL( auto bound_mixed_equal, unbound_mixed_equal->Bind(*string_schema_, /*case_sensitive=*/true)); auto bound_pred_mixed_equal = std::dynamic_pointer_cast(bound_mixed_equal); ASSERT_NE(bound_pred_mixed_equal, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected_mixed_equal, - transform->Project("part", bound_pred_mixed_equal)); + ICEBERG_UNWRAP_OR_FAIL(auto projected_mixed_equal, + transform->Project("part", bound_pred_mixed_equal)); ASSERT_NE(projected_mixed_equal, nullptr); EXPECT_EQ(projected_mixed_equal->op(), Expression::Operation::kEq); @@ -1393,15 +1392,15 @@ TEST_F(TransformProjectTest, TruncateProjectStringStartsWithChineseCharactersSho // Chinese characters (3-byte UTF-8) // "δ½ ε₯½δΈ–η•Œ" has 4 code points, width is 5 auto unbound_chinese_short = Expressions::StartsWith("value", "δ½ ε₯½δΈ–η•Œ"); - ICEBERG_ASSIGN_OR_THROW( + ICEBERG_UNWRAP_OR_FAIL( auto bound_chinese_short, unbound_chinese_short->Bind(*string_schema_, /*case_sensitive=*/true)); auto bound_pred_chinese_short = std::dynamic_pointer_cast(bound_chinese_short); ASSERT_NE(bound_pred_chinese_short, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected_chinese_short, - transform->Project("part", bound_pred_chinese_short)); + ICEBERG_UNWRAP_OR_FAIL(auto projected_chinese_short, + transform->Project("part", bound_pred_chinese_short)); ASSERT_NE(projected_chinese_short, nullptr); EXPECT_EQ(projected_chinese_short->op(), Expression::Operation::kStartsWith); @@ -1422,15 +1421,15 @@ TEST_F(TransformProjectTest, TruncateProjectStringStartsWithChineseCharactersEqu // Chinese characters exactly matching width // "δ½ ε₯½δΈ–η•Œε₯½" has exactly 5 code points auto unbound_chinese_equal = Expressions::StartsWith("value", "δ½ ε₯½δΈ–η•Œε₯½"); - ICEBERG_ASSIGN_OR_THROW( + ICEBERG_UNWRAP_OR_FAIL( auto bound_chinese_equal, unbound_chinese_equal->Bind(*string_schema_, /*case_sensitive=*/true)); auto bound_pred_chinese_equal = std::dynamic_pointer_cast(bound_chinese_equal); ASSERT_NE(bound_pred_chinese_equal, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected_chinese_equal, - transform->Project("part", bound_pred_chinese_equal)); + ICEBERG_UNWRAP_OR_FAIL(auto projected_chinese_equal, + transform->Project("part", bound_pred_chinese_equal)); ASSERT_NE(projected_chinese_equal, nullptr); EXPECT_EQ(projected_chinese_equal->op(), Expression::Operation::kEq); @@ -1452,15 +1451,15 @@ TEST_F(TransformProjectTest, // NotStartsWith with code point count == width // Should convert to NotEq auto unbound_not_starts_equal = Expressions::NotStartsWith("value", "πŸ˜œπŸ§πŸ€”πŸ€ͺπŸ₯³"); - ICEBERG_ASSIGN_OR_THROW( + ICEBERG_UNWRAP_OR_FAIL( auto bound_not_starts_equal, unbound_not_starts_equal->Bind(*string_schema_, /*case_sensitive=*/true)); auto bound_pred_not_starts_equal = std::dynamic_pointer_cast(bound_not_starts_equal); ASSERT_NE(bound_pred_not_starts_equal, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected_not_starts_equal, - transform->Project("part", bound_pred_not_starts_equal)); + ICEBERG_UNWRAP_OR_FAIL(auto projected_not_starts_equal, + transform->Project("part", bound_pred_not_starts_equal)); ASSERT_NE(projected_not_starts_equal, nullptr); EXPECT_EQ(projected_not_starts_equal->op(), Expression::Operation::kNotEq); @@ -1482,15 +1481,15 @@ TEST_F(TransformProjectTest, // NotStartsWith with code point count < width // Should remain NotStartsWith auto unbound_not_starts_short = Expressions::NotStartsWith("value", "😜🧐"); - ICEBERG_ASSIGN_OR_THROW( + ICEBERG_UNWRAP_OR_FAIL( auto bound_not_starts_short, unbound_not_starts_short->Bind(*string_schema_, /*case_sensitive=*/true)); auto bound_pred_not_starts_short = std::dynamic_pointer_cast(bound_not_starts_short); ASSERT_NE(bound_pred_not_starts_short, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected_not_starts_short, - transform->Project("part", bound_pred_not_starts_short)); + ICEBERG_UNWRAP_OR_FAIL(auto projected_not_starts_short, + transform->Project("part", bound_pred_not_starts_short)); ASSERT_NE(projected_not_starts_short, nullptr); EXPECT_EQ(projected_not_starts_short->op(), Expression::Operation::kNotStartsWith); @@ -1514,15 +1513,15 @@ TEST_F(TransformProjectTest, // Should return nullptr (cannot project) auto unbound_not_starts_long = Expressions::NotStartsWith("value", "πŸ˜œπŸ§πŸ€”πŸ€ͺπŸ₯³πŸ˜΅β€πŸ’«πŸ˜‚"); - ICEBERG_ASSIGN_OR_THROW( + ICEBERG_UNWRAP_OR_FAIL( auto bound_not_starts_long, unbound_not_starts_long->Bind(*string_schema_, /*case_sensitive=*/true)); auto bound_pred_not_starts_long = std::dynamic_pointer_cast(bound_not_starts_long); ASSERT_NE(bound_pred_not_starts_long, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected_not_starts_long, - transform->Project("part", bound_pred_not_starts_long)); + ICEBERG_UNWRAP_OR_FAIL(auto projected_not_starts_long, + transform->Project("part", bound_pred_not_starts_long)); EXPECT_EQ(projected_not_starts_long, nullptr); } @@ -1533,12 +1532,12 @@ TEST_F(TransformProjectTest, YearProjectEquality) { int32_t date_value = TemporalTestHelper::CreateDate({.year = 2021, .month = 6, .day = 1}); auto unbound = Expressions::Equal("value", Literal::Date(date_value)); - ICEBERG_ASSIGN_OR_THROW(auto bound, - unbound->Bind(*date_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*date_schema_, /*case_sensitive=*/true)); auto bound_pred = std::dynamic_pointer_cast(bound); ASSERT_NE(bound_pred, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected, transform->Project("part", bound_pred)); + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); ASSERT_NE(projected, nullptr); EXPECT_EQ(projected->op(), Expression::Operation::kEq); } @@ -1551,23 +1550,23 @@ TEST_F(TransformProjectTest, YearProjectComparison) { // LT projects to LTE auto unbound_lt = Expressions::LessThan("value", Literal::Date(date_value)); - ICEBERG_ASSIGN_OR_THROW(auto bound_lt, - unbound_lt->Bind(*date_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound_lt, + unbound_lt->Bind(*date_schema_, /*case_sensitive=*/true)); auto bound_pred_lt = std::dynamic_pointer_cast(bound_lt); ASSERT_NE(bound_pred_lt, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected_lt, transform->Project("part", bound_pred_lt)); + ICEBERG_UNWRAP_OR_FAIL(auto projected_lt, transform->Project("part", bound_pred_lt)); ASSERT_NE(projected_lt, nullptr); EXPECT_EQ(projected_lt->op(), Expression::Operation::kLtEq); // GT projects to GTE auto unbound_gt = Expressions::GreaterThan("value", Literal::Date(date_value)); - ICEBERG_ASSIGN_OR_THROW(auto bound_gt, - unbound_gt->Bind(*date_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound_gt, + unbound_gt->Bind(*date_schema_, /*case_sensitive=*/true)); auto bound_pred_gt = std::dynamic_pointer_cast(bound_gt); ASSERT_NE(bound_pred_gt, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected_gt, transform->Project("part", bound_pred_gt)); + ICEBERG_UNWRAP_OR_FAIL(auto projected_gt, transform->Project("part", bound_pred_gt)); ASSERT_NE(projected_gt, nullptr); EXPECT_EQ(projected_gt->op(), Expression::Operation::kGtEq); } @@ -1578,12 +1577,12 @@ TEST_F(TransformProjectTest, MonthProjectEquality) { int64_t ts_value = TemporalTestHelper::CreateTimestamp({.year = 2021, .month = 6, .day = 1}); auto unbound = Expressions::Equal("value", Literal::Timestamp(ts_value)); - ICEBERG_ASSIGN_OR_THROW(auto bound, - unbound->Bind(*timestamp_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*timestamp_schema_, /*case_sensitive=*/true)); auto bound_pred = std::dynamic_pointer_cast(bound); ASSERT_NE(bound_pred, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected, transform->Project("part", bound_pred)); + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); ASSERT_NE(projected, nullptr); EXPECT_EQ(projected->op(), Expression::Operation::kEq); } @@ -1594,12 +1593,12 @@ TEST_F(TransformProjectTest, DayProjectEquality) { int32_t date_value = TemporalTestHelper::CreateDate({.year = 2021, .month = 6, .day = 15}); auto unbound = Expressions::Equal("value", Literal::Date(date_value)); - ICEBERG_ASSIGN_OR_THROW(auto bound, - unbound->Bind(*date_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*date_schema_, /*case_sensitive=*/true)); auto bound_pred = std::dynamic_pointer_cast(bound); ASSERT_NE(bound_pred, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected, transform->Project("part", bound_pred)); + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); ASSERT_NE(projected, nullptr); EXPECT_EQ(projected->op(), Expression::Operation::kEq); } @@ -1610,12 +1609,12 @@ TEST_F(TransformProjectTest, HourProjectEquality) { int64_t ts_value = TemporalTestHelper::CreateTimestamp( {.year = 2021, .month = 6, .day = 1, .hour = 14, .minute = 30}); auto unbound = Expressions::Equal("value", Literal::Timestamp(ts_value)); - ICEBERG_ASSIGN_OR_THROW(auto bound, - unbound->Bind(*timestamp_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*timestamp_schema_, /*case_sensitive=*/true)); auto bound_pred = std::dynamic_pointer_cast(bound); ASSERT_NE(bound_pred, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected, transform->Project("part", bound_pred)); + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); ASSERT_NE(projected, nullptr); EXPECT_EQ(projected->op(), Expression::Operation::kEq); } @@ -1624,13 +1623,13 @@ TEST_F(TransformProjectTest, VoidProjectReturnsNull) { auto transform = Transform::Void(); auto unbound = Expressions::Equal("value", Literal::Int(100)); - ICEBERG_ASSIGN_OR_THROW(auto bound, - unbound->Bind(*int_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*int_schema_, /*case_sensitive=*/true)); auto bound_pred = std::dynamic_pointer_cast(bound); ASSERT_NE(bound_pred, nullptr); // Void transform always returns null (no projection possible) - ICEBERG_ASSIGN_OR_THROW(auto projected, transform->Project("part", bound_pred)); + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); EXPECT_EQ(projected, nullptr); } @@ -1643,12 +1642,12 @@ TEST_F(TransformProjectTest, TemporalProjectInSet) { auto unbound_in = Expressions::In( "value", {Literal::Date(date1), Literal::Date(date2), Literal::Date(date3)}); - ICEBERG_ASSIGN_OR_THROW(auto bound_in, - unbound_in->Bind(*date_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound_in, + unbound_in->Bind(*date_schema_, /*case_sensitive=*/true)); auto bound_pred_in = std::dynamic_pointer_cast(bound_in); ASSERT_NE(bound_pred_in, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected_in, transform->Project("part", bound_pred_in)); + ICEBERG_UNWRAP_OR_FAIL(auto projected_in, transform->Project("part", bound_pred_in)); ASSERT_NE(projected_in, nullptr); EXPECT_EQ(projected_in->op(), Expression::Operation::kIn); } @@ -1663,12 +1662,12 @@ TEST_F(TransformProjectTest, DayTimestampProjectionFix) { // If we fix (for buggy writers), we project to day <= 0. auto unbound = Expressions::LessThan("value", Literal::Timestamp(0)); - ICEBERG_ASSIGN_OR_THROW(auto bound, - unbound->Bind(*timestamp_schema_, /*case_sensitive=*/true)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*timestamp_schema_, /*case_sensitive=*/true)); auto bound_pred = std::dynamic_pointer_cast(bound); ASSERT_NE(bound_pred, nullptr); - ICEBERG_ASSIGN_OR_THROW(auto projected, transform->Project("part", bound_pred)); + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); ASSERT_NE(projected, nullptr); auto unbound_projected = @@ -1680,4 +1679,560 @@ TEST_F(TransformProjectTest, DayTimestampProjectionFix) { EXPECT_EQ(val, 0) << "Expected projected value to be 0 (fix applied), but got " << val; } +// Test fixture for Transform::ProjectStrict tests +class TransformProjectStrictTest : public ::testing::Test { + protected: + void SetUp() override { + // Create test schemas for different source types + int_schema_ = std::make_shared( + std::vector{SchemaField::MakeRequired(1, "value", int32())}, + /*schema_id=*/0); + long_schema_ = std::make_shared( + std::vector{SchemaField::MakeRequired(1, "value", int64())}, + /*schema_id=*/0); + string_schema_ = std::make_shared( + std::vector{SchemaField::MakeRequired(1, "value", string())}, + /*schema_id=*/0); + date_schema_ = std::make_shared( + std::vector{SchemaField::MakeRequired(1, "value", date())}, + /*schema_id=*/0); + timestamp_schema_ = std::make_shared( + std::vector{SchemaField::MakeRequired(1, "value", timestamp())}, + /*schema_id=*/0); + decimal_schema_ = std::make_shared( + std::vector{SchemaField::MakeRequired(1, "value", decimal(9, 2))}, + /*schema_id=*/0); + } + + std::shared_ptr int_schema_; + std::shared_ptr long_schema_; + std::shared_ptr string_schema_; + std::shared_ptr date_schema_; + std::shared_ptr timestamp_schema_; + std::shared_ptr decimal_schema_; +}; + +TEST_F(TransformProjectStrictTest, IdentityStrictProjection) { + auto transform = Transform::Identity(); + + // Identity strict projection should behave the same as inclusive projection + auto unbound = Expressions::Equal("value", Literal::Int(100)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*int_schema_, /*case_sensitive=*/true)); + auto bound_pred = std::dynamic_pointer_cast(bound); + ASSERT_NE(bound_pred, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); + ASSERT_NE(projected, nullptr); + EXPECT_EQ(projected->op(), Expression::Operation::kEq); + + auto unbound_projected = + internal::checked_pointer_cast>( + std::move(projected)); + EXPECT_EQ(unbound_projected->op(), Expression::Operation::kEq); + EXPECT_EQ(unbound_projected->literals().size(), 1); + EXPECT_EQ(std::get(unbound_projected->literals().front().value()), 100); +} + +TEST_F(TransformProjectStrictTest, BucketStrictEqualityReturnsFalse) { + auto transform = Transform::Bucket(10); + + // Bucket strict projection: equality should return FALSE (cannot guarantee equality) + auto unbound = Expressions::Equal("value", Literal::Int(100)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*int_schema_, /*case_sensitive=*/true)); + auto bound_pred = std::dynamic_pointer_cast(bound); + ASSERT_NE(bound_pred, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); + EXPECT_EQ(projected, nullptr); +} + +TEST_F(TransformProjectStrictTest, BucketStrictNotEqual) { + auto transform = Transform::Bucket(10); + + // Bucket strict projection: notEqual can be projected + auto unbound = Expressions::NotEqual("value", Literal::Int(100)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*int_schema_, /*case_sensitive=*/true)); + auto bound_pred = std::dynamic_pointer_cast(bound); + ASSERT_NE(bound_pred, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); + ASSERT_NE(projected, nullptr); + EXPECT_EQ(projected->op(), Expression::Operation::kNotEq); + + auto unbound_projected = + internal::checked_pointer_cast>( + std::move(projected)); + EXPECT_EQ(unbound_projected->op(), Expression::Operation::kNotEq); + EXPECT_EQ(unbound_projected->literals().size(), 1); + // bucket(100, 10) = 6 + EXPECT_EQ(std::get(unbound_projected->literals().front().value()), 6); +} + +TEST_F(TransformProjectStrictTest, BucketStrictComparisonReturnsNull) { + auto transform = Transform::Bucket(10); + + // Bucket strict projection: comparison predicates return null + auto unbound_lt = Expressions::LessThan("value", Literal::Int(100)); + ICEBERG_UNWRAP_OR_FAIL(auto bound_lt, + unbound_lt->Bind(*int_schema_, /*case_sensitive=*/true)); + auto bound_pred_lt = std::dynamic_pointer_cast(bound_lt); + ASSERT_NE(bound_pred_lt, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected_lt, + transform->ProjectStrict("part", bound_pred_lt)); + EXPECT_EQ(projected_lt, nullptr); +} + +TEST_F(TransformProjectStrictTest, BucketStrictNotIn) { + auto transform = Transform::Bucket(10); + + // Bucket strict projection: NOT_IN can be projected + auto unbound_not_in = Expressions::NotIn( + "value", {Literal::Int(99), Literal::Int(100), Literal::Int(101)}); + ICEBERG_UNWRAP_OR_FAIL(auto bound_not_in, + unbound_not_in->Bind(*int_schema_, /*case_sensitive=*/true)); + auto bound_pred_not_in = std::dynamic_pointer_cast(bound_not_in); + ASSERT_NE(bound_pred_not_in, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected_not_in, + transform->ProjectStrict("part", bound_pred_not_in)); + ASSERT_NE(projected_not_in, nullptr); + EXPECT_EQ(projected_not_in->op(), Expression::Operation::kNotIn); +} + +TEST_F(TransformProjectStrictTest, BucketStrictInReturnsNull) { + auto transform = Transform::Bucket(10); + + // Bucket strict projection: IN returns null (cannot guarantee) + auto unbound_in = + Expressions::In("value", {Literal::Int(99), Literal::Int(100), Literal::Int(101)}); + ICEBERG_UNWRAP_OR_FAIL(auto bound_in, + unbound_in->Bind(*int_schema_, /*case_sensitive=*/true)); + auto bound_pred_in = std::dynamic_pointer_cast(bound_in); + ASSERT_NE(bound_pred_in, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected_in, + transform->ProjectStrict("part", bound_pred_in)); + EXPECT_EQ(projected_in, nullptr); +} + +TEST_F(TransformProjectStrictTest, BucketStrictString) { + auto transform = Transform::Bucket(10); + + // Bucket strict projection for string + auto unbound_not_eq = Expressions::NotEqual("value", Literal::String("abcdefg")); + ICEBERG_UNWRAP_OR_FAIL(auto bound_not_eq, + unbound_not_eq->Bind(*string_schema_, /*case_sensitive=*/true)); + auto bound_pred_not_eq = std::dynamic_pointer_cast(bound_not_eq); + ASSERT_NE(bound_pred_not_eq, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected_not_eq, + transform->ProjectStrict("part", bound_pred_not_eq)); + ASSERT_NE(projected_not_eq, nullptr); + EXPECT_EQ(projected_not_eq->op(), Expression::Operation::kNotEq); +} + +TEST_F(TransformProjectStrictTest, TruncateStrictIntEqualityReturnsNull) { + auto transform = Transform::Truncate(10); + + // Truncate strict projection: equality returns null (cannot guarantee) + auto unbound = Expressions::Equal("value", Literal::Int(123)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*int_schema_, /*case_sensitive=*/true)); + auto bound_pred = std::dynamic_pointer_cast(bound); + ASSERT_NE(bound_pred, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); + EXPECT_EQ(projected, nullptr); +} + +TEST_F(TransformProjectStrictTest, TruncateStrictIntLessThan) { + auto transform = Transform::Truncate(10); + + // Truncate strict projection: LT projects to LT + auto unbound = Expressions::LessThan("value", Literal::Int(100)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*int_schema_, /*case_sensitive=*/true)); + auto bound_pred = std::dynamic_pointer_cast(bound); + ASSERT_NE(bound_pred, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); + ASSERT_NE(projected, nullptr); + EXPECT_EQ(projected->op(), Expression::Operation::kLt); + + auto unbound_projected = + internal::checked_pointer_cast>( + std::move(projected)); + EXPECT_EQ(unbound_projected->op(), Expression::Operation::kLt); + EXPECT_EQ(unbound_projected->literals().size(), 1); + EXPECT_EQ(std::get(unbound_projected->literals().front().value()), 100); +} + +TEST_F(TransformProjectStrictTest, TruncateStrictIntLessThanOrEqual) { + auto transform = Transform::Truncate(10); + + // Truncate strict projection: LTE projects to LT + auto unbound = Expressions::LessThanOrEqual("value", Literal::Int(100)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*int_schema_, /*case_sensitive=*/true)); + auto bound_pred = std::dynamic_pointer_cast(bound); + ASSERT_NE(bound_pred, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); + ASSERT_NE(projected, nullptr); + EXPECT_EQ(projected->op(), Expression::Operation::kLt); + + auto unbound_projected = + internal::checked_pointer_cast>( + std::move(projected)); + EXPECT_EQ(unbound_projected->op(), Expression::Operation::kLt); + EXPECT_EQ(unbound_projected->literals().size(), 1); + EXPECT_EQ(std::get(unbound_projected->literals().front().value()), 100); +} + +TEST_F(TransformProjectStrictTest, TruncateStrictIntGreaterThan) { + auto transform = Transform::Truncate(10); + + // Truncate strict projection: GT projects to GT + auto unbound = Expressions::GreaterThan("value", Literal::Int(100)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*int_schema_, /*case_sensitive=*/true)); + auto bound_pred = std::dynamic_pointer_cast(bound); + ASSERT_NE(bound_pred, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); + ASSERT_NE(projected, nullptr); + EXPECT_EQ(projected->op(), Expression::Operation::kGt); + + auto unbound_projected = + internal::checked_pointer_cast>( + std::move(projected)); + EXPECT_EQ(unbound_projected->op(), Expression::Operation::kGt); + EXPECT_EQ(unbound_projected->literals().size(), 1); + EXPECT_EQ(std::get(unbound_projected->literals().front().value()), 100); +} + +TEST_F(TransformProjectStrictTest, TruncateStrictIntGreaterThanOrEqualLowerBound) { + auto transform = Transform::Truncate(10); + + // Truncate strict projection: GTE projects to GT (lower bound, value = 100) + auto unbound = Expressions::GreaterThanOrEqual("value", Literal::Int(100)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*int_schema_, /*case_sensitive=*/true)); + auto bound_pred = std::dynamic_pointer_cast(bound); + ASSERT_NE(bound_pred, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); + ASSERT_NE(projected, nullptr); + EXPECT_EQ(projected->op(), Expression::Operation::kGt); + + auto unbound_projected = + internal::checked_pointer_cast>( + std::move(projected)); + EXPECT_EQ(unbound_projected->op(), Expression::Operation::kGt); + EXPECT_EQ(unbound_projected->literals().size(), 1); + // For GTE with value 100 and width 10, truncate(100) = 100, so GT should be 90 + EXPECT_EQ(std::get(unbound_projected->literals().front().value()), 90); +} + +TEST_F(TransformProjectStrictTest, TruncateStrictIntGreaterThanOrEqualUpperBound) { + auto transform = Transform::Truncate(10); + + // Truncate strict projection: GTE projects to GT (upper bound, value = 99) + auto unbound = Expressions::GreaterThanOrEqual("value", Literal::Int(99)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*int_schema_, /*case_sensitive=*/true)); + auto bound_pred = std::dynamic_pointer_cast(bound); + ASSERT_NE(bound_pred, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); + ASSERT_NE(projected, nullptr); + EXPECT_EQ(projected->op(), Expression::Operation::kGt); + + auto unbound_projected = + internal::checked_pointer_cast>( + std::move(projected)); + EXPECT_EQ(unbound_projected->op(), Expression::Operation::kGt); + EXPECT_EQ(unbound_projected->literals().size(), 1); + // For GTE with value 99 and width 10, truncate(99) = 90, so GT should be 90 + EXPECT_EQ(std::get(unbound_projected->literals().front().value()), 90); +} + +TEST_F(TransformProjectStrictTest, TruncateStrictIntNotEqual) { + auto transform = Transform::Truncate(10); + + // Truncate strict projection: notEqual can be projected + auto unbound = Expressions::NotEqual("value", Literal::Int(100)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*int_schema_, /*case_sensitive=*/true)); + auto bound_pred = std::dynamic_pointer_cast(bound); + ASSERT_NE(bound_pred, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); + ASSERT_NE(projected, nullptr); + EXPECT_EQ(projected->op(), Expression::Operation::kNotEq); + + auto unbound_projected = + internal::checked_pointer_cast>( + std::move(projected)); + EXPECT_EQ(unbound_projected->op(), Expression::Operation::kNotEq); + EXPECT_EQ(unbound_projected->literals().size(), 1); + EXPECT_EQ(std::get(unbound_projected->literals().front().value()), 100); +} + +TEST_F(TransformProjectStrictTest, TruncateStrictIntNotIn) { + auto transform = Transform::Truncate(10); + + // Truncate strict projection: NOT_IN can be projected + auto unbound_not_in = Expressions::NotIn( + "value", {Literal::Int(99), Literal::Int(100), Literal::Int(101)}); + ICEBERG_UNWRAP_OR_FAIL(auto bound_not_in, + unbound_not_in->Bind(*int_schema_, /*case_sensitive=*/true)); + auto bound_pred_not_in = std::dynamic_pointer_cast(bound_not_in); + ASSERT_NE(bound_pred_not_in, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected_not_in, + transform->ProjectStrict("part", bound_pred_not_in)); + ASSERT_NE(projected_not_in, nullptr); + EXPECT_EQ(projected_not_in->op(), Expression::Operation::kNotIn); +} + +TEST_F(TransformProjectStrictTest, TruncateStrictString) { + auto transform = Transform::Truncate(5); + + // Truncate strict projection for string + auto unbound_lt = Expressions::LessThan("value", Literal::String("abcdefg")); + ICEBERG_UNWRAP_OR_FAIL(auto bound_lt, + unbound_lt->Bind(*string_schema_, /*case_sensitive=*/true)); + auto bound_pred_lt = std::dynamic_pointer_cast(bound_lt); + ASSERT_NE(bound_pred_lt, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected_lt, + transform->ProjectStrict("part", bound_pred_lt)); + ASSERT_NE(projected_lt, nullptr); + EXPECT_EQ(projected_lt->op(), Expression::Operation::kLt); + + auto unbound_projected_lt = + internal::checked_pointer_cast>( + std::move(projected_lt)); + EXPECT_EQ(unbound_projected_lt->op(), Expression::Operation::kLt); + EXPECT_EQ(unbound_projected_lt->literals().size(), 1); + EXPECT_EQ(std::get(unbound_projected_lt->literals().front().value()), + "abcde"); +} + +TEST_F(TransformProjectStrictTest, YearStrictEqualityReturnsNull) { + auto transform = Transform::Year(); + + // Year strict projection: equality returns null (cannot guarantee) + int32_t date_value = + TemporalTestHelper::CreateDate({.year = 2021, .month = 6, .day = 1}); + auto unbound = Expressions::Equal("value", Literal::Date(date_value)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*date_schema_, /*case_sensitive=*/true)); + auto bound_pred = std::dynamic_pointer_cast(bound); + ASSERT_NE(bound_pred, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); + EXPECT_EQ(projected, nullptr); +} + +TEST_F(TransformProjectStrictTest, YearStrictLessThan) { + auto transform = Transform::Year(); + + // Year strict projection: LT projects to LT + int32_t date_value = + TemporalTestHelper::CreateDate({.year = 2021, .month = 1, .day = 1}); + auto unbound = Expressions::LessThan("value", Literal::Date(date_value)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*date_schema_, /*case_sensitive=*/true)); + auto bound_pred = std::dynamic_pointer_cast(bound); + ASSERT_NE(bound_pred, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); + ASSERT_NE(projected, nullptr); + EXPECT_EQ(projected->op(), Expression::Operation::kLt); + + auto unbound_projected = + internal::checked_pointer_cast>( + std::move(projected)); + EXPECT_EQ(unbound_projected->op(), Expression::Operation::kLt); + EXPECT_EQ(unbound_projected->literals().size(), 1); + EXPECT_EQ(std::get(unbound_projected->literals().front().value()), 2021); +} + +TEST_F(TransformProjectStrictTest, YearStrictGreaterThanOrEqual) { + auto transform = Transform::Year(); + + // Year strict projection: GTE projects to GT (lower bound) + int32_t date_value = + TemporalTestHelper::CreateDate({.year = 2021, .month = 1, .day = 1}); + auto unbound = Expressions::GreaterThanOrEqual("value", Literal::Date(date_value)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*date_schema_, /*case_sensitive=*/true)); + auto bound_pred = std::dynamic_pointer_cast(bound); + ASSERT_NE(bound_pred, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); + ASSERT_NE(projected, nullptr); + EXPECT_EQ(projected->op(), Expression::Operation::kGt); + + auto unbound_projected = + internal::checked_pointer_cast>( + std::move(projected)); + EXPECT_EQ(unbound_projected->op(), Expression::Operation::kGt); + EXPECT_EQ(unbound_projected->literals().size(), 1); + EXPECT_EQ(std::get(unbound_projected->literals().front().value()), 2020); +} + +TEST_F(TransformProjectStrictTest, YearStrictNotEqual) { + auto transform = Transform::Year(); + + // Year strict projection: notEqual can be projected + int32_t date_value = + TemporalTestHelper::CreateDate({.year = 2021, .month = 1, .day = 1}); + auto unbound = Expressions::NotEqual("value", Literal::Date(date_value)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*date_schema_, /*case_sensitive=*/true)); + auto bound_pred = std::dynamic_pointer_cast(bound); + ASSERT_NE(bound_pred, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); + ASSERT_NE(projected, nullptr); + EXPECT_EQ(projected->op(), Expression::Operation::kNotEq); + + auto unbound_projected = + internal::checked_pointer_cast>( + std::move(projected)); + EXPECT_EQ(unbound_projected->op(), Expression::Operation::kNotEq); + EXPECT_EQ(unbound_projected->literals().size(), 1); + EXPECT_EQ(std::get(unbound_projected->literals().front().value()), 2021); +} + +TEST_F(TransformProjectStrictTest, MonthStrictLessThan) { + auto transform = Transform::Month(); + + // Month strict projection: LT projects to LT + int64_t ts_value = + TemporalTestHelper::CreateTimestamp({.year = 2017, .month = 12, .day = 1}); + auto unbound = Expressions::LessThan("value", Literal::Timestamp(ts_value)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*timestamp_schema_, /*case_sensitive=*/true)); + auto bound_pred = std::dynamic_pointer_cast(bound); + ASSERT_NE(bound_pred, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); + ASSERT_NE(projected, nullptr); + EXPECT_EQ(projected->op(), Expression::Operation::kLt); +} + +TEST_F(TransformProjectStrictTest, DayStrictLessThan) { + auto transform = Transform::Day(); + + // Day strict projection: LT projects to LT + int64_t ts_value = + TemporalTestHelper::CreateTimestamp({.year = 2017, .month = 12, .day = 1}); + auto unbound = Expressions::LessThan("value", Literal::Timestamp(ts_value)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*timestamp_schema_, /*case_sensitive=*/true)); + auto bound_pred = std::dynamic_pointer_cast(bound); + ASSERT_NE(bound_pred, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); + ASSERT_NE(projected, nullptr); + EXPECT_EQ(projected->op(), Expression::Operation::kLt); +} + +TEST_F(TransformProjectStrictTest, HourStrictLessThan) { + auto transform = Transform::Hour(); + + // Hour strict projection: LT projects to LT + int64_t ts_value = TemporalTestHelper::CreateTimestamp( + {.year = 2017, .month = 12, .day = 1, .hour = 10, .minute = 0}); + auto unbound = Expressions::LessThan("value", Literal::Timestamp(ts_value)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*timestamp_schema_, /*case_sensitive=*/true)); + auto bound_pred = std::dynamic_pointer_cast(bound); + ASSERT_NE(bound_pred, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); + ASSERT_NE(projected, nullptr); + EXPECT_EQ(projected->op(), Expression::Operation::kLt); +} + +TEST_F(TransformProjectStrictTest, DayStrictEpoch) { + auto transform = Transform::Day(); + + // Day strict projection at epoch: LT projects to LT + auto unbound = Expressions::LessThan("value", Literal::Timestamp(0)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*timestamp_schema_, /*case_sensitive=*/true)); + auto bound_pred = std::dynamic_pointer_cast(bound); + ASSERT_NE(bound_pred, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); + ASSERT_NE(projected, nullptr); + EXPECT_EQ(projected->op(), Expression::Operation::kLt); +} + +TEST_F(TransformProjectStrictTest, MonthStrictNotEqualNegative) { + auto transform = Transform::Month(); + + // Month strict projection: notEqual with negative dates may convert to NOT_IN + int64_t ts_value = + TemporalTestHelper::CreateTimestamp({.year = 1969, .month = 1, .day = 1}); + auto unbound = Expressions::NotEqual("value", Literal::Timestamp(ts_value)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*timestamp_schema_, /*case_sensitive=*/true)); + auto bound_pred = std::dynamic_pointer_cast(bound); + ASSERT_NE(bound_pred, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); + ASSERT_NE(projected, nullptr); + // For negative dates, NOT_EQ may convert to NOT_IN + EXPECT_TRUE(projected->op() == Expression::Operation::kNotEq || + projected->op() == Expression::Operation::kNotIn); +} + +TEST_F(TransformProjectStrictTest, YearStrictUpperBound) { + auto transform = Transform::Year(); + + // Year strict projection: upper bound (end of year) + int32_t date_value = + TemporalTestHelper::CreateDate({.year = 2017, .month = 12, .day = 31}); + auto unbound = Expressions::LessThanOrEqual("value", Literal::Date(date_value)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*date_schema_, /*case_sensitive=*/true)); + auto bound_pred = std::dynamic_pointer_cast(bound); + ASSERT_NE(bound_pred, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); + ASSERT_NE(projected, nullptr); + EXPECT_EQ(projected->op(), Expression::Operation::kLt); + + auto unbound_projected = + internal::checked_pointer_cast>( + std::move(projected)); + EXPECT_EQ(unbound_projected->op(), Expression::Operation::kLt); + EXPECT_EQ(unbound_projected->literals().size(), 1); + EXPECT_EQ(std::get(unbound_projected->literals().front().value()), 2018); +} + +TEST_F(TransformProjectStrictTest, VoidStrictReturnsNull) { + auto transform = Transform::Void(); + + // Void transform always returns null for strict projection + auto unbound = Expressions::Equal("value", Literal::Int(100)); + ICEBERG_UNWRAP_OR_FAIL(auto bound, + unbound->Bind(*int_schema_, /*case_sensitive=*/true)); + auto bound_pred = std::dynamic_pointer_cast(bound); + ASSERT_NE(bound_pred, nullptr); + + ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); + EXPECT_EQ(projected, nullptr); +} + } // namespace iceberg diff --git a/src/iceberg/transform.cc b/src/iceberg/transform.cc index f8d2f0655..614489710 100644 --- a/src/iceberg/transform.cc +++ b/src/iceberg/transform.cc @@ -306,6 +306,66 @@ Result> Transform::Project( std::unreachable(); } +Result> Transform::ProjectStrict( + std::string_view name, const std::shared_ptr& predicate) { + switch (transform_type_) { + case TransformType::kIdentity: + return ProjectionUtil::IdentityProject(name, predicate); + case TransformType::kBucket: { + // If the predicate has a transformed child that matches the given transform, return + // a predicate. + if (predicate->term()->kind() == Term::Kind::kTransform) { + const auto boundTransform = + internal::checked_pointer_cast(predicate->term()); + if (*this == *boundTransform->transform()) { + return ProjectionUtil::RemoveTransform(name, predicate); + } else { + return nullptr; + } + } + ICEBERG_ASSIGN_OR_RAISE(auto func, Bind(predicate->term()->type())); + return ProjectionUtil::BucketProjectStrict(name, predicate, func); + } + case TransformType::kTruncate: { + // If the predicate has a transformed child that matches the given transform, return + // a predicate. + if (predicate->term()->kind() == Term::Kind::kTransform) { + const auto boundTransform = + internal::checked_pointer_cast(predicate->term()); + if (*this == *boundTransform->transform()) { + return ProjectionUtil::RemoveTransform(name, predicate); + } else { + return nullptr; + } + } + ICEBERG_ASSIGN_OR_RAISE(auto func, Bind(predicate->term()->type())); + return ProjectionUtil::TruncateProjectStrict(name, predicate, func); + } + case TransformType::kYear: + case TransformType::kMonth: + case TransformType::kDay: + case TransformType::kHour: { + // If the predicate has a transformed child that matches the given transform, return + // a predicate. + if (predicate->term()->kind() == Term::Kind::kTransform) { + const auto boundTransform = + internal::checked_pointer_cast(predicate->term()); + if (*this == *boundTransform->transform()) { + return ProjectionUtil::RemoveTransform(name, predicate); + } else { + return nullptr; + } + } + ICEBERG_ASSIGN_OR_RAISE(auto func, Bind(predicate->term()->type())); + return ProjectionUtil::TemporalProjectStrict(name, predicate, func); + } + case TransformType::kUnknown: + case TransformType::kVoid: + return nullptr; + } + std::unreachable(); +} + bool TransformFunction::Equals(const TransformFunction& other) const { return transform_type_ == other.transform_type_ && *source_type_ == *other.source_type_; } diff --git a/src/iceberg/transform.h b/src/iceberg/transform.h index 64b850725..53993b4e3 100644 --- a/src/iceberg/transform.h +++ b/src/iceberg/transform.h @@ -182,6 +182,18 @@ class ICEBERG_EXPORT Transform : public util::Formattable { Result> Project( std::string_view name, const std::shared_ptr& predicate); + /// \brief Transforms a BoundPredicate to a strict predicate on the partition values + /// produced by the transform. + /// + /// This strict transform guarantees that if Projected(transform(value)) is true, then + /// predicate->Test(value) is also true. + /// \param name The name of the partition column. + /// \param predicate The predicate to project. + /// \return A Result containing either a unique pointer to the projected predicate, + /// nullptr if the projection cannot be performed, or an Error if the projection fails. + Result> ProjectStrict( + std::string_view name, const std::shared_ptr& predicate); + /// \brief Returns a string representation of this transform (e.g., "bucket[16]"). std::string ToString() const override; diff --git a/src/iceberg/util/projection_util_internal.h b/src/iceberg/util/projection_util_internal.h index 3ce2dbf8f..df4fe9789 100644 --- a/src/iceberg/util/projection_util_internal.h +++ b/src/iceberg/util/projection_util_internal.h @@ -24,10 +24,12 @@ #include #include #include +#include #include #include "iceberg/expression/literal.h" #include "iceberg/expression/predicate.h" +#include "iceberg/expression/term.h" #include "iceberg/result.h" #include "iceberg/transform.h" #include "iceberg/transform_function.h" @@ -40,248 +42,230 @@ namespace iceberg { class ProjectionUtil { private: + static Result AdjustLiteral(const Literal& literal, int adjustment) { + switch (literal.type()->type_id()) { + case TypeId::kInt: + return Literal::Int(std::get(literal.value()) + adjustment); + case TypeId::kLong: + return Literal::Long(std::get(literal.value()) + adjustment); + case TypeId::kDate: + return Literal::Date(std::get(literal.value()) + adjustment); + case TypeId::kTimestamp: + return Literal::Timestamp(std::get(literal.value()) + adjustment); + case TypeId::kTimestampTz: + return Literal::TimestampTz(std::get(literal.value()) + adjustment); + case TypeId::kDecimal: { + const auto& decimal_type = + internal::checked_cast(*literal.type()); + Decimal adjusted = std::get(literal.value()) + Decimal(adjustment); + return Literal::Decimal(adjusted.value(), decimal_type.precision(), + decimal_type.scale()); + } + default: + return NotSupported("{} is not a valid literal type for value adjustment", + literal.type()->ToString()); + } + } + + static Result PlusOne(const Literal& literal) { + return AdjustLiteral(literal, /*adjustment=*/+1); + } + + static Result MinusOne(const Literal& literal) { + return AdjustLiteral(literal, /*adjustment=*/-1); + } + + static Result> MakePredicate( + Expression::Operation op, std::string_view name, + const std::shared_ptr& func, const Literal& literal) { + ICEBERG_ASSIGN_OR_RAISE(auto ref, NamedReference::Make(std::string(name))); + ICEBERG_ASSIGN_OR_RAISE(auto lit, func->Transform(literal)); + return UnboundPredicateImpl::Make(op, std::move(ref), std::move(lit)); + } + static Result> TransformSet( - std::string_view name, const std::shared_ptr& predicate, + std::string_view name, const std::shared_ptr& pred, const std::shared_ptr& func) { std::vector transformed; - transformed.reserve(predicate->literal_set().size()); - for (const auto& lit : predicate->literal_set()) { + transformed.reserve(pred->literal_set().size()); + for (const auto& lit : pred->literal_set()) { ICEBERG_ASSIGN_OR_RAISE(auto transformed_lit, func->Transform(lit)); transformed.push_back(std::move(transformed_lit)); } ICEBERG_ASSIGN_OR_RAISE(auto ref, NamedReference::Make(std::string(name))); - return UnboundPredicateImpl::Make(predicate->op(), std::move(ref), + return UnboundPredicateImpl::Make(pred->op(), std::move(ref), std::move(transformed)); } - // General transform for all literal predicates. This is used as a fallback for special - // cases that are not handled by the other transform functions. - static Result> GenericTransform( - std::unique_ptr ref, - const std::shared_ptr& predicate, + static Result> TruncateByteArray( + std::string_view name, const std::shared_ptr& pred, const std::shared_ptr& func) { - ICEBERG_ASSIGN_OR_RAISE(auto transformed, func->Transform(predicate->literal())); - switch (predicate->op()) { + switch (pred->op()) { case Expression::Operation::kLt: - case Expression::Operation::kLtEq: { - return UnboundPredicateImpl::Make( - Expression::Operation::kLtEq, std::move(ref), std::move(transformed)); - } + case Expression::Operation::kLtEq: + return MakePredicate(Expression::Operation::kLtEq, name, func, pred->literal()); case Expression::Operation::kGt: - case Expression::Operation::kGtEq: { - return UnboundPredicateImpl::Make( - Expression::Operation::kGtEq, std::move(ref), std::move(transformed)); - } - case Expression::Operation::kEq: { - return UnboundPredicateImpl::Make( - Expression::Operation::kEq, std::move(ref), std::move(transformed)); - } + case Expression::Operation::kGtEq: + return MakePredicate(Expression::Operation::kGtEq, name, func, pred->literal()); + case Expression::Operation::kEq: + case Expression::Operation::kStartsWith: + return MakePredicate(pred->op(), name, func, pred->literal()); default: return nullptr; } } - static Result> TruncateByteArray( - std::string_view name, const std::shared_ptr& predicate, + static Result> TruncateByteArrayStrict( + std::string_view name, const std::shared_ptr& pred, const std::shared_ptr& func) { - ICEBERG_ASSIGN_OR_RAISE(auto ref, NamedReference::Make(std::string(name))); - switch (predicate->op()) { - case Expression::Operation::kStartsWith: { - ICEBERG_ASSIGN_OR_RAISE(auto transformed, func->Transform(predicate->literal())); - return UnboundPredicateImpl::Make( - Expression::Operation::kStartsWith, std::move(ref), std::move(transformed)); - } + switch (pred->op()) { + case Expression::Operation::kLt: + case Expression::Operation::kLtEq: + return MakePredicate(Expression::Operation::kLt, name, func, pred->literal()); + case Expression::Operation::kGt: + case Expression::Operation::kGtEq: + return MakePredicate(Expression::Operation::kGt, name, func, pred->literal()); + case Expression::Operation::kNotEq: + return MakePredicate(Expression::Operation::kNotEq, name, func, pred->literal()); default: - return GenericTransform(std::move(ref), predicate, func); + return nullptr; } } - template - requires std::is_same_v || std::is_same_v - static Result> TruncateInteger( - std::string_view name, const std::shared_ptr& predicate, + // Apply to int32, int64, decimal, and temporal types + static Result> TransformNumeric( + std::string_view name, const std::shared_ptr& pred, const std::shared_ptr& func) { - const Literal& literal = predicate->literal(); - ICEBERG_ASSIGN_OR_RAISE(auto ref, NamedReference::Make(std::string(name))); + switch (func->source_type()->type_id()) { + case TypeId::kInt: + case TypeId::kLong: + case TypeId::kDecimal: + case TypeId::kDate: + case TypeId::kTimestamp: + case TypeId::kTimestampTz: + break; + default: + return NotSupported("{} is not a valid input type for numeric transform", + func->source_type()->ToString()); + } - switch (predicate->op()) { + switch (pred->op()) { case Expression::Operation::kLt: { // adjust closed and then transform ltEq - if constexpr (std::is_same_v) { - ICEBERG_ASSIGN_OR_RAISE( - auto transformed, - func->Transform(Literal::Int(std::get(literal.value()) - 1))); - return UnboundPredicateImpl::Make( - Expression::Operation::kLtEq, std::move(ref), std::move(transformed)); - } else { - ICEBERG_ASSIGN_OR_RAISE( - auto transformed, - func->Transform(Literal::Long(std::get(literal.value()) - 1))); - return UnboundPredicateImpl::Make( - Expression::Operation::kLtEq, std::move(ref), std::move(transformed)); - } + ICEBERG_ASSIGN_OR_RAISE(auto adjusted, MinusOne(pred->literal())); + return MakePredicate(Expression::Operation::kLtEq, name, func, adjusted); } case Expression::Operation::kGt: { // adjust closed and then transform gtEq - if constexpr (std::is_same_v) { - ICEBERG_ASSIGN_OR_RAISE( - auto transformed, - func->Transform(Literal::Int(std::get(literal.value()) + 1))); - return UnboundPredicateImpl::Make( - Expression::Operation::kGtEq, std::move(ref), std::move(transformed)); - } else { - ICEBERG_ASSIGN_OR_RAISE( - auto transformed, - func->Transform(Literal::Long(std::get(literal.value()) + 1))); - return UnboundPredicateImpl::Make( - Expression::Operation::kGtEq, std::move(ref), std::move(transformed)); - } + ICEBERG_ASSIGN_OR_RAISE(auto adjusted, PlusOne(pred->literal())); + return MakePredicate(Expression::Operation::kGtEq, name, func, adjusted); } + case Expression::Operation::kLtEq: + case Expression::Operation::kGtEq: + case Expression::Operation::kEq: + return MakePredicate(pred->op(), name, func, pred->literal()); default: - return GenericTransform(std::move(ref), predicate, func); + return nullptr; } } - static Result> TransformTemporal( - std::string_view name, const std::shared_ptr& predicate, + static Result> TransformNumericStrict( + std::string_view name, const std::shared_ptr& pred, const std::shared_ptr& func) { - const Literal& literal = predicate->literal(); - ICEBERG_ASSIGN_OR_RAISE(auto ref, NamedReference::Make(std::string(name))); - switch (func->source_type()->type_id()) { - case TypeId::kDate: { - switch (predicate->op()) { - case Expression::Operation::kLt: { - ICEBERG_ASSIGN_OR_RAISE( - auto transformed, - func->Transform(Literal::Date(std::get(literal.value()) - 1))); - return UnboundPredicateImpl::Make( - Expression::Operation::kLtEq, std::move(ref), std::move(transformed)); - } - case Expression::Operation::kGt: { - ICEBERG_ASSIGN_OR_RAISE( - auto transformed, - func->Transform(Literal::Date(std::get(literal.value()) + 1))); - return UnboundPredicateImpl::Make( - Expression::Operation::kGtEq, std::move(ref), std::move(transformed)); - } - default: - return GenericTransform(std::move(ref), predicate, func); - } - } - case TypeId::kTimestamp: { - switch (predicate->op()) { - case Expression::Operation::kLt: { - ICEBERG_ASSIGN_OR_RAISE(auto transformed, - func->Transform(Literal::Timestamp( - std::get(literal.value()) - 1))); - return UnboundPredicateImpl::Make( - Expression::Operation::kLtEq, std::move(ref), std::move(transformed)); - } - case Expression::Operation::kGt: { - ICEBERG_ASSIGN_OR_RAISE(auto transformed, - func->Transform(Literal::Timestamp( - std::get(literal.value()) + 1))); - return UnboundPredicateImpl::Make( - Expression::Operation::kGtEq, std::move(ref), std::move(transformed)); - } - default: - return GenericTransform(std::move(ref), predicate, func); - } + case TypeId::kInt: + case TypeId::kLong: + case TypeId::kDecimal: + case TypeId::kDate: + case TypeId::kTimestamp: + case TypeId::kTimestampTz: + break; + default: + return NotSupported("{} is not a valid input type for numeric transform", + func->source_type()->ToString()); + } + + switch (pred->op()) { + case Expression::Operation::kLtEq: { + ICEBERG_ASSIGN_OR_RAISE(auto adjusted, PlusOne(pred->literal())); + return MakePredicate(Expression::Operation::kLt, name, func, adjusted); } - case TypeId::kTimestampTz: { - switch (predicate->op()) { - case Expression::Operation::kLt: { - ICEBERG_ASSIGN_OR_RAISE(auto transformed, - func->Transform(Literal::TimestampTz( - std::get(literal.value()) - 1))); - return UnboundPredicateImpl::Make( - Expression::Operation::kLtEq, std::move(ref), std::move(transformed)); - } - case Expression::Operation::kGt: { - ICEBERG_ASSIGN_OR_RAISE(auto transformed, - func->Transform(Literal::TimestampTz( - std::get(literal.value()) + 1))); - return UnboundPredicateImpl::Make( - Expression::Operation::kGtEq, std::move(ref), std::move(transformed)); - } - default: - return GenericTransform(std::move(ref), predicate, func); - } + case Expression::Operation::kGtEq: { + ICEBERG_ASSIGN_OR_RAISE(auto adjusted, MinusOne(pred->literal())); + return MakePredicate(Expression::Operation::kGt, name, func, adjusted); } + case Expression::Operation::kLt: + case Expression::Operation::kGt: + case Expression::Operation::kNotEq: + return MakePredicate(pred->op(), name, func, pred->literal()); default: - return NotSupported("{} is not a valid input type for temporal transform", - func->source_type()->ToString()); + return nullptr; } } - static Result> TruncateDecimal( - std::string_view name, const std::shared_ptr& predicate, + static Result> TruncateStringLiteral( + std::string_view name, const std::shared_ptr& pred, const std::shared_ptr& func) { - const Literal& boundary = predicate->literal(); - ICEBERG_ASSIGN_OR_RAISE(auto ref, NamedReference::Make(std::string(name))); + const auto op = pred->op(); + if (op != Expression::Operation::kStartsWith && + op != Expression::Operation::kNotStartsWith) { + return TruncateByteArray(name, pred, func); + } - // For boundary adjustments, extract type info once - auto make_adjusted_literal = [&boundary](int adjustment) { - const auto& type = internal::checked_pointer_cast(boundary.type()); - Decimal adjusted = std::get(boundary.value()) + Decimal(adjustment); - return Literal::Decimal(adjusted.value(), type->precision(), type->scale()); - }; + const auto& literal = pred->literal(); + const auto length = + StringUtils::CodePointCount(std::get(literal.value())); + const auto width = static_cast( + internal::checked_pointer_cast(func)->width()); - switch (predicate->op()) { - case Expression::Operation::kLt: { - // adjust closed and then transform ltEq - ICEBERG_ASSIGN_OR_RAISE(auto transformed, - func->Transform(make_adjusted_literal(-1))); - return UnboundPredicateImpl::Make( - Expression::Operation::kLtEq, std::move(ref), std::move(transformed)); - } - case Expression::Operation::kGt: { - // adjust closed and then transform gtEq - ICEBERG_ASSIGN_OR_RAISE(auto transformed, - func->Transform(make_adjusted_literal(1))); - return UnboundPredicateImpl::Make( - Expression::Operation::kGtEq, std::move(ref), std::move(transformed)); + if (length < width) { + return MakePredicate(op, name, func, literal); + } + + if (length == width) { + if (op == Expression::Operation::kStartsWith) { + return MakePredicate(Expression::Operation::kEq, name, func, literal); + } else { + return MakePredicate(Expression::Operation::kNotEq, name, func, literal); } - default: - return GenericTransform(std::move(ref), predicate, func); } + + if (op == Expression::Operation::kStartsWith) { + return TruncateByteArray(name, pred, func); + } + + return nullptr; } - static Result> TruncateStringLiteral( - std::string_view name, const std::shared_ptr& predicate, + static Result> TruncateStringLiteralStrict( + std::string_view name, const std::shared_ptr& pred, const std::shared_ptr& func) { - const auto op = predicate->op(); + const auto op = pred->op(); if (op != Expression::Operation::kStartsWith && op != Expression::Operation::kNotStartsWith) { - return TruncateByteArray(name, predicate, func); + return TruncateByteArrayStrict(name, pred, func); } - const auto& truncate_transform = - internal::checked_pointer_cast(func); - const auto& str_value = std::get(predicate->literal().value()); - const auto width = truncate_transform->width(); - ICEBERG_ASSIGN_OR_RAISE(auto ref, NamedReference::Make(std::string(name))); + const auto& literal = pred->literal(); + const auto length = + StringUtils::CodePointCount(std::get(literal.value())); + const auto width = static_cast( + internal::checked_pointer_cast(func)->width()); - if (StringUtils::CodePointCount(str_value) < width) { - return UnboundPredicateImpl::Make(op, std::move(ref), - predicate->literal()); + if (length < width) { + return MakePredicate(op, name, func, literal); } - if (StringUtils::CodePointCount(str_value) == width) { + if (length == width) { if (op == Expression::Operation::kStartsWith) { - return UnboundPredicateImpl::Make( - Expression::Operation::kEq, std::move(ref), predicate->literal()); + return MakePredicate(Expression::Operation::kEq, name, func, literal); } else { - return UnboundPredicateImpl::Make( - Expression::Operation::kNotEq, std::move(ref), predicate->literal()); + return MakePredicate(Expression::Operation::kNotEq, name, func, literal); } } - if (op == Expression::Operation::kStartsWith) { - ICEBERG_ASSIGN_OR_RAISE(auto transformed, func->Transform(predicate->literal())); - return UnboundPredicateImpl::Make( - Expression::Operation::kStartsWith, std::move(ref), std::move(transformed)); + if (op == Expression::Operation::kNotStartsWith) { + return MakePredicate(Expression::Operation::kNotStartsWith, name, func, literal); } return nullptr; @@ -304,14 +288,13 @@ class ProjectionUtil { const auto& literal = projected->literals().front(); ICEBERG_DCHECK(std::holds_alternative(literal.value()), "Expected int32_t"); - auto value = std::get(literal.value()); - if (value < 0) { + if (auto value = std::get(literal.value()); value < 0) { return UnboundPredicateImpl::Make(Expression::Operation::kLt, std::move(projected->term()), Literal::Int(value + 1)); } - return std::move(projected); + return projected; } case Expression::Operation::kLtEq: { @@ -319,34 +302,33 @@ class ProjectionUtil { const auto& literal = projected->literals().front(); ICEBERG_DCHECK(std::holds_alternative(literal.value()), "Expected int32_t"); - auto value = std::get(literal.value()); - if (value < 0) { + + if (auto value = std::get(literal.value()); value < 0) { return UnboundPredicateImpl::Make(Expression::Operation::kLtEq, std::move(projected->term()), Literal::Int(value + 1)); } - return std::move(projected); + return projected; } case Expression::Operation::kGt: case Expression::Operation::kGtEq: // incorrect projected values are already greater than the bound for GT, GT_EQ - return std::move(projected); + return projected; case Expression::Operation::kEq: { ICEBERG_DCHECK(!projected->literals().empty(), "Expected at least one literal"); const auto& literal = projected->literals().front(); ICEBERG_DCHECK(std::holds_alternative(literal.value()), "Expected int32_t"); - auto value = std::get(literal.value()); - if (value < 0) { + if (auto value = std::get(literal.value()); value < 0) { // match either the incorrect value (projectedValue + 1) or the correct value // (projectedValue) return UnboundPredicateImpl::Make( Expression::Operation::kIn, std::move(projected->term()), {literal, Literal::Int(value + 1)}); } - return std::move(projected); + return projected; } case Expression::Operation::kIn: { @@ -377,7 +359,7 @@ class ProjectionUtil { std::move(projected->term()), std::move(values)); } - return std::move(projected); + return projected; } case Expression::Operation::kNotIn: @@ -386,30 +368,128 @@ class ProjectionUtil { return nullptr; default: - return std::move(projected); + return projected; + } + } + + // Fixes a strict projection to account for incorrectly transformed values. + // align with Java implementation: + // https://github.com/apache/iceberg/blob/1.10.x/api/src/main/java/org/apache/iceberg/transforms/ProjectionUtil.java#L347 + static Result> FixStrictTimeProjection( + std::unique_ptr> projected) { + if (projected == nullptr) { + return nullptr; + } + + switch (projected->op()) { + case Expression::Operation::kLt: + case Expression::Operation::kLtEq: + // the correct bound is a correct strict projection for the incorrectly + // transformed values. + return projected; + + case Expression::Operation::kGt: { + // GT and GT_EQ need to be adjusted because values that do not match the predicate + // may have been transformed into partition values that match the projected + // predicate. + ICEBERG_DCHECK(!projected->literals().empty(), "Expected at least one literal"); + const auto& literal = projected->literals().front(); + ICEBERG_DCHECK(std::holds_alternative(literal.value()), + "Expected int32_t"); + if (auto value = std::get(literal.value()); value <= 0) { + return UnboundPredicateImpl::Make(Expression::Operation::kGt, + std::move(projected->term()), + Literal::Int(value + 1)); + } + return projected; + } + + case Expression::Operation::kGtEq: { + ICEBERG_DCHECK(!projected->literals().empty(), "Expected at least one literal"); + const auto& literal = projected->literals().front(); + ICEBERG_DCHECK(std::holds_alternative(literal.value()), + "Expected int32_t"); + if (auto value = std::get(literal.value()); value <= 0) { + return UnboundPredicateImpl::Make(Expression::Operation::kGtEq, + std::move(projected->term()), + Literal::Int(value + 1)); + } + return projected; + } + + case Expression::Operation::kEq: + case Expression::Operation::kIn: + // there is no strict projection for EQ and IN + return nullptr; + + case Expression::Operation::kNotEq: { + ICEBERG_DCHECK(!projected->literals().empty(), "Expected at least one literal"); + const auto& literal = projected->literals().front(); + ICEBERG_DCHECK(std::holds_alternative(literal.value()), + "Expected int32_t"); + if (auto value = std::get(literal.value()); value < 0) { + return UnboundPredicateImpl::Make( + Expression::Operation::kNotIn, std::move(projected->term()), + {literal, Literal::Int(value + 1)}); + } + return projected; + } + + case Expression::Operation::kNotIn: { + ICEBERG_DCHECK(!projected->literals().empty(), "Expected at least one literal"); + const auto& literals = projected->literals(); + ICEBERG_DCHECK( + std::ranges::all_of(literals, + [](const auto& lit) { + return std::holds_alternative(lit.value()); + }), + "Expected int32_t"); + std::unordered_set value_set; + bool has_negative_value = false; + for (const auto& lit : literals) { + auto value = std::get(lit.value()); + value_set.insert(value); + if (value < 0) { + value_set.insert(value + 1); + has_negative_value = true; + } + } + if (has_negative_value) { + auto values = + std::views::transform(value_set, + [](int32_t value) { return Literal::Int(value); }) | + std::ranges::to(); + return UnboundPredicateImpl::Make(Expression::Operation::kNotIn, + std::move(projected->term()), + std::move(values)); + } + return projected; + } + + default: + return nullptr; } } public: static Result> IdentityProject( - std::string_view name, const std::shared_ptr& predicate) { + std::string_view name, const std::shared_ptr& pred) { ICEBERG_ASSIGN_OR_RAISE(auto ref, NamedReference::Make(std::string(name))); - switch (predicate->kind()) { + switch (pred->kind()) { case BoundPredicate::Kind::kUnary: { - return UnboundPredicateImpl::Make(predicate->op(), - std::move(ref)); + return UnboundPredicateImpl::Make(pred->op(), std::move(ref)); } case BoundPredicate::Kind::kLiteral: { const auto& literalPredicate = - internal::checked_pointer_cast(predicate); - return UnboundPredicateImpl::Make(predicate->op(), std::move(ref), + internal::checked_pointer_cast(pred); + return UnboundPredicateImpl::Make(pred->op(), std::move(ref), literalPredicate->literal()); } case BoundPredicate::Kind::kSet: { const auto& setPredicate = - internal::checked_pointer_cast(predicate); + internal::checked_pointer_cast(pred); return UnboundPredicateImpl::Make( - predicate->op(), std::move(ref), + pred->op(), std::move(ref), std::vector(setPredicate->literal_set().begin(), setPredicate->literal_set().end())); } @@ -418,30 +498,29 @@ class ProjectionUtil { } static Result> BucketProject( - std::string_view name, const std::shared_ptr& predicate, + std::string_view name, const std::shared_ptr& pred, const std::shared_ptr& func) { ICEBERG_ASSIGN_OR_RAISE(auto ref, NamedReference::Make(std::string(name))); - switch (predicate->kind()) { + switch (pred->kind()) { case BoundPredicate::Kind::kUnary: { - return UnboundPredicateImpl::Make(predicate->op(), - std::move(ref)); + return UnboundPredicateImpl::Make(pred->op(), std::move(ref)); } case BoundPredicate::Kind::kLiteral: { - if (predicate->op() == Expression::Operation::kEq) { + if (pred->op() == Expression::Operation::kEq) { const auto& literalPredicate = - internal::checked_pointer_cast(predicate); + internal::checked_pointer_cast(pred); ICEBERG_ASSIGN_OR_RAISE(auto transformed, func->Transform(literalPredicate->literal())); - return UnboundPredicateImpl::Make( - predicate->op(), std::move(ref), std::move(transformed)); + return UnboundPredicateImpl::Make(pred->op(), std::move(ref), + std::move(transformed)); } break; } case BoundPredicate::Kind::kSet: { // notIn can't be projected - if (predicate->op() == Expression::Operation::kIn) { + if (pred->op() == Expression::Operation::kIn) { const auto& setPredicate = - internal::checked_pointer_cast(predicate); + internal::checked_pointer_cast(pred); return TransformSet(name, setPredicate, func); } break; @@ -455,19 +534,19 @@ class ProjectionUtil { } static Result> TruncateProject( - std::string_view name, const std::shared_ptr& predicate, + std::string_view name, const std::shared_ptr& pred, const std::shared_ptr& func) { ICEBERG_ASSIGN_OR_RAISE(auto ref, NamedReference::Make(std::string(name))); // Handle unary predicates uniformly for all types - if (predicate->kind() == BoundPredicate::Kind::kUnary) { - return UnboundPredicateImpl::Make(predicate->op(), std::move(ref)); + if (pred->kind() == BoundPredicate::Kind::kUnary) { + return UnboundPredicateImpl::Make(pred->op(), std::move(ref)); } // Handle set predicates (kIn) uniformly for all types - if (predicate->kind() == BoundPredicate::Kind::kSet) { - if (predicate->op() == Expression::Operation::kIn) { + if (pred->kind() == BoundPredicate::Kind::kSet) { + if (pred->op() == Expression::Operation::kIn) { const auto& setPredicate = - internal::checked_pointer_cast(predicate); + internal::checked_pointer_cast(pred); return TransformSet(name, setPredicate, func); } return nullptr; @@ -475,15 +554,13 @@ class ProjectionUtil { // Handle literal predicates based on source type const auto& literalPredicate = - internal::checked_pointer_cast(predicate); + internal::checked_pointer_cast(pred); switch (func->source_type()->type_id()) { case TypeId::kInt: - return TruncateInteger(name, literalPredicate, func); case TypeId::kLong: - return TruncateInteger(name, literalPredicate, func); case TypeId::kDecimal: - return TruncateDecimal(name, literalPredicate, func); + return TransformNumeric(name, literalPredicate, func); case TypeId::kString: return TruncateStringLiteral(name, literalPredicate, func); case TypeId::kBinary: @@ -495,16 +572,16 @@ class ProjectionUtil { } static Result> TemporalProject( - std::string_view name, const std::shared_ptr& predicate, + std::string_view name, const std::shared_ptr& pred, const std::shared_ptr& func) { ICEBERG_ASSIGN_OR_RAISE(auto ref, NamedReference::Make(std::string(name))); - if (predicate->kind() == BoundPredicate::Kind::kUnary) { - return UnboundPredicateImpl::Make(predicate->op(), std::move(ref)); - } else if (predicate->kind() == BoundPredicate::Kind::kLiteral) { + if (pred->kind() == BoundPredicate::Kind::kUnary) { + return UnboundPredicateImpl::Make(pred->op(), std::move(ref)); + } else if (pred->kind() == BoundPredicate::Kind::kLiteral) { const auto& literalPredicate = - internal::checked_pointer_cast(predicate); + internal::checked_pointer_cast(pred); ICEBERG_ASSIGN_OR_RAISE(auto projected, - TransformTemporal(name, literalPredicate, func)); + TransformNumeric(name, literalPredicate, func)); if (func->transform_type() != TransformType::kDay || func->source_type()->type_id() != TypeId::kDate) { return FixInclusiveTimeProjection( @@ -512,10 +589,9 @@ class ProjectionUtil { std::move(projected))); } return projected; - } else if (predicate->kind() == BoundPredicate::Kind::kSet && - predicate->op() == Expression::Operation::kIn) { - const auto& setPredicate = - internal::checked_pointer_cast(predicate); + } else if (pred->kind() == BoundPredicate::Kind::kSet && + pred->op() == Expression::Operation::kIn) { + const auto& setPredicate = internal::checked_pointer_cast(pred); ICEBERG_ASSIGN_OR_RAISE(auto projected, TransformSet(name, setPredicate, func)); if (func->transform_type() != TransformType::kDay || func->source_type()->type_id() != TypeId::kDate) { @@ -530,30 +606,135 @@ class ProjectionUtil { } static Result> RemoveTransform( - std::string_view name, const std::shared_ptr& predicate) { + std::string_view name, const std::shared_ptr& pred) { ICEBERG_ASSIGN_OR_RAISE(auto ref, NamedReference::Make(std::string(name))); - switch (predicate->kind()) { + switch (pred->kind()) { case BoundPredicate::Kind::kUnary: { - return UnboundPredicateImpl::Make(predicate->op(), - std::move(ref)); + return UnboundPredicateImpl::Make(pred->op(), std::move(ref)); } case BoundPredicate::Kind::kLiteral: { const auto& literalPredicate = - internal::checked_pointer_cast(predicate); - return UnboundPredicateImpl::Make(predicate->op(), std::move(ref), + internal::checked_pointer_cast(pred); + return UnboundPredicateImpl::Make(pred->op(), std::move(ref), literalPredicate->literal()); } case BoundPredicate::Kind::kSet: { const auto& setPredicate = - internal::checked_pointer_cast(predicate); + internal::checked_pointer_cast(pred); return UnboundPredicateImpl::Make( - predicate->op(), std::move(ref), + pred->op(), std::move(ref), std::vector(setPredicate->literal_set().begin(), setPredicate->literal_set().end())); } } std::unreachable(); } + + static Result> BucketProjectStrict( + std::string_view name, const std::shared_ptr& pred, + const std::shared_ptr& func) { + ICEBERG_ASSIGN_OR_RAISE(auto ref, NamedReference::Make(std::string(name))); + switch (pred->kind()) { + case BoundPredicate::Kind::kUnary: { + return UnboundPredicateImpl::Make(pred->op(), std::move(ref)); + } + case BoundPredicate::Kind::kLiteral: { + if (pred->op() == Expression::Operation::kNotEq) { + const auto& literalPredicate = + internal::checked_pointer_cast(pred); + ICEBERG_ASSIGN_OR_RAISE(auto transformed, + func->Transform(literalPredicate->literal())); + // TODO(anyone): need to translate not(eq(...)) into notEq in expressions + return UnboundPredicateImpl::Make(pred->op(), std::move(ref), + std::move(transformed)); + } + break; + } + case BoundPredicate::Kind::kSet: { + if (pred->op() == Expression::Operation::kNotIn) { + const auto& setPredicate = + internal::checked_pointer_cast(pred); + return TransformSet(name, setPredicate, func); + } + break; + } + } + + // no strict projection for comparison or equality + return nullptr; + } + + static Result> TruncateProjectStrict( + std::string_view name, const std::shared_ptr& pred, + const std::shared_ptr& func) { + ICEBERG_ASSIGN_OR_RAISE(auto ref, NamedReference::Make(std::string(name))); + // Handle unary predicates uniformly for all types + if (pred->kind() == BoundPredicate::Kind::kUnary) { + return UnboundPredicateImpl::Make(pred->op(), std::move(ref)); + } + + // Handle set predicates (kNotIn) uniformly for all types + if (pred->kind() == BoundPredicate::Kind::kSet) { + if (pred->op() == Expression::Operation::kNotIn) { + const auto& setPredicate = + internal::checked_pointer_cast(pred); + return TransformSet(name, setPredicate, func); + } + return nullptr; + } + + // Handle literal predicates based on source type + const auto& literalPredicate = + internal::checked_pointer_cast(pred); + + switch (func->source_type()->type_id()) { + case TypeId::kInt: + case TypeId::kLong: + case TypeId::kDecimal: + return TransformNumericStrict(name, literalPredicate, func); + case TypeId::kString: + return TruncateStringLiteralStrict(name, literalPredicate, func); + case TypeId::kBinary: + return TruncateByteArrayStrict(name, literalPredicate, func); + default: + return NotSupported("{} is not a valid input type for truncate transform", + func->source_type()->ToString()); + } + } + + static Result> TemporalProjectStrict( + std::string_view name, const std::shared_ptr& pred, + const std::shared_ptr& func) { + ICEBERG_ASSIGN_OR_RAISE(auto ref, NamedReference::Make(std::string(name))); + if (pred->kind() == BoundPredicate::Kind::kUnary) { + return UnboundPredicateImpl::Make(pred->op(), std::move(ref)); + } else if (pred->kind() == BoundPredicate::Kind::kLiteral) { + const auto& literalPredicate = + internal::checked_pointer_cast(pred); + ICEBERG_ASSIGN_OR_RAISE(auto projected, + TransformNumericStrict(name, literalPredicate, func)); + if (func->transform_type() != TransformType::kDay || + func->source_type()->type_id() != TypeId::kDate) { + return FixStrictTimeProjection( + internal::checked_pointer_cast>( + std::move(projected))); + } + return projected; + } else if (pred->kind() == BoundPredicate::Kind::kSet && + pred->op() == Expression::Operation::kNotIn) { + const auto& setPredicate = internal::checked_pointer_cast(pred); + ICEBERG_ASSIGN_OR_RAISE(auto projected, TransformSet(name, setPredicate, func)); + if (func->transform_type() != TransformType::kDay || + func->source_type()->type_id() != TypeId::kDate) { + return FixStrictTimeProjection( + internal::checked_pointer_cast>( + std::move(projected))); + } + return projected; + } + + return nullptr; + } }; } // namespace iceberg