uqa-execution 0.4.5

Volcano physical operators with row-batch pipelines
//
// Unified Query Algebra
//
// Copyright (c) 2023-2026 Cognica, Inc.
//

//! Sequence ACL publication with retained authorization and registry guards.

use super::roles::{
    dependencies::{prepare_role_dependencies, RoleDependencyCandidate},
    locking::RoleLockContext,
};
use crate::catalog::{
    context::CatalogContext,
    projection::{resolve_regclass_kind_by_oid, sequence_relation_oid},
    sequence::sequence_row,
    sequence_introspection::SequenceIntrospectionCatalog,
};
use crate::row_locks::shared_objects::SharedObjectLockSession;
use std::{
    collections::{BTreeMap, BTreeSet},
    ops::DerefMut,
};
use uqa_core::RelationIdentity;
use uqa_sql::catalog::security::acl_command::{AclCommandRoles, ResolvedAclRoles};
use uqa_sql::{
    ast::{GrantSequenceStmt, GrantSequenceTarget},
    catalog::security::{
        dependencies::added_acl_roles,
        sequence::requested_acl_privileges,
        sequence_grants::{
            apply_sequence_acl, bind_named_sequence_grants, bind_sequence_grant_schemas,
            sequence_acl_warning, sequence_grants_in_schemas, validate_sequence_acl_roles,
            validate_sequence_grant_target_kinds, ResolvedSequenceGrantTarget,
            SequenceGrantNamespace,
        },
        sequence_inquiry::SequencePrivilegeInquiry,
        BoundSequenceSecurity,
    },
    SQLError,
};
use uqa_storage::{CatalogFacade, StorageBackendResult};

pub type SequenceSecurityWrite<'a> =
    Box<dyn DerefMut<Target = BTreeMap<RelationIdentity, BoundSequenceSecurity>> + 'a>;
pub trait SequencePrivilegePublication {
    fn prepare_writer(&self) -> Result<(), SQLError>;
    fn refresh_catalog(&self) -> StorageBackendResult<()>;
    fn security_write(&self) -> SequenceSecurityWrite<'_>;
    fn catalog_changed(&self);
    fn notice(&self, level: &str, message: &str);
}
pub struct SequencePrivilegeContext<'a> {
    pub inquiry: SequencePrivilegeInquiry<'a>,
    pub locks: &'a dyn SharedObjectLockSession,
    pub sequences: &'a dyn SequenceIntrospectionCatalog,
    pub namespaces: &'a dyn SequenceGrantNamespace,
    pub publication: &'a dyn SequencePrivilegePublication,
    pub catalog: CatalogContext<'a>,
    pub storage: Option<&'a dyn CatalogFacade>,
}

struct SequencePrivilegeCandidate<'a> {
    registry: SequenceSecurityWrite<'a>,
    updates: Vec<(String, RelationIdentity, BoundSequenceSecurity)>,
    notices: Vec<(&'static str, String)>,
}

