Skip to content

Commit 549b436

Browse files
committed
Preserve metadata-changing projections
1 parent d9f1b2a commit 549b436

3 files changed

Lines changed: 241 additions & 21 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: 239 additions & 21 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

@@ -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

Comments
 (0)