Skip to content

Commit 33c5049

Browse files
committed
add optimizer rule test
1 parent d1864ae commit 33c5049

1 file changed

Lines changed: 159 additions & 0 deletions

File tree

  • datafusion/optimizer/src/reorder_join

‎datafusion/optimizer/src/reorder_join/mod.rs‎

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)