@@ -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
@@ -1313,8 +1347,32 @@ fn try_collapse_projection_chain(
13131347 }
13141348
13151349 // To unify 3 or more sequential projections:
1316- let unified: Arc < dyn ExecutionPlan > =
1317- Arc :: new ( ProjectionExec :: try_new ( current_exprs, current_input) ?) ;
1350+ let unified_projection = ProjectionExec :: try_new ( current_exprs, current_input) ?;
1351+ let metadata_fields = unified_projection
1352+ . schema ( )
1353+ . fields ( )
1354+ . iter ( )
1355+ . zip ( outer. expr ( ) )
1356+ . zip ( outer. schema ( ) . fields ( ) )
1357+ . map ( |( ( unified_field, outer_expr) , outer_field) | {
1358+ if outer_expr. expr . is :: < Column > ( ) {
1359+ unified_field
1360+ . as_ref ( )
1361+ . clone ( )
1362+ . with_metadata ( outer_field. metadata ( ) . clone ( ) )
1363+ } else {
1364+ unified_field. as_ref ( ) . clone ( )
1365+ }
1366+ } )
1367+ . collect :: < Vec < _ > > ( ) ;
1368+ let metadata_schema =
1369+ Schema :: new_with_metadata ( metadata_fields, outer. schema ( ) . metadata ( ) . clone ( ) ) ;
1370+ let unified_projection = ProjectionExec :: try_new_with_schema_metadata (
1371+ unified_projection. expr ( ) . to_vec ( ) ,
1372+ Arc :: clone ( unified_projection. input ( ) ) ,
1373+ & metadata_schema,
1374+ ) ?;
1375+ let unified: Arc < dyn ExecutionPlan > = Arc :: new ( unified_projection) ;
13181376 remove_unnecessary_projections ( unified) . data ( ) . map ( Some )
13191377}
13201378
@@ -1441,6 +1499,7 @@ mod tests {
14411499 use crate :: statistics:: { StatisticsArgs , StatisticsContext } ;
14421500 use crate :: test;
14431501 use crate :: test:: exec:: StatisticsExec ;
1502+ use crate :: union:: UnionExec ;
14441503
14451504 use arrow:: datatypes:: { DataType , Field , Schema } ;
14461505 use datafusion_common:: ScalarValue ;
@@ -1491,6 +1550,165 @@ mod tests {
14911550 Ok ( ( ) )
14921551 }
14931552
1553+ #[ test]
1554+ fn test_projection_pushdown_preserves_output_metadata ( ) -> Result < ( ) > {
1555+ let input_schema = Arc :: new ( Schema :: new ( vec ! [
1556+ Field :: new( "input" , DataType :: Int32 , false ) . with_metadata( HashMap :: from( [ (
1557+ "source" . to_string( ) ,
1558+ "input" . to_string( ) ,
1559+ ) ] ) ) ,
1560+ Field :: new( "unused" , DataType :: Int32 , false ) ,
1561+ ] ) ) ;
1562+ let input: Arc < dyn ExecutionPlan > = UnionExec :: try_new ( vec ! [
1563+ Arc :: new( EmptyExec :: new( Arc :: clone( & input_schema) ) ) ,
1564+ Arc :: new( EmptyExec :: new( input_schema) ) ,
1565+ ] ) ?;
1566+ let projected_schema =
1567+ Schema :: new ( vec ! [ Field :: new( "input" , DataType :: Int32 , false ) ] ) ;
1568+ let projection = ProjectionExec :: try_new_with_schema_metadata (
1569+ [ ProjectionExpr {
1570+ expr : Arc :: new ( Column :: new ( "input" , 0 ) ) ,
1571+ alias : "input" . to_string ( ) ,
1572+ } ] ,
1573+ input,
1574+ & projected_schema,
1575+ ) ?;
1576+
1577+ assert ! ( !is_projection_removable( & projection) ) ;
1578+ let plan: Arc < dyn ExecutionPlan > = Arc :: new ( projection) ;
1579+ let optimized = remove_unnecessary_projections ( Arc :: clone ( & plan) ) ?;
1580+ assert ! ( optimized. transformed) ;
1581+ assert_eq ! ( optimized. data. schema( ) , plan. schema( ) ) ;
1582+ Ok ( ( ) )
1583+ }
1584+
1585+ #[ test]
1586+ fn test_projection_chain_does_not_restore_stripped_column_metadata ( ) -> Result < ( ) > {
1587+ let source_metadata =
1588+ HashMap :: from ( [ ( "PARQUET:field_id" . to_string ( ) , "6" . to_string ( ) ) ] ) ;
1589+ let input_schema = Arc :: new ( Schema :: new ( vec ! [
1590+ Field :: new( "sales" , DataType :: Int64 , true ) . with_metadata( source_metadata) ,
1591+ ] ) ) ;
1592+ let input: Arc < dyn ExecutionPlan > = Arc :: new ( EmptyExec :: new ( input_schema) ) ;
1593+ let inner: Arc < dyn ExecutionPlan > = Arc :: new ( ProjectionExec :: try_new (
1594+ [ ProjectionExpr {
1595+ expr : Arc :: new ( Column :: new ( "sales" , 0 ) ) ,
1596+ alias : "inner_sales" . to_string ( ) ,
1597+ } ] ,
1598+ input,
1599+ ) ?) ;
1600+ let projected_schema =
1601+ Schema :: new ( vec ! [ Field :: new( "sales" , DataType :: Int64 , true ) ] ) ;
1602+ let outer = ProjectionExec :: try_new_with_schema_metadata (
1603+ [ ProjectionExpr {
1604+ expr : Arc :: new ( Column :: new ( "inner_sales" , 0 ) ) ,
1605+ alias : "sales" . to_string ( ) ,
1606+ } ] ,
1607+ inner,
1608+ & projected_schema,
1609+ ) ?;
1610+
1611+ let Some ( collapsed) = try_collapse_projection_chain ( & outer) ? else {
1612+ return internal_err ! ( "projection chain should collapse" ) ;
1613+ } ;
1614+ assert_eq ! ( collapsed. schema( ) , outer. schema( ) ) ;
1615+ assert ! ( collapsed. schema( ) . field( 0 ) . metadata( ) . is_empty( ) ) ;
1616+ Ok ( ( ) )
1617+ }
1618+
1619+ #[ test]
1620+ fn test_join_projection_pushdown_preserves_output_metadata ( ) -> Result < ( ) > {
1621+ let source_metadata =
1622+ HashMap :: from ( [ ( "source" . to_string ( ) , "iceberg" . to_string ( ) ) ] ) ;
1623+ let left_schema = Arc :: new ( Schema :: new ( vec ! [
1624+ Field :: new( "left_keep" , DataType :: Int32 , false )
1625+ . with_metadata( source_metadata. clone( ) ) ,
1626+ Field :: new( "left_unused" , DataType :: Int32 , false ) ,
1627+ ] ) ) ;
1628+ let right_schema = Arc :: new ( Schema :: new ( vec ! [
1629+ Field :: new( "right_keep" , DataType :: Int32 , false )
1630+ . with_metadata( source_metadata. clone( ) ) ,
1631+ Field :: new( "right_unused" , DataType :: Int32 , false ) ,
1632+ ] ) ) ;
1633+ let left: Arc < dyn ExecutionPlan > =
1634+ Arc :: new ( EmptyExec :: new ( Arc :: clone ( & left_schema) ) ) ;
1635+ let right: Arc < dyn ExecutionPlan > =
1636+ Arc :: new ( EmptyExec :: new ( Arc :: clone ( & right_schema) ) ) ;
1637+ let join_schema = Arc :: new ( Schema :: new ( vec ! [
1638+ left_schema. field( 0 ) . clone( ) ,
1639+ left_schema. field( 1 ) . clone( ) ,
1640+ right_schema. field( 0 ) . clone( ) ,
1641+ right_schema. field( 1 ) . clone( ) ,
1642+ ] ) ) ;
1643+ let join: Arc < dyn ExecutionPlan > =
1644+ Arc :: new ( EmptyExec :: new ( Arc :: clone ( & join_schema) ) ) ;
1645+ let projected_schema = Schema :: new ( vec ! [
1646+ Field :: new( "left_keep" , DataType :: Int32 , false ) ,
1647+ Field :: new( "right_keep" , DataType :: Int32 , false ) ,
1648+ ] ) ;
1649+ let projection = ProjectionExec :: try_new_with_schema_metadata (
1650+ [
1651+ ProjectionExpr {
1652+ expr : Arc :: new ( Column :: new ( "left_keep" , 0 ) ) ,
1653+ alias : "left_keep" . to_string ( ) ,
1654+ } ,
1655+ ProjectionExpr {
1656+ expr : Arc :: new ( Column :: new ( "right_keep" , 2 ) ) ,
1657+ alias : "right_keep" . to_string ( ) ,
1658+ } ,
1659+ ] ,
1660+ join,
1661+ & projected_schema,
1662+ ) ?;
1663+ let column_indices = [
1664+ ColumnIndex {
1665+ index : 0 ,
1666+ side : JoinSide :: Left ,
1667+ } ,
1668+ ColumnIndex {
1669+ index : 1 ,
1670+ side : JoinSide :: Left ,
1671+ } ,
1672+ ColumnIndex {
1673+ index : 0 ,
1674+ side : JoinSide :: Right ,
1675+ } ,
1676+ ColumnIndex {
1677+ index : 1 ,
1678+ side : JoinSide :: Right ,
1679+ } ,
1680+ ] ;
1681+
1682+ let pushed = try_pushdown_through_join_with_column_indices (
1683+ & projection,
1684+ & left,
1685+ & right,
1686+ & [ ] ,
1687+ & join_schema,
1688+ None ,
1689+ & column_indices,
1690+ ) ?
1691+ . expect ( "projection should be pushed through the join" ) ;
1692+
1693+ assert ! (
1694+ pushed
1695+ . projected_left_child
1696+ . schema( )
1697+ . field( 0 )
1698+ . metadata( )
1699+ . is_empty( )
1700+ ) ;
1701+ assert ! (
1702+ pushed
1703+ . projected_right_child
1704+ . schema( )
1705+ . field( 0 )
1706+ . metadata( )
1707+ . is_empty( )
1708+ ) ;
1709+ Ok ( ( ) )
1710+ }
1711+
14941712 #[ test]
14951713 fn test_collect_column_indices ( ) -> Result < ( ) > {
14961714 let expr = Arc :: new ( BinaryExpr :: new (
0 commit comments