@@ -83,6 +83,8 @@ pub struct ProjectionExec {
8383 metrics : ExecutionPlanMetricsSet ,
8484 /// Cache holding plan properties like equivalences, output partitioning etc.
8585 cache : Arc < PlanProperties > ,
86+ /// Complete group-contiguous tuple projected from the input, if any.
87+ group_contiguous_exprs : Vec < Arc < dyn PhysicalExpr > > ,
8688}
8789
8890impl ProjectionExec {
@@ -208,6 +210,8 @@ impl ProjectionExec {
208210 // Construct a map from the input expressions to the output expression of the Projection
209211 let projection_mapping =
210212 projector. projection ( ) . projection_mapping ( & input. schema ( ) ) ?;
213+ let group_contiguous_exprs =
214+ Self :: project_group_contiguous_exprs ( & input, & projection_mapping) ;
211215 let cache = Self :: compute_properties (
212216 & input,
213217 & projection_mapping,
@@ -219,9 +223,26 @@ impl ProjectionExec {
219223 input,
220224 metrics : ExecutionPlanMetricsSet :: new ( ) ,
221225 cache : Arc :: new ( cache) ,
226+ group_contiguous_exprs,
222227 } )
223228 }
224229
230+ /// Projects the complete group-contiguous tuple, dropping it when any
231+ /// component cannot be mapped to the output schema.
232+ fn project_group_contiguous_exprs (
233+ input : & Arc < dyn ExecutionPlan > ,
234+ projection_mapping : & ProjectionMapping ,
235+ ) -> Vec < Arc < dyn PhysicalExpr > > {
236+ input
237+ . equivalence_properties ( )
238+ . project_expressions (
239+ input. group_contiguous_exprs ( ) . iter ( ) ,
240+ projection_mapping,
241+ )
242+ . collect :: < Option < Vec < _ > > > ( )
243+ . unwrap_or_default ( )
244+ }
245+
225246 /// The projection expressions stored as tuples of (expression, output column name)
226247 pub fn expr ( & self ) -> & [ ProjectionExpr ] {
227248 self . projector . projection ( ) . as_ref ( )
@@ -345,6 +366,10 @@ impl ExecutionPlan for ProjectionExec {
345366 & self . cache
346367 }
347368
369+ fn group_contiguous_exprs ( & self ) -> & [ Arc < dyn PhysicalExpr > ] {
370+ & self . group_contiguous_exprs
371+ }
372+
348373 fn maintains_input_order ( & self ) -> Vec < bool > {
349374 // Tell optimizer this operator doesn't reorder its input
350375 vec ! [ true ]
@@ -386,11 +411,21 @@ impl ExecutionPlan for ProjectionExec {
386411 ) -> Result < Arc < dyn ExecutionPlan > > {
387412 validate_child_count ! ( self , children) ;
388413 match options. children_properties {
389- ChildrenPropertiesMode :: Keep => Ok ( Arc :: new ( Self {
390- input : children. swap_remove ( 0 ) ,
391- metrics : ExecutionPlanMetricsSet :: new ( ) ,
392- ..Self :: clone ( & * self )
393- } ) ) ,
414+ ChildrenPropertiesMode :: Keep => {
415+ let input = children. swap_remove ( 0 ) ;
416+ let projection_mapping = self
417+ . projector
418+ . projection ( )
419+ . projection_mapping ( & input. schema ( ) ) ?;
420+ let group_contiguous_exprs =
421+ Self :: project_group_contiguous_exprs ( & input, & projection_mapping) ;
422+ Ok ( Arc :: new ( Self {
423+ input,
424+ metrics : ExecutionPlanMetricsSet :: new ( ) ,
425+ group_contiguous_exprs,
426+ ..Self :: clone ( & * self )
427+ } ) )
428+ }
394429 ChildrenPropertiesMode :: Recompute => {
395430 // `Keep` above requires the child's properties to be unchanged
396431 // outright. A rule that introduces a sort below this projection
@@ -631,6 +666,8 @@ impl ExecutionPlan for ProjectionExec {
631666 metrics : _,
632667 // Derived plan properties, recomputed on decode.
633668 cache : _,
669+ // Derived from the input assertion and projection expressions.
670+ group_contiguous_exprs : _,
634671 } = self ;
635672 let projection_exprs = projector. projection ( ) . as_ref ( ) ;
636673 let input = ctx. encode_child ( input) ?;
@@ -1515,6 +1552,7 @@ mod tests {
15151552 use crate :: filter_pushdown:: PushedDown ;
15161553 use crate :: statistics:: { StatisticsArgs , StatisticsContext } ;
15171554 use crate :: test;
1555+ use crate :: test:: TestMemoryExec ;
15181556 use crate :: test:: exec:: StatisticsExec ;
15191557
15201558 use arrow:: datatypes:: { DataType , Field , Schema } ;
@@ -1526,6 +1564,70 @@ mod tests {
15261564 BinaryExpr , Column , DynamicFilterPhysicalExpr , Literal , binary, col, lit,
15271565 } ;
15281566
1567+ #[ test]
1568+ fn group_contiguous_projection_is_all_or_nothing ( ) -> Result < ( ) > {
1569+ let schema = Arc :: new ( Schema :: new ( vec ! [
1570+ Field :: new( "key" , DataType :: Int32 , false ) ,
1571+ Field :: new( "time" , DataType :: Int32 , false ) ,
1572+ ] ) ) ;
1573+ let time_bin = binary (
1574+ col ( "time" , & schema) ?,
1575+ Operator :: Divide ,
1576+ lit ( ScalarValue :: Int32 ( Some ( 10 ) ) ) ,
1577+ & schema,
1578+ ) ?;
1579+ let plain_source = TestMemoryExec :: try_new ( & [ vec ! [ ] ] , Arc :: clone ( & schema) , None ) ?;
1580+ let source = plain_source. clone ( ) . try_with_group_contiguous_exprs ( vec ! [
1581+ col( "key" , & schema) ?,
1582+ Arc :: clone( & time_bin) ,
1583+ ] ) ?;
1584+
1585+ let projection = ProjectionExec :: try_new (
1586+ [
1587+ ProjectionExpr :: new ( col ( "key" , & schema) ?, "key" ) ,
1588+ ProjectionExpr :: new ( time_bin, "time_bin" ) ,
1589+ ] ,
1590+ Arc :: new ( source. clone ( ) ) ,
1591+ ) ?;
1592+ let projected_schema = projection. schema ( ) ;
1593+ let expected = [
1594+ col ( "key" , & projected_schema) ?,
1595+ col ( "time_bin" , & projected_schema) ?,
1596+ ] ;
1597+ assert_eq ! ( projection. group_contiguous_exprs( ) . len( ) , expected. len( ) ) ;
1598+ assert ! (
1599+ projection
1600+ . group_contiguous_exprs( )
1601+ . iter( )
1602+ . zip( expected)
1603+ . all( |( actual, expected) | actual. eq( & expected) )
1604+ ) ;
1605+
1606+ // A strict subset is not sufficient: contiguity of `(key, time_bin)`
1607+ // does not imply that `key` alone is contiguous.
1608+ let partial_projection = ProjectionExec :: try_new (
1609+ [ ProjectionExpr :: new ( col ( "key" , & schema) ?, "key" ) ] ,
1610+ Arc :: new ( source. clone ( ) ) ,
1611+ ) ?;
1612+ assert ! ( partial_projection. group_contiguous_exprs( ) . is_empty( ) ) ;
1613+
1614+ // Row-preserving operators do not inherit the assertion unless they
1615+ // opt in explicitly.
1616+ let filter = FilterExec :: try_new ( lit ( true ) , Arc :: new ( source. clone ( ) ) ) ?;
1617+ assert ! ( filter. group_contiguous_exprs( ) . is_empty( ) ) ;
1618+
1619+ // The assertion deliberately lives outside PlanProperties. Exercise
1620+ // the child-replacement fast path with identical cached properties and
1621+ // verify that ProjectionExec still recomputes it.
1622+ assert ! ( Arc :: ptr_eq( source. properties( ) , plain_source. properties( ) ) ) ;
1623+ let projection: Arc < dyn ExecutionPlan > = Arc :: new ( projection) ;
1624+ let replaced =
1625+ replace_children_if_necessary ( projection, vec ! [ Arc :: new( plain_source) ] ) ?;
1626+ assert ! ( replaced. group_contiguous_exprs( ) . is_empty( ) ) ;
1627+
1628+ Ok ( ( ) )
1629+ }
1630+
15291631 #[ test]
15301632 fn test_try_new_with_schema_metadata_only_replaces_metadata ( ) -> Result < ( ) > {
15311633 let input_schema = Arc :: new ( Schema :: new ( vec ! [ Field :: new(
0 commit comments