use crate::catalog::roles::RoleReference;
use crate::{
catalog::{domain::DomainCatalog, roles::RoleReferenceNames, security::BoundSchemaSecurity},
SQLError,
};
pub trait TypeObjectCatalog: DomainCatalog {
fn schema_security(&self, name: &str) -> Option<BoundSchemaSecurity>;
fn resolve_drop_type_oid(&self, name: &str) -> Result<Option<i64>, SQLError>;
fn format_drop_type(&self, oid: i64) -> Result<Option<String>, String>;
fn enum_by_type_oid(&self, oid: u32) -> Option<crate::catalog::enum_type::StoredEnum>;
fn composite_by_type_oid(
&self,
oid: u32,
) -> Option<crate::catalog::composite_type::StoredComposite>;
fn user_array_element(&self, oid: u32) -> Option<u32>;
fn row_type_relation(&self, oid: u32) -> Option<RowTypeRelation>;
}
#[derive(Debug, Clone)]
pub struct RowTypeRelation {
pub kind: &'static str,
pub name: String,
pub local_name: String,
pub owner: uqa_core::catalog_role::RoleIdentity,
pub schema: String,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum TypeDropKind {
Domain,
Type,
}
pub trait TypeObjectAuthority {
fn schema_usage(&self, schema: &str, role: &RoleReference) -> bool;
fn current_user_has_role_privileges(
&self,
role: &dyn crate::catalog::roles::identity::RoleSubject,
) -> bool;
}
pub struct TypeObjectBinding<'a> {
pub catalog: &'a dyn TypeObjectCatalog,
pub authority: &'a dyn TypeObjectAuthority,
pub session: &'a dyn RoleReferenceNames,
}
pub enum BoundTypeDrop {
Target(u32),
Skipped(String),
}
pub fn resolve_drop_domain(
context: &TypeObjectBinding<'_>,
name: &str,
if_exists: bool,
) -> Result<BoundTypeDrop, SQLError> {
resolve_type_drop(context, name, if_exists, TypeDropKind::Domain)
}
pub fn resolve_drop_type(
context: &TypeObjectBinding<'_>,
name: &str,
if_exists: bool,
) -> Result<BoundTypeDrop, SQLError> {
resolve_type_drop(context, name, if_exists, TypeDropKind::Type)
}
fn resolve_type_drop(
context: &TypeObjectBinding<'_>,
name: &str,
if_exists: bool,
kind: TypeDropKind,
) -> Result<BoundTypeDrop, SQLError> {
let parsed = crate::parse_regtype_name(name)?
.ok_or_else(|| SQLError::Internal("DROP has no type name".into()))?;
let label = format!(
"{}{}",
parsed.names.join("."),
"[]".repeat(parsed.array_dimensions)
);
if let [schema, _] = parsed.names.as_slice() {
if context.catalog.schema_security(schema).is_none() {
if if_exists {
return Ok(BoundTypeDrop::Skipped(format!(
"schema \"{schema}\" does not exist, skipping"
)));
}
return Err(SQLError::Routine {
sqlstate: "3F000".into(),
message: format!("schema \"{schema}\" does not exist"),
});
}
if !context
.authority
.schema_usage(schema, &context.session.current_role())
{
return Err(SQLError::Routine {
sqlstate: "42501".into(),
message: format!("permission denied for schema {schema}"),
});
}
}
let oid = context.catalog.resolve_drop_type_oid(name)?;
let Some(oid) = oid else {
if if_exists {
return Ok(BoundTypeDrop::Skipped(format!(
"type \"{label}\" does not exist, skipping"
)));
}
return Err(SQLError::Routine {
sqlstate: "42704".into(),
message: format!("type \"{label}\" does not exist"),
});
};
let type_oid = u32::try_from(oid)
.map_err(|_| SQLError::Internal(format!("DROP type OID {oid} is out of range")))?;
let target = drop_target(context, type_oid, &label, kind)?;
let owns_schema = target.schema.as_ref().is_some_and(|schema| {
context
.catalog
.schema_security(schema)
.is_some_and(|security| {
context
.authority
.current_user_has_role_privileges(&security.role_owner)
})
});
if !owns_schema
&& !context
.authority
.current_user_has_role_privileges(&target.owner)
{
return Err(SQLError::Routine {
sqlstate: "42501".into(),
message: format!("must be owner of type {}", format_type(context, oid)?),
});
}
match target.required_by {
None => Ok(BoundTypeDrop::Target(type_oid)),
Some(Requirement::System) => Err(SQLError::Routine {
sqlstate: "2BP01".into(),
message: format!(
"cannot drop type {} because it is required by the database system",
format_type(context, oid)?
),
}),
Some(Requirement::Object(object)) => Err(SQLError::Diagnostic {
sqlstate: "2BP01".into(),
message: format!(
"cannot drop type {} because {object} requires it",
format_type(context, oid)?
),
detail: None,
hint: Some(format!("You can drop {object} instead.")),
}),
}
}
enum Requirement {
System,
Object(String),
}
struct DropTarget {
schema: Option<String>,
owner: uqa_core::catalog_role::RoleIdentity,
required_by: Option<Requirement>,
}
fn drop_target(
context: &TypeObjectBinding<'_>,
oid: u32,
label: &str,
kind: TypeDropKind,
) -> Result<DropTarget, SQLError> {
if let Some(domain) = context.catalog.domain_by_oid(oid) {
return Ok(DropTarget {
schema: Some(domain.identity.schema),
owner: domain.owner,
required_by: None,
});
}
if kind == TypeDropKind::Domain {
return Err(SQLError::Routine {
sqlstate: "42809".into(),
message: format!("\"{label}\" is not a domain"),
});
}
if let Some(definition) = context.catalog.enum_by_type_oid(oid) {
return Ok(DropTarget {
schema: Some(definition.identity.schema.clone()),
owner: definition.owner,
required_by: (definition.array_oid == oid)
.then(|| {
format_type(context, i64::from(definition.oid))
.map(|element| Requirement::Object(format!("type {element}")))
})
.transpose()?,
});
}
if let Some(definition) = context.catalog.composite_by_type_oid(oid) {
return Ok(DropTarget {
schema: Some(definition.identity.schema.clone()),
owner: definition.owner,
required_by: (definition.array_oid == oid)
.then(|| {
format_type(context, i64::from(definition.oid))
.map(|element| Requirement::Object(format!("type {element}")))
})
.transpose()?,
});
}
if let Some(element) = context.catalog.user_array_element(oid) {
let domain = context
.catalog
.domain_by_oid(element)
.ok_or_else(|| SQLError::Internal(format!("array type {oid} has no element type")))?;
return Ok(DropTarget {
schema: Some(domain.identity.schema),
owner: domain.owner,
required_by: Some(Requirement::Object(format!(
"type {}",
format_type(context, i64::from(element))?
))),
});
}
if let Some(relation) = context.catalog.row_type_relation(oid) {
return Ok(DropTarget {
schema: Some(relation.schema),
owner: relation.owner,
required_by: Some(Requirement::Object(format!(
"{} {}",
relation.kind, relation.name
))),
});
}
Ok(DropTarget {
schema: None,
owner: uqa_core::catalog_role::RoleIdentity::BOOTSTRAP,
required_by: Some(Requirement::System),
})
}
fn format_type(context: &TypeObjectBinding<'_>, oid: i64) -> Result<String, SQLError> {
context
.catalog
.format_drop_type(oid)
.map_err(SQLError::Internal)?
.ok_or_else(|| SQLError::Internal("DROP type target disappeared".into()))
}
pub fn resolve_alter_enum(
context: &TypeObjectBinding<'_>,
name: &str,
) -> Result<crate::catalog::enum_type::StoredEnum, SQLError> {
let parsed = crate::parse_regtype_name(name)?
.ok_or_else(|| SQLError::Internal("ALTER TYPE has no type name".into()))?;
let label = format!(
"{}{}",
parsed.names.join("."),
"[]".repeat(parsed.array_dimensions)
);
let oid = context
.catalog
.resolve_drop_type_oid(name)?
.ok_or_else(|| SQLError::Routine {
sqlstate: "42704".into(),
message: format!("type \"{label}\" does not exist"),
})?;
let definition = u32::try_from(oid)
.ok()
.and_then(|oid| context.catalog.enum_by_type_oid(oid))
.filter(|definition| i64::from(definition.oid) == oid)
.ok_or_else(|| {
format_type(context, oid).map_or_else(
|error| error,
|name| SQLError::Routine {
sqlstate: "42809".into(),
message: format!("{name} is not an enum"),
},
)
})?;
if !context
.authority
.current_user_has_role_privileges(&definition.owner)
{
return Err(SQLError::Routine {
sqlstate: "42501".into(),
message: format!("must be owner of type {}", format_type(context, oid)?),
});
}
Ok(definition)
}