use std::collections::{HashMap, HashSet};
use vibesql_ast::{Expression, FromClause};
use super::{graph, predicates, utils};
use crate::optimizer::is_constant_false_where;
use crate::select::scan::bloom_context;
use crate::{
debug_output::{Category, DebugEvent, JsonValue},
errors::ExecutorError,
schema::CombinedSchema,
select::{
cte::CteResult,
join::{nested_loop_join, JoinOrderAnalyzer, JoinOrderSearch},
scan::{
derived::execute_derived_table,
table::{execute_table_scan, execute_table_scan_with_bloom},
FromResult,
},
SelectResult,
},
timeout::TimeoutContext,
};
fn join_profile_enabled() -> bool {
std::env::var("JOIN_PROFILE").is_ok()
}
fn join_reorder_verbose() -> bool {
std::env::var("JOIN_REORDER_VERBOSE").is_ok()
}
pub(crate) fn execute_with_join_reordering<F>(
from: &FromClause,
cte_results: &HashMap<String, CteResult>,
database: &vibesql_storage::Database,
where_clause: Option<&Expression>,
outer_row: Option<&vibesql_storage::Row>,
outer_schema: Option<&CombinedSchema>,
execute_subquery: F,
) -> Result<FromResult, ExecutorError>
where
F: Fn(&vibesql_ast::SelectStmt) -> Result<SelectResult, ExecutorError> + Copy,
{
let mut table_refs = Vec::new();
graph::flatten_join_tree(from, &mut table_refs);
if let Some(where_expr) = where_clause {
if is_constant_false_where(where_expr) == Some(true) {
let schema = utils::build_schema_from_table_refs(&table_refs, database, cte_results)?;
return Ok(FromResult::from_rows(schema, Vec::new()));
}
}
let mut join_conditions = Vec::new();
let mut join_conditions_with_types = Vec::new();
graph::extract_all_conditions(from, &mut join_conditions);
graph::extract_conditions_with_types(from, &mut join_conditions_with_types);
let table_names: Vec<String> =
table_refs.iter().map(|t| t.alias.clone().unwrap_or_else(|| t.name.clone())).collect();
let mut duplicate_table_aliases = std::collections::HashSet::new();
{
let mut seen_names = std::collections::HashSet::new();
for name in &table_names {
let normalized = vibesql_catalog::TableIdentifier::unquoted(name);
if seen_names.contains(&normalized) {
duplicate_table_aliases.insert(normalized.clone());
}
seen_names.insert(normalized);
}
}
let column_to_table =
utils::build_column_to_table_map(database, &table_names, &table_refs, cte_results);
let mut analyzer = JoinOrderAnalyzer::with_column_map(column_to_table.clone());
analyzer.register_tables(table_names.clone());
let table_set: HashSet<String> = table_names.iter().map(|t| t.to_lowercase()).collect();
if join_reorder_verbose() {
DebugEvent::new(Category::Optimizer, "column_mapping", "JOIN_REORDER")
.text(format!(
"Schema-based column mapping: {} columns resolved from {} tables",
column_to_table.len(),
table_names.len()
))
.field_int("columns_resolved", column_to_table.len() as i64)
.field_int("table_count", table_names.len() as i64)
.field_str_array("tables", &table_names)
.emit();
if column_to_table.is_empty() && !table_names.is_empty() {
DebugEvent::new(Category::Optimizer, "column_mapping_warning", "JOIN_REORDER")
.text(format!("Warning: No schema columns found for tables: {:?}", table_names))
.field_str_array("tables", &table_names)
.emit();
}
}
for condition_with_type in &join_conditions_with_types {
analyzer.analyze_predicate_with_type(
&condition_with_type.condition,
&table_set,
condition_with_type.join_type.clone(),
);
}
let where_equijoins = if let Some(where_expr) = where_clause {
analyzer.analyze_predicate(where_expr, &table_set);
if join_reorder_verbose() {
DebugEvent::new(Category::Optimizer, "where_clause_analysis", "JOIN_REORDER")
.text(format!("WHERE clause present: {:?}", where_expr))
.field_bool("has_where_clause", true)
.field_int("table_count", table_set.len() as i64)
.emit();
}
let equijoins = predicates::extract_where_equijoins_with_schema(
where_expr,
&table_set,
&column_to_table,
);
if join_reorder_verbose() {
DebugEvent::new(Category::Optimizer, "where_equijoins", "JOIN_REORDER")
.text(format!("Extracted {} WHERE equijoins", equijoins.len()))
.field_int("equijoin_count", equijoins.len() as i64)
.emit();
}
equijoins
} else {
if join_reorder_verbose() {
DebugEvent::new(Category::Optimizer, "where_clause_analysis", "JOIN_REORDER")
.text("No WHERE clause")
.field_bool("has_where_clause", false)
.emit();
}
Vec::new()
};
join_conditions.extend(where_equijoins);
let mut table_local_predicates = if let Some(where_expr) = where_clause {
predicates::extract_table_local_predicates_with_schema(
where_expr,
&table_set,
&column_to_table,
)
} else {
HashMap::new()
};
if let Some(where_expr) = where_clause {
for (table, preds) in predicates::extract_in_predicates_from_or(where_expr, &table_set) {
table_local_predicates.entry(table).or_default().extend(preds);
}
}
if let Some(where_expr) = where_clause {
for (table, preds) in predicates::extract_common_or_predicates_with_schema(
where_expr,
&table_set,
&column_to_table,
) {
table_local_predicates.entry(table).or_default().extend(preds);
}
}
if join_reorder_verbose() && !table_local_predicates.is_empty() {
let predicate_tables: Vec<String> = table_local_predicates.keys().cloned().collect();
DebugEvent::new(Category::Optimizer, "table_local_predicates", "JOIN_REORDER")
.text(format!("Table-local predicates: {:?}", predicate_tables))
.field_str_array("tables_with_predicates", &predicate_tables)
.field_int("predicate_table_count", predicate_tables.len() as i64)
.emit();
}
let alias_to_table: HashMap<String, String> = table_refs
.iter()
.map(|t| {
let key = t.alias.clone().unwrap_or_else(|| t.name.clone()).to_lowercase();
(key, t.name.clone())
})
.collect();
let optimizer_start = std::time::Instant::now();
let search = JoinOrderSearch::from_analyzer_with_predicates(
&analyzer,
database,
&table_local_predicates,
&alias_to_table,
);
let optimal_order = search.find_optimal_order();
let optimizer_time = optimizer_start.elapsed();
if join_reorder_verbose() {
DebugEvent::new(Category::Optimizer, "join_order_decision", "JOIN_REORDER")
.text(format!("Original order: {:?}, Optimal order: {:?}", table_names, optimal_order))
.field_str_array("original_order", &table_names)
.field_str_array("optimal_order", &optimal_order)
.field_int("join_condition_count", join_conditions.len() as i64)
.field_duration_us("optimizer_time_us", optimizer_time)
.field_bool("order_changed", table_names != optimal_order)
.emit();
}
let profile = join_profile_enabled();
let mut scan_times: Vec<(String, std::time::Duration)> = Vec::new();
let mut join_times: Vec<(String, std::time::Duration, usize, usize)> = Vec::new();
if profile {
DebugEvent::new(Category::Execution, "optimizer_time", "JOIN_PROFILE")
.text(format!("Optimizer time: {:?}", optimizer_time))
.field_duration_us("optimizer_time_us", optimizer_time)
.field_duration_ms("optimizer_time_ms", optimizer_time)
.emit();
}
let table_map: HashMap<String, graph::TableRef> = table_refs
.into_iter()
.map(|t| {
let key = t.alias.clone().unwrap_or_else(|| t.name.clone()).to_lowercase();
(key, t)
})
.collect();
let mut table_column_counts: HashMap<String, usize> = HashMap::new();
let mut result: Option<FromResult> = None;
let mut joined_tables: HashSet<String> = HashSet::new();
let mut applied_conditions: HashSet<usize> = HashSet::new();
for table_name in &optimal_order {
let table_ref = table_map.get(table_name).ok_or_else(|| {
ExecutorError::UnsupportedFeature(format!("Table not found in map: {}", table_name))
})?;
let table_filter = table_local_predicates
.get(&table_name.to_lowercase())
.and_then(|preds| utils::combine_predicates_with_qualification(preds, &table_ref.name));
if std::env::var("SCAN_PUSHDOWN_DEBUG").is_ok() {
eprintln!(
"[SCAN_PUSHDOWN] Table {} has {} local predicates, filter: {:?}",
table_name,
table_local_predicates
.get(&table_name.to_lowercase())
.map(|p| p.len())
.unwrap_or(0),
table_filter
.as_ref()
.map(|f| format!("{:?}", f).chars().take(200).collect::<String>())
);
}
let bloom_ctx = if let Some(ref prev_result) = result {
let prev_row_count = prev_result.data.as_slice().len();
if profile {
eprintln!(
"[BLOOM_PREFILTER] Considering Bloom filter for table '{}': accumulated_rows={}, min_required={}, joined_tables={:?}",
table_name, prev_row_count, BLOOM_FILTER_MIN_ACCUMULATED_ROWS, joined_tables
);
}
if prev_row_count >= BLOOM_FILTER_MIN_ACCUMULATED_ROWS {
if let Some((acc_table, acc_col, new_col)) = find_connecting_join_condition(
table_name,
&joined_tables,
&join_conditions,
&applied_conditions,
&column_to_table,
) {
if let Some(col_idx) =
prev_result.schema.get_column_index(Some(&acc_table), &acc_col)
{
if let Some(bloom) = bloom_context::build_bloom_filter_from_rows(
prev_result.data.as_slice(),
col_idx,
0.01, ) {
if profile {
eprintln!(
"[BLOOM_PREFILTER] Built Bloom filter from {}.{} ({} rows) for scanning {}.{}",
acc_table, acc_col, prev_row_count, table_name, new_col
);
}
Some(bloom_context::BloomFilterScanContext::new(bloom, new_col))
} else {
None
}
} else {
None
}
} else {
None
}
} else {
None
}
} else {
None
};
let scan_start = std::time::Instant::now();
let table_result = if table_ref.is_subquery {
if let Some(subquery) = &table_ref.subquery {
execute_derived_table(
subquery,
table_name,
table_ref.column_aliases.as_ref(),
execute_subquery,
)?
} else {
return Err(ExecutorError::UnsupportedFeature(
"Subquery reference missing query".to_string(),
));
}
} else if bloom_ctx.is_some() {
execute_table_scan_with_bloom(
&table_ref.name,
table_ref.alias.as_ref(),
cte_results,
database,
table_filter.as_ref(),
bloom_ctx.as_ref(),
)?
} else {
execute_table_scan(
&table_ref.name,
table_ref.alias.as_ref(),
None, cte_results,
database,
table_filter.as_ref(),
None,
None,
outer_row,
outer_schema,
)?
};
let scan_time = scan_start.elapsed();
if profile {
let scan_rows = table_result.data.as_slice().len();
scan_times.push((format!("{} ({} rows)", table_name, scan_rows), scan_time));
}
let col_count = if let Some((_, schema)) = table_result.schema.get_table(table_name) {
schema.columns.len()
} else {
table_result.schema.total_columns
};
table_column_counts.insert(table_name.clone(), col_count);
if let Some(prev_result) = result {
let mut applicable_conditions: Vec<Expression> = Vec::new();
for (idx, condition) in join_conditions.iter().enumerate() {
if applied_conditions.contains(&idx) {
continue;
}
let mut referenced_tables = HashSet::new();
graph::extract_referenced_tables_with_schema(
condition,
&mut referenced_tables,
&table_set,
&column_to_table,
);
let references_new_table = referenced_tables.contains(&table_name.to_lowercase());
let references_joined_table =
referenced_tables.iter().any(|t| joined_tables.contains(t));
if references_new_table && references_joined_table {
applicable_conditions.push(condition.clone());
applied_conditions.insert(idx);
} else if column_to_table.is_empty()
&& joined_tables.len() == 1
&& referenced_tables.iter().all(|t| t == graph::OUTER_REF_MARKER)
{
if std::env::var("JOIN_REORDER_VERBOSE").is_ok() {
eprintln!("[JOIN_REORDER] CTE fallback: including condition {:?} for 2-table join", condition);
}
applicable_conditions.push(condition.clone());
applied_conditions.insert(idx);
}
}
if std::env::var("JOIN_REORDER_VERBOSE").is_ok() {
eprintln!(
"[JOIN_REORDER] Joining {} to {:?}, found {} applicable conditions",
table_name,
joined_tables,
applicable_conditions.len()
);
eprintln!("[JOIN_REORDER] join_conditions total: {}", join_conditions.len());
for (idx, cond) in join_conditions.iter().enumerate() {
if !applied_conditions.contains(&idx) {
let mut refs = HashSet::new();
graph::extract_referenced_tables_with_schema(
cond,
&mut refs,
&table_set,
&column_to_table,
);
eprintln!("[JOIN_REORDER] cond[{}] refs: {:?}, new_table: {}, matches_new: {}, matches_joined: {}",
idx, refs, table_name.to_lowercase(),
refs.contains(&table_name.to_lowercase()),
refs.iter().any(|t| joined_tables.contains(t)));
}
}
}
let join_type = &vibesql_ast::JoinType::Inner;
let timeout_ctx = TimeoutContext::new_default();
let left_rows = prev_result.data.as_slice().len();
let right_rows = table_result.data.as_slice().len();
let join_start = std::time::Instant::now();
result = Some(nested_loop_join(
prev_result,
table_result,
join_type,
&None, false, None, database,
&applicable_conditions, &timeout_ctx,
cte_results,
None, None,
)?);
let join_time = join_start.elapsed();
let result_rows = result.as_ref().map(|r| r.data.as_slice().len()).unwrap_or(0);
if profile {
join_times.push((
table_name.clone(),
join_time,
left_rows * right_rows,
result_rows,
));
}
} else {
result = Some(table_result);
}
joined_tables.insert(table_name.to_lowercase());
}
let result =
result.ok_or_else(|| ExecutorError::UnsupportedFeature("No tables in join".to_string()))?;
let column_permutation =
utils::build_column_permutation(&table_names, &optimal_order, &table_column_counts);
let reorder_start = std::time::Instant::now();
let rows = result.data.into_rows();
let reordered_rows: Vec<vibesql_storage::Row> = rows
.into_iter()
.map(|row| {
let mut new_values = Vec::with_capacity(row.values.len());
for &idx in &column_permutation {
new_values.push(row.values[idx].clone());
}
if let Some(row_ids) = row.row_ids {
vibesql_storage::Row::with_row_ids(new_values, row_ids)
} else if let Some(row_id) = row.row_id {
vibesql_storage::Row::with_row_id(new_values, row_id)
} else {
vibesql_storage::Row::new(new_values)
}
})
.collect();
let reorder_time = reorder_start.elapsed();
if profile {
let total_scan: std::time::Duration = scan_times.iter().map(|(_, t)| *t).sum();
let total_join: std::time::Duration = join_times.iter().map(|(_, t, _, _)| *t).sum();
let grand_total = total_scan + total_join + reorder_time;
let scan_data: Vec<(String, JsonValue)> = scan_times
.iter()
.map(|(name, time)| {
(
name.clone(),
JsonValue::Object(vec![
("duration_us".to_string(), JsonValue::Int(time.as_micros() as i64)),
("duration_ms".to_string(), JsonValue::Number(time.as_secs_f64() * 1000.0)),
]),
)
})
.collect();
let join_data: Vec<(String, JsonValue)> = join_times
.iter()
.map(|(name, time, cartesian, result_rows)| {
(
name.clone(),
JsonValue::Object(vec![
("duration_us".to_string(), JsonValue::Int(time.as_micros() as i64)),
("duration_ms".to_string(), JsonValue::Number(time.as_secs_f64() * 1000.0)),
("cartesian_product".to_string(), JsonValue::Int(*cartesian as i64)),
("result_rows".to_string(), JsonValue::Int(*result_rows as i64)),
]),
)
})
.collect();
let scan_breakdown: String = scan_times
.iter()
.map(|(name, time)| format!(" {}: {:?}", name, time))
.collect::<Vec<_>>()
.join("\n");
DebugEvent::new(Category::Execution, "join_profile_summary", "JOIN_PROFILE")
.text(format!(
"=== Multi-way JOIN Timing Breakdown ===\n\
Table scans ({} tables): {:?}\n\
{}\n\
Total join time: {:?}\n\
Column reorder: {:?} ({} rows)\n\
Grand total: {:?}",
scan_times.len(),
total_scan,
scan_breakdown,
total_join,
reorder_time,
reordered_rows.len(),
grand_total
))
.field_int("table_count", scan_times.len() as i64)
.field_int("join_count", join_times.len() as i64)
.field_duration_us("total_scan_time_us", total_scan)
.field_duration_us("total_join_time_us", total_join)
.field_duration_us("reorder_time_us", reorder_time)
.field_duration_us("grand_total_us", grand_total)
.field_int("result_row_count", reordered_rows.len() as i64)
.field("scans", JsonValue::Object(scan_data))
.field("joins", JsonValue::Object(join_data))
.emit();
}
let mut new_schema =
utils::build_reordered_schema(&result.schema, &table_names, &optimal_order);
new_schema.duplicate_aliases.extend(duplicate_table_aliases);
Ok(FromResult::from_rows(new_schema, reordered_rows))
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn execute_with_semi_join_reordering<F>(
from: &FromClause,
cte_results: &HashMap<String, CteResult>,
database: &vibesql_storage::Database,
where_clause: Option<&Expression>,
outer_row: Option<&vibesql_storage::Row>,
outer_schema: Option<&CombinedSchema>,
execute_subquery: F,
) -> Result<FromResult, ExecutorError>
where
F: Fn(&vibesql_ast::SelectStmt) -> Result<SelectResult, ExecutorError> + Copy,
{
if let Some(where_expr) = where_clause {
if is_constant_false_where(where_expr) == Some(true) {
let mut table_refs = Vec::new();
graph::flatten_join_tree(from, &mut table_refs);
let schema = utils::build_schema_from_table_refs(&table_refs, database, cte_results)?;
return Ok(FromResult::from_rows(schema, Vec::new()));
}
}
let (inner_from, derived_query, derived_alias, semi_join_type, semi_condition) = match from {
FromClause::Join { left, right, join_type, condition, .. }
if matches!(join_type, vibesql_ast::JoinType::Semi | vibesql_ast::JoinType::Anti) =>
{
match right.as_ref() {
FromClause::Subquery { query, alias, .. } => {
(left.as_ref(), query, alias, join_type, condition)
}
_ => {
return Err(ExecutorError::UnsupportedFeature(
"execute_with_semi_join_reordering: right side must be subquery"
.to_string(),
));
}
}
}
_ => {
return Err(ExecutorError::UnsupportedFeature(
"execute_with_semi_join_reordering called with non-semi-join".to_string(),
));
}
};
let profile = join_profile_enabled();
if profile || std::env::var("SUBQUERY_TRANSFORM_VERBOSE").is_ok() {
eprintln!(
"[SEMI_JOIN_REORDER] Starting semi-join reordering for derived table: {}",
derived_alias
);
}
let derived_start = std::time::Instant::now();
let derived_result =
execute_derived_table(derived_query, derived_alias, None, execute_subquery)?;
let derived_time = derived_start.elapsed();
let derived_row_count = derived_result.data.as_slice().len();
if profile {
eprintln!(
"[SEMI_JOIN_REORDER] Derived table '{}' executed: {} rows in {:?}",
derived_alias, derived_row_count, derived_time
);
}
if derived_row_count == 0 && matches!(*semi_join_type, vibesql_ast::JoinType::Semi) {
if profile {
eprintln!(
"[SEMI_JOIN_REORDER] Short-circuit: empty derived table means empty semi-join result"
);
}
let inner_schema = build_inner_schema_without_execution(
inner_from,
database,
cte_results,
outer_row,
outer_schema,
execute_subquery,
)?;
return Ok(FromResult::from_rows(inner_schema, vec![]));
}
let target_table = identify_semi_join_target_table(semi_condition, inner_from);
if profile {
eprintln!("[SEMI_JOIN_REORDER] Semi-join target table: {:?}", target_table);
}
if let Some(target_name) = target_table {
return execute_with_early_semi_join(
inner_from,
&target_name,
derived_result,
semi_join_type,
semi_condition,
cte_results,
database,
where_clause,
outer_row,
outer_schema,
execute_subquery,
profile,
);
}
let inner_start = std::time::Instant::now();
let inner_result = execute_with_join_reordering(
inner_from,
cte_results,
database,
where_clause,
outer_row,
outer_schema,
execute_subquery,
)?;
let inner_time = inner_start.elapsed();
let inner_row_count = inner_result.data.as_slice().len();
if profile {
eprintln!(
"[SEMI_JOIN_REORDER] Inner join completed: {} rows in {:?}",
inner_row_count, inner_time
);
}
let timeout_ctx = TimeoutContext::new_default();
let semi_start = std::time::Instant::now();
let result = crate::select::join::nested_loop_join(
inner_result,
derived_result,
semi_join_type,
semi_condition,
false, None, database,
&[], &timeout_ctx,
cte_results,
None, None,
)?;
let semi_time = semi_start.elapsed();
if profile {
eprintln!(
"[SEMI_JOIN_REORDER] Semi-join applied: {} rows in {:?}",
result.data.as_slice().len(),
semi_time
);
eprintln!("[SEMI_JOIN_REORDER] Total time: {:?}", derived_time + inner_time + semi_time);
}
Ok(result)
}
#[allow(clippy::too_many_arguments)]
fn build_inner_schema_without_execution<F>(
inner_from: &FromClause,
database: &vibesql_storage::Database,
cte_results: &HashMap<String, CteResult>,
_outer_row: Option<&vibesql_storage::Row>,
_outer_schema: Option<&CombinedSchema>,
_execute_subquery: F,
) -> Result<CombinedSchema, ExecutorError>
where
F: Fn(&vibesql_ast::SelectStmt) -> Result<SelectResult, ExecutorError> + Copy,
{
let mut table_refs = Vec::new();
super::graph::flatten_join_tree(inner_from, &mut table_refs);
let mut result_schema: Option<CombinedSchema> = None;
for table_ref in &table_refs {
let table_name = &table_ref.name;
let alias = table_ref.alias.as_ref().unwrap_or(table_name).clone();
if let Some((cte_schema, _)) = cte_results.get(&table_name.to_lowercase()) {
let schema = cte_schema.clone();
match result_schema.take() {
None => result_schema = Some(CombinedSchema::from_table(alias, schema)),
Some(existing) => {
result_schema = Some(CombinedSchema::combine(existing, alias, schema))
}
}
continue;
}
if let Some(table) = database.get_table(table_name) {
let schema = table.schema.clone();
match result_schema.take() {
None => result_schema = Some(CombinedSchema::from_table(alias, schema)),
Some(existing) => {
result_schema = Some(CombinedSchema::combine(existing, alias, schema))
}
}
}
}
result_schema.ok_or_else(|| {
ExecutorError::UnsupportedFeature("No tables found in inner FROM clause".to_string())
})
}
fn identify_semi_join_target_table(
condition: &Option<Expression>,
inner_from: &FromClause,
) -> Option<String> {
let condition = condition.as_ref()?;
let mut table_refs = Vec::new();
super::graph::flatten_join_tree(inner_from, &mut table_refs);
let table_names: Vec<String> = table_refs
.iter()
.map(|t| t.alias.clone().unwrap_or_else(|| t.name.clone()).to_lowercase())
.collect();
find_inner_table_in_condition(condition, &table_names)
}
fn find_inner_table_in_condition(expr: &Expression, inner_tables: &[String]) -> Option<String> {
match expr {
Expression::BinaryOp { left, right, .. } => {
if let Some(t) = find_inner_table_in_condition(left, inner_tables) {
return Some(t);
}
find_inner_table_in_condition(right, inner_tables)
}
Expression::ColumnRef(col_id)
if col_id.schema_canonical().is_none() && col_id.table_canonical().is_some() =>
{
let t = col_id.table_canonical().unwrap();
let t_lower = t.to_lowercase();
if inner_tables.contains(&t_lower) {
return Some(t_lower);
}
infer_table_from_column(col_id.column_canonical(), inner_tables)
}
Expression::ColumnRef(col_id)
if col_id.schema_canonical().is_none() && col_id.table_canonical().is_none() =>
{
infer_table_from_column(col_id.column_canonical(), inner_tables)
}
_ => None,
}
}
fn infer_table_from_column(column: &str, inner_tables: &[String]) -> Option<String> {
let col_lower = column.to_lowercase();
let prefix_to_table = [
("o_", "orders"),
("c_", "customer"),
("l_", "lineitem"),
("p_", "part"),
("s_", "supplier"),
("ps_", "partsupp"),
("n_", "nation"),
("r_", "region"),
];
for (prefix, table) in &prefix_to_table {
if col_lower.starts_with(prefix) && inner_tables.contains(&table.to_string()) {
return Some(table.to_string());
}
}
None
}
#[allow(clippy::too_many_arguments)]
fn execute_with_early_semi_join<F>(
inner_from: &FromClause,
target_table: &str,
derived_result: FromResult,
semi_join_type: &vibesql_ast::JoinType,
semi_condition: &Option<Expression>,
cte_results: &HashMap<String, CteResult>,
database: &vibesql_storage::Database,
where_clause: Option<&Expression>,
outer_row: Option<&vibesql_storage::Row>,
outer_schema: Option<&CombinedSchema>,
execute_subquery: F,
profile: bool,
) -> Result<FromResult, ExecutorError>
where
F: Fn(&vibesql_ast::SelectStmt) -> Result<SelectResult, ExecutorError> + Copy,
{
let mut table_refs = Vec::new();
super::graph::flatten_join_tree(inner_from, &mut table_refs);
let target_idx = table_refs.iter().position(|t| {
let name = t.alias.as_ref().unwrap_or(&t.name).to_lowercase();
name == target_table.to_lowercase()
});
let target_idx = match target_idx {
Some(idx) => idx,
None => {
if profile {
eprintln!(
"[SEMI_JOIN_REORDER] Target table '{}' not found in inner tables, falling back",
target_table
);
}
let inner_result = execute_with_join_reordering(
inner_from,
cte_results,
database,
where_clause,
outer_row,
outer_schema,
execute_subquery,
)?;
let timeout_ctx = TimeoutContext::new_default();
return crate::select::join::nested_loop_join(
inner_result,
derived_result,
semi_join_type,
semi_condition,
false,
None, database,
&[],
&timeout_ctx,
cte_results,
None, None,
);
}
};
let target_ref = &table_refs[target_idx];
if profile {
eprintln!(
"[SEMI_JOIN_REORDER] Applying early semi-join to table '{}' at index {}",
target_table, target_idx
);
}
let target_scan_start = std::time::Instant::now();
let target_result = execute_table_scan(
&target_ref.name,
target_ref.alias.as_ref(),
None, cte_results,
database,
None, None,
None,
outer_row,
outer_schema,
)?;
let target_scan_time = target_scan_start.elapsed();
if profile {
eprintln!(
"[SEMI_JOIN_REORDER] Target table scan: {} rows in {:?}",
target_result.data.as_slice().len(),
target_scan_time
);
}
let timeout_ctx = TimeoutContext::new_default();
let semi_join_start = std::time::Instant::now();
let filtered_result = crate::select::join::nested_loop_join(
target_result,
derived_result,
semi_join_type,
semi_condition,
false,
None, database,
&[],
&timeout_ctx,
cte_results,
None, None,
)?;
let semi_join_time = semi_join_start.elapsed();
let filtered_count = filtered_result.data.as_slice().len();
if profile {
eprintln!(
"[SEMI_JOIN_REORDER] Early semi-join applied: {} rows in {:?}",
filtered_count, semi_join_time
);
}
if filtered_count == 0 {
if profile {
eprintln!("[SEMI_JOIN_REORDER] Early semi-join produced 0 rows, short-circuiting");
}
let full_schema = build_inner_schema_without_execution(
inner_from,
database,
cte_results,
outer_row,
outer_schema,
execute_subquery,
)?;
return Ok(FromResult::from_rows(full_schema, vec![]));
}
let remaining_tables: Vec<_> = table_refs
.iter()
.enumerate()
.filter(|(i, _)| *i != target_idx)
.map(|(_, t)| t.clone())
.collect();
if profile {
eprintln!(
"[SEMI_JOIN_REORDER] Joining filtered result with {} remaining tables",
remaining_tables.len()
);
}
let table_names: Vec<String> =
table_refs.iter().map(|t| t.alias.as_ref().unwrap_or(&t.name).to_lowercase()).collect();
let column_to_table =
utils::build_column_to_table_map(database, &table_names, &table_refs, cte_results);
let target_alias = table_refs[target_idx]
.alias
.as_ref()
.unwrap_or(&table_refs[target_idx].name)
.to_lowercase();
let mut accumulated_tables: HashSet<String> = HashSet::new();
accumulated_tables.insert(target_alias);
let mut accumulated = filtered_result;
for table_ref in &remaining_tables {
let table_start = std::time::Instant::now();
let table_result = execute_table_scan(
&table_ref.name,
table_ref.alias.as_ref(),
None, cte_results,
database,
None,
None,
None,
outer_row,
outer_schema,
)?;
let table_scan_time = table_start.elapsed();
let new_table_alias = table_ref.alias.as_ref().unwrap_or(&table_ref.name).to_lowercase();
if profile {
eprintln!(
"[SEMI_JOIN_REORDER] Scanned '{}': {} rows in {:?}",
new_table_alias,
table_result.data.as_slice().len(),
table_scan_time
);
}
let equijoins = if let Some(where_expr) = where_clause {
let mut join_table_set = accumulated_tables.clone();
join_table_set.insert(new_table_alias.clone());
let all_equijoins = predicates::extract_where_equijoins_with_schema(
where_expr,
&join_table_set,
&column_to_table,
);
let applicable_equijoins: Vec<_> = all_equijoins
.into_iter()
.filter(|eq| {
let mut referenced = HashSet::new();
graph::extract_referenced_tables_with_schema(
eq,
&mut referenced,
&join_table_set,
&column_to_table,
);
referenced.contains(&new_table_alias)
&& referenced.iter().any(|t| accumulated_tables.contains(t))
})
.collect();
if profile && !applicable_equijoins.is_empty() {
eprintln!(
"[SEMI_JOIN_REORDER] Found {} equijoin predicates for joining '{}'",
applicable_equijoins.len(),
new_table_alias
);
}
applicable_equijoins
} else {
Vec::new()
};
let (join_type, join_condition) = if !equijoins.is_empty() {
(vibesql_ast::JoinType::Inner, None)
} else {
(vibesql_ast::JoinType::Cross, None)
};
let join_start = std::time::Instant::now();
accumulated = crate::select::join::nested_loop_join(
accumulated,
table_result,
&join_type,
&join_condition,
false,
None, database,
&equijoins,
&timeout_ctx,
cte_results,
None, None,
)?;
let join_time = join_start.elapsed();
accumulated_tables.insert(new_table_alias.clone());
if profile {
eprintln!(
"[SEMI_JOIN_REORDER] After join with '{}': {} rows in {:?}",
new_table_alias,
accumulated.data.as_slice().len(),
join_time
);
}
}
Ok(accumulated)
}
const BLOOM_FILTER_MIN_ACCUMULATED_ROWS: usize = 10;
#[allow(dead_code)] const BLOOM_FILTER_MIN_SCAN_ROWS: usize = 1000;
fn find_connecting_join_condition(
new_table: &str,
joined_tables: &HashSet<String>,
join_conditions: &[Expression],
applied_conditions: &HashSet<usize>,
column_to_table: &HashMap<String, String>,
) -> Option<(String, String, String)> {
let new_table_lower = new_table.to_lowercase();
let profile =
std::env::var("JOIN_PROFILE").is_ok() || std::env::var("BLOOM_PREFILTER_DEBUG").is_ok();
if profile {
eprintln!(
"[BLOOM_PREFILTER] Looking for join condition connecting '{}' to {:?} ({} conditions, {} applied)",
new_table_lower, joined_tables, join_conditions.len(), applied_conditions.len()
);
}
for (idx, condition) in join_conditions.iter().enumerate() {
if applied_conditions.contains(&idx) {
continue;
}
if let Some((left_table, left_col, right_table, right_col)) =
bloom_context::extract_equijoin_columns(condition, column_to_table)
{
if profile {
eprintln!(
"[BLOOM_PREFILTER] Checking condition {}: {}.{} = {}.{}",
idx, left_table, left_col, right_table, right_col
);
}
if left_table == new_table_lower && joined_tables.contains(&right_table) {
if profile {
eprintln!(
"[BLOOM_PREFILTER] -> MATCH: accumulated {}.{} for new {}.{}",
right_table, right_col, new_table_lower, left_col
);
}
return Some((right_table, right_col, left_col));
}
if right_table == new_table_lower && joined_tables.contains(&left_table) {
if profile {
eprintln!(
"[BLOOM_PREFILTER] -> MATCH: accumulated {}.{} for new {}.{}",
left_table, left_col, new_table_lower, right_col
);
}
return Some((left_table, left_col, right_col));
}
}
}
if profile {
eprintln!("[BLOOM_PREFILTER] -> No matching condition found");
}
None
}