Skip to main content

uqa_sql/schema/
relation_alteration.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Native view and foreign-table alteration targets and relation rename declarations.
8use crate::{
9    ast::{
10        AlterForeignTableAction, AlterForeignTableStmt, AlterViewAction, AlterViewKind,
11        AlterViewStmt, TableLockMode,
12    },
13    catalog::resolution::{resolve_relation_rename_source, RelationResolution},
14    SQLError,
15};
16use uqa_core::RelationIdentity;
17
18pub trait RelationAlterNames {
19    fn resolve_relation_kind(&self, name: &str) -> Result<RelationResolution, SQLError>;
20    fn relation_kind_at(&self, name: &str) -> Result<Option<&'static str>, String>;
21}
22
23pub struct RelationAlterTarget {
24    pub canonical: String,
25    pub relation: RelationIdentity,
26    pub kind: &'static str,
27}
28
29impl RelationAlterTarget {
30    pub fn resolve(
31        resolution: RelationResolution,
32        name: &str,
33        if_exists: bool,
34        notice: &mut dyn FnMut(&str),
35    ) -> Result<Option<Self>, SQLError> {
36        resolve_relation_rename_source(resolution, name, if_exists, notice)?
37            .map(|(canonical, kind)| Self::from_name(canonical, kind))
38            .transpose()
39    }
40
41    pub(crate) fn from_name(canonical: String, kind: &'static str) -> Result<Self, SQLError> {
42        let relation = RelationIdentity::from_legacy_name(&canonical).map_err(|error| {
43            SQLError::Internal(format!(
44                "invalid relation ALTER target `{canonical}`: {error}"
45            ))
46        })?;
47        Ok(Self {
48            canonical,
49            relation,
50            kind,
51        })
52    }
53
54    /// Execution checks actual ownership and namespace authority before applying the requested kind.
55    pub fn require_kind(&self, expected: &str) -> Result<(), SQLError> {
56        if self.kind == expected {
57            Ok(())
58        } else {
59            Err(SQLError::Routine {
60                sqlstate: "42809".into(),
61                message: format!("\"{}\" is not a {expected}", self.relation.name),
62            })
63        }
64    }
65}
66
67pub fn view_alter_lock_mode(statement: &AlterViewStmt) -> TableLockMode {
68    match (&statement.kind, &statement.action) {
69        (AlterViewKind::MaterializedView, AlterViewAction::Set(_) | AlterViewAction::Reset(_)) => {
70            TableLockMode::ShareUpdateExclusive
71        }
72        _ => TableLockMode::AccessExclusive,
73    }
74}
75
76#[cfg(test)]
77mod tests;
78
79pub fn view_alter_target(
80    resolution: RelationResolution,
81    statement: &AlterViewStmt,
82    notice: &mut dyn FnMut(&str),
83) -> Result<Option<RelationAlterTarget>, SQLError> {
84    let resolution = if matches!(statement.action, AlterViewAction::RenameTo(_)) {
85        resolve_relation_rename_source(resolution, &statement.name, statement.if_exists, notice)?
86    } else {
87        resolution.into_found()
88    };
89    let Some((canonical, actual_kind)) = resolution else {
90        if statement.if_exists {
91            return Ok(None);
92        }
93        return Err(SQLError::Routine {
94            sqlstate: "42P01".into(),
95            message: format!("relation \"{}\" does not exist", statement.name),
96        });
97    };
98    RelationAlterTarget::from_name(canonical, actual_kind).map(Some)
99}
100
101pub fn foreign_table_alter_target(
102    resolution: RelationResolution,
103    statement: &AlterForeignTableStmt,
104    notice: &mut dyn FnMut(&str),
105) -> Result<Option<RelationAlterTarget>, SQLError> {
106    let resolution = if matches!(statement.action, AlterForeignTableAction::RenameTo(_)) {
107        let Some((canonical, kind)) = resolve_relation_rename_source(
108            resolution,
109            &statement.name,
110            statement.if_exists,
111            notice,
112        )?
113        else {
114            return Ok(None);
115        };
116        RelationResolution::Found(canonical, kind)
117    } else {
118        resolution
119    };
120    match resolution {
121        RelationResolution::Found(canonical, kind) => {
122            RelationAlterTarget::from_name(canonical, kind).map(Some)
123        }
124        RelationResolution::MissingSchema(schema) if statement.if_exists => {
125            notice(&format!("schema \"{schema}\" does not exist, skipping"));
126            Ok(None)
127        }
128        RelationResolution::MissingRelation if statement.if_exists => {
129            notice(&format!(
130                "foreign table \"{}\" does not exist, skipping",
131                statement.name
132            ));
133            Ok(None)
134        }
135        RelationResolution::MissingSchema(schema) => Err(SQLError::Routine {
136            sqlstate: "3F000".into(),
137            message: format!("schema \"{schema}\" does not exist"),
138        }),
139        RelationResolution::MissingRelation => Err(SQLError::Routine {
140            sqlstate: "42P01".into(),
141            message: format!("foreign table \"{}\" does not exist", statement.name),
142        }),
143    }
144}
145
146pub fn relation_rename_target(
147    catalog: &dyn RelationAlterNames,
148    source: &RelationIdentity,
149    new_name: &str,
150    context: &str,
151) -> Result<RelationIdentity, SQLError> {
152    let (schema, local_name) = RelationIdentity::parse_reference(new_name)
153        .map_err(|error| SQLError::Internal(format!("invalid {context} target: {error}")))?;
154    if schema.is_some() {
155        return Err(SQLError::Internal(format!(
156            "{context} produced a qualified target"
157        )));
158    }
159    let target = RelationIdentity::new(&source.schema, local_name);
160    if target == *source
161        || catalog
162            .relation_kind_at(&target.qualified_name())
163            .map_err(|error| {
164                SQLError::Internal(format!(
165                    "check {context} target `{}`: {error}",
166                    target.qualified_name()
167                ))
168            })?
169            .is_some()
170    {
171        return Err(SQLError::Routine {
172            sqlstate: "42P07".into(),
173            message: format!("relation \"{}\" already exists", target.name),
174        });
175    }
176    Ok(target)
177}
178
179pub fn set_view_options(options: &mut Vec<(String, String)>, changes: &[(String, String)]) {
180    for (name, value) in changes {
181        options.retain(|(current, _)| current != name);
182        options.push((name.clone(), value.clone()));
183    }
184}
185
186pub fn reset_view_options(options: &mut Vec<(String, String)>, names: &[String]) {
187    options.retain(|(current, _)| !names.contains(current));
188}