Skip to content

Commit 90d8a9c

Browse files
committed
fix(unparser): avoid hoisting full join predicates
1 parent ddbb930 commit 90d8a9c

2 files changed

Lines changed: 32 additions & 10 deletions

File tree

datafusion/sql/src/unparser/plan.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,8 +1001,11 @@ impl Unparser<'_> {
10011001
// aside, see what the left subtree adds, and fold that into
10021002
// this join's `ON` instead (where, for the non-preserved side,
10031003
// it means the same thing).
1004-
let left_is_null_extended =
1005-
matches!(join.join_type, JoinType::Right | JoinType::Full);
1004+
// This relocation is only valid when the left input is not
1005+
// preserved. A FULL JOIN also null-extends its left input, but
1006+
// preserves left rows, so moving the predicate into ON would
1007+
// make filtered-out left rows reappear as unmatched rows.
1008+
let left_is_null_extended = matches!(join.join_type, JoinType::Right);
10061009
let outer_selection = if left_is_null_extended {
10071010
select.take_selection()
10081011
} else {

datafusion/sql/tests/cases/plan_to_sql.rs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4416,10 +4416,11 @@ fn test_unparse_chained_intersect_build_side_is_self_contained() -> Result<()> {
44164416

44174417
#[test]
44184418
fn test_join_filter_nested_under_null_extending_join() -> Result<()> {
4419-
// A predicate extracted from a nested join's input must not reach the
4420-
// SELECT-global `WHERE` when an enclosing join null-extends that input:
4421-
// `WHERE` runs after every join and would discard the rows the enclosing
4422-
// join is there to preserve. It belongs in the enclosing join's `ON`.
4419+
// When an enclosing RIGHT JOIN null-extends a nested left input, a predicate
4420+
// from that input must not reach the SELECT-global `WHERE`: `WHERE` runs
4421+
// after every join and would discard the preserved right-side rows. It
4422+
// belongs in the enclosing join's `ON` because the left input is not
4423+
// preserved. FULL JOIN differs because it preserves both inputs.
44234424
let schema = Schema::new(vec![Field::new("id", DataType::Utf8, false)]);
44244425
let a = table_scan_with_filters(
44254426
Some("a"),
@@ -4431,7 +4432,7 @@ fn test_join_filter_nested_under_null_extending_join() -> Result<()> {
44314432
let b = table_scan(Some("b"), &schema, Some(vec![0]))?.build()?;
44324433
let c = table_scan(Some("c"), &schema, Some(vec![0]))?.build()?;
44334434

4434-
let nested_under_right = |inner_join_type| -> Result<String> {
4435+
let nested_under_outer = |inner_join_type, outer_join_type| -> Result<String> {
44354436
let inner = LogicalPlanBuilder::from(a.clone())
44364437
.join(
44374438
b.clone(),
@@ -4443,7 +4444,7 @@ fn test_join_filter_nested_under_null_extending_join() -> Result<()> {
44434444
let outer = LogicalPlanBuilder::from(inner)
44444445
.join(
44454446
c.clone(),
4446-
datafusion_expr::JoinType::Right,
4447+
outer_join_type,
44474448
(vec!["a.id"], vec!["c.id"]),
44484449
None,
44494450
)?
@@ -4452,13 +4453,31 @@ fn test_join_filter_nested_under_null_extending_join() -> Result<()> {
44524453
};
44534454

44544455
assert_snapshot!(
4455-
nested_under_right(datafusion_expr::JoinType::Inner)?,
4456+
nested_under_outer(
4457+
datafusion_expr::JoinType::Inner,
4458+
datafusion_expr::JoinType::Right,
4459+
)?,
44564460
@"SELECT a.id, b.id, c.id FROM a INNER JOIN b ON a.id = b.id RIGHT OUTER JOIN c ON a.id = c.id AND (a.id = 'x')"
44574461
);
44584462
assert_snapshot!(
4459-
nested_under_right(datafusion_expr::JoinType::Left)?,
4463+
nested_under_outer(
4464+
datafusion_expr::JoinType::Left,
4465+
datafusion_expr::JoinType::Right,
4466+
)?,
44604467
@"SELECT a.id, b.id, c.id FROM a LEFT OUTER JOIN b ON a.id = b.id RIGHT OUTER JOIN c ON a.id = c.id AND (a.id = 'x')"
44614468
);
44624469

4470+
// A FULL JOIN also null-extends its left input, but unlike a RIGHT JOIN it
4471+
// preserves that input. Hoisting the predicate into ON would therefore
4472+
// make filtered-out left rows reappear as unmatched rows. Keep the prior
4473+
// WHERE placement until FULL JOIN inputs can be emitted as derived tables.
4474+
assert_snapshot!(
4475+
nested_under_outer(
4476+
datafusion_expr::JoinType::Inner,
4477+
datafusion_expr::JoinType::Full,
4478+
)?,
4479+
@"SELECT a.id, b.id, c.id FROM a INNER JOIN b ON a.id = b.id FULL JOIN c ON a.id = c.id WHERE (a.id = 'x')"
4480+
);
4481+
44634482
Ok(())
44644483
}

0 commit comments

Comments
 (0)