Skip to main content

qail_core/migrate/
alter.rs

1//! ALTER TABLE Operations (AST-native)
2//!
3//! All ALTER TABLE operations as typed enums - no raw SQL!
4//!
5//! # Example
6//! ```ignore
7//! use qail_core::migrate::alter::{AlterTable, AlterOp};
8//!
9//! let alter = AlterTable::new("users")
10//!     .add_column(Column::new("bio", ColumnType::Text))
11//!     .drop_column("legacy_field")
12//!     .rename_column("username", "handle");
13//! ```
14
15use super::schema::{CheckExpr, Column};
16use super::types::ColumnType;
17
18/// ALTER TABLE operation
19#[derive(Debug, Clone)]
20pub enum AlterOp {
21    AddColumn(Column),
22    DropColumn { name: String, cascade: bool },
23    RenameColumn { from: String, to: String },
24    AlterType {
25        column: String,
26        new_type: ColumnType,
27        using: Option<String>,
28    },
29    SetNotNull(String),
30    DropNotNull(String),
31    SetDefault { column: String, expr: String },
32    DropDefault(String),
33    AddConstraint {
34        name: String,
35        constraint: TableConstraint,
36    },
37    DropConstraint { name: String, cascade: bool },
38    RenameTable(String),
39    SetSchema(String),
40    SetRowLevelSecurity(bool),
41    ForceRowLevelSecurity(bool),
42}
43
44/// Table-level constraints
45#[derive(Debug, Clone)]
46pub enum TableConstraint {
47    PrimaryKey(Vec<String>),
48    Unique(Vec<String>),
49    Check(CheckExpr),
50    /// FOREIGN KEY (cols) REFERENCES table(ref_cols)
51    ForeignKey {
52        columns: Vec<String>,
53        ref_table: String,
54        ref_columns: Vec<String>,
55    },
56    /// EXCLUDE USING method (...)
57    Exclude {
58        method: String,
59        elements: Vec<String>,
60    },
61}
62
63/// Fluent builder for ALTER TABLE statements
64#[derive(Debug, Clone)]
65pub struct AlterTable {
66    pub table: String,
67    pub ops: Vec<AlterOp>,
68    pub only: bool,
69    pub if_exists: bool,
70}
71
72impl AlterTable {
73    /// Create a new ALTER TABLE builder
74    pub fn new(table: impl Into<String>) -> Self {
75        Self {
76            table: table.into(),
77            ops: Vec::new(),
78            only: false,
79            if_exists: false,
80        }
81    }
82
83    /// ALTER TABLE ONLY (no child tables)
84    pub fn only(mut self) -> Self {
85        self.only = true;
86        self
87    }
88
89    /// ALTER TABLE IF EXISTS
90    pub fn if_exists(mut self) -> Self {
91        self.if_exists = true;
92        self
93    }
94
95    /// ADD COLUMN
96    pub fn add_column(mut self, col: Column) -> Self {
97        self.ops.push(AlterOp::AddColumn(col));
98        self
99    }
100
101    /// DROP COLUMN
102    pub fn drop_column(mut self, name: impl Into<String>) -> Self {
103        self.ops.push(AlterOp::DropColumn {
104            name: name.into(),
105            cascade: false,
106        });
107        self
108    }
109
110    /// DROP COLUMN CASCADE
111    pub fn drop_column_cascade(mut self, name: impl Into<String>) -> Self {
112        self.ops.push(AlterOp::DropColumn {
113            name: name.into(),
114            cascade: true,
115        });
116        self
117    }
118
119    /// RENAME COLUMN old TO new
120    pub fn rename_column(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
121        self.ops.push(AlterOp::RenameColumn {
122            from: from.into(),
123            to: to.into(),
124        });
125        self
126    }
127
128    pub fn set_type(mut self, column: impl Into<String>, new_type: ColumnType) -> Self {
129        self.ops.push(AlterOp::AlterType {
130            column: column.into(),
131            new_type,
132            using: None,
133        });
134        self
135    }
136
137    pub fn set_type_using(
138        mut self,
139        column: impl Into<String>,
140        new_type: ColumnType,
141        using: impl Into<String>,
142    ) -> Self {
143        self.ops.push(AlterOp::AlterType {
144            column: column.into(),
145            new_type,
146            using: Some(using.into()),
147        });
148        self
149    }
150
151    /// ALTER COLUMN SET NOT NULL
152    pub fn set_not_null(mut self, column: impl Into<String>) -> Self {
153        self.ops.push(AlterOp::SetNotNull(column.into()));
154        self
155    }
156
157    /// ALTER COLUMN DROP NOT NULL
158    pub fn drop_not_null(mut self, column: impl Into<String>) -> Self {
159        self.ops.push(AlterOp::DropNotNull(column.into()));
160        self
161    }
162
163    pub fn set_default(mut self, column: impl Into<String>, expr: impl Into<String>) -> Self {
164        self.ops.push(AlterOp::SetDefault {
165            column: column.into(),
166            expr: expr.into(),
167        });
168        self
169    }
170
171    pub fn drop_default(mut self, column: impl Into<String>) -> Self {
172        self.ops.push(AlterOp::DropDefault(column.into()));
173        self
174    }
175
176    pub fn add_constraint(
177        mut self,
178        name: impl Into<String>,
179        constraint: TableConstraint,
180    ) -> Self {
181        self.ops.push(AlterOp::AddConstraint {
182            name: name.into(),
183            constraint,
184        });
185        self
186    }
187
188    pub fn drop_constraint(mut self, name: impl Into<String>) -> Self {
189        self.ops.push(AlterOp::DropConstraint {
190            name: name.into(),
191            cascade: false,
192        });
193        self
194    }
195
196    pub fn drop_constraint_cascade(mut self, name: impl Into<String>) -> Self {
197        self.ops.push(AlterOp::DropConstraint {
198            name: name.into(),
199            cascade: true,
200        });
201        self
202    }
203
204    pub fn rename_to(mut self, name: impl Into<String>) -> Self {
205        self.ops.push(AlterOp::RenameTable(name.into()));
206        self
207    }
208
209    pub fn set_schema(mut self, schema: impl Into<String>) -> Self {
210        self.ops.push(AlterOp::SetSchema(schema.into()));
211        self
212    }
213
214    pub fn enable_rls(mut self) -> Self {
215        self.ops.push(AlterOp::SetRowLevelSecurity(true));
216        self
217    }
218
219    pub fn disable_rls(mut self) -> Self {
220        self.ops.push(AlterOp::SetRowLevelSecurity(false));
221        self
222    }
223
224    /// FORCE ROW LEVEL SECURITY — policies apply even to table owner.
225    pub fn force_rls(mut self) -> Self {
226        self.ops.push(AlterOp::ForceRowLevelSecurity(true));
227        self
228    }
229
230    /// NO FORCE ROW LEVEL SECURITY — owner bypasses policies (default).
231    pub fn no_force_rls(mut self) -> Self {
232        self.ops.push(AlterOp::ForceRowLevelSecurity(false));
233        self
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use crate::migrate::types::ColumnType;
241
242    #[test]
243    fn test_alter_table_builder() {
244        let alter = AlterTable::new("users")
245            .add_column(Column::new("bio", ColumnType::Text))
246            .drop_column("legacy")
247            .rename_column("username", "handle")
248            .set_not_null("email");
249
250        assert_eq!(alter.table, "users");
251        assert_eq!(alter.ops.len(), 4);
252    }
253
254    #[test]
255    fn test_alter_type_with_using() {
256        let alter = AlterTable::new("users")
257            .set_type_using("age", ColumnType::Int, "age::integer");
258
259        match &alter.ops[0] {
260            AlterOp::AlterType { column, using, .. } => {
261                assert_eq!(column, "age");
262                assert_eq!(using.as_ref().unwrap(), "age::integer");
263            }
264            _ => panic!("Expected AlterType"),
265        }
266    }
267
268    #[test]
269    fn test_add_constraint() {
270        let alter = AlterTable::new("users").add_constraint(
271            "pk_users",
272            TableConstraint::PrimaryKey(vec!["id".into()]),
273        );
274
275        assert_eq!(alter.ops.len(), 1);
276    }
277}