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