@@ -176,3 +176,162 @@ fn optimize_join_subtree(
176176 // Use the Ibaraki-Kameda algorithm to find the optimal join ordering
177177 optimal_left_deep_join_plan ( query_graph, cost_estimator)
178178}
179+
180+ #[ cfg( test) ]
181+ mod tests {
182+ use super :: * ;
183+ use crate :: test:: * ;
184+ use crate :: OptimizerContext ;
185+ use datafusion_expr:: logical_plan:: JoinType ;
186+ use datafusion_expr:: { col, LogicalPlanBuilder } ;
187+
188+ /// Test that the JoinReorder rule correctly optimizes a three-way join
189+ #[ test]
190+ fn test_join_reorder_three_way_join ( ) -> Result < ( ) > {
191+ // Create three-way join: customer JOIN orders JOIN lineitem
192+ let customer = scan_tpch_table ( "customer" ) ;
193+ let orders = scan_tpch_table ( "orders" ) ;
194+ let lineitem = scan_tpch_table ( "lineitem" ) ;
195+
196+ let plan = LogicalPlanBuilder :: from ( customer)
197+ . join (
198+ orders,
199+ JoinType :: Inner ,
200+ ( vec ! [ "c_custkey" ] , vec ! [ "o_custkey" ] ) ,
201+ None ,
202+ ) ?
203+ . join (
204+ lineitem,
205+ JoinType :: Inner ,
206+ ( vec ! [ "o_orderkey" ] , vec ! [ "l_orderkey" ] ) ,
207+ None ,
208+ ) ?
209+ . build ( ) ?;
210+
211+ // Apply the optimizer rule
212+ let optimizer = JoinReorder :: default ( ) ;
213+ let config = OptimizerContext :: new ( ) ;
214+ let result = optimizer. rewrite ( plan. clone ( ) , & config) ?;
215+
216+ // Verify that the plan was transformed
217+ assert ! ( result. transformed) ;
218+ assert ! ( matches!( result. data, LogicalPlan :: Join ( _) ) ) ;
219+
220+ Ok ( ( ) )
221+ }
222+
223+ /// Test that the JoinReorder rule doesn't transform non-join plans
224+ #[ test]
225+ fn test_join_reorder_non_join_plan ( ) -> Result < ( ) > {
226+ // Create a simple table scan (no joins)
227+ let plan = scan_tpch_table ( "customer" ) ;
228+
229+ // Apply the optimizer rule
230+ let optimizer = JoinReorder :: default ( ) ;
231+ let config = OptimizerContext :: new ( ) ;
232+ let result = optimizer. rewrite ( plan. clone ( ) , & config) ?;
233+
234+ // Verify that the plan was NOT transformed
235+ assert ! ( !result. transformed) ;
236+
237+ Ok ( ( ) )
238+ }
239+
240+ /// Test that the JoinReorder rule handles join with filter
241+ #[ test]
242+ fn test_join_reorder_with_filter ( ) -> Result < ( ) > {
243+ // Create three-way join with a filter on one of the joins
244+ let customer = scan_tpch_table ( "customer" ) ;
245+ let orders = scan_tpch_table ( "orders" ) ;
246+ let lineitem = scan_tpch_table ( "lineitem" ) ;
247+
248+ let plan = LogicalPlanBuilder :: from ( customer)
249+ . join (
250+ orders,
251+ JoinType :: Inner ,
252+ ( vec ! [ "c_custkey" ] , vec ! [ "o_custkey" ] ) ,
253+ None ,
254+ ) ?
255+ . join_with_expr_keys (
256+ lineitem,
257+ JoinType :: Inner ,
258+ ( vec ! [ col( "o_orderkey" ) ] , vec ! [ col( "l_orderkey" ) ] ) ,
259+ Some ( col ( "l_quantity" ) . gt ( datafusion_expr:: lit ( 10.0 ) ) ) ,
260+ ) ?
261+ . build ( ) ?;
262+
263+ // Apply the optimizer rule
264+ let optimizer = JoinReorder :: default ( ) ;
265+ let config = OptimizerContext :: new ( ) ;
266+ let result = optimizer. rewrite ( plan, & config) ?;
267+
268+ // Verify that the plan was transformed and is a valid join
269+ assert ! ( result. transformed) ;
270+ assert ! ( matches!( result. data, LogicalPlan :: Join ( _) ) ) ;
271+
272+ // The filter should still be present in the optimized plan
273+ if let LogicalPlan :: Join ( join) = & result. data {
274+ // Check that at least one join in the tree has a filter
275+ let has_filter = join. filter . is_some ( )
276+ || matches ! ( join. left. as_ref( ) , LogicalPlan :: Join ( j) if j. filter. is_some( ) )
277+ || matches ! ( join. right. as_ref( ) , LogicalPlan :: Join ( j) if j. filter. is_some( ) ) ;
278+ assert ! ( has_filter, "Filter should be preserved in optimized plan" ) ;
279+ }
280+
281+ Ok ( ( ) )
282+ }
283+
284+ /// Test is_join_subtree_root correctly identifies consecutive joins
285+ #[ test]
286+ fn test_is_join_subtree_root ( ) -> Result < ( ) > {
287+ // Create a consecutive join subtree: should be identified as root
288+ let customer = scan_tpch_table ( "customer" ) ;
289+ let orders = scan_tpch_table ( "orders" ) ;
290+ let lineitem = scan_tpch_table ( "lineitem" ) ;
291+
292+ let consecutive_join = LogicalPlanBuilder :: from ( customer)
293+ . join (
294+ orders,
295+ JoinType :: Inner ,
296+ ( vec ! [ "c_custkey" ] , vec ! [ "o_custkey" ] ) ,
297+ None ,
298+ ) ?
299+ . join (
300+ lineitem,
301+ JoinType :: Inner ,
302+ ( vec ! [ "o_orderkey" ] , vec ! [ "l_orderkey" ] ) ,
303+ None ,
304+ ) ?
305+ . build ( ) ?;
306+
307+ assert ! ( is_join_subtree_root( & consecutive_join) ) ;
308+
309+ // Create a non-consecutive join (join with filter between joins)
310+ // This creates: Join -> Filter -> Join -> ...
311+ let customer = scan_tpch_table ( "customer" ) ;
312+ let orders = scan_tpch_table ( "orders" ) ;
313+ let lineitem = scan_tpch_table ( "lineitem" ) ;
314+
315+ let non_consecutive_join = LogicalPlanBuilder :: from ( customer)
316+ . join (
317+ orders,
318+ JoinType :: Inner ,
319+ ( vec ! [ "c_custkey" ] , vec ! [ "o_custkey" ] ) ,
320+ None ,
321+ ) ?
322+ . filter ( col ( "o_totalprice" ) . gt ( datafusion_expr:: lit ( 1000.0 ) ) ) ?
323+ . join (
324+ lineitem,
325+ JoinType :: Inner ,
326+ ( vec ! [ "o_orderkey" ] , vec ! [ "l_orderkey" ] ) ,
327+ None ,
328+ ) ?
329+ . build ( ) ?;
330+
331+ // The top-level is a Join, but its left child is a Filter that contains a nested Join.
332+ // This breaks the consecutive join pattern, so it should NOT be identified as a join subtree root.
333+ assert ! ( !is_join_subtree_root( & non_consecutive_join) ) ;
334+
335+ Ok ( ( ) )
336+ }
337+ }
0 commit comments