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,
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 ViewAlterTarget {
24    pub canonical: String,
25    pub relation: RelationIdentity,
26    pub kind: &'static str,
27}
28
29pub fn view_alter_target(
30    resolution: RelationResolution,
31    statement: &AlterViewStmt,
32    notice: &mut dyn FnMut(&str),
33) -> Result<Option<ViewAlterTarget>, SQLError> {
34    let kind = match statement.kind {
35        AlterViewKind::View => "view",
36        AlterViewKind::MaterializedView => "materialized view",
37    };
38    let resolution = if matches!(statement.action, AlterViewAction::RenameTo(_)) {
39        resolve_relation_rename_source(resolution, &statement.name, statement.if_exists, notice)?
40    } else {
41        resolution.into_found()
42    };
43    let Some((canonical, actual_kind)) = resolution else {
44        if statement.if_exists {
45            return Ok(None);
46        }
47        return Err(SQLError::Routine {
48            sqlstate: "42P01".into(),
49            message: format!("relation \"{}\" does not exist", statement.name),
50        });
51    };
52    if actual_kind != kind {
53        return Err(SQLError::Routine {
54            sqlstate: "42809".into(),
55            message: format!("\"{}\" is not a {kind}", statement.name),
56        });
57    }
58    let relation = RelationIdentity::from_legacy_name(&canonical).map_err(|error| {
59        SQLError::Internal(format!("invalid ALTER VIEW target `{canonical}`: {error}"))
60    })?;
61    Ok(Some(ViewAlterTarget {
62        canonical,
63        relation,
64        kind,
65    }))
66}
67
68pub fn foreign_table_alter_target(
69    resolution: RelationResolution,
70    statement: &AlterForeignTableStmt,
71    notice: &mut dyn FnMut(&str),
72) -> Result<Option<String>, SQLError> {
73    let resolution = if matches!(statement.action, AlterForeignTableAction::RenameTo(_)) {
74        let Some((canonical, kind)) = resolve_relation_rename_source(
75            resolution,
76            &statement.name,
77            statement.if_exists,
78            notice,
79        )?
80        else {
81            return Ok(None);
82        };
83        RelationResolution::Found(canonical, kind)
84    } else {
85        resolution
86    };
87    match resolution {
88        RelationResolution::Found(canonical, "foreign table") => Ok(Some(canonical)),
89        RelationResolution::Found(_, _) => Err(SQLError::Routine {
90            sqlstate: "42809".into(),
91            message: format!("\"{}\" is not a foreign table", statement.name),
92        }),
93        RelationResolution::MissingSchema(schema) if statement.if_exists => {
94            notice(&format!("schema \"{schema}\" does not exist, skipping"));
95            Ok(None)
96        }
97        RelationResolution::MissingRelation if statement.if_exists => {
98            notice(&format!(
99                "foreign table \"{}\" does not exist, skipping",
100                statement.name
101            ));
102            Ok(None)
103        }
104        RelationResolution::MissingSchema(schema) => Err(SQLError::Routine {
105            sqlstate: "3F000".into(),
106            message: format!("schema \"{schema}\" does not exist"),
107        }),
108        RelationResolution::MissingRelation => Err(SQLError::Routine {
109            sqlstate: "42P01".into(),
110            message: format!("foreign table \"{}\" does not exist", statement.name),
111        }),
112    }
113}
114
115pub fn relation_rename_target(
116    catalog: &dyn RelationAlterNames,
117    source: &RelationIdentity,
118    new_name: &str,
119    context: &str,
120) -> Result<RelationIdentity, SQLError> {
121    let (schema, local_name) = RelationIdentity::parse_reference(new_name)
122        .map_err(|error| SQLError::Internal(format!("invalid {context} target: {error}")))?;
123    if schema.is_some() {
124        return Err(SQLError::Internal(format!(
125            "{context} produced a qualified target"
126        )));
127    }
128    let target = RelationIdentity::new(&source.schema, local_name);
129    if target == *source
130        || catalog
131            .relation_kind_at(&target.qualified_name())
132            .map_err(|error| {
133                SQLError::Internal(format!(
134                    "check {context} target `{}`: {error}",
135                    target.qualified_name()
136                ))
137            })?
138            .is_some()
139    {
140        return Err(SQLError::Routine {
141            sqlstate: "42P07".into(),
142            message: format!("relation \"{}\" already exists", target.name),
143        });
144    }
145    Ok(target)
146}
147
148pub fn set_view_options(options: &mut Vec<(String, String)>, changes: &[(String, String)]) {
149    for (name, value) in changes {
150        options.retain(|(current, _)| current != name);
151        options.push((name.clone(), value.clone()));
152    }
153}
154
155pub fn reset_view_options(options: &mut Vec<(String, String)>, names: &[String]) {
156    options.retain(|(current, _)| !names.contains(current));
157}