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!(
85        statement.action,
86        AlterViewAction::RenameTo(_) | AlterViewAction::SetSchema(_)
87    ) {
88        resolve_relation_rename_source(resolution, &statement.name, statement.if_exists, notice)?
89    } else {
90        resolution.into_found()
91    };
92    let Some((canonical, actual_kind)) = resolution else {
93        if statement.if_exists {
94            return Ok(None);
95        }
96        return Err(SQLError::Routine {
97            sqlstate: "42P01".into(),
98            message: format!("relation \"{}\" does not exist", statement.name),
99        });
100    };
101    RelationAlterTarget::from_name(canonical, actual_kind).map(Some)
102}
103
104pub fn foreign_table_alter_target(
105    resolution: RelationResolution,
106    statement: &AlterForeignTableStmt,
107    notice: &mut dyn FnMut(&str),
108) -> Result<Option<RelationAlterTarget>, SQLError> {
109    let resolution = if matches!(
110        statement.action,
111        AlterForeignTableAction::RenameTo(_)
112            | AlterForeignTableAction::SetSchema(_)
113            | AlterForeignTableAction::DropColumns(_)
114    ) {
115        let Some((canonical, kind)) = resolve_relation_rename_source(
116            resolution,
117            &statement.name,
118            statement.if_exists,
119            notice,
120        )?
121        else {
122            return Ok(None);
123        };
124        RelationResolution::Found(canonical, kind)
125    } else {
126        resolution
127    };
128    match resolution {
129        RelationResolution::Found(canonical, kind) => {
130            RelationAlterTarget::from_name(canonical, kind).map(Some)
131        }
132        RelationResolution::MissingSchema(schema) if statement.if_exists => {
133            notice(&format!("schema \"{schema}\" does not exist, skipping"));
134            Ok(None)
135        }
136        RelationResolution::MissingRelation if statement.if_exists => {
137            notice(&format!(
138                "foreign table \"{}\" does not exist, skipping",
139                statement.name
140            ));
141            Ok(None)
142        }
143        RelationResolution::MissingSchema(schema) => Err(SQLError::Routine {
144            sqlstate: "3F000".into(),
145            message: format!("schema \"{schema}\" does not exist"),
146        }),
147        RelationResolution::MissingRelation => Err(SQLError::Routine {
148            sqlstate: "42P01".into(),
149            message: format!("foreign table \"{}\" does not exist", statement.name),
150        }),
151    }
152}
153
154pub fn relation_rename_target(
155    catalog: &dyn RelationAlterNames,
156    source: &RelationIdentity,
157    new_name: &str,
158    context: &str,
159) -> Result<RelationIdentity, SQLError> {
160    let (schema, local_name) = RelationIdentity::parse_reference(new_name)
161        .map_err(|error| SQLError::Internal(format!("invalid {context} target: {error}")))?;
162    if schema.is_some() {
163        return Err(SQLError::Internal(format!(
164            "{context} produced a qualified target"
165        )));
166    }
167    let target = RelationIdentity::new(&source.schema, local_name);
168    if target == *source
169        || catalog
170            .relation_kind_at(&target.qualified_name())
171            .map_err(|error| {
172                SQLError::Internal(format!(
173                    "check {context} target `{}`: {error}",
174                    target.qualified_name()
175                ))
176            })?
177            .is_some()
178    {
179        return Err(SQLError::Routine {
180            sqlstate: "42P07".into(),
181            message: format!("relation \"{}\" already exists", target.name),
182        });
183    }
184    Ok(target)
185}
186
187pub fn set_view_options(options: &mut Vec<(String, String)>, changes: &[(String, String)]) {
188    for (name, value) in changes {
189        options.retain(|(current, _)| current != name);
190        options.push((name.clone(), value.clone()));
191    }
192}
193
194pub fn reset_view_options(options: &mut Vec<(String, String)>, names: &[String]) {
195    options.retain(|(current, _)| !names.contains(current));
196}
197
198pub mod relocation;