Skip to content

Commit 0a8252c

Browse files
committed
Preserve metadata-changing projections
1 parent d9f1b2a commit 0a8252c

3 files changed

Lines changed: 181 additions & 19 deletions

File tree

datafusion/physical-plan/src/joins/cross_join.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,7 @@ impl ExecutionPlan for CrossJoinExec {
452452

453453
let (new_left, new_right) = new_join_children(
454454
&projection_as_columns,
455+
projection.schema().as_ref(),
455456
far_right_left_col_ind,
456457
far_left_right_col_ind,
457458
self.left(),

datafusion/physical-plan/src/joins/sort_merge_join/exec.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,7 @@ impl ExecutionPlan for SortMergeJoinExec {
662662

663663
let (new_left, new_right) = new_join_children(
664664
&projection_as_columns,
665+
projection.schema().as_ref(),
665666
far_right_left_col_ind,
666667
far_left_right_col_ind,
667668
self.children()[0],

datafusion/physical-plan/src/projection.rs

Lines changed: 179 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
10731094
pub 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(
11161146
fn 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

Comments
 (0)