uqa-execution 0.5.0

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

//! Rename routines inside the caller transaction and publish dependent definitions in catalog order.

use super::{
    catalog::RoutineMutationContext,
    compilation::{persisted_routine_body, StoredRoutineCompilationContext},
};
use crate::schema::{
    namespaces::NamespaceCatalogRefresh, relation_alteration::RoleTargetSchemaAccess,
};
use std::{collections::BTreeMap, sync::Arc};
use uqa_sql::{
    ast::{FunctionBinding, RenameRoutineStmt},
    catalog::roles::role_inherits,
    routines::{
        declaration::resolve_routine_identity_types,
        lifecycle::{
            binding::resolve_sql_routine_alter_target,
            rename::{self as analysis, RoutineRenameTarget},
            require_routine_ownership,
        },
        SQLUserFunction,
    },
    SQLError,
};
use uqa_storage::StorageBackendResult;

pub trait RoutineRenameDependents {
    fn rewrite_schema_routine_identity(
        &self,
        target: &FunctionBinding,
        new_name: &str,
    ) -> StorageBackendResult<()>;
    fn rewrite_view_routine_identity(
        &self,
        target: &FunctionBinding,
        new_name: &str,
    ) -> StorageBackendResult<()>;
    fn rewrite_event_routine_identity(
        &self,
        target: &FunctionBinding,
        new_name: &str,
    ) -> Result<(), SQLError>;
}
pub struct RoutineRenameContext<'a> {
    pub mutation: RoutineMutationContext<'a>,
    pub refresh: &'a dyn NamespaceCatalogRefresh,
    pub schemas: &'a dyn RoleTargetSchemaAccess,
    pub compilation: StoredRoutineCompilationContext<'a>,
    pub dependents: &'a dyn RoutineRenameDependents,
}

pub fn rename_sql_routine(
    context: &RoutineRenameContext<'_>,
    stmt: &RenameRoutineStmt,
) -> Result<(), SQLError> {
    context.mutation.writer.prepare_writer()?;
    context.refresh.refresh_catalog().map_err(|error| {
        SQLError::Internal(format!(
            "synchronize catalogs before routine rename: {error}"
        ))
    })?;
    let registry = context.mutation.registry.routine_snapshot();
    let previous = registry.clone();
    let target = resolve_routine_rename_target(context, stmt, &registry)?;
    let mut renamed_registry = analysis::move_routine_registry_entry(registry, &target)?;
    super::catalog::revision::renamed(&mut renamed_registry, &target.new_name, &target.binding)?;
    **context.mutation.registry.routines_write() = renamed_registry;

    let rewritten_registry =
        rewrite_routine_owned_dependency_identity(context, &target.binding, &target.new_name)?;
    **context.mutation.registry.routines_write() = rewritten_registry.clone();
    context
        .dependents
        .rewrite_schema_routine_identity(&target.binding, &target.new_name)
        .map_err(|error| {
            SQLError::Internal(format!("rewrite schema routine dependencies: {error}"))
        })?;
    context
        .dependents
        .rewrite_view_routine_identity(&target.binding, &target.new_name)
        .map_err(|error| {
            SQLError::Internal(format!("rewrite view routine dependencies: {error}"))
        })?;
    context
        .dependents
        .rewrite_event_routine_identity(&target.binding, &target.new_name)?;
    context
        .mutation
        .publication
        .persist_routine_definitions(&rewritten_registry)?;
    super::catalog::publication::record_changes(
        context.mutation.changes,
        &previous,
        &rewritten_registry,
    );
    context.mutation.changes.catalog_registry_changed();
    Ok(())
}

