Skip to main content

uqa_sql/schema/table_alteration/
syntax.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Validate ALTER TABLE transaction restrictions and bind native relation-kind actions.
8use crate::{
9    ast::{AlterTableAction, AlterTableStmt, TableLockMode},
10    SQLError,
11};
12
13mod views;
14
15pub fn table_alter_lock_mode(statement: &AlterTableStmt) -> TableLockMode {
16    let mut mode = TableLockMode::ShareUpdateExclusive;
17    for action in &statement.actions {
18        match action {
19            AlterTableAction::SetTriggerEnableMode { .. }
20            | AlterTableAction::AddForeignKeyConstraint { .. } => {
21                mode = TableLockMode::ShareRowExclusive;
22            }
23            AlterTableAction::ValidateConstraint { .. }
24            | AlterTableAction::AttachPartition { .. }
25            | AlterTableAction::DetachPartition {
26                concurrently: true, ..
27            }
28            | AlterTableAction::DetachPartition { finalize: true, .. } => {}
29            _ => return TableLockMode::AccessExclusive,
30        }
31    }
32    mode
33}
34
35pub fn validate_alter_table_transaction(
36    stmt: &AlterTableStmt,
37    in_transaction_block: bool,
38) -> Result<(), SQLError> {
39    if in_transaction_block
40        && stmt.actions.iter().any(|action| {
41            matches!(
42                action,
43                AlterTableAction::DetachPartition {
44                    concurrently: true,
45                    ..
46                }
47            )
48        })
49    {
50        return Err(SQLError::Routine {
51            sqlstate: "25001".into(),
52            message: "ALTER TABLE ... DETACH CONCURRENTLY cannot run inside a transaction block"
53                .into(),
54        });
55    }
56    Ok(())
57}
58
59pub fn alter_sequence_from_table_syntax(
60    canonical: &str,
61    stmt: &AlterTableStmt,
62) -> Result<crate::ast::AlterSequence, SQLError> {
63    let mut alter = crate::ast::AlterSequence {
64        name: canonical.to_string(),
65        if_exists: stmt.if_exists,
66        ..crate::ast::AlterSequence::default()
67    };
68    match stmt.actions.as_slice() {
69        [AlterTableAction::RenameColumn { .. }] => {
70            let relation = uqa_core::RelationIdentity::from_legacy_name(canonical)
71                .map_err(SQLError::Internal)?;
72            return Err(SQLError::Diagnostic {
73                sqlstate: "42809".into(),
74                message: format!("cannot rename columns of relation \"{}\"", relation.name),
75                detail: Some("This operation is not supported for sequences.".into()),
76                hint: None,
77            });
78        }
79        [AlterTableAction::SetPersistence { persistence }] => {
80            alter.persistence = Some(*persistence);
81        }
82        [AlterTableAction::RenameTable { to }] => {
83            alter.lifecycle = crate::ast::SequenceLifecycle::RenameTo { name: to.clone() };
84        }
85        [AlterTableAction::SetSchema { schema }] => {
86            alter.lifecycle = crate::ast::SequenceLifecycle::SetSchema {
87                schema: schema.clone(),
88            };
89        }
90        [AlterTableAction::ChangeOwner { owner }] => {
91            alter.role_owner = Some(owner.clone());
92        }
93        _ => {
94            return Err(SQLError::Routine {
95                sqlstate: "42809".into(),
96                message: format!("ALTER TABLE: relation `{canonical}` is a sequence, not a table"),
97            });
98        }
99    }
100    Ok(alter)
101}
102
103/// Return a native view owner or name change, or None for a valid sequence of view event renames.
104pub fn alter_view_from_table_syntax(
105    canonical: &str,
106    local_name: &str,
107    kind: &str,
108    stmt: &AlterTableStmt,
109) -> Result<Option<crate::ast::AlterViewStmt>, SQLError> {
110    views::validate_actions(local_name, kind, &stmt.actions)?;
111    let action = match stmt.actions.as_slice() {
112        [AlterTableAction::RenameColumn { from, to }] => {
113            Some(crate::ast::AlterViewAction::RenameColumn {
114                from: from.clone(),
115                to: to.clone(),
116            })
117        }
118        [AlterTableAction::RenameTable { to }] => {
119            Some(crate::ast::AlterViewAction::RenameTo(to.clone()))
120        }
121        [AlterTableAction::ChangeOwner { owner }] => {
122            Some(crate::ast::AlterViewAction::OwnerTo(owner.clone()))
123        }
124        [AlterTableAction::SetSchema { schema }] => {
125            Some(crate::ast::AlterViewAction::SetSchema(schema.clone()))
126        }
127        _ => None,
128    };
129    if let Some(action) = action {
130        return Ok(Some(crate::ast::AlterViewStmt {
131            name: canonical.to_string(),
132            kind: if kind == "view" {
133                crate::ast::AlterViewKind::View
134            } else {
135                crate::ast::AlterViewKind::MaterializedView
136            },
137            if_exists: stmt.if_exists,
138            action,
139        }));
140    }
141    if kind == "view"
142        && stmt.actions.iter().all(|action| {
143            matches!(
144                action,
145                AlterTableAction::RenameRule { .. } | AlterTableAction::RenameTrigger { .. }
146            )
147        })
148    {
149        return Ok(None);
150    }
151    Err(SQLError::Routine {
152        sqlstate: "42809".into(),
153        message: format!("ALTER TABLE: relation `{canonical}` is a {kind}, not a table"),
154    })
155}
156
157/// Return native foreign-table column, owner or name changes, or None for valid trigger actions.
158pub fn alter_foreign_table_from_table_syntax(
159    canonical: &str,
160    stmt: &AlterTableStmt,
161) -> Result<Option<crate::ast::AlterForeignTableStmt>, SQLError> {
162    if stmt.actions.iter().all(|action| {
163        matches!(
164            action,
165            AlterTableAction::RenameTrigger { .. } | AlterTableAction::SetTriggerEnableMode { .. }
166        )
167    }) {
168        return Ok(None);
169    }
170    let drops = stmt
171        .actions
172        .iter()
173        .map(|action| match action {
174            AlterTableAction::DropColumn {
175                name,
176                if_exists,
177                cascade,
178            } => Some(crate::ast::DropColumnAction {
179                name: name.clone(),
180                if_exists: *if_exists,
181                cascade: *cascade,
182            }),
183            _ => None,
184        })
185        .collect::<Option<Vec<_>>>();
186    if let Some(columns) = drops {
187        return Ok(Some(crate::ast::AlterForeignTableStmt {
188            name: canonical.to_string(),
189            if_exists: stmt.if_exists,
190            action: crate::ast::AlterForeignTableAction::DropColumns(columns),
191        }));
192    }
193    let action = match stmt.actions.as_slice() {
194        [AlterTableAction::ChangeOwner { owner }] => {
195            crate::ast::AlterForeignTableAction::OwnerTo(owner.clone())
196        }
197        [AlterTableAction::RenameTable { to }] => {
198            crate::ast::AlterForeignTableAction::RenameTo(to.clone())
199        }
200        [AlterTableAction::SetSchema { schema }] => {
201            crate::ast::AlterForeignTableAction::SetSchema(schema.clone())
202        }
203        _ => {
204            return Err(SQLError::Routine {
205                sqlstate: "42809".into(),
206                message: format!(
207                    "ALTER TABLE: relation `{canonical}` is a foreign table, not a table"
208                ),
209            });
210        }
211    };
212    Ok(Some(crate::ast::AlterForeignTableStmt {
213        name: canonical.to_string(),
214        if_exists: stmt.if_exists,
215        action,
216    }))
217}
218
219#[cfg(test)]
220mod tests;