@@ -41,10 +41,55 @@ use crate::materialized::cast_to_materialized;
4141use super :: normal_form:: SpjNormalForm ;
4242use super :: QueryRewriteOptions ;
4343
44+ /// Logical rewrite metadata propagated alongside equivalent candidate plans.
45+ #[ derive( Debug , Clone , Default , PartialEq , PartialOrd , Eq , Hash ) ]
46+ pub struct RewriteContext {
47+ root_table_refs : Vec < String > ,
48+ }
49+
50+ impl RewriteContext {
51+ /// Create a new rewrite context from the root table refs visible during rewrite.
52+ pub fn new ( root_table_refs : Vec < String > ) -> Self {
53+ Self { root_table_refs }
54+ }
55+
56+ /// Returns the root table refs that produced this rewrite opportunity.
57+ pub fn root_table_refs ( & self ) -> & [ String ] {
58+ & self . root_table_refs
59+ }
60+ }
61+
62+ /// Inputs provided to a cost function when selecting the best candidate plan.
63+ pub struct CostContext < ' a > {
64+ candidate_plans : Box < dyn Iterator < Item = & ' a dyn ExecutionPlan > + ' a > ,
65+ rewrite_context : & ' a RewriteContext ,
66+ }
67+
68+ impl < ' a > CostContext < ' a > {
69+ /// Create a new cost context.
70+ pub fn new (
71+ candidate_plans : Box < dyn Iterator < Item = & ' a dyn ExecutionPlan > + ' a > ,
72+ rewrite_context : & ' a RewriteContext ,
73+ ) -> Self {
74+ Self {
75+ candidate_plans,
76+ rewrite_context,
77+ }
78+ }
79+
80+ /// Consume the context and return the candidate plans iterator.
81+ pub fn into_candidate_plans ( self ) -> Box < dyn Iterator < Item = & ' a dyn ExecutionPlan > + ' a > {
82+ self . candidate_plans
83+ }
84+
85+ /// Returns rewrite metadata for the current candidate set.
86+ pub fn rewrite_context ( & self ) -> & RewriteContext {
87+ self . rewrite_context
88+ }
89+ }
90+
4491/// A cost function. Used to evaluate the best physical plan among multiple equivalent choices.
45- pub type CostFn = Arc <
46- dyn for < ' a > Fn ( Box < dyn Iterator < Item = & ' a dyn ExecutionPlan > + ' a > ) -> Vec < f64 > + Send + Sync ,
47- > ;
92+ pub type CostFn = Arc < dyn for < ' a > Fn ( CostContext < ' a > ) -> Vec < f64 > + Send + Sync > ;
4893
4994/// A logical optimizer that generates candidate logical plans in the form of [`OneOf`] nodes.
5095#[ derive( Debug ) ]
@@ -186,9 +231,10 @@ impl TreeNodeRewriter for ViewMatchingRewriter<'_> {
186231 } else {
187232 Ok ( Transformed :: new (
188233 LogicalPlan :: Extension ( Extension {
189- node : Arc :: new ( OneOf {
190- branches : Some ( node) . into_iter ( ) . chain ( candidates) . collect_vec ( ) ,
191- } ) ,
234+ node : Arc :: new ( OneOf :: with_rewrite_context (
235+ Some ( node) . into_iter ( ) . chain ( candidates) . collect_vec ( ) ,
236+ RewriteContext :: new ( vec ! [ table_reference. to_string( ) ] ) ,
237+ ) ) ,
192238 } ) ,
193239 true ,
194240 TreeNodeRecursion :: Jump ,
@@ -241,9 +287,9 @@ impl ExtensionPlanner for ViewExploitationPlanner {
241287 physical_inputs : & [ Arc < dyn ExecutionPlan > ] ,
242288 _session_state : & SessionState ,
243289 ) -> Result < Option < Arc < dyn ExecutionPlan > > > {
244- if node. as_any ( ) . downcast_ref :: < OneOf > ( ) . is_none ( ) {
290+ let Some ( one_of ) = node. as_any ( ) . downcast_ref :: < OneOf > ( ) else {
245291 return Ok ( None ) ;
246- }
292+ } ;
247293
248294 // Compare schemas ignoring nullability differences.
249295 // Different table types (FileScanTable, LiveTable, MV) may expose
@@ -282,6 +328,7 @@ impl ExtensionPlanner for ViewExploitationPlanner {
282328 physical_inputs. to_vec ( ) ,
283329 None ,
284330 Arc :: clone ( & self . cost ) ,
331+ one_of. rewrite_context ( ) . clone ( ) ,
285332 ) ?) ) )
286333 }
287334}
@@ -291,12 +338,29 @@ impl ExtensionPlanner for ViewExploitationPlanner {
291338#[ derive( Debug , Clone , PartialEq , PartialOrd , Eq , Hash ) ]
292339pub struct OneOf {
293340 branches : Vec < LogicalPlan > ,
341+ rewrite_context : RewriteContext ,
294342}
295343
296344impl OneOf {
297345 /// Create a new OneOf node with the given branches.
298346 pub fn new ( branches : Vec < LogicalPlan > ) -> Self {
299- Self { branches }
347+ Self :: with_rewrite_context ( branches, RewriteContext :: default ( ) )
348+ }
349+
350+ /// Create a new OneOf node with the given branches and rewrite context.
351+ pub fn with_rewrite_context (
352+ branches : Vec < LogicalPlan > ,
353+ rewrite_context : RewriteContext ,
354+ ) -> Self {
355+ Self {
356+ branches,
357+ rewrite_context,
358+ }
359+ }
360+
361+ /// Returns logical rewrite metadata for this candidate set.
362+ pub fn rewrite_context ( & self ) -> & RewriteContext {
363+ & self . rewrite_context
300364 }
301365}
302366
@@ -333,7 +397,10 @@ impl UserDefinedLogicalNodeCore for OneOf {
333397 _exprs : Vec < datafusion:: prelude:: Expr > ,
334398 inputs : Vec < LogicalPlan > ,
335399 ) -> Result < Self > {
336- Ok ( Self { branches : inputs } )
400+ Ok ( Self {
401+ branches : inputs,
402+ rewrite_context : self . rewrite_context . clone ( ) ,
403+ } )
337404 }
338405}
339406
@@ -349,6 +416,7 @@ pub struct OneOfExec {
349416 best : usize ,
350417 // Cost function to use in optimization
351418 cost : CostFn ,
419+ rewrite_context : RewriteContext ,
352420}
353421
354422impl std:: fmt:: Debug for OneOfExec {
@@ -357,6 +425,7 @@ impl std::fmt::Debug for OneOfExec {
357425 . field ( "candidates" , & self . candidates )
358426 . field ( "required_input_ordering" , & self . required_input_ordering )
359427 . field ( "best" , & self . best )
428+ . field ( "rewrite_context" , & self . rewrite_context )
360429 . finish_non_exhaustive ( )
361430 }
362431}
@@ -367,23 +436,28 @@ impl OneOfExec {
367436 candidates : Vec < Arc < dyn ExecutionPlan > > ,
368437 required_input_ordering : Option < OrderingRequirements > ,
369438 cost : CostFn ,
439+ rewrite_context : RewriteContext ,
370440 ) -> Result < Self > {
371441 if candidates. is_empty ( ) {
372442 return Err ( DataFusionError :: Plan (
373443 "can't create OneOfExec with empty children" . to_string ( ) ,
374444 ) ) ;
375445 }
376446
377- let best = cost ( Box :: new ( candidates. iter ( ) . map ( |c| c. as_ref ( ) ) ) )
378- . iter ( )
379- . position_min_by_key ( |& cost| OrderedFloat ( * cost) )
380- . unwrap ( ) ;
447+ let best = cost ( CostContext :: new (
448+ Box :: new ( candidates. iter ( ) . map ( |c| c. as_ref ( ) ) ) ,
449+ & rewrite_context,
450+ ) )
451+ . iter ( )
452+ . position_min_by_key ( |& cost| OrderedFloat ( * cost) )
453+ . unwrap ( ) ;
381454
382455 Ok ( Self {
383456 candidates,
384457 required_input_ordering,
385458 best,
386459 cost,
460+ rewrite_context,
387461 } )
388462 }
389463
@@ -393,6 +467,11 @@ impl OneOfExec {
393467 Arc :: clone ( & self . candidates [ self . best ] )
394468 }
395469
470+ /// Returns rewrite metadata for this candidate set.
471+ pub fn rewrite_context ( & self ) -> & RewriteContext {
472+ & self . rewrite_context
473+ }
474+
396475 /// Modify this plan's required input ordering.
397476 /// Used for sort pushdown
398477 pub fn with_required_input_ordering ( self , requirement : Option < OrderingRequirements > ) -> Self {
@@ -444,6 +523,7 @@ impl ExecutionPlan for OneOfExec {
444523 children,
445524 self . required_input_ordering . clone ( ) ,
446525 Arc :: clone ( & self . cost ) ,
526+ self . rewrite_context . clone ( ) ,
447527 ) ?) )
448528 }
449529
@@ -473,7 +553,10 @@ impl ExecutionPlan for OneOfExec {
473553
474554impl DisplayAs for OneOfExec {
475555 fn fmt_as ( & self , t : DisplayFormatType , f : & mut std:: fmt:: Formatter ) -> std:: fmt:: Result {
476- let costs = ( self . cost ) ( Box :: new ( self . children ( ) . iter ( ) . map ( |arc| arc. as_ref ( ) ) ) ) ;
556+ let costs = ( self . cost ) ( CostContext :: new (
557+ Box :: new ( self . children ( ) . iter ( ) . map ( |arc| arc. as_ref ( ) ) ) ,
558+ & self . rewrite_context ,
559+ ) ) ;
477560 match t {
478561 DisplayFormatType :: Default | DisplayFormatType :: Verbose => {
479562 write ! (
@@ -595,3 +678,87 @@ mod tests_nullability {
595678 assert ! ( !schemas_equal_ignoring_nullability( & a, & b) ) ;
596679 }
597680}
681+
682+ #[ cfg( test) ]
683+ mod tests_rewrite_context {
684+ use super :: * ;
685+ use arrow_schema:: Schema ;
686+ use datafusion:: physical_plan:: empty:: EmptyExec ;
687+ use datafusion_expr:: LogicalPlanBuilder ;
688+ use std:: sync:: Mutex ;
689+
690+ #[ test]
691+ fn one_of_preserves_rewrite_context_when_rebuilt ( ) {
692+ let plan = LogicalPlanBuilder :: empty ( false )
693+ . build ( )
694+ . expect ( "empty plan" ) ;
695+ let one_of = OneOf :: with_rewrite_context (
696+ vec ! [ plan. clone( ) ] ,
697+ RewriteContext :: new ( vec ! [ "catalog.schema.root_table" . to_string( ) ] ) ,
698+ ) ;
699+
700+ let rebuilt =
701+ UserDefinedLogicalNodeCore :: with_exprs_and_inputs ( & one_of, vec ! [ ] , vec ! [ plan] )
702+ . expect ( "rebuild one_of" ) ;
703+
704+ assert_eq ! (
705+ rebuilt. rewrite_context( ) . root_table_refs( ) ,
706+ [ "catalog.schema.root_table" . to_string( ) ]
707+ ) ;
708+ }
709+
710+ #[ test]
711+ fn one_of_exec_passes_rewrite_context_to_cost_function ( ) {
712+ let seen = Arc :: new ( Mutex :: new ( Vec :: < String > :: new ( ) ) ) ;
713+ let seen_clone = Arc :: clone ( & seen) ;
714+ let cost: CostFn = Arc :: new ( move |ctx| {
715+ * seen_clone. lock ( ) . expect ( "lock seen" ) =
716+ ctx. rewrite_context ( ) . root_table_refs ( ) . to_vec ( ) ;
717+ ctx. into_candidate_plans ( ) . map ( |_| 1.0 ) . collect ( )
718+ } ) ;
719+ let context = RewriteContext :: new ( vec ! [ "catalog.schema.root_table" . to_string( ) ] ) ;
720+ let schema = Arc :: new ( Schema :: empty ( ) ) ;
721+ let candidates = vec ! [
722+ Arc :: new( EmptyExec :: new( Arc :: clone( & schema) ) ) as Arc <dyn ExecutionPlan >,
723+ Arc :: new( EmptyExec :: new( schema) ) as Arc <dyn ExecutionPlan >,
724+ ] ;
725+
726+ let exec =
727+ OneOfExec :: try_new ( candidates, None , cost, context. clone ( ) ) . expect ( "one_of exec" ) ;
728+
729+ assert_eq ! ( exec. rewrite_context( ) , & context) ;
730+ assert_eq ! ( * seen. lock( ) . expect( "lock seen" ) , context. root_table_refs( ) ) ;
731+ }
732+
733+ #[ test]
734+ fn one_of_exec_with_new_children_preserves_rewrite_context ( ) {
735+ let cost: CostFn = Arc :: new ( |ctx| ctx. into_candidate_plans ( ) . map ( |_| 1.0 ) . collect ( ) ) ;
736+ let context = RewriteContext :: new ( vec ! [ "catalog.schema.root_table" . to_string( ) ] ) ;
737+ let schema = Arc :: new ( Schema :: empty ( ) ) ;
738+ let exec = Arc :: new (
739+ OneOfExec :: try_new (
740+ vec ! [
741+ Arc :: new( EmptyExec :: new( Arc :: clone( & schema) ) ) as Arc <dyn ExecutionPlan >,
742+ Arc :: new( EmptyExec :: new( Arc :: clone( & schema) ) ) as Arc <dyn ExecutionPlan >,
743+ ] ,
744+ None ,
745+ cost,
746+ context. clone ( ) ,
747+ )
748+ . expect ( "one_of exec" ) ,
749+ ) ;
750+
751+ let rebuilt = exec
752+ . with_new_children ( vec ! [
753+ Arc :: new( EmptyExec :: new( Arc :: clone( & schema) ) ) as Arc <dyn ExecutionPlan >,
754+ Arc :: new( EmptyExec :: new( schema) ) as Arc <dyn ExecutionPlan >,
755+ ] )
756+ . expect ( "rebuild exec" ) ;
757+ let rebuilt = rebuilt
758+ . as_any ( )
759+ . downcast_ref :: < OneOfExec > ( )
760+ . expect ( "expected OneOfExec" ) ;
761+
762+ assert_eq ! ( rebuilt. rewrite_context( ) , & context) ;
763+ }
764+ }
0 commit comments