use std::collections::HashMap;
use vibesql_ast::{Expression, FromClause, SelectItem};
use crate::{schema::CombinedSchema, select::cte::CteResult};
fn extract_column_names_from_select_list(select_list: &[SelectItem]) -> Vec<String> {
let mut columns = Vec::new();
for item in select_list {
match item {
SelectItem::Wildcard { .. } | SelectItem::QualifiedWildcard { .. } => {
return Vec::new();
}
SelectItem::Expression { expr, alias, .. } => {
let col_name = if let Some(a) = alias {
a.to_lowercase()
} else {
extract_column_name_from_expr(expr).to_lowercase()
};
columns.push(col_name);
}
}
}
columns
}
fn extract_column_name_from_expr(expr: &Expression) -> String {
match expr {
Expression::ColumnRef(col_id) => col_id.column_canonical().to_string(),
Expression::AggregateFunction { name, args, .. } => {
if args.len() == 1 {
if let Expression::ColumnRef(col_id) = &args[0] {
return col_id.column_canonical().to_string();
}
}
name.to_string()
}
Expression::Function { name, .. } => name.to_string(),
Expression::BinaryOp { left, .. } => extract_column_name_from_expr(left),
Expression::Wildcard => "*".to_string(),
Expression::Collate { expr, .. } => extract_column_name_from_expr(expr),
_ => "?column?".to_string(),
}
}
pub(crate) fn should_apply_join_reordering(table_count: usize) -> bool {
if table_count < 2 {
return false;
}
std::env::var("JOIN_REORDER_DISABLED").is_err()
}
pub(crate) fn count_tables_in_from(from: &FromClause) -> usize {
match from {
FromClause::Table { .. } => 1,
FromClause::Subquery { .. } => 1,
FromClause::Values { .. } => 1,
FromClause::Join { left, right, .. } => {
count_tables_in_from(left) + count_tables_in_from(right)
}
}
}
pub(crate) fn all_joins_are_cross(from: &FromClause) -> bool {
match from {
FromClause::Table { .. } | FromClause::Subquery { .. } | FromClause::Values { .. } => true,
FromClause::Join { left, right, join_type, condition, natural, using_columns, .. } => {
matches!(join_type, vibesql_ast::JoinType::Cross)
&& condition.is_none()
&& !*natural
&& using_columns.is_none()
&& all_joins_are_cross(left)
&& all_joins_are_cross(right)
}
}
}
pub(super) fn build_reordered_schema(
current_schema: &CombinedSchema,
original_order: &[String],
_optimal_order: &[String],
) -> CombinedSchema {
let mut new_table_schemas = HashMap::new();
let mut current_position = 0;
for table_name in original_order {
let table_schema = current_schema.get_table(table_name).map(|(_, schema)| schema.clone());
if let Some(schema) = table_schema {
let col_count = schema.columns.len();
let table_id = vibesql_catalog::TableIdentifier::unquoted(table_name);
new_table_schemas.insert(table_id, (current_position, schema));
current_position += col_count;
}
}
CombinedSchema {
table_schemas: new_table_schemas,
total_columns: current_position,
hidden_columns: std::collections::HashSet::new(),
outer_schema: None,
duplicate_aliases: std::collections::HashSet::new(),
joined_columns: std::collections::HashSet::new(),
using_coalesce_indices: std::collections::HashMap::new(),
column_replacement_map: std::collections::HashMap::new(),
alias_tables: std::collections::HashSet::new(),
shadowed_tables: std::collections::HashMap::new(),
}
}
pub(super) fn build_column_permutation(
original_order: &[String],
optimal_order: &[String],
column_counts: &HashMap<String, usize>,
) -> Vec<usize> {
let mut optimal_positions: HashMap<String, usize> = HashMap::new();
let mut current_position = 0;
for table in optimal_order {
optimal_positions.insert(table.clone(), current_position);
current_position += column_counts.get(table).unwrap_or(&0);
}
let mut permutation = Vec::new();
for table in original_order {
let table_lower = table.to_lowercase();
let start_pos = optimal_positions.get(&table_lower).unwrap_or(&0);
let col_count = column_counts.get(&table_lower).unwrap_or(&0);
for i in 0..*col_count {
permutation.push(start_pos + i);
}
}
permutation
}
pub(super) fn build_column_to_table_map(
database: &vibesql_storage::Database,
table_names: &[String],
table_refs: &[super::graph::TableRef],
cte_results: &HashMap<String, CteResult>,
) -> HashMap<String, String> {
let mut column_to_table: HashMap<String, Vec<String>> = HashMap::new();
if std::env::var("JOIN_REORDER_VERBOSE").is_ok() {
eprintln!(
"[JOIN_REORDER] build_column_to_table_map: database.tables.keys(): {:?}",
database.tables.keys().collect::<Vec<_>>()
);
eprintln!(
"[JOIN_REORDER] build_column_to_table_map: table_refs: {:?}",
table_refs.iter().map(|r| (&r.name, &r.alias)).collect::<Vec<_>>()
);
}
for table_name in table_names {
let table_lower = table_name.to_lowercase();
let table_ref = table_refs.iter().find(|r| {
r.alias.as_ref().map(|a| a.to_lowercase()) == Some(table_lower.clone())
|| r.name.to_lowercase() == table_lower
});
let column_names: Vec<String> = if let Some(tr) = table_ref {
if tr.is_subquery {
if let Some(subquery) = &tr.subquery {
extract_column_names_from_select_list(&subquery.select_list)
} else {
continue;
}
} else if let Some(cte_result) = cte_results
.get(&tr.name)
.or_else(|| cte_results.get(&tr.name.to_lowercase()))
.or_else(|| cte_results.get(&tr.name.to_uppercase()))
{
cte_result.0.columns.iter().map(|c| c.name.to_lowercase()).collect()
} else {
let actual_table_name = &tr.name;
let schema_prefixed =
format!("{}.{}", vibesql_catalog::DEFAULT_SCHEMA, actual_table_name);
let table = database
.tables
.get(actual_table_name)
.or_else(|| database.tables.get(&actual_table_name.to_lowercase()))
.or_else(|| database.tables.get(&actual_table_name.to_uppercase()))
.or_else(|| database.tables.get(&schema_prefixed))
.or_else(|| database.tables.get(&schema_prefixed.to_lowercase()))
.or_else(|| database.tables.get(&schema_prefixed.to_uppercase()))
.or_else(|| {
let target = actual_table_name.to_lowercase();
database
.tables
.iter()
.find(|(k, _)| {
let key_lower = k.to_lowercase();
key_lower == target || key_lower.ends_with(&format!(".{}", target))
})
.map(|(_, v)| v)
});
if let Some(t) = table {
t.schema.columns.iter().map(|c| c.name.to_lowercase()).collect()
} else {
continue;
}
}
} else {
let schema_prefixed = format!("{}.{}", vibesql_catalog::DEFAULT_SCHEMA, table_name);
let table = database
.tables
.get(table_name)
.or_else(|| database.tables.get(&table_lower))
.or_else(|| database.tables.get(&table_name.to_uppercase()))
.or_else(|| database.tables.get(&schema_prefixed))
.or_else(|| database.tables.get(&schema_prefixed.to_lowercase()))
.or_else(|| database.tables.get(&schema_prefixed.to_uppercase()))
.or_else(|| {
database
.tables
.iter()
.find(|(k, _)| {
let key_lower = k.to_lowercase();
key_lower == table_lower
|| key_lower.ends_with(&format!(".{}", table_lower))
})
.map(|(_, v)| v)
});
if let Some(t) = table {
t.schema.columns.iter().map(|c| c.name.to_lowercase()).collect()
} else {
continue;
}
};
for col_name in column_names {
column_to_table.entry(col_name).or_default().push(table_lower.clone());
}
}
let mut result: HashMap<String, String> = HashMap::new();
for (col, tables) in column_to_table {
if tables.len() == 1 {
result.insert(col, tables.into_iter().next().unwrap());
}
}
result
}
pub(super) fn combine_predicates_with_qualification(
predicates: &[vibesql_ast::Expression],
table_name: &str,
) -> Option<vibesql_ast::Expression> {
match predicates.len() {
0 => None,
1 => Some(qualify_columns(&predicates[0], table_name)),
_ => {
let mut result = qualify_columns(&predicates[0], table_name);
for pred in &predicates[1..] {
result = vibesql_ast::Expression::BinaryOp {
op: vibesql_ast::BinaryOperator::And,
left: Box::new(result),
right: Box::new(qualify_columns(pred, table_name)),
};
}
Some(result)
}
}
}
fn qualify_columns(expr: &vibesql_ast::Expression, table_name: &str) -> vibesql_ast::Expression {
use vibesql_ast::Expression;
let table_name_lower = table_name.to_lowercase();
match expr {
Expression::ColumnRef(col_id)
if col_id.schema_canonical().is_none() && col_id.table_canonical().is_none() =>
{
Expression::ColumnRef(vibesql_ast::ColumnIdentifier::qualified(
&table_name_lower,
false,
col_id.column_canonical(),
false,
))
}
Expression::ColumnRef(col_id)
if col_id.schema_canonical().is_none() && col_id.table_canonical().is_some() =>
{
expr.clone()
}
Expression::ColumnRef(_) => {
expr.clone()
}
Expression::BinaryOp { op, left, right } => Expression::BinaryOp {
op: *op,
left: Box::new(qualify_columns(left, table_name)),
right: Box::new(qualify_columns(right, table_name)),
},
Expression::UnaryOp { op, expr: inner } => {
Expression::UnaryOp { op: *op, expr: Box::new(qualify_columns(inner, table_name)) }
}
Expression::InList { expr: inner, values, negated } => Expression::InList {
expr: Box::new(qualify_columns(inner, table_name)),
values: values.iter().map(|v| qualify_columns(v, table_name)).collect(),
negated: *negated,
},
Expression::Between { expr: inner, low, high, negated, symmetric } => Expression::Between {
expr: Box::new(qualify_columns(inner, table_name)),
low: Box::new(qualify_columns(low, table_name)),
high: Box::new(qualify_columns(high, table_name)),
negated: *negated,
symmetric: *symmetric,
},
_ => expr.clone(),
}
}
pub(super) fn resolve_column_to_table(
column: &str,
column_to_table: &HashMap<String, String>,
) -> Option<String> {
column_to_table.get(&column.to_lowercase()).cloned()
}
pub(super) fn resolve_column_with_fallback(
column: &str,
column_to_table: &HashMap<String, String>,
) -> Option<String> {
resolve_column_to_table(column, column_to_table)
}
pub(super) fn build_schema_from_table_refs(
table_refs: &[super::graph::TableRef],
database: &vibesql_storage::Database,
cte_results: &HashMap<String, CteResult>,
) -> Result<CombinedSchema, crate::errors::ExecutorError> {
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(|| {
crate::errors::ExecutorError::UnsupportedFeature(
"No tables found in FROM clause".to_string(),
)
})
}