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