use nodedb_types::DatabaseId;
use crate::control::security::audit::AuditEvent;
use crate::control::security::identity::AuthenticatedIdentity;
use crate::control::state::SharedState;
use super::super::super::result::{DdlError, DdlResult};
fn err(sqlstate: &str, message: impl Into<String>) -> DdlError {
DdlError {
sqlstate: sqlstate.to_string(),
message: message.into(),
}
}
#[derive(Clone, Copy)]
pub struct DropCollectionRequest<'a> {
pub name: &'a str,
pub if_exists: bool,
pub purge: bool,
pub cascade: bool,
pub cascade_force: bool,
pub database_id: DatabaseId,
}
pub fn drop_collection(
state: &SharedState,
identity: &AuthenticatedIdentity,
req: &DropCollectionRequest<'_>,
) -> Result<Vec<DdlResult>, DdlError> {
let DropCollectionRequest {
name,
if_exists,
purge,
cascade,
cascade_force,
database_id,
} = *req;
let name_lower = name.to_lowercase();
let name = name_lower.as_str();
let tenant_id = identity.tenant_id;
let dependents: Vec<crate::control::cascade::Dependent> = {
let catalog = state.credentials.catalog();
let mut visited = std::collections::HashSet::new();
crate::control::cascade::collect_dependents(catalog, tenant_id.as_u64(), name, &mut visited)
.map_err(|e| err("XX000", e.to_string()))?
};
let blocking_dependents: Vec<&crate::control::cascade::Dependent> = dependents
.iter()
.filter(|d| d.kind != crate::control::cascade::DependentKind::Sequence)
.collect();
if !blocking_dependents.is_empty() && !cascade {
let deps_list: Vec<String> = blocking_dependents
.iter()
.map(|d| format!("{}:{}", d.kind.as_str(), d.name))
.collect();
return Err(err(
"2BP01",
format!(
"cannot drop collection '{name}': {} dependent object(s) exist ({}); \
drop them individually or retry with CASCADE (batched-cascade propose \
not yet implemented — CASCADE currently rejected to avoid orphaned rows)",
blocking_dependents.len(),
deps_list.join(", ")
),
));
}
if cascade {
return Err(err(
"0A000",
"DROP COLLECTION ... CASCADE requires atomic batched Delete* + PurgeCollection \
in one metadata-raft commit — that proposer surface has not landed yet. \
Drop dependents individually in the meantime.",
));
}
let _ = cascade_force;
let is_owner = state
.permissions
.get_owner_in_database("collection", database_id.as_u64(), tenant_id, name)
.as_deref()
== Some(&identity.username);
let is_admin = identity.is_superuser
|| identity.has_role(&crate::control::security::identity::Role::TenantAdmin);
if !is_owner && !is_admin {
return Err(err(
"42501",
"permission denied: only owner, superuser, or tenant_admin can drop collections",
));
}
if purge && !is_admin {
return Err(err(
"42501",
"permission denied: only superuser or tenant_admin may DROP COLLECTION ... PURGE",
));
}
{
let catalog = state.credentials.catalog();
if catalog
.get_materialized_view(tenant_id.as_u64(), name)
.map_err(|error| err("XX000", error.to_string()))?
.is_some()
{
return Err(err(
"2BP01",
format!(
"collection '{name}' is owned by a materialized view; drop the materialized view"
),
));
}
match catalog.get_collection(database_id, tenant_id.as_u64(), name) {
Ok(Some(coll)) if coll.is_active => {}
Ok(Some(_)) if purge => {}
Ok(Some(_)) => {
return Ok(vec![DdlResult::Status {
command: "DROP COLLECTION".to_string(),
rows_affected: None,
}]);
}
Ok(None) if purge || if_exists => {
return Ok(vec![DdlResult::Status {
command: "DROP COLLECTION".to_string(),
rows_affected: None,
}]);
}
_ => {
return Err(err("42P01", format!("collection '{name}' does not exist")));
}
}
}
let action = if purge {
format!("requested purge of collection '{name}'")
} else {
format!("requested drop of collection '{name}'")
};
state.audit_record(
AuditEvent::AdminAction,
Some(tenant_id),
&identity.username,
&action,
);
let entry = if purge {
crate::control::catalog_entry::CatalogEntry::PurgeCollection {
database_id: database_id.as_u64(),
tenant_id: tenant_id.as_u64(),
name: name.to_string(),
}
} else {
crate::control::catalog_entry::CatalogEntry::DeactivateCollection {
database_id: database_id.as_u64(),
tenant_id: tenant_id.as_u64(),
name: name.to_string(),
}
};
let mut local_lifecycle = if state.metadata_raft.get().is_none() {
Some(
state
.quiesce
.try_acquire_lifecycle(database_id.as_u64(), tenant_id.as_u64(), name)
.ok_or_else(|| err("55006", format!("collection '{name}' lifecycle is busy")))?,
)
} else {
None
};
let log_index = crate::control::metadata_proposer::propose_catalog_entry(state, &entry)
.map_err(|error| err("XX000", error.to_string()))?;
if log_index == 0 {
let catalog = state.credentials.catalog();
if purge {
let purge_lsn = state.wal.next_lsn().as_u64();
let purge_result = tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
crate::control::server::shared::ddl::neutral::collection::purge::hard_purge_collection(
state,
database_id.as_u64(),
tenant_id.as_u64(),
name,
purge_lsn,
local_lifecycle.is_some(),
)
.await
})
});
if let Err(failure) = purge_result {
if failure.retry_queued
&& let Some(guard) = local_lifecycle.take()
{
guard.disarm();
}
panic!("local collection reclaim failed: {}", failure.error);
}
state
.permissions
.install_replicated_remove_owner_in_database(
"collection",
database_id.as_u64(),
tenant_id.as_u64(),
name,
);
state
.permissions
.remove_grants_for_target(&format!("collection:{}:{name}", tenant_id.as_u64()));
} else {
crate::control::catalog_entry::apply::collection::deactivate(
database_id.as_u64(),
tenant_id.as_u64(),
name,
catalog,
);
}
}
let catalog = state.credentials.catalog();
if let Ok(seqs) = catalog.load_sequences_for_tenant(tenant_id.as_u64()) {
let prefix = format!("{name}_");
let suffix = "_seq";
for seq in &seqs {
if seq.name.starts_with(&prefix) && seq.name.ends_with(suffix) {
catalog
.delete_sequence(tenant_id.as_u64(), &seq.name)
.map_err(|e| {
err(
"XX000",
format!("failed to drop sequence '{}': {e}", seq.name),
)
})?;
let _ = state
.sequence_registry
.remove(tenant_id.as_u64(), &seq.name);
}
}
}
let completion = if purge {
format!("purged collection '{name}' (log_index={log_index})")
} else {
format!("dropped collection '{name}' (log_index={log_index})")
};
state.audit_record(
AuditEvent::AdminAction,
Some(tenant_id),
&identity.username,
&completion,
);
Ok(vec![DdlResult::Status {
command: "DROP COLLECTION".to_string(),
rows_affected: None,
}])
}