use vibesql_ast::{RenameTableStmt, TriggerAction};
use vibesql_storage::Database;
use crate::{
errors::ExecutorError,
trigger_rename::{rewrite_column_refs_in_trigger_sql, rewrite_table_refs_in_trigger_sql},
};
pub(super) fn execute_rename_table(
stmt: &RenameTableStmt,
database: &mut Database,
) -> Result<String, ExecutorError> {
if crate::sqlite_schema::is_reserved_object_name(&stmt.new_table_name) {
return Err(ExecutorError::SqliteCompatError(format!(
"object name reserved for internal use: {}",
stmt.new_table_name
)));
}
if database.get_table(&stmt.new_table_name).is_some()
|| database.index_exists(&stmt.new_table_name)
{
return Err(ExecutorError::RenameTargetExists(stmt.new_table_name.clone()));
}
let old_table = database
.get_table(&stmt.table_name)
.ok_or_else(|| ExecutorError::TableNotFound(stmt.table_name.clone()))?;
let mut new_table = old_table.clone();
new_table.schema_mut().name = stmt.new_table_name.clone();
let renamed_sql = new_table
.schema
.sql_source
.as_deref()
.and_then(|sql| crate::alter_rewrite::rename_table(sql, &stmt.new_table_name));
match renamed_sql {
Some(text) => new_table.schema_mut().set_sql_source(text),
None => new_table.schema_mut().invalidate_sql_source(),
}
database
.drop_table(&stmt.table_name)
.map_err(|e| ExecutorError::StorageError(e.to_string()))?;
database
.create_table(new_table.schema.clone())
.map_err(|e| ExecutorError::StorageError(e.to_string()))?;
let restored_table = database
.get_table_mut(&stmt.new_table_name)
.ok_or_else(|| ExecutorError::TableAlreadyExists(stmt.new_table_name.clone()))?;
for row in new_table.scan() {
restored_table
.insert(row.clone())
.map_err(|e| ExecutorError::StorageError(e.to_string()))?;
}
rewrite_triggers_for_rename(database, &stmt.table_name, &stmt.new_table_name);
database.invalidate_columnar_cache(&stmt.table_name);
database.invalidate_columnar_cache(&stmt.new_table_name);
Ok(format!("Table '{}' renamed to '{}'", stmt.table_name, stmt.new_table_name))
}
fn rewrite_triggers_for_rename(database: &mut Database, old_name: &str, new_name: &str) {
let trigger_names = database.catalog.list_triggers();
for name in trigger_names {
let Some(existing) = database.catalog.get_trigger(&name) else {
continue;
};
let on_target_match = existing.table_name.eq_ignore_ascii_case(old_name);
let body_text = match &existing.triggered_action {
TriggerAction::RawSql(sql) => sql.clone(),
};
let new_body = rewrite_table_refs_in_trigger_sql(&body_text, old_name, new_name);
let new_sql_definition = existing
.sql_definition
.as_ref()
.map(|sql| rewrite_table_refs_in_trigger_sql(sql, old_name, new_name));
let body_changed = new_body != body_text;
let sql_def_changed = new_sql_definition.as_deref() != existing.sql_definition.as_deref();
if !on_target_match && !body_changed && !sql_def_changed {
continue;
}
let mut updated = existing.clone();
if on_target_match {
updated.table_name = new_name.to_string();
}
if body_changed {
updated.triggered_action = TriggerAction::RawSql(new_body);
}
updated.sql_definition = new_sql_definition;
let _ = database.catalog.update_trigger(updated);
}
}
pub(super) fn rewrite_triggers_for_column_rename(
database: &mut Database,
table: &str,
old_column: &str,
new_column: &str,
) -> Result<(), ExecutorError> {
let schema: std::collections::HashMap<String, Vec<String>> = database
.list_tables()
.into_iter()
.filter_map(|name| {
database.get_table(&name).map(|tbl| {
(
name.to_ascii_lowercase(),
tbl.schema.columns.iter().map(|c| c.name.clone()).collect(),
)
})
})
.collect();
let renamed_table_lc = table.to_ascii_lowercase();
let old_column_owned = old_column.to_string();
let table_has_column = move |t: &str, c: &str| -> bool {
let t_lc = t.to_ascii_lowercase();
if t_lc == renamed_table_lc && c.eq_ignore_ascii_case(&old_column_owned) {
return true;
}
schema.get(&t_lc).is_some_and(|cols| cols.iter().any(|col| col.eq_ignore_ascii_case(c)))
};
let trigger_names = database.catalog.list_triggers();
let mut pending_updates = Vec::new();
for name in trigger_names {
let Some(existing) = database.catalog.get_trigger(&name) else {
continue;
};
let body_text = match &existing.triggered_action {
TriggerAction::RawSql(sql) => sql.clone(),
};
let ambiguity_error = |col: String| {
ExecutorError::Other(format!(
"error in trigger {}: ambiguous column name: {}",
name, col
))
};
let new_body = rewrite_column_refs_in_trigger_sql(
&body_text,
table,
old_column,
new_column,
&table_has_column,
)
.map_err(ambiguity_error)?;
let new_sql_definition = existing
.sql_definition
.as_ref()
.map(|sql| {
rewrite_column_refs_in_trigger_sql(
sql,
table,
old_column,
new_column,
&table_has_column,
)
})
.transpose()
.map_err(ambiguity_error)?;
let body_changed = new_body != body_text;
let sql_def_changed = new_sql_definition.as_deref() != existing.sql_definition.as_deref();
if !body_changed && !sql_def_changed {
continue;
}
let mut updated = existing.clone();
if body_changed {
updated.triggered_action = TriggerAction::RawSql(new_body);
}
if sql_def_changed {
updated.sql_definition = new_sql_definition;
}
pending_updates.push(updated);
}
for updated in pending_updates {
let _ = database.catalog.update_trigger(updated);
}
Ok(())
}