use crate::schema::columns::removal::drop_column;
use crate::schema::constraints::{
add_check_constraint, add_foreign_key_constraint, add_not_null_constraint, alter_constraint,
drop::drop_constraint, set_not_null_constraint, table_constraint_state, validate_constraint,
};
use uqa_sql::{
ast::{AlterTableAction, AlterTableStmt},
SQLError, SQLResult,
};
pub mod binding;
mod context;
pub mod entry;
mod locking;
mod recursion;
pub use context::*;
use recursion::{materialize_recursive_action_names, run_recursive_alter_action};
fn ddl_storage_error(action: &str, error: uqa_storage::StorageBackendError) -> SQLError {
uqa_sql::catalog::errors::storage_error(action, &error)
}
pub fn run_alter_table<S: Clone + 'static>(
context: &TableAlterContext<'_, S>,
stmt: AlterTableStmt,
) -> Result<SQLResult, SQLError> {
let mode = uqa_sql::schema::table_alteration::syntax::table_alter_lock_mode(&stmt).into();
let AlterTableStmt {
table,
qualifier,
if_exists,
recurse,
actions,
} = stmt;
context.constraints.access.ensure_table_owner(&table)?;
for mut action in actions {
if let AlterTableAction::AddColumn {
column,
if_not_exists: true,
..
} = &action
{
if context
.addition
.state
.has_column(&table, &column.name)
.map_err(|error| ddl_storage_error("ALTER TABLE ADD COLUMN", error))?
{
context.constraints.notices.lock().push((
"NOTICE".into(),
format!(
"column \"{}\" of relation \"{qualifier}\" already exists, skipping",
column.name
),
));
continue;
}
}
let column_checks = prepare_alter_action(context, &table, recurse, &mut action, mode)?;
run_recursive_alter_action(
context,
AlterTableStmt {
table: table.clone(),
qualifier: qualifier.clone(),
if_exists,
recurse,
actions: Vec::new(),
},
action,
)?;
for mut action in column_checks {
materialize_recursive_action_names(context, &table, recurse, &mut action)?;
run_recursive_alter_action(
context,
AlterTableStmt {
table: table.clone(),
qualifier: qualifier.clone(),
if_exists,
recurse,
actions: Vec::new(),
},
action,
)?;
}
}
Ok(SQLResult::empty())
}
fn prepare_alter_action<S: Clone + 'static>(
context: &TableAlterContext<'_, S>,
table: &str,
recurse: bool,
action: &mut AlterTableAction,
mode: crate::row_locks::RelationLockMode,
) -> Result<Vec<AlterTableAction>, SQLError> {
match action {
AlterTableAction::AddColumn { column, .. } => {
column.ty = uqa_sql::type_resolution::resolve_declared_column_type(
context.hierarchy.publication.types,
&column.ty,
)?;
}
AlterTableAction::AlterColumnType { ty, .. } => {
*ty = uqa_sql::type_resolution::resolve_declared_column_type(
context.hierarchy.publication.types,
ty,
)?;
}
_ => {}
}
materialize_recursive_action_names(context, table, recurse, action)?;
let mut column_checks = if let AlterTableAction::AddColumn { column, checks, .. } = action {
uqa_sql::schema::constraint_changes::take_column_check(column)
.into_iter()
.chain(std::mem::take(checks))
.map(|constraint| AlterTableAction::AddCheckConstraint { constraint })
.collect()
} else {
Vec::new()
};
locking::prepare_table_alter_action(&context.binding, context, table, recurse, action, mode)?;
for check in &mut column_checks {
locking::prepare_table_alter_action(
&context.binding,
context,
table,
recurse,
check,
mode,
)?;
}
context.binding.locks.prepare_definition_write()?;
Ok(column_checks)
}
#[expect(
clippy::too_many_lines,
reason = "preserves DDL dependency and action order"
)]
fn run_alter_table_action<S: Clone + 'static>(
context: &TableAlterContext<'_, S>,
stmt: AlterTableStmt,
action: AlterTableAction,
recursing: bool,
inherited_not_null_name: Option<String>,
) -> Result<(), SQLError> {
if matches!(&action, AlterTableAction::AddKeyConstraint { .. }) {
let persistence = context
.hierarchy
.catalog
.table_persistence(&stmt.table)
.map_err(|error| ddl_storage_error("ALTER TABLE ADD CONSTRAINT", error))?;
if persistence == Some(uqa_sql::ast::RelationPersistence::Temporary) {
context.addition.namespace.ensure_temporary_privilege()?;
} else {
context
.addition
.namespace
.ensure_existing_create(&stmt.table)?;
}
}
if !matches!(
action,
AlterTableAction::RenameColumn { .. }
| AlterTableAction::RenameTable { .. }
| AlterTableAction::RenameTrigger { .. }
| AlterTableAction::RenameConstraint { .. }
| AlterTableAction::RenameRule { .. }
) {
context
.constraints
.access
.ensure_no_pending_events(&stmt.table, "ALTER TABLE")?;
}
match action {
AlterTableAction::ChangeOwner { owner } => {
context
.ownership
.alter_table_role_owner(&stmt.table, &owner)?;
}
action @ (AlterTableAction::AddInheritance { .. }
| AlterTableAction::DropInheritance { .. }
| AlterTableAction::AttachPartition { .. }
| AlterTableAction::DetachPartition { .. }) => {
crate::schema::hierarchy::run_alter_hierarchy_action(
&context.hierarchy,
&stmt.table,
action,
)?;
}
AlterTableAction::AddColumn {
column,
key_constraints,
if_not_exists,
..
} => {
crate::schema::columns::addition::add_column(
&context.addition,
&stmt.table,
&stmt.qualifier,
column,
&key_constraints,
if_not_exists,
)?;
}
AlterTableAction::AddKeyConstraint { constraint } => {
crate::schema::constraints::add_key_constraint(
&context.constraints,
&stmt.table,
&stmt.qualifier,
constraint,
)?;
}
AlterTableAction::AddCheckConstraint { constraint } => {
add_check_constraint(
&context.constraints,
&stmt.table,
&stmt.qualifier,
constraint,
)?;
}
AlterTableAction::AddForeignKeyConstraint { constraint } => {
add_foreign_key_constraint(
&context.constraints,
&stmt.table,
&stmt.qualifier,
constraint,
)?;
}
AlterTableAction::AddNotNullConstraint {
name,
column,
validated,
no_inherit,
} => {
add_not_null_constraint(
&context.constraints,
&stmt.table,
name,
&column,
validated,
no_inherit,
!recursing,
)?;
}
AlterTableAction::ValidateConstraint { name } => {
validate_constraint(&context.constraints, &stmt.table, &name, stmt.recurse)?;
}
AlterTableAction::AlterConstraint {
name,
enforceability,
deferrability,
no_inherit,
} => {
alter_constraint(
&context.constraints,
&stmt.table,
&name,
enforceability,
deferrability,
no_inherit,
)?;
}
AlterTableAction::DropConstraint {
name,
if_exists,
cascade,
} => {
drop_constraint(
&context.constraints,
&stmt.table,
&name,
if_exists,
cascade,
stmt.recurse,
)?;
}
AlterTableAction::DropColumn {
name,
if_exists,
cascade,
} => {
drop_column(&context.removal, &stmt.table, &name, if_exists, cascade)?;
}
AlterTableAction::RenameColumn { from, to } => {
uqa_sql::schema::columns::validate_postgres_column_name(&to)?;
if !context
.addition
.state
.has_column(&stmt.table, &from)
.map_err(|err| ddl_storage_error("ALTER TABLE RENAME COLUMN", err))?
{
return Err(SQLError::Routine {
sqlstate: "42703".into(),
message: format!("column \"{from}\" does not exist"),
});
}
if context
.addition
.state
.has_column(&stmt.table, &to)
.map_err(|err| ddl_storage_error("ALTER TABLE RENAME COLUMN", err))?
{
let relation = uqa_core::RelationIdentity::from_legacy_name(&stmt.table)
.map_err(SQLError::Internal)?;
return Err(SQLError::Routine {
sqlstate: "42701".into(),
message: format!(
"column \"{to}\" of relation \"{}\" already exists",
relation.name
),
});
}
context
.lifecycle
.rename_column(&stmt.table, &from, &to)
.map_err(|e| ddl_storage_error("ALTER TABLE RENAME COLUMN", e))?;
}
AlterTableAction::RenameTable { to } => {
let source = uqa_core::RelationIdentity::from_legacy_name(&stmt.table)
.map_err(SQLError::Internal)?;
let target = uqa_sql::schema::relation_alteration::relation_rename_target(
context.binding.names,
&source,
&to,
"ALTER TABLE RENAME",
)?;
if !context
.lifecycle
.rename_table(&stmt.table, &target.qualified_name())
.map_err(|e| ddl_storage_error("ALTER TABLE RENAME", e))?
{
return Err(SQLError::Unsupported(format!(
"ALTER TABLE RENAME: rename of `{}` failed",
stmt.table
)));
}
}
AlterTableAction::RenameTrigger { from, to } => {
context.events.rename_trigger(&stmt.table, &from, &to)?;
}
AlterTableAction::RenameConstraint { from, to } => {
if !crate::schema::constraints::renaming::rename_constraint(
&context.constraints,
&stmt.table,
&from,
&to,
stmt.recurse,
)? {
context
.events
.rename_trigger_constraint(&stmt.table, &from, &to)?;
}
}
AlterTableAction::RenameRule { from, to } => {
context.events.rename_rule(&stmt.table, &from, &to)?;
}
AlterTableAction::SetTriggerEnableMode {
name,
user_only: _,
mode,
} => {
context
.events
.set_trigger_enable_mode(&stmt.table, name.as_deref(), mode)?;
}
AlterTableAction::SetRuleEnableMode { name, mode } => {
context
.events
.set_rule_enable_mode(&stmt.table, &name, mode)?;
}
AlterTableAction::SetPersistence { persistence } => {
return Err(SQLError::Unsupported(format!(
"ALTER TABLE SET {} is not supported for tables",
match persistence {
uqa_sql::ast::RelationPersistence::Permanent => "LOGGED",
uqa_sql::ast::RelationPersistence::Unlogged => "UNLOGGED",
uqa_sql::ast::RelationPersistence::Temporary => "TEMPORARY",
}
)));
}
AlterTableAction::SetSchema { schema } => {
return Err(SQLError::Unsupported(format!(
"ALTER TABLE SET SCHEMA {schema} is not supported for tables"
)));
}
AlterTableAction::SetDefault { name, default } => {
crate::schema::columns::alteration::set_default(
&context.columns,
&stmt.table,
&name,
default,
)?;
}
AlterTableAction::DropDefault { name } => {
crate::schema::columns::alteration::drop_default(&context.columns, &stmt.table, &name)?;
}
AlterTableAction::SetExpression { name, expression } => {
crate::schema::columns::alteration::set_expression(
&context.columns,
&stmt.table,
&stmt.qualifier,
&name,
expression,
)?;
}
AlterTableAction::DropExpression { name } => {
crate::schema::columns::alteration::drop_expression(
&context.columns,
&stmt.table,
&name,
)?;
}
AlterTableAction::SetNotNull { name } => {
set_not_null_constraint(
&context.constraints,
&stmt.table,
&name,
stmt.recurse,
!recursing,
inherited_not_null_name,
)?;
}
AlterTableAction::DropNotNull { name } => {
let (columns, _) = table_constraint_state(&context.constraints, &stmt.table)?;
let column = columns
.iter()
.find(|column| column.name == name)
.ok_or_else(|| SQLError::UnknownColumn(format!("{}.{name}", stmt.table)))?;
uqa_sql::schema::constraint_changes::not_null_removal::validate_column_removal(
&stmt.table,
column,
)?;
if let Some(constraint_name) = column.not_null_name.as_deref() {
drop_constraint(
&context.constraints,
&stmt.table,
constraint_name,
false,
false,
stmt.recurse,
)?;
}
}
AlterTableAction::AlterColumnType { name, ty, using } => {
crate::schema::columns::alteration::alter_type(
&context.columns,
&stmt.table,
&stmt.qualifier,
&name,
&ty,
using.as_ref(),
)?;
}
}
Ok(())
}