@@ -473,33 +473,44 @@ fn collect_and_predicates(
473473
474474/// Recursively collect all OR sub-conditions and flatten nested OR
475475/// structures.
476+ ///
477+ /// Returns `false` if any disjunct cannot be converted. OR pushdown must be
478+ /// all-or-nothing: a pushed predicate is only used to skip row groups whose
479+ /// statistics prove no row can match, so it must be implied by the true filter
480+ /// (`true_filter => pushed`). Dropping a disjunct narrows the OR into a subset
481+ /// of the true filter, which makes the reader skip row groups that actually
482+ /// contain matching rows. (Dropping AND conjuncts only loosens the predicate,
483+ /// so that stays safe.)
476484fn collect_or_predicates (
477485 expr : & Arc < dyn datafusion:: physical_expr:: PhysicalExpr > ,
478486 schema : & SchemaRef ,
479487 predicates : & mut Vec < Predicate > ,
480- ) {
488+ ) -> bool {
481489 // Handle short-circuit OR expression (SCOrExpr)
482490 if let Some ( sc_or) = expr. as_any ( ) . downcast_ref :: < SCOrExpr > ( ) {
483491 // Recursively collect OR sub-conditions from both sides
484- collect_or_predicates ( & sc_or. left , schema, predicates) ;
485- collect_or_predicates ( & sc_or. right , schema, predicates) ;
486- return ;
492+ return collect_or_predicates ( & sc_or. left , schema, predicates)
493+ && collect_or_predicates ( & sc_or. right , schema, predicates) ;
487494 }
488495
489496 // Handle BinaryExpr with OR operator
490497 if let Some ( binary) = expr. as_any ( ) . downcast_ref :: < BinaryExpr > ( ) {
491498 if matches ! ( binary. op( ) , Operator :: Or ) {
492499 // Recursively collect OR sub-conditions from both sides
493- collect_or_predicates ( binary. left ( ) , schema, predicates) ;
494- collect_or_predicates ( binary. right ( ) , schema, predicates) ;
495- return ;
500+ return collect_or_predicates ( binary. left ( ) , schema, predicates)
501+ && collect_or_predicates ( binary. right ( ) , schema, predicates) ;
496502 }
497503 }
498504
499- // Not an OR expression, convert the whole expression
500- // (could be AND, comparison, IS NULL, etc.)
501- if let Some ( pred) = convert_expr_to_orc ( expr, schema) {
502- predicates. push ( pred) ;
505+ // Not an OR expression, convert the whole expression as a single disjunct
506+ // (could be AND, comparison, IS NULL, etc.). If it cannot be converted, the
507+ // entire OR is unpushable.
508+ match convert_expr_to_orc ( expr, schema) {
509+ Some ( pred) => {
510+ predicates. push ( pred) ;
511+ true
512+ }
513+ None => false ,
503514 }
504515}
505516
@@ -531,7 +542,11 @@ fn convert_expr_to_orc(
531542 // Handle top-level short-circuit OR expression (SCOrExpr)
532543 if let Some ( _sc_or) = expr. as_any ( ) . downcast_ref :: < SCOrExpr > ( ) {
533544 let mut predicates = Vec :: new ( ) ;
534- collect_or_predicates ( expr, schema, & mut predicates) ;
545+ if !collect_or_predicates ( expr, schema, & mut predicates) {
546+ // an OR disjunct could not be converted: pushing a narrowed
547+ // predicate would skip row groups holding matching rows
548+ return None ;
549+ }
535550
536551 if predicates. is_empty ( ) {
537552 return None ;
@@ -574,7 +589,11 @@ fn convert_expr_to_orc(
574589 // Handle top-level OR expression (BinaryExpr with OR operator)
575590 if matches ! ( binary. op( ) , Operator :: Or ) {
576591 let mut predicates = Vec :: new ( ) ;
577- collect_or_predicates ( expr, schema, & mut predicates) ;
592+ if !collect_or_predicates ( expr, schema, & mut predicates) {
593+ // an OR disjunct could not be converted: pushing a narrowed
594+ // predicate would skip row groups holding matching rows
595+ return None ;
596+ }
578597
579598 if predicates. is_empty ( ) {
580599 return None ;
@@ -1122,6 +1141,115 @@ mod tests {
11221141 assert_eq ! ( condition_count, 3 ) ;
11231142 }
11241143
1144+ #[ test]
1145+ fn test_or_with_unconvertible_disjunct_not_pushed ( ) {
1146+ let schema = create_test_schema ( ) ;
1147+ // id = 1 OR (id = age)
1148+ // The second disjunct compares two columns and cannot be converted.
1149+ // The whole OR must NOT push down a narrowed predicate, otherwise the
1150+ // reader would skip row groups that only satisfy the dropped disjunct.
1151+ let id = Arc :: new ( Column :: new ( "id" , 0 ) ) ;
1152+ let lit1 = Arc :: new ( Literal :: new ( ScalarValue :: Int32 ( Some ( 1 ) ) ) ) ;
1153+ let conv = Arc :: new ( BinaryExpr :: new ( id. clone ( ) , Operator :: Eq , lit1) ) ;
1154+
1155+ let age = Arc :: new ( Column :: new ( "age" , 2 ) ) ;
1156+ let unconv = Arc :: new ( BinaryExpr :: new ( id, Operator :: Eq , age) ) ;
1157+
1158+ let or_expr = Arc :: new ( BinaryExpr :: new ( conv, Operator :: Or , unconv) ) ;
1159+
1160+ let result = convert_predicate_to_orc ( Some ( or_expr) , & schema) ;
1161+ assert ! (
1162+ result. is_none( ) ,
1163+ "OR with an unconvertible disjunct must not push down, got: {result:?}"
1164+ ) ;
1165+ }
1166+
1167+ #[ test]
1168+ fn test_or_with_unconvertible_and_branch_not_pushed ( ) {
1169+ let schema = create_test_schema ( ) ;
1170+ // (id = age AND age = score) OR (id = 2)
1171+ // The first disjunct is an AND of two column-column comparisons, neither
1172+ // of which converts, so the AND yields no predicate. That disjunct must
1173+ // poison the whole OR rather than being silently dropped. This mirrors
1174+ // the production bug where `cast(type)=2 AND cast(gjo)=1` was dropped.
1175+ let id = Arc :: new ( Column :: new ( "id" , 0 ) ) ;
1176+ let age = Arc :: new ( Column :: new ( "age" , 2 ) ) ;
1177+ let id_eq_age = Arc :: new ( BinaryExpr :: new ( id. clone ( ) , Operator :: Eq , age. clone ( ) ) ) ;
1178+ let age_eq_score = Arc :: new ( BinaryExpr :: new (
1179+ age,
1180+ Operator :: Eq ,
1181+ Arc :: new ( Column :: new ( "score" , 3 ) ) ,
1182+ ) ) ;
1183+ let and_branch = Arc :: new ( BinaryExpr :: new ( id_eq_age, Operator :: And , age_eq_score) ) ;
1184+
1185+ let lit2 = Arc :: new ( Literal :: new ( ScalarValue :: Int32 ( Some ( 2 ) ) ) ) ;
1186+ let id_eq_2 = Arc :: new ( BinaryExpr :: new ( id, Operator :: Eq , lit2) ) ;
1187+
1188+ let or_expr = Arc :: new ( BinaryExpr :: new ( and_branch, Operator :: Or , id_eq_2) ) ;
1189+
1190+ let result = convert_predicate_to_orc ( Some ( or_expr) , & schema) ;
1191+ assert ! (
1192+ result. is_none( ) ,
1193+ "OR whose disjunct is a fully-unconvertible AND must not push down, got: {result:?}"
1194+ ) ;
1195+ }
1196+
1197+ #[ test]
1198+ fn test_and_keeps_convertible_conjunct_when_or_unconvertible ( ) {
1199+ let schema = create_test_schema ( ) ;
1200+ // name = "x" AND (id = 1 OR id = age)
1201+ // The OR is unconvertible, but it is an AND conjunct. Dropping it only
1202+ // loosens the pushed predicate, so the convertible name = "x" conjunct
1203+ // must still push down.
1204+ let name = Arc :: new ( Column :: new ( "name" , 1 ) ) ;
1205+ let name_lit = Arc :: new ( Literal :: new ( ScalarValue :: Utf8 ( Some ( "x" . to_string ( ) ) ) ) ) ;
1206+ let name_eq = Arc :: new ( BinaryExpr :: new ( name, Operator :: Eq , name_lit) ) ;
1207+
1208+ let id = Arc :: new ( Column :: new ( "id" , 0 ) ) ;
1209+ let lit1 = Arc :: new ( Literal :: new ( ScalarValue :: Int32 ( Some ( 1 ) ) ) ) ;
1210+ let id_eq_1 = Arc :: new ( BinaryExpr :: new ( id. clone ( ) , Operator :: Eq , lit1) ) ;
1211+ let age = Arc :: new ( Column :: new ( "age" , 2 ) ) ;
1212+ let id_eq_age = Arc :: new ( BinaryExpr :: new ( id, Operator :: Eq , age) ) ;
1213+ let or_expr = Arc :: new ( BinaryExpr :: new ( id_eq_1, Operator :: Or , id_eq_age) ) ;
1214+
1215+ let and_expr = Arc :: new ( BinaryExpr :: new ( name_eq, Operator :: And , or_expr) ) ;
1216+
1217+ let result = convert_predicate_to_orc ( Some ( and_expr) , & schema) ;
1218+ assert ! ( result. is_some( ) ) ;
1219+ let debug_str = format ! ( "{:?}" , result. expect( "Expected valid ORC predicate" ) ) ;
1220+ // Only the name = "x" conjunct survives; the unconvertible OR is dropped.
1221+ assert ! (
1222+ debug_str. contains( "\" name\" " ) && debug_str. contains( "Equal" ) ,
1223+ "Expected name = \" x\" to push down, got: {debug_str}"
1224+ ) ;
1225+ assert ! (
1226+ !debug_str. contains( "Or(" ) ,
1227+ "Unconvertible OR must not appear in the pushed predicate, got: {debug_str}"
1228+ ) ;
1229+ }
1230+
1231+ #[ test]
1232+ fn test_sc_or_with_unconvertible_disjunct_not_pushed ( ) {
1233+ let schema = create_test_schema ( ) ;
1234+ // Short-circuit OR: id = 1 OR (id = age)
1235+ // Same invariant as the BinaryExpr::Or case, but exercising the
1236+ // SCOrExpr path that the fix also updates.
1237+ let id = Arc :: new ( Column :: new ( "id" , 0 ) ) ;
1238+ let lit1 = Arc :: new ( Literal :: new ( ScalarValue :: Int32 ( Some ( 1 ) ) ) ) ;
1239+ let conv = Arc :: new ( BinaryExpr :: new ( id. clone ( ) , Operator :: Eq , lit1) ) ;
1240+
1241+ let age = Arc :: new ( Column :: new ( "age" , 2 ) ) ;
1242+ let unconv = Arc :: new ( BinaryExpr :: new ( id, Operator :: Eq , age) ) ;
1243+
1244+ let sc_or_expr = Arc :: new ( SCOrExpr :: new ( conv, unconv) ) ;
1245+
1246+ let result = convert_predicate_to_orc ( Some ( sc_or_expr) , & schema) ;
1247+ assert ! (
1248+ result. is_none( ) ,
1249+ "SCOrExpr with an unconvertible disjunct must not push down, got: {result:?}"
1250+ ) ;
1251+ }
1252+
11251253 #[ test]
11261254 fn test_complex_mixed_predicates ( ) {
11271255 let schema = create_test_schema ( ) ;
0 commit comments