@@ -155,12 +155,108 @@ impl QueryGraph {
155155 }
156156}
157157
158+ /// Extracts the join subtree from a logical plan, separating it from wrapper operators.
159+ ///
160+ /// This function traverses the plan tree from the root downward, collecting all non-join
161+ /// operators until it finds the topmost join node. The join subtree (all consecutive joins)
162+ /// is extracted and returned separately from the wrapper operators.
163+ ///
164+ /// # Arguments
165+ ///
166+ /// * `plan` - The logical plan to extract from
167+ ///
168+ /// # Returns
169+ ///
170+ /// Returns a tuple of (join_subtree, wrapper_operators) where:
171+ /// - `join_subtree` is the topmost join and all joins beneath it
172+ /// - `wrapper_operators` is a vector of non-join operators above the joins, in order from root to join
173+ ///
174+ /// # Errors
175+ ///
176+ /// Returns an error if the plan doesn't contain any joins.
177+ pub ( crate ) fn extract_join_subtree (
178+ plan : LogicalPlan ,
179+ ) -> Result < ( LogicalPlan , Vec < LogicalPlan > ) > {
180+ let mut wrappers = Vec :: new ( ) ;
181+ let mut current = plan;
182+
183+ // Descend through non-join nodes until we find a join
184+ loop {
185+ match current {
186+ LogicalPlan :: Join ( _) => {
187+ // Found the join subtree root
188+ return Ok ( ( current, wrappers) ) ;
189+ }
190+ other => {
191+ // Check if this node contains joins in its children
192+ if !contains_join ( & other) {
193+ return plan_err ! (
194+ "Plan does not contain any join nodes: {}" ,
195+ other. display( )
196+ ) ;
197+ }
198+
199+ // This node is a wrapper - store it and descend to its child
200+ // For now, we only support single-child wrappers (Filter, Sort, Limit, Aggregate, etc.)
201+ let inputs = other. inputs ( ) ;
202+ if inputs. len ( ) != 1 {
203+ return plan_err ! (
204+ "Join extraction only supports single-input operators, found {} inputs in: {}" ,
205+ inputs. len( ) ,
206+ other. display( )
207+ ) ;
208+ }
209+
210+ wrappers. push ( other. clone ( ) ) ;
211+ current = ( * inputs[ 0 ] ) . clone ( ) ;
212+ }
213+ }
214+ }
215+ }
216+
217+ /// Reconstructs a logical plan by wrapping an optimized join plan with the original wrapper operators.
218+ ///
219+ /// This function takes an optimized join plan and re-applies the wrapper operators (Filter, Sort,
220+ /// Aggregate, etc.) that were removed during extraction. The wrappers are applied in reverse order
221+ /// (innermost to outermost) to reconstruct the original plan structure.
222+ ///
223+ /// # Arguments
224+ ///
225+ /// * `join_plan` - The optimized join plan to wrap
226+ /// * `wrappers` - Vector of wrapper operators in order from outermost to innermost (root to join)
227+ ///
228+ /// # Returns
229+ ///
230+ /// Returns the fully reconstructed logical plan with all wrapper operators reapplied.
231+ ///
232+ /// # Errors
233+ ///
234+ /// Returns an error if reconstructing any wrapper operator fails.
235+ pub ( crate ) fn reconstruct_plan (
236+ join_plan : LogicalPlan ,
237+ wrappers : Vec < LogicalPlan > ,
238+ ) -> Result < LogicalPlan > {
239+ let mut current = join_plan;
240+
241+ // Apply wrappers in reverse order (from innermost to outermost)
242+ for wrapper in wrappers. into_iter ( ) . rev ( ) {
243+ // Use with_new_exprs to reconstruct the wrapper with the new input
244+ current = wrapper. with_new_exprs ( wrapper. expressions ( ) , vec ! [ current] ) ?;
245+ }
246+
247+ Ok ( current)
248+ }
249+
158250impl TryFrom < LogicalPlan > for QueryGraph {
159251 type Error = DataFusionError ;
160252
161253 fn try_from ( value : LogicalPlan ) -> Result < Self , Self :: Error > {
254+ // First, extract the join subtree from any wrapper operators
255+ let ( join_subtree, _wrappers) = extract_join_subtree ( value) ?;
256+
257+ // Now convert only the join subtree to a query graph
162258 let mut query_graph = QueryGraph :: new ( ) ;
163- flatten_joins_recursive ( value , & mut query_graph) ?;
259+ flatten_joins_recursive ( join_subtree , & mut query_graph) ?;
164260 Ok ( query_graph)
165261 }
166262}
0 commit comments