use std::collections::BTreeSet;
use super::{optimizer::JoinOrderSearch, state::JoinCost};
use crate::select::join::reorder::{JoinEdge, JoinOrderAnalyzer};
#[test]
fn test_single_table_order() {
let mut analyzer = JoinOrderAnalyzer::new();
analyzer.register_tables(vec!["t1".to_string()]);
let db = vibesql_storage::Database::new();
let search = JoinOrderSearch::from_analyzer(&analyzer, &db);
let order = search.find_optimal_order();
assert_eq!(order.len(), 1);
assert_eq!(order[0], "t1");
}
#[test]
fn test_two_table_order() {
let mut analyzer = JoinOrderAnalyzer::new();
analyzer.register_tables(vec!["t1".to_string(), "t2".to_string()]);
analyzer.add_edge(JoinEdge {
left_table: "t1".to_string(),
left_column: "id".to_string(),
right_table: "t2".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
let db = vibesql_storage::Database::new();
let search = JoinOrderSearch::from_analyzer(&analyzer, &db);
let order = search.find_optimal_order();
assert_eq!(order.len(), 2);
assert!(order.contains(&"t1".to_string()));
assert!(order.contains(&"t2".to_string()));
}
#[test]
fn test_three_table_chain() {
let mut analyzer = JoinOrderAnalyzer::new();
analyzer.register_tables(vec!["t1".to_string(), "t2".to_string(), "t3".to_string()]);
analyzer.add_edge(JoinEdge {
left_table: "t1".to_string(),
left_column: "id".to_string(),
right_table: "t2".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t2".to_string(),
left_column: "id".to_string(),
right_table: "t3".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
let db = vibesql_storage::Database::new();
let search = JoinOrderSearch::from_analyzer(&analyzer, &db);
let order = search.find_optimal_order();
assert_eq!(order.len(), 3);
assert!(order.contains(&"t1".to_string()));
assert!(order.contains(&"t2".to_string()));
assert!(order.contains(&"t3".to_string()));
}
#[test]
fn test_cost_comparison() {
let cost_with_edge = JoinCost::new(100, 1000);
let cost_without_edge = JoinCost::new(500, 1000);
assert!(cost_with_edge.total() < cost_without_edge.total());
}
#[test]
fn test_search_prunes_bad_paths() {
let mut analyzer = JoinOrderAnalyzer::new();
analyzer.register_tables(vec!["t1".to_string(), "t2".to_string(), "t3".to_string()]);
analyzer.add_edge(JoinEdge {
left_table: "t1".to_string(),
left_column: "id".to_string(),
right_table: "t2".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t2".to_string(),
left_column: "id".to_string(),
right_table: "t3".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
let db = vibesql_storage::Database::new();
let search = JoinOrderSearch::from_analyzer(&analyzer, &db);
let order = search.find_optimal_order();
assert_eq!(order.len(), 3);
}
#[test]
fn test_disconnected_tables() {
let mut analyzer = JoinOrderAnalyzer::new();
analyzer.register_tables(vec!["t1".to_string(), "t2".to_string(), "t3".to_string()]);
let db = vibesql_storage::Database::new();
let search = JoinOrderSearch::from_analyzer(&analyzer, &db);
let order = search.find_optimal_order();
assert_eq!(order.len(), 3);
}
#[test]
fn test_star_join_pattern() {
let mut analyzer = JoinOrderAnalyzer::new();
analyzer.register_tables(vec![
"t1".to_string(),
"t2".to_string(),
"t3".to_string(),
"t4".to_string(),
]);
analyzer.add_edge(JoinEdge {
left_table: "t1".to_string(),
left_column: "id".to_string(),
right_table: "t2".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t1".to_string(),
left_column: "id".to_string(),
right_table: "t3".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t1".to_string(),
left_column: "id".to_string(),
right_table: "t4".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
let db = vibesql_storage::Database::new();
let search = JoinOrderSearch::from_analyzer(&analyzer, &db);
let order = search.find_optimal_order();
assert_eq!(order.len(), 4);
assert!(order.contains(&"t1".to_string()));
assert!(order.contains(&"t2".to_string()));
assert!(order.contains(&"t3".to_string()));
assert!(order.contains(&"t4".to_string()));
let t1_pos = order.iter().position(|t| t == "t1").unwrap();
assert!(
t1_pos <= 1,
"Hub table t1 should be early in join order, found at position {}",
t1_pos
);
}
#[test]
fn test_parallel_bfs_selection() {
let mut analyzer = JoinOrderAnalyzer::new();
analyzer.register_tables(vec!["t1".to_string(), "t2".to_string()]);
analyzer.add_edge(JoinEdge {
left_table: "t1".to_string(),
left_column: "id".to_string(),
right_table: "t2".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
let db = vibesql_storage::Database::new();
let search = JoinOrderSearch::from_analyzer(&analyzer, &db);
assert!(!search.should_use_parallel_search(), "Small queries should use DFS");
let mut analyzer = JoinOrderAnalyzer::new();
analyzer.register_tables(vec![
"t1".to_string(),
"t2".to_string(),
"t3".to_string(),
"t4".to_string(),
"t5".to_string(),
]);
analyzer.add_edge(JoinEdge {
left_table: "t1".to_string(),
left_column: "id".to_string(),
right_table: "t2".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t1".to_string(),
left_column: "id".to_string(),
right_table: "t3".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t1".to_string(),
left_column: "id".to_string(),
right_table: "t4".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t1".to_string(),
left_column: "id".to_string(),
right_table: "t5".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t2".to_string(),
left_column: "id".to_string(),
right_table: "t3".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t3".to_string(),
left_column: "id".to_string(),
right_table: "t4".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t4".to_string(),
left_column: "id".to_string(),
right_table: "t5".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t2".to_string(),
left_column: "id".to_string(),
right_table: "t5".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
let search = JoinOrderSearch::from_analyzer(&analyzer, &db);
assert!(
search.should_use_parallel_search(),
"Highly connected 5-table query should use parallel BFS"
);
let order = search.find_optimal_order();
assert_eq!(order.len(), 5);
}
#[test]
fn test_parallel_bfs_produces_valid_ordering() {
let mut analyzer = JoinOrderAnalyzer::new();
analyzer.register_tables(vec![
"t1".to_string(),
"t2".to_string(),
"t3".to_string(),
"t4".to_string(),
]);
analyzer.add_edge(JoinEdge {
left_table: "t1".to_string(),
left_column: "id".to_string(),
right_table: "t2".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t1".to_string(),
left_column: "id".to_string(),
right_table: "t3".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t1".to_string(),
left_column: "id".to_string(),
right_table: "t4".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t2".to_string(),
left_column: "id".to_string(),
right_table: "t3".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t3".to_string(),
left_column: "id".to_string(),
right_table: "t4".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t2".to_string(),
left_column: "id".to_string(),
right_table: "t4".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
let db = vibesql_storage::Database::new();
let search = JoinOrderSearch::from_analyzer(&analyzer, &db);
assert!(search.should_use_parallel_search());
let order_parallel = search.context.find_optimal_order_parallel();
let order_dfs = search.context.find_optimal_order_dfs();
assert_eq!(order_parallel.len(), 4);
assert_eq!(order_dfs.len(), 4);
for table in &["t1", "t2", "t3", "t4"] {
assert!(order_parallel.contains(&table.to_string()));
assert!(order_dfs.contains(&table.to_string()));
}
}
#[test]
fn test_tpch_q3_star_schema_no_cross_join() {
let mut analyzer = JoinOrderAnalyzer::new();
analyzer.register_tables(vec![
"customer".to_string(),
"orders".to_string(),
"lineitem".to_string(),
]);
analyzer.add_edge(JoinEdge {
left_table: "customer".to_string(),
left_column: "c_custkey".to_string(),
right_table: "orders".to_string(),
right_column: "o_custkey".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "lineitem".to_string(),
left_column: "l_orderkey".to_string(),
right_table: "orders".to_string(),
right_column: "o_orderkey".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
let db = vibesql_storage::Database::new();
let search = JoinOrderSearch::from_analyzer(&analyzer, &db);
let order = search.find_optimal_order();
assert_eq!(order.len(), 3);
assert!(order.contains(&"customer".to_string()));
assert!(order.contains(&"orders".to_string()));
assert!(order.contains(&"lineitem".to_string()));
for i in 1..order.len() {
let current_table = &order[i];
let previous_tables: BTreeSet<String> = order[0..i].iter().cloned().collect();
let has_connection = search.context.has_join_edge(&previous_tables, current_table);
assert!(
has_connection,
"Table {} at position {} has no join condition with previous tables {:?}. \
This would cause a CROSS JOIN and memory limit exceeded. \
Full order: {:?}",
current_table, i, previous_tables, order
);
}
}
#[test]
fn test_join_order_determinism() {
let mut analyzer = JoinOrderAnalyzer::new();
analyzer.register_tables(vec![
"customer".to_string(),
"orders".to_string(),
"lineitem".to_string(),
"supplier".to_string(),
"nation".to_string(),
]);
analyzer.add_edge(JoinEdge {
left_table: "customer".to_string(),
left_column: "c_custkey".to_string(),
right_table: "orders".to_string(),
right_column: "o_custkey".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "orders".to_string(),
left_column: "o_orderkey".to_string(),
right_table: "lineitem".to_string(),
right_column: "l_orderkey".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "lineitem".to_string(),
left_column: "l_suppkey".to_string(),
right_table: "supplier".to_string(),
right_column: "s_suppkey".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "supplier".to_string(),
left_column: "s_nationkey".to_string(),
right_table: "nation".to_string(),
right_column: "n_nationkey".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "customer".to_string(),
left_column: "c_nationkey".to_string(),
right_table: "nation".to_string(),
right_column: "n_nationkey".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
let db = vibesql_storage::Database::new();
let search = JoinOrderSearch::from_analyzer(&analyzer, &db);
let mut orders = Vec::new();
for _ in 0..10 {
let order = search.find_optimal_order();
orders.push(order);
}
let first_order = &orders[0];
for (i, order) in orders.iter().enumerate().skip(1) {
assert_eq!(
order, first_order,
"Join order iteration {} differed from first: {:?} != {:?}",
i, order, first_order
);
}
assert_eq!(first_order.len(), 5);
}
#[test]
fn test_time_bounded_search_returns_valid_ordering() {
let mut analyzer = JoinOrderAnalyzer::new();
analyzer.register_tables(vec![
"t1".to_string(),
"t2".to_string(),
"t3".to_string(),
"t4".to_string(),
"t5".to_string(),
]);
analyzer.add_edge(JoinEdge {
left_table: "t1".to_string(),
left_column: "id".to_string(),
right_table: "t2".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t2".to_string(),
left_column: "id".to_string(),
right_table: "t3".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t3".to_string(),
left_column: "id".to_string(),
right_table: "t4".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t4".to_string(),
left_column: "id".to_string(),
right_table: "t5".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
let db = vibesql_storage::Database::new();
let mut search = JoinOrderSearch::from_analyzer(&analyzer, &db);
search.context.config.time_budget_ms = 1; search.context.config.use_time_budget = true;
let order = search.find_optimal_order();
assert_eq!(order.len(), 5);
assert!(order.contains(&"t1".to_string()));
assert!(order.contains(&"t2".to_string()));
assert!(order.contains(&"t3".to_string()));
assert!(order.contains(&"t4".to_string()));
assert!(order.contains(&"t5".to_string()));
}
#[test]
fn test_time_bounded_search_completes_fast_for_small_queries() {
let mut analyzer = JoinOrderAnalyzer::new();
analyzer.register_tables(vec!["t1".to_string(), "t2".to_string(), "t3".to_string()]);
analyzer.add_edge(JoinEdge {
left_table: "t1".to_string(),
left_column: "id".to_string(),
right_table: "t2".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t2".to_string(),
left_column: "id".to_string(),
right_table: "t3".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
let db = vibesql_storage::Database::new();
let search = JoinOrderSearch::from_analyzer(&analyzer, &db);
let start = std::time::Instant::now();
let order = search.find_optimal_order();
let elapsed = start.elapsed();
assert!(elapsed.as_millis() < 500, "Small query took too long: {:?}", elapsed);
assert_eq!(order.len(), 3);
}
#[test]
fn test_time_bounded_search_with_generous_budget() {
let mut analyzer = JoinOrderAnalyzer::new();
analyzer.register_tables(vec![
"t1".to_string(),
"t2".to_string(),
"t3".to_string(),
"t4".to_string(),
]);
analyzer.add_edge(JoinEdge {
left_table: "t1".to_string(),
left_column: "id".to_string(),
right_table: "t2".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t1".to_string(),
left_column: "id".to_string(),
right_table: "t3".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t1".to_string(),
left_column: "id".to_string(),
right_table: "t4".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
let db = vibesql_storage::Database::new();
let mut search = JoinOrderSearch::from_analyzer(&analyzer, &db);
search.context.config.time_budget_ms = 10000; search.context.config.use_time_budget = true;
let order = search.find_optimal_order();
assert_eq!(order.len(), 4);
let t1_pos = order.iter().position(|t| t == "t1").unwrap();
assert!(t1_pos <= 1, "Hub table should be early in order");
}
#[test]
fn test_legacy_behavior_with_time_budget_disabled() {
let mut analyzer = JoinOrderAnalyzer::new();
analyzer.register_tables(vec!["t1".to_string(), "t2".to_string(), "t3".to_string()]);
analyzer.add_edge(JoinEdge {
left_table: "t1".to_string(),
left_column: "id".to_string(),
right_table: "t2".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
analyzer.add_edge(JoinEdge {
left_table: "t2".to_string(),
left_column: "id".to_string(),
right_table: "t3".to_string(),
right_column: "id".to_string(),
join_type: vibesql_ast::JoinType::Inner,
});
let db = vibesql_storage::Database::new();
let mut search = JoinOrderSearch::from_analyzer(&analyzer, &db);
search.context.config.use_time_budget = false;
let order = search.find_optimal_order();
assert_eq!(order.len(), 3);
assert!(order.contains(&"t1".to_string()));
assert!(order.contains(&"t2".to_string()));
assert!(order.contains(&"t3".to_string()));
}