impl SequencePrivilegeContext<'_> {
    pub fn grant_sequence_privileges(&self, statement: &GrantSequenceStmt) -> Result<(), SQLError> {
        self.grant_sequence_privileges_with_roles(statement, &mut AclCommandRoles::default())
    }

    pub(crate) fn grant_sequence_privileges_with_roles(
        &self,
        statement: &GrantSequenceStmt,
        command_roles: &mut AclCommandRoles,
    ) -> Result<(), SQLError> {
        let targets = self.resolve_sequence_grant_targets(&statement.target)?;
        let RoleDependencyCandidate {
            roles,
            memberships,
            value:
                SequencePrivilegeCandidate {
                    mut registry,
                    updates,
                    notices,
                },
            ..
        } = prepare_role_dependencies(
            &RoleLockContext {
                roles: self.inquiry.roles,
                session: self.locks,
            },
            || self.publication.prepare_writer(),
            || self.prepare_privileges(statement, &targets, command_roles),
        )?;
        for (name, relation, security) in &updates {
            self.persist_sequence_security(name, relation, security)?;
        }
        let changed = !updates.is_empty();
        for (_, relation, security) in updates {
            registry.insert(relation, security);
        }
        drop(registry);
        drop(memberships);
        drop(roles);
        for (level, message) in notices {
            self.publication.notice(level, &message);
        }
        if changed {
            self.publication.catalog_changed();
        }
        Ok(())
    }

    fn prepare_privileges<'a>(
        &'a self,
        statement: &GrantSequenceStmt,
        targets: &[ResolvedSequenceGrantTarget],
        command_roles: &mut AclCommandRoles,
    ) -> Result<RoleDependencyCandidate<'a, SequencePrivilegeCandidate<'a>>, SQLError> {
        let roles = self.inquiry.roles.role_definitions();
        let ResolvedAclRoles {
            grantees,
            current_user,
            ..
        } = command_roles.resolve_validated(
            self.inquiry.names,
            &roles,
            &statement.grantees,
            statement.grantor.as_ref(),
            |resolved| {
                validate_sequence_acl_roles(
                    statement,
                    &resolved.grantees,
                    resolved.requested_grantor.as_deref(),
                    &resolved.current_user,
                    &roles,
                )
            },
        )?;
        validate_sequence_grant_target_kinds(targets)?;
        let privileges = requested_acl_privileges(&statement.privileges)?;
        let memberships = self.inquiry.roles.role_memberships();
        let registry = self.publication.security_write();
        let mut updates = Vec::new();
        let mut notices = Vec::new();
        let mut dependencies = BTreeSet::new();
        for target in targets {
            let current = registry.get(&target.relation).cloned().ok_or_else(|| {
                SQLError::Internal(format!(
                    "sequence `{}` has no security metadata",
                    target.name
                ))
            })?;
            let current = current.resolve(&roles).map_err(SQLError::Internal)?;
            let (next, grantable) = apply_sequence_acl(
                statement,
                &grantees,
                &privileges,
                &current_user,
                &roles,
                &memberships,
                &current,
            )?;
            if grantable != privileges.len() {
                notices.push(sequence_acl_warning(
                    statement.is_grant,
                    grantable != 0,
                    &target.relation.name,
                ));
            }
            added_acl_roles(
                current.acl.as_deref().unwrap_or_default(),
                &current.role_owner,
                next.acl.as_deref().unwrap_or_default(),
                &next.role_owner,
                &mut dependencies,
            );
            if next != current {
                let next =
                    BoundSequenceSecurity::bind(&next, &roles).map_err(SQLError::Internal)?;
                next.validate(&roles).map_err(SQLError::Internal)?;
                updates.push((target.name.clone(), target.relation.clone(), next));
            }
        }
        Ok(RoleDependencyCandidate {
            value: SequencePrivilegeCandidate {
                registry,
                updates,
                notices,
            },
            memberships,
            roles,
            dependencies,
        })
    }

    fn resolve_sequence_grant_targets(
        &self,
        target: &GrantSequenceTarget,
    ) -> Result<Vec<ResolvedSequenceGrantTarget>, SQLError> {
        match target {
            GrantSequenceTarget::Sequences { names } => {
                bind_named_sequence_grants(self.inquiry.resolution, names)
            }
            GrantSequenceTarget::AllSequencesInSchemas { schemas } => {
                self.resolve_all_sequences_in_schemas(schemas)
            }
        }
    }

    fn resolve_all_sequences_in_schemas(
        &self,
        schemas: &[String],
    ) -> Result<Vec<ResolvedSequenceGrantTarget>, SQLError> {
        self.publication.refresh_catalog().map_err(|error| {
            SQLError::Internal(format!("load schemas for sequence privileges: {error}"))
        })?;
        self.sequences.refresh_sequences().map_err(|error| {
            SQLError::Internal(format!("load sequences for privileges: {error}"))
        })?;
        let schemas = bind_sequence_grant_schemas(self.namespaces, schemas)?;
        let sequences = self.sequences.states();
        let targets = sequence_grants_in_schemas(&schemas, sequences.keys());
        Ok(targets)
    }

    pub fn persist_sequence_security(
        &self,
        name: &str,
        relation: &RelationIdentity,
        security: &BoundSequenceSecurity,
    ) -> Result<(), SQLError> {
        let state = self
            .sequences
            .sequence_state(relation)
            .ok_or_else(|| SQLError::Internal(format!("sequence `{name}` disappeared")))?;
        let object_id = self
            .sequences
            .object_ids()
            .get(relation)
            .copied()
            .ok_or_else(|| {
                SQLError::Internal(format!("sequence `{name}` has no object identity"))
            })?;
        let persistence = self
            .sequences
            .sequence_persistence(relation)
            .ok_or_else(|| {
                SQLError::Internal(format!("sequence `{name}` has no persistence metadata"))
            })?;
        if persistence == uqa_sql::ast::RelationPersistence::Temporary {
            return Ok(());
        }
        let Some(catalog) = self.storage else {
            return Ok(());
        };
        let row = sequence_row(name, object_id, state, persistence, security)
            .map_err(|error| SQLError::Internal(format!("build sequence catalog row: {error}")))?;
        if !catalog
            .replace_sequence_row(&row)
            .map_err(|error| SQLError::Internal(format!("persist sequence privileges: {error}")))?
        {
            return Err(SQLError::Internal(format!(
                "sequence `{name}` disappeared during privilege change"
            )));
        }
        Ok(())
    }

    pub fn resolve_sequence_privilege_oid(
        &self,
        oid: i64,
    ) -> Result<Option<(String, RelationIdentity)>, SQLError> {
        self.sequences.refresh_sequences().map_err(|error| {
            SQLError::Internal(format!("load sequences for privilege inquiry: {error}"))
        })?;
        if let Some((relation, _)) = self
            .sequences
            .object_ids()
            .iter()
            .find(|(_, object_id)| sequence_relation_oid(**object_id) == oid)
        {
            return Ok(Some((relation.qualified_name(), relation.clone())));
        }
        if let Some((name, _kind)) = resolve_regclass_kind_by_oid(&self.catalog, oid)? {
            return Err(SQLError::Routine {
                sqlstate: "42809".into(),
                message: format!("\"{name}\" is not a sequence"),
            });
        }
        Ok(None)
    }
}