use super::{
publication::dependencies::CatalogPublicationChanges,
relation_alteration::{
rewrite_relation_rename_dependents, role_transfer_target, RelationAlterLocks,
RelationRenameDependencies, RoleTransferContext,
},
};
use crate::catalog::view::ViewPublication;
use crate::catalog::view::{catalog_view_row, StoredView};
use uqa_core::RelationIdentity;
use uqa_sql::{
ast::{AlterViewAction, AlterViewKind, AlterViewStmt, RelationPersistence},
catalog::security::table::{rewrite_acl_owner, validate_table_security_invariants},
schema::relation_alteration::{self, RelationAlterNames},
semantics::view_rewrite::{context::ViewRewriteContext, validate_view_definition_check_option},
SQLError,
};
use uqa_storage::StorageBackendResult;
pub use crate::catalog::view::ViewRegistryWrite;
pub trait ViewAlterCatalog {
fn view(&self, relation: &RelationIdentity) -> Option<StoredView>;
fn persistence(&self, relation: &RelationIdentity) -> Option<RelationPersistence>;
}
pub trait ViewAlterAccess {
fn ensure_owner(&self, name: &str, view: &StoredView) -> Result<String, SQLError>;
}
pub trait ViewAlterPublication: ViewPublication {
fn persist_rename(
&self,
from: &RelationIdentity,
to: &RelationIdentity,
) -> StorageBackendResult<Option<bool>>;
}
pub struct ViewAlterContext<'a> {
pub names: &'a dyn RelationAlterNames,
pub catalog: &'a dyn ViewAlterCatalog,
pub access: &'a dyn ViewAlterAccess,
pub locks: &'a dyn RelationAlterLocks,
pub roles: RoleTransferContext<'a>,
pub dependencies: &'a dyn RelationRenameDependencies,
pub publication: &'a dyn ViewAlterPublication,
pub changes: &'a dyn CatalogPublicationChanges,
pub rewrite: ViewRewriteContext<'a>,
pub notices: &'a parking_lot::Mutex<Vec<(String, String)>>,
}
pub type ViewAlterWrite<'a> = Box<dyn FnOnce(&ViewAlterContext<'_>) -> Result<(), SQLError> + 'a>;
pub trait ViewAlterTransactions {
fn with_view_write(&self, write: ViewAlterWrite<'_>) -> Result<(), SQLError>;
}
pub fn alter_view(
transactions: &dyn ViewAlterTransactions,
statement: &AlterViewStmt,
) -> Result<(), SQLError> {
transactions.with_view_write(Box::new(|context| execute_alter_view(context, statement)))
}
fn execute_alter_view(
context: &ViewAlterContext<'_>,
statement: &AlterViewStmt,
) -> Result<(), SQLError> {
let Some(target) = relation_alteration::view_alter_target(
context.names.resolve_relation_kind(&statement.name)?,
statement,
&mut |message| {
context
.notices
.lock()
.push(("NOTICE".into(), message.into()));
},
)?
else {
return Ok(());
};
let relation = &target.relation;
let canonical = &target.canonical;
let expected_kind = target.kind;
let mut view = context
.catalog
.view(relation)
.ok_or_else(|| SQLError::Internal(format!("{expected_kind} `{canonical}` disappeared")))?;
context.access.ensure_owner(canonical, &view)?;
context.locks.lock_exclusive(canonical)?;
match &statement.action {
AlterViewAction::Set(changes) => {
relation_alteration::set_view_options(&mut view.options, changes);
}
AlterViewAction::Reset(names) => {
relation_alteration::reset_view_options(&mut view.options, names);
}
AlterViewAction::OwnerTo(owner) => {
alter_view_role_owner(context, canonical, &mut view, owner)?;
}
AlterViewAction::RenameTo(new_name) => {
return rename_view(context, relation, new_name, expected_kind);
}
}
if statement.kind == AlterViewKind::View {
validate_view_definition_check_option(
context.rewrite,
canonical,
&view.rewrite_definition(),
)?;
}
if view.persistence != RelationPersistence::Temporary && context.publication.has_catalog() {
context
.publication
.save_view(&catalog_view_row(relation, &view).map_err(|error| {
SQLError::Internal(format!(
"serialize altered {expected_kind} `{canonical}`: {error}"
))
})?)
.map_err(|error| {
SQLError::Internal(format!(
"persist altered {expected_kind} `{canonical}`: {error}"
))
})?;
}
context
.publication
.views_write()
.insert(target.relation, view);
context.changes.catalog_registry_changed();
Ok(())
}
fn alter_view_role_owner(
context: &ViewAlterContext<'_>,
canonical_name: &str,
view: &mut StoredView,
requested_owner: &str,
) -> Result<(), SQLError> {
let current_owner = context.access.ensure_owner(canonical_name, view)?;
let (new_owner, current_user_is_superuser) =
role_transfer_target(&context.roles, requested_owner)?;
if current_owner == new_owner {
return Ok(());
}
let relation = RelationIdentity::from_legacy_name(canonical_name).map_err(|error| {
SQLError::Internal(format!(
"resolve view owner target `{canonical_name}`: {error}"
))
})?;
if !current_user_is_superuser {
context
.roles
.schemas
.require_schema_create(&relation.schema, &new_owner)?;
}
let mut security = view.security();
rewrite_acl_owner(&mut security, &new_owner);
let output_columns = view.output_columns.as_deref().ok_or_else(|| {
SQLError::Internal(format!(
"loaded view `{canonical_name}` has no durable public column metadata"
))
})?;
validate_table_security_invariants(
&security, Some(output_columns), &context.roles.roles.role_definitions(),
).map_err(|error| {
SQLError::Internal(format!(
"view `{canonical_name}` produced invalid privilege metadata after owner transfer: {error}"
))
})?;
view.set_security(security);
Ok(())
}
fn rename_view(
context: &ViewAlterContext<'_>,
relation: &RelationIdentity,
new_name: &str,
expected_kind: &str,
) -> Result<(), SQLError> {
let target = relation_alteration::relation_rename_target(
context.names,
relation,
new_name,
"ALTER VIEW RENAME TO",
)?;
rewrite_relation_rename_dependents(context.dependencies, relation, &target).map_err(
|error| {
SQLError::Internal(format!(
"rewrite dependencies while renaming {expected_kind} `{}`: {error}",
relation.qualified_name()
))
},
)?;
let persistent = context
.catalog
.persistence(relation)
.is_some_and(|persistence| persistence != RelationPersistence::Temporary);
if persistent
&& context
.publication
.persist_rename(relation, &target)
.map_err(|error| {
SQLError::Internal(format!(
"persist {expected_kind} rename `{}` to `{}`: {error}",
relation.qualified_name(),
target.qualified_name()
))
})?
== Some(false)
{
return Err(SQLError::Internal(format!(
"{expected_kind} `{}` disappeared during rename",
relation.qualified_name()
)));
}
let mut views = context.publication.views_write();
if views.contains_key(&target) {
return Err(SQLError::Internal(format!(
"{expected_kind} rename target `{}` appeared after preflight",
target.qualified_name()
)));
}
let view = views.remove(relation).ok_or_else(|| {
SQLError::Internal(format!(
"{expected_kind} `{}` disappeared during rename",
relation.qualified_name()
))
})?;
views.insert(target, view);
drop(views);
context.changes.catalog_registry_changed();
Ok(())
}