Skip to content

Commit 0ef91f7

Browse files
Flyangzslfan1989
andauthored
[AURON #2369] fix incorrect ORC predicate pushdown with OR (#2370)
# Which issue does this PR close? Closes #2369 # Rationale for this change Fix some ORC predicate pushdown with OR losing data. # What changes are included in this PR? - Made OR pushdown all-or-nothing: collect_or_predicates now returns bool, and if any disjunct fails to convert, the whole OR is not pushed down (convert_expr_to_orc returns None). Convertible AND conjuncts around the OR still push down safely. - Added unit tests covering: OR with an unconvertible disjunct, OR whose disjunct is a fully-unconvertible AND, and an unconvertible OR nested in an AND where the sibling conjunct still pushes down. # Are there any user-facing changes? no # How was this patch tested? unit tests Co-authored-by: Shilun Fan <slfan1989@apache.org> Signed-off-by: Shilun Fan <slfan1989@apache.org>
1 parent ec6eb49 commit 0ef91f7

2 files changed

Lines changed: 154 additions & 13 deletions

File tree

native-engine/datafusion-ext-plans/src/orc_exec.rs

Lines changed: 141 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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.)
476484
fn 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();

spark-extension-shims-spark/src/test/scala/org/apache/auron/AuronQuerySuite.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,4 +1123,17 @@ class AuronQuerySuite extends AuronQueryTest with BaseAuronSQLSuite with AuronSQ
11231123
|FROM t_filter_agg_2289""".stripMargin)
11241124
}
11251125
}
1126+
1127+
test("test OR pushdown with an unconvertible disjunct for orc table") {
1128+
withTable("orc_or") {
1129+
sql("create table orc_or(id int, b string) using orc")
1130+
// enough rows so `id` statistics differ across row groups and pruning kicks in
1131+
sql("insert into orc_or select cast(id as int), cast(id as string) from range(0, 1000000)")
1132+
// `b = 900000` (string col vs int literal) -> cast(b as double)=2.0 -> not convertible
1133+
// `id = 5` -> convertible
1134+
// OR drops the b-branch -> pushes only `id = 5` -> row groups without id=5 are skipped,
1135+
// losing the row where b='900000'
1136+
checkSparkAnswerAndOperator("select * from orc_or where id = 5 or b = 900000")
1137+
}
1138+
}
11261139
}

0 commit comments

Comments
 (0)