Skip to content

Commit 9c14234

Browse files
committed
fix: preserve projection field metadata during physical planning
1 parent 1955d5a commit 9c14234

3 files changed

Lines changed: 131 additions & 4 deletions

File tree

datafusion/core/src/physical_planner.rs

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,6 +1239,7 @@ impl DefaultPhysicalPlanner {
12391239
children.one()?,
12401240
input,
12411241
expr,
1242+
node.schema(),
12421243
)?,
12431244
LogicalPlan::Filter(Filter {
12441245
predicate, input, ..
@@ -1462,6 +1463,7 @@ impl DefaultPhysicalPlanner {
14621463
physical_left,
14631464
input,
14641465
expr,
1466+
left.schema(),
14651467
)?,
14661468
_ => physical_left,
14671469
};
@@ -1476,6 +1478,7 @@ impl DefaultPhysicalPlanner {
14761478
physical_right,
14771479
input,
14781480
expr,
1481+
right.schema(),
14791482
)?,
14801483
_ => physical_right,
14811484
};
@@ -1860,6 +1863,7 @@ impl DefaultPhysicalPlanner {
18601863
join,
18611864
input,
18621865
expr,
1866+
new_logical.schema(),
18631867
)?
18641868
} else {
18651869
join
@@ -3088,6 +3092,7 @@ impl DefaultPhysicalPlanner {
30883092
input_exec: Arc<dyn ExecutionPlan>,
30893093
input: &Arc<LogicalPlan>,
30903094
expr: &[Expr],
3095+
output_schema: &DFSchema,
30913096
) -> Result<Arc<dyn ExecutionPlan>> {
30923097
let input_logical_schema = input.as_ref().schema();
30933098
let input_physical_schema = input_exec.schema();
@@ -3145,7 +3150,11 @@ impl DefaultPhysicalPlanner {
31453150
.into_iter()
31463151
.map(|(expr, alias)| ProjectionExpr { expr, alias })
31473152
.collect();
3148-
Ok(Arc::new(ProjectionExec::try_new(proj_exprs, input_exec)?))
3153+
Ok(Arc::new(ProjectionExec::try_new_with_schema_metadata(
3154+
proj_exprs,
3155+
input_exec,
3156+
output_schema.as_arrow(),
3157+
)?))
31493158
}
31503159
PlanAsyncExpr::Async(
31513160
async_map,
@@ -3157,8 +3166,11 @@ impl DefaultPhysicalPlanner {
31573166
.into_iter()
31583167
.map(|(expr, alias)| ProjectionExpr { expr, alias })
31593168
.collect();
3160-
let new_proj_exec =
3161-
ProjectionExec::try_new(proj_exprs, Arc::new(async_exec))?;
3169+
let new_proj_exec = ProjectionExec::try_new_with_schema_metadata(
3170+
proj_exprs,
3171+
Arc::new(async_exec),
3172+
output_schema.as_arrow(),
3173+
)?;
31623174
Ok(Arc::new(new_proj_exec))
31633175
}
31643176
_ => internal_err!("Unexpected PlanAsyncExpressions variant"),
@@ -3505,6 +3517,43 @@ mod tests {
35053517
Ok(())
35063518
}
35073519

3520+
#[tokio::test]
3521+
async fn test_projection_preserves_field_metadata_for_aggregate() -> Result<()> {
3522+
use datafusion_common::metadata::FieldMetadata;
3523+
use datafusion_expr::expr::AggregateFunction;
3524+
use datafusion_functions_aggregate::min_max::max_udaf;
3525+
3526+
let schema = Schema::new(vec![Field::new("value", DataType::Utf8, false)]);
3527+
let input = LogicalPlan::EmptyRelation(EmptyRelation {
3528+
produce_one_row: false,
3529+
schema: Arc::new(schema.to_dfschema()?),
3530+
});
3531+
let metadata =
3532+
FieldMetadata::from(HashMap::from([("foo".to_string(), "bar".to_string())]));
3533+
let projection = LogicalPlan::Projection(Projection::try_new(
3534+
vec![col("value").alias_with_metadata("value", Some(metadata))],
3535+
Arc::new(input),
3536+
)?);
3537+
let aggregate = LogicalPlan::Aggregate(Aggregate::try_new(
3538+
Arc::new(projection),
3539+
vec![],
3540+
vec![Expr::AggregateFunction(AggregateFunction::new_udf(
3541+
max_udaf(),
3542+
vec![col("value")],
3543+
false,
3544+
None,
3545+
vec![],
3546+
None,
3547+
))],
3548+
)?);
3549+
3550+
DefaultPhysicalPlanner::default()
3551+
.create_physical_plan(&aggregate, &SessionContext::new().state())
3552+
.await?;
3553+
3554+
Ok(())
3555+
}
3556+
35083557
#[derive(Debug, Default)]
35093558
struct NullAccumulator;
35103559

datafusion/physical-expr/src/projection.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,56 @@ impl ProjectionExprs {
539539
})
540540
}
541541

542+
/// Create a new [`Projector`] using field and schema metadata from
543+
/// `projected_schema`.
544+
///
545+
/// Field names, data types, and nullability are still derived from the physical
546+
/// projection expressions and `input_schema`; only field and schema metadata are
547+
/// taken from `projected_schema`.
548+
///
549+
/// # Errors
550+
///
551+
/// Returns an error if the projection cannot be applied to `input_schema`, or if
552+
/// `projected_schema` has a different number of fields than the projection.
553+
pub fn make_projector_with_schema_metadata(
554+
&self,
555+
input_schema: &Schema,
556+
projected_schema: &Schema,
557+
) -> Result<Projector> {
558+
let output_schema = self.project_schema(input_schema)?;
559+
if output_schema.fields().len() != projected_schema.fields().len() {
560+
return Err(internal_datafusion_err!(
561+
"Projection has {} output fields but metadata schema has {} fields",
562+
output_schema.fields().len(),
563+
projected_schema.fields().len()
564+
));
565+
}
566+
567+
let fields = output_schema
568+
.fields()
569+
.iter()
570+
.zip(projected_schema.fields())
571+
.map(|(field, projected_field)| {
572+
Arc::new(
573+
field
574+
.as_ref()
575+
.clone()
576+
.with_metadata(projected_field.metadata().clone()),
577+
)
578+
})
579+
.collect::<Vec<_>>();
580+
let output_schema = Arc::new(Schema::new_with_metadata(
581+
fields,
582+
projected_schema.metadata().clone(),
583+
));
584+
585+
Ok(Projector {
586+
projection: self.clone(),
587+
output_schema,
588+
expression_metrics: None,
589+
})
590+
}
591+
542592
pub fn create_expression_metrics(
543593
&self,
544594
metrics: &ExecutionPlanMetricsSet,

datafusion/physical-plan/src/projection.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ use std::pin::Pin;
4040
use std::sync::Arc;
4141
use std::task::{Context, Poll};
4242

43-
use arrow::datatypes::SchemaRef;
43+
use arrow::datatypes::{Schema, SchemaRef};
4444
use arrow::record_batch::RecordBatch;
4545
use datafusion_common::config::ConfigOptions;
4646
use datafusion_common::tree_node::{
@@ -143,6 +143,34 @@ impl ProjectionExec {
143143
Self::try_from_projector(projector, input)
144144
}
145145

146+
/// Create a projection using field and schema metadata from
147+
/// `projected_schema`.
148+
///
149+
/// Field names, data types, and nullability are still derived from the physical
150+
/// projection expressions and the input plan; only field and schema metadata are
151+
/// taken from `projected_schema`.
152+
///
153+
/// # Errors
154+
///
155+
/// Returns an error if the projection cannot be applied to the input plan, or if
156+
/// `projected_schema` has a different number of fields than the projection.
157+
pub fn try_new_with_schema_metadata<I, E>(
158+
expr: I,
159+
input: Arc<dyn ExecutionPlan>,
160+
projected_schema: &Schema,
161+
) -> Result<Self>
162+
where
163+
I: IntoIterator<Item = E>,
164+
E: Into<ProjectionExpr>,
165+
{
166+
let input_schema = input.schema();
167+
let expr_arc = expr.into_iter().map(Into::into).collect::<Arc<_>>();
168+
let projection = ProjectionExprs::from_expressions(expr_arc);
169+
let projector = projection
170+
.make_projector_with_schema_metadata(&input_schema, projected_schema)?;
171+
Self::try_from_projector(projector, input)
172+
}
173+
146174
fn try_from_projector(
147175
projector: Projector,
148176
input: Arc<dyn ExecutionPlan>,

0 commit comments

Comments
 (0)