uqa-sql 0.4.0

PostgreSQL-compatible SQL compiler built on libpg_query
Documentation
//
// Unified Query Algebra
//
// Copyright (c) 2023-2026 Cognica, Inc.
//

//! Native view and foreign-table alteration targets and relation rename declarations.
use crate::{
    ast::{
        AlterForeignTableAction, AlterForeignTableStmt, AlterViewAction, AlterViewKind,
        AlterViewStmt, TableLockMode,
    },
    catalog::resolution::{resolve_relation_rename_source, RelationResolution},
    SQLError,
};
use uqa_core::RelationIdentity;

pub trait RelationAlterNames {
    fn resolve_relation_kind(&self, name: &str) -> Result<RelationResolution, SQLError>;
    fn relation_kind_at(&self, name: &str) -> Result<Option<&'static str>, String>;
}

pub struct RelationAlterTarget {
    pub canonical: String,
    pub relation: RelationIdentity,
    pub kind: &'static str,
}

impl RelationAlterTarget {
    pub fn resolve(
        resolution: RelationResolution,
        name: &str,
        if_exists: bool,
        notice: &mut dyn FnMut(&str),
    ) -> Result<Option<Self>, SQLError> {
        resolve_relation_rename_source(resolution, name, if_exists, notice)?
            .map(|(canonical, kind)| Self::from_name(canonical, kind))
            .transpose()
    }

    pub(crate) fn from_name(canonical: String, kind: &'static str) -> Result<Self, SQLError> {
        let relation = RelationIdentity::from_legacy_name(&canonical).map_err(|error| {
            SQLError::Internal(format!(
                "invalid relation ALTER target `{canonical}`: {error}"
            ))
        })?;
        Ok(Self {
            canonical,
            relation,
            kind,
        })
    }

    /// Execution checks actual ownership and namespace authority before applying the requested kind.
    pub fn require_kind(&self, expected: &str) -> Result<(), SQLError> {
        if self.kind == expected {
            Ok(())
        } else {
            Err(SQLError::Routine {
                sqlstate: "42809".into(),
                message: format!("\"{}\" is not a {expected}", self.relation.name),
            })
        }
    }
}

pub fn view_alter_lock_mode(statement: &AlterViewStmt) -> TableLockMode {
    match (&statement.kind, &statement.action) {
        (AlterViewKind::MaterializedView, AlterViewAction::Set(_) | AlterViewAction::Reset(_)) => {
            TableLockMode::ShareUpdateExclusive
        }
        _ => TableLockMode::AccessExclusive,
    }
}

#[cfg(test)]
mod tests;

pub fn view_alter_target(
    resolution: RelationResolution,
    statement: &AlterViewStmt,
    notice: &mut dyn FnMut(&str),
) -> Result<Option<RelationAlterTarget>, SQLError> {
    let resolution = if matches!(statement.action, AlterViewAction::RenameTo(_)) {
        resolve_relation_rename_source(resolution, &statement.name, statement.if_exists, notice)?
    } else {
        resolution.into_found()
    };
    let Some((canonical, actual_kind)) = resolution else {
        if statement.if_exists {
            return Ok(None);
        }
        return Err(SQLError::Routine {
            sqlstate: "42P01".into(),
            message: format!("relation \"{}\" does not exist", statement.name),
        });
    };
    RelationAlterTarget::from_name(canonical, actual_kind).map(Some)
}

pub fn foreign_table_alter_target(
    resolution: RelationResolution,
    statement: &AlterForeignTableStmt,
    notice: &mut dyn FnMut(&str),
) -> Result<Option<RelationAlterTarget>, SQLError> {
    let resolution = if matches!(statement.action, AlterForeignTableAction::RenameTo(_)) {
        let Some((canonical, kind)) = resolve_relation_rename_source(
            resolution,
            &statement.name,
            statement.if_exists,
            notice,
        )?
        else {
            return Ok(None);
        };
        RelationResolution::Found(canonical, kind)
    } else {
        resolution
    };
    match resolution {
        RelationResolution::Found(canonical, kind) => {
            RelationAlterTarget::from_name(canonical, kind).map(Some)
        }
        RelationResolution::MissingSchema(schema) if statement.if_exists => {
            notice(&format!("schema \"{schema}\" does not exist, skipping"));
            Ok(None)
        }
        RelationResolution::MissingRelation if statement.if_exists => {
            notice(&format!(
                "foreign table \"{}\" does not exist, skipping",
                statement.name
            ));
            Ok(None)
        }
        RelationResolution::MissingSchema(schema) => Err(SQLError::Routine {
            sqlstate: "3F000".into(),
            message: format!("schema \"{schema}\" does not exist"),
        }),
        RelationResolution::MissingRelation => Err(SQLError::Routine {
            sqlstate: "42P01".into(),
            message: format!("foreign table \"{}\" does not exist", statement.name),
        }),
    }
}

pub fn relation_rename_target(
    catalog: &dyn RelationAlterNames,
    source: &RelationIdentity,
    new_name: &str,
    context: &str,
) -> Result<RelationIdentity, SQLError> {
    let (schema, local_name) = RelationIdentity::parse_reference(new_name)
        .map_err(|error| SQLError::Internal(format!("invalid {context} target: {error}")))?;
    if schema.is_some() {
        return Err(SQLError::Internal(format!(
            "{context} produced a qualified target"
        )));
    }
    let target = RelationIdentity::new(&source.schema, local_name);
    if target == *source
        || catalog
            .relation_kind_at(&target.qualified_name())
            .map_err(|error| {
                SQLError::Internal(format!(
                    "check {context} target `{}`: {error}",
                    target.qualified_name()
                ))
            })?
            .is_some()
    {
        return Err(SQLError::Routine {
            sqlstate: "42P07".into(),
            message: format!("relation \"{}\" already exists", target.name),
        });
    }
    Ok(target)
}

pub fn set_view_options(options: &mut Vec<(String, String)>, changes: &[(String, String)]) {
    for (name, value) in changes {
        options.retain(|(current, _)| current != name);
        options.push((name.clone(), value.clone()));
    }
}

pub fn reset_view_options(options: &mut Vec<(String, String)>, names: &[String]) {
    options.retain(|(current, _)| !names.contains(current));
}