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::SetPersistence { persistence }] => {
70            alter.persistence = Some(*persistence);
71        }
72        [AlterTableAction::RenameTable { to }] => {
73            alter.lifecycle = crate::ast::SequenceLifecycle::RenameTo { name: to.clone() };
74        }
75        [AlterTableAction::SetSchema { schema }] => {
76            alter.lifecycle = crate::ast::SequenceLifecycle::SetSchema {
77                schema: schema.clone(),
78            };
79        }
80        [AlterTableAction::ChangeOwner { owner }] => {
81            alter.role_owner = Some(owner.clone());
82        }
83        _ => {
84            return Err(SQLError::Routine {
85                sqlstate: "42809".into(),
86                message: format!("ALTER TABLE: relation `{canonical}` is a sequence, not a table"),
87            });
88        }
89    }
90    Ok(alter)
91}
92
93/// Return a native view owner or name change, or None for a valid sequence of view event renames.
94pub fn alter_view_from_table_syntax(
95    canonical: &str,
96    local_name: &str,
97    kind: &str,
98    stmt: &AlterTableStmt,
99) -> Result<Option<crate::ast::AlterViewStmt>, SQLError> {
100    views::validate_actions(local_name, kind, &stmt.actions)?;
101    let action = match stmt.actions.as_slice() {
102        [AlterTableAction::RenameTable { to }] => {
103            Some(crate::ast::AlterViewAction::RenameTo(to.clone()))
104        }
105        [AlterTableAction::ChangeOwner { owner }] => {
106            Some(crate::ast::AlterViewAction::OwnerTo(owner.clone()))
107        }
108        [AlterTableAction::SetSchema { schema }] => {
109            Some(crate::ast::AlterViewAction::SetSchema(schema.clone()))
110        }
111        _ => None,
112    };
113    if let Some(action) = action {
114        return Ok(Some(crate::ast::AlterViewStmt {
115            name: canonical.to_string(),
116            kind: if kind == "view" {
117                crate::ast::AlterViewKind::View
118            } else {
119                crate::ast::AlterViewKind::MaterializedView
120            },
121            if_exists: stmt.if_exists,
122            action,
123        }));
124    }
125    if kind == "view"
126        && stmt.actions.iter().all(|action| {
127            matches!(
128                action,
129                AlterTableAction::RenameRule { .. } | AlterTableAction::RenameTrigger { .. }
130            )
131        })
132    {
133        return Ok(None);
134    }
135    Err(SQLError::Routine {
136        sqlstate: "42809".into(),
137        message: format!("ALTER TABLE: relation `{canonical}` is a {kind}, not a table"),
138    })
139}
140
141/// Return native foreign-table column, owner or name changes, or None for valid trigger actions.
142pub fn alter_foreign_table_from_table_syntax(
143    canonical: &str,
144    stmt: &AlterTableStmt,
145) -> Result<Option<crate::ast::AlterForeignTableStmt>, SQLError> {
146    if stmt.actions.iter().all(|action| {
147        matches!(
148            action,
149            AlterTableAction::RenameTrigger { .. } | AlterTableAction::SetTriggerEnableMode { .. }
150        )
151    }) {
152        return Ok(None);
153    }
154    let drops = stmt
155        .actions
156        .iter()
157        .map(|action| match action {
158            AlterTableAction::DropColumn {
159                name,
160                if_exists,
161                cascade,
162            } => Some(crate::ast::DropColumnAction {
163                name: name.clone(),
164                if_exists: *if_exists,
165                cascade: *cascade,
166            }),
167            _ => None,
168        })
169        .collect::<Option<Vec<_>>>();
170    if let Some(columns) = drops {
171        return Ok(Some(crate::ast::AlterForeignTableStmt {
172            name: canonical.to_string(),
173            if_exists: stmt.if_exists,
174            action: crate::ast::AlterForeignTableAction::DropColumns(columns),
175        }));
176    }
177    let action = match stmt.actions.as_slice() {
178        [AlterTableAction::ChangeOwner { owner }] => {
179            crate::ast::AlterForeignTableAction::OwnerTo(owner.clone())
180        }
181        [AlterTableAction::RenameTable { to }] => {
182            crate::ast::AlterForeignTableAction::RenameTo(to.clone())
183        }
184        [AlterTableAction::SetSchema { schema }] => {
185            crate::ast::AlterForeignTableAction::SetSchema(schema.clone())
186        }
187        _ => {
188            return Err(SQLError::Routine {
189                sqlstate: "42809".into(),
190                message: format!(
191                    "ALTER TABLE: relation `{canonical}` is a foreign table, not a table"
192                ),
193            });
194        }
195    };
196    Ok(Some(crate::ast::AlterForeignTableStmt {
197        name: canonical.to_string(),
198        if_exists: stmt.if_exists,
199        action,
200    }))
201}
202
203#[cfg(test)]
204mod tests;