@@ -451,6 +451,92 @@ public void testToIcebergExpressionDotColumn() {
451451 Assertions .assertEquals (Expression .Operation .TRUE , convertedExpr .op ());
452452 }
453453
454+ @ Test
455+ public void testAndPartialPushdown () {
456+ ScalarOperatorToIcebergExpr .IcebergContext context = new ScalarOperatorToIcebergExpr .IcebergContext (SCHEMA .asStruct ());
457+ ScalarOperatorToIcebergExpr converter = new ScalarOperatorToIcebergExpr ();
458+
459+ // Build a convertible predicate: k1 > 10
460+ BinaryPredicateOperator convertible = new BinaryPredicateOperator (
461+ BinaryType .GT , K1 , ConstantOperator .createInt (10 ));
462+
463+ // Build an unconvertible predicate: CAST(k6 AS INT) < 5
464+ // CastOperator(INT, K6) where K6 is a string column causes getLiteralValue to return null
465+ CastOperator cast = new CastOperator (IntegerType .INT , K6 );
466+ BinaryPredicateOperator unconvertible = new BinaryPredicateOperator (
467+ BinaryType .LT , cast , ConstantOperator .createInt (5 ));
468+
469+ // Verify that the unconvertible predicate alone produces alwaysTrue
470+ Expression unconvertibleExpr = converter .convert (Lists .newArrayList (unconvertible ), context );
471+ Assertions .assertEquals (Expression .Operation .TRUE , unconvertibleExpr .op (),
472+ "Unconvertible predicate should produce alwaysTrue" );
473+
474+ Expression convertedExpr ;
475+ Expression expectedExpr ;
476+
477+ // AND(convertible, unconvertible) -> returns the convertible side
478+ convertedExpr = converter .convert (Lists .newArrayList (
479+ new CompoundPredicateOperator (CompoundPredicateOperator .CompoundType .AND ,
480+ convertible , unconvertible )), context );
481+ expectedExpr = Expressions .greaterThan ("k1" , 10 );
482+ Assertions .assertEquals (expectedExpr .toString (), convertedExpr .toString (),
483+ "AND(convertible, unconvertible) should push down the convertible side" );
484+
485+ // AND(unconvertible, convertible) -> returns the convertible side
486+ convertedExpr = converter .convert (Lists .newArrayList (
487+ new CompoundPredicateOperator (CompoundPredicateOperator .CompoundType .AND ,
488+ unconvertible , convertible )), context );
489+ expectedExpr = Expressions .greaterThan ("k1" , 10 );
490+ Assertions .assertEquals (expectedExpr .toString (), convertedExpr .toString (),
491+ "AND(unconvertible, convertible) should push down the convertible side" );
492+
493+ // OR(convertible, unconvertible) -> returns alwaysTrue (no partial pushdown for OR)
494+ convertedExpr = converter .convert (Lists .newArrayList (
495+ new CompoundPredicateOperator (CompoundPredicateOperator .CompoundType .OR ,
496+ convertible , unconvertible )), context );
497+ Assertions .assertEquals (Expression .Operation .TRUE , convertedExpr .op (),
498+ "OR(convertible, unconvertible) should NOT do partial pushdown" );
499+
500+ // NOT(AND(convertible, unconvertible)) -> returns alwaysTrue (no partial pushdown inside NOT)
501+ CompoundPredicateOperator andOp = new CompoundPredicateOperator (
502+ CompoundPredicateOperator .CompoundType .AND , convertible , unconvertible );
503+ convertedExpr = converter .convert (Lists .newArrayList (
504+ new CompoundPredicateOperator (CompoundPredicateOperator .CompoundType .NOT , andOp )), context );
505+ Assertions .assertEquals (Expression .Operation .TRUE , convertedExpr .op (),
506+ "NOT(AND(convertible, unconvertible)) should NOT do partial pushdown" );
507+
508+ // AND(convertible1, convertible2) -> returns and(left, right) (regression test)
509+ BinaryPredicateOperator convertible2 = new BinaryPredicateOperator (
510+ BinaryType .LT , K2 , ConstantOperator .createInt (20 ));
511+ convertedExpr = converter .convert (Lists .newArrayList (
512+ new CompoundPredicateOperator (CompoundPredicateOperator .CompoundType .AND ,
513+ convertible , convertible2 )), context );
514+ expectedExpr = Expressions .and (
515+ Expressions .greaterThan ("k1" , 10 ),
516+ Expressions .lessThan ("k2" , 20 ));
517+ Assertions .assertEquals (expectedExpr .toString (), convertedExpr .toString (),
518+ "AND(convertible1, convertible2) should return and(left, right)" );
519+
520+ // Nested AND: AND(AND(convertible, unconvertible), convertible2) -> returns and(convertible, convertible2)
521+ CompoundPredicateOperator innerAnd = new CompoundPredicateOperator (
522+ CompoundPredicateOperator .CompoundType .AND , convertible , unconvertible );
523+ convertedExpr = converter .convert (Lists .newArrayList (
524+ new CompoundPredicateOperator (CompoundPredicateOperator .CompoundType .AND ,
525+ innerAnd , convertible2 )), context );
526+ expectedExpr = Expressions .and (
527+ Expressions .greaterThan ("k1" , 10 ),
528+ Expressions .lessThan ("k2" , 20 ));
529+ Assertions .assertEquals (expectedExpr .toString (), convertedExpr .toString (),
530+ "AND(AND(convertible, unconvertible), convertible2) should return and(convertible, convertible2)" );
531+
532+ // Strict mode: AND(convertible, unconvertible) -> returns null (no partial pushdown)
533+ convertedExpr = converter .convertStrict (Lists .newArrayList (
534+ new CompoundPredicateOperator (CompoundPredicateOperator .CompoundType .AND ,
535+ convertible , unconvertible )), context );
536+ Assertions .assertNull (convertedExpr ,
537+ "Strict mode: AND(convertible, unconvertible) should return null" );
538+ }
539+
454540 @ Test
455541 public void testConvertLastUpdatedSequenceNumberPredicate () {
456542 ScalarOperatorToIcebergExpr .IcebergContext context = new ScalarOperatorToIcebergExpr .IcebergContext (SCHEMA .asStruct ());
0 commit comments