2121
2222#include < limits>
2323#include < memory>
24- #include < utility>
2524
2625#include " iceberg/expression/expressions.h"
2726#include " iceberg/schema.h"
@@ -367,49 +366,6 @@ TEST_F(PredicateTest, UnboundPredicateBindUnary) {
367366 EXPECT_EQ (bound_is_null->op (), Expression::Operation::kFalse );
368367}
369368
370- TEST_F (PredicateTest, BindNestedNullPredicates) {
371- for (auto [outer_optional, inner_optional] :
372- {std::pair{false , false }, std::pair{true , false }, std::pair{false , true }}) {
373- auto inner = std::make_shared<StructType>(
374- std::vector<SchemaField>{SchemaField::MakeRequired (4 , " leaf" , int32 ())});
375- auto outer = std::make_shared<StructType>(
376- std::vector<SchemaField>{SchemaField (3 , " middle" , inner, inner_optional)});
377- Schema schema (
378- std::vector<SchemaField>{SchemaField::MakeOptional (1 , " unrelated" , int32 ()),
379- SchemaField (2 , " key" , outer, outer_optional)});
380- SCOPED_TRACE (schema.ToString ());
381- bool nullable = outer_optional || inner_optional;
382- ICEBERG_UNWRAP_OR_FAIL (auto is_null,
383- Expressions::IsNull (" key.middle.leaf" )->Bind (schema, true ));
384- ICEBERG_UNWRAP_OR_FAIL (auto not_null,
385- Expressions::NotNull (" key.middle.leaf" )->Bind (schema, true ));
386- EXPECT_EQ (is_null->op (),
387- nullable ? Expression::Operation::kIsNull : Expression::Operation::kFalse );
388- EXPECT_EQ (not_null->op (),
389- nullable ? Expression::Operation::kNotNull : Expression::Operation::kTrue );
390- }
391- }
392-
393- TEST_F (PredicateTest, BindTransformedNestedNullPredicates) {
394- for (bool parent_optional : {false , true }) {
395- Schema schema (std::vector<SchemaField>{SchemaField (
396- 1 , " key" ,
397- std::make_shared<StructType>(
398- std::vector<SchemaField>{SchemaField::MakeRequired (2 , " nested" , int32 ())}),
399- parent_optional)});
400- auto truncate = Expressions::Truncate (" key.nested" , 10 );
401- ICEBERG_UNWRAP_OR_FAIL (
402- auto is_null, Expressions::IsNull<BoundTransform>(truncate)->Bind (schema, true ));
403- ICEBERG_UNWRAP_OR_FAIL (
404- auto not_null,
405- Expressions::NotNull<BoundTransform>(truncate)->Bind (schema, true ));
406- EXPECT_EQ (is_null->op (), parent_optional ? Expression::Operation::kIsNull
407- : Expression::Operation::kFalse );
408- EXPECT_EQ (not_null->op (), parent_optional ? Expression::Operation::kNotNull
409- : Expression::Operation::kTrue );
410- }
411- }
412-
413369TEST_F (PredicateTest, UnboundPredicateBindUnaryWithUnknownType) {
414370 // Test IS NULL on unknown type field - should return AlwaysTrue
415371 auto is_null_unknown = Expressions::IsNull (" always_null" ); // always_null is unknown
@@ -428,6 +384,45 @@ TEST_F(PredicateTest, UnboundPredicateBindUnaryWithUnknownType) {
428384 EXPECT_EQ (bound_not_null->op (), Expression::Operation::kFalse );
429385}
430386
387+ TEST_F (PredicateTest, UnboundPredicateBindUnaryWithNestedStructs) {
388+ // A required field nested in an optional struct can still produce null values
389+ auto nested_schema = std::make_shared<Schema>(
390+ std::vector<SchemaField>{
391+ SchemaField::MakeOptional (1 , " optional_parent" ,
392+ std::make_shared<StructType>(std::vector<SchemaField>{
393+ SchemaField::MakeRequired (2 , " leaf" , int32 ())})),
394+ SchemaField::MakeRequired (3 , " required_parent" ,
395+ std::make_shared<StructType>(std::vector<SchemaField>{
396+ SchemaField::MakeRequired (4 , " leaf" , int32 ())})),
397+ },
398+ /* schema_id=*/ 0 );
399+
400+ auto is_null_optional_parent = Expressions::IsNull (" optional_parent.leaf" );
401+ auto bound_is_null_optional =
402+ is_null_optional_parent->Bind (*nested_schema, /* case_sensitive=*/ true );
403+ ASSERT_THAT (bound_is_null_optional, IsOk ());
404+ EXPECT_EQ (bound_is_null_optional.value ()->op (), Expression::Operation::kIsNull );
405+
406+ auto not_null_optional_parent = Expressions::NotNull (" optional_parent.leaf" );
407+ auto bound_not_null_optional =
408+ not_null_optional_parent->Bind (*nested_schema, /* case_sensitive=*/ true );
409+ ASSERT_THAT (bound_not_null_optional, IsOk ());
410+ EXPECT_EQ (bound_not_null_optional.value ()->op (), Expression::Operation::kNotNull );
411+
412+ // A required field with required ancestors can never produce null values
413+ auto is_null_required_parent = Expressions::IsNull (" required_parent.leaf" );
414+ auto bound_is_null_required =
415+ is_null_required_parent->Bind (*nested_schema, /* case_sensitive=*/ true );
416+ ASSERT_THAT (bound_is_null_required, IsOk ());
417+ EXPECT_EQ (bound_is_null_required.value ()->op (), Expression::Operation::kFalse );
418+
419+ auto not_null_required_parent = Expressions::NotNull (" required_parent.leaf" );
420+ auto bound_not_null_required =
421+ not_null_required_parent->Bind (*nested_schema, /* case_sensitive=*/ true );
422+ ASSERT_THAT (bound_not_null_required, IsOk ());
423+ EXPECT_EQ (bound_not_null_required.value ()->op (), Expression::Operation::kTrue );
424+ }
425+
431426TEST_F (PredicateTest, UnboundPredicateBindLiteral) {
432427 auto equal_pred = Expressions::Equal (" age" , Literal::Int (25 ));
433428 auto bound_result = equal_pred->Bind (*schema_, /* case_sensitive=*/ true );
0 commit comments