/// Move every overload registered under `registry_key` into `schema` while its schema is renamed: each keeps its signature, body and identity, and the definitions that call it are rewritten to the new name. The statement already checked the schema's ownership, so no routine ownership is required.
pub fn relocate_sql_routines(
    context: &RoutineRenameContext<'_>,
    registry_key: &str,
    schema: &str,
) -> Result<(), SQLError> {
    let identity = analysis::routine_rename_identity(registry_key)?;
    let new_name = uqa_core::RelationIdentity::new(schema, &identity.name).qualified_name();
    loop {
        let registry = context.mutation.registry.routine_snapshot();
        let previous = registry.clone();
        let Some(function) = registry
            .get(registry_key)
            .and_then(|overloads| overloads.first())
            .cloned()
        else {
            return Ok(());
        };
        let signature = uqa_sql::routines::routine_signature_types(&function.def);
        let object_id = function.def.object_id.ok_or_else(|| {
            SQLError::Internal(format!(
                "routine `{registry_key}` has no catalog object identity"
            ))
        })?;
        let target = RoutineRenameTarget {
            old_name: registry_key.to_string(),
            new_name: new_name.clone(),
            position: 0,
            binding: FunctionBinding {
                object_id: Some(object_id),
                name: registry_key.to_string(),
                argument_types: signature,
                builtin: false,
                dispatch: None,
                invocation: None,
                resolution_error: None,
            },
        };
        let mut renamed_registry = analysis::move_routine_registry_entry(registry, &target)?;
        super::catalog::revision::renamed(
            &mut renamed_registry,
            &target.new_name,
            &target.binding,
        )?;
        **context.mutation.registry.routines_write() = renamed_registry;
        let rewritten_registry =
            rewrite_routine_owned_dependency_identity(context, &target.binding, &target.new_name)?;
        **context.mutation.registry.routines_write() = rewritten_registry.clone();
        context
            .dependents
            .rewrite_schema_routine_identity(&target.binding, &target.new_name)
            .map_err(|error| {
                SQLError::Internal(format!("rewrite schema routine dependencies: {error}"))
            })?;
        context
            .dependents
            .rewrite_view_routine_identity(&target.binding, &target.new_name)
            .map_err(|error| {
                SQLError::Internal(format!("rewrite view routine dependencies: {error}"))
            })?;
        context
            .dependents
            .rewrite_event_routine_identity(&target.binding, &target.new_name)?;
        context
            .mutation
            .publication
            .persist_routine_definitions(&rewritten_registry)?;
        super::catalog::publication::record_changes(
            context.mutation.changes,
            &previous,
            &rewritten_registry,
        );
        context.mutation.changes.catalog_registry_changed();
    }
}

fn resolve_routine_rename_target(
    context: &RoutineRenameContext<'_>,
    stmt: &RenameRoutineStmt,
    registry: &BTreeMap<String, Vec<Arc<SQLUserFunction>>>,
) -> Result<RoutineRenameTarget, SQLError> {
    let requested_types = resolve_routine_identity_types(
        context.compilation.analysis.types,
        stmt.arg_types.as_deref(),
        &stmt.arg_type_references,
        "ALTER routine RENAME",
    )?;
    let (old_name, position) = resolve_sql_routine_alter_target(
        context.mutation.names,
        registry,
        &stmt.name,
        requested_types.as_deref(),
        stmt.kind,
    )?;
    let function = registry
        .get(&old_name)
        .and_then(|overloads| overloads.get(position))
        .ok_or_else(|| {
            SQLError::Internal(format!(
                "resolved ALTER routine target `{old_name}` disappeared before rename"
            ))
        })?;
    let current_user = context.mutation.names.current_role();
    let roles = context.mutation.roles.role_definitions();
    let memberships = context.mutation.roles.role_memberships();
    // `AlterObjectRename_internal` names the routine as a function, by its catalog name.
    require_routine_ownership(
        "function",
        &uqa_sql::routines::routine_local_name(&function.def.name)?,
        role_inherits(
            &roles,
            &memberships,
            &current_user,
            &uqa_sql::routines::security::bound_routine_owner(&function.def)?,
        ),
    )?;
    drop(memberships);
    drop(roles);
    let old_identity = analysis::routine_rename_identity(&old_name)?;
    context
        .schemas
        .require_schema_create(&old_identity.schema, &current_user)?;
    analysis::finish_routine_rename_target(
        stmt,
        old_name,
        position,
        &function.def,
        &old_identity,
        registry,
    )
}

fn rewrite_routine_owned_dependency_identity(
    context: &RoutineRenameContext<'_>,
    target: &FunctionBinding,
    new_name: &str,
) -> Result<BTreeMap<String, Vec<Arc<SQLUserFunction>>>, SQLError> {
    let registry = context.mutation.registry.routine_snapshot();
    let mut rewritten = BTreeMap::new();
    for (name, overloads) in registry {
        let mut next_overloads = Vec::with_capacity(overloads.len());
        for function in overloads {
            let mut def = function.def.clone();
            let changed =
                analysis::rewrite_routine_owned_dependency_identity(&mut def, target, new_name)?;
            if changed {
                let body = persisted_routine_body(&context.compilation, &def)?;
                next_overloads.push(super::catalog::revision::replacement(def, body)?);
            } else {
                next_overloads.push(function);
            }
        }
        rewritten.insert(name, next_overloads);
    }
    Ok(rewritten)
}