@@ -4416,10 +4416,11 @@ fn test_unparse_chained_intersect_build_side_is_self_contained() -> Result<()> {
44164416
44174417#[ test]
44184418fn 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