mod join_analysis;
mod predicate;
mod prefix;
mod select_analysis;
#[cfg(test)]
mod tests;
mod transform;
mod types;
use std::collections::HashSet;
use vibesql_ast::{Expression, SelectItem, SelectStmt};
use join_analysis::{extract_equijoin_tables, extract_equijoin_tables_from_joins};
use predicate::extract_local_predicates;
use prefix::build_column_prefix_map;
use select_analysis::{
collect_unqualified_columns, extract_tables_from_select, has_global_aggregates,
};
use transform::{
add_exists_to_where, build_exists_checks, flatten_from_clause, rebuild_from_clause,
remove_eliminated_predicates,
};
use types::EliminatedTable;
pub fn eliminate_unused_tables(stmt: &SelectStmt) -> SelectStmt {
if std::env::var("TABLE_ELIM_DISABLED").is_ok() {
return stmt.clone();
}
let verbose = std::env::var("TABLE_ELIM_VERBOSE").is_ok();
let from = match &stmt.from {
Some(f) => f,
None => return stmt.clone(),
};
let mut tables = Vec::new();
flatten_from_clause(from, &mut tables);
if verbose {
eprintln!(
"[TABLE_ELIM_OPT] Analyzing {} tables: {:?}",
tables.len(),
tables.iter().map(|t| &t.name).collect::<Vec<_>>()
);
}
if tables.len() < 2 {
return stmt.clone();
}
let is_select_literal = stmt.select_list.len() == 1
&& matches!(
&stmt.select_list[0],
SelectItem::Expression { expr: Expression::Literal(_), .. }
);
if is_select_literal {
if verbose {
eprintln!("[TABLE_ELIM_OPT] Skipping: SELECT literal subquery");
}
return stmt.clone();
}
let table_names: HashSet<String> =
tables.iter().map(|t| t.alias.as_ref().unwrap_or(&t.name).to_lowercase()).collect();
if stmt.group_by.is_none() && has_global_aggregates(&stmt.select_list, &table_names) {
if verbose {
eprintln!("[TABLE_ELIM_OPT] Skipping: query has global aggregates without GROUP BY");
}
return stmt.clone();
}
let unqualified_columns = collect_unqualified_columns(&stmt.select_list);
if verbose && !unqualified_columns.is_empty() {
eprintln!(
"[TABLE_ELIM_OPT] Found {} unqualified columns in SELECT: {:?}",
unqualified_columns.len(),
unqualified_columns.iter().take(5).collect::<Vec<_>>()
);
}
let table_column_prefixes = build_column_prefix_map(stmt, &table_names);
if verbose && !table_column_prefixes.is_empty() {
eprintln!("[TABLE_ELIM_OPT] Column prefixes: {:?}", table_column_prefixes);
}
let select_tables = extract_tables_from_select(&stmt.select_list, &table_names);
if verbose {
eprintln!("[TABLE_ELIM_OPT] Tables in SELECT: {:?}", select_tables);
}
if !unqualified_columns.is_empty() {
let known_prefixes: Vec<_> = table_column_prefixes.values().collect();
let has_unknown_columns = unqualified_columns.iter().any(|col| {
let col_lower = col.to_lowercase();
!known_prefixes.iter().any(|prefix| col_lower.starts_with(*prefix))
});
if has_unknown_columns {
if verbose {
eprintln!(
"[TABLE_ELIM_OPT] Skipping: unqualified columns don't match any known prefix"
);
}
return stmt.clone();
}
}
let mut equijoin_tables = if let Some(where_expr) = &stmt.where_clause {
extract_equijoin_tables(where_expr, &table_names, &table_column_prefixes)
} else {
HashSet::new()
};
if let Some(from_clause) = &stmt.from {
let on_condition_tables =
extract_equijoin_tables_from_joins(from_clause, &table_names, &table_column_prefixes);
equijoin_tables.extend(on_condition_tables);
}
if verbose {
eprintln!("[TABLE_ELIM_OPT] Tables in equijoins: {:?}", equijoin_tables);
}
let local_predicates = if let Some(where_expr) = &stmt.where_clause {
extract_local_predicates(where_expr, &table_names, &table_column_prefixes)
} else {
std::collections::HashMap::new()
};
let mut eliminated = Vec::new();
let mut kept_tables = Vec::new();
for table in tables {
let table_key = table.alias.as_ref().unwrap_or(&table.name).to_lowercase();
let in_select_qualified = select_tables.contains(&table_key);
let in_select_unqualified = if !unqualified_columns.is_empty() {
if let Some(prefix) = table_column_prefixes.get(&table_key) {
let matches_prefix =
unqualified_columns.iter().any(|col| col.to_lowercase().starts_with(prefix));
if verbose && matches_prefix {
eprintln!(
"[TABLE_ELIM_OPT] Table '{}' might be referenced by unqualified cols (prefix '{}')",
table_key, prefix
);
}
matches_prefix
} else {
if verbose {
eprintln!(
"[TABLE_ELIM_OPT] Table '{}' has no known prefix, conservatively keeping",
table_key
);
}
true
}
} else {
false
};
let in_select = in_select_qualified || in_select_unqualified;
let in_equijoin = equijoin_tables.contains(&table_key);
if verbose {
eprintln!(
"[TABLE_ELIM_OPT] Table '{}': in_select={} (qualified={}, unqualified={}), in_equijoin={}",
table_key, in_select, in_select_qualified, in_select_unqualified, in_equijoin
);
}
let filter = local_predicates.get(&table_key).cloned();
if !in_select && !in_equijoin && filter.is_some() {
if verbose {
eprintln!(
"[TABLE_ELIM_OPT] ✓ Eliminating table '{}' with filter: {:?}",
table_key, filter
);
}
eliminated.push(EliminatedTable {
name: table.name.clone(),
alias: table.alias.clone(),
filter,
});
} else {
if verbose && !in_select && !in_equijoin && filter.is_none() {
eprintln!(
"[TABLE_ELIM_OPT] ✗ Keeping table '{}': no filter (cross join multiplies rows)",
table_key
);
}
kept_tables.push(table);
}
}
if eliminated.is_empty() {
return stmt.clone();
}
if kept_tables.is_empty() {
if verbose {
eprintln!(
"[TABLE_ELIM_OPT] Skipping: would eliminate all tables, leaving no FROM clause"
);
}
return stmt.clone();
}
let new_from = rebuild_from_clause(&kept_tables);
let exists_checks = build_exists_checks(&eliminated);
let eliminated_names: HashSet<String> =
eliminated.iter().map(|t| t.alias.as_ref().unwrap_or(&t.name).to_lowercase()).collect();
let eliminated_prefixes: HashSet<String> =
eliminated_names.iter().filter_map(|t| table_column_prefixes.get(t).cloned()).collect();
let filtered_where = if let Some(where_expr) = &stmt.where_clause {
remove_eliminated_predicates(where_expr, &eliminated_names, &eliminated_prefixes)
} else {
None
};
let new_where = add_exists_to_where(filtered_where.as_ref(), exists_checks);
if verbose {
eprintln!("[TABLE_ELIM_OPT] Applied elimination: {} tables removed", eliminated.len());
}
SelectStmt {
with_clause: stmt.with_clause.clone(),
distinct: stmt.distinct,
select_list: stmt.select_list.clone(),
into_table: stmt.into_table.clone(),
into_variables: stmt.into_variables.clone(),
from: new_from,
where_clause: new_where,
group_by: stmt.group_by.clone(),
having: stmt.having.clone(),
window_definitions: stmt.window_definitions.clone(),
order_by: stmt.order_by.clone(),
limit: stmt.limit.clone(),
offset: stmt.offset.clone(),
set_operation: stmt.set_operation.clone(),
values: stmt.values.clone(),
}
}