@@ -1014,6 +1014,11 @@ pub fn remove_unnecessary_projections(
10141014 plan : Arc < dyn ExecutionPlan > ,
10151015) -> Result < Transformed < Arc < dyn ExecutionPlan > > > {
10161016 let maybe_modified = if let Some ( projection) = plan. downcast_ref :: < ProjectionExec > ( ) {
1017+ // Metadata is observable by physical expressions. Moving a projection that
1018+ // defines metadata can therefore change query results.
1019+ if projection_overrides_metadata ( projection) ? {
1020+ return Ok ( Transformed :: no ( plan) ) ;
1021+ }
10171022 // If the projection does not cause any change on the input, we can
10181023 // safely remove it:
10191024 if is_projection_removable ( projection) {
@@ -1029,10 +1034,12 @@ pub fn remove_unnecessary_projections(
10291034 Ok ( maybe_modified. map_or_else ( || Transformed :: no ( plan) , Transformed :: yes) )
10301035}
10311036
1032- /// Compare the inputs and outputs of the projection. All expressions must be
1033- /// columns without alias, and projection does not change the order of fields.
1034- /// For example, if the input schema is `a, b`, `SELECT a, b` is removable,
1035- /// but `SELECT b, a` and `SELECT a+1, b` and `SELECT a AS c, b` are not.
1037+ /// Compare the input and output of a projection. A removable projection contains
1038+ /// only unaliased columns in input order and preserves the exact schema, including
1039+ /// field and schema metadata.
1040+ ///
1041+ /// For example, if the input schema is `a, b`, `SELECT a, b` is removable, but
1042+ /// `SELECT b, a`, `SELECT a + 1, b`, and a metadata-only projection are not.
10361043fn is_projection_removable ( projection : & ProjectionExec ) -> bool {
10371044 let exprs = projection. expr ( ) ;
10381045 exprs. iter ( ) . enumerate ( ) . all ( |( idx, proj_expr) | {
@@ -1041,6 +1048,7 @@ fn is_projection_removable(projection: &ProjectionExec) -> bool {
10411048 } ;
10421049 col. name ( ) == proj_expr. alias && col. index ( ) == idx
10431050 } ) && exprs. len ( ) == projection. input ( ) . schema ( ) . fields ( ) . len ( )
1051+ && projection. schema ( ) == projection. input ( ) . schema ( )
10441052}
10451053
10461054/// Given the expression set of a projection, checks if the projection causes
@@ -1073,14 +1081,20 @@ pub fn new_projections_for_columns(
10731081 . collect ( )
10741082}
10751083
1076- /// Creates a new [`ProjectionExec`] instance with the given child plan and
1077- /// projected expressions.
1084+ /// Creates an equivalent [`ProjectionExec`] with a new child.
1085+ ///
1086+ /// The original output metadata is preserved because a parent expression may
1087+ /// observe it; recomputing metadata from `child` could change query results.
10781088pub fn make_with_child (
10791089 projection : & ProjectionExec ,
10801090 child : & Arc < dyn ExecutionPlan > ,
10811091) -> Result < Arc < dyn ExecutionPlan > > {
1082- ProjectionExec :: try_new ( projection. expr ( ) . to_vec ( ) , Arc :: clone ( child) )
1083- . map ( |e| Arc :: new ( e) as _ )
1092+ ProjectionExec :: try_new_with_schema_metadata (
1093+ projection. expr ( ) . to_vec ( ) ,
1094+ Arc :: clone ( child) ,
1095+ projection. schema ( ) . as_ref ( ) ,
1096+ )
1097+ . map ( |e| Arc :: new ( e) as _ )
10841098}
10851099
10861100/// Returns `true` if all the expressions in the argument are `Column`s.
@@ -1326,17 +1340,44 @@ pub fn update_join_filter(
13261340 } )
13271341}
13281342
1343+ /// Returns whether a projection defines metadata that its expressions and input
1344+ /// schema cannot reproduce.
1345+ ///
1346+ /// Such a projection is an execution boundary: a parent expression such as
1347+ /// `arrow_metadata` can observe its output field metadata.
1348+ fn projection_overrides_metadata ( projection : & ProjectionExec ) -> Result < bool > {
1349+ let derived_schema = projection
1350+ . projector
1351+ . projection ( )
1352+ . project_schema ( projection. input ( ) . schema ( ) . as_ref ( ) ) ?;
1353+ let output_schema = projection. schema ( ) ;
1354+ Ok ( derived_schema. metadata ( ) != output_schema. metadata ( )
1355+ || derived_schema
1356+ . fields ( )
1357+ . iter ( )
1358+ . zip ( output_schema. fields ( ) )
1359+ . any ( |( derived, output) | derived. metadata ( ) != output. metadata ( ) ) )
1360+ }
1361+
13291362/// Collapse a chain of consecutive [`ProjectionExec`]s into one. Returns
13301363/// `None` if nothing could be merged.
13311364fn try_collapse_projection_chain (
13321365 outer : & ProjectionExec ,
13331366) -> Result < Option < Arc < dyn ExecutionPlan > > > {
1367+ if projection_overrides_metadata ( outer) ? {
1368+ return Ok ( None ) ;
1369+ }
1370+
13341371 let mut current_exprs: Vec < ProjectionExpr > = outer. expr ( ) . to_vec ( ) ;
13351372 let mut current_input: Arc < dyn ExecutionPlan > = Arc :: clone ( outer. input ( ) ) ;
13361373 let mut column_ref_map: HashMap < Column , usize > = HashMap :: new ( ) ;
13371374 let mut collapsed_any = false ;
13381375
13391376 ' outer: while let Some ( inner_proj) = current_input. downcast_ref :: < ProjectionExec > ( ) {
1377+ if projection_overrides_metadata ( inner_proj) ? {
1378+ break ;
1379+ }
1380+
13401381 // Collect the column references usage in the outer projection.
13411382 column_ref_map. clear ( ) ;
13421383 for proj_expr in & current_exprs {
@@ -1385,9 +1426,14 @@ fn try_collapse_projection_chain(
13851426 return Ok ( None ) ;
13861427 }
13871428
1388- // To unify 3 or more sequential projections:
1429+ // Expression substitution must not change the outer projection's output
1430+ // metadata contract.
13891431 let unified: Arc < dyn ExecutionPlan > =
1390- Arc :: new ( ProjectionExec :: try_new ( current_exprs, current_input) ?) ;
1432+ Arc :: new ( ProjectionExec :: try_new_with_schema_metadata (
1433+ current_exprs,
1434+ current_input,
1435+ outer. schema ( ) . as_ref ( ) ,
1436+ ) ?) ;
13911437 remove_unnecessary_projections ( unified) . data ( ) . map ( Some )
13921438}
13931439
@@ -1517,11 +1563,14 @@ mod tests {
15171563 use crate :: test;
15181564 use crate :: test:: exec:: StatisticsExec ;
15191565
1566+ use arrow:: array:: StringArray ;
15201567 use arrow:: datatypes:: { DataType , Field , Schema } ;
15211568 use datafusion_common:: ScalarValue ;
15221569 use datafusion_common:: stats:: { ColumnStatistics , Precision , Statistics } ;
15231570
1524- use datafusion_expr:: Operator ;
1571+ use datafusion_expr:: { Operator , ScalarUDF } ;
1572+ use datafusion_functions:: core:: arrow_metadata:: ArrowMetadataFunc ;
1573+ use datafusion_physical_expr:: ScalarFunctionExpr ;
15251574 use datafusion_physical_expr:: expressions:: {
15261575 BinaryExpr , Column , DynamicFilterPhysicalExpr , Literal , binary, col, lit,
15271576 } ;
@@ -1566,6 +1615,91 @@ mod tests {
15661615 Ok ( ( ) )
15671616 }
15681617
1618+ fn identity_projection_with_metadata (
1619+ input : Arc < dyn ExecutionPlan > ,
1620+ ) -> Result < Arc < dyn ExecutionPlan > > {
1621+ let metadata_schema =
1622+ Schema :: new_with_metadata (
1623+ vec ! [ Field :: new( "i" , DataType :: Int32 , true ) . with_metadata(
1624+ HashMap :: from( [ ( "event_field" . to_string( ) , "true" . to_string( ) ) ] ) ,
1625+ ) ] ,
1626+ HashMap :: from ( [ ( "schema-key" . to_string ( ) , "schema-value" . to_string ( ) ) ] ) ,
1627+ ) ;
1628+ Ok ( Arc :: new ( ProjectionExec :: try_new_with_schema_metadata (
1629+ [ ProjectionExpr {
1630+ expr : Arc :: new ( Column :: new ( "i" , 0 ) ) ,
1631+ alias : "i" . to_string ( ) ,
1632+ } ] ,
1633+ input,
1634+ & metadata_schema,
1635+ ) ?) )
1636+ }
1637+
1638+ #[ test]
1639+ fn test_metadata_projection_is_not_removable ( ) -> Result < ( ) > {
1640+ let projection = identity_projection_with_metadata ( test:: scan_partitioned ( 1 ) ) ?;
1641+ let expected_schema = projection. schema ( ) ;
1642+
1643+ let optimized = remove_unnecessary_projections ( projection) ?. data ;
1644+
1645+ assert ! ( optimized. downcast_ref:: <ProjectionExec >( ) . is_some( ) ) ;
1646+ assert_eq ! ( optimized. schema( ) , expected_schema) ;
1647+ Ok ( ( ) )
1648+ }
1649+
1650+ #[ test]
1651+ fn test_make_with_child_preserves_output_metadata ( ) -> Result < ( ) > {
1652+ let projection = identity_projection_with_metadata ( test:: scan_partitioned ( 1 ) ) ?;
1653+ let projection = projection
1654+ . downcast_ref :: < ProjectionExec > ( )
1655+ . expect ( "test plan should be a ProjectionExec" ) ;
1656+
1657+ let rebuilt = make_with_child ( projection, & test:: scan_partitioned ( 1 ) ) ?;
1658+
1659+ assert_eq ! ( rebuilt. schema( ) , projection. schema( ) ) ;
1660+ Ok ( ( ) )
1661+ }
1662+
1663+ #[ tokio:: test]
1664+ async fn test_metadata_observing_parent_blocks_projection_collapse ( ) -> Result < ( ) > {
1665+ let inner = identity_projection_with_metadata ( test:: scan_partitioned ( 1 ) ) ?;
1666+ let arrow_metadata = ScalarFunctionExpr :: new (
1667+ "arrow_metadata" ,
1668+ Arc :: new ( ScalarUDF :: new_from_impl ( ArrowMetadataFunc :: new ( ) ) ) ,
1669+ vec ! [
1670+ Arc :: new( Column :: new( "i" , 0 ) ) ,
1671+ Arc :: new( Literal :: new( ScalarValue :: Utf8 ( Some (
1672+ "event_field" . to_string( ) ,
1673+ ) ) ) ) ,
1674+ ] ,
1675+ Arc :: new ( Field :: new ( "arrow_metadata" , DataType :: Utf8 , true ) ) ,
1676+ Arc :: new ( ConfigOptions :: default ( ) ) ,
1677+ ) ;
1678+ let outer: Arc < dyn ExecutionPlan > = Arc :: new ( ProjectionExec :: try_new (
1679+ [ ProjectionExpr {
1680+ expr : Arc :: new ( arrow_metadata) ,
1681+ alias : "metadata" . to_string ( ) ,
1682+ } ] ,
1683+ inner,
1684+ ) ?) ;
1685+
1686+ let outer_projection = outer
1687+ . downcast_ref :: < ProjectionExec > ( )
1688+ . expect ( "test plan should be a ProjectionExec" ) ;
1689+ assert ! ( try_collapse_projection_chain( outer_projection) ?. is_none( ) ) ;
1690+
1691+ let optimized = remove_unnecessary_projections ( outer) ?. data ;
1692+ let batches =
1693+ collect ( optimized. execute ( 0 , Arc :: new ( TaskContext :: default ( ) ) ) ?) . await ?;
1694+ let values = batches[ 0 ]
1695+ . column ( 0 )
1696+ . as_any ( )
1697+ . downcast_ref :: < StringArray > ( )
1698+ . expect ( "metadata expression should return Utf8" ) ;
1699+ assert_eq ! ( values. value( 0 ) , "true" ) ;
1700+ Ok ( ( ) )
1701+ }
1702+
15691703 #[ test]
15701704 fn test_collect_column_indices ( ) -> Result < ( ) > {
15711705 let expr = Arc :: new ( BinaryExpr :: new (
0 commit comments