@@ -744,9 +744,10 @@ pub fn try_embed_projection<Exec: EmbeddedProjection + 'static>(
744744 } ) ;
745745 }
746746 // Old projection may contain some alias or expression such as `a + 1` and `CAST('true' AS BOOLEAN)`, but our projection_exprs in hash join just contain column, so we need to create the new projection to keep the original projection.
747- let new_projection = Arc :: new ( ProjectionExec :: try_new (
747+ let new_projection = Arc :: new ( ProjectionExec :: try_new_with_schema_metadata (
748748 new_projection_exprs,
749749 Arc :: clone ( & new_execution_plan) as _ ,
750+ projection. schema ( ) . as_ref ( ) ,
750751 ) ?) ;
751752 if is_projection_removable ( & new_projection) {
752753 // Residual is identity — embedding fully absorbed the projection.
@@ -872,8 +873,11 @@ pub fn try_pushdown_through_join_with_column_indices(
872873 }
873874 let mut left_proj: Vec < ( Column , String ) > = Vec :: new ( ) ;
874875 let mut right_proj: Vec < ( Column , String ) > = Vec :: new ( ) ;
876+ let mut left_fields = Vec :: new ( ) ;
877+ let mut right_fields = Vec :: new ( ) ;
878+ let projection_schema = projection. schema ( ) ;
875879 let mut seen_right = false ;
876- for ( col, alias) in & projection_as_columns {
880+ for ( projection_index , ( col, alias) ) in projection_as_columns. iter ( ) . enumerate ( ) {
877881 let Some ( origin) = column_indices. get ( col. index ( ) ) else {
878882 return plan_err ! (
879883 "Projection column {} is outside the {}-entry column index mapping" ,
@@ -889,10 +893,14 @@ pub fn try_pushdown_through_join_with_column_indices(
889893 return Ok ( None ) ;
890894 }
891895 left_proj. push ( ( Column :: new ( col. name ( ) , origin. index ) , alias. clone ( ) ) ) ;
896+ left_fields
897+ . push ( Arc :: clone ( & projection_schema. fields ( ) [ projection_index] ) ) ;
892898 }
893899 JoinSide :: Right => {
894900 seen_right = true ;
895901 right_proj. push ( ( Column :: new ( col. name ( ) , origin. index ) , alias. clone ( ) ) ) ;
902+ right_fields
903+ . push ( Arc :: clone ( & projection_schema. fields ( ) [ projection_index] ) ) ;
896904 }
897905 // Synthetic column (e.g. mark): belongs to neither child.
898906 // Phase 2 declines; Phase 3 keeps it at the join output instead.
@@ -922,8 +930,16 @@ pub fn try_pushdown_through_join_with_column_indices(
922930 return Ok ( None ) ;
923931 } ;
924932
925- let ( new_left, new_right) =
926- new_join_children_from_groups ( & left_proj, & right_proj, join_left, join_right) ?;
933+ let left_schema = Schema :: new ( left_fields) ;
934+ let right_schema = Schema :: new ( right_fields) ;
935+ let ( new_left, new_right) = new_join_children_from_groups (
936+ & left_proj,
937+ & right_proj,
938+ & left_schema,
939+ & right_schema,
940+ join_left,
941+ join_right,
942+ ) ?;
927943
928944 Ok ( Some ( JoinData {
929945 projected_left_child : new_left,
@@ -968,6 +984,7 @@ fn is_projection_removable(projection: &ProjectionExec) -> bool {
968984 } ;
969985 col. name ( ) == proj_expr. alias && col. index ( ) == idx
970986 } ) && exprs. len ( ) == projection. input ( ) . schema ( ) . fields ( ) . len ( )
987+ && projection. schema ( ) == projection. input ( ) . schema ( )
971988}
972989
973990/// Given the expression set of a projection, checks if the projection causes
@@ -1006,8 +1023,12 @@ pub fn make_with_child(
10061023 projection : & ProjectionExec ,
10071024 child : & Arc < dyn ExecutionPlan > ,
10081025) -> Result < Arc < dyn ExecutionPlan > > {
1009- ProjectionExec :: try_new ( projection. expr ( ) . to_vec ( ) , Arc :: clone ( child) )
1010- . map ( |e| Arc :: new ( e) as _ )
1026+ ProjectionExec :: try_new_with_schema_metadata (
1027+ projection. expr ( ) . to_vec ( ) ,
1028+ Arc :: clone ( child) ,
1029+ projection. schema ( ) . as_ref ( ) ,
1030+ )
1031+ . map ( |e| Arc :: new ( e) as _ )
10111032}
10121033
10131034/// Returns `true` if all the expressions in the argument are `Column`s.
@@ -1072,22 +1093,30 @@ pub fn physical_to_column_exprs(
10721093/// of the original children of the join.
10731094pub fn new_join_children (
10741095 projection_as_columns : & [ ( Column , String ) ] ,
1096+ projection_schema : & Schema ,
10751097 far_right_left_col_ind : i32 ,
10761098 far_left_right_col_ind : i32 ,
10771099 left_child : & Arc < dyn ExecutionPlan > ,
10781100 right_child : & Arc < dyn ExecutionPlan > ,
10791101) -> Result < ( ProjectionExec , ProjectionExec ) > {
1080- let new_left = ProjectionExec :: try_new (
1102+ let left_schema = Schema :: new (
1103+ projection_schema. fields ( ) [ 0 ..=far_right_left_col_ind as usize ] . to_vec ( ) ,
1104+ ) ;
1105+ let new_left = ProjectionExec :: try_new_with_schema_metadata (
10811106 projection_as_columns[ 0 ..=far_right_left_col_ind as _ ]
10821107 . iter ( )
10831108 . map ( |( col, alias) | ProjectionExpr {
10841109 expr : Arc :: new ( Column :: new ( col. name ( ) , col. index ( ) ) ) as _ ,
10851110 alias : alias. clone ( ) ,
10861111 } ) ,
10871112 Arc :: clone ( left_child) ,
1113+ & left_schema,
10881114 ) ?;
10891115 let left_size = left_child. schema ( ) . fields ( ) . len ( ) as i32 ;
1090- let new_right = ProjectionExec :: try_new (
1116+ let right_schema = Schema :: new (
1117+ projection_schema. fields ( ) [ far_left_right_col_ind as usize ..] . to_vec ( ) ,
1118+ ) ;
1119+ let new_right = ProjectionExec :: try_new_with_schema_metadata (
10911120 projection_as_columns[ far_left_right_col_ind as _ ..]
10921121 . iter ( )
10931122 . map ( |( col, alias) | {
@@ -1102,6 +1131,7 @@ pub fn new_join_children(
11021131 }
11031132 } ) ,
11041133 Arc :: clone ( right_child) ,
1134+ & right_schema,
11051135 ) ?;
11061136
11071137 Ok ( ( new_left, new_right) )
@@ -1116,22 +1146,26 @@ pub fn new_join_children(
11161146fn new_join_children_from_groups (
11171147 left_proj : & [ ( Column , String ) ] ,
11181148 right_proj : & [ ( Column , String ) ] ,
1149+ left_schema : & Schema ,
1150+ right_schema : & Schema ,
11191151 left_child : & Arc < dyn ExecutionPlan > ,
11201152 right_child : & Arc < dyn ExecutionPlan > ,
11211153) -> Result < ( ProjectionExec , ProjectionExec ) > {
1122- let build = |cols : & [ ( Column , String ) ] , child : & Arc < dyn ExecutionPlan > | {
1123- ProjectionExec :: try_new (
1124- cols. iter ( ) . map ( |( col, alias) | ProjectionExpr {
1125- expr : Arc :: new ( Column :: new ( col. name ( ) , col. index ( ) ) ) as _ ,
1126- alias : alias. clone ( ) ,
1127- } ) ,
1128- Arc :: clone ( child) ,
1129- )
1130- } ;
1154+ let build =
1155+ |cols : & [ ( Column , String ) ] , schema : & Schema , child : & Arc < dyn ExecutionPlan > | {
1156+ ProjectionExec :: try_new_with_schema_metadata (
1157+ cols. iter ( ) . map ( |( col, alias) | ProjectionExpr {
1158+ expr : Arc :: new ( Column :: new ( col. name ( ) , col. index ( ) ) ) as _ ,
1159+ alias : alias. clone ( ) ,
1160+ } ) ,
1161+ Arc :: clone ( child) ,
1162+ schema,
1163+ )
1164+ } ;
11311165
11321166 Ok ( (
1133- build ( left_proj, left_child) ?,
1134- build ( right_proj, right_child) ?,
1167+ build ( left_proj, left_schema , left_child) ?,
1168+ build ( right_proj, right_schema , right_child) ?,
11351169 ) )
11361170}
11371171
@@ -1441,6 +1475,7 @@ mod tests {
14411475 use crate :: statistics:: { StatisticsArgs , StatisticsContext } ;
14421476 use crate :: test;
14431477 use crate :: test:: exec:: StatisticsExec ;
1478+ use crate :: union:: UnionExec ;
14441479
14451480 use arrow:: datatypes:: { DataType , Field , Schema } ;
14461481 use datafusion_common:: ScalarValue ;
@@ -1491,6 +1526,131 @@ mod tests {
14911526 Ok ( ( ) )
14921527 }
14931528
1529+ #[ test]
1530+ fn test_projection_pushdown_preserves_output_metadata ( ) -> Result < ( ) > {
1531+ let input_schema = Arc :: new ( Schema :: new ( vec ! [
1532+ Field :: new( "input" , DataType :: Int32 , false ) . with_metadata( HashMap :: from( [ (
1533+ "source" . to_string( ) ,
1534+ "input" . to_string( ) ,
1535+ ) ] ) ) ,
1536+ Field :: new( "unused" , DataType :: Int32 , false ) ,
1537+ ] ) ) ;
1538+ let input: Arc < dyn ExecutionPlan > = UnionExec :: try_new ( vec ! [
1539+ Arc :: new( EmptyExec :: new( Arc :: clone( & input_schema) ) ) ,
1540+ Arc :: new( EmptyExec :: new( input_schema) ) ,
1541+ ] ) ?;
1542+ let projected_schema =
1543+ Schema :: new ( vec ! [ Field :: new( "input" , DataType :: Int32 , false ) ] ) ;
1544+ let projection = ProjectionExec :: try_new_with_schema_metadata (
1545+ [ ProjectionExpr {
1546+ expr : Arc :: new ( Column :: new ( "input" , 0 ) ) ,
1547+ alias : "input" . to_string ( ) ,
1548+ } ] ,
1549+ input,
1550+ & projected_schema,
1551+ ) ?;
1552+
1553+ assert ! ( !is_projection_removable( & projection) ) ;
1554+ let plan: Arc < dyn ExecutionPlan > = Arc :: new ( projection) ;
1555+ let optimized = remove_unnecessary_projections ( Arc :: clone ( & plan) ) ?;
1556+ assert ! ( optimized. transformed) ;
1557+ assert_eq ! ( optimized. data. schema( ) , plan. schema( ) ) ;
1558+ Ok ( ( ) )
1559+ }
1560+
1561+ #[ test]
1562+ fn test_join_projection_pushdown_preserves_output_metadata ( ) -> Result < ( ) > {
1563+ let source_metadata =
1564+ HashMap :: from ( [ ( "source" . to_string ( ) , "iceberg" . to_string ( ) ) ] ) ;
1565+ let left_schema = Arc :: new ( Schema :: new ( vec ! [
1566+ Field :: new( "left_keep" , DataType :: Int32 , false )
1567+ . with_metadata( source_metadata. clone( ) ) ,
1568+ Field :: new( "left_unused" , DataType :: Int32 , false ) ,
1569+ ] ) ) ;
1570+ let right_schema = Arc :: new ( Schema :: new ( vec ! [
1571+ Field :: new( "right_keep" , DataType :: Int32 , false )
1572+ . with_metadata( source_metadata. clone( ) ) ,
1573+ Field :: new( "right_unused" , DataType :: Int32 , false ) ,
1574+ ] ) ) ;
1575+ let left: Arc < dyn ExecutionPlan > =
1576+ Arc :: new ( EmptyExec :: new ( Arc :: clone ( & left_schema) ) ) ;
1577+ let right: Arc < dyn ExecutionPlan > =
1578+ Arc :: new ( EmptyExec :: new ( Arc :: clone ( & right_schema) ) ) ;
1579+ let join_schema = Arc :: new ( Schema :: new ( vec ! [
1580+ left_schema. field( 0 ) . clone( ) ,
1581+ left_schema. field( 1 ) . clone( ) ,
1582+ right_schema. field( 0 ) . clone( ) ,
1583+ right_schema. field( 1 ) . clone( ) ,
1584+ ] ) ) ;
1585+ let join: Arc < dyn ExecutionPlan > =
1586+ Arc :: new ( EmptyExec :: new ( Arc :: clone ( & join_schema) ) ) ;
1587+ let projected_schema = Schema :: new ( vec ! [
1588+ Field :: new( "left_keep" , DataType :: Int32 , false ) ,
1589+ Field :: new( "right_keep" , DataType :: Int32 , false ) ,
1590+ ] ) ;
1591+ let projection = ProjectionExec :: try_new_with_schema_metadata (
1592+ [
1593+ ProjectionExpr {
1594+ expr : Arc :: new ( Column :: new ( "left_keep" , 0 ) ) ,
1595+ alias : "left_keep" . to_string ( ) ,
1596+ } ,
1597+ ProjectionExpr {
1598+ expr : Arc :: new ( Column :: new ( "right_keep" , 2 ) ) ,
1599+ alias : "right_keep" . to_string ( ) ,
1600+ } ,
1601+ ] ,
1602+ join,
1603+ & projected_schema,
1604+ ) ?;
1605+ let column_indices = [
1606+ ColumnIndex {
1607+ index : 0 ,
1608+ side : JoinSide :: Left ,
1609+ } ,
1610+ ColumnIndex {
1611+ index : 1 ,
1612+ side : JoinSide :: Left ,
1613+ } ,
1614+ ColumnIndex {
1615+ index : 0 ,
1616+ side : JoinSide :: Right ,
1617+ } ,
1618+ ColumnIndex {
1619+ index : 1 ,
1620+ side : JoinSide :: Right ,
1621+ } ,
1622+ ] ;
1623+
1624+ let pushed = try_pushdown_through_join_with_column_indices (
1625+ & projection,
1626+ & left,
1627+ & right,
1628+ & [ ] ,
1629+ & join_schema,
1630+ None ,
1631+ & column_indices,
1632+ ) ?
1633+ . expect ( "projection should be pushed through the join" ) ;
1634+
1635+ assert ! (
1636+ pushed
1637+ . projected_left_child
1638+ . schema( )
1639+ . field( 0 )
1640+ . metadata( )
1641+ . is_empty( )
1642+ ) ;
1643+ assert ! (
1644+ pushed
1645+ . projected_right_child
1646+ . schema( )
1647+ . field( 0 )
1648+ . metadata( )
1649+ . is_empty( )
1650+ ) ;
1651+ Ok ( ( ) )
1652+ }
1653+
14941654 #[ test]
14951655 fn test_collect_column_indices ( ) -> Result < ( ) > {
14961656 let expr = Arc :: new ( BinaryExpr :: new (
0 commit comments