use std::collections::{HashMap, HashSet};
use vibesql_ast::{Expression, FromClause, SelectItem, SelectStmt};
pub(super) fn build_column_prefix_map(
stmt: &SelectStmt,
table_names: &HashSet<String>,
) -> HashMap<String, String> {
let mut table_columns: HashMap<String, Vec<String>> = HashMap::new();
collect_qualified_columns_from_select(&stmt.select_list, &mut table_columns);
if let Some(from) = &stmt.from {
collect_qualified_columns_from_from(from, &mut table_columns);
}
if let Some(where_expr) = &stmt.where_clause {
collect_qualified_columns_from_expr(where_expr, &mut table_columns);
}
let mut prefixes = HashMap::new();
for (table, columns) in &table_columns {
let table_lower = table.to_lowercase();
if !table_names.contains(&table_lower) {
continue; }
if let Some(prefix) = find_common_prefix(columns) {
prefixes.insert(table_lower, prefix);
}
}
for table_name in table_names {
if !prefixes.contains_key(table_name) {
if let Some(prefix) = derive_prefix_from_table_name(table_name) {
prefixes.insert(table_name.clone(), prefix);
}
}
}
prefixes
}
pub(super) fn derive_prefix_from_table_name(table_name: &str) -> Option<String> {
let name = table_name.to_lowercase();
if name.len() <= 3 && !name.contains('_') {
return Some(format!("{}_", name));
}
if name.ends_with("_dim") {
let first_char = name.chars().next()?;
return Some(format!("{}_", first_char));
}
if let Some(underscore_pos) = name.find('_') {
let first_word = &name[..underscore_pos];
let rest = &name[underscore_pos + 1..];
if !rest.is_empty() {
let mut prefix = String::new();
prefix.push(first_word.chars().next()?);
if let Some(second_first) = rest.chars().next() {
prefix.push(second_first);
}
prefix.push('_');
return Some(prefix);
}
}
let first_char = name.chars().next()?;
Some(format!("{}_", first_char))
}
fn collect_qualified_columns_from_select(
select_list: &[SelectItem],
table_columns: &mut HashMap<String, Vec<String>>,
) {
for item in select_list {
if let SelectItem::Expression { expr, .. } = item {
collect_qualified_columns_from_expr(expr, table_columns);
}
}
}
fn collect_qualified_columns_from_from(
from: &FromClause,
table_columns: &mut HashMap<String, Vec<String>>,
) {
match from {
FromClause::Table { .. } => {}
FromClause::Subquery { .. } => {}
FromClause::Values { .. } => {}
FromClause::Join { left, right, condition, .. } => {
collect_qualified_columns_from_from(left, table_columns);
collect_qualified_columns_from_from(right, table_columns);
if let Some(cond) = condition {
collect_qualified_columns_from_expr(cond, table_columns);
}
}
}
}
fn collect_qualified_columns_from_expr(
expr: &Expression,
table_columns: &mut HashMap<String, Vec<String>>,
) {
match expr {
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 column = col_id.column_canonical();
table_columns.entry(t.to_lowercase()).or_default().push(column.to_lowercase());
}
Expression::BinaryOp { left, right, .. } => {
collect_qualified_columns_from_expr(left, table_columns);
collect_qualified_columns_from_expr(right, table_columns);
}
Expression::UnaryOp { expr, .. } => {
collect_qualified_columns_from_expr(expr, table_columns);
}
Expression::Function { args, .. } | Expression::AggregateFunction { args, .. } => {
for arg in args {
collect_qualified_columns_from_expr(arg, table_columns);
}
}
Expression::InList { expr, values, .. } => {
collect_qualified_columns_from_expr(expr, table_columns);
for v in values {
collect_qualified_columns_from_expr(v, table_columns);
}
}
Expression::Case { operand, when_clauses, else_result } => {
if let Some(op) = operand {
collect_qualified_columns_from_expr(op, table_columns);
}
for clause in when_clauses {
for cond in &clause.conditions {
collect_qualified_columns_from_expr(cond, table_columns);
}
collect_qualified_columns_from_expr(&clause.result, table_columns);
}
if let Some(else_res) = else_result {
collect_qualified_columns_from_expr(else_res, table_columns);
}
}
Expression::IsNull { expr, .. } | Expression::Cast { expr, .. } => {
collect_qualified_columns_from_expr(expr, table_columns);
}
_ => {}
}
}
pub(super) fn find_common_prefix(columns: &[String]) -> Option<String> {
if columns.is_empty() {
return None;
}
let first = &columns[0];
if let Some(underscore_pos) = first.find('_') {
let prefix = &first[..=underscore_pos];
if columns.iter().all(|c| c.starts_with(prefix)) {
return Some(prefix.to_string());
}
}
if first.len() >= 2 {
let prefix = &first[..2];
if columns.iter().all(|c| c.starts_with(prefix)) {
return Some(prefix.to_string());
}
}
None
}